Created
November 25, 2022 10:50
-
-
Save leeyisoft/9037c64d3ff97a851a17eae2c90c9c8d to your computer and use it in GitHub Desktop.
Perfect negotiation with Flutter-WebRTC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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