diff --git a/packages/stream_video/CHANGELOG.md b/packages/stream_video/CHANGELOG.md index 576d28860..ec76f1c2f 100644 --- a/packages/stream_video/CHANGELOG.md +++ b/packages/stream_video/CHANGELOG.md @@ -4,6 +4,8 @@ * Fixed incoming call timeout handling. - Use `streamVideo.observeCoreRingingEventsForBackground()` instead of `streamVideo.observeCallDeclinedRingingEvent()` in `firebaseMessagingBackgroundHandler` to support all necessary events. +* Improved SFU stats implementation. + ## 1.0.1 ### ✅ Added diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index f50fe37b8..89f7602c6 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -967,7 +967,12 @@ class Call { networkMonitor: networkMonitor, streamVideo: _streamVideo, statsOptions: _sfuStatsOptions!, - leftoverTraceRecords: _previousSession?.getTrace().snapshot ?? [], + leftoverTraceRecords: + _previousSession + ?.getTrace() + .expand((slice) => slice.snapshot) + .toList() ?? + const [], onReconnectionNeeded: (pc, strategy) { _session?.trace('pc_reconnection_needed', { 'peerConnectionId': pc.type.name, @@ -1481,8 +1486,8 @@ class Call { return; } - _session?.trace('call_reconnect', { - 'strategy': _reconnectStrategy.name, + _session?.trace('callReconnect', { + 'strategy': strategy.name, }); _stateManager.lifecycleCallConnecting( @@ -1531,8 +1536,8 @@ class Call { await _reconnectMigrate(); } - _session?.trace('call_reconnect_success', { - 'strategy': _reconnectStrategy.name, + _session?.trace('callReconnectSuccess', { + 'strategy': strategy.name, }); } catch (error) { switch (error) { @@ -1541,8 +1546,8 @@ class Call { _logger.w(() => '[reconnect] unrecoverable error'); _stateManager.lifecycleCallReconnectingFailed(); - _session?.trace('call_reconnect_failed', { - 'strategy': _reconnectStrategy.name, + _session?.trace('callReconnectFailed', { + 'strategy': strategy.name, 'error': error.toString(), }); diff --git a/packages/stream_video/lib/src/call/session/call_session.dart b/packages/stream_video/lib/src/call/session/call_session.dart index 5166ff0a3..c614204cc 100644 --- a/packages/stream_video/lib/src/call/session/call_session.dart +++ b/packages/stream_video/lib/src/call/session/call_session.dart @@ -104,6 +104,7 @@ class CallSession extends Disposable { final InternetConnection networkMonitor; final StatsOptions statsOptions; final Tracer _tracer; + final Tracer _zonedTracer = Tracer(null); final StreamVideo _streamVideo; final Duration joinResponseTimeout; @@ -121,6 +122,7 @@ class CallSession extends Disposable { StatsReporter? statsReporter; Timer? _peerConnectionCheckTimer; + bool _isLeavingOrClosed = false; sfu_models.ClientDetails? _clientDetails; @@ -132,12 +134,13 @@ class CallSession extends Disposable { onCancel: () => Result.error('UpdateViewportVisibility cancelled'), ); - TraceSlice getTrace() { - return _tracer.take(); + List getTrace() { + return [_tracer.take(), _zonedTracer.take()]; } void setTraceEnabled(bool enabled) { _tracer.setEnabled(enabled); + _zonedTracer.setEnabled(enabled); } void trace(String tag, dynamic data) { @@ -459,6 +462,12 @@ class CallSession extends Disposable { fastReconnectDeadline: event.fastReconnectDeadline, ), ); + } on TimeoutException catch (e, stk) { + final message = + 'Waiting for "joinResponse" has timed out after ${joinResponseTimeout.inMilliseconds}ms'; + _tracer.trace('joinRequestTimeout', message); + _logger.e(() => '[start] failed: $e'); + return Result.failure(VideoErrors.compose(e, stk)); } catch (e, stk) { _logger.e(() => '[start] failed: $e'); return Result.failure(VideoErrors.compose(e, stk)); @@ -585,6 +594,7 @@ class CallSession extends Disposable { void leave({String? reason}) { _logger.d(() => '[leave] no args'); + _isLeavingOrClosed = true; sfuWS.leave(sessionId: sessionId, reason: reason); } @@ -593,6 +603,7 @@ class CallSession extends Disposable { String? closeReason, }) async { _logger.d(() => '[close] code: $code, closeReason: $closeReason'); + _isLeavingOrClosed = true; await _eventsSubscription?.cancel(); await _networkStatusSubscription?.cancel(); @@ -614,6 +625,7 @@ class CallSession extends Disposable { @override Future dispose() async { _logger.d(() => '[dispose] no args'); + _isLeavingOrClosed = true; await close(StreamWebSocketCloseCode.normalClosure); return await super.dispose(); @@ -683,6 +695,7 @@ class CallSession extends Disposable { } else if (event is SfuParticipantLeftEvent) { stateManager.sfuParticipantLeft(event); } else if (event is SfuConnectionQualityChangedEvent) { + _tracer.trace('ConnectionQualityChanged', event.toJson()); stateManager.sfuConnectionQualityChanged(event); } else if (event is SfuAudioLevelChangedEvent) { stateManager.sfuUpdateAudioLevelChanged(event); @@ -905,7 +918,7 @@ class CallSession extends Disposable { } Future _onRenegotiationNeeded(StreamPeerConnection pc) async { - if (stateManager.callState.status.isDisconnected) { + if (_isLeavingOrClosed || stateManager.callState.status.isDisconnected) { _logger.w(() => '[negotiate] call is disconnected'); return; } @@ -1037,7 +1050,7 @@ class CallSession extends Disposable { } final result = TracerZone.run( - _tracer, + _zonedTracer, ++zonedTracerSeq, () async { return rtcManager.setCameraEnabled( @@ -1060,7 +1073,7 @@ class CallSession extends Disposable { } final result = TracerZone.run( - _tracer, + _zonedTracer, ++zonedTracerSeq, () async { return rtcManager.setMicrophoneEnabled( diff --git a/packages/stream_video/lib/src/call/session/call_session_factory.dart b/packages/stream_video/lib/src/call/session/call_session_factory.dart index c1fe1d886..c648c414b 100644 --- a/packages/stream_video/lib/src/call/session/call_session_factory.dart +++ b/packages/stream_video/lib/src/call/session/call_session_factory.dart @@ -72,18 +72,17 @@ class CallSessionFactory { _logger.v(() => '[makeCallSession] sfuName: $sfuName, sfuUrl: $sfuUrl'); - final tracer = Tracer('$sessionSeq') + final tracer = Tracer('$sessionSeq-$sfuName') ..setEnabled(statsOptions.enableRtcStats) ..traceMultiple( leftoverTraceRecords .map( (r) => r.copyWith( - id: '${max(0, sessionSeq - 1)}', + id: '${max(0, sessionSeq - 1)}-$sfuName', ), ) .toList(), - ) - ..trace('create', {'url': sfuName}); + ); return CallSession( sessionSeq: sessionSeq, diff --git a/packages/stream_video/lib/src/call/stats/sfu_stats_reporter.dart b/packages/stream_video/lib/src/call/stats/sfu_stats_reporter.dart index 3fcf36003..1057f4933 100644 --- a/packages/stream_video/lib/src/call/stats/sfu_stats_reporter.dart +++ b/packages/stream_video/lib/src/call/stats/sfu_stats_reporter.dart @@ -188,11 +188,14 @@ class SfuStatsReporter { final publisherTrace = callSession.rtcManager?.publisher?.tracer .take(); final sessionTrace = callSession.getTrace(); + final mediaDeviceNotifierTrace = RtcMediaDeviceNotifier.instance + .getTrace(); traces.addAll([ if (subscriberTrace != null) subscriberTrace, if (publisherTrace != null) publisherTrace, - sessionTrace, + ...sessionTrace, + mediaDeviceNotifierTrace, ]); } diff --git a/packages/stream_video/lib/src/call/stats/stats_reporter.dart b/packages/stream_video/lib/src/call/stats/stats_reporter.dart index 02275a4e7..d135fe790 100644 --- a/packages/stream_video/lib/src/call/stats/stats_reporter.dart +++ b/packages/stream_video/lib/src/call/stats/stats_reporter.dart @@ -46,6 +46,8 @@ class StatsReporter extends StateNotifier { return Stream.periodic(interval, (tick) => (collectStats(), tick)).asyncMap( (data) async { final stats = await data.$1; + if (!mounted) return stats; + unawaited(_processStats(stats, data.$2)); return stats; }, @@ -92,6 +94,8 @@ class StatsReporter extends StateNotifier { stats, int tick, ) async { + if (!mounted) return; + var publisherStats = state?.publisher ?? PeerConnectionStats.empty(); var subscriberStats = state?.subscriber ?? PeerConnectionStats.empty(); diff --git a/packages/stream_video/lib/src/sfu/sfu_extensions.dart b/packages/stream_video/lib/src/sfu/sfu_extensions.dart index b19efdbd9..f0223abc1 100644 --- a/packages/stream_video/lib/src/sfu/sfu_extensions.dart +++ b/packages/stream_video/lib/src/sfu/sfu_extensions.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; - import 'package:collection/collection.dart'; -import 'package:sdp_transform/sdp_transform.dart'; import '../../protobuf/video/sfu/event/events.pb.dart' as sfu_events; import '../../protobuf/video/sfu/models/models.pb.dart' as sfu_models; @@ -17,6 +14,8 @@ import '../sfu/data/models/sfu_video_sender.dart'; import '../utils/string.dart'; import '../webrtc/model/rtc_video_dimension.dart'; import 'data/events/sfu_events.dart'; +import 'data/models/sfu_connection_info.dart'; +import 'data/models/sfu_model_mapper_extensions.dart'; import 'data/models/sfu_participant.dart'; extension CodecX on sfu_models.Codec { @@ -24,9 +23,9 @@ extension CodecX on sfu_models.Codec { return { 'name': name, 'fmtp': fmtp, - 'clock_rate': clockRate, - 'encoding_parameters': encodingParameters, - 'payload_type': payloadType, + 'clockRate': clockRate, + 'encodingParameters': encodingParameters, + 'payloadType': payloadType, }; } } @@ -68,8 +67,8 @@ extension SfuParticipantX on SfuParticipant { extension TrackInfoX on sfu_models.TrackInfo { Map toJson() { return { - 'track_id': trackId, - 'track_type': trackType.toString(), + 'trackId': trackId, + 'trackType': trackType.value, 'layers': layers.map((layer) => layer.toJson()).toList(), 'mid': mid, 'dtx': dtx, @@ -77,7 +76,7 @@ extension TrackInfoX on sfu_models.TrackInfo { 'red': red, 'muted': muted, 'codec': codec.toJson(), - 'publish_option_id': publishOptionId, + 'publishOptionId': publishOptionId, }; } } @@ -86,24 +85,22 @@ extension VideoLayerX on sfu_models.VideoLayer { Map toJson() { return { 'rid': rid, - 'video_dimension': { + 'videoDimension': { 'width': videoDimension.width, 'height': videoDimension.height, }, 'bitrate': bitrate, 'fps': fps, - 'quality': quality.toString(), + 'quality': quality.value, }; } } extension SetPublisherRequestX on sfu.SetPublisherRequest { Map toJson() { - final parsedSdp = parse(sdp); - return { - 'sdp': parsedSdp, - 'session_id': sessionId, + 'sdp': sdp, + 'sessionId': sessionId, 'tracks': tracks.map((track) => track.toJson()).toList(), }; } @@ -112,10 +109,10 @@ extension SetPublisherRequestX on sfu.SetPublisherRequest { extension ClientDetailsX on sfu_models.ClientDetails { Map toJson() { return { - 'sdk': sdk.toJson(), - 'os': os.toJson(), - 'browser': browser.toJson(), - 'device': device.toJson(), + if (hasSdk()) 'sdk': sdk.toJson(), + if (hasOs()) 'os': os.toJson(), + if (hasBrowser()) 'browser': browser.toJson(), + if (hasDevice()) 'device': device.toJson(), }; } } @@ -123,7 +120,7 @@ extension ClientDetailsX on sfu_models.ClientDetails { extension SdkX on sfu_models.Sdk { Map toJson() { return { - 'type': type.toString(), + 'type': type.value, 'major': major, 'minor': minor, 'patch': patch, @@ -161,11 +158,10 @@ extension DeviceX on sfu_models.Device { extension SendAnswerRequestX on sfu.SendAnswerRequest { Map toJson() { - final parsedSdp = parse(sdp); return { - 'peer_type': peerType.toString(), - 'sdp': parsedSdp, - 'session_id': sessionId, + 'peerType': peerType.value, + 'sdp': sdp, + 'sessionId': sessionId, }; } } @@ -173,9 +169,9 @@ extension SendAnswerRequestX on sfu.SendAnswerRequest { extension ICETrickleX on sfu_models.ICETrickle { Map toJson() { return { - 'peer_type': peerType.toString(), - 'ice_candidate': jsonDecode(iceCandidate), - 'session_id': sessionId, + 'peerType': peerType.value, + 'iceCandidate': iceCandidate, + 'sessionId': sessionId, }; } } @@ -183,8 +179,8 @@ extension ICETrickleX on sfu_models.ICETrickle { extension ICERestartRequestX on sfu.ICERestartRequest { Map toJson() { return { - 'session_id': sessionId, - 'peer_type': peerType.toString(), + 'sessionId': sessionId, + 'peerType': peerType.value, }; } } @@ -192,8 +188,8 @@ extension ICERestartRequestX on sfu.ICERestartRequest { extension UpdateMuteStatesRequestX on sfu.UpdateMuteStatesRequest { Map toJson() { return { - 'session_id': sessionId, - 'mute_states': muteStates.map((state) => state.toJson()).toList(), + 'sessionId': sessionId, + 'muteStates': muteStates.map((state) => state.toJson()).toList(), }; } } @@ -201,7 +197,7 @@ extension UpdateMuteStatesRequestX on sfu.UpdateMuteStatesRequest { extension TrackMuteStateX on sfu.TrackMuteState { Map toJson() { return { - 'track_type': trackType.toString(), + 'trackType': trackType.value, 'muted': muted, }; } @@ -210,7 +206,7 @@ extension TrackMuteStateX on sfu.TrackMuteState { extension UpdateSubscriptionsRequestX on sfu.UpdateSubscriptionsRequest { Map toJson() { return { - 'session_id': sessionId, + 'sessionId': sessionId, 'tracks': tracks.map((sub) => sub.toJson()).toList(), }; } @@ -219,9 +215,9 @@ extension UpdateSubscriptionsRequestX on sfu.UpdateSubscriptionsRequest { extension TrackSubscriptionDetailsX on sfu.TrackSubscriptionDetails { Map toJson() { return { - 'user_id': userId, - 'session_id': sessionId, - 'track_type': trackType.toString(), + 'userId': userId, + 'sessionId': sessionId, + 'trackType': trackType.value, 'dimension': dimension.toJson(), }; } @@ -239,7 +235,7 @@ extension VideoDimensionX on sfu_models.VideoDimension { extension StartNoiseCancellationRequestX on sfu.StartNoiseCancellationRequest { Map toJson() { return { - 'session_id': sessionId, + 'sessionId': sessionId, }; } } @@ -247,7 +243,7 @@ extension StartNoiseCancellationRequestX on sfu.StartNoiseCancellationRequest { extension StopNoiseCancellationRequestX on sfu.StopNoiseCancellationRequest { Map toJson() { return { - 'session_id': sessionId, + 'sessionId': sessionId, }; } } @@ -255,14 +251,14 @@ extension StopNoiseCancellationRequestX on sfu.StopNoiseCancellationRequest { extension ReconnectDetailsX on sfu_events.ReconnectDetails { Map toJson() { return { - 'strategy': strategy.toString(), - 'announced_tracks': announcedTracks + 'strategy': strategy.value, + 'announcedTracks': announcedTracks .map((track) => track.toJson()) .toList(), 'subscriptions': subscriptions.map((sub) => sub.toJson()).toList(), - 'reconnect_attempt': reconnectAttempt, - 'from_sfu_id': fromSfuId, - 'previous_session_id': previousSessionId, + 'reconnectAttempt': reconnectAttempt, + 'fromSfuId': fromSfuId, + 'previousSessionId': previousSessionId, 'reason': reason, }; } @@ -271,8 +267,8 @@ extension ReconnectDetailsX on sfu_events.ReconnectDetails { extension SfuChangePublishQualityEventJsonX on SfuChangePublishQualityEvent { Map toJson() { return { - 'audio_senders': audioSenders.map((sender) => sender.toJson()).toList(), - 'video_senders': videoSenders.map((sender) => sender.toJson()).toList(), + 'audioSenders': audioSenders.map((sender) => sender.toJson()).toList(), + 'videoSenders': videoSenders.map((sender) => sender.toJson()).toList(), }; } } @@ -280,7 +276,7 @@ extension SfuChangePublishQualityEventJsonX on SfuChangePublishQualityEvent { extension SfuChangePublishOptionsEventJsonX on SfuChangePublishOptionsEvent { Map toJson() { return { - 'publish_options': publishOptions + 'publishOptions': publishOptions .map((option) => option.toJson()) .toList(), 'reason': reason, @@ -292,8 +288,8 @@ extension SfuAudioSenderJsonX on SfuAudioSender { Map toJson() { return { 'codec': codec.toJson(), - 'track_type': trackType.toString(), - 'publish_option_id': publishOptionId, + 'trackType': trackType.toDTO().value, + 'publishOptionId': publishOptionId, }; } } @@ -303,8 +299,8 @@ extension SfuVideoSenderJsonX on SfuVideoSender { return { 'codec': codec.toJson(), 'layers': layers.map((layer) => layer.toJson()).toList(), - 'track_type': trackType.toString(), - 'publish_option_id': publishOptionId, + 'trackType': trackType.toDTO().value, + 'publishOptionId': publishOptionId, }; } } @@ -314,10 +310,11 @@ extension SfuPublishOptionsJsonX on SfuPublishOptions { return { 'id': id, 'codec': codec.toJson(), - 'track_type': trackType.toString(), - 'video_dimension': videoDimension?.toJson(), - 'max_spatial_layers': maxSpatialLayers, - 'max_temporal_layers': maxTemporalLayers, + 'trackType': trackType.toDTO().value, + 'videoDimension': videoDimension?.toJson(), + 'maxSpatialLayers': maxSpatialLayers, + 'maxTemporalLayers': maxTemporalLayers, + 'useSingleLayer': useSingleLayer, 'bitrate': bitrate, 'fps': fps, }; @@ -327,11 +324,11 @@ extension SfuPublishOptionsJsonX on SfuPublishOptions { extension SfuCodecJsonX on SfuCodec { Map toJson() { return { - 'payload_type': payloadType, + 'payloadType': payloadType, 'name': name, - 'fmtp_line': fmtpLine, - 'clock_rate': clockRate, - 'encoding_parameters': encodingParameters, + 'fmtp': fmtpLine, + 'clockRate': clockRate, + 'encodingParameters': encodingParameters, }; } } @@ -341,10 +338,10 @@ extension SfuVideoLayerSettingJsonX on SfuVideoLayerSetting { return { 'name': name, 'active': active, - 'max_bitrate': maxBitrate, - 'max_framerate': maxFramerate, - 'scale_resolution_down_by': scaleResolutionDownBy, - 'scalability_mode': scalabilityMode, + 'maxBitrate': maxBitrate, + 'maxFramerate': maxFramerate, + 'scaleResolutionDownBy': scaleResolutionDownBy, + 'scalabilityMode': scalabilityMode, 'codec': codec.toJson(), }; } @@ -362,7 +359,7 @@ extension RtcVideoDimensionJsonX on RtcVideoDimension { extension SfuGoAwayEventJsonX on SfuGoAwayEvent { Map toJson() { return { - 'go_away_reason': goAwayReason.toString(), + 'goAwayReason': goAwayReason.toString(), }; } } @@ -380,8 +377,8 @@ extension SfuErrorJsonX on SfuError { return { 'message': message, 'code': code.toString(), - 'should_retry': shouldRetry, - 'reconnect_strategy': reconnectStrategy.toString(), + 'shouldRetry': shouldRetry, + 'reconnectStrategy': reconnectStrategy.toDto().value, }; } } @@ -389,7 +386,28 @@ extension SfuErrorJsonX on SfuError { extension SfuCallEndedEventJsonX on SfuCallEndedEvent { Map toJson() { return { - 'call_ended_reason': callEndedReason.toString(), + 'callEndedReason': callEndedReason.toString(), + }; + } +} + +extension SfuConnectionQualityChangedEventJsonX + on SfuConnectionQualityChangedEvent { + Map toJson() { + return { + 'connectionQualityUpdates': connectionQualityUpdates + .map((codec) => codec.toJson()) + .toList(), + }; + } +} + +extension SfuConnectionQualityInfoJsonX on SfuConnectionQualityInfo { + Map toJson() { + return { + 'userId': userId, + 'sessionId': sessionId, + 'connectionQuality': connectionQuality.index, }; } } @@ -397,21 +415,19 @@ extension SfuCallEndedEventJsonX on SfuCallEndedEvent { extension SfuTrackPublishedEventJsonX on SfuTrackPublishedEvent { Map toJson() { return { - 'user_id': userId, - 'session_id': sessionId, - 'track_type': trackType.toString(), + 'userId': userId, + 'sessionId': sessionId, + 'trackType': trackType.toDTO().value, 'participant': { - 'user_id': participant.userId, - 'session_id': participant.sessionId, - 'user_name': participant.userName, - 'user_image': participant.userImage, - 'track_lookup_prefix': participant.trackLookupPrefix, - 'published_tracks': participant.publishedTracks + 'userId': participant.userId, + 'sessionId': participant.sessionId, + 'trackLookupPrefix': participant.trackLookupPrefix, + 'publishedTracks': participant.publishedTracks .map((track) => track.toString()) .toList(), - 'is_speaking': participant.isSpeaking, - 'is_dominant_speaker': participant.isDominantSpeaker, - 'audio_level': participant.audioLevel, + 'isSpeaking': participant.isSpeaking, + 'isDominantSpeaker': participant.isDominantSpeaker, + 'audioLevel': participant.audioLevel, 'roles': participant.roles, }, }; @@ -421,21 +437,19 @@ extension SfuTrackPublishedEventJsonX on SfuTrackPublishedEvent { extension SfuTrackUnpublishedEventJsonX on SfuTrackUnpublishedEvent { Map toJson() { return { - 'user_id': userId, - 'session_id': sessionId, - 'track_type': trackType.toString(), + 'userId': userId, + 'sessionId': sessionId, + 'trackType': trackType.toDTO().value, 'participant': { - 'user_id': participant.userId, - 'session_id': participant.sessionId, - 'user_name': participant.userName, - 'user_image': participant.userImage, - 'track_lookup_prefix': participant.trackLookupPrefix, - 'published_tracks': participant.publishedTracks + 'userId': participant.userId, + 'sessionId': participant.sessionId, + 'trackLookupPrefix': participant.trackLookupPrefix, + 'publishedTracks': participant.publishedTracks .map((track) => track.toString()) .toList(), - 'is_speaking': participant.isSpeaking, - 'is_dominant_speaker': participant.isDominantSpeaker, - 'audio_level': participant.audioLevel, + 'isSpeaking': participant.isSpeaking, + 'isDominantSpeaker': participant.isDominantSpeaker, + 'audioLevel': participant.audioLevel, 'roles': participant.roles, }, }; @@ -445,13 +459,13 @@ extension SfuTrackUnpublishedEventJsonX on SfuTrackUnpublishedEvent { extension PublishOptionX on sfu_models.PublishOption { Map toJson() { return { - 'track_type': trackType.toString(), + 'trackType': trackType.value, 'codec': codec.toJson(), 'bitrate': bitrate, 'fps': fps, - 'max_spatial_layers': maxSpatialLayers, - 'max_temporal_layers': maxTemporalLayers, - 'video_dimension': videoDimension.toJson(), + 'maxSpatialLayers': maxSpatialLayers, + 'maxTemporalLayers': maxTemporalLayers, + 'videoDimension': videoDimension.toJson(), 'id': id, }; } @@ -460,7 +474,7 @@ extension PublishOptionX on sfu_models.PublishOption { extension SubscribeOptionX on sfu_models.SubscribeOption { Map toJson() { return { - 'track_type': trackType.toString(), + 'trackType': trackType.value, 'codecs': codecs.map((codec) => codec.toJson()).toList(), }; } @@ -468,23 +482,18 @@ extension SubscribeOptionX on sfu_models.SubscribeOption { extension JoinRequestX on sfu_events.JoinRequest { Map toJson() { - final subscriberSdpParsed = subscriberSdp.isNotEmpty - ? parse(subscriberSdp) - : null; - final publisherSdpParsed = publisherSdp.isNotEmpty - ? parse(publisherSdp) - : null; return { 'token': token, - 'session_id': sessionId, - 'subscriber_sdp': subscriberSdpParsed, - 'client_details': clientDetails.toJson(), - 'reconnect_details': reconnectDetails.toJson(), - 'publisher_sdp': publisherSdpParsed, - 'preferred_publish_options': preferredPublishOptions + 'sessionId': sessionId, + 'unifiedSessionId': unifiedSessionId, + 'subscriberSdp': subscriberSdp, + 'publisherSdp': publisherSdp, + 'clientDetails': clientDetails.toJson(), + 'reconnectDetails': reconnectDetails.toJson(), + 'preferredPublishOptions': preferredPublishOptions .map((option) => option.toJson()) .toList(), - 'preferred_subscribe_options': preferredSubscribeOptions + 'preferredSubscribeOptions': preferredSubscribeOptions .map((option) => option.toJson()) .toList(), 'source': source.toString(), diff --git a/packages/stream_video/lib/src/webrtc/rtc_manager.dart b/packages/stream_video/lib/src/webrtc/rtc_manager.dart index b92834191..5851b935c 100644 --- a/packages/stream_video/lib/src/webrtc/rtc_manager.dart +++ b/packages/stream_video/lib/src/webrtc/rtc_manager.dart @@ -999,6 +999,10 @@ extension PublisherRtcManager on RtcManager { Future> setTrackFacingMode({ required FacingMode facingMode, }) async { + _logger.d( + () => '[setTrackFacingMode] facingMode: $facingMode', + ); + final track = getPublisherTrackByType(SfuTrackType.video); if (track == null) return Result.error('Track not found'); @@ -1025,6 +1029,10 @@ extension PublisherRtcManager on RtcManager { Future> setCameraVideoParameters({ required RtcVideoParameters params, }) async { + _logger.d( + () => '[setCameraVideoParameters] params: $params', + ); + final track = getPublisherTrackByType(SfuTrackType.video); if (track == null) { diff --git a/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device.dart b/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device.dart index e7001bd0c..4f88e3798 100644 --- a/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device.dart +++ b/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device.dart @@ -44,6 +44,15 @@ class RtcMediaDevice with EquatableMixin { @override List get props => [id, kind, groupId, label]; + Map toJson() { + return { + 'id': id, + 'label': label, + 'groupId': groupId, + 'kind': kind.alias, + }; + } + /// The set of external audio ports that are considered external outputs static const Set iOSExternalPorts = { 'bluetoothA2DP', diff --git a/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device_notifier.dart b/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device_notifier.dart index 237c8cc44..086ed5838 100644 --- a/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device_notifier.dart +++ b/packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device_notifier.dart @@ -1,8 +1,11 @@ import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as rtc; import '../../../open_api/video/coordinator/api.dart'; +import '../../call/stats/tracer.dart'; import '../../errors/video_error_composer.dart'; import '../../platform_detector/platform_detector.dart'; import '../../utils/extensions.dart'; @@ -40,6 +43,13 @@ class RtcMediaDeviceNotifier { Stream> get onDeviceChange => _devicesController.stream; final _devicesController = BehaviorSubject>(); + final _tracer = Tracer(null); + + @internal + TraceSlice getTrace() { + return _tracer.take(); + } + /// Allows to handle call interruption callbacks. /// [onInterruptionStart] is called when the call interruption begins. /// [onInterruptionEnd] is called when the call interruption ends. @@ -143,6 +153,11 @@ class RtcMediaDeviceNotifier { ), ]; + _tracer.trace( + 'navigator.mediaDevices.enumeratedevices', + mediaDevices.map((device) => device.toJson()).toList(), + ); + _devicesController.add(mediaDevices); if (kind != null) { @@ -180,12 +195,14 @@ class RtcMediaDeviceNotifier { /// This does not affect the microphone or remote track subscriptions. /// Use as a global "mute all sounds" toggle or when the app goes to background. Future pauseAudioPlayout() { + _tracer.trace('navigator.mediaDevices.pauseAudioPlayout', null); return rtc.Helper.pauseAudioPlayout(); } /// Resumes audio output (playout) muted via [pauseAudioPlayout]. /// Does not change microphone state or remote track subscriptions. Future resumeAudioPlayout() { + _tracer.trace('navigator.mediaDevices.resumeAudioPlayout', null); return rtc.Helper.resumeAudioPlayout(); } @@ -195,6 +212,7 @@ class RtcMediaDeviceNotifier { /// To ensure you receive `onInterruptionEnd`, explicitly call /// [resumeAudioPlayout] (e.g., when the app resumes from background). Future regainAndroidAudioFocus() { + _tracer.trace('navigator.mediaDevices.regainAndroidAudioFocus', null); return rtc.Helper.regainAndroidAudioFocus(); } } diff --git a/packages/stream_video/test/src/call/fixtures/call_test_helpers.dart b/packages/stream_video/test/src/call/fixtures/call_test_helpers.dart index 17bf8b58a..de373be8e 100644 --- a/packages/stream_video/test/src/call/fixtures/call_test_helpers.dart +++ b/packages/stream_video/test/src/call/fixtures/call_test_helpers.dart @@ -303,10 +303,12 @@ MockCallSession setupMockCallSession() { when( callSession.getTrace, ).thenReturn( - TraceSlice( - snapshot: [], - rollback: () {}, - ), + [ + TraceSlice( + snapshot: [], + rollback: () {}, + ), + ], ); when(