Subscriber as primary (#8)
* Update protos * Signal client * Improve transport * First implementation * Fix publish * Update `PCTransport` for debounce negotiation * debounce func * reconnect & events * Update debounce func * engine events * make sure events don't emit after dispose * prefix flutter_webrtc * Cleaner `iceServers` update * don't mutate user provided params * fix tests * event manager * Fix: `Room.onDisconnected` gets fired multiple times * data publish in example * cleaner logic * safer events * un-prefix with LK * organize imports * Clean up * remove `Tuple` * `EventsEmitter` can now be directly listened to `EventsEmitter` extends `EventsListenable`
This commit is contained in:
+85
-13
@@ -1,49 +1,120 @@
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||
|
||||
import 'logger.dart';
|
||||
import 'types.dart';
|
||||
import 'utils.dart';
|
||||
import 'extensions.dart';
|
||||
|
||||
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
|
||||
|
||||
/// a wrapper around PeerConnection
|
||||
class PCTransport {
|
||||
final RTCPeerConnection pc;
|
||||
final List<RTCIceCandidate> _pendingCandidates = [];
|
||||
final rtc.RTCPeerConnection pc;
|
||||
final List<rtc.RTCIceCandidate> _pendingCandidates = [];
|
||||
bool restartingIce = false;
|
||||
bool renegotiate = false;
|
||||
PCTransportOnOffer? onOffer;
|
||||
Function? _cancelDebounce;
|
||||
|
||||
PCTransport(this.pc);
|
||||
// private constructor
|
||||
PCTransport._(this.pc);
|
||||
|
||||
static Future<PCTransport> create([RTCConfiguration? rtcConfig]) async {
|
||||
rtcConfig ??= const RTCConfiguration();
|
||||
logger.fine('PCTransport creating ${rtcConfig.toMap()}');
|
||||
final _ = await rtc.createPeerConnection(rtcConfig.toMap());
|
||||
return PCTransport._(_);
|
||||
}
|
||||
|
||||
late final negotiate = Utils.createDebounceFunc(
|
||||
() => createAndSendOffer(),
|
||||
cancelFunc: (f) => _cancelDebounce = f,
|
||||
wait: const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
Future<void> dispose() async {
|
||||
logger.fine('${objectId} dispose()');
|
||||
// Ensure debounce won't fire
|
||||
_cancelDebounce?.call();
|
||||
_cancelDebounce = null;
|
||||
|
||||
// Ensure callbacks won't fire any more
|
||||
pc.onRenegotiationNeeded = null;
|
||||
pc.onIceCandidate = null;
|
||||
pc.onIceConnectionState = null;
|
||||
pc.onTrack = null;
|
||||
|
||||
List<RTCRtpSender> senders = [];
|
||||
// Remove all senders
|
||||
List<rtc.RTCRtpSender> senders = [];
|
||||
try {
|
||||
senders = await pc.getSenders();
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
logger.warning('getSenders() failed with error: $_');
|
||||
}
|
||||
|
||||
for (final e in senders) {
|
||||
try {
|
||||
await pc.removeTrack(e);
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
logger.warning('removeTrack() failed with error: $_');
|
||||
}
|
||||
}
|
||||
|
||||
await pc.close();
|
||||
await pc.dispose();
|
||||
}
|
||||
|
||||
Future<void> setRemoteDescription(RTCSessionDescription sd) async {
|
||||
Future<void> setRemoteDescription(rtc.RTCSessionDescription sd) async {
|
||||
await pc.setRemoteDescription(sd);
|
||||
|
||||
await Future.forEach<RTCIceCandidate>(_pendingCandidates, (candidate) async {
|
||||
for (final candidate in _pendingCandidates) {
|
||||
await pc.addCandidate(candidate);
|
||||
});
|
||||
}
|
||||
|
||||
_pendingCandidates.clear();
|
||||
restartingIce = false;
|
||||
|
||||
if (renegotiate) {
|
||||
renegotiate = false;
|
||||
await createAndSendOffer(); // await or un-awaited ?
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addIceCandidate(RTCIceCandidate candidate) async {
|
||||
Future<void> createAndSendOffer([RTCOfferOptions? options]) async {
|
||||
if (onOffer == null) {
|
||||
logger.warning('onOffer is null');
|
||||
return;
|
||||
}
|
||||
|
||||
if (options?.iceRestart ?? false) {
|
||||
logger.fine('restarting ICE');
|
||||
restartingIce = true;
|
||||
}
|
||||
|
||||
if (pc.signalingState == rtc.RTCSignalingState.RTCSignalingStateHaveLocalOffer) {
|
||||
// we're waiting for the peer to accept our offer, so we'll just wait
|
||||
// the only exception to this is when ICE restart is needed
|
||||
final currentSD = await getRemoteDescription();
|
||||
if ((options?.iceRestart ?? false) && currentSD != null) {
|
||||
// TODO: handle when ICE restart is needed but we don't have a remote description
|
||||
// the best thing to do is to recreate the peerconnection
|
||||
await pc.setRemoteDescription(currentSD);
|
||||
} else {
|
||||
renegotiate = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// actually negotiate
|
||||
logger.fine('starting to negotiate');
|
||||
final offer = await pc.createOffer(options?.toMap() ?? <String, dynamic>{});
|
||||
await pc.setLocalDescription(offer);
|
||||
onOffer?.call(offer);
|
||||
}
|
||||
|
||||
Future<void> addIceCandidate(rtc.RTCIceCandidate candidate) async {
|
||||
final desc = await getRemoteDescription();
|
||||
|
||||
if (desc != null && !restartingIce) {
|
||||
@@ -54,15 +125,16 @@ class PCTransport {
|
||||
_pendingCandidates.add(candidate);
|
||||
}
|
||||
|
||||
Future<RTCSessionDescription?> getRemoteDescription() async {
|
||||
Future<rtc.RTCSessionDescription?> getRemoteDescription() async {
|
||||
// Checking agains null doesn't work as intended
|
||||
// if (pc.iceConnectionState == null) return null;
|
||||
|
||||
try {
|
||||
final result = await pc.getRemoteDescription();
|
||||
logger.fine('pc.getRemoteDescription $result');
|
||||
return result;
|
||||
} catch (_) {
|
||||
logger.warning('pc.getRemoteDescription did throw: $_');
|
||||
logger.warning('pc.getRemoteDescription failed with error: $_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user