Send video layers (#55)
* signal method * videoLayers param on addTrack request * screen share presets * implemented * cleaner syntax
This commit is contained in:
@@ -119,29 +119,35 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.fine(
|
||||||
|
'Compute encodings with resolution: ${dimensions}, options: ${publishOptions}');
|
||||||
|
|
||||||
|
// Video encodings and simulcasts
|
||||||
|
final encodings = Utils.computeVideoEncodings(
|
||||||
|
isScreenShare: track.source == TrackSource.screenShareVideo,
|
||||||
|
dimensions: dimensions,
|
||||||
|
options: publishOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.fine('Using encodings: ${encodings?.map((e) => e.toMap())}');
|
||||||
|
|
||||||
|
final layers = Utils.computeVideoLayers(dimensions, encodings);
|
||||||
|
|
||||||
|
logger.fine('Video layers: ${layers.map((e) => e)}');
|
||||||
|
|
||||||
final trackInfo = await room.engine.addTrack(
|
final trackInfo = await room.engine.addTrack(
|
||||||
cid: track.getCid(),
|
cid: track.getCid(),
|
||||||
name: track.name,
|
name: track.name,
|
||||||
kind: track.kind,
|
kind: track.kind,
|
||||||
source: track.source.toPBType(),
|
source: track.source.toPBType(),
|
||||||
dimensions: dimensions,
|
dimensions: dimensions,
|
||||||
|
videoLayers: layers,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.fine('publishVideoTrack addTrack response: ${trackInfo}');
|
logger.fine('publishVideoTrack addTrack response: ${trackInfo}');
|
||||||
|
|
||||||
await track.start();
|
await track.start();
|
||||||
|
|
||||||
logger.fine(
|
|
||||||
'Compute encodings with resolution: ${dimensions}, options: ${publishOptions}');
|
|
||||||
|
|
||||||
// Video encodings and simulcasts
|
|
||||||
final encodings = Utils.computeVideoEncodings(
|
|
||||||
dimensions: dimensions,
|
|
||||||
options: publishOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.fine('Using encodings: ${encodings?.map((e) => e.toMap())}');
|
|
||||||
|
|
||||||
final transceiverInit = rtc.RTCRtpTransceiverInit(
|
final transceiverInit = rtc.RTCRtpTransceiverInit(
|
||||||
direction: rtc.TransceiverDirection.SendOnly,
|
direction: rtc.TransceiverDirection.SendOnly,
|
||||||
sendEncodings: encodings,
|
sendEncodings: encodings,
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ class RTCEngine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
required lk_models.TrackSource source,
|
required lk_models.TrackSource source,
|
||||||
VideoDimensions? dimensions,
|
VideoDimensions? dimensions,
|
||||||
bool? dtx,
|
bool? dtx,
|
||||||
|
List<lk_models.VideoLayer>? videoLayers,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO: Check if cid already published
|
// TODO: Check if cid already published
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ class RTCEngine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
source: source,
|
source: source,
|
||||||
dimensions: dimensions,
|
dimensions: dimensions,
|
||||||
dtx: dtx,
|
dtx: dtx,
|
||||||
|
videoLayers: videoLayers,
|
||||||
);
|
);
|
||||||
|
|
||||||
// wait for response, or timeout
|
// wait for response, or timeout
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
required lk_models.TrackSource source,
|
required lk_models.TrackSource source,
|
||||||
VideoDimensions? dimensions,
|
VideoDimensions? dimensions,
|
||||||
bool? dtx,
|
bool? dtx,
|
||||||
|
List<lk_models.VideoLayer>? videoLayers,
|
||||||
}) {
|
}) {
|
||||||
final req = lk_rtc.AddTrackRequest(
|
final req = lk_rtc.AddTrackRequest(
|
||||||
cid: cid,
|
cid: cid,
|
||||||
@@ -158,10 +159,17 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
source: source,
|
source: source,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type == lk_models.TrackType.VIDEO && dimensions != null) {
|
if (type == lk_models.TrackType.VIDEO) {
|
||||||
// video specific
|
// video specific
|
||||||
req.width = dimensions.width;
|
if (dimensions != null) {
|
||||||
req.height = dimensions.height;
|
req.width = dimensions.width;
|
||||||
|
req.height = dimensions.height;
|
||||||
|
}
|
||||||
|
if (videoLayers != null && videoLayers.isNotEmpty) {
|
||||||
|
req.layers
|
||||||
|
..clear()
|
||||||
|
..addAll(videoLayers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == lk_models.TrackType.AUDIO && dtx != null) {
|
if (type == lk_models.TrackType.AUDIO && dtx != null) {
|
||||||
@@ -184,6 +192,17 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
void sendUpdateVideoLayers(
|
||||||
|
String trackSid,
|
||||||
|
List<lk_models.VideoLayer> layers,
|
||||||
|
) =>
|
||||||
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
|
updateLayers: lk_rtc.UpdateVideoLayers(
|
||||||
|
trackSid: trackSid,
|
||||||
|
layers: layers,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
||||||
leave: lk_rtc.LeaveRequest(),
|
leave: lk_rtc.LeaveRequest(),
|
||||||
));
|
));
|
||||||
|
|||||||
+118
-42
@@ -1,9 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
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 'extensions.dart';
|
||||||
import 'livekit.dart';
|
import 'livekit.dart';
|
||||||
|
import 'logger.dart';
|
||||||
import 'options.dart';
|
import 'options.dart';
|
||||||
import 'track/options.dart';
|
import 'track/options.dart';
|
||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
@@ -55,31 +58,63 @@ class Utils {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<VideoParameters> _presetsForDimensions(
|
static List<VideoParameters> _presetsForDimensions({
|
||||||
VideoDimensions dimensions,
|
required bool isScreenShare,
|
||||||
) {
|
required VideoDimensions dimensions,
|
||||||
final double aspect = dimensions.width / dimensions.height;
|
}) {
|
||||||
|
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()) {
|
if ((aspect - 16.0 / 9.0).abs() < (aspect - 4.0 / 3.0).abs()) {
|
||||||
return VideoParameters.presets169;
|
return VideoParameters.presets169;
|
||||||
}
|
}
|
||||||
return VideoParameters.presets43;
|
return VideoParameters.presets43;
|
||||||
}
|
}
|
||||||
|
|
||||||
static VideoParameters _findPresetForDimensions(
|
static VideoEncoding _findAppropriateEncoding({
|
||||||
VideoDimensions dimensions, {
|
required bool isScreenShare,
|
||||||
|
required VideoDimensions dimensions,
|
||||||
required List<VideoParameters> presets,
|
required List<VideoParameters> presets,
|
||||||
}) {
|
}) {
|
||||||
assert(presets.isNotEmpty, 'presets should not be empty');
|
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) {
|
for (final preset in presets) {
|
||||||
if (dimensions.width >= preset.dimensions.width &&
|
result = preset.encoding;
|
||||||
dimensions.height >= preset.dimensions.height) result = preset;
|
if (preset.dimensions.width >= size) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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({
|
static List<rtc.RTCRtpEncoding>? computeVideoEncodings({
|
||||||
|
required bool isScreenShare,
|
||||||
VideoDimensions? dimensions,
|
VideoDimensions? dimensions,
|
||||||
VideoPublishOptions? options,
|
VideoPublishOptions? options,
|
||||||
}) {
|
}) {
|
||||||
@@ -87,53 +122,94 @@ class Utils {
|
|||||||
|
|
||||||
VideoEncoding? videoEncoding = options.videoEncoding;
|
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
|
// don't set encoding when we are not simulcasting and user isn't restricting
|
||||||
// encoding parameters
|
// encoding parameters
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final presets = _presetsForDimensions(dimensions);
|
final presets = _presetsForDimensions(
|
||||||
|
isScreenShare: isScreenShare,
|
||||||
|
dimensions: dimensions,
|
||||||
|
);
|
||||||
|
|
||||||
if (videoEncoding == null) {
|
if (videoEncoding == null) {
|
||||||
// find the right encoding based on width/height
|
// find the right encoding based on width/height
|
||||||
final preset = _findPresetForDimensions(dimensions, presets: presets);
|
videoEncoding = _findAppropriateEncoding(
|
||||||
// print('Using preset: ${preset.id}');
|
isScreenShare: isScreenShare,
|
||||||
videoEncoding = preset.encoding;
|
dimensions: dimensions,
|
||||||
// log.debug('using video encoding', videoEncoding);
|
presets: presets,
|
||||||
|
);
|
||||||
|
logger.fine('using video encoding', videoEncoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not simulcast
|
// Not simulcast
|
||||||
if (!options.simulcast) return [videoEncoding.toRTCRtpEncoding()];
|
if (!useSimulcast) return [videoEncoding.toRTCRtpEncoding()];
|
||||||
|
|
||||||
// Compute for simulcast
|
final VideoParameters lowPreset = presets.first;
|
||||||
final midPreset = presets[1];
|
VideoParameters? midPreset;
|
||||||
final lowPreset = presets[0];
|
if (presets.length > 1) {
|
||||||
return [
|
midPreset = presets[1];
|
||||||
videoEncoding.toRTCRtpEncoding(
|
}
|
||||||
rid: 'f',
|
final original = VideoParameters(
|
||||||
),
|
dimensions: dimensions,
|
||||||
// if resolution is high enough, we would send both h and q res..
|
encoding: videoEncoding,
|
||||||
// otherwise only send h
|
);
|
||||||
if (dimensions.max() >= 960) ...[
|
|
||||||
midPreset.encoding.toRTCRtpEncoding(
|
final size = dimensions.max();
|
||||||
rid: 'h',
|
List<VideoParameters> computedPresets = [original];
|
||||||
// passing decimals to hardware encoder of android devices
|
|
||||||
// often causes issues so we better use integers
|
if (size >= 960 && midPreset != null) {
|
||||||
scaleResolutionDownBy: 2,
|
computedPresets = [lowPreset, midPreset, original];
|
||||||
),
|
} else if (size >= 500) {
|
||||||
lowPreset.encoding.toRTCRtpEncoding(
|
computedPresets = [lowPreset, original];
|
||||||
rid: 'q',
|
}
|
||||||
scaleResolutionDownBy: 4,
|
|
||||||
),
|
return encodingsFromPresets(
|
||||||
] else
|
dimensions,
|
||||||
lowPreset.encoding.toRTCRtpEncoding(
|
presets: computedPresets,
|
||||||
rid: 'h',
|
);
|
||||||
scaleResolutionDownBy: 2,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// makes a debounce func, with 1 param
|
||||||
static Function(T) createDebounceFunc<T>(
|
static Function(T) createDebounceFunc<T>(
|
||||||
Function(T) f, {
|
Function(T) f, {
|
||||||
|
|||||||
Reference in New Issue
Block a user