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:
Hiroshi Horie
2021-09-18 00:10:16 +09:00
committed by GitHub
parent 50f56c5e1e
commit f4bd82a919
36 changed files with 1334 additions and 588 deletions
+85 -13
View File
@@ -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: $_');
}
}
}