In my application i need to make a call between flutter app and python script using WebRTC. When i make a call from flutter to flutter everything works on local network and Internet. But when i make call from flutter app to python script it works on local network but not through internet. I have tried replacing the STUN server with TURN server but still the same results. Please note that flutter app is able to connect through internet even with stun server. So STUN/TURN server is not the issue. Can anyone tell me the issue with my pythin script. I am attaching both flutter and python script
Flutter Code:
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 Code
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))
Call should connect through internet from flutter app to python script but it is only connecting on local network not through internet. On the other hand call is connecting from flutter to flutter through internet and local betwork as well so flutter code is ok and something is wrong with python code.
After some testing, there is some observation. When my Python script runs through 4g and flutter app runs through WiFi network then everything works fine. But when my Python script runs on WiFi and mobile app runs on 4g then python script is not reachable
So there is no restriction from the provider but somehow depends on laptop when connected to WiFi