Skip to content

Commit

Permalink
Move web video decoder from re_renderer to re_video (rerun-io#7924)
Browse files Browse the repository at this point in the history
### What

Pure refactor.

This greatly simplifies how video playing is structured:
* `re_renderer` holds what's now called the `VideoPlayer` - it's job is
submitting things to a decoder and copy frames to textures
* the only platform specific part is now the texture copy, removing
duplicated logic between the web & native chunk decoders
* re_video has now an AsynDecoder for everything (that's a learning from
the ongoing ffmpeg work) and that's what re_renderer's video player
always uses

----

This leaves us with the following structure:

`re_renderer::Video` - _several_ -> `re_renderer::VideoPlayer` (->
`re_renderer::ChunkDecoder`) -> `re_video::AsyncDecoder`

`AsyncDecoder` is a trait implemented by
* `WebVideoDecoder`
* `AsyncDecoderWrapper`
    * in turn implemented by `SyncDav1dDecoder`
* upcoming: ffmpeg h264 decoder  

----

Another nice property of this - and what sparked this refactor - is that
every decoded frame now goes through the same `Frame` datastructure,
making easy to report additional information from the decoders.


Future work:
* `ChunkDecoder` should be folded into the `VideoPlayer` in re_renderer.
Left it for now since this pr is already messy and there's going to be
changes to our decode pacing soonish.
* distinction between re_renderer's `Video` (data + several players) and
`VideoPlayer` is still meaningful, but a bit confusing. I'm sure we can
do something about that
* the video pacing itself could be separated from the texture writing.
That would allow to move `Video` and `VideoPlayer` to `re_video` as
well. The annoyance here though is that this requires some sort of
consumer abstraction for the frame 🤔 (which seems a bit unnecessary for
us right now)


### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/7924?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/7924?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!
* [x] If have noted any breaking changes to the log API in
`CHANGELOG.md` and the migration guide

- [PR Build Summary](https://build.rerun.io/pr/7924)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)

To run all checks from `main`, comment on the PR with `@rerun-bot
full-check`.
…to `AsyncDecoderWrapper`
  • Loading branch information
Wumpf authored Oct 29, 2024
1 parent 7174abe commit 3cd789e
Show file tree
Hide file tree
Showing 20 changed files with 603 additions and 688 deletions.
5 changes: 5 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6518,14 +6518,18 @@ dependencies = [
"econtext",
"indicatif",
"itertools 0.13.0",
"js-sys",
"parking_lot",
"re_build_info",
"re_build_tools",
"re_log",
"re_mp4",
"re_rav1d",
"re_tracing",
"serde",
"thiserror",
"wasm-bindgen",
"web-sys",
]

[[package]]
Expand Down Expand Up @@ -6583,6 +6587,7 @@ dependencies = [
"re_types_blueprint",
"re_types_core",
"re_ui",
"re_video",
"re_viewer_context",
"re_viewport",
"re_viewport_blueprint",
Expand Down
21 changes: 21 additions & 0 deletions crates/store/re_video/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ features = ["all"]
[features]
default = ["av1"]

## Enable serialization for data structures that support it.
serde = ["dep:serde"]

## Native AV1 decoding.
av1 = ["dep:dav1d"]

Expand All @@ -46,6 +49,7 @@ econtext.workspace = true
itertools.workspace = true
parking_lot.workspace = true
re_mp4.workspace = true
serde = { workspace = true, optional = true }
thiserror.workspace = true

# We enable re_rav1d on native, UNLESS we're on Linux Arm64
Expand All @@ -61,6 +65,23 @@ dav1d = { workspace = true, optional = true, default-features = false, features
"bitdepth_8",
] }


# web
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys.workspace = true
wasm-bindgen.workspace = true
web-sys = { workspace = true, features = [
"DomException",
"EncodedVideoChunk",
"EncodedVideoChunkInit",
"EncodedVideoChunkType",
"HardwareAcceleration",
"VideoDecoder",
"VideoDecoderConfig",
"VideoDecoderInit",
"VideoFrame",
] }

[dev-dependencies]
indicatif.workspace = true
criterion.workspace = true
Expand Down
27 changes: 11 additions & 16 deletions crates/store/re_video/examples/frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,6 @@ fn main() {
video.config.coded_height
);

let mut decoder = re_video::decode::new_decoder(video_path.to_string(), &video)
.expect("Failed to create decoder");

write_video_frames(&video, &video_blob, decoder.as_mut(), &output_dir);
}

fn write_video_frames(
video: &re_video::VideoData,
video_blob: &[u8],
decoder: &mut dyn re_video::decode::SyncDecoder,
output_dir: &PathBuf,
) {
let progress = ProgressBar::new(video.samples.len() as u64).with_message("Decoding video");
progress.enable_steady_tick(Duration::from_millis(100));

Expand All @@ -59,11 +47,18 @@ fn write_video_frames(
}
};

let mut decoder = re_video::decode::new_decoder(
&video_path.to_string(),
&video,
re_video::decode::DecodeHardwareAcceleration::Auto,
on_output,
)
.expect("Failed to create decoder");

let start = Instant::now();
for sample in &video.samples {
let should_stop = std::sync::atomic::AtomicBool::new(false);
let chunk = sample.get(video_blob).unwrap();
decoder.submit_chunk(&should_stop, chunk, &on_output);
let chunk = sample.get(&video_blob).unwrap();
decoder.submit_chunk(chunk).expect("Failed to submit chunk");
}

let end = Instant::now();
Expand All @@ -78,7 +73,7 @@ fn write_video_frames(
);

println!("Writing frames to {}", output_dir.display());
std::fs::create_dir_all(output_dir).expect("failed to create output directory");
std::fs::create_dir_all(&output_dir).expect("failed to create output directory");

let width = num_digits(frames.len());
for (i, frame) in frames.iter().enumerate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ use std::sync::{

use crossbeam::channel::{unbounded, Receiver, Sender};

use super::{Chunk, Frame, OutputCallback, Result, SyncDecoder};
use super::{AsyncDecoder, Chunk, Frame, OutputCallback, Result};

enum Command {
Chunk(Chunk),
Flush { on_done: Sender<()> },
Reset,
Stop,
}
Expand All @@ -33,8 +32,25 @@ impl Default for Comms {
}
}

/// Blocking decoder of video chunks.
#[cfg(with_dav1d)]
pub trait SyncDecoder {
/// Submit some work and read the results.
///
/// Stop early if `should_stop` is `true` or turns `true`.
fn submit_chunk(
&mut self,
should_stop: &std::sync::atomic::AtomicBool,
chunk: Chunk,
on_output: &OutputCallback,
);

/// Clear and reset everything
fn reset(&mut self) {}
}

/// Runs a [`SyncDecoder`] in a background thread, for non-blocking video decoding.
pub struct AsyncDecoder {
pub struct AsyncDecoderWrapper {
/// Where the decoding happens
_thread: std::thread::JoinHandle<()>,

Expand All @@ -45,7 +61,7 @@ pub struct AsyncDecoder {
comms: Comms,
}

impl AsyncDecoder {
impl AsyncDecoderWrapper {
pub fn new(
debug_name: String,
mut sync_decoder: Box<dyn SyncDecoder + Send>,
Expand Down Expand Up @@ -75,18 +91,22 @@ impl AsyncDecoder {
comms,
}
}
}

impl AsyncDecoder for AsyncDecoderWrapper {
// NOTE: The interface is all `&mut self` to avoid certain types of races.
pub fn decode(&mut self, chunk: Chunk) {
fn submit_chunk(&mut self, chunk: Chunk) -> Result<()> {
re_tracing::profile_function!();
self.command_tx.send(Command::Chunk(chunk)).ok();

Ok(())
}

/// Resets the decoder.
///
/// This does not block, all chunks sent to `decode` before this point will be discarded.
// NOTE: The interface is all `&mut self` to avoid certain types of races.
pub fn reset(&mut self) {
fn reset(&mut self) -> Result<()> {
re_tracing::profile_function!();

// Increment resets first…
Expand All @@ -96,19 +116,12 @@ impl AsyncDecoder {

// …so it is visible on the decoder thread when it gets the `Reset` command.
self.command_tx.send(Command::Reset).ok();
}

/// Blocks until all pending frames have been decoded.
// NOTE: The interface is all `&mut self` to avoid certain types of races.
pub fn flush(&mut self) {
re_tracing::profile_function!();
let (tx, rx) = crossbeam::channel::bounded(0);
self.command_tx.send(Command::Flush { on_done: tx }).ok();
rx.recv().ok();
Ok(())
}
}

impl Drop for AsyncDecoder {
impl Drop for AsyncDecoderWrapper {
fn drop(&mut self) {
re_tracing::profile_function!();

Expand Down Expand Up @@ -145,9 +158,6 @@ fn decoder_thread(
decoder.submit_chunk(&comms.should_stop, chunk, on_output);
}
}
Command::Flush { on_done } => {
on_done.send(()).ok();
}
Command::Reset => {
decoder.reset();
comms.num_outstanding_resets.fetch_sub(1, Ordering::Release);
Expand Down
6 changes: 3 additions & 3 deletions crates/store/re_video/src/decode/av1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use crate::Time;
use dav1d::{PixelLayout, PlanarImageComponent};

use super::{
Chunk, Error, Frame, OutputCallback, PixelFormat, Result, SyncDecoder, YuvMatrixCoefficients,
YuvPixelLayout, YuvRange,
async_decoder_wrapper::SyncDecoder, Chunk, Error, Frame, OutputCallback, PixelFormat, Result,
YuvMatrixCoefficients, YuvPixelLayout, YuvRange,
};

pub struct SyncDav1dDecoder {
Expand Down Expand Up @@ -232,7 +232,7 @@ fn output_picture(
width: picture.width(),
height: picture.height(),
format,
timestamp: Time(picture.timestamp().unwrap_or(0)),
presentation_timestamp: Time(picture.timestamp().unwrap_or(0)),
duration: Time(picture.duration()),
};
on_output(Ok(frame));
Expand Down
Loading

0 comments on commit 3cd789e

Please sign in to comment.