Skip to content

Conversation

@yume-chan
Copy link
Owner

@yume-chan yume-chan commented Jan 6, 2026

Summary by CodeRabbit

  • New Features

    • Stream-based decoding primitives (VideoDecoderStream), renderer flow-control (RendererController), public PauseController/PerformanceCounter, monotonic increasingNow(), exported noop, writable frame streams, canvas device-pixel option and WebGL renderer options.
  • Refactor

    • Decoder and renderer APIs remodeled around Transform/Writable streams; renderers expose writable streams and pipeline metrics (decodeQueueSize, framesRendered/presented/skippedRendering).
  • Breaking changes

    • Several public types/return signatures renamed or changed (including PTY/ADB types), some codec utilities and re-exports removed, and resume/dispose return shapes adjusted.

✏️ Tip: You can customize this high-level summary in your review settings.

@diffray diffray bot added the diffray-review-started diffray review status: started label Jan 6, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Rename ADB PTY types; convert scrcpy decoders and renderers to TransformStream-based pipelines; promote PauseController and PerformanceCounter to public exports; add VideoDecoderStream and increasingNow(); update public types, method signatures, and metric getters across decoders and renderers.

Changes

Cohort / File(s) Summary
ADB Subprocess PTY
libraries/adb/src/commands/subprocess/pty.ts, libraries/adb/src/commands/subprocess/none/pty.ts, libraries/adb/src/commands/subprocess/shell/pty.ts, libraries/adb/src/commands/subprocess/none/service.ts, libraries/adb/src/commands/subprocess/shell/service.ts
Rename AdbPtyProcess<T>AdbPty<T>; rename classes AdbNoneProtocolPtyProcessAdbNoneProtocolPty, AdbShellProtocolPtyProcessAdbShellProtocolPty; update imports, implementations, and factory return types.
TinyH264 — Pause & Performance
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts, libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts, libraries/scrcpy-decoder-tinyh264/src/types.ts
Promote PauseController (now extends TransformStream) and PerformanceCounter to public exports; change PauseController I/O (add skipRendering), rename metrics (framesDrawnframesRendered, framesSkippedframesSkippedRendering), and make resume() non-async/MaybePromiseLike.
TinyH264 — Decoder
libraries/scrcpy-decoder-tinyh264/src/decoder.ts
Rewire decoder to a pipeline using public PauseController/PerformanceCounter; add getters (paused, writable, framesRendered, framesPresented, framesSkippedRendering); resume() now returns undefined; move configuration handling to #configure.
WebCodecs — Codec Types
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts
Replace decode-based CodecDecoder interface with TransformStream-based CodecDecoder type alias and CodecDecoder namespace (Input, Config, Output); remove CodecDecoderOptions; simplify constructor shape.
WebCodecs — Codec Implementations
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts, .../h264.ts, .../h265.ts, .../h26x.ts
Convert codec classes to TransformStream<CodecDecoder.Input, CodecDecoder.Output> subclasses; simplify constructors; change configure() to return CodecDecoder.Config; emit config/EncodedVideoChunk from transforms.
WebCodecs — Stream Utilities & Time
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts, libraries/scrcpy-decoder-webcodecs/src/video/time.ts
Add VideoDecoderStream (TransformStream wrapper for VideoDecoder) with queue/skip logic and observability (decodeQueueSize, framesDecoded, framesSkipped); add increasingNow() monotonic timestamp helper.
WebCodecs — Main Decoder
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
Rework WebCodecsVideoDecoder into streaming pipeline (PauseController → Transform → InspectStream → VideoDecoderStream); Options now pick hardwareAcceleration/optimizeForLatency; rename/add getters (e.g., framesRendered, framesPresented, framesSkippedRendering, decodeQueueSize, totalDecodeTime); resume() returns undefined.
Render Flow & Renderers
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts, .../canvas.ts, .../insertable-stream.ts, .../type.ts, .../webgl.ts, .../bitmap.ts, .../index.ts, .../snapshot.ts
Add RendererController (transform-based flow control + performance counters); shift renderers to writable: WritableStream<VideoFrame> API, add renderer options namespaces, update constructors/resize handling and disposal signatures; expose flow-control re-export.
Decoder Utilities & Exports
libraries/scrcpy-decoder-tinyh264/src/decoder.ts, libraries/scrcpy-decoder-webcodecs/src/video/codec/index.ts, libraries/scrcpy-decoder-webcodecs/src/video/codec/utils.ts
Export noop; remove some codec utils exports (hexDigits, hexTwoDigits, decimalTwoDigits) and drop re-export of ./utils.js; update imports to use public PauseController/PerformanceCounter.
Struct / Tooling / Editor
libraries/struct/src/utils.ts, .vscode/settings.json
Export SharedEncoder and add encodeInto typing; add unconfigured to cSpell words.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Source as Packet Source
participant Pause as PauseController
participant Inspect as InspectStream
participant Decoder as VideoDecoderStream
participant Renderer as RendererController
Note over Source,Renderer: Streaming pipeline replaces direct decode calls
Source->>Pause: push configuration/data packet
alt paused
Pause-->>Pause: buffer config & frames (tag skipRendering)
else resumed
Pause->>Inspect: forward packets (with skip flags)
Inspect->>Decoder: forward packets / config
Decoder->>Decoder: produce VideoFrame(s)
Decoder->>Renderer: enqueue VideoFrame
Renderer->>Renderer: present/draw frame
end
Note right of Renderer: update metrics (framesRendered / framesPresented / framesSkippedRendering)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: converting the webcodecs decoder architecture from callback/promise-based to stream-based using TransformStream.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI Agents
In @libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts:
- Line 211: The dispose() method can throw if called before the microtask that
initializes this.#controller completes; guard the call by checking that
this.#controller is defined before invoking terminate(). Update dispose() to do
something like: if (this.#controller) this.#controller.terminate(); (or
null-check and no-op) so calling dispose() immediately after construction is
safe without relying on initialization timing for the controller.
- Around line 142-152: The code in resume() can access frames[frames.length - 1]
when this.#pendingFrames is empty causing a spread of undefined; add a guard at
the start (e.g., const frames = this.#pendingFrames.slice(); if (frames.length
=== 0) return;) to bail out early, then proceed with the existing loop and final
enqueue; reference this.#pendingFrames, resume(), and this.#controller.enqueue
when making the change.
- Around line 50-55: The constructor defers setting private field #controller
via "await Promise.resolve()", which can leave #controller undefined if
dispose(), resume(), or #resumeInternal() are invoked synchronously after
construction; fix by ensuring #controller is assigned synchronously in the
constructor (remove the microtask await) or, alternatively, introduce a ready
promise (e.g., this.controllerReady) that resolves when controller is set and
await that promise at the start of dispose(), resume(), and #resumeInternal() to
guard access; update references in constructor, dispose(), resume(), and
#resumeInternal() accordingly.

In @libraries/scrcpy-decoder-webcodecs/src/video/time.ts:
- Around line 21-28: The increasingNow function violates strict monotonicity by
calling nextUp(now) when now <= prevValue; change the branch so it calls
nextUp(prevValue) instead, assign that result to prevValue, and return it;
ensure you still call performance.now() to get now, compare with prevValue, and
only call nextUp(prevValue) when now <= prevValue so the returned value is
strictly greater than the prior prevValue.

In @libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts:
- Around line 71-84: The code calls this.#decoder.configure(this.#config!) after
resetting the decoder but uses a non-null assertion for the optional field
this.#config; add an explicit runtime guard before calling configure (check if
this.#config is falsy and throw a clear Error such as "Decoder not configured")
so decoding paths like the keyframe handler in video-decoder-stream.ts validate
configuration instead of relying on the protocol; keep the throw synchronous so
callers see a clear error and only call this.#decoder.configure when
this.#config is present.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (1)

114-139: Consider logging or handling pipeline errors instead of silently swallowing them.

The .catch(noop) on line 138 silently discards any errors from the streaming pipeline. While the pipeTo promise is intentionally not awaited (fire-and-forget), silently swallowing errors could make debugging difficult if the pipeline fails unexpectedly.

Based on learnings, configuration failures should propagate up rather than being recovered locally. Consider at minimum logging the error for diagnostic purposes.

🔎 Proposed improvement
         void this.#pause.readable
             .pipeTo(
                 new WritableStream({
                     write: async (packet) => {
                         // ... existing logic
                     },
                 }),
             )
-            .catch(noop);
+            .catch((error) => {
+                // Pipeline errors are expected on dispose, but log unexpected errors
+                if (error && !(error instanceof TypeError && error.message.includes("terminated"))) {
+                    console.error("TinyH264Decoder pipeline error:", error);
+                }
+            });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5bd2e and 10024a2.

📒 Files selected for processing (18)
  • libraries/adb/src/commands/subprocess/none/pty.ts
  • libraries/adb/src/commands/subprocess/none/service.ts
  • libraries/adb/src/commands/subprocess/pty.ts
  • libraries/adb/src/commands/subprocess/shell/pty.ts
  • libraries/adb/src/commands/subprocess/shell/service.ts
  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
  • libraries/scrcpy-decoder-tinyh264/src/types.ts
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/time.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
  • libraries/struct/src/utils.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-03-30T19:57:44.305Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 739
File: apps/cli/src/index.ts:163-167
Timestamp: 2025-03-30T19:57:44.305Z
Learning: The none protocol in ADB doesn't pass exit codes (always returns 0), unlike the shell protocol. The API is designed to make this distinction clear by using `exited` instead of `exit` with a code parameter for none protocol.

Applied to files:

  • libraries/adb/src/commands/subprocess/none/pty.ts
  • libraries/adb/src/commands/subprocess/pty.ts
  • libraries/adb/src/commands/subprocess/none/service.ts
📚 Learning: 2025-08-23T14:04:38.056Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 786
File: libraries/adb/src/commands/sync/sync.ts:154-157
Timestamp: 2025-08-23T14:04:38.056Z
Learning: Both ADB none protocol (`exec:`) and shell protocol (`shell:`) are executed by shell using `sh -c ...` on the Android side and require proper argument escaping. The two protocols only differ in how standard input/output are encoded for transmission, not in their execution model. The `escapeArg()` function should be used for both protocols to prevent command injection when paths contain special characters.

Applied to files:

  • libraries/adb/src/commands/subprocess/shell/service.ts
  • libraries/adb/src/commands/subprocess/shell/pty.ts
  • libraries/adb/src/commands/subprocess/none/service.ts
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
📚 Learning: 2025-08-14T11:06:21.851Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 780
File: libraries/adb-credential-web/src/storage/index.ts:1-5
Timestamp: 2025-08-14T11:06:21.851Z
Learning: The yume-chan/adb-credential-web package doesn't use "exports" in package.json for better compatibility. Instead, they re-export everything from the main entrypoint rather than using subpath exports.

Applied to files:

  • libraries/adb/src/commands/subprocess/none/service.ts
🧬 Code graph analysis (13)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPerformanceCounter (15-36)
libraries/adb/src/commands/subprocess/none/pty.ts (1)
libraries/adb/src/commands/subprocess/pty.ts (1)
  • AdbPty (8-15)
libraries/adb/src/commands/subprocess/shell/service.ts (1)
libraries/adb/src/commands/subprocess/shell/pty.ts (1)
  • AdbShellProtocolPty (20-112)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (13-15)
  • Output (22-22)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (2)
libraries/adb-scrcpy/src/video.ts (3)
  • data (76-79)
  • data (81-84)
  • data (86-100)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • CodecDecoder (7-10)
  • Config (17-20)
libraries/adb/src/commands/subprocess/shell/pty.ts (1)
libraries/adb/src/commands/subprocess/pty.ts (1)
  • AdbPty (8-15)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (4)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
  • Config (17-20)
libraries/stream-extra/src/concat.ts (1)
  • concatUint8Arrays (120-136)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (2)
  • Input (221-221)
  • Output (222-224)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • CodecDecoder (7-10)
  • Config (17-20)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
libraries/media-codec/src/av1.ts (1)
  • Av1 (197-762)
libraries/adb/src/commands/subprocess/none/service.ts (1)
libraries/adb/src/commands/subprocess/none/pty.ts (1)
  • AdbNoneProtocolPty (12-45)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)
  • PauseController (12-218)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
🪛 Biome (2.1.2)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts

[error] 296-296: Unexpected constant condition.

(lint/correctness/noConstantCondition)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: diffray code review
  • GitHub Check: build (20.x)
🔇 Additional comments (40)
libraries/struct/src/utils.ts (2)

11-14: LGTM! Adding standard TextEncoder method.

The encodeInto method is part of the standard TextEncoder API and is more efficient than encode() for performance-critical paths where you can reuse buffers. The signature correctly matches the specification.


32-32: LGTM! Exposing shared encoder instance.

Making SharedEncoder public is a reasonable optimization that allows consuming code to reuse the singleton instance. Since TextEncoder is stateless, sharing the instance is safe.

libraries/adb/src/commands/subprocess/shell/service.ts (3)

5-5: LGTM! Import properly updated.

The import correctly references the renamed AdbShellProtocolPty class.


42-42: LGTM! Return type properly updated.

The return type correctly reflects the renamed class.


62-62: LGTM! Instantiation properly updated.

The instantiation correctly uses the renamed AdbShellProtocolPty class.

libraries/adb/src/commands/subprocess/none/pty.ts (2)

10-10: LGTM! Import properly updated.

The import correctly references the renamed AdbPty interface.


12-45: LGTM! Class rename with no behavioral changes.

The class rename from AdbNoneProtocolPtyProcess to AdbNoneProtocolPty is cleaner and more consistent. The implementation remains unchanged, maintaining all functionality.

libraries/adb/src/commands/subprocess/shell/pty.ts (2)

16-16: LGTM! Import properly updated.

The import correctly references the renamed AdbPty interface.


20-112: LGTM! Class rename with no behavioral changes.

The class rename from AdbShellProtocolPtyProcess to AdbShellProtocolPty is cleaner and more consistent. The implementation remains unchanged, maintaining all functionality including exit code handling, stdout streaming, and terminal control.

libraries/adb/src/commands/subprocess/none/service.ts (3)

4-4: LGTM! Import properly updated.

The import correctly references the renamed AdbNoneProtocolPty class.


45-45: LGTM! Return type properly updated.

The return type correctly reflects the renamed class.


55-55: LGTM! Instantiation properly updated.

The instantiation correctly uses the renamed AdbNoneProtocolPty class.

libraries/adb/src/commands/subprocess/pty.ts (1)

8-15: LGTM! Clean interface rename.

The interface rename from AdbPtyProcess to AdbPty is cleaner and more concise. All references across the codebase have been properly updated, including the implementing classes (AdbNoneProtocolPty and AdbShellProtocolPty) and all imports. The interface contract remains unchanged with no breaking modifications.

libraries/scrcpy-decoder-tinyh264/src/types.ts (2)

15-36: LGTM!

The renamed properties (framesRendered, framesPresented, framesSkippedRendering) provide clearer semantics, and the documentation accurately explains the distinction between rendered and presented frames, including the Chrome bug caveat.


52-52: Good flexibility with MaybePromiseLike.

Changing the return type from Promise<undefined> to MaybePromiseLike<undefined> allows implementations to return synchronously when no async work is needed, which is a good performance-conscious design choice.

libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)

3-17: LGTM!

The class correctly implements ScrcpyVideoDecoderPerformanceCounter with the renamed getters. Keeping the internal field names (#framesDrawn, #framesSkipped) while renaming only the public API is a reasonable choice that minimizes churn.

libraries/scrcpy-decoder-tinyh264/src/decoder.ts (3)

44-51: LGTM!

Clean delegation to PauseController for both the paused state and writable stream. This aligns well with the stream-based architecture.


64-90: LGTM!

The performance counter getters are properly delegated with clear documentation that matches the interface definitions.


223-225: Verify return type matches interface.

The resume() method now returns undefined synchronously, while the interface ScrcpyVideoDecoderPauseController.resume() specifies MaybePromiseLike<undefined>. This is compatible since undefined satisfies MaybePromiseLike<undefined>, but ensure this is intentional given the previous async behavior.

libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

12-15: Good design: TransformStream-based pause controller with typed namespace.

The PauseController extending TransformStream is a clean streaming-based design. The namespace exports provide well-typed Input and Output types that make the data flow explicit.

Also applies to: 220-225

libraries/scrcpy-decoder-webcodecs/src/video/time.ts (1)

3-17: LGTM!

The nextUp implementation correctly handles special cases and uses bit manipulation to compute the next representable float64 value.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)

48-49: Clarify the async Promise.resolve pattern.

The await Promise.resolve() pattern is unusual. If this is intentional to defer initialization to the next microtask, consider adding a comment explaining why.


64-69: LGTM!

Configuration handling is correct - stores the config for potential decoder resets and immediately configures the decoder.


86-89: LGTM!

Flush handling correctly waits for pending decodes before closing the decoder.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (5)

195-259: LGTM! Streaming pipeline is well-structured.

The pipeline composition correctly chains pause control, timestamp annotation, codec decoding, configuration inspection, frame decoding, and rendering. The use of increasingNow() for timestamps enables both decode time tracking and frame skipping logic.

Note: Line 259 uses catch(noop) which silently suppresses errors. This appears intentional for cleanup scenarios, but ensure critical errors are logged elsewhere if needed.


262-299: LGTM! The do-while loop is correct.

The while (true) at line 296 is used correctly in a do-while loop with an explicit break condition at line 294. This pattern ensures all queued frames are rendered in sequence, minimizing latency. The static analysis warning is a false positive.

The frame queuing strategy (lines 264-271) correctly handles backpressure by dropping intermediate frames when the renderer can't keep up.


347-357: LGTM! Options interface is appropriately simplified.

The interface correctly exposes only the relevant decoder configuration options while requiring the essential codec and renderer parameters.


320-326: LGTM!

Pause/resume methods correctly delegate to the PauseController.


179-193: LGTM!

Codec selection logic is clear and handles unsupported codecs appropriately.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (1)

7-15: LGTM! Configuration method aligns with streaming architecture.

The configure method correctly returns a CodecDecoder.Config object instead of performing side effects, which aligns with the new stream-based decoder architecture.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)

7-18: LGTM! Configuration method matches H264 pattern.

The configure method correctly returns a CodecDecoder.Config object. The comment about Microsoft Edge's explicit size requirement (lines 12-14) provides valuable context for the implementation.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)

5-8: Clean stream-based refactor with proper Annex B handling.

The class now properly extends TransformStream with the appropriate input/output types. The design cleanly separates configuration from data processing.


11-42: Transform logic is correct for H.264/H.265 Annex B format.

The implementation correctly:

  1. Stores configuration and emits the parsed config on configuration packets
  2. Concatenates configuration with the first frame data (per WebCodecs AVC/HEVC Annex B requirements)
  3. Clears the stored config after use to avoid re-concatenation
  4. Treats undefined keyframe as key (necessary for proper decoding)

The reference to the W3C WebCodecs spec in the comment is helpful for maintainability.


44-44: Abstract method signature aligns with the streaming design.

The configure method now returns CodecDecoder.Config instead of void, enabling the transformer to emit configuration downstream. This is consistent with the Output type definition.

libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)

6-9: Consistent streaming pattern with H26x decoder.

The class properly extends TransformStream with the same input/output types, maintaining consistency with the other codec implementations.


18-30: Config emission depends on sequence header presence in each packet.

The implementation correctly parses for sequence headers on every data packet and emits config when found. For AV1, sequence headers can appear in any keyframe, so this on-demand config emission is appropriate.

One consideration: if the downstream consumer requires config before processing chunks, ensure it handles the case where the first few packets might not contain a sequence header. Based on the Output type being Config | EncodedVideoChunk, downstream should be prepared to handle either.


32-39: EncodedVideoChunk creation is correct.

The keyframe handling (packet.keyframe === false ? "delta" : "key") is consistent with the H26x implementation and the comment explains the rationale clearly.

libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)

7-10: Clean type alias design for streaming decoders.

The CodecDecoder type alias cleanly defines the streaming contract. Using a namespace for associated types (Input, Config, Output) is a good TypeScript pattern for grouping related types.


25-27: Simplified constructor signature.

The parameterless constructor is cleaner and aligns with the new streaming architecture where decoders are self-contained TransformStreams.


12-22: Well-structured type definitions.

The types are well-designed:

  • Input correctly unions configuration and timestamped data packets
  • Config extends VideoDecoderConfig with required dimensions (needed for sizing)
  • Output union enables emitting both config and encoded chunks

The tsconfig includes "DOM" lib, which provides the WebCodecs API types (VideoDecoderConfig and EncodedVideoChunk) as globals, so they are properly available.

@diffray diffray bot added diffray-review-failed diffray review status: failed and removed diffray-review-started diffray review status: started labels Jan 6, 2026
@diffray diffray bot added diffray-review-started diffray review status: started and removed diffray-review-failed diffray review status: failed labels Jan 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

120-167: Non-null assertions unsafe due to constructor race condition.

Lines 142 and 157 use non-null assertions (this.#controller!) which are unsafe given the asynchronous controller initialization in the constructor. These assertions will throw if #resumeInternal is called before the microtask completes.

The iteration logic using entries() correctly handles empty arrays and properly assigns the skipRendering flag.

🤖 Fix all issues with AI Agents
In @libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts:
- Around line 52-63: The constructor's start handler defers assigning the
private field #controller via await Promise.resolve(), causing a race where
dispose(), resume(), or other methods may access an undefined #controller;
remove the unnecessary await Promise.resolve() so the assignment
this.#controller = controller happens synchronously in the start callback, and
also add a defensive guard (e.g., if (!this.#controller) return or no-op) in
dispose() and resume() to avoid non-null assertion failures when they are called
before controller is set.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10024a2 and 72c460b.

📒 Files selected for processing (4)
  • .vscode/settings.json
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/time.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
✅ Files skipped from review due to trivial changes (1)
  • .vscode/settings.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
🧬 Code graph analysis (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (13-15)
  • Output (22-22)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • frame (262-299)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build (20.x)
  • GitHub Check: diffray code review
🔇 Additional comments (9)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)

64-103: Transform logic correctly handles pause/resume flow.

The transform callback properly manages paused state by queuing configuration and data packets, and correctly awaits any in-progress resume operation before enqueueing new packets. The use of the controller parameter (not this.#controller) avoids race condition issues.


169-171: Resume signature correctly implements interface.

The return type change from MaybePromiseLike<undefined> to undefined is valid, as undefined satisfies the interface requirement for MaybePromiseLike<undefined>.


213-221: Good defensive programming with optional chaining.

Line 220 correctly uses optional chaining (this.#controller?.terminate()) to handle the case where #controller might be undefined. This resolves the past crash issue, though the root cause (constructor race condition) should still be addressed.


229-234: Clean public API definition.

The namespace exports provide a clear and well-structured public API for the PauseController, with Input and Output types that accurately reflect the streaming transformation.

libraries/scrcpy-decoder-webcodecs/src/video/time.ts (2)

1-17: Correct IEEE-754 implementation for next representable value.

The nextUp function correctly implements IEEE-754 bit manipulation to compute the next representable double-precision value. Special cases (NaN, Infinity, zero) are properly handled, and the bit arithmetic correctly increments for positive values and decrements for negative values.


21-28: Monotonicity correctly implemented.

The function now correctly calls nextUp(prevValue) (line 24) instead of nextUp(now) when now <= prevValue, ensuring strict monotonicity as required. The past review issue has been resolved.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)

46-63: Decoder initialization correctly structured.

The constructor properly initializes the VideoDecoder within the TransformStream's start callback, ensuring the decoder is ready before any transform operations. The output callback correctly increments the frames counter and enqueues frames, while the error callback propagates errors appropriately.


64-73: Config handling with appropriate guards.

Lines 65-68 correctly save the configuration and configure the decoder. The guard at lines 71-73 ensures the decoder is configured before processing chunks, with a clear error message. This addresses the protocol requirement that configuration must precede data chunks.


90-93: Proper flush and cleanup logic.

The flush callback correctly awaits pending decode operations before closing the decoder, ensuring clean shutdown of the video pipeline.

@diffray diffray bot added diffray-review-failed diffray review status: failed and removed diffray-review-started diffray review status: started labels Jan 6, 2026
@yume-chan yume-chan removed the diffray-review-failed diffray review status: failed label Jan 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

50-98: Excellent fix for the race condition!

The closure pattern elegantly solves the previous race condition by capturing controller synchronously during super() construction, then assigning it to #controller immediately after. This ensures #controller is always initialized before any methods can access it.

The transform logic correctly handles:

  • Buffering configuration and frames when paused
  • Clearing pending frames on new keyframes
  • Blocking during resume with await this.#resuming
  • Enqueuing with appropriate skipRendering flags
Optional: Add clarifying comment

Consider adding a brief comment explaining the closure pattern for future maintainers:

     constructor() {
+        // Capture controller via closure to ensure synchronous initialization
         let controller!: TransformStreamDefaultController<PauseController.Output>;
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (1)

91-94: Consider adding cleanup for abnormal stream termination.

While the flush callback properly closes the decoder, this only runs on normal stream completion. If the stream is cancelled or errors before flush completes, the decoder may remain open (though it will eventually be garbage collected).

For cleaner resource management, consider adding an abort signal or catch handler to explicitly close the decoder on abnormal termination.

💡 Optional enhancement for robust cleanup

You could capture abort/cancel by listening to the controller's abort signal or by adding error handling in the transform. However, given the current architecture and the fact that the decoder will be GC'd eventually, this is more of a nice-to-have than a must-fix.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72c460b and 95b46ba.

📒 Files selected for processing (2)
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
🧬 Code graph analysis (2)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (1)
  • frame (262-299)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (4)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (2)
  • Input (13-15)
  • Output (22-22)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPauseController (38-63)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (10)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)

145-156: Resume logic correctly handles all cases.

The iteration using .entries() safely handles empty #pendingFrames arrays (the loop simply won't execute), and the skipRendering logic correctly marks all frames except the last one. This resolves the previous critical issue about accessing empty arrays.

The comment at lines 145-147 accurately explains the synchronization guarantee.


164-165: Disposal and return type are correct.

  • The resume() return type undefined correctly satisfies the MaybePromiseLike<undefined> interface requirement.
  • dispose() safely calls terminate() because #controller is always initialized via the constructor's closure pattern, resolving the previous crash concern.

Also applies to: 208-221


224-229: Clean type exports using namespace pattern.

The namespace pattern cleanly associates Input and Output types with the PauseController class, making the API surface clear and type-safe.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (7)

1-7: LGTM! Clean stream-based architecture.

The class properly extends TransformStream with appropriate input/output types. The union input type VideoDecoderConfig | EncodedVideoChunk elegantly handles both configuration and data packets in a single stream, aligning well with the PR's goal of converting to stream-based decoders.


8-44: Well-designed public API surface.

The exposed metrics (decodeQueueSize, framesDecoded, framesSkipped) and onDequeue event provide excellent observability into decoder behavior, which is essential for monitoring performance and latency in streaming video applications.


46-64: Proper decoder initialization and event wiring.

The initialization pattern correctly creates the VideoDecoder in the start callback and wires up output/error handlers. The error propagation on line 58 ensures configuration failures and decode errors bubble up the pipeline as expected.


65-70: Config handling looks correct.

The type guard on line 66 appropriately distinguishes VideoDecoderConfig from EncodedVideoChunk. Storing the config before calling configure() is essential for the keyframe reset logic later in the transform.


72-74: Excellent defensive coding.

The explicit state check with a clear error message ensures that encoded chunks are only processed after configuration. This validation makes the non-null assertion on line 86 safe, as the decoder can only be configured through line 68, which always sets #config first.


76-87: Smart latency management with justified non-null assertion.

The keyframe-based frame dropping strategy effectively caps latency when the decoder can't keep up. The non-null assertion on line 86 is safe because the state check on line 72 guarantees the decoder was previously configured via line 68, which always sets #config before calling configure().


89-89: LGTM!

The decode call is straightforward and safe at this point, with proper error handling wired through the decoder's error callback.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts:
- Around line 129-141: The snapshot() method lacks an explicit return type;
update its signature to declare the correct async return type (Promise<Blob |
undefined>) so callers and the compiler know it returns either a Blob or
undefined, keeping the existing logic that borrows a VideoFrameCapturerPool,
calls capturer.capture(frame), and returns that result; ensure the method
signature on the snapshot() function is changed accordingly.
- Around line 102-106: The close handler currently only closes
this.#readableController and disposes this.#counter, risking leaked VideoFrame
objects; update the close function where the ReadableStream is created (the
close: () => { ... } block) to also check and close any pending VideoFrame
references by calling close() on this.#captureFrame and this.#nextFrame if they
are non-null and then nulling them out before disposing the counter and closing
the controller so all frames are released at shutdown.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts:
- Around line 1-5: The interface uses the nonstandard property name `writeable`;
change `VideoFrameRenderer.writeable` to `writable` to match the standard stream
API and the `RendererController` usage in `flow-control.ts`, then update all
implementations and references to `VideoFrameRenderer` (including any
classes/functions that consume or construct that interface) to use `writable` so
names are consistent across the codebase.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts:
- Around line 14-60: createProgram may receive null from gl.createProgram() if
the WebGL context is lost; add an immediate null check after const program =
gl.createProgram() in createProgram and handle it before calling
gl.attachShader/linkProgram. Specifically, if program is null, delete the
vertexShader and fragmentShader (to avoid leaks) and throw a clear Error (e.g.,
"Failed to create program: WebGL context lost") so subsequent
gl.attachShader/gl.linkProgram calls never run on a null program.

In @libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts:
- Around line 13-16: The explicit cast "(this.#canvas as HTMLCanvasElement)" is
unnecessary because both OffscreenCanvas and HTMLCanvasElement support
getContext("bitmaprenderer"); remove the cast and call
this.#canvas.getContext("bitmaprenderer", { alpha: false })! to preserve type
safety; if TypeScript still complains about the union type, either add a short
comment explaining why a cast is required or narrow the type first (e.g., check
instanceof HTMLCanvasElement or assert via a single localized cast) while
keeping the comment explaining the reason.
🧹 Nitpick comments (5)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (1)

18-30: Consider caching to avoid redundant sequence header parsing.

The code instantiates a new Av1 parser and searches for sequence headers on every packet. Since AV1 sequence headers typically appear only once at stream start, this creates unnecessary overhead for the majority of packets that won't contain sequence headers.

Consider tracking whether a configuration has already been emitted and skipping the parsing for subsequent packets, unless dynamic resolution changes are expected.

♻️ Potential optimization approach
+    private configSent = false;
+
     constructor() {
         super({
             transform: (packet, controller) => {
                 if (packet.type === "configuration") {
                     // AV1 decoder doesn't need configuration packets
                     return;
                 }
 
-                const parser = new Av1(packet.data);
-                const sequenceHeader = parser.searchSequenceHeaderObu();
-
-                if (sequenceHeader) {
+                if (!this.configSent) {
+                    const parser = new Av1(packet.data);
+                    const sequenceHeader = parser.searchSequenceHeaderObu();
+
+                    if (sequenceHeader) {
-                    const width = sequenceHeader.max_frame_width_minus_1 + 1;
-                    const height = sequenceHeader.max_frame_height_minus_1 + 1;
-
-                    controller.enqueue({
-                        codec: Av1.toCodecString(sequenceHeader),
-                        codedWidth: width,
-                        codedHeight: height,
-                    });
+                        const width = sequenceHeader.max_frame_width_minus_1 + 1;
+                        const height = sequenceHeader.max_frame_height_minus_1 + 1;
+
+                        controller.enqueue({
+                            codec: Av1.toCodecString(sequenceHeader),
+                            codedWidth: width,
+                            codedHeight: height,
+                        });
+                        this.configSent = true;
+                    }
                 }
 
                 controller.enqueue(

Note: If the stream supports dynamic resolution changes, you may want to check for sequence headers periodically rather than only once.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (1)

13-15: Consider documenting the pool capacity choice.

The pool capacity of 4 appears to be a magic number. Consider adding a comment explaining the rationale for this size (e.g., expected concurrent snapshot operations, memory constraints, etc.).

📝 Suggested documentation improvement
+// Pool size of 4 allows up to 4 concurrent snapshot operations
+// while keeping memory usage bounded
 const VideoFrameCapturerPool =
     /* #__PURE__ */
     new Pool(() => new VideoFrameCapturer(), 4);
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (1)

11-14: Consider aligning bitmaprenderer context configurations across renderers.

The alpha: true configuration in bitmap.ts follows MDN best practices to avoid the performance cost of alpha: false. However, snapshot.ts (line 15) still uses alpha: false for its ImageBitmapRenderingContext in the VideoFrameCapturer class. While the use cases differ (rendering vs. capturing), inconsistent configurations can create confusion. Either align both to use alpha: true following the MDN guidance, or document why snapshot.ts requires alpha: false.

libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (1)

24-26: Consider renaming getter to match standard spelling.

The getter is named writeable but exposes writable from the generator. The standard spelling is "writable" (as used in the Web Streams API and line 25). Consider renaming for consistency.

📝 Suggested rename
-    get writeable() {
+    get writable() {
         return this.#generator.writable;
     }
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

19-35: Frame lifecycle looks correct, but consider getter naming.

The stream implementation properly manages frame lifecycle by closing the previous frame before saving the new one. The close/abort handlers correctly call dispose().

However, the getter is named writeable (non-standard spelling) instead of writable (standard Web Streams API spelling). This inconsistency also appears in insertable-stream.ts.

📝 Suggested rename for consistency
-    get writeable() {
+    get writable() {
         return this.#writable;
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95b46ba and bf514f2.

📒 Files selected for processing (16)
  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
  • libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/utils.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
💤 Files with no reviewable changes (2)
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/utils.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-tinyh264/src/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (8)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
libraries/stream-extra/src/consumable.ts (1)
  • WritableStream (71-71)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (4)
libraries/scrcpy-decoder-webcodecs/src/video/pool.ts (1)
  • Pool (1-37)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPerformanceCounter (15-36)
libraries/stream-extra/src/push-readable.ts (2)
  • PushReadableStreamController (6-14)
  • PushReadableStream (41-307)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (2)
libraries/scrcpy-decoder-tinyh264/src/decoder.ts (1)
  • canvas (40-42)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)
  • canvas (8-10)
libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (2)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/event/src/event-emitter.ts (1)
  • EventEmitter (12-61)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
libraries/media-codec/src/av1.ts (1)
  • Av1 (197-762)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (3)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (4)
  • CodecDecoder (7-10)
  • Input (13-15)
  • Output (22-22)
  • Config (17-20)
libraries/stream-extra/src/concat.ts (1)
  • concatUint8Arrays (120-136)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (2)
  • glCreateContext (11-59)
  • glLoseContext (61-67)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)
libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (1)
  • Options (205-212)
libraries/scrcpy-decoder-tinyh264/src/utils/gl.ts (1)
  • createCanvas (1-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (38)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (3)

1-4: LGTM!

The imports are clean and correctly reference the necessary dependencies for the stream-based codec implementation.


6-9: LGTM!

The class declaration correctly extends TransformStream and implements CodecDecoder, aligning with the PR's objective to convert decoders to a stream-based architecture.


32-39: LGTM!

The EncodedVideoChunk generation correctly handles the keyframe fallback—treating undefined as "key" is a safe default that ensures decodability. The packet data and timestamp are properly forwarded to the chunk.

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts (4)

1-1: LGTM!

The new imports are properly used: concatUint8Arrays for merging configuration with frame data (line 26), and TransformStream for the stream-based decoder architecture (line 6).


5-8: LGTM!

The refactor to extend TransformStream<CodecDecoder.Input, CodecDecoder.Output> correctly aligns the decoder with the stream-based architecture, replacing the previous direct VideoDecoder approach.


11-42: LGTM!

The transform logic correctly implements the stream-based decoding:

  • Configuration packets are stored and the configure() result is enqueued for downstream processing
  • Data packets properly handle Annex B format by concatenating configuration with the first frame (lines 25-27), then clearing the config
  • The keyframe type derivation (line 35) correctly treats undefined as "key" to ensure frames decode properly

The absence of error handling allows configuration failures to propagate up the client pipeline, which aligns with the intended error handling strategy.

Based on learnings, configuration failures should propagate to the client pipeline for proper error handling.


44-44: All subclasses have been updated to implement the new signature. Both H264Decoder and H265Decoder correctly override configure() with the return type CodecDecoder.Config, and the return values are properly enqueued into the stream.

libraries/scrcpy-decoder-webcodecs/src/video/video-decoder-stream.ts (2)

1-44: LGTM: Well-structured stream wrapper with clean metrics API.

The class structure is sound. The definite assignment assertion on line 8 is necessary because the field is assigned after the super() call. The public getters provide a clean observability surface.


46-103: LGTM: Stream implementation correctly handles decoder lifecycle.

The constructor properly implements the TransformStream pattern:

  • Start handler initializes the decoder with appropriate callbacks
  • Transform handler validates configuration state before decoding
  • Flush handler correctly awaits pending frames before closing
  • Cancel handler immediately closes on error

The implementation follows best practices for WebCodecs decoder management.

libraries/scrcpy-decoder-tinyh264/src/decoder.ts (4)

26-28: LGTM: Appropriate noop export for error handling.

Exporting the noop function supports the streaming pattern used throughout the refactoring, particularly for .catch(noop) on void promises.


44-90: LGTM: Improved metric naming and documentation.

The refactoring to use PauseController and PerformanceCounter improves the public API surface. The renamed metrics (framesRendered, framesSkippedRendering) better distinguish rendering from decoding stages, and the documentation clearly explains frame presentation semantics including the Chrome bug caveat.


114-138: LGTM: Streaming pipeline correctly handles TinyH264 constraints.

The refactored pipeline properly:

  • Validates decoder configuration before processing data packets
  • Documents the TinyH264 limitation regarding frame metadata and skipping
  • Copies packet data to avoid BufferCombiner issues
  • Uses the standard void/catch(noop) pattern for fire-and-forget error handling

208-208: No action required. The PerformanceCounter class correctly provides the increaseFramesRendered() method at line 54 in libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts, which increments the #framesDrawn counter. The method is properly used at line 208 in the decoder.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (5)

42-57: LGTM: Clean pause controller with timestamp-based frame skipping.

The PauseController integration and #skipFramesUntil mechanism provide a robust approach to handling frame skipping during pause/resume cycles using monotonic timestamps.


74-115: LGTM: Well-documented decode time tracking rationale.

The delegation to VideoDecoderStream is clean, and the comment at lines 104-108 clearly explains why totalDecodeTime is tracked at this level rather than in the stream—it includes pre-processing time and relies on timestamps set by this class.


117-147: LGTM: Comprehensive rendering metrics with clear documentation.

The RendererController integration provides good observability. The documentation on framesPresented (lines 127-135) effectively explains the latency optimization and documents the Chrome nested Worker limitation.


152-176: LGTM: Clean codec selection and well-typed Options interface.

The constructor properly instantiates codec-specific decoders with appropriate defaults. The Options interface cleanly extends relevant VideoDecoderConfig properties, and the default values (hardwareAcceleration = "no-preference", optimizeForLatency = true) align well with low-latency streaming requirements.

Also applies to: 269-279


177-240: No action required. Line 239 correctly uses renderer.writeable, which matches the VideoFrameRenderer interface definition in type.ts. While the standard Web Streams API uses writable, this codebase intentionally defines and consistently uses writeable as the property name across all VideoFrameRenderer implementations (CanvasVideoFrameRenderer, InsertableStreamVideoFrameRenderer).

Likely an incorrect or invalid review comment.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (4)

78-101: LGTM! Well-designed frame queuing with proper lifecycle management.

The write logic correctly handles:

  • Cloning frames for snapshot capture (Line 82) to avoid ownership conflicts
  • Skipping intermediate frames (Lines 88-91) to minimize latency
  • Fire-and-forget drawing (Line 100) to avoid blocking the writable stream

This design aligns with the official Scrcpy client's low-latency approach.


109-127: LGTM! Proper backpressure handling with frame draining.

The #draw method correctly:

  • Drains all queued frames in a loop (Lines 120-124)
  • Respects backpressure via await on enqueue (Line 122)
  • Updates performance metrics (Line 123)

The comments on Lines 112-117 provide excellent context for the latency-focused design choice.


71-77: LGTM! Proper abort signal wiring.

The writable's start handler correctly propagates cancellation from the readable stream to the writable stream (Lines 74-75), ensuring proper cleanup when the consumer cancels.


38-64: LGTM! Clean delegation to PerformanceCounter.

The performance counter getters correctly delegate to the internal PerformanceCounter instance with comprehensive JSDoc documentation that matches the interface contract.

libraries/scrcpy-decoder-webcodecs/src/video/render/index.ts (1)

5-5: LGTM! Properly exports the new flow-control module.

The export is consistent with the existing pattern and makes the RendererController class available as part of the public render API.

libraries/scrcpy-decoder-webcodecs/src/video/render/bitmap.ts (1)

6-7: Reference the HTML spec clarifying ImageBitmap width/height behavior.

The HTML specification explicitly states that transferFromImageBitmap() makes the context's output bitmap refer to the provided ImageBitmap, with its intrinsic size derived from the ImageBitmap itself—not the canvas dimensions. ImageBitmap width/height can differ from canvas width/height; that's the intended behavior. Update the comment to reflect this, or clarify if there's a different reason options is not supported.

libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (3)

3-3: LGTM!

The type import for WritableStream is correctly added to support the new stream-based API.


29-31: LGTM!

The stream getter appropriately exposes the internal MediaStream for external use.


49-52: LGTM!

The resize handler correctly synchronizes element dimensions with video frame dimensions, maintaining proper aspect ratio.

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (6)

2-2: LGTM!

The WritableStream import is correctly added to support the new stream-based rendering pipeline.


12-17: LGTM!

The new private fields appropriately support the device-pixel rendering feature and frame lifecycle management.


37-77: Verify error handling for concurrent draw calls.

The useDevicePixels feature is well-implemented with proper runtime validation. However, on line 72, void this.draw(this.#frame) discards the promise, which could lead to unhandled rejections if draw() fails. Additionally, rapid resize events might trigger overlapping draw calls.

Consider whether the draw operation should be awaited or if there should be debouncing/throttling for resize events.


79-92: LGTM!

The #updateSize method correctly clamps dimensions to the frame size and avoids unnecessary updates when dimensions haven't changed.


105-122: LGTM!

The Options interface is well-documented with clear guidance on the useDevicePixels feature, including performance implications and usage constraints.


96-102: No breaking changes from the return type modification.

The dispose() method is not part of the VideoFrameRenderer interface contract, nor does it implement the standard Disposable interface which would require dispose(): void. The current callers at lines 30-31 (stream handler callbacks) do not await the method, so the return type change to undefined introduces no compatibility issues.

The cleanup logic properly disconnects the ResizeObserver and closes the saved frame.

libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts (5)

1-2: LGTM!

The cspell directives correctly suppress false positives for GLSL precision qualifiers.


95-96: Verify context type narrowing is intentional.

Line 95 narrows the context type to WebGLRenderingContext, but glCreateContext (line 111) can return WebGL2RenderingContext. While the instanceof check on line 161 handles WebGL2 features at runtime, the type narrowing loses compile-time type safety for WebGL2-specific APIs.

Consider using a union type to preserve type information:

#context: WebGLRenderingContext | WebGL2RenderingContext;

105-152: LGTM!

The constructor properly integrates the options parameter, passing it to the parent class and using enableCapture to configure WebGL context attributes. The trade-off between preserveDrawingBuffer and desynchronized is well-balanced for capture vs. latency.

The program creation uses the new helper function and is properly initialized with useProgram.


169-191: LGTM!

The override keyword correctly indicates method inheritance. The addition of gl.flush() ensures commands are submitted for execution, which can improve frame timing in streaming scenarios, especially with the desynchronized context setting.


193-213: LGTM!

The disposal sequence correctly cleans up resources in the proper order: program deletion, context invalidation, then parent cleanup. The Options interface appropriately extends the parent options and adds the enableCapture flag with clear documentation.

Comment on lines 102 to 106
close: () => {
this.#readableController.close();
this.#counter.dispose();
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential resource leak: unclosed VideoFrames at shutdown.

The close handler disposes the performance counter but doesn't close #captureFrame and #nextFrame. If these hold VideoFrame objects when the stream closes, they won't be released, causing a resource leak.

🔧 Proposed fix to close pending frames
             close: () => {
+                this.#captureFrame?.close();
+                this.#nextFrame?.close();
                 this.#readableController.close();
                 this.#counter.dispose();
             },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
close: () => {
this.#readableController.close();
this.#counter.dispose();
},
});
close: () => {
this.#captureFrame?.close();
this.#nextFrame?.close();
this.#readableController.close();
this.#counter.dispose();
},
});
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts around
lines 102 - 106, The close handler currently only closes
this.#readableController and disposes this.#counter, risking leaked VideoFrame
objects; update the close function where the ReadableStream is created (the
close: () => { ... } block) to also check and close any pending VideoFrame
references by calling close() on this.#captureFrame and this.#nextFrame if they
are non-null and then nulling them out before disposing the counter and closing
the controller so all frames are released at shutdown.

Comment on lines 1 to 5
import type { WritableStream } from "@yume-chan/stream-extra";

export interface VideoFrameRenderer {
setSize(width: number, height: number): void;

draw(frame: VideoFrame): MaybePromiseLike<void>;

dispose(): MaybePromiseLike<undefined>;
writeable: WritableStream<VideoFrame>;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Excellent stream-based simplification, but verify spelling: "writeable" vs "writable".

The conversion to a stream-based interface is a clean architectural improvement. However, there's a potential spelling inconsistency:

  • This interface uses writeable (Line 4)
  • RendererController in flow-control.ts uses writable (Line 29)
  • Standard property naming in streams uses writable (e.g., TransformStream.writable)

Verify whether writeable is intentional or should be writable for consistency with standard stream APIs and other implementations in this PR.

#!/bin/bash
# Description: Check usage of writeable vs writable across the codebase.

echo "=== Searching for 'writeable' usage ==="
rg -n '\bwriteable\b' --type ts

echo -e "\n=== Searching for 'writable' usage in stream contexts ==="
rg -n '\bwritable\b.*Stream' --type ts -A 1
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts around lines 1 -
5, The interface uses the nonstandard property name `writeable`; change
`VideoFrameRenderer.writeable` to `writable` to match the standard stream API
and the `RendererController` usage in `flow-control.ts`, then update all
implementations and references to `VideoFrameRenderer` (including any
classes/functions that consume or construct that interface) to use `writable` so
names are consistent across the codebase.

Comment on lines +14 to +60
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}

function createProgram(
gl: WebGLRenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
);

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

try {
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
return program;
}

// Don't check shader compile status unless linking fails
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#dont_check_shader_compile_status_unless_linking_fails
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(vertexShader)!);
}

if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(fragmentShader)!);
}

throw new Error(gl.getProgramInfoLog(program)!);
} finally {
// Delete objects eagerly
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#delete_objects_eagerly
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add null check for program creation.

Line 33 calls gl.createProgram() which can return null if the WebGL context is lost. The subsequent operations (lines 34-36, 39) assume a non-null program, which could throw if the context was lost between initialization and program creation.

The shader compilation error handling follows best practices by checking compile status only when linking fails. The eager deletion of shaders in the finally block is appropriate.

🛡️ Proposed null safety fix
     const program = gl.createProgram();
+    if (!program) {
+        throw new Error("Failed to create WebGL program");
+    }
     gl.attachShader(program, vertexShader);
     gl.attachShader(program, fragmentShader);
     gl.linkProgram(program);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
function createProgram(
gl: WebGLRenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
try {
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
return program;
}
// Don't check shader compile status unless linking fails
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#dont_check_shader_compile_status_unless_linking_fails
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(vertexShader)!);
}
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(fragmentShader)!);
}
throw new Error(gl.getProgramInfoLog(program)!);
} finally {
// Delete objects eagerly
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#delete_objects_eagerly
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
}
}
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
function createProgram(
gl: WebGLRenderingContext,
vertexShaderSource: string,
fragmentShaderSource: string,
) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
);
const program = gl.createProgram();
if (!program) {
throw new Error("Failed to create WebGL program");
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
try {
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
return program;
}
// Don't check shader compile status unless linking fails
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#dont_check_shader_compile_status_unless_linking_fails
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(vertexShader)!);
}
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(fragmentShader)!);
}
throw new Error(gl.getProgramInfoLog(program)!);
} finally {
// Delete objects eagerly
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#delete_objects_eagerly
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
}
}
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/webgl.ts around lines 14
- 60, createProgram may receive null from gl.createProgram() if the WebGL
context is lost; add an immediate null check after const program =
gl.createProgram() in createProgram and handle it before calling
gl.attachShader/linkProgram. Specifically, if program is null, delete the
vertexShader and fragmentShader (to avoid leaks) and throw a clear Error (e.g.,
"Failed to create program: WebGL context lost") so subsequent
gl.attachShader/gl.linkProgram calls never run on a null program.

Comment on lines 13 to 16
this.#context = (this.#canvas as HTMLCanvasElement).getContext(
"bitmaprenderer",
{ alpha: false },
)!;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Verify the necessity of the HTMLCanvasElement type cast.

The type cast (this.#canvas as HTMLCanvasElement) appears suspicious because:

  • this.#canvas can be either OffscreenCanvas (Line 7) or HTMLCanvasElement (Line 9).
  • Both canvas types support getContext("bitmaprenderer").

If the cast is needed due to TypeScript type definitions, please add a comment explaining why. Otherwise, consider removing the cast to preserve type safety.

🔍 Proposed fix to remove unnecessary cast
-        this.#context = (this.#canvas as HTMLCanvasElement).getContext(
-            "bitmaprenderer",
-            { alpha: false },
-        )!;
+        this.#context = this.#canvas.getContext(
+            "bitmaprenderer",
+            { alpha: false },
+        )!;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.#context = (this.#canvas as HTMLCanvasElement).getContext(
"bitmaprenderer",
{ alpha: false },
)!;
this.#context = this.#canvas.getContext(
"bitmaprenderer",
{ alpha: false },
)!;
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts around lines 13 -
16, The explicit cast "(this.#canvas as HTMLCanvasElement)" is unnecessary
because both OffscreenCanvas and HTMLCanvasElement support
getContext("bitmaprenderer"); remove the cast and call
this.#canvas.getContext("bitmaprenderer", { alpha: false })! to preserve type
safety; if TypeScript still complains about the union type, either add a short
comment explaining why a cast is required or narrow the type first (e.g., check
instanceof HTMLCanvasElement or assert via a single localized cast) while
keeping the comment explaining the reason.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (1)

13-62: Missing cleanup logic for MediaStreamTrackGenerator.

The class no longer has a dispose() method after removing the lifecycle methods. The MediaStreamTrackGenerator track should be stopped when no longer needed to release resources. Consider adding cleanup:

🔧 Suggested addition
+    dispose(): undefined {
+        this.#generator.stop();
+        this.#stream.getTracks().forEach(track => track.stop());
+    }
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (1)

212-223: Missing controller.terminate() in dispose.

The dispose() method clears internal state but doesn't terminate the TransformStream. Downstream consumers may hang waiting for more data. Consider terminating the controller:

🔧 Suggested fix
     dispose() {
         if (this.#disposed) {
             return;
         }

         this.#disposed = true;

         this.#pendingConfiguration = undefined;
         this.#pendingFrames.length = 0;

         this.#disposeVisibilityTracker?.();
+        this.#controller.terminate();
     }
🤖 Fix all issues with AI agents
In @libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts:
- Around line 220-234: The TransformStream transform callback filters out frames
when frame.timestamp <= this.#skipFramesUntil but never closes them, leaking GPU
resources; update the transform function inside the TransformStream in
decoder.ts to call frame.close() for skipped VideoFrame instances (i.e., when
frame.timestamp <= this.#skipFramesUntil) before returning, and keep
controller.enqueue(frame) only for frames that are forwarded so that all skipped
frames are explicitly closed.

In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts:
- Around line 108-113: The abort() handler currently leaves #nextFrame open
which can keep a pending VideoFrame alive; after calling
this.#readableController.error(reason) and this.#counter.dispose() also
explicitly close/cancel #nextFrame to release any held VideoFrame and avoid
leaks (but keep the existing behavior of not closing #captureFrame so snapshots
still work); make the close/cancel call defensive (check for existence and
whether it's already closed) so it’s safe to call from abort and won’t interfere
with normal #draw() completion.
🧹 Nitpick comments (1)
libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (1)

56-76: Consider error handling for resize redraw.

The void this.draw() on line 72 swallows any errors from the redraw operation. While this prevents unhandled promise rejections, errors during resize-triggered redraws will be silently lost.

💡 Optional: Add error logging
                 if (this.#updateSize(width, height) && this.#frame) {
-                    void this.draw(this.#frame);
+                    void this.draw(this.#frame).catch((e) => {
+                        console.error("Failed to redraw on resize:", e);
+                    });
                 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf514f2 and 80458c1.

📒 Files selected for processing (7)
  • libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts
  • libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-24T03:50:43.091Z
Learnt from: yume-chan
Repo: yume-chan/ya-webadb PR: 782
File: libraries/scrcpy-decoder-tinyh264/src/decoder.ts:193-196
Timestamp: 2025-08-24T03:50:43.091Z
Learning: In TinyH264Decoder, configuration failures should propagate the actual error up the client pipeline rather than being recovered locally. The decoder is designed to handle mid-stream configuration switches when the video encoder restarts, and subsequent valid config packets will re-configure correctly regardless of current decoder state.

Applied to files:

  • libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts
🧬 Code graph analysis (3)
libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (8)
libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (3)
  • PauseController (12-224)
  • Output (228-230)
  • Input (227-227)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (2)
  • RendererController (17-159)
  • frame (117-136)
libraries/scrcpy-decoder-webcodecs/src/video/codec/type.ts (3)
  • CodecDecoder (7-10)
  • Output (22-22)
  • Input (13-15)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts (1)
  • H264Decoder (6-16)
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts (1)
  • H265Decoder (6-19)
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts (1)
  • Av1Codec (6-43)
libraries/scrcpy-decoder-webcodecs/src/video/time.ts (1)
  • increasingNow (21-28)
libraries/stream-extra/src/inspect.ts (1)
  • InspectStream (5-23)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)
libraries/stream-extra/src/consumable.ts (1)
  • WritableStream (71-71)
libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (7)
libraries/scrcpy-decoder-webcodecs/src/video/pool.ts (1)
  • Pool (1-37)
libraries/scrcpy-decoder-webcodecs/src/video/snapshot.ts (1)
  • VideoFrameCapturer (1-42)
libraries/stream-extra/src/stream.ts (1)
  • TransformStream (41-41)
libraries/scrcpy-decoder-tinyh264/src/types.ts (1)
  • ScrcpyVideoDecoderPerformanceCounter (15-36)
libraries/stream-extra/src/push-readable.ts (2)
  • PushReadableStreamController (6-14)
  • PushReadableStream (41-307)
libraries/scrcpy-decoder-tinyh264/src/utils/performance.ts (1)
  • PerformanceCounter (3-64)
libraries/adb-scrcpy/src/client.ts (1)
  • controller (285-287)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (11)
libraries/scrcpy-decoder-webcodecs/src/video/render/type.ts (1)

1-5: Clean stream-based interface design.

The simplified interface exposing only writable: WritableStream<VideoFrame> aligns well with the stream-based architecture and matches the standard TransformStream.writable naming convention.

libraries/scrcpy-decoder-webcodecs/src/video/render/insertable-stream.ts (1)

49-52: Good use of resize event for dimension synchronization.

The resize event listener correctly syncs the video element's width/height attributes with its intrinsic videoWidth/videoHeight, ensuring proper rendering dimensions.

libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts (2)

152-158: Proper cleanup in dispose.

The dispose() method correctly closes both #captureFrame and #nextFrame, preventing resource leaks when manually cleaning up.


78-94: Frame lifecycle is correct; no use-after-close issue.

The write handler intentionally stores the original frame in #nextFrame (line 92) without cloning because frame ownership transfers to the stream via enqueue() in the #draw method. The #nextFrame reference is cleared to undefined (line 129) before the frame is enqueued, so when subsequent writes arrive during drawing, they do not close an in-flight frame—they only replace the queued frame reference. The renderer receives ownership of the enqueued frame and is responsible for closing it. The pattern is sound: #captureFrame is cloned for snapshot safety, while #nextFrame maintains the original reference that transfers ownership downstream.

libraries/scrcpy-decoder-tinyh264/src/utils/pause.ts (2)

50-102: Clean synchronous controller initialization.

The controller capture pattern correctly assigns the controller synchronously in the start callback, avoiding the race condition from the previous implementation that used await Promise.resolve().


149-162: Empty array case handled correctly.

The for...of iteration over entries() safely handles an empty #pendingFrames array by simply not executing the loop body, avoiding the previous concern about accessing frames[-1].

libraries/scrcpy-decoder-webcodecs/src/video/render/canvas.ts (2)

19-32: Good stream-based frame handling.

The WritableStream properly manages the frame lifecycle: closes the previous frame before storing the new one, conditionally updates size based on options, and delegates to the abstract draw() method.


79-92: Well-designed size update logic.

The #updateSize method correctly clamps dimensions to avoid upscaling beyond source resolution and returns a boolean to enable conditional redraws, avoiding unnecessary work.

libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts (3)

177-240: Well-structured streaming pipeline.

The pipeline composition is clean and readable with clear stage responsibilities:

  1. Timestamp injection
  2. Codec-specific packet transformation
  3. Configuration inspection/size tracking
  4. Raw video decoding
  5. Decode time tracking and skip filtering
  6. Render flow control
  7. Final rendering

The void ... .catch(noop) pattern prevents unhandled rejections.


270-281: Clean options interface using type reuse.

Using Pick<VideoDecoderConfig, ...> ensures type consistency with the native WebCodecs API and avoids duplicating type definitions.


259-267: dispose() effectively halts pipeline processing through the #disposed flag.

The dispose() method doesn't need to explicitly abort the writable stream. When #pause.dispose() is called, it sets #disposed = true, which causes PauseController's transform function to return early without enqueueing packets (lines 58-59 and 83-84). This prevents any further data from flowing through the pipeline, even if the source stream continues sending data.

The design is intentional: dispose() handles resource cleanup while stream closure is managed separately via the comment's note about cleanup happening "when writable ends."

Likely an incorrect or invalid review comment.

Comment on lines +220 to +234
.pipeThrough(
new TransformStream<VideoFrame, VideoFrame>({
transform: (frame, controller) => {
// `frame.timestamp` is the same `EncodedVideoChunk.timestamp` set above
this.#totalDecodeTime +=
performance.now() - frame.timestamp;

// Don't count these frames as skipped rendering
if (frame.timestamp <= this.#skipFramesUntil) {
return;
}

controller.enqueue(frame);
},
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Resource leak: skipped frames not closed.

When frame.timestamp <= this.#skipFramesUntil, the frame is filtered out but not closed. VideoFrame objects hold GPU resources and must be explicitly closed to avoid memory leaks.

🔧 Proposed fix
                     transform: (frame, controller) => {
                         // `frame.timestamp` is the same `EncodedVideoChunk.timestamp` set above
                         this.#totalDecodeTime +=
                             performance.now() - frame.timestamp;

                         // Don't count these frames as skipped rendering
                         if (frame.timestamp <= this.#skipFramesUntil) {
+                            frame.close();
                             return;
                         }

                         controller.enqueue(frame);
                     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.pipeThrough(
new TransformStream<VideoFrame, VideoFrame>({
transform: (frame, controller) => {
// `frame.timestamp` is the same `EncodedVideoChunk.timestamp` set above
this.#totalDecodeTime +=
performance.now() - frame.timestamp;
// Don't count these frames as skipped rendering
if (frame.timestamp <= this.#skipFramesUntil) {
return;
}
controller.enqueue(frame);
},
}),
.pipeThrough(
new TransformStream<VideoFrame, VideoFrame>({
transform: (frame, controller) => {
// `frame.timestamp` is the same `EncodedVideoChunk.timestamp` set above
this.#totalDecodeTime +=
performance.now() - frame.timestamp;
// Don't count these frames as skipped rendering
if (frame.timestamp <= this.#skipFramesUntil) {
frame.close();
return;
}
controller.enqueue(frame);
},
}),
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/decoder.ts around lines 220 -
234, The TransformStream transform callback filters out frames when
frame.timestamp <= this.#skipFramesUntil but never closes them, leaking GPU
resources; update the transform function inside the TransformStream in
decoder.ts to call frame.close() for skipped VideoFrame instances (i.e., when
frame.timestamp <= this.#skipFramesUntil) before returning, and keep
controller.enqueue(frame) only for frames that are forwarded so that all skipped
frames are explicitly closed.

Comment on lines +108 to +113
abort: (reason) => {
this.#readableController.error(reason);
this.#counter.dispose();
// Don't close `#captureFrame` to allow using `snapshot` on the last frame
// Don't close `#nextFrame` to make sure all frames are rendered
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider closing #nextFrame on abort.

On abort(), the stream terminates abnormally and #draw() won't complete. The comment says "Don't close #nextFrame to make sure all frames are rendered," but after abort, no more frames will be rendered. This could leak the VideoFrame.

🔧 Suggested fix
             abort: (reason) => {
                 this.#readableController.error(reason);
                 this.#counter.dispose();
                 // Don't close `#captureFrame` to allow using `snapshot` on the last frame
-                // Don't close `#nextFrame` to make sure all frames are rendered
+                this.#nextFrame?.close();
+                this.#nextFrame = undefined;
             },
🤖 Prompt for AI Agents
In @libraries/scrcpy-decoder-webcodecs/src/video/render/flow-control.ts around
lines 108 - 113, The abort() handler currently leaves #nextFrame open which can
keep a pending VideoFrame alive; after calling
this.#readableController.error(reason) and this.#counter.dispose() also
explicitly close/cancel #nextFrame to release any held VideoFrame and avoid
leaks (but keep the existing behavior of not closing #captureFrame so snapshots
still work); make the close/cancel call defensive (check for existence and
whether it's already closed) so it’s safe to call from abort and won’t interfere
with normal #draw() completion.

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.

2 participants