Skip to content

Instantly share code, notes, and snippets.

@leeyisoft
Created November 25, 2022 10:50
Show Gist options
  • Save leeyisoft/9037c64d3ff97a851a17eae2c90c9c8d to your computer and use it in GitHub Desktop.
Save leeyisoft/9037c64d3ff97a851a17eae2c90c9c8d to your computer and use it in GitHub Desktop.
Perfect negotiation with Flutter-WebRTC
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:vpl_frontend/calls/ws_messages.dart';
import 'package:vpl_frontend/utils/api.dart';
import 'call_manager_base.dart';
class RtcConnection {
MediaStream _localStream;
RTCPeerConnection _peerConnection;
RTCVideoRenderer _localRenderer = RTCVideoRenderer();
RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
RTCVideoRenderer get localRenderer => _localRenderer;
RTCVideoRenderer get remoteRenderer => _remoteRenderer;
// final mediaConstraints = <String, dynamic>{
// 'audio': true,
// 'video': {
// 'mandatory': {
// 'minWidth': '640', // Provide your own width, height and frame rate here
// 'minHeight': '480',
// 'minFrameRate': '15',
// },
// 'facingMode': 'user',
// 'optional': [],
// }
// };
final mediaConstraints = <String, dynamic>{
'audio': true,
'video': true,
};
// final mediaConstraints = <String, dynamic>{};
var configuration;
final offerSdpConstraints = <String, dynamic>{
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': true,
},
'optional': [],
};
final loopbackConstraints = <String, dynamic>{
'mandatory': {},
'optional': [
{'DtlsSrtpKeyAgreement': false}
],
};
final CallManager callManager;
RtcConnection(this.callManager) : isPolite = callManager.isCreator {
// handle offer
callManager.onRtcOffer = (RtcOfferAnswer offer) async {
print("onRtcOffer");
await _onReceivedDescription(offer);
};
// handle answer
callManager.onRtcAnswer = (RtcOfferAnswer answer) async {
print("onRtcAnswer");
await _onReceivedDescription(answer);
};
// handle candidate
callManager.onRtcCandidate = (RtcCandidate cdt) async {
print("onRtcCandidate");
await _onReceivedCandidate(cdt);
};
}
bool makingOffer = false;
bool ignoreOffer = false;
bool isSettingRemoteAnswerPending = false;
final bool isPolite;
Future<void> _onReceivedDescription(RtcOfferAnswer description) async {
print("onReceivedDescription start _peerConnection=" + _peerConnection?.toString());
// this code implements the "polite peer" principle, as described here:
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
try {
var offerCollision = (description.type == "offer") &&
(makingOffer ||
(_peerConnection.signalingState != RTCSignalingState.RTCSignalingStateStable || _peerConnection.signalingState != null));
print("onReceivedDescription offerCollision: " + offerCollision.toString());
ignoreOffer = !isPolite && offerCollision;
print(" state: " + _peerConnection.signalingState.toString());
print(" makingOffer: " + makingOffer.toString());
print(" offerCollision: " + offerCollision.toString());
print(" ignoreOffer: " + ignoreOffer.toString());
print(" polite: " + isPolite.toString());
if (ignoreOffer) {
print("onReceivedDescription offer ignored");
return;
}
await _peerConnection.setRemoteDescription(RTCSessionDescription(description.sdp, description.type)); // SRD rolls back as needed
if (description.type == "offer") {
print("onReceivedDescription received offer");
await _peerConnection.setLocalDescription(await _peerConnection.createAnswer(mediaConstraints));
var localDesc = await _peerConnection.getLocalDescription();
//send answer via callManager
callManager.sendCallMessage(MsgType.rtc_answer, RtcOfferAnswer(localDesc.sdp, localDesc.type));
print("onReceivedDescription answer sent");
}
} catch (e) {
print(e.toString());
}
}
Future<void> _onReceivedCandidate(RtcCandidate cdt) async {
RTCIceCandidate candidate = RTCIceCandidate(cdt.candidate, cdt.sdpMid, cdt.sdpMlineIndex);
try {
print("onReceivedCandidate");
await _peerConnection.addCandidate(candidate);
} catch (err) {
if (!ignoreOffer) {
// Suppress ignored offer's candidates
print("onReceivedCandidate error: " + err.toString());
} else {
print("onReceivedCandidate ignored");
}
}
}
void _onRenegotiationNeeded() async {
try {
print('onRenegotiationNeeded start');
makingOffer = true;
await _peerConnection.setLocalDescription(await _peerConnection.createOffer(mediaConstraints));
print('onRenegotiationNeeded state after setLocalDescription: ' + _peerConnection.signalingState.toString());
// send offer via callManager
var localDesc = await _peerConnection.getLocalDescription();
callManager.sendCallMessage(MsgType.rtc_offer, RtcOfferAnswer(localDesc.sdp, localDesc.type));
print('onRenegotiationNeeded; offer sent');
} catch (e) {
print("onRenegotiationNeeded error: " + e.toString());
} finally {
makingOffer = false;
print('onRenegotiationNeeded done');
}
}
void initRTC() async {
await _localRenderer.initialize();
await _remoteRenderer.initialize();
}
void _onSignalingState(RTCSignalingState state) {
print("onSignalingState: " + state.toString());
}
void _onIceGatheringState(RTCIceGatheringState state) {
print("onIceGatheringState: " + state.toString());
}
void _onIceConnectionState(RTCIceConnectionState state) {
print("onIceConnectionState: " + state.toString());
}
void _onPeerConnectionState(RTCPeerConnectionState state) {
print(state);
}
void _onAddStream(MediaStream stream) {
print('onAddStream: ' + stream.id);
_remoteRenderer.srcObject = stream;
}
void _onRemoveStream(MediaStream stream) {
print('onRemoveStream: ' + stream.id);
_remoteRenderer.srcObject = null;
}
void _onIceCandidate(RTCIceCandidate candidate) {
if (candidate == null) {
print('onIceCandidate: empty!');
return;
}
print('onCandidate: ${candidate.candidate}');
try {
// send candidate via callManager
RtcCandidate rtcCandidate = RtcCandidate(candidate.candidate, candidate.sdpMid, candidate.sdpMlineIndex);
callManager.sendCallMessage(MsgType.rtc_candidate, rtcCandidate);
} catch (e) {
print("onCandidate error: " + e.toString());
}
}
void _onTrack(RTCTrackEvent event) {
print('onTrack, kind:' + event.track.kind);
// if (_remoteRenderer.srcObject == null || _remoteRenderer.srcObject.id != event.streams[0].id) {
// if (event.track.kind == 'video') {
print('onTrack track added, type:' + event.track.kind);
event.track.onUnMute = () {
_remoteRenderer.srcObject = event.streams[0];
};
// }
}
void _onAddTrack(MediaStream stream, MediaStreamTrack track) {
print('onAddTrack track, kind:' + track.kind);
if (_remoteRenderer.srcObject == null || _remoteRenderer.srcObject.id != stream.id) {
_remoteRenderer.srcObject = stream;
print('onTrack add track added');
}
}
void _onRemoveTrack(MediaStream stream, MediaStreamTrack track) {
print('onRemoveTrack');
// if (track.kind == 'video') {
// _remoteRenderer.srcObject = null;
// }
_remoteRenderer.srcObject = null;
}
void makeConnection() async {
var iceServers = await getIceServers();
configuration = <String, dynamic>{'iceServers': iceServers};
if (_peerConnection != null) {
print("error: _peerConnection not null");
return;
}
try {
_peerConnection = await createPeerConnection(configuration, loopbackConstraints);
_peerConnection.onSignalingState = _onSignalingState;
_peerConnection.onIceGatheringState = _onIceGatheringState;
_peerConnection.onIceConnectionState = _onIceConnectionState;
_peerConnection.onConnectionState = _onPeerConnectionState;
_peerConnection.onIceCandidate = _onIceCandidate;
_peerConnection.onRenegotiationNeeded = _onRenegotiationNeeded;
_localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
_peerConnection.onTrack = _onTrack;
_peerConnection.onAddTrack = _onAddTrack;
_peerConnection.onRemoveTrack = _onRemoveTrack;
_localStream.getTracks().forEach((track) async {
await _peerConnection.addTrack(track, _localStream);
});
_localRenderer.srcObject = _localStream;
// _createOffer();
} catch (e) {
print(e.toString());
}
}
void closeRTC() async {
_localRenderer.dispose();
_remoteRenderer.dispose();
await _localStream?.dispose();
await _peerConnection?.close();
_peerConnection = null;
_localRenderer.srcObject = null;
_remoteRenderer.srcObject = null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment