From 3b3ccf3688bd98f8ad0a3444d3a809837de899b5 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:01:24 +0800 Subject: [PATCH 1/3] impl 1 --- lib/src/publication/remote.dart | 127 +++++++++++++------- lib/src/publication/track_settings.dart | 75 ++++++++++++ test/publication/track_settings_test.dart | 136 ++++++++++++++++++++++ 3 files changed, 293 insertions(+), 45 deletions(-) create mode 100644 lib/src/publication/track_settings.dart create mode 100644 test/publication/track_settings_test.dart diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index f9e30bb5e..b8bf9ef60 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -33,6 +33,7 @@ import '../types/other.dart'; import '../types/video_dimensions.dart'; import '../utils.dart'; import 'track_publication.dart'; +import 'track_settings.dart'; /// Represents a track publication from a RemoteParticipant. Provides methods to /// control if we should subscribe to the track, and its quality (for video). @@ -48,11 +49,16 @@ class RemoteTrackPublication extends TrackPublication int? _fps; int get fps => _fps ?? 0; - VideoQuality? _videoQuality = VideoQuality.HIGH; - VideoQuality get videoQuality => _videoQuality ?? VideoQuality.HIGH; + // Manual settings (set by user via setVideoQuality / setVideoDimensions) + VideoQuality? _requestedMaxQuality; + VideoDimensions? _requestedVideoDimensions; - VideoDimensions? _videoDimensions; - VideoDimensions? get videoDimensions => _videoDimensions; + // Adaptive stream state (set automatically by visibility observer) + VideoDimensions? _adaptiveStreamDimensions; + bool _adaptiveStreamEnabled = true; + + VideoQuality get videoQuality => _requestedMaxQuality ?? VideoQuality.HIGH; + VideoDimensions? get videoDimensions => _requestedVideoDimensions; /// The server may pause the track when they are bandwidth limitations and resume /// when there is more capacity. This property will be updated when the track is @@ -144,11 +150,6 @@ class RemoteTrackPublication extends TrackPublication final videoTrack = track as VideoTrack; - final settings = lk_rtc.UpdateTrackSettings( - trackSids: [sid], - disabled: true, - ); - // filter visible build contexts final viewSizes = videoTrack.viewKeys .map((e) => e.currentContext) @@ -161,15 +162,19 @@ class RemoteTrackPublication extends TrackPublication logger.finer('[Visibility] ${track?.sid} watching ${viewSizes.length} views...'); if (viewSizes.isNotEmpty) { - // compute largest size final largestSize = viewSizes.reduce((value, element) => maxOfSizes(value, element)); - - settings - ..disabled = false - ..width = largestSize.width.ceil() - ..height = largestSize.height.ceil(); + _adaptiveStreamDimensions = VideoDimensions( + largestSize.width.ceil(), + largestSize.height.ceil(), + ); + _adaptiveStreamEnabled = true; + } else { + _adaptiveStreamDimensions = null; + _adaptiveStreamEnabled = false; } + final settings = _buildTrackSettings(); + // Only send new settings to server if it changed if (settings != _lastSentTrackSettings) { _lastSentTrackSettings = settings; @@ -229,7 +234,7 @@ class RemoteTrackPublication extends TrackPublication return didUpdate; } - bool _canUpdateManualVideoSettings() { + bool _isManualOperationAllowed() { if (kind != TrackType.VIDEO) { logger.warning('Manual video setting updates are only supported for video tracks'); return false; @@ -240,55 +245,62 @@ class RemoteTrackPublication extends TrackPublication return false; } - if (participant.room.roomOptions.adaptiveStream) { - logger.warning('Manual video setting update ignored because adaptive stream is enabled'); - return false; - } - return true; } + /// For tracks that support simulcasting, adjust subscribed quality. + /// + /// This indicates the highest quality the client can accept. If network + /// bandwidth does not allow, the server will automatically reduce quality to + /// optimize for uninterrupted video. + /// + /// When adaptive stream is enabled, the server will use the smaller of + /// this setting and the adaptive stream dimensions. Future setVideoQuality(VideoQuality newValue) async { - if (newValue == _videoQuality) return; - if (!_canUpdateManualVideoSettings()) return; - _videoQuality = newValue; - _videoDimensions = null; - sendUpdateTrackSettings(); + if (newValue == _requestedMaxQuality) return; + if (!_isManualOperationAllowed()) return; + _requestedMaxQuality = newValue; + _requestedVideoDimensions = null; + _emitTrackUpdate(); } /// Set preferred video dimensions for this track. /// /// Server will choose the appropriate layer based on these dimensions. /// Will override previous calls to [setVideoQuality]. + /// + /// When adaptive stream is enabled, the server will use the smaller of + /// this setting and the adaptive stream dimensions. Future setVideoDimensions(VideoDimensions newValue) async { - if (newValue.width == _videoDimensions?.width && newValue.height == _videoDimensions?.height) { + if (newValue.width == _requestedVideoDimensions?.width && + newValue.height == _requestedVideoDimensions?.height) { return; } - if (!_canUpdateManualVideoSettings()) return; - _videoDimensions = newValue; - _videoQuality = null; - sendUpdateTrackSettings(); + if (!_isManualOperationAllowed()) return; + _requestedVideoDimensions = newValue; + _requestedMaxQuality = null; + _emitTrackUpdate(); } /// Set desired FPS, server will do its best to return FPS close to this. /// It's only supported for video codecs that support SVC currently. Future setVideoFPS(int newValue) async { if (newValue == _fps) return; - if (!_canUpdateManualVideoSettings()) return; + if (!_isManualOperationAllowed()) return; _fps = newValue; - sendUpdateTrackSettings(); + _emitTrackUpdate(); } Future enable() async { if (_enabled) return; _enabled = true; - sendUpdateTrackSettings(); + _emitTrackUpdate(); } Future disable() async { if (!_enabled) return; _enabled = false; - sendUpdateTrackSettings(); + _emitTrackUpdate(); } Future subscribe() async { @@ -333,26 +345,51 @@ class RemoteTrackPublication extends TrackPublication participant.room.engine.signalClient.sendUpdateSubscription(subscription); } - @internal - void sendUpdateTrackSettings() { + lk_rtc.UpdateTrackSettings _buildTrackSettings() { + // disabled if manually disabled or adaptive stream says no views visible + final isDisabled = !_enabled || !_adaptiveStreamEnabled; + final settings = lk_rtc.UpdateTrackSettings( trackSids: [sid], - disabled: !_enabled, + disabled: isDisabled, ); + if (kind == TrackType.VIDEO) { - if (_videoDimensions != null) { - settings.width = _videoDimensions!.width; - settings.height = _videoDimensions!.height; - } else if (_videoQuality != null) { - settings.quality = _videoQuality!.toPBType(); - } else { - settings.quality = VideoQuality.HIGH.toPBType(); + final resolved = resolveVideoSettings( + adaptiveStreamDimensions: _adaptiveStreamDimensions, + requestedDimensions: _requestedVideoDimensions, + requestedMaxQuality: _requestedMaxQuality, + layerDimensionsForQuality: (quality) { + final pbQuality = quality.toPBType(); + final layer = latestInfo?.layers + .where((l) => l.quality == pbQuality) + .firstOrNull; + if (layer == null) return null; + return VideoDimensions(layer.width, layer.height); + }, + ); + + if (resolved.dimensions != null) { + settings.width = resolved.dimensions!.width; + settings.height = resolved.dimensions!.height; + } else if (resolved.quality != null) { + settings.quality = resolved.quality!.toPBType(); } if (_fps != null) settings.fps = _fps!; } + return settings; + } + + void _emitTrackUpdate() { + final settings = _buildTrackSettings(); + _lastSentTrackSettings = settings; participant.room.engine.signalClient.sendUpdateTrackSettings(settings); } + @internal + @Deprecated('Use _emitTrackUpdate instead') + void sendUpdateTrackSettings() => _emitTrackUpdate(); + @internal // Update internal var and return true if changed Future updateSubscriptionAllowed(bool allowed) async { diff --git a/lib/src/publication/track_settings.dart b/lib/src/publication/track_settings.dart new file mode 100644 index 000000000..20965b6fa --- /dev/null +++ b/lib/src/publication/track_settings.dart @@ -0,0 +1,75 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../types/other.dart'; +import '../types/video_dimensions.dart'; + +/// The result of merging adaptive stream and manual video settings. +/// +/// Separates the "what to send" decision from protobuf serialization, +/// making the merge logic testable without protobuf dependencies. +class ResolvedVideoSettings { + /// If non-null, send dimensions (width/height) to the server. + final VideoDimensions? dimensions; + + /// If non-null and [dimensions] is null, send quality to the server. + final VideoQuality? quality; + + const ResolvedVideoSettings({this.dimensions, this.quality}); +} + +/// Merges adaptive stream dimensions with manual quality/dimension settings, +/// always picking the more conservative (smaller) of the two. +/// +/// This matches the JS SDK's merge behavior in `emitTrackUpdate()`. +/// +/// [adaptiveStreamDimensions] — set automatically by the visibility observer. +/// [requestedDimensions] — set manually via `setVideoDimensions()`. +/// [requestedMaxQuality] — set manually via `setVideoQuality()`. +/// [layerDimensionsForQuality] — resolves a quality to dimensions using the +/// track's published layer info. Passed as a callback so callers can provide +/// it from whatever source they have (protobuf TrackInfo, test fixture, etc). +ResolvedVideoSettings resolveVideoSettings({ + VideoDimensions? adaptiveStreamDimensions, + VideoDimensions? requestedDimensions, + VideoQuality? requestedMaxQuality, + VideoDimensions? Function(VideoQuality quality)? layerDimensionsForQuality, +}) { + VideoDimensions? minDimensions = requestedDimensions; + + if (adaptiveStreamDimensions != null) { + if (minDimensions != null) { + // Use the smaller of adaptive vs manually requested dimensions + if (adaptiveStreamDimensions.area() < minDimensions.area()) { + minDimensions = adaptiveStreamDimensions; + } + } else if (requestedMaxQuality != null) { + // Compare adaptive dimensions with the max quality layer dimensions + final maxQualityLayer = layerDimensionsForQuality?.call(requestedMaxQuality); + if (maxQualityLayer != null && + adaptiveStreamDimensions.area() < maxQualityLayer.area()) { + minDimensions = adaptiveStreamDimensions; + } + } else { + minDimensions = adaptiveStreamDimensions; + } + } + + if (minDimensions != null) { + return ResolvedVideoSettings(dimensions: minDimensions); + } else if (requestedMaxQuality != null) { + return ResolvedVideoSettings(quality: requestedMaxQuality); + } + return ResolvedVideoSettings(quality: VideoQuality.HIGH); +} diff --git a/test/publication/track_settings_test.dart b/test/publication/track_settings_test.dart new file mode 100644 index 000000000..62bcac543 --- /dev/null +++ b/test/publication/track_settings_test.dart @@ -0,0 +1,136 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:livekit_client/src/publication/track_settings.dart'; +import 'package:livekit_client/src/types/other.dart'; +import 'package:livekit_client/src/types/video_dimensions.dart'; + +/// Test helper: returns layer dimensions for a standard 3-layer SVC/simulcast track. +VideoDimensions? _testLayerDimensions(VideoQuality quality) { + return { + VideoQuality.LOW: VideoDimensions(320, 180), + VideoQuality.MEDIUM: VideoDimensions(640, 360), + VideoQuality.HIGH: VideoDimensions(1280, 720), + }[quality]; +} + +void main() { + group('resolveVideoSettings', () { + group('no adaptive stream', () { + test('defaults to HIGH quality when nothing set', () { + final r = resolveVideoSettings(); + expect(r.quality, VideoQuality.HIGH); + expect(r.dimensions, isNull); + }); + + test('uses requested quality', () { + final r = resolveVideoSettings( + requestedMaxQuality: VideoQuality.LOW, + ); + expect(r.quality, VideoQuality.LOW); + expect(r.dimensions, isNull); + }); + + test('uses requested dimensions', () { + final r = resolveVideoSettings( + requestedDimensions: VideoDimensions(800, 600), + ); + expect(r.dimensions, VideoDimensions(800, 600)); + expect(r.quality, isNull); + }); + }); + + group('adaptive stream only', () { + test('uses adaptive stream dimensions', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(480, 270), + ); + expect(r.dimensions, VideoDimensions(480, 270)); + expect(r.quality, isNull); + }); + }); + + group('adaptive stream + requested dimensions', () { + test('adaptive wins when smaller', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(320, 180), + requestedDimensions: VideoDimensions(1280, 720), + ); + expect(r.dimensions, VideoDimensions(320, 180)); + }); + + test('requested wins when smaller', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(1920, 1080), + requestedDimensions: VideoDimensions(640, 360), + ); + expect(r.dimensions, VideoDimensions(640, 360)); + }); + + test('equal areas keep requested', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(640, 360), + requestedDimensions: VideoDimensions(640, 360), + ); + expect(r.dimensions, VideoDimensions(640, 360)); + }); + }); + + group('adaptive stream + requested quality', () { + test('adaptive wins when smaller than quality layer', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(320, 180), + requestedMaxQuality: VideoQuality.HIGH, + layerDimensionsForQuality: _testLayerDimensions, + ); + // adaptive 320*180 < HIGH 1280*720 → sends adaptive dimensions + expect(r.dimensions, VideoDimensions(320, 180)); + expect(r.quality, isNull); + }); + + test('quality wins when adaptive is larger than quality layer', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(1920, 1080), + requestedMaxQuality: VideoQuality.LOW, + layerDimensionsForQuality: _testLayerDimensions, + ); + // adaptive 1920*1080 > LOW 320*180 → sends quality directly + expect(r.quality, VideoQuality.LOW); + expect(r.dimensions, isNull); + }); + + test('quality sent directly when no layer info available', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(320, 180), + requestedMaxQuality: VideoQuality.LOW, + // no layerDimensionsForQuality → can't compare + ); + expect(r.quality, VideoQuality.LOW); + expect(r.dimensions, isNull); + }); + + test('quality sent when layer lookup returns null', () { + final r = resolveVideoSettings( + adaptiveStreamDimensions: VideoDimensions(320, 180), + requestedMaxQuality: VideoQuality.MEDIUM, + layerDimensionsForQuality: (_) => null, + ); + expect(r.quality, VideoQuality.MEDIUM); + expect(r.dimensions, isNull); + }); + }); + }); +} From e32605a7e3854276f3ca555c77d147b1e8138ac8 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:14:24 +0800 Subject: [PATCH 2/3] Create adaptive-stream-manual-quality-merge --- .changes/adaptive-stream-manual-quality-merge | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/adaptive-stream-manual-quality-merge diff --git a/.changes/adaptive-stream-manual-quality-merge b/.changes/adaptive-stream-manual-quality-merge new file mode 100644 index 000000000..083a3def6 --- /dev/null +++ b/.changes/adaptive-stream-manual-quality-merge @@ -0,0 +1 @@ +patch type="improved" "Allow manual video quality selection with adaptive stream enabled" From e80813d970ed5cb6f26dcef7e394f341d7ffb351 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:17:51 +0800 Subject: [PATCH 3/3] refactor --- lib/src/publication/remote.dart | 27 ++++-------- lib/src/publication/track_settings.dart | 53 +++++++++++------------ test/publication/track_settings_test.dart | 31 +++++++------ 3 files changed, 49 insertions(+), 62 deletions(-) diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index b8bf9ef60..aa74cb87d 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -50,15 +50,14 @@ class RemoteTrackPublication extends TrackPublication int get fps => _fps ?? 0; // Manual settings (set by user via setVideoQuality / setVideoDimensions) - VideoQuality? _requestedMaxQuality; - VideoDimensions? _requestedVideoDimensions; + VideoSettings? _userPreference; // Adaptive stream state (set automatically by visibility observer) VideoDimensions? _adaptiveStreamDimensions; bool _adaptiveStreamEnabled = true; - VideoQuality get videoQuality => _requestedMaxQuality ?? VideoQuality.HIGH; - VideoDimensions? get videoDimensions => _requestedVideoDimensions; + VideoQuality get videoQuality => _userPreference?.quality ?? VideoQuality.HIGH; + VideoDimensions? get videoDimensions => _userPreference?.dimensions; /// The server may pause the track when they are bandwidth limitations and resume /// when there is more capacity. This property will be updated when the track is @@ -257,10 +256,9 @@ class RemoteTrackPublication extends TrackPublication /// When adaptive stream is enabled, the server will use the smaller of /// this setting and the adaptive stream dimensions. Future setVideoQuality(VideoQuality newValue) async { - if (newValue == _requestedMaxQuality) return; + if (newValue == _userPreference?.quality) return; if (!_isManualOperationAllowed()) return; - _requestedMaxQuality = newValue; - _requestedVideoDimensions = null; + _userPreference = VideoSettings.quality(newValue); _emitTrackUpdate(); } @@ -272,13 +270,9 @@ class RemoteTrackPublication extends TrackPublication /// When adaptive stream is enabled, the server will use the smaller of /// this setting and the adaptive stream dimensions. Future setVideoDimensions(VideoDimensions newValue) async { - if (newValue.width == _requestedVideoDimensions?.width && - newValue.height == _requestedVideoDimensions?.height) { - return; - } + if (newValue == _userPreference?.dimensions) return; if (!_isManualOperationAllowed()) return; - _requestedVideoDimensions = newValue; - _requestedMaxQuality = null; + _userPreference = VideoSettings.dimensions(newValue); _emitTrackUpdate(); } @@ -357,13 +351,10 @@ class RemoteTrackPublication extends TrackPublication if (kind == TrackType.VIDEO) { final resolved = resolveVideoSettings( adaptiveStreamDimensions: _adaptiveStreamDimensions, - requestedDimensions: _requestedVideoDimensions, - requestedMaxQuality: _requestedMaxQuality, + userPreference: _userPreference, layerDimensionsForQuality: (quality) { final pbQuality = quality.toPBType(); - final layer = latestInfo?.layers - .where((l) => l.quality == pbQuality) - .firstOrNull; + final layer = latestInfo?.layers.where((l) => l.quality == pbQuality).firstOrNull; if (layer == null) return null; return VideoDimensions(layer.width, layer.height); }, diff --git a/lib/src/publication/track_settings.dart b/lib/src/publication/track_settings.dart index 20965b6fa..0853969a0 100644 --- a/lib/src/publication/track_settings.dart +++ b/lib/src/publication/track_settings.dart @@ -12,41 +12,39 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:meta/meta.dart' show immutable, internal; + import '../types/other.dart'; import '../types/video_dimensions.dart'; -/// The result of merging adaptive stream and manual video settings. +/// Represents a video quality setting — either explicit dimensions or a +/// quality level (LOW/MEDIUM/HIGH), never both. /// -/// Separates the "what to send" decision from protobuf serialization, -/// making the merge logic testable without protobuf dependencies. -class ResolvedVideoSettings { - /// If non-null, send dimensions (width/height) to the server. +/// Used for both user-requested settings and the resolved merge result. +@internal +@immutable +class VideoSettings { final VideoDimensions? dimensions; - - /// If non-null and [dimensions] is null, send quality to the server. final VideoQuality? quality; - const ResolvedVideoSettings({this.dimensions, this.quality}); + const VideoSettings.dimensions(VideoDimensions this.dimensions) : quality = null; + + const VideoSettings.quality(VideoQuality this.quality) : dimensions = null; + + static const high = VideoSettings.quality(VideoQuality.HIGH); } -/// Merges adaptive stream dimensions with manual quality/dimension settings, +/// Merges adaptive stream dimensions with manual [VideoSettings], /// always picking the more conservative (smaller) of the two. /// /// This matches the JS SDK's merge behavior in `emitTrackUpdate()`. -/// -/// [adaptiveStreamDimensions] — set automatically by the visibility observer. -/// [requestedDimensions] — set manually via `setVideoDimensions()`. -/// [requestedMaxQuality] — set manually via `setVideoQuality()`. -/// [layerDimensionsForQuality] — resolves a quality to dimensions using the -/// track's published layer info. Passed as a callback so callers can provide -/// it from whatever source they have (protobuf TrackInfo, test fixture, etc). -ResolvedVideoSettings resolveVideoSettings({ +@internal +VideoSettings resolveVideoSettings({ VideoDimensions? adaptiveStreamDimensions, - VideoDimensions? requestedDimensions, - VideoQuality? requestedMaxQuality, + VideoSettings? userPreference, VideoDimensions? Function(VideoQuality quality)? layerDimensionsForQuality, }) { - VideoDimensions? minDimensions = requestedDimensions; + VideoDimensions? minDimensions = userPreference?.dimensions; if (adaptiveStreamDimensions != null) { if (minDimensions != null) { @@ -54,11 +52,10 @@ ResolvedVideoSettings resolveVideoSettings({ if (adaptiveStreamDimensions.area() < minDimensions.area()) { minDimensions = adaptiveStreamDimensions; } - } else if (requestedMaxQuality != null) { + } else if (userPreference?.quality != null) { // Compare adaptive dimensions with the max quality layer dimensions - final maxQualityLayer = layerDimensionsForQuality?.call(requestedMaxQuality); - if (maxQualityLayer != null && - adaptiveStreamDimensions.area() < maxQualityLayer.area()) { + final maxQualityLayer = layerDimensionsForQuality?.call(userPreference!.quality!); + if (maxQualityLayer != null && adaptiveStreamDimensions.area() < maxQualityLayer.area()) { minDimensions = adaptiveStreamDimensions; } } else { @@ -67,9 +64,9 @@ ResolvedVideoSettings resolveVideoSettings({ } if (minDimensions != null) { - return ResolvedVideoSettings(dimensions: minDimensions); - } else if (requestedMaxQuality != null) { - return ResolvedVideoSettings(quality: requestedMaxQuality); + return VideoSettings.dimensions(minDimensions); + } else if (userPreference?.quality != null) { + return VideoSettings.quality(userPreference!.quality!); } - return ResolvedVideoSettings(quality: VideoQuality.HIGH); + return VideoSettings.high; } diff --git a/test/publication/track_settings_test.dart b/test/publication/track_settings_test.dart index 62bcac543..b85336dd9 100644 --- a/test/publication/track_settings_test.dart +++ b/test/publication/track_settings_test.dart @@ -36,17 +36,17 @@ void main() { expect(r.dimensions, isNull); }); - test('uses requested quality', () { + test('uses preferred quality', () { final r = resolveVideoSettings( - requestedMaxQuality: VideoQuality.LOW, + userPreference: VideoSettings.quality(VideoQuality.LOW), ); expect(r.quality, VideoQuality.LOW); expect(r.dimensions, isNull); }); - test('uses requested dimensions', () { + test('uses preferred dimensions', () { final r = resolveVideoSettings( - requestedDimensions: VideoDimensions(800, 600), + userPreference: VideoSettings.dimensions(VideoDimensions(800, 600)), ); expect(r.dimensions, VideoDimensions(800, 600)); expect(r.quality, isNull); @@ -63,37 +63,37 @@ void main() { }); }); - group('adaptive stream + requested dimensions', () { + group('adaptive stream + preferred dimensions', () { test('adaptive wins when smaller', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(320, 180), - requestedDimensions: VideoDimensions(1280, 720), + userPreference: VideoSettings.dimensions(VideoDimensions(1280, 720)), ); expect(r.dimensions, VideoDimensions(320, 180)); }); - test('requested wins when smaller', () { + test('preferred wins when smaller', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(1920, 1080), - requestedDimensions: VideoDimensions(640, 360), + userPreference: VideoSettings.dimensions(VideoDimensions(640, 360)), ); expect(r.dimensions, VideoDimensions(640, 360)); }); - test('equal areas keep requested', () { + test('equal areas keep preferred', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(640, 360), - requestedDimensions: VideoDimensions(640, 360), + userPreference: VideoSettings.dimensions(VideoDimensions(640, 360)), ); expect(r.dimensions, VideoDimensions(640, 360)); }); }); - group('adaptive stream + requested quality', () { + group('adaptive stream + preferred quality', () { test('adaptive wins when smaller than quality layer', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(320, 180), - requestedMaxQuality: VideoQuality.HIGH, + userPreference: VideoSettings.quality(VideoQuality.HIGH), layerDimensionsForQuality: _testLayerDimensions, ); // adaptive 320*180 < HIGH 1280*720 → sends adaptive dimensions @@ -104,7 +104,7 @@ void main() { test('quality wins when adaptive is larger than quality layer', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(1920, 1080), - requestedMaxQuality: VideoQuality.LOW, + userPreference: VideoSettings.quality(VideoQuality.LOW), layerDimensionsForQuality: _testLayerDimensions, ); // adaptive 1920*1080 > LOW 320*180 → sends quality directly @@ -115,8 +115,7 @@ void main() { test('quality sent directly when no layer info available', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(320, 180), - requestedMaxQuality: VideoQuality.LOW, - // no layerDimensionsForQuality → can't compare + userPreference: VideoSettings.quality(VideoQuality.LOW), ); expect(r.quality, VideoQuality.LOW); expect(r.dimensions, isNull); @@ -125,7 +124,7 @@ void main() { test('quality sent when layer lookup returns null', () { final r = resolveVideoSettings( adaptiveStreamDimensions: VideoDimensions(320, 180), - requestedMaxQuality: VideoQuality.MEDIUM, + userPreference: VideoSettings.quality(VideoQuality.MEDIUM), layerDimensionsForQuality: (_) => null, ); expect(r.quality, VideoQuality.MEDIUM);