I'm currently working on a Flutter project where I'm trying to establish a Peer-to-Peer audio and video connection using Flutter.
I've successfully implemented the offer/answer system, and both devices are able to share the stream IDs. However, I'm encountering an issue where the video remains black and doesn't display. Im really stuck because the logs don't display me any error the justify this black screen.
Here's an overview of my setup:
- I'm using Flutter with the flutter_webrtc package for WebRTC functionality.
- I'm using Firebase Realtime Database to exchange offer/answer SDP and ICE candidates between the devices.
- I’m using 2 physical android phone, one Android12 and the second Android13
- My connexion is strong enough to have RTC calls and they are not blocked on it.
Remotre stream only display a black screen Versions of my libraries Firebase data
(in the firebase data like IPAdress are in ICECandidates, not visible on screen)
I would greatly appreciate any insights or suggestions on how to resolve this issue and display the video streams correctly. Thank you in advance for your help!
(PS : I am not a native english speaker so sorry for languages approximation) (PSS : I am only doing flutter for a short period and I’m a junior, so sorry for that too)
Here is my code :
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import '../Resources/styles.dart';
class CameraSharingScreen extends StatefulWidget {
final String roomId;
final bool isHost;
CameraSharingScreen({required this.roomId, required this.isHost});
@override
_CameraSharingScreenState createState() => _CameraSharingScreenState();
}
class _CameraSharingScreenState extends State<CameraSharingScreen> {
DatabaseReference? _sessionRef;
RTCPeerConnection? _peerConnection;
MediaStream? _localStream;
MediaStream? _remoteStream;
RTCVideoRenderer _localRenderer = RTCVideoRenderer();
RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
String statePhone = "";
String ipAddress = "";
String localDescription = "";
String remoteDescription = "";
bool _isOfferPending = false;
Timer? _offerTimer;
Timer? _answerTimer;
final Map<String, dynamic> offerSdpConstraints = {
"mandatory": {
// "OfferToReceiveAudio": true,
"OfferToReceiveVideo": true,
},
"optional": [],
};
@override
void initState() {
super.initState();
initializeApp();
initializeRenderers();
// Step1 : create Session
createSession();
getLocalStream();
//_createPeerConnection();
}
@override
void dispose() {
_localStream?.dispose();
_remoteStream?.dispose();
_peerConnection?.close();
_peerConnection = null;
_localRenderer.dispose();
_remoteRenderer.dispose();
_offerTimer?.cancel();
_answerTimer?.cancel();
super.dispose();
}
initializeApp() async {
await Firebase.initializeApp();
}
initializeRenderers() async {
await _localRenderer.initialize();
await _remoteRenderer.initialize();
}
createSession() {
FirebaseDatabase.instance
.ref()
.child('sessions')
.child(widget.roomId)
.remove();
_sessionRef =
FirebaseDatabase.instance.ref().child('sessions').child(widget.roomId);
// _sessionRef = FirebaseDatabase.instance.ref().child('sessions').push();
}
getLocalStream() async {
MediaStream stream = await navigator.mediaDevices.getUserMedia({
// 'audio': true,
'video': true,
});
setState(() {
_localStream = stream;
});
_localRenderer.srcObject = _localStream;
}
_createOffer() async {
_isOfferPending = true;
//Step desc 1 : Set Local E
RTCSessionDescription description =
await _peerConnection!.createOffer({'offerToReceiveVideo': 1});
_peerConnection!.setLocalDescription(description);
log("local plouf: " + description.sdp!);
_sessionRef!.child('offer').set({
'sdp': description.sdp,
'type': description.type,
});
}
_createAnswer() async {
if (!_isOfferPending) {
RTCSessionDescription description =
await _peerConnection!.createAnswer({'offerToReceiveVideo': 1});
_peerConnection!.setLocalDescription(description);
log("local plouf: " + description.sdp!);
_sessionRef!.child('answer').set({
'sdp': description.sdp,
'type': description.type,
});
}
}
setRemoteDescription(dynamic data) async {
RTCSessionDescription description = RTCSessionDescription(
data['sdp'], statePhone == "e" ? "answer" : "offer");
await _peerConnection!.setRemoteDescription(description);
log("remote plouf: " + description.sdp!);
}
setRemoteIceCandidate(dynamic data) async {
if (data != null) {
RTCIceCandidate candidate = RTCIceCandidate(
data['candidate'],
data['sdpMid'],
data['sdpMLineIndex'],
);
await _peerConnection!.addCandidate(candidate);
log("remote plouf: " + candidate.candidate!);
}
}
_createPeerConnection() async {
Map<String, dynamic> configuration = {
// "sdpSemantics": "plan-b",
'iceServers': [
{'url': 'stun:stun.l.google.com:19302'},
],
};
_peerConnection =
await createPeerConnection(configuration, offerSdpConstraints);
if (_localStream != null) {
_localStream!.getTracks().forEach((track) {
_peerConnection!.addTrack(track, _localStream!);
});
}
_peerConnection!.onTrack = (event) {
if (event.track.kind == 'video') {
setState(() {
_remoteStream = event.streams[0];
log("IMPORTANT stream detected ! " +
_localStream!.id +
" // " +
_remoteStream!.id);
_remoteRenderer.srcObject = _remoteStream;
});
}
};
if (statePhone == "e") {
//Step 2 Create Offer // Step desc 2 Send Local E
_createOffer();
//Step 5 : Listen Answer
_sessionRef!.child('answer').onValue.listen((event) {
if (event.snapshot.value != null) {
//Step desc 6 : Set E remote from R local
setRemoteDescription(event.snapshot.value);
}
});
}
if (statePhone == "r") {
//Step 3 : listen for Offer // Step desc 3 : Set Remote 4 from Local E
_sessionRef!.child('offer').onValue.listen((event) {
if (event.snapshot.value != null) {
setRemoteDescription(event.snapshot.value);
//Step 4 : Create Answer // Step desc 4 & 5
_createAnswer();
}
});
}
_peerConnection!.onIceCandidate = (candidate) {
if (candidate != null) {
_sessionRef!.child('iceCandidates').push().set({
'candidate': candidate.candidate,
'sdpMid': /* candidate.sdpMid */ 'video',
'sdpMLineIndex': candidate.sdpMLineIndex,
});
}
};
_sessionRef!.child('iceCandidates').onChildAdded.listen((event) {
setRemoteIceCandidate(event.snapshot.value);
});
}
becameReceiver() {
String sessionKey = "-NZkn0LpxoWvxCZpOsuF";
_sessionRef =
FirebaseDatabase.instance.ref().child('sessions').child(sessionKey);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Column(
children: [
Text(statePhone +
" // " +
"${(_localStream == null) ? '' : _localStream!.id}"),
Text(statePhone +
" // " +
"${(_remoteStream == null) ? '' : _remoteStream!.id}"),
],
),
),
body: Container(
decoration: BoxDecoration(gradient: Styles.mainGradientColor),
child: Row(
children: [
Flexible(
child: RTCVideoView(_remoteRenderer),
),
Flexible(
child: RTCVideoView(_localRenderer),
),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
floatingActionButton: FloatingActionButton(
child: Icon((widget.isHost) ? Icons.send : Icons.call_received),
onPressed: () {
statePhone = (widget.isHost) ? "e" : "r";
setState(() {});
_createPeerConnection();
},
),
);
}
}