WebRTC在本地网络上可用但无法通过互联网使用(Python)
在我的应用程序中,我需要通过WebRTC让Flutter应用和Python脚本进行通话。当我在Flutter之间拨打电话时,无论是在本地网络还是互联网上都能正常工作。但是,当我从Flutter应用拨打电话到Python脚本时,它在本地网络上可以工作,但在互联网上却不行。
我尝试把STUN服务器换成TURN服务器,但结果还是一样。需要注意的是,Flutter应用即使使用STUN服务器也能通过互联网连接。所以STUN/TURN服务器不是问题。有没有人能告诉我我的Python脚本有什么问题?我会附上Flutter和Python的代码。
Flutter代码:
import 'dart:async';
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:wasa/locator.dart';
typedef void StreamStateCallback(MediaStream stream);
class Signaling {
Map<String, dynamic> configuration = {
'iceServers': [
{
'urls': [
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302'
]
}
// {
// "urls": "stun:stun.relay.metered.ca:80",
// },
// {
// "urls": "turn:global.relay.metered.ca:80",
// "username": "335637ac6798c949833d965a",
// "credential": "x3A8BQfH17c3suAR",
// },
// {
// "urls": "turn:global.relay.metered.ca:80?transport=tcp",
// "username": "335637ac6798c949833d965a",
// "credential": "x3A8BQfH17c3suAR",
// },
// {
// "urls": "turn:global.relay.metered.ca:443",
// "username": "335637ac6798c949833d965a",
// "credential": "x3A8BQfH17c3suAR",
// },
// {
// "urls": "turns:global.relay.metered.ca:443?transport=tcp",
// "username": "335637ac6798c949833d965a",
// "credential": "x3A8BQfH17c3suAR",
// },
]
};
RTCPeerConnection? peerConnection;
MediaStream? localStream;
MediaStream? remoteStream;
String? roomId;
String? currentRoomText;
StreamStateCallback? onAddRemoteStream;
StreamSubscription<DocumentSnapshot<Object?>>? roomStream;
StreamSubscription<QuerySnapshot<Map<String, dynamic>>>? callerStream;
StreamSubscription<QuerySnapshot<Map<String, dynamic>>>? calleeStream;
Future<String> createRoom(RTCVideoRenderer remoteRenderer) async {
FirebaseFirestore db = FirebaseFirestore.instance;
DocumentReference roomRef = db.collection('rooms').doc(storage.user!.uuid);
print('Create PeerConnection with configuration: $configuration');
peerConnection = await createPeerConnection(configuration);
registerPeerConnectionListeners();
localStream?.getTracks().forEach((track) {
peerConnection?.addTrack(track, localStream!);
});
// Code for collecting ICE candidates below
var callerCandidatesCollection = roomRef.collection('callerCandidates');
peerConnection?.onIceCandidate = (RTCIceCandidate candidate) {
print('Got candidate: ${candidate.toMap()}');
callerCandidatesCollection.add(candidate.toMap());
};
// Finish Code for collecting ICE candidate
// Add code for creating a room
RTCSessionDescription offer = await peerConnection!.createOffer();
await peerConnection!.setLocalDescription(offer);
print('Created offer: $offer');
Map<String, dynamic> roomWithOffer = {'offer': offer.toMap()};
await roomRef.set(roomWithOffer);
var roomId = roomRef.id;
print('New room created with SDK offer. Room ID: $roomId');
currentRoomText = 'Current room is $roomId - You are the caller!';
// Created a Room
peerConnection?.onTrack = (RTCTrackEvent event) {
print('Got remote track: ${event.streams[0]}');
event.streams[0].getTracks().forEach((track) {
print('Add a track to the remoteStream $track');
remoteStream?.addTrack(track);
});
};
// Listening for remote session description below
roomStream = roomRef.snapshots().listen((snapshot) async {
print('Got updated room: ${snapshot.data()}');
Map<String, dynamic> data = snapshot.data() as Map<String, dynamic>;
if (peerConnection?.getRemoteDescription() != null &&
data['answer'] != null) {
var answer = RTCSessionDescription(
data['answer']['sdp'],
data['answer']['type'],
);
print("Someone tried to connect");
await peerConnection?.setRemoteDescription(answer);
}
});
// Listening for remote session description above
// Listen for remote Ice candidates below
calleeStream =
roomRef.collection('calleeCandidates').snapshots().listen((snapshot) {
snapshot.docChanges.forEach((change) {
if (change.type == DocumentChangeType.added) {
Map<String, dynamic> data = change.doc.data() as Map<String, dynamic>;
print('Got new remote ICE candidate: ${jsonEncode(data)}');
peerConnection!.addCandidate(
RTCIceCandidate(
data['candidate'],
data['sdpMid'],
data['sdpMLineIndex'],
),
);
}
});
});
// Listen for remote ICE candidates above
return roomId;
}
Future<void> joinRoom(String roomId, RTCVideoRenderer remoteVideo) async {
FirebaseFirestore db = FirebaseFirestore.instance;
print(roomId);
DocumentReference roomRef = db.collection('rooms').doc('$roomId');
var roomSnapshot = await roomRef.get();
print('Got room ${roomSnapshot.exists}');
if (roomSnapshot.exists) {
print('Create PeerConnection with configuration: $configuration');
peerConnection = await createPeerConnection(configuration);
registerPeerConnectionListeners();
localStream?.getTracks().forEach((track) {
peerConnection?.addTrack(track, localStream!);
});
// Code for collecting ICE candidates below
var calleeCandidatesCollection = roomRef.collection('calleeCandidates');
peerConnection!.onIceCandidate = (RTCIceCandidate? candidate) {
if (candidate == null) {
print('onIceCandidate: complete!');
return;
}
print('onIceCandidate: ${candidate.toMap()}');
calleeCandidatesCollection.add(candidate.toMap());
};
// Code for collecting ICE candidate above
peerConnection?.onTrack = (RTCTrackEvent event) {
print('Got remote track: ${event.streams[0]}');
event.streams[0].getTracks().forEach((track) {
print('Add a track to the remoteStream: $track');
remoteStream?.addTrack(track);
});
};
// Code for creating SDP answer below
var data = roomSnapshot.data() as Map<String, dynamic>;
print('Got offer $data');
var offer = data['offer'];
await peerConnection?.setRemoteDescription(
RTCSessionDescription(offer['sdp'], offer['type']),
);
var answer = await peerConnection!.createAnswer();
print('Created Answer $answer');
await peerConnection!.setLocalDescription(answer);
Map<String, dynamic> roomWithAnswer = {
'answer': {'type': answer.type, 'sdp': answer.sdp}
};
await roomRef.update(roomWithAnswer);
// Finished creating SDP answer
// Listening for remote ICE candidates below
callerStream =
roomRef.collection('callerCandidates').snapshots().listen((snapshot) {
snapshot.docChanges.forEach((document) {
var data = document.doc.data() as Map<String, dynamic>;
print(data);
print('Got new remote ICE candidate: $data');
peerConnection!.addCandidate(
RTCIceCandidate(
data['candidate'],
data['sdpMid'],
data['sdpMLineIndex'],
),
);
});
});
}
}
Future<void> openUserMedia(
RTCVideoRenderer localVideo,
RTCVideoRenderer remoteVideo,
) async {
var stream = await navigator.mediaDevices
.getUserMedia({'video': true, 'audio': true});
localVideo.srcObject = stream;
localStream = stream;
remoteVideo.srcObject = await createLocalMediaStream('key');
}
Future<void> hangUp(RTCVideoRenderer localVideo, BuildContext context) async {
List<MediaStreamTrack> tracks = localVideo.srcObject!.getTracks();
tracks.forEach((track) {
track.stop();
});
if (remoteStream != null) {
remoteStream!.getTracks().forEach((track) => track.stop());
}
if (peerConnection != null) peerConnection!.close();
calleeStream?.cancel();
callerStream?.cancel();
roomStream?.cancel();
if (storage.user!.uuid != null) {
var db = FirebaseFirestore.instance;
var roomRef = db.collection('rooms').doc(storage.user!.uuid);
var calleeCandidates = await roomRef.collection('calleeCandidates').get();
calleeCandidates.docs.forEach((document) => document.reference.delete());
var callerCandidates = await roomRef.collection('callerCandidates').get();
callerCandidates.docs.forEach((document) => document.reference.delete());
await roomRef.delete();
}
localStream!.dispose();
remoteStream?.dispose();
Navigator.pop(context);
}
void registerPeerConnectionListeners() {
peerConnection?.onIceGatheringState = (RTCIceGatheringState state) {
print('ICE gathering state changed: $state');
};
peerConnection?.onConnectionState = (RTCPeerConnectionState state) {
print('Connection state change: $state');
};
peerConnection?.onSignalingState = (RTCSignalingState state) {
print('Signaling state change: $state');
};
peerConnection?.onIceGatheringState = (RTCIceGatheringState state) {
print('ICE connection state change: $state');
};
peerConnection?.onAddStream = (MediaStream stream) {
print("Add remote stream");
onAddRemoteStream?.call(stream);
remoteStream = stream;
};
}
}
Python代码:
import asyncio
import re
from itertools import islice
import numpy as np
import sounddevice as sd
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCConfiguration, \
RTCIceServer
from aiortc.contrib.media import MediaPlayer, MediaRecorder, MediaBlackhole
from google.cloud import firestore
from google.oauth2 import service_account
recorder = MediaRecorder('PathToOutputFile.mp3')
async def join_room(room_id):
credentials = service_account.Credentials.from_service_account_file(PathToKey.json")
db = firestore.Client(credentials=credentials)
room_ref = db.collection('rooms').document(room_id)
room_snapshot = room_ref.get()
if room_snapshot.exists:
servers = RTCConfiguration(
iceServers=[RTCIceServer(
urls=['turn:global.relay.metered.ca:80', 'turn:global.relay.metered.ca:80?transport=tcp', 'turn:global.relay.metered.ca:443', 'turns:global.relay.metered.ca:443?transport=tcp'],
credential='x3A8BQfH17c3suAR',
username='335637ac6798c949833d965a')])
# servers = RTCConfiguration(iceServers=[RTCIceServer(urls=['stun:stun1.l.google.com:19302','stun:stun2.l.google.com:19302'])])
peer_connection = RTCPeerConnection(configuration=servers)
@peer_connection.on("track")
def on_track(track):
print("Track %s received", track.kind)
if track.kind == "audio":
print('audio')
recorder.addTrack(track)
elif track.kind == "video":
print('video')
# recorder.addTrack(track)
@track.on("ended")
async def on_ended():
print("Track %s ended", track.kind)
@peer_connection.on("connectionstatechange")
async def on_connectionstatechange():
print("Connection state is %s" % peer_connection.connectionState)
if peer_connection.connectionState == "connected":
await recorder.start()
elif peer_connection.connectionState == "failed":
await recorder.stop()
await peer_connection.close()
elif peer_connection.connectionState == "closed":
await recorder.stop()
await peer_connection.close()
@peer_connection.on("iceConnectionState")
async def on_iceConnectionState():
print("iceConnectionState is %s" % peer_connection.iceConnectionState)
@peer_connection.on("iceGatheringState")
async def on_iceGatheringState():
print("iceGatheringState is %s" % peer_connection.iceGatheringState)
# open media source
# Set the desired video size and fps in the 'options' dictionary
player = MediaPlayer('/Users/muhammadazeem/Downloads/sample.mp4')
if player.audio:
audio_sender = peer_connection.addTrack(player.audio)
if player.video:
video_sender = peer_connection.addTrack(player.video)
# Fetch existing ICE candidates from Firestore
candidates_collection = room_ref.collection('callerCandidates')
existing_candidates = candidates_collection.stream()
for document in existing_candidates:
data = document.to_dict()
print(f'Existing remote ICE candidate: {data}')
# Define a regular expression pattern to extract information
pattern = re.compile(r'candidate:(\S+) (\d+) (\S+) (\d+) (\S+) (\d+) typ (\S+)')
candidate_string = data['candidate']
# Use the pattern to search for matches in the candidate string
match = pattern.search(candidate_string)
if match:
# Extract information from the match groups
foundation, component_id, transport, priority, connection_address, port, candidate_type = match.groups()
candidate = RTCIceCandidate(
ip=connection_address,
protocol=transport,
port=port,
foundation=foundation,
component=component_id,
priority=priority,
type=candidate_type,
sdpMid=data['sdpMid'],
sdpMLineIndex=data['sdpMLineIndex'],
)
await peer_connection.addIceCandidate(candidate)
# Assume you have the offer SDP from Firestore
offer_sdp = room_snapshot.get('offer')['sdp']
await peer_connection.setRemoteDescription(RTCSessionDescription(sdp=offer_sdp, type='offer'))
# Create an answer SDP
answer = await peer_connection.createAnswer()
print('Created Answer')
await peer_connection.setLocalDescription(answer)
print('answer set successfully')
# Update the "answer" field in Firestore
room_with_answer = {'answer': {'type': answer.type, 'sdp': answer.sdp}}
room_ref.update(room_with_answer)
print('answer written successfully')
# Extract candidates, sdpMid, and sdpMLineIndex
candidates = []
desc = peer_connection.localDescription
for candidate_line in peer_connection.localDescription.sdp.splitlines():
if candidate_line.startswith('a=candidate:'):
candidate = candidate_line[len('a='):]
candidates.append(candidate)
print('candidates created successfully')
# Store candidates in Firestore
for candidate in candidates:
candidate_data = {
'candidate': candidate,
'sdpMid': '0',
'sdpMLineIndex': 0,
}
room_ref.collection('calleeCandidates').add(candidate_data)
print('candidates written successfully')
try:
# Placeholder for the main loop, you might want to handle user input or other tasks
while True:
await asyncio.sleep(1)
finally:
print('close')
# await candidate_listener.aclose()
if __name__ == '__main__':
room_id_to_join = "Room_Id"
asyncio.run(join_room(room_id_to_join))
本来应该能通过互联网从Flutter应用连接到Python脚本,但现在只能在本地网络上连接。另一方面,Flutter之间的通话在互联网和本地网络上都能连接,所以Flutter的代码是没问题的,问题出在Python代码上。
1 个回答
0
经过一些测试,我发现了一些情况。当我的Python脚本通过4G网络运行,而Flutter应用通过WiFi网络运行时,一切都很正常。但是当我的Python脚本通过WiFi运行,而手机应用通过4G运行时,Python脚本就无法访问了。
所以这并不是网络提供商的限制,而是似乎和笔记本电脑连接WiFi时的情况有关。