Send video layers (#55)

* signal method

* videoLayers param on addTrack request

* screen share presets

* implemented

* cleaner syntax
This commit is contained in:
Hiroshi Horie
2021-12-15 19:09:38 +07:00
committed by GitHub
parent b24fad0f3b
commit 28e7091325
4 changed files with 159 additions and 56 deletions
+118 -42
View File
@@ -1,9 +1,12 @@
import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:collection/collection.dart';
import './proto/livekit_models.pb.dart' as lk_models;
import 'extensions.dart';
import 'livekit.dart';
import 'logger.dart';
import 'options.dart';
import 'track/options.dart';
import 'types.dart';
@@ -55,31 +58,63 @@ class Utils {
);
}
static List<VideoParameters> _presetsForDimensions(
VideoDimensions dimensions,
) {
final double aspect = dimensions.width / dimensions.height;
static List<VideoParameters> _presetsForDimensions({
required bool isScreenShare,
required VideoDimensions dimensions,
}) {
if (isScreenShare) return VideoParameters.presetsScreenShare;
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()) {
return VideoParameters.presets169;
}
return VideoParameters.presets43;
}
static VideoParameters _findPresetForDimensions(
VideoDimensions dimensions, {
static VideoEncoding _findAppropriateEncoding({
required bool isScreenShare,
required VideoDimensions dimensions,
required List<VideoParameters> presets,
}) {
assert(presets.isNotEmpty, 'presets should not be empty');
VideoParameters result = presets.first;
VideoEncoding result = presets.first.encoding;
// handle portrait by swapping dimensions
final size = dimensions.max();
for (final preset in presets) {
if (dimensions.width >= preset.dimensions.width &&
dimensions.height >= preset.dimensions.height) result = preset;
result = preset.encoding;
if (preset.dimensions.width >= size) break;
}
return result;
}
static final videoRids = ['q', 'h', 'f'];
static List<rtc.RTCRtpEncoding> encodingsFromPresets(
VideoDimensions dimensions, {
required List<VideoParameters> presets,
}) {
List<rtc.RTCRtpEncoding> result = [];
presets.forEachIndexed((i, e) {
if (i >= videoRids.length) {
return;
}
final size = dimensions.max();
final rid = videoRids[i];
result.add(e.encoding.toRTCRtpEncoding(
rid: rid,
scaleResolutionDownBy: size / e.dimensions.height,
));
});
return result;
}
static List<rtc.RTCRtpEncoding>? computeVideoEncodings({
required bool isScreenShare,
VideoDimensions? dimensions,
VideoPublishOptions? options,
}) {
@@ -87,53 +122,94 @@ class Utils {
VideoEncoding? videoEncoding = options.videoEncoding;
if ((videoEncoding == null && !options.simulcast) || dimensions == null) {
final useSimulcast = !isScreenShare && options.simulcast;
if ((videoEncoding == null && !useSimulcast) || dimensions == null) {
// don't set encoding when we are not simulcasting and user isn't restricting
// encoding parameters
return null;
}
final presets = _presetsForDimensions(dimensions);
final presets = _presetsForDimensions(
isScreenShare: isScreenShare,
dimensions: dimensions,
);
if (videoEncoding == null) {
// find the right encoding based on width/height
final preset = _findPresetForDimensions(dimensions, presets: presets);
// print('Using preset: ${preset.id}');
videoEncoding = preset.encoding;
// log.debug('using video encoding', videoEncoding);
videoEncoding = _findAppropriateEncoding(
isScreenShare: isScreenShare,
dimensions: dimensions,
presets: presets,
);
logger.fine('using video encoding', videoEncoding);
}
// Not simulcast
if (!options.simulcast) return [videoEncoding.toRTCRtpEncoding()];
if (!useSimulcast) return [videoEncoding.toRTCRtpEncoding()];
// Compute for simulcast
final midPreset = presets[1];
final lowPreset = presets[0];
return [
videoEncoding.toRTCRtpEncoding(
rid: 'f',
),
// if resolution is high enough, we would send both h and q res..
// otherwise only send h
if (dimensions.max() >= 960) ...[
midPreset.encoding.toRTCRtpEncoding(
rid: 'h',
// passing decimals to hardware encoder of android devices
// often causes issues so we better use integers
scaleResolutionDownBy: 2,
),
lowPreset.encoding.toRTCRtpEncoding(
rid: 'q',
scaleResolutionDownBy: 4,
),
] else
lowPreset.encoding.toRTCRtpEncoding(
rid: 'h',
scaleResolutionDownBy: 2,
),
];
final VideoParameters lowPreset = presets.first;
VideoParameters? midPreset;
if (presets.length > 1) {
midPreset = presets[1];
}
final original = VideoParameters(
dimensions: dimensions,
encoding: videoEncoding,
);
final size = dimensions.max();
List<VideoParameters> computedPresets = [original];
if (size >= 960 && midPreset != null) {
computedPresets = [lowPreset, midPreset, original];
} else if (size >= 500) {
computedPresets = [lowPreset, original];
}
return encodingsFromPresets(
dimensions,
presets: computedPresets,
);
}
static List<lk_models.VideoLayer> computeVideoLayers(
VideoDimensions dimensions,
List<rtc.RTCRtpEncoding>? encodings,
) {
// default to a single layer, HQ
if (encodings == null) {
return [
lk_models.VideoLayer(
quality: lk_models.VideoQuality.HIGH,
width: dimensions.width,
height: dimensions.height,
bitrate: 0,
)
];
}
return encodings.map((e) {
final scale = e.scaleResolutionDownBy ?? 1;
var quality = videoQualityForRid(e.rid);
if (quality == null && encodings.length == 1) {
quality = lk_models.VideoQuality.HIGH;
}
return lk_models.VideoLayer(
quality: quality,
width: (dimensions.width.toDouble() / scale).floor(),
height: (dimensions.height.toDouble() / scale).floor(),
bitrate: e.maxBitrate ?? 0,
);
}).toList();
}
static lk_models.VideoQuality? videoQualityForRid(String? rid) => {
'f': lk_models.VideoQuality.HIGH,
'h': lk_models.VideoQuality.MEDIUM,
'q': lk_models.VideoQuality.LOW,
}[rid];
// makes a debounce func, with 1 param
static Function(T) createDebounceFunc<T>(
Function(T) f, {