Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CeeblueTV/wrts-client/llms.txt

Use this file to discover all available pages before exploring further.

Buffer management is the core of WebRTS’s low-latency strategy. Because live streams have no replay opportunity, every decision about when to speed up, slow down, skip, or adapt quality must be made in real time based on how much media is buffered ahead of the playback head. The Player class continuously measures this buffer amount in milliseconds and classifies it into one of four states that drive all adaptation logic cooperatively.

Buffer State Machine

The BufferState enum defines four possible states for the player’s buffer:
The buffer has no meaningful state. This is the initial state when the player is stopped, and also the state during the first buffering phase before playback begins. Congestion-detection and adaptation algorithms are paused while in NONE.
The buffer is critically low, indicating network congestion or a delay in receiving data. This state triggers recovery mechanisms: the player switches to a lower video quality rendition, reduces playbackRate, and (when player.reliable = false) aborts in-flight downloads to skip to the live edge.
The buffer is healthy. Playback proceeds at playbackRate = 1.0. No bitrate changes or frame skipping occur unless a transition out of OK is detected.
The buffer has grown too large relative to the configured latency target, meaning the client is receiving data faster than it is consuming it. This state triggers live-sync: the player increases playbackRate and may switch to a higher video quality rendition.

Configuring Buffer Thresholds

The default thresholds are 150 ms (low) and 550 ms (high), but these can be tuned for your network conditions:
// Tighten the window for ideal local-network conditions
player.bufferLimitLow = 200;   // ms — enter LOW below this
player.bufferLimitHigh = 500;  // ms — enter HIGH above this

// Read the computed midpoint (used for hysteresis)
console.log(player.bufferLimitMiddle); // e.g. 350 ms
bufferLimitMiddle is computed automatically as bufferLimitLow + Math.round((bufferLimitHigh - bufferLimitLow) / 2) whenever either threshold is set.

Hysteresis

State transitions include deliberate hysteresis to prevent rapid oscillation between states:
  • LOW → OK requires the buffer to rise above bufferLimitMiddle (not just above bufferLimitLow)
  • OK → LOW requires the buffer to fall below bufferLimitLow
  • OK → HIGH requires the buffer to exceed bufferLimitHigh
  • HIGH → OK requires the buffer to drop below bufferLimitMiddle (not merely below bufferLimitHigh)
This means the OK band acts as a stable operating region, and the player must cross the midpoint before recovering from either a LOW or a HIGH state.

How Buffer Amount Is Measured

The player computes bufferAmount from the HTML MediaSource buffered time ranges:
// From Player.ts
const currentTime = Math.max(this.currentTime, this.startTime);
bufferAmount = Math.max(0, Math.round((this.endTime - currentTime) * 1000));
// Result is in milliseconds
Where endTime is the furthest buffered media position and currentTime is the current playback position (clamped to startTime if playback hasn’t advanced yet). The result is always non-negative. onBufferChange() fires whenever bufferAmount changes by at least 50 ms (the BUFFER_CHANGE_STEP constant), keeping event frequency manageable while still reacting promptly to buffer shifts.

Playback Rate Adaptation

To gently approach the live edge without jarring jumps, the player adjusts <video>.playbackRate continuously based on buffer state. The default range is 0.84×–1.16×.

HIGH State — Speeding Up

When bufferState === HIGH and maxRate > 100, the rate increases linearly from a floor of 1.08× up to 1.16× (by default) based on how far the buffer exceeds bufferLimitHigh:
// From Player.ts — adjustPlaybackRate()
const ratio =
    Math.max(0, bufferAmount - bufferLimitHigh) /
    Math.max(1, 2 * (bufferLimitHigh - bufferLimitMiddle));

playbackRate = Math.round(
    PLAYBACK_RATE_MAX_FLOOR + (maxRate - PLAYBACK_RATE_MAX_FLOOR) * Math.min(ratio, 1)
) / 100;
// PLAYBACK_RATE_MAX_FLOOR = 108, PLAYBACK_RATE_MAX = 116
The minimum increase is always at least 1.08× when in HIGH state (enforced by PLAYBACK_RATE_MAX_FLOOR = 108). This floor prevents a too-small increase that would be ineffective at draining the buffer.

LOW State — Slowing Down

When bufferState === LOW and minRate < 100, the rate decreases linearly from 0.92× down to 0.84× (by default) based on how far the buffer falls below bufferLimitMiddle:
// From Player.ts — adjustPlaybackRate()
const ratio =
    Math.max(0, bufferLimitMiddle - bufferAmount) /
    Math.max(1, 2 * (bufferLimitMiddle - bufferLimitLow));

playbackRate = Math.min(
    Math.round(PLAYBACK_RATE_MIN_CEIL - (PLAYBACK_RATE_MIN_CEIL - minRate) * Math.min(ratio, 1)) / 100,
    currentPlaybackRate
);
// PLAYBACK_RATE_MIN_CEIL = 92, PLAYBACK_RATE_MIN = 84
The maximum decrease is capped at 0.92× as a ceiling (enforced by PLAYBACK_RATE_MIN_CEIL = 92). Decreases smaller than 8% are considered too timid to be helpful.

OK State

When bufferState === OK, playbackRate is reset to exactly 1.0.

Playback Rate Constants Summary

ConstantValueMeaning
PLAYBACK_RATE_MIN84Default minimum rate (0.84×)
PLAYBACK_RATE_MIN_CEIL92Forced ceiling for minimum rate (0.92×)
PLAYBACK_RATE_MAX_FLOOR108Forced floor for maximum rate (1.08×)
PLAYBACK_RATE_MAX116Default maximum rate (1.16×)

Customizing Playback Rate Range

You can narrow the range per platform or use case by overriding onBufferChange:
player.onBufferChange = () => {
    // Narrow range: slow down to 0.92× minimum, speed up to 1.08× maximum
    player.adjustPlaybackRate(92, 108);
};
On iOS / Safari, ManagedMediaSource is used instead of MediaSource. Playback rate changes on this path can produce audible audio glitches during live streaming. The default behavior still adapts the rate because the latency and stall-prevention benefits outweigh the cost for most use cases. If smooth audio matters more for your application, disable rate adaptation when ManagedMediaSource is detected:
player.onBufferChange = () => {
    if (window.ManagedMediaSource) {
        return; // iOS / Safari — skip playbackRate adjustments
    }
    player.adjustPlaybackRate();
};

Adaptive Bitrate Interaction

When the stream provides multiple video quality renditions (Multi-Bitrate / MBR), buffer state drives track selection automatically.
  • Buffer LOW + stall or aborted download — the player switches to a lower video track to match available bandwidth. The current download is aborted immediately to free the connection for the lower-quality request.
  • Buffer back above bufferLimitMiddle + bandwidth estimate allows — the player may switch to a higher video track. A “bandwidth emulation” fetch is used to probe whether the higher rendition fits within available throughput before committing.
  • Manual track selection — if you set player.videoTrack to a specific track index, MBR is disabled and the player stays on that track. Set player.videoTrack = undefined to re-enable automatic selection.
// Fix to a specific quality and disable MBR
player.videoTrack = 0; // highest quality index

// Re-enable automatic MBR
player.videoTrack = undefined;

Frame Skipping (Unreliable Mode)

Frame skipping is the most aggressive live-edge recovery mechanism and is active only when player.reliable = false (the default). It is managed by HTTPAdaptiveSource.

Sequence Skipping

When the buffer is critically low and the player is in the buffering state (recovering from a stall), HTTPAdaptiveSource calculates the delay to the live edge:
delay = metadata.liveTime - player.currentTime
If delay > maxSequenceDuration, the source increments the sequence number to skip that media segment entirely. This repeats until the delay fits within one sequence duration. Example: If the player is 2.5 seconds behind and sequences are 1 second long, it skips sequences n and n+1 and downloads n+2 directly.

First-Frame-Only Download

For the lowest-quality video rendition (one with no lower fallback), when the buffer enters LOW state during normal playback (not a stall), HTTPAdaptiveSource performs a HEAD request to obtain the first-sample-length response header. If available, it downloads only the key frame of the segment using a byte-range request. This provides a visual update for the viewer while consuming minimal bandwidth. When the downloaded key frame is reached, or if the buffer drops to zero mid-download, the video transfer is aborted and the remaining duration is padded to cover the full segment’s worth of time.

Audio Priority

In severe congestion scenarios, audio continuity is always preserved. Audio tracks use the skippable controller (sequence-level skipping), while video tracks in the last rendition use the alterable controller (frame-level dropping). This means audio remains continuous even when video frames are skipped, which is less jarring to the viewer than broken audio.

The Stall Recovery Flow

1

Stall Detected

The HTMLVideoElement fires a waiting event. The player checks whether bufferAmount <= bufferLimitLow to confirm this is a genuine stall (not just a programmatic seek).
2

Player Enters Buffering State

player.buffering becomes true. BufferState is forced to LOW. The video is paused. A data-timeout timer is started (default ~14 seconds) to detect complete network failure. onStall() is fired.
3

Source Aborts In-Flight Downloads

HTTPAdaptiveSource receives the Stall event and immediately aborts all alterable and skippable controllers, freeing the HTTP/2 connection for new requests.
4

Sequence Skipping

The source calculates the delay to the live edge. If the delay exceeds maxSequenceDuration, it advances the sequence number to skip the gap. It verifies via a HEAD request that the target sequence exists before committing to the skip.
5

Buffering Until Middle

The player resumes downloading from the new sequence position. It remains in the buffering state until bufferAmount >= bufferLimitMiddle, ensuring a stable cushion before resuming playback.
6

Playback Resumes

Once bufferAmount >= bufferLimitMiddle, player.buffering becomes false, BufferState transitions to OK, and video.play() is called.

Buffer Events

The Player class emits three buffer-related events that you can override:
EventWhen it firesTypical use
onBufferState(oldState)When bufferState transitions to a new valueLogging, UI indicator updates
onBufferChange()When bufferAmount changes by ≥ 50 msDriving adjustPlaybackRate() (default behavior)
onStall()When a stall is detected (before recovery begins)Logging, analytics
player.onBufferState = (oldState) => {
    console.log(`Buffer: ${oldState}${player.bufferState} (${player.bufferAmount}ms)`);
};

player.onStall = () => {
    console.warn('Stall detected — attempting live-edge recovery');
};

Configuring Network Profiles

Buffer thresholds should be tuned based on the network conditions between the streamer and viewer. The following profiles are example configurations taken from the example player — they are not library defaults:
ProfileDescriptionbufferLimitLowbufferLimitHighExpected latency ≈
IdealStreamer and viewer are geographically close, minimal hops200 ms500 ms350 ms
NormalStable and reliable network, geographical distance negligible400 ms800 ms600 ms
DistantStreamer and viewer are far apart or many intermediaries (VPNs, CDN)1000 ms2000 ms1500 ms
UnstableMaximum resilience for challenging networks; latency secondary2000 ms4000 ms3000 ms
// Example: configure for a distant viewer
player.bufferLimitLow = 1000;
player.bufferLimitHigh = 2000;

// Example: configure for ideal local-network conditions
player.bufferLimitLow = 200;
player.bufferLimitHigh = 500;
After an intentional pause (e.g., the user clicks pause mid-stream), use player.goLive() to seek back to the live edge when resuming. This avoids the player replaying stale buffered content from the paused position:
// Resume and snap back to live
player.paused = false;
player.goLive('user resumed');
goLive() moves currentTime to Math.max(startTime, endTime - bufferLimitMiddle / 1000), placing the player at the target buffer midpoint rather than the raw live edge, which reduces the risk of an immediate stall.

Build docs developers (and LLMs) love