WebRTC working on Local network but not through Internet (Python)

50 Views Asked by At

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.

1

There are 1 best solutions below

1
Muhammad Azeem On

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