Skip to content

feat: S is the playback mode (stream / key frames) in every view; Q is the view (#153) - #155

Merged
compot-dev merged 9 commits into
v2from
feat/s-keyframes
Oct 13, 2026
Merged

compot-dev merged 9 commits into
v2from
feat/s-keyframes

Conversation

@compot-dev

@compot-dev compot-dev commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #153.

  • S toggles only the playback mode — every frame or key frames only — in multi-view, the U view and the single view; it no longer switches to multi-view.
  • Q switches only the view and no longer turns S off.
  • Single view with S: only parameter sets and key frames reach the decoder (MJPEG: one frame per second); turning S off goes back to every frame from the next key frame on (no smeared P frames).
  • Menu: Q is lit (orange fill) in multi-view, S whenever S is on — both may be on.
  • /stats.json single view: frames_out and decode (full / key frames).

Device test (AG-216, CI build of this branch):

step view result
single, S on single decode: key frames, 1 fps
S off single full, 30 fps
S on again single 1 fps; the view never changed
Q with S on multi-view Q and S both lit, all cells on key frames
Q with S on single S kept: key frames, 1 fps
S off, Q multi-view adaptive grid again
0 FATAL EXCEPTION.

Version: 2.01 (versionCode 26).

compot-dev and others added 3 commits October 13, 2026 18:05
…ti-view and in the single view; it never changes the view (#153)

Co-Authored-By: Claude <noreply@anthropic.com>
…n /stats.json (#153)

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Healthy streams can fall back to TCP 🐞 Bug ≡ Correctness ⭐ New
Description
skipForKeyFrames() drops non-key frames in single-view S mode, but singleUdpCheck still treats
eight seconds without a rendered frame as a broken UDP stream. On a healthy UDP feed with key frames
more than eight seconds apart and continuing packet traffic, the check reports a false stall,
enables packet-loss salvage, and can force Auto transport onto TCP.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1797-1799]

+        int type = NalAssembler.fragment(buffer.data()[4], codecH265);
+        if (!QuadCell.isKeyNal(type, codecH265)) return true;
+        if (!slides && QuadCell.isKeyFrameNal(type, codecH265)) singleWaitKey = false;
Evidence
The new filter prevents ordinary frames from reaching the decoder, while the existing watchdog
counts arriving bytes without rendered output as a stall. Reporting that stall causes the UDP
session to abandon Auto transport.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1794-1801]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1830-1854]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[767-787]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Single-view S mode intentionally drops non-key frames, but the UDP watchdog interprets the resulting gap in rendered frames as a transport failure.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1794-1801]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1830-1854]

## Recommended Fix
Make the single-view UDP stall check distinguish intentional key-frame-only output gaps from evidence of packet loss before enabling salvage or reporting UDP broken. Preserve detection of genuinely stalled streams.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. New camera views can stay blank briefly ✓ Resolved
Description
The MJPEG throttle compares each frame against lastSlideJpegMs, which is not reset when the
single-view stream changes. With S enabled, switching soon after a frame from the previous camera
was shown drops the new camera’s initial frames for the remainder of the one-second interval.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1747-1749]

+                        if ((!slides || now - lastSlideJpegMs >= QuadCell.JPEG_REDUCED_MS)
+                                && drawJpeg(buffer, gen)) {
+                            lastSlideJpegMs = now;
Evidence
The new filter rejects frames until 1,000 ms after the last successful JPEG draw. Camera switching
replaces the stream without resetting the activity-level throttle timestamp, so a frame from the
previous camera can suppress frames from the new one.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1744-1750]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1785-1786]
android/app/src/main/java/com/openipc/decoder/Decoder.java[585-605]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[89-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The single-view MJPEG throttle retains the previous camera’s last-rendered timestamp, delaying the first frame of a newly selected camera while S is enabled.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1744-1749]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[585-605]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1858-1880]
## Recommended Fix
Reset `lastSlideJpegMs` when ending or replacing a single-view stream so the first MJPEG frame of the next stream can render immediately; retain the one-second spacing between successfully rendered frames within that stream.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Single-view mode changes may be ignored ✓ Resolved
Description
skipForKeyFrames() reads slides on the video worker, but setSlides() writes that non-volatile
field on the UI thread without synchronization. When S is toggled in single view, the worker is not
guaranteed to observe the new value, so it can keep passing every frame or keep dropping inter
frames after the menu shows the opposite mode.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1790]

+        if (!slides && !singleWaitKey) return false;
Evidence
setSlides() updates slides from the menu path, while the newly added single-view worker reads it
in both frame-selection branches; unlike singleWaitKey, slides has no visibility mechanism.

android/app/src/main/java/com/openipc/decoder/Decoder.java[100-104]
android/app/src/main/java/com/openipc/decoder/Decoder.java[360-369]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1735-1750]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1781-1795]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The single-view video worker reads the UI-owned `slides` field without a visibility guarantee, so toggling S may not change frame selection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[103-103]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1745]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state safely visible to the video worker, for example by declaring `slides` volatile, and use that state consistently in both the MJPEG throttle and NAL filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (5)
4. Camera stats include earlier streams ⊘ Outdated
Description
debugState() assigns single.frames_out the sum of two counters that persist across stream and
decoder shutdowns. After a camera change or an MJPEG-to-video transition, the current single
camera's stats include frames output by the previous stream, so the value cannot be read as that
camera's output count.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
The new stats field uses the persistent decoder and JPEG counters; closing a decoder and stopping
streams do not reset them.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1450-1459]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1681-1703]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[302-307]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new single-view output count includes frames from earlier streams because its source counters are lifetime totals.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1453-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
## Recommended Fix
Record a baseline of the combined output counters when a single-view stream starts and subtract it in `debugState()`. Preserve the lifetime counters for existing consumers that use their differences.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Single-view playback can use the wrong mode ✓ Resolved
Description
skipForKeyFrames() reads slides on the video worker thread, but setSlides() changes that
non-volatile field on the UI thread. After S is toggled in single view, the worker is not guaranteed
to observe the change, so it can continue decoding every frame or filtering to key frames.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1790]

+        if (!slides && !singleWaitKey) return false;
Evidence
slides is declared without volatile synchronization, setSlides() writes it, and the newly added
video-worker filter reads it.

android/app/src/main/java/com/openipc/decoder/Decoder.java[107-107]
android/app/src/main/java/com/openipc/decoder/Decoder.java[364-369]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1737-1751]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The video worker reads `slides` without a cross-thread visibility guarantee after the UI thread toggles it.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[107-107]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state read by the video worker volatile, or publish it through another explicit thread-safe mechanism.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A discarded key frame ends recovery early ✓ Resolved
Description
skipForKeyFrames() clears singleWaitKey when it recognizes a key-frame NAL, before the listener
checks whether the surface is ready or the decoder accepts the frame. If that frame is discarded
while surfacePending is true, later P frames pass the filter without the key frame needed to
resume cleanly.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1794]

+        if (!slides && QuadCell.isKeyFrameNal(type, codecH265)) singleWaitKey = false;
Evidence
The filter clears the flag first; the following listener branch can recycle that same frame when
surface creation is pending. Subsequent non-key frames then bypass the filter, while a new decoder
requires configuration before accepting frames.

android/app/src/main/java/com/openipc/decoder/Decoder.java[376-379]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1749-1759]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[405-450]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The S-off recovery flag can be cleared for a key frame that is subsequently discarded.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1749-1759]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Keep the wait state until the surface is ready and a key frame can be submitted to a configured decoder; do not clear it in the preliminary filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. A failed JPEG draw delays the next frame ✓ Resolved
Description
The MJPEG branch updates lastSlideJpegMs before drawJpeg() establishes that a frame was
displayed. If rendering fails during surface recreation while S is on, subsequent frames are skipped
for up to the one-second reduction interval even after the surface becomes available.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1745-1746]

+                            lastSlideJpegMs = now;
+                            drawJpeg(buffer, gen);
Evidence
drawJpeg() only records a displayed frame for a non-null render result, while the new timestamp
assignment runs beforehand. The renderer can return null for an unavailable surface; the analogous
multi-view path timestamps only successful renders.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1692-1705]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1748]
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[40-55]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[709-721]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An unsuccessful single-view JPEG render advances the reduction timer and suppresses immediate retries.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1692-1705]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1748]
## Recommended Fix
Have the drawing path report whether a frame was displayed, and update `lastSlideJpegMs` only on success.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Single-view counts include earlier cameras ✓ Resolved
Description
debugState() adds the lifetime videoDecoder.framesOut() and jpegFramesOut counters under the
currently selected single camera. After a camera or stream switch, neither counter is reset, so the
reported count includes frames displayed before the current stream started.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
The statistic is placed beside the current single-view URL, but decoder close and stream stop do not
reset the counters used to produce it.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1448-1459]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1682-1701]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1852-1881]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[300-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new single-view frame count carries output from prior cameras and connections.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1452-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1852-1881]
## Recommended Fix
Capture counter baselines when a single-view stream starts or changes, and report the difference for that stream, including both video and JPEG outputs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

9. Long-running frame counts turn negative ⊘ Outdated
Description
debugState() adds two signed 32-bit frame counters before publishing single.frames_out. After
roughly 2.27 years at 30 frames per second, the total can overflow and /stats.json emits a
negative count to consumers.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1459]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
Both contributing counters are int, and the new expression adds them as int before the JSON
serializer writes the result unchanged. The video counter increments for each rendered frame, so
sustained playback can eventually wrap the published total.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457-1460]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1685-1703]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[59-62]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[508-512]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[325-331]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new lifetime `single.frames_out` value can overflow its signed 32-bit counters and become negative.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1457-1460]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1685-1703]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[59-62]
## Recommended Fix
Use long counters for rendered video and JPEG frames, and publish their sum using long arithmetic. Update counter accessors and their callers as needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 16/18, lines 89/200; both must reach the floor). Router rationale: Playback logic spans multiple paths with several subtle, independent failure modes.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 52ac0eb ⚖️ Balanced

Results up to commit 7be441e


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)


Remediation recommended
1. New camera views can stay blank briefly 🐞 Bug ≡ Correctness ⭐ New
Description
The MJPEG throttle compares each frame against lastSlideJpegMs, which is not reset when the
single-view stream changes. With S enabled, switching soon after a frame from the previous camera
was shown drops the new camera’s initial frames for the remainder of the one-second interval.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1747-1749]

+                        if ((!slides || now - lastSlideJpegMs >= QuadCell.JPEG_REDUCED_MS)
+                                && drawJpeg(buffer, gen)) {
+                            lastSlideJpegMs = now;
Evidence
The new filter rejects frames until 1,000 ms after the last successful JPEG draw. Camera switching
replaces the stream without resetting the activity-level throttle timestamp, so a frame from the
previous camera can suppress frames from the new one.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1744-1750]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1785-1786]
android/app/src/main/java/com/openipc/decoder/Decoder.java[585-605]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[89-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The single-view MJPEG throttle retains the previous camera’s last-rendered timestamp, delaying the first frame of a newly selected camera while S is enabled.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1744-1749]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[585-605]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1858-1880]

## Recommended Fix
Reset `lastSlideJpegMs` when ending or replacing a single-view stream so the first MJPEG frame of the next stream can render immediately; retain the one-second spacing between successfully rendered frames within that stream.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Single-view mode changes may be ignored ✓ Resolved
Description
skipForKeyFrames() reads slides on the video worker, but setSlides() writes that non-volatile
field on the UI thread without synchronization. When S is toggled in single view, the worker is not
guaranteed to observe the new value, so it can keep passing every frame or keep dropping inter
frames after the menu shows the opposite mode.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1790]

+        if (!slides && !singleWaitKey) return false;
Evidence
setSlides() updates slides from the menu path, while the newly added single-view worker reads it
in both frame-selection branches; unlike singleWaitKey, slides has no visibility mechanism.

android/app/src/main/java/com/openipc/decoder/Decoder.java[100-104]
android/app/src/main/java/com/openipc/decoder/Decoder.java[360-369]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1735-1750]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1781-1795]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The single-view video worker reads the UI-owned `slides` field without a visibility guarantee, so toggling S may not change frame selection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[103-103]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1745]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state safely visible to the video worker, for example by declaring `slides` volatile, and use that state consistently in both the MJPEG throttle and NAL filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Camera stats include earlier streams ⊘ Outdated
Description
debugState() assigns single.frames_out the sum of two counters that persist across stream and
decoder shutdowns. After a camera change or an MJPEG-to-video transition, the current single
camera's stats include frames output by the previous stream, so the value cannot be read as that
camera's output count.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
The new stats field uses the persistent decoder and JPEG counters; closing a decoder and stopping
streams do not reset them.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1450-1459]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1681-1703]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[302-307]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new single-view output count includes frames from earlier streams because its source counters are lifetime totals.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1453-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
## Recommended Fix
Record a baseline of the combined output counters when a single-view stream starts and subtract it in `debugState()`. Preserve the lifetime counters for existing consumers that use their differences.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (4)
4. Single-view playback can use the wrong mode ✓ Resolved
Description
skipForKeyFrames() reads slides on the video worker thread, but setSlides() changes that
non-volatile field on the UI thread. After S is toggled in single view, the worker is not guaranteed
to observe the change, so it can continue decoding every frame or filtering to key frames.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1790]

+        if (!slides && !singleWaitKey) return false;
Evidence
slides is declared without volatile synchronization, setSlides() writes it, and the newly added
video-worker filter reads it.

android/app/src/main/java/com/openipc/decoder/Decoder.java[107-107]
android/app/src/main/java/com/openipc/decoder/Decoder.java[364-369]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1737-1751]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The video worker reads `slides` without a cross-thread visibility guarantee after the UI thread toggles it.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[107-107]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state read by the video worker volatile, or publish it through another explicit thread-safe mechanism.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. A discarded key frame ends recovery early ✓ Resolved
Description
skipForKeyFrames() clears singleWaitKey when it recognizes a key-frame NAL, before the listener
checks whether the surface is ready or the decoder accepts the frame. If that frame is discarded
while surfacePending is true, later P frames pass the filter without the key frame needed to
resume cleanly.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1794]

+        if (!slides && QuadCell.isKeyFrameNal(type, codecH265)) singleWaitKey = false;
Evidence
The filter clears the flag first; the following listener branch can recycle that same frame when
surface creation is pending. Subsequent non-key frames then bypass the filter, while a new decoder
requires configuration before accepting frames.

android/app/src/main/java/com/openipc/decoder/Decoder.java[376-379]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1749-1759]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[405-450]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The S-off recovery flag can be cleared for a key frame that is subsequently discarded.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1749-1759]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Keep the wait state until the surface is ready and a key frame can be submitted to a configured decoder; do not clear it in the preliminary filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A failed JPEG draw delays the next frame ✓ Resolved
Description
The MJPEG branch updates lastSlideJpegMs before drawJpeg() establishes that a frame was
displayed. If rendering fails during surface recreation while S is on, subsequent frames are skipped
for up to the one-second reduction interval even after the surface becomes available.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1745-1746]

+                            lastSlideJpegMs = now;
+                            drawJpeg(buffer, gen);
Evidence
drawJpeg() only records a displayed frame for a non-null render result, while the new timestamp
assignment runs beforehand. The renderer can return null for an unavailable surface; the analogous
multi-view path timestamps only successful renders.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1692-1705]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1748]
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[40-55]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[709-721]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An unsuccessful single-view JPEG render advances the reduction timer and suppresses immediate retries.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1692-1705]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1748]
## Recommended Fix
Have the drawing path report whether a frame was displayed, and update `lastSlideJpegMs` only on success.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Single-view counts include earlier cameras ✓ Resolved
Description
debugState() adds the lifetime videoDecoder.framesOut() and jpegFramesOut counters under the
currently selected single camera. After a camera or stream switch, neither counter is reset, so the
reported count includes frames displayed before the current stream started.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
The statistic is placed beside the current single-view URL, but decoder close and stream stop do not
reset the counters used to produce it.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1448-1459]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1682-1701]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1852-1881]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[300-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new single-view frame count carries output from prior cameras and connections.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1452-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1852-1881]
## Recommended Fix
Capture counter baselines when a single-view stream starts or changes, and report the difference for that stream, including both video and JPEG outputs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
8. Long-running frame counts turn negative ⊘ Outdated
Description
debugState() adds two signed 32-bit frame counters before publishing single.frames_out. After
roughly 2.27 years at 30 frames per second, the total can overflow and /stats.json emits a
negative count to consumers.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1459]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
Both contributing counters are int, and the new expression adds them as int before the JSON
serializer writes the result unchanged. The video counter increments for each rendered frame, so
sustained playback can eventually wrap the published total.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457-1460]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1685-1703]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[59-62]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[508-512]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[325-331]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new lifetime `single.frames_out` value can overflow its signed 32-bit counters and become negative.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1457-1460]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1685-1703]
- android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[59-62]
## Recommended Fix
Use long counters for rendered video and JPEG frames, and publish their sum using long arithmetic. Update counter accessors and their callers as needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Context sources
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 15/18, lines 87/200; both must reach the floor). Router rationale: Multiple independent decoder, rendering, state, and concurrency paths changed.

Grey Divider

Qodo Logo

* decoder, the rest is dropped; after S is turned off, until the next key frame.
*/
private boolean skipForKeyFrames(Frame buffer) {
if (!slides && !singleWaitKey) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Single-view mode changes may be ignored 🐞 Bug ☼ Reliability

skipForKeyFrames() reads slides on the video worker, but setSlides() writes that non-volatile
field on the UI thread without synchronization. When S is toggled in single view, the worker is not
guaranteed to observe the new value, so it can keep passing every frame or keep dropping inter
frames after the menu shows the opposite mode.
Agent Prompt
## Issue description
The single-view video worker reads the UI-owned `slides` field without a visibility guarantee, so toggling S may not change frame selection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[103-103]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1745]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state safely visible to the video worker, for example by declaring `slides` volatile, and use that state consistently in both the MJPEG throttle and NAL filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

: codecH265 ? "H.265" : "H.264");
single.put("audio", audioCodec);
// Frames shown so far and the playback mode of S (#153)
single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Camera stats include earlier streams 🐞 Bug ≡ Correctness

debugState() assigns single.frames_out the sum of two counters that persist across stream and
decoder shutdowns. After a camera change or an MJPEG-to-video transition, the current single
camera's stats include frames output by the previous stream, so the value cannot be read as that
camera's output count.
Agent Prompt
## Issue description
The new single-view output count includes frames from earlier streams because its source counters are lifetime totals.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1453-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
## Recommended Fix
Record a baseline of the combined output counters when a single-view stream starts and subtract it in `debugState()`. Preserve the lifetime counters for existing consumers that use their differences.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Single-view playback can use the wrong mode 🐞 Bug ☼ Reliability ⭐ New
Description
skipForKeyFrames() reads slides on the video worker thread, but setSlides() changes that
non-volatile field on the UI thread. After S is toggled in single view, the worker is not guaranteed
to observe the change, so it can continue decoding every frame or filtering to key frames.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1790]

+        if (!slides && !singleWaitKey) return false;
Evidence
slides is declared without volatile synchronization, setSlides() writes it, and the newly added
video-worker filter reads it.

android/app/src/main/java/com/openipc/decoder/Decoder.java[107-107]
android/app/src/main/java/com/openipc/decoder/Decoder.java[364-369]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1737-1751]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The video worker reads `slides` without a cross-thread visibility guarantee after the UI thread toggles it.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[107-107]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state read by the video worker volatile, or publish it through another explicit thread-safe mechanism.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A discarded key frame ends recovery early 🐞 Bug ≡ Correctness ⭐ New
Description
skipForKeyFrames() clears singleWaitKey when it recognizes a key-frame NAL, before the listener
checks whether the surface is ready or the decoder accepts the frame. If that frame is discarded
while surfacePending is true, later P frames pass the filter without the key frame needed to
resume cleanly.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1794]

+        if (!slides && QuadCell.isKeyFrameNal(type, codecH265)) singleWaitKey = false;
Evidence
The filter clears the flag first; the following listener branch can recycle that same frame when
surface creation is pending. Subsequent non-key frames then bypass the filter, while a new decoder
requires configuration before accepting frames.

android/app/src/main/java/com/openipc/decoder/Decoder.java[376-379]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1749-1759]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[405-450]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The S-off recovery flag can be cleared for a key frame that is subsequently discarded.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1749-1759]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Keep the wait state until the surface is ready and a key frame can be submitted to a configured decoder; do not clear it in the preliminary filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. A failed JPEG draw delays the next frame 🐞 Bug ☼ Reliability ⭐ New
Description
The MJPEG branch updates lastSlideJpegMs before drawJpeg() establishes that a frame was
displayed. If rendering fails during surface recreation while S is on, subsequent frames are skipped
for up to the one-second reduction interval even after the surface becomes available.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1745-1746]

+                            lastSlideJpegMs = now;
+                            drawJpeg(buffer, gen);
Evidence
drawJpeg() only records a displayed frame for a non-null render result, while the new timestamp
assignment runs beforehand. The renderer can return null for an unavailable surface; the analogous
multi-view path timestamps only successful renders.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1692-1705]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1748]
android/app/src/main/java/com/openipc/decoder/JpegRenderer.java[40-55]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[709-721]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An unsuccessful single-view JPEG render advances the reduction timer and suppresses immediate retries.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1692-1705]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1748]
## Recommended Fix
Have the drawing path report whether a frame was displayed, and update `lastSlideJpegMs` only on success.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
4. Single-view counts include earlier cameras 🐞 Bug ≡ Correctness ⭐ New
Description
debugState() adds the lifetime videoDecoder.framesOut() and jpegFramesOut counters under the
currently selected single camera. After a camera or stream switch, neither counter is reset, so the
reported count includes frames displayed before the current stream started.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
The statistic is placed beside the current single-view URL, but decoder close and stream stop do not
reset the counters used to produce it.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1448-1459]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1682-1701]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1852-1881]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[300-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new single-view frame count carries output from prior cameras and connections.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1452-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1852-1881]
## Recommended Fix
Capture counter baselines when a single-view stream starts or changes, and report the difference for that stream, including both video and JPEG outputs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Single-view mode changes may be ignored 🐞 Bug ☼ Reliability
Description
skipForKeyFrames() reads slides on the video worker, but setSlides() writes that non-volatile
field on the UI thread without synchronization. When S is toggled in single view, the worker is not
guaranteed to observe the new value, so it can keep passing every frame or keep dropping inter
frames after the menu shows the opposite mode.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1790]

+        if (!slides && !singleWaitKey) return false;
Evidence
setSlides() updates slides from the menu path, while the newly added single-view worker reads it
in both frame-selection branches; unlike singleWaitKey, slides has no visibility mechanism.

android/app/src/main/java/com/openipc/decoder/Decoder.java[100-104]
android/app/src/main/java/com/openipc/decoder/Decoder.java[360-369]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1735-1750]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1781-1795]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The single-view video worker reads the UI-owned `slides` field without a visibility guarantee, so toggling S may not change frame selection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[103-103]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1742-1745]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1789-1795]
## Recommended Fix
Make the playback-mode state safely visible to the video worker, for example by declaring `slides` volatile, and use that state consistently in both the MJPEG throttle and NAL filter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Camera stats include earlier streams 🐞 Bug ≡ Correctness
Description
debugState() assigns single.frames_out the sum of two counters that persist across stream and
decoder shutdowns. After a camera change or an MJPEG-to-video transition, the current single
camera's stats include frames output by the previous stream, so the value cannot be read as that
camera's output count.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[1457]

+            single.put("frames_out", videoDecoder.framesOut() + jpegFramesOut);
Evidence
The new stats field uses the persistent decoder and JPEG counters; closing a decoder and stopping
streams do not reset them.

android/app/src/main/java/com/openipc/decoder/Decoder.java[1450-1459]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1681-1703]
android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java[302-307]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new single-view output count includes frames from earlier streams because its source counters are lifetime totals.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1453-1459]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1851-1867]
## Recommended Fix
Record a baseline of the combined output counters when a single-view stream starts and subtract it in `debugState()`. Preserve the lifetime counters for existing consumers that use their differences.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: Playback and decoder-state changes carry meaningful behavioral risk across multiple paths.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java
Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java
Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
…ad reads it); frames_out is documented as a running total

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo → fix: 1. slides is volatile. 2. frames_out is meant as a running total across single-view streams (a rate is the difference of two samples, as used in the device test); documented so, not reset.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

Co-Authored-By: Claude <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Qodo → fix: 1. slides is volatile. 2. frames_out is meant as a running total across single-view streams (a rate is the difference of two samples, as used in the device test); documented so, not reset.

Agreed on both points:

  • finding 1 (duplicated by finding 5) is addressed because slides is now volatile, providing visibility to the decode thread.
  • findings 4 and 6 are not bugs if frames_out is intentionally a lifetime running total used to calculate rates between samples, as documented in the code.

The remaining open concerns are finding 2 (key-frame recovery) and finding 3 (failed JPEG draws advancing the throttle timestamp).

Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8f605dc

…otal, the key-frame wait ends only with a frame that reaches the decoder, the MJPEG second counts from a frame shown

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo round 2 → 7be441e: 4. the key-frame wait ends only with a frame that passes the surface check to the decoder (filter after surfacePending); 5. the MJPEG second counts from a frame really drawn (drawJpeg returns it); 2/6/7. frames_out replaced by out_fps — frames shown in the last second, from the single view's once-a-second check (no running total, no overflow). Device: single view full 30 fps → S key frames ~1 fps → S off 30 fps, view stays single.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Qodo round 2 → 7be441e: 4. the key-frame wait ends only with a frame that passes the surface check to the decoder (filter after surfacePending); 5. the MJPEG second counts from a frame really drawn (drawJpeg returns it); 2/6/7. frames_out replaced by out_fps — frames shown in the last second, from the single view's once-a-second check (no running total, no overflow). Device: single view full 30 fps → S key frames ~1 fps → S off 30 fps, view stays single.

The round-2 changes address the listed points:

  • Key-frame waiting now clears only when an eligible key frame reaches decoding after the surface check.
  • MJPEG throttling measures one second from the last frame successfully drawn.
  • out_fps reports frames shown during the latest one-second interval rather than a cumulative count.
  • Single-view behavior remains unchanged by S: approximately 30 FPS in full, approximately 1 FPS in key frames, then back to approximately 30 FPS when S is disabled.

The view remains single throughout; Q controls the view and S controls playback mode independently.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7be441e

…am (Qodo on #155)

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@compot-dev
compot-dev merged commit f65171e into v2 Oct 13, 2026
4 checks passed
@compot-dev
compot-dev deleted the feat/s-keyframes branch October 13, 2026 19:15
Comment on lines +1797 to +1799
int type = NalAssembler.fragment(buffer.data()[4], codecH265);
if (!QuadCell.isKeyNal(type, codecH265)) return true;
if (!slides && QuadCell.isKeyFrameNal(type, codecH265)) singleWaitKey = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Healthy streams can fall back to tcp 🐞 Bug ≡ Correctness

skipForKeyFrames() drops non-key frames in single-view S mode, but singleUdpCheck still treats
eight seconds without a rendered frame as a broken UDP stream. On a healthy UDP feed with key frames
more than eight seconds apart and continuing packet traffic, the check reports a false stall,
enables packet-loss salvage, and can force Auto transport onto TCP.
Agent Prompt
## Issue description
Single-view S mode intentionally drops non-key frames, but the UDP watchdog interprets the resulting gap in rendered frames as a transport failure.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1794-1801]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1830-1854]

## Recommended Fix
Make the single-view UDP stall check distinguish intentional key-frame-only output gaps from evidence of packet loss before enabling salvage or reporting UDP broken. Preserve detection of genuinely stalled streams.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 52ac0eb

compot-dev added a commit that referenced this pull request Oct 15, 2026
…ad reads it); frames_out is documented as a running total

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…otal, the key-frame wait ends only with a frame that reaches the decoder, the MJPEG second counts from a frame shown

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…am (Qodo on #155)

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
feat: S is the playback mode (stream / key frames) in every view; Q is the view (#153)
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.

S (key frames only) in both views; it must not switch between multi-view and single view

1 participant