【发布时间】:2021-05-10 06:11:09
【问题描述】:
我正在尝试在 Flutter 中使用 Webrtc 进行 3 对等视频聊天。 A 需要看到 B 和 C。B 需要看到 A 和 C。C 需要看到 A 和 B。我也使用 socket.io 作为信号服务器。我使用了 2 个对等连接。两个peer连接成功。当我尝试连接第三个并尝试传递报价时,出现错误。
在“RTCPeerConnection”上执行“createAnswer”:PeerConnection 无法在 have-remote-offer 或 have-local-pranswer 之外的状态下创建答案。
我的方法不适合多个对等连接吗?
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:sdp_transform/sdp_transform.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
class Home extends StatefulWidget {
final String title;
const Home({Key key, this.title}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _offer = false;
bool firstTimeOffer = true;
bool firstTimeRemoteDescription = true;
bool firstTimeAnswer = true;
bool firstTimeCandidate = true;
RTCPeerConnection _peerConnection;
RTCPeerConnection _peerConnection2;
MediaStream _localStream;
final _localVideoRenderer = new RTCVideoRenderer();
final _remoteVideoRenderer = new RTCVideoRenderer();
final _remoteVideoRenderer2 = RTCVideoRenderer();
IO.Socket socket;
var pcConfig = {};
var candidates = [];
final txtController = TextEditingController();
@override
void dispose() {
txtController.dispose();
_localVideoRenderer.dispose();
super.dispose();
}
@override
void initState() {
initRenderers();
_createPeerConnection().then((pc) {
_peerConnection = pc;
});
_createPeerConnection2().then((pc) {
_peerConnection2 = pc;
});
_getUserMedia();
connectToServer();
super.initState();
}
void connectToServer() {
// connectToServer();
socket = IO.io('http://localhost:3000', <String, dynamic>{
'transports': ['websocket'],
'autoConnect': false,
});
socket.connect();
socket.onDisconnect((_) => print("disconnected"));
socket.on('connection-success', (data) => print(data));
socket.on('offerOrAnswer', (sdp) async {
this.txtController.text = sdp;
});
socket.on('candidate', (candidate) {
// print('From Peer.....');
// print(json.encode(candidate));
this.candidates = [...this.candidates, candidate];
});
}
_getUserMedia() async {
final Map<String, dynamic> mediaConstraints = {
'audio': false,
'video': {'facingMode': 'user'}
};
MediaStream stream =
await navigator.mediaDevices.getUserMedia(mediaConstraints);
_localVideoRenderer.srcObject = stream;
// _localVideoRenderer.mirror = true;
return stream;
}
initRenderers() async {
await _localVideoRenderer.initialize();
await _remoteVideoRenderer.initialize();
}
_createPeerConnection() async {
Map<String, dynamic> configuration = {
"iceServers": [
{"url": "stun:stun.l.google.com:19302"},
]
};
final Map<String, dynamic> offerSdpConstraints = {
"mandatory": {
"OfferToReceiveAudio": true,
"OfferToReceiveVideo": true,
},
"optional": [],
};
_localStream = await _getUserMedia();
RTCPeerConnection pc =
await createPeerConnection(configuration, offerSdpConstraints);
// if (pc != null) print(pc);
pc.addStream(_localStream);
// triggered when a new candidate is returned
pc.onIceCandidate = (e) {
//send the candidates to the remote peer
// see addCandidate below to be triggered
if (e.candidate != null) {
var candidate = {
'candidate': e.candidate.toString(),
'sdpMid': e.sdpMid.toString(),
'sdpMlineIndex': e.sdpMlineIndex
};
print(e.candidate);
this.sendToPeer('candidate', candidate);
}
};
pc.onIceConnectionState = (e) {
print(e);
};
pc.onAddStream = (stream) {
print('addStream: ' + stream.id);
_remoteVideoRenderer.srcObject = stream;
};
return pc;
}
_createPeerConnection2() async {
Map<String, dynamic> configuration = {
"iceServers": [
{"url": "stun:stun.l.google.com:19302"},
]
};
final Map<String, dynamic> offerSdpConstraints = {
"mandatory": {
"OfferToReceiveAudio": true,
"OfferToReceiveVideo": true,
},
"optional": [],
};
_localStream = await _getUserMedia();
RTCPeerConnection pc =
await createPeerConnection(configuration, offerSdpConstraints);
// if (pc != null) print(pc);
pc.addStream(_localStream);
// triggered when a new candidate is returned
pc.onIceCandidate = (e) {
//send the candidates to the remote peer
// see addCandidate below to be triggered
if (e.candidate != null) {
var candidate = {
'candidate': e.candidate.toString(),
'sdpMid': e.sdpMid.toString(),
'sdpMlineIndex': e.sdpMlineIndex
};
print(e.candidate);
this.sendToPeer('candidate', candidate);
}
};
pc.onIceConnectionState = (e) {
print(e);
};
pc.onAddStream = (stream) {
print('addStream: ' + stream.id);
_remoteVideoRenderer2.srcObject = stream;
};
return pc;
}
sendToPeer(messageType, payload) {
socket.emit(messageType, {'socketID': socket.id, 'payload': payload});
}
void _createOffer() async {
if (firstTimeOffer) {
print('if true firsttimeoffer');
firstTimeOffer = false;
RTCSessionDescription description =
await _peerConnection.createOffer({'offerToReceiveVideo': 1});
_offer = true;
_peerConnection.setLocalDescription(description);
this.sendToPeer('offerOrAnswer', description.sdp);
//print(description.sdp);
} else {
print('if false offer');
RTCSessionDescription description =
await _peerConnection2.createOffer({'offerToReceiveVideo': 1});
_offer = true;
_peerConnection2.setLocalDescription(description);
this.sendToPeer('offerOrAnswer', description.sdp);
}
}
void _setRemoteDescription() async {
if (firstTimeRemoteDescription) {
print('if true remote');
firstTimeRemoteDescription = false;
RTCSessionDescription description = new RTCSessionDescription(
txtController.text, _offer ? 'answer' : 'offer');
await _peerConnection.setRemoteDescription(description);
} else {
print('if false remote');
RTCSessionDescription description = new RTCSessionDescription(
txtController.text, _offer ? 'answer' : 'offer');
await _peerConnection2.setRemoteDescription(description);
}
}
void _createAnswer() async {
if (firstTimeAnswer) {
print('if true createanswer');
firstTimeAnswer = false;
RTCSessionDescription description =
await _peerConnection.createAnswer({'offerToReceiveVideo': 1});
_offer = true;
_peerConnection.setLocalDescription(description);
this.sendToPeer('offerOrAnswer', description.sdp);
} else {
print('if false createanswer');
RTCSessionDescription description =
await _peerConnection2.createAnswer({'offerToReceiveVideo': 1});
_offer = true;
_peerConnection2.setLocalDescription(description);
this.sendToPeer('offerOrAnswer', description.sdp);
}
}
void _addCandidate() async {
if (firstTimeCandidate) {
print('if true addcandi');
firstTimeCandidate = false;
this.candidates.forEach((candidate) async {
print(json.encode(candidate));
dynamic session = new RTCIceCandidate(candidate['candidate'],
candidate['sdpMid'], candidate['sdpMlineIndex']);
await _peerConnection.addCandidate(session);
});
} else {
print('if false addcandi');
this.candidates.forEach((candidate) async {
print(json.encode(candidate));
dynamic session = new RTCIceCandidate(candidate['candidate'],
candidate['sdpMid'], candidate['sdpMlineIndex']);
await _peerConnection2.addCandidate(session);
});
}
}
SizedBox videoRenderers() => SizedBox(
height: 210,
child: Row(children: [
Flexible(
child: new Container(
key: new Key("local"),
margin: new EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
decoration: new BoxDecoration(color: Colors.black),
child: RTCVideoView(_localVideoRenderer)),
),
Flexible(
child: new Container(
key: new Key("remote"),
margin: new EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
decoration: new BoxDecoration(color: Colors.black),
child: RTCVideoView(_remoteVideoRenderer)),
),
Flexible(
child: new Container(
key: new Key("remote2"),
margin: new EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
decoration: new BoxDecoration(color: Colors.black),
child: RTCVideoView(_remoteVideoRenderer2)),
)
]));
Row offerAndAnswerButtons() =>
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[
new ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.amber),
),
// onPressed: () {
// return showDialog(
// context: context,
// builder: (context) {
// return AlertDialog(
// content: Text(sdpController.text),
// );
// });
// },
onPressed: _createOffer,
child: Text('Offer'),
),
ElevatedButton(
onPressed: _createAnswer,
child: Text('Answer'),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.amber),
),
),
]);
Row sdpCandidateButtons() =>
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[
ElevatedButton(
onPressed: _setRemoteDescription,
child: Text('Set Remote Desc'),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.amber),
),
),
ElevatedButton(
onPressed: _addCandidate,
child: Text('Add Candidate'),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.amber),
),
)
]);
Padding sdpCandidatesTF() => Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: txtController,
keyboardType: TextInputType.multiline,
maxLines: 4,
maxLength: TextField.noMaxLength,
),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: Column(
children: [
videoRenderers(),
offerAndAnswerButtons(),
sdpCandidatesTF(),
sdpCandidateButtons()
],
),
),
);
}
}
【问题讨论】: