diff --git a/docusaurus/docs/Flutter/02-tutorials/03-livestreaming.mdx b/docusaurus/docs/Flutter/02-tutorials/03-livestreaming.mdx index 087171049..4c1d1c2a3 100644 --- a/docusaurus/docs/Flutter/02-tutorials/03-livestreaming.mdx +++ b/docusaurus/docs/Flutter/02-tutorials/03-livestreaming.mdx @@ -350,15 +350,15 @@ With these two pieces of information, we can update the settings in OBS then sel ### Viewing a livestream (HLS) The final piece of livestreaming using Stream is support for HLS or HTTP Live Streaming. HLS unlike WebRTC based streaming tends to have a 10 to 20 second delay but offers video buffering under poor network condition. -To enable HLS support, your call must first be placed into “broadcasting” mode using the `call.startBroadcasting()` method. +To enable HLS support, your `call` must first be placed into “broadcasting” mode using the `call.startHLS()` method. -Next, we can obtain the HLS URL by querying the `hlsPlaylistURL` from the result of our create call function: +Next, we can obtain the HLS URL by querying the `hlsPlaylistURL` from `call.state`: ```dart -final result = await call.getOrCreateCall(); +final result = await call.startHLS(); if (result.isSuccess) { - final url = result.getDataOrNull()!.data.metadata.details.hlsPlaylistUrl; + final url = call.state.value.egress.hlsPlaylistUrl; ... } diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index ef3e3a244..8e255c3d1 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -933,17 +933,21 @@ class Call { return result; } - Future> startBroadcasting() async { + /// Starts the broadcasting of the call. + Future> startHLS() async { final result = await _permissionsManager.startBroadcasting(); if (result.isSuccess) { - _stateManager.setCallBroadcasting(isBroadcasting: true); + _stateManager.setCallBroadcasting( + isBroadcasting: true, + hlsPlaylistUrl: result.getDataOrNull() + ); } - return result; } - Future> stopBroadcasting() async { + /// Stops the broadcasting of the call. + Future> stopHLS() async { final result = await _permissionsManager.stopBroadcasting(); if (result.isSuccess) { @@ -1106,6 +1110,7 @@ class Call { return result; } + /// Starts the livestreaming of the call. Future> goLive({ bool? startHls, bool? startRecording, @@ -1125,6 +1130,7 @@ class Call { return result; } + /// Stops the livestreaming of the call. Future> stopLive() async { final result = await _coordinatorClient.stopLive(callCid); diff --git a/packages/stream_video/lib/src/call/permissions/permissions_manager.dart b/packages/stream_video/lib/src/call/permissions/permissions_manager.dart index d5f1c0925..8f767f1a1 100644 --- a/packages/stream_video/lib/src/call/permissions/permissions_manager.dart +++ b/packages/stream_video/lib/src/call/permissions/permissions_manager.dart @@ -155,7 +155,7 @@ class PermissionsManager { return result; } - Future> startBroadcasting() async { + Future> startBroadcasting() async { if (!hasPermission(CallPermission.startBroadcastCall)) { _logger.w(() => '[startBroadcasting] rejected (no permission)'); return Result.error('Cannot start broadcasting (no permission)'); diff --git a/packages/stream_video/lib/src/call/state/mixins/state_call_actions_mixin.dart b/packages/stream_video/lib/src/call/state/mixins/state_call_actions_mixin.dart index c98fa52db..442a1dcfd 100644 --- a/packages/stream_video/lib/src/call/state/mixins/state_call_actions_mixin.dart +++ b/packages/stream_video/lib/src/call/state/mixins/state_call_actions_mixin.dart @@ -19,10 +19,22 @@ mixin StateCallActionsMixin on StateNotifier { ); } - void setCallBroadcasting({required bool isBroadcasting}) { - _logger.e(() => '[setCallBroadcasting] isBroadcasting:$isBroadcasting'); + void setCallBroadcasting({ + required bool isBroadcasting, + String? hlsPlaylistUrl, + }) { + _logger.e( + () => '[setCallBroadcasting] isBroadcasting:$isBroadcasting' + ', hlsPlaylistUrl: $hlsPlaylistUrl', + ); + final curEgress = state.egress; + final newEgress = curEgress.copyWith( + hlsPlaylistUrl: hlsPlaylistUrl, + ); + state = state.copyWith( isBroadcasting: isBroadcasting, + egress: newEgress, ); } } diff --git a/packages/stream_video/lib/src/call/state/mixins/state_lifecycle_mixin.dart b/packages/stream_video/lib/src/call/state/mixins/state_lifecycle_mixin.dart index 46ed6d088..7ef25af17 100644 --- a/packages/stream_video/lib/src/call/state/mixins/state_lifecycle_mixin.dart +++ b/packages/stream_video/lib/src/call/state/mixins/state_lifecycle_mixin.dart @@ -81,6 +81,7 @@ mixin StateLifecycleMixin on StateNotifier { status: stage.data.toCallStatus(state: state, ringing: ringing), createdByUserId: stage.data.metadata.details.createdBy.id, settings: stage.data.metadata.settings, + egress: stage.data.metadata.details.egress, ownCapabilities: stage.data.metadata.details.ownCapabilities.toList(), callParticipants: stage.data.metadata.toCallParticipants(state), ); @@ -95,6 +96,7 @@ mixin StateLifecycleMixin on StateNotifier { createdByUserId: stage.data.metadata.details.createdBy.id, isRingingFlow: stage.data.ringing, settings: stage.data.metadata.settings, + egress: stage.data.metadata.details.egress, ownCapabilities: stage.data.metadata.details.ownCapabilities.toList(), callParticipants: stage.data.metadata.toCallParticipants(state), ); @@ -118,6 +120,7 @@ mixin StateLifecycleMixin on StateNotifier { status: status, createdByUserId: stage.data.metadata.details.createdBy.id, settings: stage.data.metadata.settings, + egress: stage.data.metadata.details.egress, ownCapabilities: stage.data.metadata.details.ownCapabilities.toList(), callParticipants: stage.data.metadata.toCallParticipants(state), ); diff --git a/packages/stream_video/lib/src/call_state.dart b/packages/stream_video/lib/src/call_state.dart index a2e3ef623..6ed1dac63 100644 --- a/packages/stream_video/lib/src/call_state.dart +++ b/packages/stream_video/lib/src/call_state.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'models/call_cid.dart'; +import 'models/call_egress.dart'; import 'models/call_metadata.dart'; import 'models/call_participant_state.dart'; import 'models/call_permission.dart'; @@ -29,6 +30,7 @@ class CallState extends Equatable { isTranscribing: false, isBackstage: false, settings: const CallSettings(), + egress: const CallEgress(), videoInputDevice: null, audioInputDevice: null, audioOutputDevice: null, @@ -55,6 +57,7 @@ class CallState extends Equatable { isTranscribing: metadata.details.transcribing, isBackstage: metadata.details.backstage, settings: metadata.settings, + egress: metadata.details.egress, videoInputDevice: null, audioInputDevice: null, audioOutputDevice: null, @@ -80,6 +83,7 @@ class CallState extends Equatable { required this.isTranscribing, required this.isBackstage, required this.settings, + required this.egress, required this.ownCapabilities, required this.callParticipants, required this.videoInputDevice, @@ -94,6 +98,7 @@ class CallState extends Equatable { final String sessionId; final CallStatus status; final CallSettings settings; + final CallEgress egress; final bool isRecording; final bool isBroadcasting; final bool isTranscribing; @@ -130,6 +135,7 @@ class CallState extends Equatable { bool? isTranscribing, bool? isBackstage, CallSettings? settings, + CallEgress? egress, RtcMediaDevice? videoInputDevice, RtcMediaDevice? audioInputDevice, RtcMediaDevice? audioOutputDevice, @@ -148,6 +154,7 @@ class CallState extends Equatable { isTranscribing: isTranscribing ?? this.isTranscribing, isBackstage: isBackstage ?? this.isBackstage, settings: settings ?? this.settings, + egress: egress ?? this.egress, videoInputDevice: videoInputDevice ?? this.videoInputDevice, audioInputDevice: audioInputDevice ?? this.audioInputDevice, audioOutputDevice: audioOutputDevice ?? this.audioOutputDevice, @@ -168,6 +175,7 @@ class CallState extends Equatable { isBroadcasting, isBackstage, settings, + egress, videoInputDevice, audioInputDevice, audioOutputDevice, @@ -180,7 +188,8 @@ class CallState extends Equatable { return 'CallState(status: $status, currentUserId: $currentUserId,' ' callCid: $callCid, createdByUserId: $createdByUserId,' ' sessionId: $sessionId, isRecording: $isRecording,' - ' settings: $settings, videoInputDevice: $videoInputDevice,' + ' settings: $settings, egress: $egress, ' + ' videoInputDevice: $videoInputDevice,' ' audioInputDevice: $audioInputDevice,' ' audioOutputDevice: $audioOutputDevice,' ' ownCapabilities: $ownCapabilities,' diff --git a/packages/stream_video/lib/src/coordinator/coordinator_client.dart b/packages/stream_video/lib/src/coordinator/coordinator_client.dart index 900e0c4c2..aab625fdb 100644 --- a/packages/stream_video/lib/src/coordinator/coordinator_client.dart +++ b/packages/stream_video/lib/src/coordinator/coordinator_client.dart @@ -115,7 +115,7 @@ abstract class CoordinatorClient { Future> stopRecording(StreamCallCid callCid); /// Starts broadcasting for the call described by the given [callCid]. - Future> startBroadcasting(StreamCallCid callCid); + Future> startBroadcasting(StreamCallCid callCid); /// Stops broadcasting for the call described by the given [callCid]. Future> stopBroadcasting(StreamCallCid callCid); diff --git a/packages/stream_video/lib/src/coordinator/open_api/coordinator_client_open_api.dart b/packages/stream_video/lib/src/coordinator/open_api/coordinator_client_open_api.dart index 9f920dfba..b6d842c20 100644 --- a/packages/stream_video/lib/src/coordinator/open_api/coordinator_client_open_api.dart +++ b/packages/stream_video/lib/src/coordinator/open_api/coordinator_client_open_api.dart @@ -469,10 +469,12 @@ class CoordinatorClientOpenApi extends CoordinatorClient { } @override - Future> startBroadcasting(StreamCallCid callCid) async { + Future> startBroadcasting(StreamCallCid callCid) async { try { - await defaultApi.startBroadcasting(callCid.type, callCid.id); - return const Result.success(none); + final result = await defaultApi + .startBroadcasting(callCid.type, callCid.id) + .then((it) => it?.playlistUrl); + return Result.success(result); } catch (e, stk) { return Result.failure(VideoErrors.compose(e, stk)); } diff --git a/packages/stream_video/lib/src/coordinator/open_api/open_api_extensions.dart b/packages/stream_video/lib/src/coordinator/open_api/open_api_extensions.dart index def78d2d5..c63131926 100644 --- a/packages/stream_video/lib/src/coordinator/open_api/open_api_extensions.dart +++ b/packages/stream_video/lib/src/coordinator/open_api/open_api_extensions.dart @@ -2,6 +2,7 @@ import '../../../../open_api/video/coordinator/api.dart' as open; import '../../logger/stream_log.dart'; import '../../models/call_cid.dart'; import '../../models/call_credentials.dart'; +import '../../models/call_egress.dart'; import '../../models/call_metadata.dart'; import '../../models/call_permission.dart'; import '../../models/call_reaction.dart'; @@ -94,7 +95,7 @@ extension EnvelopeExt on open.CallResponse { return CallMetadata( cid: StreamCallCid(cid: cid), details: CallDetails( - hlsPlaylistUrl: egress.hls?.playlistUrl ?? '', + egress: egress.toCallEgress(), createdBy: createdBy.toCallUser(), team: team ?? '', ownCapabilities: @@ -125,6 +126,25 @@ extension EnvelopeExt on open.CallResponse { } } +extension EgressExt on open.EgressResponse { + CallEgress toCallEgress() { + return CallEgress( + hlsPlaylistUrl: hls?.playlistUrl, + rtmps: rtmps.map((it) => it.toCallRtmp()).toList(), + ); + } +} + +extension EgressRtmpExt on open.EgressRTMPResponse { + CallEgressRtmp toCallRtmp() { + return CallEgressRtmp( + name: name, + streamKey: streamKey, + url: url, + ); + } +} + extension CallSettingsExt on open.CallSettingsResponse { // TODO open api provides wider settings options CallSettings toCallSettings() { diff --git a/packages/stream_video/lib/src/coordinator/retry/coordinator_client_retry.dart b/packages/stream_video/lib/src/coordinator/retry/coordinator_client_retry.dart index 4a6c87d73..5b72844f0 100644 --- a/packages/stream_video/lib/src/coordinator/retry/coordinator_client_retry.dart +++ b/packages/stream_video/lib/src/coordinator/retry/coordinator_client_retry.dart @@ -354,7 +354,7 @@ class CoordinatorClientRetry extends CoordinatorClient { } @override - Future> startBroadcasting(StreamCallCid callCid) { + Future> startBroadcasting(StreamCallCid callCid) { return _retryManager.execute( () => _delegate.startBroadcasting(callCid), (error, nextAttemptDelay) async { diff --git a/packages/stream_video/lib/src/models/call_egress.dart b/packages/stream_video/lib/src/models/call_egress.dart new file mode 100644 index 000000000..054b43580 --- /dev/null +++ b/packages/stream_video/lib/src/models/call_egress.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; + +class CallEgress with EquatableMixin { + const CallEgress({ + this.hlsPlaylistUrl, + this.rtmps = const [], + }); + + final String? hlsPlaylistUrl; + final List rtmps; + + @override + bool? get stringify => true; + + @override + List get props => [hlsPlaylistUrl]; + + /// Returns a copy of this [CallEgress] with the given fields + /// replaced with the new values. + CallEgress copyWith({ + String? hlsPlaylistUrl, + List? rtmps, + }) { + return CallEgress( + hlsPlaylistUrl: hlsPlaylistUrl ?? this.hlsPlaylistUrl, + rtmps: rtmps ?? this.rtmps, + ); + } +} + +class CallEgressRtmp { + const CallEgressRtmp({ + required this.name, + required this.streamKey, + required this.url, + }); + + final String name; + + final String streamKey; + + final String url; + + /// Returns a copy of this [CallEgressRtmp] with the given fields + /// replaced with the new values. + CallEgressRtmp copyWith({ + String? name, + String? streamKey, + String? url, + }) { + return CallEgressRtmp( + name: name ?? this.name, + streamKey: streamKey ?? this.streamKey, + url: url ?? this.url, + ); + } +} diff --git a/packages/stream_video/lib/src/models/call_metadata.dart b/packages/stream_video/lib/src/models/call_metadata.dart index 03ee7b4fb..5513cd7b0 100644 --- a/packages/stream_video/lib/src/models/call_metadata.dart +++ b/packages/stream_video/lib/src/models/call_metadata.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'call_cid.dart'; +import 'call_egress.dart'; import 'call_permission.dart'; import 'call_settings.dart'; @@ -34,7 +35,6 @@ class CallMetadata with EquatableMixin { @immutable class CallDetails with EquatableMixin { const CallDetails({ - required this.hlsPlaylistUrl, required this.createdBy, required this.team, required this.ownCapabilities, @@ -43,6 +43,7 @@ class CallDetails with EquatableMixin { required this.recording, required this.backstage, required this.transcribing, + required this.egress, required this.custom, required this.rtmpIngress, this.startsAt, @@ -51,7 +52,6 @@ class CallDetails with EquatableMixin { this.updatedAt, }); - final String hlsPlaylistUrl; final CallUser createdBy; final String team; final Iterable ownCapabilities; @@ -60,6 +60,7 @@ class CallDetails with EquatableMixin { final bool recording; final bool backstage; final bool transcribing; + final CallEgress egress; final Map custom; final String rtmpIngress; final DateTime? startsAt; @@ -69,7 +70,6 @@ class CallDetails with EquatableMixin { @override List get props => [ - hlsPlaylistUrl, createdBy, ownCapabilities, blockedUserIds, @@ -77,6 +77,7 @@ class CallDetails with EquatableMixin { recording, backstage, transcribing, + egress, custom, rtmpIngress, startsAt, diff --git a/packages/stream_video/lib/src/models/models.dart b/packages/stream_video/lib/src/models/models.dart index 381878ecc..43ccb5428 100644 --- a/packages/stream_video/lib/src/models/models.dart +++ b/packages/stream_video/lib/src/models/models.dart @@ -2,6 +2,7 @@ export 'call_cid.dart'; export 'call_created_data.dart'; export 'call_credentials.dart'; export 'call_device.dart'; +export 'call_egress.dart'; export 'call_joined_data.dart'; export 'call_metadata.dart'; export 'call_participant_state.dart'; diff --git a/packages/stream_video_push_notification/test/stream_video_push_notification_test.dart b/packages/stream_video_push_notification/test/stream_video_push_notification_test.dart index d7a4af0ee..82aa91d29 100644 --- a/packages/stream_video_push_notification/test/stream_video_push_notification_test.dart +++ b/packages/stream_video_push_notification/test/stream_video_push_notification_test.dart @@ -21,7 +21,6 @@ Future main() async { metadata: CallMetadata( cid: streamCallCid, details: const CallDetails( - hlsPlaylistUrl: '', createdBy: CallUser( id: "jc", name: "JC M", @@ -37,6 +36,9 @@ Future main() async { transcribing: false, custom: {}, rtmpIngress: '', + egress: CallEgress( + hlsPlaylistUrl: '', + ), ), settings: const CallSettings(), users: const {