* add audio_session package * `AudioManager` initial design * try to integrate audio manager * configure audio session * `DisposeAware` disposing if already published causes exception. * organize * guard flutter_webrtc calls * websocket async fix * use `ConnectionState` instead of `_isClosed` and `isReconnecting` * change create audio track defaults * update protos * protocol 3 speaker updates * fix exception * emit `RoomDisconnectedEvent` only once * fix exception * keep track of local / remote audio tracks * explicit types * manage track state * re-structure audio management * change defaults * unpublish all * use experimental build * change defaults * configuring is optional * call native `RTCAudioSession.setConfiguration` * defaults adjustment * iOS only for now * use lib 92.4515.07 * clean up * revert audio options for now * remove pod source * rename apple related audio * `createListener` method * organize native audio * `SpeakingChangedEvent` only on `Participant` * refactoring * fix ios compile * fix configure audio only for iOS logic * minor fix & clean up * change dispose logic * format protos * fix unpublishTrack * `createListener` into a mixin * simplify * use flutter_webrtc master * unpublish for example * update ios icon * android icon * web icon * favicon
169 lines
4.5 KiB
Dart
169 lines
4.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
|
|
|
import 'constants.dart';
|
|
import 'extensions.dart';
|
|
import 'logger.dart';
|
|
import 'support/disposable.dart';
|
|
import 'types.dart';
|
|
import 'utils.dart';
|
|
|
|
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
|
|
|
|
/// a wrapper around PeerConnection
|
|
class PCTransport extends Disposable {
|
|
final rtc.RTCPeerConnection pc;
|
|
final List<rtc.RTCIceCandidate> _pendingCandidates = [];
|
|
bool restartingIce = false;
|
|
bool renegotiate = false;
|
|
PCTransportOnOffer? onOffer;
|
|
Function? _cancelDebounce;
|
|
|
|
// private constructor
|
|
PCTransport._(this.pc) {
|
|
//
|
|
onDispose(() async {
|
|
_cancelDebounce?.call();
|
|
_cancelDebounce = null;
|
|
|
|
// Ensure callbacks won't fire any more
|
|
pc.onRenegotiationNeeded = null;
|
|
pc.onIceCandidate = null;
|
|
pc.onIceConnectionState = null;
|
|
pc.onTrack = null;
|
|
|
|
// Remove all senders
|
|
List<rtc.RTCRtpSender> senders = [];
|
|
try {
|
|
senders = await pc.getSenders();
|
|
} catch (_) {
|
|
logger.warning('getSenders() failed with error: $_');
|
|
}
|
|
|
|
for (final e in senders) {
|
|
try {
|
|
await pc.removeTrack(e);
|
|
} catch (_) {
|
|
logger.warning('removeTrack() failed with error: $_');
|
|
}
|
|
}
|
|
|
|
await pc.close();
|
|
await pc.dispose();
|
|
});
|
|
}
|
|
|
|
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: Timeouts.debounce,
|
|
);
|
|
|
|
// @override
|
|
// Future<void> dispose() async {
|
|
// super.dispose();
|
|
// // Ensure debounce won't fire
|
|
|
|
// }
|
|
|
|
Future<void> setRemoteDescription(rtc.RTCSessionDescription sd) async {
|
|
if (isDisposed) {
|
|
logger.warning('[$objectId] setRemoteDescription() already disposed');
|
|
return;
|
|
}
|
|
|
|
await pc.setRemoteDescription(sd);
|
|
|
|
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> createAndSendOffer([RTCOfferOptions? options]) async {
|
|
if (isDisposed) {
|
|
logger.warning('[$objectId] createAndSendOffer() already disposed');
|
|
return;
|
|
}
|
|
|
|
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 {
|
|
if (isDisposed) {
|
|
logger.warning('[$objectId] addIceCandidate() already disposed');
|
|
return;
|
|
}
|
|
|
|
final desc = await getRemoteDescription();
|
|
|
|
if (desc != null && !restartingIce) {
|
|
await pc.addCandidate(candidate);
|
|
return;
|
|
}
|
|
|
|
_pendingCandidates.add(candidate);
|
|
}
|
|
|
|
Future<rtc.RTCSessionDescription?> getRemoteDescription() async {
|
|
if (isDisposed) {
|
|
logger.warning('[$objectId] getRemoteDescription() already disposed');
|
|
return null;
|
|
}
|
|
|
|
// 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 failed with error: $_');
|
|
}
|
|
}
|
|
}
|