diff --git a/lib/src/options.dart b/lib/src/options.dart index be7b60d..33a8e6e 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -6,6 +6,7 @@ import 'track/options.dart'; import 'track/track.dart'; import 'types/other.dart'; import 'types/video_encoding.dart'; +import 'types/video_parameters.dart'; /// Options used when connecting to the server. class ConnectOptions { @@ -88,9 +89,15 @@ class VideoPublishOptions { /// Defaults to true. final bool simulcast; + final List videoSimulcastLayers; + + final List screenShareSimulcastLayers; + const VideoPublishOptions({ this.videoEncoding, this.simulcast = true, + this.videoSimulcastLayers = const [], + this.screenShareSimulcastLayers = const [], }); @override diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index f476098..7dfd5d7 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -23,7 +23,7 @@ class CameraCaptureOptions extends VideoCaptureOptions { const CameraCaptureOptions({ this.cameraPosition = CameraPosition.front, - VideoParameters params = VideoParametersPresets.h720_169, + VideoParameters params = VideoParametersPresets.h540_169, }) : super(params: params); CameraCaptureOptions.from({required VideoCaptureOptions captureOptions}) @@ -51,7 +51,7 @@ class CameraCaptureOptions extends VideoCaptureOptions { /// Options used when creating a [LocalVideoTrack] that captures the screen. class ScreenShareCaptureOptions extends VideoCaptureOptions { const ScreenShareCaptureOptions({ - VideoParameters params = VideoParametersPresets.h720_169, + VideoParameters params = VideoParametersPresets.screenShareH720FPS15, }) : super(params: params); ScreenShareCaptureOptions.from({required VideoCaptureOptions captureOptions}) @@ -71,7 +71,7 @@ abstract class VideoCaptureOptions extends LocalTrackOptions { final VideoParameters params; const VideoCaptureOptions({ - this.params = VideoParametersPresets.h720_169, + this.params = VideoParametersPresets.h540_169, }); @override diff --git a/lib/src/types/video_dimensions.dart b/lib/src/types/video_dimensions.dart index 1ab7d16..2c98b9c 100644 --- a/lib/src/types/video_dimensions.dart +++ b/lib/src/types/video_dimensions.dart @@ -40,6 +40,12 @@ class VideoDimensions { } extension VideoDimensionsHelpers on VideoDimensions { + // aspect ratios + static const aspect169 = 16.0 / 9.0; + static const aspect43 = 4.0 / 3.0; + + double aspect() => width > height ? width / height : height / width; + /// Returns the larger value int max() => math.max(width, height); diff --git a/lib/src/types/video_parameters.dart b/lib/src/types/video_parameters.dart index 069d484..3213d69 100644 --- a/lib/src/types/video_parameters.dart +++ b/lib/src/types/video_parameters.dart @@ -56,6 +56,18 @@ class VideoParameters implements Comparable { } extension VideoParametersPresets on VideoParameters { + // 16:9 default + static final List defaultSimulcast169 = [ + h180_169, + h360_169, + ]; + + // 4:3 default + static final List defaultSimulcast43 = [ + h180_43, + h360_43, + ]; + // all 16:9 presets static final List all169 = [ h90_169, diff --git a/lib/src/utils.dart b/lib/src/utils.dart index d67c79e..e44fe73 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; @@ -34,6 +35,9 @@ typedef RetryCondition = bool Function( // Collection of state-less static methods class Utils { + // order of rids + static final videoRids = ['q', 'h', 'f']; + /// Returns a [Future] that will retry [future] while it throws /// for a maximum of [tries] times with [delay] in between. /// If all the attempts throws, the future will throw a [List] of the @@ -191,17 +195,62 @@ class Utils { required bool isScreenShare, required VideoDimensions dimensions, }) { - if (isScreenShare) return VideoParametersPresets.allScreenShare; + if (isScreenShare) { + return VideoParametersPresets.allScreenShare; + } - final double aspect = dimensions.width > dimensions.height - ? dimensions.width / dimensions.height - : dimensions.height / dimensions.width; - if ((aspect - 16.0 / 9.0).abs() < (aspect - 4.0 / 3.0).abs()) { + final a = dimensions.aspect(); + if ((a - VideoDimensionsHelpers.aspect169).abs() < + (a - VideoDimensionsHelpers.aspect43).abs()) { return VideoParametersPresets.all169; } + return VideoParametersPresets.all43; } + static List _computeDefaultScreenShareSimulcastParams({ + required VideoParameters original, + }) { + final layers = [ + rtc.RTCRtpEncoding(scaleResolutionDownBy: 2, maxFramerate: 3) + ]; + return layers.map((e) { + final scale = e.scaleResolutionDownBy ?? 1; + final fps = e.maxFramerate ?? 3; + + return VideoParameters( + dimensions: VideoDimensions((original.dimensions.width / scale).floor(), + (original.dimensions.height / scale).floor()), + encoding: VideoEncoding( + maxBitrate: math.max( + 150 * 1000, + (original.encoding.maxBitrate / + (math.pow(scale, 2) * + (original.encoding.maxFramerate / fps))) + .floor(), + ), + maxFramerate: fps, + ), + ); + }).toList(); + } + + static List _computeDefaultSimulcastParams({ + required bool isScreenShare, + required VideoParameters original, + }) { + if (isScreenShare) { + return _computeDefaultScreenShareSimulcastParams(original: original); + } + final a = original.dimensions.aspect(); + if ((a - VideoDimensionsHelpers.aspect169).abs() < + (a - VideoDimensionsHelpers.aspect43).abs()) { + return VideoParametersPresets.defaultSimulcast169; + } + + return VideoParametersPresets.defaultSimulcast43; + } + static VideoEncoding _findAppropriateEncoding({ required bool isScreenShare, required VideoDimensions dimensions, @@ -221,8 +270,6 @@ class Utils { return result; } - static final videoRids = ['q', 'h', 'f']; - @internal static List encodingsFromPresets( VideoDimensions dimensions, { @@ -233,11 +280,12 @@ class Utils { if (i >= videoRids.length) { return; } + final size = dimensions.min(); final rid = videoRids[i]; result.add(e.encoding.toRTCRtpEncoding( rid: rid, - scaleResolutionDownBy: findEvenScaleDownBy(dimensions, e.dimensions), + scaleResolutionDownBy: math.max(1, size / e.dimensions.min()), )); }); return result; @@ -279,12 +327,10 @@ class Utils { VideoEncoding? videoEncoding = options.videoEncoding; - final useSimulcast = !isScreenShare && options.simulcast; - - if ((videoEncoding == null && !useSimulcast) || dimensions == null) { + if ((videoEncoding == null && !options.simulcast) || dimensions == null) { // don't set encoding when we are not simulcasting and user isn't restricting // encoding parameters - return null; + return [rtc.RTCRtpEncoding()]; } final presets = _presetsForDimensions( @@ -299,34 +345,48 @@ class Utils { dimensions: dimensions, presets: presets, ); + logger.fine('using video encoding', videoEncoding); } - // Not simulcast - if (!useSimulcast) return [videoEncoding.toRTCRtpEncoding()]; - - final VideoParameters lowPreset = presets.first; - VideoParameters? midPreset; - if (presets.length > 1) { - midPreset = presets[1]; + if (!options.simulcast) { + // not using simulcast + return [videoEncoding.toRTCRtpEncoding()]; } + final original = VideoParameters( dimensions: dimensions, encoding: videoEncoding, ); + final userParams = isScreenShare + ? options.screenShareSimulcastLayers + : options.videoSimulcastLayers; + + final params = (userParams.isNotEmpty + ? userParams + : _computeDefaultSimulcastParams( + isScreenShare: isScreenShare, original: original)) + .sorted(); + + final VideoParameters lowPreset = params.first; + VideoParameters? midPreset; + if (params.length > 1) { + midPreset = params[1]; + } + final size = dimensions.max(); - List computedPresets = [original]; + List computedParams = [original]; if (size >= 960 && midPreset != null) { - computedPresets = [lowPreset, midPreset, original]; - } else if (size >= 500) { - computedPresets = [lowPreset, original]; + computedParams = [lowPreset, midPreset, original]; + } else if (size >= 480) { + computedParams = [lowPreset, original]; } return encodingsFromPresets( dimensions, - presets: computedPresets, + presets: computedParams, ); }