Skip to content

feat(video-streamer): add codec-aware VP9 keyframe detection for session shadowing#1705

Open
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 4 commits intomasterfrom
fix/add-keyframe-every-10s
Open

feat(video-streamer): add codec-aware VP9 keyframe detection for session shadowing#1705
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 4 commits intomasterfrom
fix/add-keyframe-every-10s

Conversation

@irvingoujAtDevolution

Summary

  • Add codec-aware VP9 keyframe detection in tag_writers.rs so the streamer correctly identifies keyframes for both VP8 and VP9 codecs during session shadowing
  • Improve unified shutdown correctness with AbortOnDrop bridge guard and shutdown-reason-aware final messages (Error vs End)
  • Add warning log when when_eof oneshot sender is dropped unexpectedly

Test plan

  • Verify VP9 session shadowing correctly detects keyframes and starts playback from the latest keyframe
  • Verify VP8 session shadowing still works as before (regression check)
  • Verify shutdown paths: external shutdown, client disconnect, and error conditions all send the correct final message
  • Verify no hanging tasks on stream exit (AbortOnDrop ensures bridge task cleanup)

🤖 Generated with Claude Code

…ion shadowing

The existing keyframe detection only handled VP8 frames. This adds proper VP9
keyframe detection based on the VP9 bitstream specification, supporting all
profiles (0-3). The codec type is now threaded through the iterator and block
tag layers so keyframe checks use the correct codec-specific logic. Also sets
VpxEncoderPreset::BestPerformance for improved re-encoding throughput.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Send Error (not End) to client when shutdown is caused by a stream error
- Use AbortOnDrop guard for bridge task to handle early returns/bails
- Remove dead UserFriendlyError::UnexpectedEOF variant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds periodic, interval-based keyframe forcing during re-encoding so the outgoing WebM stream remains seekable over time (and starts new clusters on forced keyframes), and improves EOF-wait robustness by warning when the when_eof oneshot sender is dropped.

Changes:

  • Track last forced keyframe time and force a new keyframe every KEYFRAME_INTERVAL_MS, starting a new cluster for that keyframe.
  • Log a warning and treat EOF-wait as shutdown if the when_eof oneshot sender is unexpectedly dropped.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
crates/video-streamer/src/streamer/tag_writers.rs Adds interval-based forced keyframes and cluster splitting to improve seekability/resilience.
crates/video-streamer/src/streamer/mod.rs Adds a warning log when the when_eof oneshot sender is dropped unexpectedly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +23 to +26
/// Interval in milliseconds at which keyframes are forced in the outgoing stream.
/// Periodic keyframes improve seekability and resilience to packet loss.
const KEYFRAME_INTERVAL_MS: u64 = 10_000;

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

This PR description mentions adding “codec-aware VP9 keyframe detection in tag_writers.rs”, but the change here introduces periodic forced keyframes (interval-based) rather than keyframe detection. If VP9 detection work is happening elsewhere, consider updating the PR description; otherwise, ensure the intended VP9 keyframe-detection fix is included.

Copilot uses AI. Check for mistakes.
Comment on lines 530 to +556
@@ -524,10 +540,25 @@ where
let frame_size = frame.len();
perf_trace!(block_timestamp, frame_size, "Frame available from encoder");

let timestamp = self.compute_met_timestamp(cut_block_absolute_time, abs_time)?;
let block = SimpleBlock::new_uncheked(&frame, 1, timestamp, false, None, false, false);
perf_trace!(block_timestamp, "Writing block to output");
self.write_block(block)?;
if force_keyframe {
// Start a new cluster for the forced keyframe so the stream remains seekable.
let cluster_rel = abs_time - cut_block_absolute_time;
self.maybe_report_realtime_ratio(abs_time, cluster_rel);
self.start_new_cluster(cluster_rel)?;
self.cut_block_state = CutBlockState::Met {
cut_block_absolute_time,
last_cluster_relative_time: cluster_rel,
};
let block = SimpleBlock::new_uncheked(&frame, 1, 0, false, None, false, true);
perf_trace!(block_timestamp, "Writing forced keyframe block to output");
self.write_block(block)?;
self.last_keyframe_abs_time = Some(abs_time);
} else {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The new periodic keyframe forcing / cluster-splitting behavior doesn’t appear to be covered by tests. Since this is important for session shadowing correctness, consider adding an integration test (similar to existing webm_stream_* tests) that runs long enough to cross KEYFRAME_INTERVAL_MS and asserts that a new cluster starts and the corresponding SimpleBlock is marked as a keyframe.

Copilot uses AI. Check for mistakes.
Comment on lines +547 to +551
self.start_new_cluster(cluster_rel)?;
self.cut_block_state = CutBlockState::Met {
cut_block_absolute_time,
last_cluster_relative_time: cluster_rel,
};
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In the forced-keyframe path, cut_block_state is updated to Met { .. last_cluster_relative_time: cluster_rel } before start_new_cluster(cluster_rel)? runs. If start_new_cluster fails (e.g., write error), the writer state is mutated even though the new cluster wasn't actually emitted. Consider calling start_new_cluster first and only updating cut_block_state after it succeeds (mirrors the ordering used in compute_met_timestamp).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants