如何使用flutter-webrtc包实现双用户P2P语音通话连接?
Hey there! I totally get the frustration—you’ve got the theory down, but translating that into working Flutter code can feel like hitting a wall. Let’s walk through a step-by-step, actionable solution for two-user voice calls with flutter-webrtc, focusing on the parts that often trip folks up.
Before diving into WebRTC logic, make sure you’ve got the basics locked in:
- Add the
flutter-webrtcdependency to yourpubspec.yaml - Grant microphone permissions:
- For Android: Add
<uses-permission android:name="android.permission.RECORD_AUDIO" />to yourAndroidManifest.xml - For iOS: Add
NSMicrophoneUsageDescriptiontoInfo.plistwith a clear reason for accessing the mic
- For Android: Add
- Initialize a basic renderer to handle audio streams (even for voice calls, this helps manage playback)
You’ll need two key objects for each user: RTCPeerConnection (manages the peer-to-peer connection) and MediaStream (captures local audio input). Here’s how to set them up:
import 'package:flutter_webrtc/flutter_webrtc.dart'; // Use Google's public STUN server for testing final Map<String, dynamic> configuration = { 'iceServers': [ {'urls': 'stun:stun.l.google.com:19302'} ] }; late RTCPeerConnection peerConnection; late MediaStream localStream; final RTCVideoRenderer remoteRenderer = RTCVideoRenderer(); // Initialize WebRTC components Future<void> initWebRTC() async { await remoteRenderer.initialize(); // Create peer connection with STUN config peerConnection = await createPeerConnection(configuration); // Capture local audio stream localStream = await navigator.mediaDevices.getUserMedia({ 'audio': true, 'video': false, // We only need voice for this use case }); // Add local audio track to the peer connection localStream.getTracks().forEach((track) { peerConnection.addTrack(track, localStream); }); // Listen for incoming remote audio streams peerConnection.onTrack.listen((event) { if (event.streams.isNotEmpty) { remoteRenderer.srcObject = event.streams[0]; } }); // Send ICE candidates to the other user via signaling peerConnection.onIceCandidate.listen((candidate) { if (candidate != null) { // Send this candidate through your signaling system (e.g., WebSocket, Firebase) sendSignalingMessage({ 'type': 'ice_candidate', 'candidate': candidate.toMap(), }); } }); }
You know signaling is required, but let’s map exactly what to send when for two users:
For the Call Initiator (User A):
- After initializing WebRTC, create an offer and send it to User B:
Future<void> createAndSendOffer() async { final offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); // Send the offer to User B via your signaling channel sendSignalingMessage({ 'type': 'offer', 'sdp': offer.toMap(), }); }
For the Call Receiver (User B):
- When you receive the offer from User A, set it as the remote description and send back an answer:
Future<void> handleOffer(Map<String, dynamic> offerData) async { final RTCSessionDescription offer = RTCSessionDescription( offerData['sdp']['sdp'], offerData['sdp']['type'], ); await peerConnection.setRemoteDescription(offer); // Create and send answer to User A final answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); sendSignalingMessage({ 'type': 'answer', 'sdp': answer.toMap(), }); }
Handling ICE Candidates (Both Users):
When you receive an ICE candidate from the other user, add it to your peer connection immediately:
void handleIceCandidate(Map<String, dynamic> candidateData) { final RTCIceCandidate candidate = RTCIceCandidate( candidateData['candidate']['candidate'], candidateData['candidate']['sdpMid'], candidateData['candidate']['sdpMLineIndex'], ); peerConnection.addIceCandidate(candidate); }
For the Initiator to Handle the Answer:
When User A gets the answer from User B, set it as the remote description:
Future<void> handleAnswer(Map<String, dynamic> answerData) async { final RTCSessionDescription answer = RTCSessionDescription( answerData['sdp']['sdp'], answerData['sdp']['type'], ); await peerConnection.setRemoteDescription(answer); }
- Signaling Reliability: Your signaling channel (WebSocket, etc.) must be reliable—if an offer/answer/ICE candidate gets lost, the connection will fail. Stick to a channel that guarantees message delivery for testing.
- ICE Candidate Timing: Don’t wait to send ICE candidates—send them as soon as they’re generated. They can arrive in any order, and the peer connection will sort them out automatically.
- Permissions: Double-check that microphone permissions are granted before trying to capture the local stream. Flutter won’t throw a loud error here—it’ll just fail silently.
- Cleanup: Don’t forget to dispose of renderers and close the peer connection when the call ends to avoid memory leaks.
Once you wire up the signaling logic to send/receive these messages between users, you should have a working voice call. Start with a simple WebSocket server for testing if you don’t have a signaling system already—keep it basic at first!
内容的提问来源于stack exchange,提问作者Abdul Mustafa




