Merge manual video quality/dimensions with adaptive stream#1047
Conversation
A manual setVideoQuality/Dimensions/FPS/enable/disable sent immediately via _emitTrackUpdate() but did not cancel a pending debounced visibility send. The debounce captured a stale settings snapshot that could fire ~1.5s later and overwrite the manual update; since the visibility send never updated _lastSentTrackSettings, the dedup gate then suppressed any re-correction, leaving the server permanently on stale settings. - _emitTrackUpdate() now cancels any pending debounced send before sending. - the debounced send re-builds from current state at fire time and updates _lastSentTrackSettings, so it can never deliver or wedge on stale settings.
Mirrors the JS SDK tri-state. Previously `disabled` was computed as `!_enabled || !_adaptiveStreamEnabled`, an OR that meant an explicit enable() could never keep an off-screen track streaming when adaptive stream was on. - Replace the plain bool _enabled with a tri-state bool? _requestedDisabled (null = no explicit request); an explicit enable()/disable() now takes precedence over visibility, matching JS isEnabled. - Decouple the misnamed _adaptiveStreamEnabled into _adaptiveStreamActive (feature active for this pub) and _adaptiveStreamVisible (views visible). - Extract the precedence into a pure resolveDisabled() in track_settings.dart.
The merge is performed client-side (resolveVideoSettings), and only the single resolved value is sent; the server does not receive both and pick the smaller.
The existing 'equal areas keep preferred' test passed identical dimensions as both adaptive and preferred, so the assertion held regardless of which branch ran and could not catch a < -> <= regression. Use distinct same-area dimensions (720x320 vs 640x360), and add a one-px-smaller case where adaptive should win.
_buildTrackSettings had no coverage for the disabled computation, fps passthrough, or the proto field mapping. Extract the proto assembly into a pure buildUpdateTrackSettings() (alongside the new resolveDisabled()) so both are unit-testable without a live RemoteTrackPublication, and add tests for the tri-state disabled precedence and the dimensions/quality/fps mapping.
Replace the nullable bool _requestedDisabled (null/false/true) with an @internal TrackEnabledPreference { unset, enabled, disabled } enum. Removes the double-negative (requestedDisabled = false meaning 'enabled') and makes the precedence in resolveDisabled() read as an explicit switch. Behavior is unchanged; the enum is internal-only (non-exported file, @internal).
Stale dimensions/visibility from a previous adaptive-stream session could leak into a later _buildTrackSettings. enable()/disable() emit regardless of visibility and don't require a subscription, so calling them after the track was swapped/removed could send dimensions computed for a track that is no longer attached. Reset the adaptive-stream dimensions and visibility to their construction defaults whenever updateTrack changes the track; the visibility timer repopulates them while adaptive stream is active.
…tion Drives a real RemoteTrackPublication (adaptive stream off, so no visibility timer interferes) and asserts the UpdateTrackSettings actually sent to the signal client: disable()/enable() flip the `disabled` flag, the default video path resolves to HIGH quality, and a repeated disable() is a no-op. Complements the pure-function coverage in track_settings_test.dart by exercising the _emitTrackUpdate -> _buildTrackSettings -> SignalClient path.
There was a problem hiding this comment.
Pull request overview
This PR updates the Flutter client’s remote track settings behavior to match the JS SDK by merging manual video quality/dimensions/FPS preferences with adaptive-stream visibility-derived dimensions and sending the more conservative request to the server. It also introduces a tri-state enable/disable preference so explicit user intent overrides adaptive visibility, and improves debounce correctness to avoid stale visibility updates clobbering newer manual updates.
Changes:
- Add pure resolution/build helpers (
resolveVideoSettings,resolveDisabled,buildUpdateTrackSettings) and use them fromRemoteTrackPublication. - Merge manual video settings with adaptive stream (smaller area wins; strict
<tie-break behavior) and preservefpsacross adaptive updates. - Add unit and publication-level tests covering merge behavior, tri-state enable/disable precedence, and the actual
UpdateTrackSettingsemitted.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
lib/src/publication/remote.dart |
Integrates tri-state enable/disable, merged video settings resolution, and improved debounce behavior into RemoteTrackPublication. |
lib/src/publication/track_settings.dart |
Introduces pure helper functions for resolving video settings/disabled state and building protobuf update messages. |
test/publication/track_settings_test.dart |
Adds unit tests for the pure resolution/build logic (merge precedence, tie-breaks, disable tri-state, proto building). |
test/publication/remote_track_publication_test.dart |
Adds integration-style tests asserting enable()/disable() emit correct UpdateTrackSettings via the real publication path. |
.changes/adaptive-stream-manual-quality-merge |
Adds a changelog entry for the improved adaptive-stream/manual-quality behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| bool get enabled => _enabled; | ||
| bool _enabled = true; | ||
| bool get enabled => _enabledPreference != TrackEnabledPreference.disabled; |
There was a problem hiding this comment.
Addressed in dd21b7e. RemoteTrackPublication.enabled now derives from resolveDisabled(...), so it reflects adaptive-stream visibility as well as explicit enable/disable preference. Added a publication-level regression covering hidden adaptive visibility and explicit enable override.
| if (roomOptions.adaptiveStream && newValue is RemoteVideoTrack) { | ||
| _adaptiveStreamActive = true; | ||
| // Start monitoring visibility | ||
| _visibilityTimer = Timer.periodic( | ||
| const Duration(milliseconds: 300), |
There was a problem hiding this comment.
Addressed in 26badee. After installing onVideoViewBuild, updateTrack now immediately calls _computeVideoViewVisibility(quick: true), so a newly attached adaptive video track does not keep the optimistic visible default until the first timer tick. Added a regression that verifies the initial settings send is disabled: true with no visible views and stays disabled across a manual quality update.
| // Compare adaptive dimensions with the max quality layer dimensions | ||
| final maxQualityLayer = layerDimensionsForQuality?.call(userPreference!.quality!); | ||
| if (maxQualityLayer != null && adaptiveStreamDimensions.area() < maxQualityLayer.area()) { |
There was a problem hiding this comment.
Addressed in b6f94c7. Renamed maxQualityLayer to requestedQualityLayerDimensions and updated the comment to describe the dimensions implied by the requested quality, avoiding the incorrect “max layer” wording.
|
@copilot I added a few commits, are your concerns addressed ? |
I reviewed the follow-up commits and yes — my previously raised concerns are addressed in the current branch (effective |
Summary
Previously,
setVideoQuality/setVideoDimensionson a remote track were ignored (with a warning) wheneveradaptiveStreamwas enabled. This PR adopts the JS SDK's approach instead: the manual preference is merged client-side with the dimensions derived from the visible views, and the smaller (more conservative) of the two is sent to the server.Along the way it fixes a few related correctness issues in the adaptive-stream path. The logic mirrors
client-sdk-js'sRemoteTrackPublication(isEnabled,emitTrackUpdate,areDimensionsSmaller,layerDimensionsFor).Changes
Manual quality + adaptive stream merge
setVideoQuality/setVideoDimensions/setVideoFPSare no longer rejected when adaptive stream is on; the request is merged with the visibility-derived dimensions, smaller area wins (matching JSareDimensionsSmaller, strict<).Explicit enable/disable overrides visibility
TrackEnabledPreference: unset / enabled / disabled), mirroring JS'srequestedDisabled.enable()/disable()now always wins over adaptive-stream visibility. Previously the visibility timer computeddisabledindependently and silently ignored the user's request.Debounce correctness
Misc
fpsis now preserved across adaptive-stream visibility updates (previously dropped).resolveVideoSettings,resolveDisabled,buildUpdateTrackSettings) intrack_settings.dart.Tests
UpdateTrackSettingsactually sent to the signal client forenable()/disable().