WebRTC在本地网络上可用但无法通过互联网使用(Python)

0 投票
1 回答
48 浏览
提问于 2025-04-13 21:19

在我的应用程序中,我需要通过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时的情况有关。

撰写回答