|
1 | | -import 'dart:math' as math; |
| 1 | +// import 'dart:math' as math; |
2 | 2 |
|
| 3 | +import 'dart:math'; |
| 4 | + |
| 5 | +import 'package:collection/collection.dart'; |
3 | 6 | import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; |
4 | 7 |
|
5 | | -import '../logger/stream_log.dart'; |
6 | 8 | import 'model/rtc_video_dimension.dart'; |
7 | 9 | import 'model/rtc_video_parameters.dart'; |
8 | 10 |
|
9 | | -// 16:9 default |
10 | | -const _defaultSimulcast_16x9 = { |
11 | | - 'f': RtcVideoParametersPresets.h720_16x9, |
12 | | - 'h': RtcVideoParametersPresets.h360_16x9, |
13 | | - 'q': RtcVideoParametersPresets.h180_16x9, |
14 | | -}; |
15 | | - |
16 | | -// 4:3 default |
17 | | -const _defaultSimulcast_4x3 = { |
18 | | - 'f': RtcVideoParametersPresets.h720_4x3, |
19 | | - 'h': RtcVideoParametersPresets.h360_4x3, |
20 | | - 'q': RtcVideoParametersPresets.h180_4x3, |
21 | | -}; |
22 | | - |
23 | | -List<rtc.RTCRtpEncoding> computeVideoEncodings({ |
24 | | - required RtcVideoDimension dimension, |
25 | | - required bool isScreenShare, |
| 11 | +List<rtc.RTCRtpEncoding> findOptimalVideoLayers({ |
| 12 | + required RtcVideoDimension dimensions, |
| 13 | + RtcVideoParameters targetResolution = RtcVideoParametersPresets.h720_16x9, |
26 | 14 | }) { |
27 | | - final presets = _presetsForDimension( |
28 | | - isScreenShare: isScreenShare, |
29 | | - dimension: dimension, |
| 15 | + final optimalVideoLayers = <rtc.RTCRtpEncoding>[]; |
| 16 | + |
| 17 | + final maxBitrate = getComputedMaxBitrate( |
| 18 | + targetResolution, |
| 19 | + dimensions.width, |
| 20 | + dimensions.height, |
30 | 21 | ); |
31 | | - presets.forEach((rid, preset) { |
32 | | - streamLog.v( |
33 | | - 'SV:RtcManager', |
34 | | - () => '[publishVideoTrack] #$rid; preset: $preset', |
| 22 | + |
| 23 | + var downscaleFactor = 1; |
| 24 | + for (final rid in ['f', 'h', 'q'].reversed) { |
| 25 | + optimalVideoLayers.insert( |
| 26 | + 0, |
| 27 | + rtc.RTCRtpEncoding( |
| 28 | + rid: rid, |
| 29 | + scaleResolutionDownBy: downscaleFactor.toDouble(), |
| 30 | + maxFramerate: 30, |
| 31 | + maxBitrate: (maxBitrate / downscaleFactor).round(), |
| 32 | + ), |
35 | 33 | ); |
36 | | - }); |
37 | 34 |
|
38 | | - return encodingsFromPresets(dimension, presets: presets); |
| 35 | + downscaleFactor *= 2; |
| 36 | + } |
| 37 | + |
| 38 | + return withSimulcastConstraints( |
| 39 | + dimensions: dimensions, |
| 40 | + optimalVideoLayers: optimalVideoLayers, |
| 41 | + ); |
39 | 42 | } |
40 | 43 |
|
41 | | -Map<String, RtcVideoParameters> _presetsForDimension({ |
42 | | - required bool isScreenShare, |
43 | | - required RtcVideoDimension dimension, |
44 | | -}) { |
45 | | - if (isScreenShare) { |
46 | | - return _defaultSimulcast_16x9; |
| 44 | +int getComputedMaxBitrate( |
| 45 | + RtcVideoParameters targetResolution, |
| 46 | + int currentWidth, |
| 47 | + int currentHeight, |
| 48 | +) { |
| 49 | + // if the current resolution is lower than the target resolution, |
| 50 | + // we want to proportionally reduce the target bitrate |
| 51 | + final targetWidth = targetResolution.dimension.width; |
| 52 | + final targetHeight = targetResolution.dimension.height; |
| 53 | + |
| 54 | + if (currentWidth < targetWidth || currentHeight < targetHeight) { |
| 55 | + final currentPixels = currentWidth * currentHeight; |
| 56 | + final targetPixels = targetWidth * targetHeight; |
| 57 | + final reductionFactor = currentPixels / targetPixels; |
| 58 | + |
| 59 | + return (targetResolution.encoding.maxBitrate * reductionFactor).round(); |
47 | 60 | } |
48 | 61 |
|
49 | | - final aspectRatio = dimension.aspect(); |
50 | | - streamLog.v( |
51 | | - 'SV:RtcManager', |
52 | | - () => '[publishVideoTrack] aspectRatio: $aspectRatio', |
53 | | - ); |
54 | | - if (_is16x9ratio(aspectRatio)) { |
55 | | - streamLog.v( |
56 | | - 'SV:RtcManager', |
57 | | - () => '[publishVideoTrack] defaultSimulcast_16x9', |
58 | | - ); |
59 | | - return _defaultSimulcast_16x9; |
| 62 | + return targetResolution.encoding.maxBitrate; |
| 63 | +} |
| 64 | + |
| 65 | +List<rtc.RTCRtpEncoding> withSimulcastConstraints({ |
| 66 | + required RtcVideoDimension dimensions, |
| 67 | + required List<rtc.RTCRtpEncoding> optimalVideoLayers, |
| 68 | +}) { |
| 69 | + var layers = <rtc.RTCRtpEncoding>[]; |
| 70 | + |
| 71 | + final size = max(dimensions.width, dimensions.height); |
| 72 | + if (size <= 320) { |
| 73 | + // provide only one layer 320x240 (q), the one with the highest quality |
| 74 | + layers = optimalVideoLayers.where((layer) => layer.rid == 'f').toList(); |
| 75 | + } else if (size <= 640) { |
| 76 | + // provide two layers, 160x120 (q) and 640x480 (h) |
| 77 | + layers = optimalVideoLayers.where((layer) => layer.rid != 'h').toList(); |
| 78 | + } else { |
| 79 | + // provide three layers for sizes > 640x480 |
| 80 | + layers = optimalVideoLayers; |
60 | 81 | } |
61 | | - streamLog.v( |
62 | | - 'SV:RtcManager', |
63 | | - () => '[publishVideoTrack] defaultSimulcast_4x3', |
64 | | - ); |
65 | | - return _defaultSimulcast_4x3; |
| 82 | + |
| 83 | + final ridMapping = ['q', 'h', 'f']; |
| 84 | + return layers |
| 85 | + .mapIndexed( |
| 86 | + (index, layer) => rtc.RTCRtpEncoding( |
| 87 | + rid: ridMapping[index], |
| 88 | + scaleResolutionDownBy: layer.scaleResolutionDownBy, |
| 89 | + maxFramerate: layer.maxFramerate, |
| 90 | + maxBitrate: layer.maxBitrate, |
| 91 | + ), |
| 92 | + ) |
| 93 | + .toList(); |
66 | 94 | } |
67 | 95 |
|
68 | | -List<rtc.RTCRtpEncoding> encodingsFromPresets( |
69 | | - RtcVideoDimension dimension, { |
70 | | - required Map<String, RtcVideoParameters> presets, |
| 96 | +List<rtc.RTCRtpEncoding> findOptimalScreenSharingLayers({ |
| 97 | + required RtcVideoDimension dimensions, |
| 98 | + RtcVideoParameters targetResolution = RtcVideoParametersPresets.h1080_16x9, |
71 | 99 | }) { |
72 | | - final result = <rtc.RTCRtpEncoding>[]; |
73 | | - |
74 | | - presets.forEach((rid, preset) { |
75 | | - final double scaleResolutionDownBy = |
76 | | - math.max(1, dimension.min() / preset.dimension.min()); |
77 | | - streamLog.v( |
78 | | - 'SV:RtcManager', |
79 | | - () => '[publishVideoTrack] #$rid; scaleResolutionDownBy: ' |
80 | | - '$scaleResolutionDownBy', |
81 | | - ); |
82 | | - result.add( |
| 100 | + final optimalVideoLayers = <rtc.RTCRtpEncoding>[]; |
| 101 | + |
| 102 | + for (final rid in ['f', 'h', 'q'].reversed) { |
| 103 | + optimalVideoLayers.insert( |
| 104 | + 0, |
83 | 105 | rtc.RTCRtpEncoding( |
84 | 106 | rid: rid, |
85 | | - scaleResolutionDownBy: scaleResolutionDownBy, |
86 | | - maxFramerate: preset.encoding.maxFramerate, |
87 | | - maxBitrate: preset.encoding.maxBitrate, |
| 107 | + maxFramerate: targetResolution.encoding.maxFramerate, |
| 108 | + maxBitrate: targetResolution.encoding.maxBitrate, |
88 | 109 | ), |
89 | 110 | ); |
90 | | - }); |
91 | | - return result; |
92 | | -} |
| 111 | + } |
93 | 112 |
|
94 | | -bool _is16x9ratio(double aspectRatio) { |
95 | | - return (aspectRatio - RtcVideoDimensionHelpers.aspect_16x9).abs() < |
96 | | - (aspectRatio - RtcVideoDimensionHelpers.aspect_4x3).abs(); |
| 113 | + return optimalVideoLayers; |
97 | 114 | } |
0 commit comments