diff --git a/.changes/maintain-video-quality-for-live-streaming b/.changes/maintain-video-quality-for-live-streaming new file mode 100644 index 000000000..fbb40f1c1 --- /dev/null +++ b/.changes/maintain-video-quality-for-live-streaming @@ -0,0 +1 @@ +patch type="fixed" "Maintain video quality for live streaming via new liveStreaming capture option" diff --git a/example/lib/pages/prejoin.dart b/example/lib/pages/prejoin.dart index 8579db86a..9334d31b9 100644 --- a/example/lib/pages/prejoin.dart +++ b/example/lib/pages/prejoin.dart @@ -60,6 +60,8 @@ class _PreJoinPageState extends State { MediaDevice? _selectedVideoDevice; MediaDevice? _selectedAudioDevice; VideoParameters _selectedVideoParameters = VideoParametersPresets.h720_169; + int _videoBitrate = 3 * 1000 * 1000; + bool _liveStreaming = true; @override void initState() { @@ -183,6 +185,7 @@ class _PreJoinPageState extends State { _videoTrack = await LocalVideoTrack.createCameraTrack(CameraCaptureOptions( deviceId: _selectedVideoDevice!.deviceId, params: _selectedVideoParameters, + liveStreaming: _liveStreaming, )); await _videoTrack!.start(); } @@ -203,8 +206,8 @@ class _PreJoinPageState extends State { try { //create new room - const cameraEncoding = VideoEncoding( - maxBitrate: 5 * 1000 * 1000, + final cameraEncoding = VideoEncoding( + maxBitrate: _videoBitrate, maxFramerate: 30, ); @@ -449,6 +452,57 @@ class _PreJoinPageState extends State { ), ), ), + if (_enableVideo) + Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Live Streaming:'), + Switch( + value: _liveStreaming, + onChanged: (value) async { + setState(() { + _liveStreaming = value; + }); + await _changeLocalVideoTrack(); + if (mounted) setState(() {}); + }, + ), + ], + ), + ), + if (_enableVideo) + Padding( + padding: const EdgeInsets.only(bottom: 25), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Video Bitrate:'), + SizedBox( + width: 140, + height: 40, + child: TextFormField( + initialValue: (_videoBitrate ~/ 1000).toString(), + keyboardType: TextInputType.number, + textAlign: TextAlign.right, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + border: OutlineInputBorder(), + suffixText: 'kbps', + ), + onChanged: (value) { + final kbps = int.tryParse(value); + if (kbps != null && kbps > 0) { + _videoBitrate = kbps * 1000; + } + }, + ), + ), + ], + ), + ), Padding( padding: const EdgeInsets.only(bottom: 5), child: Row( diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index e785b72fa..5611dd212 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -594,6 +594,10 @@ class LocalParticipant extends Participant { } DegradationPreference getDefaultDegradationPreference(LocalVideoTrack track) { + // keep both framerate and resolution for live streaming. + if (track.currentOptions.liveStreaming == true) { + return DegradationPreference.maintainFramerateAndResolution; + } // a few of reasons we have different default paths: // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced` diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index d5bfc5bbf..8bc2648db 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -62,12 +62,14 @@ class CameraCaptureOptions extends VideoCaptureOptions { double? maxFrameRate, VideoParameters params = VideoParametersPresets.h720_169, this.stopCameraCaptureOnMute = true, + bool? liveStreaming, TrackProcessor? processor, }) : super( params: params, deviceId: deviceId, maxFrameRate: maxFrameRate, processor: processor, + liveStreaming: liveStreaming, ); CameraCaptureOptions.from({required VideoCaptureOptions captureOptions}) @@ -231,11 +233,15 @@ abstract class VideoCaptureOptions extends LocalTrackOptions { /// A processor to apply to the video track. final TrackProcessor? processor; + /// Maintain high framerate and bitrate. + final bool? liveStreaming; + const VideoCaptureOptions({ this.params = VideoParametersPresets.h540_169, this.deviceId, this.maxFrameRate, this.processor, + this.liveStreaming, }); @override