Skip to content

Merge manual video quality/dimensions with adaptive stream#1047

Merged
hiroshihorie merged 20 commits into
mainfrom
hiroshi/better-track-settings
Jun 1, 2026
Merged

Merge manual video quality/dimensions with adaptive stream#1047
hiroshihorie merged 20 commits into
mainfrom
hiroshi/better-track-settings

Conversation

@hiroshihorie

@hiroshihorie hiroshihorie commented Apr 1, 2026

Copy link
Copy Markdown
Member

Summary

Previously, setVideoQuality / setVideoDimensions on a remote track were ignored (with a warning) whenever adaptiveStream was 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's RemoteTrackPublication (isEnabled, emitTrackUpdate, areDimensionsSmaller, layerDimensionsFor).

Changes

Manual quality + adaptive stream merge

  • setVideoQuality / setVideoDimensions / setVideoFPS are no longer rejected when adaptive stream is on; the request is merged with the visibility-derived dimensions, smaller area wins (matching JS areDimensionsSmaller, strict <).
  • When only a quality is requested, it's compared against that quality's simulcast layer dimensions before deciding which to send.

Explicit enable/disable overrides visibility

  • Enable/disable state is modeled as an internal tri-state (TrackEnabledPreference: unset / enabled / disabled), mirroring JS's requestedDisabled.
  • An explicit enable() / disable() now always wins over adaptive-stream visibility. Previously the visibility timer computed disabled independently and silently ignored the user's request.

Debounce correctness

  • A manual update cancels any pending debounced visibility update, and the debounced send rebuilds settings from current state at fire time — so a stale snapshot can't clobber a newer manual update.

Misc

  • fps is now preserved across adaptive-stream visibility updates (previously dropped).
  • Adaptive-stream visibility state is reset when the track changes, so stale dimensions can't leak into a later update.
  • Merge/disable/build logic extracted into pure functions (resolveVideoSettings, resolveDisabled, buildUpdateTrackSettings) in track_settings.dart.

Tests

  • Unit tests for the pure resolution/build logic: merge precedence, equal-area tie-break, tri-state disable, and proto building.
  • Publication-level test asserting the UpdateTrackSettings actually sent to the signal client for enable() / disable().

@hiroshihorie hiroshihorie changed the title Hiroshi/better track settings Better track settings May 29, 2026
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 annotation pointed callers at the private _emitTrackUpdate (unusable
externally), contradicted the @internal annotation, and was suppressed for the
only same-package caller (room.dart) by analysis_options anyway. Keep it as a
plain @internal shim.
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.
@hiroshihorie hiroshihorie changed the title Better track settings Merge manual video quality/dimensions with adaptive stream May 31, 2026
@hiroshihorie hiroshihorie marked this pull request as ready for review May 31, 2026 17:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from RemoteTrackPublication.
  • Merge manual video settings with adaptive stream (smaller area wins; strict < tie-break behavior) and preserve fps across adaptive updates.
  • Add unit and publication-level tests covering merge behavior, tri-state enable/disable precedence, and the actual UpdateTrackSettings emitted.

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.

Comment thread lib/src/publication/remote.dart Outdated

bool get enabled => _enabled;
bool _enabled = true;
bool get enabled => _enabledPreference != TrackEnabledPreference.disabled;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 229 to 233
if (roomOptions.adaptiveStream && newValue is RemoteVideoTrack) {
_adaptiveStreamActive = true;
// Start monitoring visibility
_visibilityTimer = Timer.periodic(
const Duration(milliseconds: 300),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/src/publication/track_settings.dart Outdated
Comment on lines +58 to +60
// Compare adaptive dimensions with the max quality layer dimensions
final maxQualityLayer = layerDimensionsForQuality?.call(userPreference!.quality!);
if (maxQualityLayer != null && adaptiveStreamDimensions.area() < maxQualityLayer.area()) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hiroshihorie

Copy link
Copy Markdown
Member Author

@copilot I added a few commits, are your concerns addressed ?

Copilot AI commented Jun 1, 2026

Copy link
Copy Markdown

@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 enabled resolution, immediate adaptive visibility initialization, and the quality-layer naming clarification are all reflected, with regression coverage added).

@hiroshihorie hiroshihorie merged commit ead71ca into main Jun 1, 2026
18 checks passed
@hiroshihorie hiroshihorie deleted the hiroshi/better-track-settings branch June 1, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants