Files
oak-editor/crates/oakcodec/tarpaulin-report.html
T
Mike-Solar 013a175707 refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
2026-08-10 20:24:25 +08:00

794 lines
1.5 MiB
Plaintext

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>:root {
--color: black;
--bg: white;
--head-bg: white;
--link: #338;
--blue: #ccf;
--red: #fcc;
--yellow: #ffc;
--green: #cfc;
}
[data-theme='dark'] {
--color: white;
--bg: black;
--head-bg: #333;
--link: #aaf;
--blue: #225;
--red: #522;
--yellow: #552;
--green: #252;
}
html,
body {
margin: 0;
padding: 0;
color: var(--color);
background: var(--bg);
}
.app {
margin: 10px;
padding: 0;
}
.files-list {
margin: 10px 0 0;
width: 100%;
border-collapse: collapse;
}
.files-list__head {
border: 1px solid #999;
}
.files-list__head > tr > th {
padding: 10px;
border: 1px solid #999;
text-align: left;
font-weight: normal;
background: var(--head-bg);
}
.files-list__body {
}
.files-list__file {
cursor: pointer;
}
.files-list__file:hover {
background: var(--blue);
}
.files-list__file > td {
padding: 10px;
border: 1px solid #999;
}
.files-list__file > td:first-child::before {
content: '\01F4C4';
margin-right: 1em;
}
.files-list__file_low {
background: var(--red);
}
.files-list__file_medium {
background: var(--yellow);
}
.files-list__file_high {
background: var(--green);
}
.files-list__file_folder > td:first-child::before {
content: '\01F4C1';
margin-right: 1em;
}
.file-header {
border: 1px solid #999;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background: var(--bg);
}
.file-header__back {
margin: 10px;
cursor: pointer;
flex-shrink: 0;
flex-grow: 0;
text-decoration: underline;
color: var(--link);
}
.file-header__name {
margin: 10px;
flex-shrink: 2;
flex-grow: 2;
}
.file-header__stat {
margin: 10px;
flex-shrink: 0;
flex-grow: 0;
}
.file-content {
margin: 10px 0 0;
border: 1px solid #999;
padding: 10px;
counter-reset: line;
display: flex;
flex-direction: column;
}
.code-line::before {
content: counter(line);
margin-right: 72px;
}
.code-line {
margin: 0;
height: 1em;
counter-increment: line;
position: absolute;
padding: 0 0.3em 0.3em 0.3em;
display: inherit;
width: 100%;
}
.code-line_covered {
background: var(--green);
}
.code-line_uncovered {
background: var(--red);
}
.code-text-container {
position: relative;
height: 1em;
padding: 0.3em 0;
}
.cover-indicator {
display: flex;
width: 100%;
position: absolute;
justify-content: end;
height: 1em;
align-items: center;
padding: 0 0.3em 0.3em 0.3em;
}
.cover-indicator.check-cover::after {
content: "\2713";
font-weight: bold;
background-color: var(--green);
height: 1em;
}
.cover-indicator.no-cover::after {
content: "\2716";
font-weight: bold;
background-color: var(--red);
height: 1em;
}
.stat-line-hit {
max-width: 48px;
overflow: hidden;
font-weight: bold;
margin-right: 4px;
background-color: var(--green);
position: relative;
top: 0.1em;
}
#theme-toggle-label {
margin-left: 1ch;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
var data = {"files":[{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","common.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational,\n//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils,\n//! colortransform).\n//!\n//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`,\n//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref,\n//! release, abi_version}` layout from `include/common/handle.h`, so the\n//! codec module can store them by value and pass them straight across\n//! the FFI boundary. Function signatures match the public headers\n//! verbatim; symbols resolve at link time.\n//!\n//! The oakcore audio parameters use a pointer-based C ABI instead of the\n//! by-value handle convention: `oakcore_audioparams_*` take and return\n//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/\n//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw\n//! pointers to the crate's handle structs; `oakcore_audioparams_time_base`\n//! returns a newly allocated rational the caller must release with\n//! `oakcore_rational_free`.\n\nuse std::ffi::{c_char, c_int, c_void};\n\n/// `OakVideoParams` — refcounted video-parameter handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakVideoParams {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (see [`OAKCODEC_ABI_VERSION`]).\n\tpub abi_version: u32,\n}\n\n/// `OakAudioParams` — refcounted audio-parameter handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakAudioParams {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakSubtitleParams` — refcounted subtitle-parameter handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakSubtitleParams {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec\n/// only stores and forwards it).\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakNodeBlock {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n// The handle structs are opaque refcounted handles pointing into a C\n// library; the boxed objects are independently synchronized there, so\n// moving a handle between threads is sound.\nunsafe impl Send for OakVideoParams {}\nunsafe impl Sync for OakVideoParams {}\nunsafe impl Send for OakAudioParams {}\nunsafe impl Sync for OakAudioParams {}\nunsafe impl Send for OakSubtitleParams {}\nunsafe impl Sync for OakSubtitleParams {}\nunsafe impl Send for OakNodeBlock {}\nunsafe impl Sync for OakNodeBlock {}\n\nextern \"C\" {\n\t/// `oakcommon_videoparams_init`.\n\tpub fn oakcommon_videoparams_init() -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_init_basic`.\n\tpub fn oakcommon_videoparams_init_basic(width: c_int, height: c_int) -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_init_with_time_base`.\n\tpub fn oakcommon_videoparams_init_with_time_base(\n\t\twidth: c_int,\n\t\theight: c_int,\n\t\ttime_base_num: i64,\n\t\ttime_base_den: i64,\n\t) -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_free` (NULL/empty no-op).\n\tpub fn oakcommon_videoparams_free(params: *mut OakVideoParams);\n\t/// `oakcommon_videoparams_get_width`.\n\tpub fn oakcommon_videoparams_get_width(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_height`.\n\tpub fn oakcommon_videoparams_get_height(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_format`.\n\tpub fn oakcommon_videoparams_get_format(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_time_base` (num/den out).\n\tpub fn oakcommon_videoparams_get_time_base(\n\t\tparams: OakVideoParams,\n\t\tout_num: *mut i64,\n\t\tout_den: *mut i64,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_width`.\n\tpub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int);\n\t/// `oakcommon_videoparams_set_height`.\n\tpub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int);\n\t/// `oakcommon_videoparams_set_format`.\n\tpub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int);\n\t/// `oakcommon_videoparams_get_is_valid`.\n\tpub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_equals`.\n\tpub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_time_base`.\n\tpub fn oakcommon_videoparams_set_time_base(\n\t\tparams: OakVideoParams,\n\t\tnum: i64,\n\t\tden: i64,\n\t);\n\t/// `oakcommon_videoparams_set_frame_rate`.\n\tpub fn oakcommon_videoparams_set_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tnum: i64,\n\t\tden: i64,\n\t);\n\t/// `oakcommon_videoparams_set_pixel_aspect_ratio`.\n\tpub fn oakcommon_videoparams_set_pixel_aspect_ratio(\n\t\tparams: OakVideoParams,\n\t\tnum: i64,\n\t\tden: i64,\n\t);\n\t/// `oakcommon_videoparams_set_interlacing`.\n\tpub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int);\n\t/// `oakcommon_videoparams_set_duration`.\n\tpub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64);\n\t/// `oakcommon_videoparams_set_start_time`.\n\tpub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64);\n\t/// `oakcommon_videoparams_set_color_range`.\n\tpub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int);\n\t/// `oakcommon_videoparams_set_video_type`.\n\tpub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int);\n\t/// `oakcommon_videoparams_set_channel_count`.\n\tpub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int);\n\t/// `oakcommon_videoparams_set_color_primaries`.\n\tpub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int);\n\t/// `oakcommon_videoparams_set_color_transfer`.\n\tpub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int);\n\t/// `oakcommon_videoparams_set_premultiplied_alpha`.\n\tpub fn oakcommon_videoparams_set_premultiplied_alpha(params: OakVideoParams, premultiplied: c_int);\n\t/// `oakcommon_videoparams_set_enabled`.\n\tpub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int);\n\t/// `oakcommon_videoparams_static_get_bytes_per_pixel`.\n\tpub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_frame_rate_as_time_base`.\n\tpub fn oakcommon_videoparams_frame_rate_as_time_base(\n\t\tframe_rate_num: i64,\n\t\tframe_rate_den: i64,\n\t\tout_num: *mut i64,\n\t\tout_den: *mut i64,\n\t);\n\t/// `oakcommon_videoparams_get_stream_index`.\n\tpub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_stream_index`.\n\tpub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int);\n\t/// `oakcommon_videoparams_get_divider`.\n\tpub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_divider`.\n\tpub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int);\n\t// NOTE: the remaining video getters below take the value-style form the\n\t// crate's existing bridge uses (the real oakcommon headers use out-pointer\n\t// args); `get_frame_rate` needs both values so it keeps the out pair.\n\t/// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out).\n\tpub fn oakcommon_videoparams_get_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tout_num: *mut c_int,\n\t\tout_den: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_duration` (time-base units).\n\tpub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -\u003e i64;\n\t/// `oakcommon_videoparams_get_channel_count`.\n\tpub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_color_primaries`.\n\tpub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_color_transfer`.\n\tpub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_interlacing` (`Interlacing` value).\n\tpub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate).\n\tpub fn oakcore_audioparams_create(\n\t\tsample_rate: c_int,\n\t\tchannel_layout: u64,\n\t\tformat: c_int,\n\t) -\u003e *mut OakAudioParams;\n\t/// `oakcore_audioparams_free` (NULL no-op).\n\tpub fn oakcore_audioparams_free(params: *mut OakAudioParams);\n\t/// `oakcore_audioparams_sample_rate`.\n\tpub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_set_sample_rate`.\n\tpub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int);\n\t/// `oakcore_audioparams_channel_layout`.\n\tpub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -\u003e u64;\n\t/// `oakcore_audioparams_set_channel_layout`.\n\tpub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64);\n\t/// `oakcore_audioparams_set_time_base`.\n\tpub fn oakcore_audioparams_set_time_base(\n\t\tparams: *mut OakAudioParams,\n\t\tnum: c_int,\n\t\tden: c_int,\n\t);\n\t/// `oakcore_audioparams_set_format`.\n\tpub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int);\n\t/// `oakcore_audioparams_set_stream_index`.\n\tpub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int);\n\t/// `oakcore_audioparams_set_duration`.\n\tpub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64);\n\t/// `oakcore_audioparams_channel_count`.\n\tpub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_format`.\n\tpub fn oakcore_audioparams_format(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_stream_index`.\n\tpub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_duration`.\n\tpub fn oakcore_audioparams_duration(params: *const OakAudioParams) -\u003e i64;\n\t/// `oakcore_audioparams_is_valid`.\n\tpub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_time_base` (newly allocated rational; caller\n\t/// releases with `oakcore_rational_free`).\n\tpub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -\u003e *mut c_void;\n\t/// `oakcore_rational_numerator`.\n\tpub fn oakcore_rational_numerator(rational: *const c_void) -\u003e c_int;\n\t/// `oakcore_rational_denominator`.\n\tpub fn oakcore_rational_denominator(rational: *const c_void) -\u003e c_int;\n\t/// `oakcore_rational_free` (NULL no-op).\n\tpub fn oakcore_rational_free(rational: *mut c_void);\n\t/// `oakcommon_subtitleparams_get_stream_index`.\n\tpub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -\u003e c_int;\n\t/// `oakcommon_subtitleparams_generate_ass_header`.\n\tpub fn oakcommon_subtitleparams_generate_ass_header(\n\t\tparams: OakSubtitleParams,\n\t\twidth: c_int,\n\t\theight: c_int,\n\t);\n\t/// `oakcommon_subtitleparams_add_subtitle`.\n\tpub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char);\n\t/// `oakcommon_config_get_int`.\n\tpub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -\u003e c_int;\n\t/// `oakcommon_config_get_bool`.\n\tpub fn oakcommon_config_get_bool(group: *const c_char, key: *const c_char, default: c_int) -\u003e c_int;\n\t/// `oakcommon_config_get` (two-stage string access).\n\tpub fn oakcommon_config_get(\n\t\tgroup: *const c_char,\n\t\tkey: *const c_char,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_filefunctions_init`.\n\tpub fn oakcommon_filefunctions_init();\n\t/// `oakcommon_filefunctions_get_configuration_location` (two-stage).\n\tpub fn oakcommon_filefunctions_get_configuration_location(\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_filefunctions_get_unique_file_identifier`.\n\tpub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -\u003e i64;\n\t/// `oakcommon_filefunctions_get_application_path` (two-stage).\n\tpub fn oakcommon_filefunctions_get_application_path(\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_filefunctions_free` (frees an internally cached string).\n\tpub fn oakcommon_filefunctions_free(ptr: *mut c_void);\n\t/// `oakcommon_colortransform_init_output`.\n\tpub fn oakcommon_colortransform_init_output(\n\t\tsrc_colorspace: c_int,\n\t\tsrc_trc: c_int,\n\t\tdst_colorspace: c_int,\n\t\tdst_trc: c_int,\n\t\tpremultiplied: c_int,\n\t\tchroma_coeffs: *const c_void,\n\t) -\u003e OakVideoParams;\n\t/// `oakcommon_colortransform_get_output`.\n\tpub fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams);\n\t/// `oakcommon_colortransform_free`.\n\tpub fn oakcommon_colortransform_free(params: *mut OakVideoParams);\n\t/// `oakcommon_ffmpegutils_get_native_sample_format`.\n\tpub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_compatible_pixel_format`.\n\tpub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`.\n\tpub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.\n\tpub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`.\n\tpub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.\n\tpub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -\u003e c_int;\n\t/// `oakcommon_oiioutils_init`.\n\tpub fn oakcommon_oiioutils_init();\n\t/// `oakcommon_oiioutils_get_oiio_base_type_from_format`.\n\tpub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_oiioutils_get_format_from_oiio_basetype`.\n\tpub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -\u003e c_int;\n\t/// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out).\n\tpub fn oakcommon_oiioutils_get_pixel_aspect_ratio(\n\t\twidth: c_int,\n\t\theight: c_int,\n\t\tout_num: *mut c_int,\n\t\tout_den: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_oiioutils_free`.\n\tpub fn oakcommon_oiioutils_free();\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","mod.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! C ABI imports from the other oak modules.\n//!\n//! The codec module links against oakcommon and oakrender at the C ABI.\n//! Every signature below mirrors the corresponding public header\n//! verbatim and is resolved at link time. The by-value handle structs\n//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of\n//! the `{ctx, addref, release, abi_version}` layout so the codec crate\n//! can hold and hand them across the FFI boundary without translation.\n\npub mod common;\npub mod render;\n\n// In-memory mocks for the oakcommon/oakrender C ABI so the crate links\n// and is testable under `cargo test` (where those dylibs are absent).\n#[cfg(test)]\npub mod test_stubs;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","render.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! oakrender C ABI imports (display textures, renderers, cancel atoms).\n//!\n//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a\n//! `CancelAtom`; both are oakrender refcounted handles with the standard\n//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params`\n//! is a flattened POD the decoders construct to describe the frame.\n\nuse std::ffi::{c_char, c_int, c_void};\n\n/// `OakRenderTexture` — refcounted GPU texture handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakRenderTexture {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakCancelAtom` — refcounted cancellation atom handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakCancelAtom {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakRenderRenderer` — refcounted display-renderer handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakRenderRenderer {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakCodecFrame {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n// Refcounted opaque handles; thread-safe in the C library.\nunsafe impl Send for OakRenderTexture {}\nunsafe impl Sync for OakRenderTexture {}\nunsafe impl Send for OakCancelAtom {}\nunsafe impl Sync for OakCancelAtom {}\nunsafe impl Send for OakRenderRenderer {}\nunsafe impl Sync for OakRenderRenderer {}\nunsafe impl Send for OakCodecFrame {}\nunsafe impl Sync for OakCodecFrame {}\n\n/// `oakrender_video_params` — flattened POD of `olive::VideoParams`\n/// passed into oakrender; see `include/render/renderer.h`.\n#[repr(C)]\npub struct oakrender_video_params {\n\t/// Width in pixels.\n\tpub width: c_int,\n\t/// Height in pixels.\n\tpub height: c_int,\n\t/// Frame-duration numerator (e.g. 1001/30000 s).\n\tpub time_base_num: c_int,\n\t/// Frame-duration denominator.\n\tpub time_base_den: c_int,\n\t/// `olive::PixelFormat::Format`.\n\tpub format: c_int,\n\t/// Pixel-aspect numerator.\n\tpub pixel_aspect_num: c_int,\n\t/// Pixel-aspect denominator.\n\tpub pixel_aspect_den: c_int,\n\t/// `olive::VideoParams::Interlacing`.\n\tpub interlacing: c_int,\n\t/// `olive::VideoParams::ColorRange`.\n\tpub color_range: c_int,\n\t/// Preview-resolution divider (1 = full).\n\tpub divider: c_int,\n\t/// `olive::VideoParams::Type` (0 = video).\n\tpub video_type: c_int,\n\t/// 0/1 premultiplied alpha.\n\tpub premultiplied_alpha: c_int,\n}\n\nextern \"C\" {\n\t/// `oakrender_cancelatom_init`.\n\tpub fn oakrender_cancelatom_init() -\u003e OakCancelAtom;\n\t/// `oakrender_cancelatom_free` (NULL/empty no-op).\n\tpub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);\n\t/// `oakrender_cancelatom_is_cancelled`.\n\tpub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -\u003e c_int;\n\t/// `oakrender_cancelatom_heard_cancel`.\n\tpub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -\u003e c_int;\n\t/// `oakrender_cancelatom_cancel`.\n\tpub fn oakrender_cancelatom_cancel(atom: OakCancelAtom);\n\t/// `oakrender_cancelatom_get_native`.\n\tpub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -\u003e *mut c_void;\n\t/// `oakrender_display_texture_create`.\n\tpub fn oakrender_display_texture_create(\n\t\trenderer: OakRenderRenderer,\n\t\tparams: *const oakrender_video_params,\n\t\tdata: *const c_void,\n\t\tlinesize: c_int,\n\t) -\u003e OakRenderTexture;\n\t/// `oakrender_display_texture_retain`.\n\tpub fn oakrender_display_texture_retain(texture: OakRenderTexture) -\u003e OakRenderTexture;\n\t/// `oakrender_display_texture_free` (NULL/empty no-op).\n\tpub fn oakrender_display_texture_free(texture: *mut OakRenderTexture);\n\t/// `oakrender_display_texture_upload`.\n\tpub fn oakrender_display_texture_upload(texture: OakRenderTexture) -\u003e c_int;\n\t/// `oakrender_display_texture_download`.\n\tpub fn oakrender_display_texture_download(\n\t\ttexture: OakRenderTexture,\n\t\tpixels: *mut c_void,\n\t\tlinesize: c_int,\n\t) -\u003e c_int;\n\t/// `oakrender_display_texture_get_params`.\n\tpub fn oakrender_display_texture_get_params(\n\t\ttexture: OakRenderTexture,\n\t\tout: *mut oakrender_video_params,\n\t) -\u003e c_int;\n\t/// `oakrender_display_texture_id`.\n\tpub fn oakrender_display_texture_id(texture: OakRenderTexture) -\u003e c_int;\n\t/// `oakrender_display_texture_is_dummy`.\n\tpub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -\u003e c_int;\n\t/// `oakrender_display_texture_get_frame` (two-stage frame access).\n\tpub fn oakrender_display_texture_get_frame(\n\t\ttexture: OakRenderTexture,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakrender_codec_frame_width`.\n\tpub fn oakrender_codec_frame_width(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_height`.\n\tpub fn oakrender_codec_frame_height(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_fb_format`.\n\tpub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_free` (NULL/empty no-op).\n\tpub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame);\n\t/// `oakrender_codec_frame_allocate`.\n\tpub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_linesize_bytes`.\n\tpub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_is_allocated`.\n\tpub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_display_renderer_blit_color_managed`.\n\tpub fn oakrender_display_renderer_blit_color_managed(\n\t\trenderer: OakRenderRenderer,\n\t\tjob: *const c_void,\n\t\tdst_texture: OakRenderTexture,\n\t\tparams: *const oakrender_video_params,\n\t) -\u003e c_int;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","test_stubs.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `#[cfg(test)]` in-memory mocks for the oakcommon / oakrender C ABI.\n//!\n//! The real symbols live in `liboakcommon` / `liboakrender` and are only\n//! linked when the crate is built against those dylibs (ctest). Under\n//! `cargo test` those libraries are not linked, so `extern \"C\"` call\n//! sites in the crate would otherwise fail to resolve. These\n//! `#[no_mangle] extern \"C\"` definitions provide the symbols and back the\n//! two stateful pieces the crate actually reads (video params and cancel\n//! atoms) with real in-memory state, so `Frame`, `FootageDescription`\n//! and friends are meaningfully testable. Everything else returns a\n//! deterministic neutral value.\n//!\n//! Kept strictly under `#[cfg(test)]`; never compiled into a real build.\n\n#![allow(dead_code, unused_variables)]\n\nuse std::collections::HashMap;\nuse std::ffi::{c_char, c_int, c_void, CStr};\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::{Mutex, OnceLock};\n\nuse crate::bridge::common::{\n\tOakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams,\n};\nuse crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture};\nuse crate::handle::OAKCODEC_ABI_VERSION;\n\n/// Per-`OakVideoParams` backing state.\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\nstruct MockParams {\n\twidth: i32,\n\theight: i32,\n\tformat: i32,\n\ttime_base_num: i64,\n\ttime_base_den: i64,\n\tstream_index: i32,\n\tdivider: i32,\n\tframe_rate_num: i32,\n\tframe_rate_den: i32,\n\tduration: i64,\n\tchannel_count: i32,\n\tcolor_primaries: i32,\n\tcolor_trc: i32,\n\tinterlacing: i32,\n\tpixel_aspect_num: i32,\n\tpixel_aspect_den: i32,\n\tstart_time: i64,\n\tcolor_range: i32,\n\tvideo_type: i32,\n\tpremultiplied_alpha: i32,\n\tenabled: i32,\n}\n\nfn params_store() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, MockParams\u003e\u003e {\n\tstatic S: OnceLock\u003cMutex\u003cHashMap\u003cusize, MockParams\u003e\u003e\u003e = OnceLock::new();\n\tS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\n/// Build a handle whose `ctx` owns a leaked `MockParams`; the map entry\n/// keeps the storage alive until `free` is called.\nfn new_params_handle(p: MockParams) -\u003e OakVideoParams {\n\tlet raw = Box::into_raw(Box::new(p.clone()));\n\tparams_store().lock().unwrap().insert(raw as usize, p);\n\tOakVideoParams {\n\t\tctx: raw as *mut c_void,\n\t\taddref: None,\n\t\trelease: None,\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\nfn params_ref(ctx: *mut c_void) -\u003e Option\u003c\u0026'static mut MockParams\u003e {\n\tlet store = params_store().lock().unwrap();\n\tstore.get(\u0026(ctx as usize))?;\n\tdrop(store);\n\t// SAFETY: entries are only removed by oakcommon_videoparams_free while\n\t// the caller still holds the handle, so the box outlives this borrow.\n\tSome(unsafe { \u0026mut *(ctx as *mut MockParams) })\n}\n\nfn params_get(ctx: *mut c_void) -\u003e MockParams {\n\tlet store = params_store().lock().unwrap();\n\tstore\n\t\t.get(\u0026(ctx as usize))\n\t\t.cloned()\n\t\t.unwrap_or_default()\n}\n\nfn params_set(ctx: *mut c_void, f: impl FnOnce(\u0026mut MockParams)) {\n\tlet mut store = params_store().lock().unwrap();\n\tif let Some(p) = store.get_mut(\u0026(ctx as usize)) {\n\t\tf(p);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_videoparams_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_init() -\u003e OakVideoParams {\n\tnew_params_handle(MockParams {\n\t\tchannel_count: 4, // internal RGBA pipeline layout\n\t\t..Default::default()\n\t})\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_init_basic(width: c_int, height: c_int) -\u003e OakVideoParams {\n\tnew_params_handle(MockParams {\n\t\twidth,\n\t\theight,\n\t\tdivider: 1,\n\t\tchannel_count: 4, // internal RGBA pipeline layout\n\t\t..Default::default()\n\t})\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_init_with_time_base(\n\twidth: c_int,\n\theight: c_int,\n\ttime_base_num: i64,\n\ttime_base_den: i64,\n) -\u003e OakVideoParams {\n\tnew_params_handle(MockParams {\n\t\twidth,\n\t\theight,\n\t\ttime_base_num,\n\t\ttime_base_den,\n\t\tdivider: 1,\n\t\tchannel_count: 4, // internal RGBA pipeline layout\n\t\t..Default::default()\n\t})\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_free(params: *mut OakVideoParams) {\n\tif params.is_null() {\n\t\treturn;\n\t}\n\tunsafe {\n\t\tlet ctx = (*params).ctx;\n\t\tparams_store().lock().unwrap().remove(\u0026(ctx as usize));\n\t\tif !ctx.is_null() {\n\t\t\tdrop(Box::from_raw(ctx as *mut MockParams));\n\t\t}\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_width(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).width\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_height(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).height\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_format(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).format\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_time_base(\n\tparams: OakVideoParams,\n\tout_num: *mut i64,\n\tout_den: *mut i64,\n) -\u003e c_int {\n\tlet p = params_get(params.ctx);\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = p.time_base_num };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = p.time_base_den };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int) {\n\tparams_set(params.ctx, |p| p.width = width);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int) {\n\tparams_set(params.ctx, |p| p.height = height);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) {\n\tparams_set(params.ctx, |p| p.format = format);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -\u003e c_int {\n\tlet p = params_get(params.ctx);\n\t((p.width \u003e 0) \u0026\u0026 (p.height \u003e 0)) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -\u003e c_int {\n\t(params_get(a.ctx) == params_get(b.ctx)) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -\u003e c_int {\n\t// U10 packs to 4 bytes; U8 to 1; U16/F16 to 2; F32 to 4.\n\tmatch format {\n\t\t0 =\u003e 1, // U8\n\t\t1 =\u003e 4, // U10\n\t\t2 =\u003e 2, // U16\n\t\t3 =\u003e 2, // F16\n\t\t4 =\u003e 4, // F32\n\t\t_ =\u003e 0,\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_frame_rate_as_time_base(\n\tframe_rate_num: i64,\n\tframe_rate_den: i64,\n\tout_num: *mut i64,\n\tout_den: *mut i64,\n) {\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = frame_rate_den };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = frame_rate_num };\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).stream_index\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int) {\n\tparams_set(params.ctx, |p| p.stream_index = index);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_divider(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).divider\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int) {\n\tparams_set(params.ctx, |p| p.divider = divider);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_frame_rate(\n\tparams: OakVideoParams,\n\tout_num: *mut c_int,\n\tout_den: *mut c_int,\n) -\u003e c_int {\n\tlet p = params_get(params.ctx);\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = p.frame_rate_num };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = p.frame_rate_den };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_duration(params: OakVideoParams) -\u003e i64 {\n\tparams_get(params.ctx).duration\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).channel_count\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).color_primaries\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).color_trc\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).interlacing\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_time_base(\n\tparams: OakVideoParams,\n\tnum: i64,\n\tden: i64,\n) {\n\tparams_set(params.ctx, |p| {\n\t\tp.time_base_num = num;\n\t\tp.time_base_den = den;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_frame_rate(\n\tparams: OakVideoParams,\n\tnum: i64,\n\tden: i64,\n) {\n\tparams_set(params.ctx, |p| {\n\t\tp.frame_rate_num = num as i32;\n\t\tp.frame_rate_den = den as i32;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_pixel_aspect_ratio(\n\tparams: OakVideoParams,\n\tnum: i64,\n\tden: i64,\n) {\n\tparams_set(params.ctx, |p| {\n\t\tp.pixel_aspect_num = num as i32;\n\t\tp.pixel_aspect_den = den as i32;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int) {\n\tparams_set(params.ctx, |p| p.interlacing = interlacing);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) {\n\tparams_set(params.ctx, |p| p.duration = duration);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64) {\n\tparams_set(params.ctx, |p| p.start_time = start_time);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int) {\n\tparams_set(params.ctx, |p| p.color_range = color_range);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) {\n\tparams_set(params.ctx, |p| p.video_type = video_type);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int) {\n\tparams_set(params.ctx, |p| p.channel_count = channels);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int) {\n\tparams_set(params.ctx, |p| p.color_primaries = primaries);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int) {\n\tparams_set(params.ctx, |p| p.color_trc = transfer);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_premultiplied_alpha(\n\tparams: OakVideoParams,\n\tpremultiplied: c_int,\n) {\n\tparams_set(params.ctx, |p| p.premultiplied_alpha = premultiplied);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int) {\n\tparams_set(params.ctx, |p| p.enabled = enabled);\n}\n\n// ---------------------------------------------------------------------------\n// oakcore_audioparams_* / oakcore_rational_* (pointer-based in-memory state)\n// ---------------------------------------------------------------------------\n\n/// Per-`OakAudioParams` backing state. The real oakcore C ABI is\n/// pointer-based (`core/include/olive/core/oakcore/audioparams.h`), so\n/// these stubs own a boxed struct and return its raw pointer.\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\nstruct MockAudioParams {\n\tsample_rate: i32,\n\tchannel_layout: u64,\n\tformat: i32,\n\tstream_index: i32,\n\tduration: i64,\n\ttime_base_num: i32,\n\ttime_base_den: i32,\n}\n\nfn audio_params_store() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, MockAudioParams\u003e\u003e {\n\tstatic S: OnceLock\u003cMutex\u003cHashMap\u003cusize, MockAudioParams\u003e\u003e\u003e = OnceLock::new();\n\tS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\nfn audio_params_get(ctx: *const c_void) -\u003e MockAudioParams {\n\tlet store = audio_params_store().lock().unwrap();\n\tstore\n\t\t.get(\u0026(ctx as usize))\n\t\t.cloned()\n\t\t.unwrap_or_default()\n}\n\n/// Per-`OakRational` backing state (an owned `(num, den)` pair).\nfn rational_store() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, (i32, i32)\u003e\u003e {\n\tstatic S: OnceLock\u003cMutex\u003cHashMap\u003cusize, (i32, i32)\u003e\u003e\u003e = OnceLock::new();\n\tS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_create(\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tformat: c_int,\n) -\u003e *mut OakAudioParams {\n\t// The timebase starts at 1/sample_rate, mirroring the real header.\n\tlet p = MockAudioParams {\n\t\tsample_rate,\n\t\tchannel_layout,\n\t\tformat,\n\t\tstream_index: 0,\n\t\tduration: 0,\n\t\ttime_base_num: 1,\n\t\ttime_base_den: sample_rate,\n\t};\n\tlet raw = Box::into_raw(Box::new(p.clone()));\n\taudio_params_store().lock().unwrap().insert(raw as usize, p);\n\traw as *mut OakAudioParams\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_free(params: *mut OakAudioParams) {\n\tif params.is_null() {\n\t\treturn;\n\t}\n\taudio_params_store()\n\t\t.lock()\n\t\t.unwrap()\n\t\t.remove(\u0026(params as usize));\n\t// SAFETY: `params` was produced by `oakcore_audioparams_create` as a\n\t// boxed `MockAudioParams`; we hold the only reference after removal.\n\tunsafe { drop(Box::from_raw(params as *mut MockAudioParams)) };\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void).sample_rate\n}\n\nfn audio_params_set(ctx: *mut c_void, f: impl FnOnce(\u0026mut MockAudioParams)) {\n\tlet mut store = audio_params_store().lock().unwrap();\n\tif let Some(p) = store.get_mut(\u0026(ctx as usize)) {\n\t\tf(p);\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_sample_rate(\n\tparams: *mut OakAudioParams,\n\tsample_rate: c_int,\n) {\n\taudio_params_set(params as *mut c_void, |p| p.sample_rate = sample_rate);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -\u003e u64 {\n\taudio_params_get(params as *const c_void).channel_layout\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_channel_layout(\n\tparams: *mut OakAudioParams,\n\tlayout: u64,\n) {\n\taudio_params_set(params as *mut c_void, |p| p.channel_layout = layout);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_time_base(\n\tparams: *mut OakAudioParams,\n\tnum: c_int,\n\tden: c_int,\n) {\n\taudio_params_set(params as *mut c_void, |p| {\n\t\tp.time_base_num = num;\n\t\tp.time_base_den = den;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int) {\n\taudio_params_set(params as *mut c_void, |p| p.format = format);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_stream_index(\n\tparams: *mut OakAudioParams,\n\tindex: c_int,\n) {\n\taudio_params_set(params as *mut c_void, |p| p.stream_index = index);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64) {\n\taudio_params_set(params as *mut c_void, |p| p.duration = duration);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void)\n\t\t.channel_layout\n\t\t.count_ones() as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_format(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void).format\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void).stream_index\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_duration(params: *const OakAudioParams) -\u003e i64 {\n\taudio_params_get(params as *const c_void).duration\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -\u003e c_int {\n\tlet p = audio_params_get(params as *const c_void);\n\t(p.sample_rate \u003e 0 \u0026\u0026 p.channel_layout != 0 \u0026\u0026 p.format \u003e= 0) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_time_base(params: *const OakAudioParams) -\u003e *mut c_void {\n\tlet p = audio_params_get(params as *const c_void);\n\tlet r = (p.time_base_num, p.time_base_den);\n\tlet raw = Box::into_raw(Box::new(r));\n\trational_store().lock().unwrap().insert(raw as usize, r);\n\traw as *mut c_void\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_rational_numerator(rational: *const c_void) -\u003e c_int {\n\trational_store()\n\t\t.lock()\n\t\t.unwrap()\n\t\t.get(\u0026(rational as usize))\n\t\t.map(|r| r.0)\n\t\t.unwrap_or(0)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_rational_denominator(rational: *const c_void) -\u003e c_int {\n\trational_store()\n\t\t.lock()\n\t\t.unwrap()\n\t\t.get(\u0026(rational as usize))\n\t\t.map(|r| r.1)\n\t\t.unwrap_or(0)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_rational_free(rational: *mut c_void) {\n\tif rational.is_null() {\n\t\treturn;\n\t}\n\trational_store().lock().unwrap().remove(\u0026(rational as usize));\n\t// SAFETY: `rational` was produced by `oakcore_audioparams_time_base` as a\n\t// boxed `(i32, i32)` pair; we hold the only reference after removal.\n\tunsafe { drop(Box::from_raw(rational as *mut (i32, i32))) };\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_subtitleparams_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_subtitleparams_get_stream_index(_params: OakSubtitleParams) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_subtitleparams_generate_ass_header(\n\t_params: OakSubtitleParams,\n\t_width: c_int,\n\t_height: c_int,\n) {\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_subtitleparams_add_subtitle(\n\t_params: OakSubtitleParams,\n\t_text: *const c_char,\n) {\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_config_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_config_get_int(\n\t_group: *const c_char,\n\t_key: *const c_char,\n\tdefault: c_int,\n) -\u003e c_int {\n\tdefault\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_config_get_bool(\n\t_group: *const c_char,\n\t_key: *const c_char,\n\tdefault: c_int,\n) -\u003e c_int {\n\tdefault\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_config_get(\n\t_group: *const c_char,\n\t_key: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\t// Empty value: report the size needed (1 for NUL) and write NUL.\n\tif buf_size \u003c= 0 {\n\t\treturn 1;\n\t}\n\tif !buf.is_null() {\n\t\tunsafe { *buf = 0 };\n\t}\n\t1\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_filefunctions_*\n// ---------------------------------------------------------------------------\n\nstatic FAKE_PATH: \u0026[u8] = b\"/mock/config/oak\\0\";\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_init() {}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_get_configuration_location(\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tcopy_cstr(FAKE_PATH, buf, buf_size)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -\u003e i64 {\n\tif path.is_null() {\n\t\treturn 0;\n\t}\n\tunsafe { CStr::from_ptr(path) }\n\t\t.to_bytes()\n\t\t.iter()\n\t\t.fold(14695981039346656037u64, |acc, \u0026b| (acc ^ b as u64).wrapping_mul(1099511628211))\n\t\tas i64\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_get_application_path(\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tcopy_cstr(b\"/mock/app/oak\\0\", buf, buf_size)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_free(_ptr: *mut c_void) {}\n\n// ---------------------------------------------------------------------------\n// oakcommon_colortransform_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_colortransform_init_output(\n\t_src_colorspace: c_int,\n\t_src_trc: c_int,\n\t_dst_colorspace: c_int,\n\t_dst_trc: c_int,\n\t_premultiplied: c_int,\n\t_chroma_coeffs: *const c_void,\n) -\u003e OakVideoParams {\n\toakcommon_videoparams_init()\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams) {\n\tif !out.is_null() {\n\t\tunsafe { *out = params.clone() };\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_colortransform_free(params: *mut OakVideoParams) {\n\toakcommon_videoparams_free(params);\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_ffmpegutils_* (pure enum mapping; identity is a safe default)\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -\u003e c_int {\n\tsample_format\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -\u003e c_int {\n\tformat\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_oiioutils_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_init() {}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -\u003e c_int {\n\tbasetype\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_get_pixel_aspect_ratio(\n\t_width: c_int,\n\t_height: c_int,\n\tout_num: *mut c_int,\n\tout_den: *mut c_int,\n) -\u003e c_int {\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = 1 };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = 1 };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_free() {}\n\n// ---------------------------------------------------------------------------\n// oakrender_cancelatom_* (real in-memory cancel state)\n// ---------------------------------------------------------------------------\n\nstatic CANCEL_FLAGS: OnceLock\u003cMutex\u003cHashMap\u003cusize, bool\u003e\u003e\u003e = OnceLock::new();\n\nfn cancel_flags() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, bool\u003e\u003e {\n\tCANCEL_FLAGS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_init() -\u003e OakCancelAtom {\n\tstatic COUNTER: AtomicU64 = AtomicU64::new(1);\n\tlet id = COUNTER.fetch_add(1, Ordering::SeqCst) as usize;\n\tcancel_flags().lock().unwrap().insert(id, false);\n\tOakCancelAtom {\n\t\tctx: id as *mut c_void,\n\t\taddref: None,\n\t\trelease: None,\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) {\n\tif atom.is_null() {\n\t\treturn;\n\t}\n\tunsafe { cancel_flags().lock().unwrap().remove(\u0026((*atom).ctx as usize)) };\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -\u003e c_int {\n\t(*cancel_flags().lock().unwrap().get(\u0026(atom.ctx as usize)).unwrap_or(\u0026false)) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -\u003e c_int {\n\toakrender_cancelatom_is_cancelled(atom)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_cancel(atom: OakCancelAtom) {\n\tif let Some(f) = cancel_flags().lock().unwrap().get_mut(\u0026(atom.ctx as usize)) {\n\t\t*f = true;\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -\u003e *mut c_void {\n\tatom.ctx\n}\n\n// ---------------------------------------------------------------------------\n// oakrender_display_texture_* / codec_frame_* (neutral)\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_create(\n\t_renderer: OakRenderRenderer,\n\t_params: *const crate::bridge::render::oakrender_video_params,\n\t_data: *const c_void,\n\t_linesize: c_int,\n) -\u003e OakRenderTexture {\n\tOakRenderTexture {\n\t\tctx: std::ptr::null_mut(),\n\t\taddref: None,\n\t\trelease: None,\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_retain(texture: OakRenderTexture) -\u003e OakRenderTexture {\n\ttexture\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_free(_texture: *mut OakRenderTexture) {}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_upload(_texture: OakRenderTexture) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_download(\n\t_texture: OakRenderTexture,\n\t_pixels: *mut c_void,\n\t_linesize: c_int,\n) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_get_params(\n\t_texture: OakRenderTexture,\n\t_out: *mut crate::bridge::render::oakrender_video_params,\n) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_id(_texture: OakRenderTexture) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_is_dummy(_texture: OakRenderTexture) -\u003e c_int {\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_get_frame(\n\t_texture: OakRenderTexture,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tif buf_size \u003c= 0 {\n\t\treturn 1;\n\t}\n\tif !buf.is_null() {\n\t\tunsafe { *buf = 0 };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_width(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_height(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_fb_format(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_free(_frame: *mut OakCodecFrame) {}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_allocate(_frame: OakCodecFrame) -\u003e c_int {\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_linesize_bytes(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_is_allocated(_frame: OakCodecFrame) -\u003e c_int {\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_renderer_blit_color_managed(\n\t_renderer: OakRenderRenderer,\n\t_job: *const c_void,\n\t_dst_texture: OakRenderTexture,\n\t_params: *const crate::bridge::render::oakrender_video_params,\n) -\u003e c_int {\n\t0\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/// Two-stage string copy helper used by the mock getters. Returns the\n/// total size needed (including the trailing NUL), or truncates the buffer\n/// and writes a NUL terminator when the buffer is too small.\nfn copy_cstr(src: \u0026[u8], buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet needed = src.len() as c_int;\n\tif buf.is_null() || buf_size \u003c needed {\n\t\treturn needed;\n\t}\n\tunsafe {\n\t\tfor (i, \u0026b) in src.iter().enumerate() {\n\t\t\t*buf.add(i) = b as c_char;\n\t\t}\n\t}\n\tneeded\n}\n\n// Silence unused-import warnings when the by-value handle types are not\n// referenced by every build; they are part of the mock's public surface.\n#[allow(unused)]\nfn _keep(_: OakAudioParams, _: OakNodeBlock) {}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","conformmanager.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ConformManager` — pcm waveform cache files for fast scrubbing.\n//!\n//! Mirrors `src/codec/src/conformmanager.h`. Stateless (NOTES.md): actual\n//! conform work is delegated to the global task submit callback\n//! ([`crate::task`]); with no registrar the state queries report\n//! `Unavailable`. Deterministic per-channel filenames derive from the\n//! source + target audio params.\n\nuse std::ffi::CString;\nuse std::path::Path;\n\n/// Conform state of one audio stream.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum ConformState {\n\t/// Conform files exist.\n\tExists = 0,\n\t/// Conform is being generated.\n\tGenerating = 1,\n\t/// No task registrar; conform unavailable.\n\tUnavailable = 2,\n}\n\n/// `olive::ConformManager` — stateless conform query/produce manager.\npub struct ConformManager;\n\nimpl ConformManager {\n\t/// The process-wide ConformManager singleton.\n\tpub fn instance() -\u003e \u0026'static ConformManager {\n\t\tstatic INSTANCE: ConformManager = ConformManager;\n\t\t\u0026INSTANCE\n\t}\n\n\t/// Query (and when possible start) the conform of one audio stream.\n\t///\n\t/// `wait != 0` treats a post-submit miss as `Unavailable`; `wait == 0`\n\t/// reports it as `Generating`. Without a task registrar the result is\n\t/// always `Unavailable`.\n\tpub fn get_conform_state(\n\t\t\u0026self,\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\twait: bool,\n\t) -\u003e crate::error::Result\u003cConformState\u003e {\n\t\tlet filenames = conform_filenames(\n\t\t\tcache_path,\n\t\t\tsource_filename,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tsample_format,\n\t\t\tchannel_layout,\n\t\t);\n\n\t\t// Return existing conform if it exists.\n\t\tif all_conforms_exist(\u0026filenames) {\n\t\t\treturn Ok(ConformState::Exists);\n\t\t}\n\n\t\t// Interim state (pre-M8): no task system, conform cannot be generated.\n\t\tif !crate::task::task_submit_is_registered() {\n\t\t\treturn Ok(ConformState::Unavailable);\n\t\t}\n\n\t\t// The task owns the \".working\" temporary names and the rename to the\n\t\t// final per-channel filenames on success; output_filename carries the\n\t\t// first channel's final path and the task derives the siblings.\n\t\tlet req = crate::task::TaskRequest {\n\t\t\tkind: crate::task::TaskKind::Conform,\n\t\t\tinput_filename: source_filename,\n\t\t\toutput_filename: filenames.first().map(String::as_str).unwrap_or(\"\"),\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t\tproxy_width: 0,\n\t\t\tproxy_height: 0,\n\t\t};\n\n\t\t// Interim simplification: submission is synchronous — we always wait\n\t\t// for the submit to return, regardless of `wait`.\n\t\tif crate::task::submit_task(\u0026req).is_err() {\n\t\t\treturn Ok(ConformState::Unavailable);\n\t\t}\n\n\t\tif all_conforms_exist(\u0026filenames) {\n\t\t\treturn Ok(ConformState::Exists);\n\t\t}\n\n\t\tif wait {\n\t\t\t// Synchronous wait already happened and the conform still does not\n\t\t\t// exist: report the wait as failed.\n\t\t\treturn Ok(ConformState::Unavailable);\n\t\t}\n\n\t\tOk(ConformState::Generating)\n\t}\n\n\t/// Number of conform (pcm) files for the given stream/params — one per\n\t/// channel; 0 on invalid arguments.\n\tpub fn get_conform_filename_count(\n\t\t\u0026self,\n\t\t_cache_path: \u0026str,\n\t\t_source_filename: \u0026str,\n\t\t_stream_index: i32,\n\t\t_sample_rate: i32,\n\t\tchannel_layout: u64,\n\t\t_sample_format: i32,\n\t) -\u003e usize {\n\t\tchannel_layout.count_ones() as usize\n\t}\n\n\t/// The `index`-th conform filename.\n\tpub fn get_conform_filename(\n\t\t\u0026self,\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tindex: usize,\n\t) -\u003e crate::error::Result\u003cString\u003e {\n\t\tlet filenames = conform_filenames(\n\t\t\tcache_path,\n\t\t\tsource_filename,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tsample_format,\n\t\t\tchannel_layout,\n\t\t);\n\t\tfilenames\n\t\t\t.get(index)\n\t\t\t.cloned()\n\t\t\t.ok_or(crate::error::Error::NotFound)\n\t}\n}\n\n/// Deterministic conform base name plus per-channel pcm filenames, mirroring\n/// `ConformManager::get_conformed_filename`: one file per channel under\n/// `cache_path`, named `\u003cidentifier\u003e-\u003cstream\u003e.\u003crate\u003e.\u003cformat\u003e.\u003clayout\u003e.\u003ci\u003e.pcm`.\nfn conform_filenames(\n\tcache_path: \u0026str,\n\tsource_filename: \u0026str,\n\tstream_index: i32,\n\tsample_rate: i32,\n\tsample_format: i32,\n\tchannel_layout: u64,\n) -\u003e Vec\u003cString\u003e {\n\tlet count = channel_layout.count_ones() as usize;\n\tlet base = format!(\n\t\t\"{}-{}.{}.{}.{}\",\n\t\tunique_file_identifier(source_filename),\n\t\tstream_index,\n\t\tsample_rate,\n\t\tsample_format,\n\t\tchannel_layout,\n\t);\n\n\tlet mut out = Vec::with_capacity(count);\n\tfor i in 0..count {\n\t\tlet p = Path::new(cache_path).join(format!(\"{}.{}.pcm\", base, i));\n\t\tout.push(p.to_string_lossy().into_owned());\n\t}\n\tout\n}\n\n/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge\n/// returns a 64-bit id directly).\nfn unique_file_identifier(filename: \u0026str) -\u003e String {\n\tlet c = match CString::new(filename) {\n\t\tOk(c) =\u003e c,\n\t\tErr(_) =\u003e return String::new(),\n\t};\n\t// # Safety: `c` is a valid NUL-terminated C string alive for the call.\n\tlet id = unsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())\n\t};\n\tformat!(\"{}\", id)\n}\n\n/// True when every conform filename already exists on disk.\nfn all_conforms_exist(filenames: \u0026[String]) -\u003e bool {\n\tfilenames.iter().all(|f| Path::new(f).exists())\n}\n\n/// Shared test support: serializes access to the global task-submit registry\n/// (unit tests run in parallel and would otherwise clear each other's\n/// registration) and provides a callback that accepts any task.\n#[cfg(test)]\npub(crate) mod test_util {\n\tuse crate::error::OAKCODEC_OK;\n\tuse crate::task::OakCodecTaskRequest;\n\n\t/// Serializes every test that mutates the task-submit registry.\n\tpub static REG_LOCK: std::sync::Mutex\u003c()\u003e = std::sync::Mutex::new(());\n\n\t/// A task-submit callback that accepts every request (no-op).\n\tpub unsafe extern \"C\" fn accept_cb(\n\t\t_req: *const OakCodecTaskRequest,\n\t\t_ud: *mut std::ffi::c_void,\n\t) -\u003e i32 {\n\t\tOAKCODEC_OK\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn temp_subdir(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_conform_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\tfn fnv1a64(bytes: \u0026[u8]) -\u003e u64 {\n\t\tlet mut h: u64 = 14695981039346656037;\n\t\tfor \u0026b in bytes {\n\t\t\th ^= b as u64;\n\t\t\th = h.wrapping_mul(1099511628211);\n\t\t}\n\t\th\n\t}\n\n\t#[test]\n\tfn unique_identifier_matches_bridge_hash() {\n\t\t// The test stub computes an FNV-1a-64 of the path bytes.\n\t\tlet expected = format!(\"{}\", fnv1a64(b\"media.mp4\") as i64);\n\t\tassert_eq!(unique_file_identifier(\"media.mp4\"), expected);\n\t\t// Deterministic: same input, same id.\n\t\tassert_eq!(\n\t\t\tunique_file_identifier(\"media.mp4\"),\n\t\t\tunique_file_identifier(\"media.mp4\")\n\t\t);\n\t\t// Different input, different id.\n\t\tassert_ne!(\n\t\t\tunique_file_identifier(\"media.mp4\"),\n\t\t\tunique_file_identifier(\"other.mp4\")\n\t\t);\n\t}\n\n\t#[test]\n\tfn filename_count_from_channel_layout() {\n\t\tlet m = ConformManager::instance();\n\t\tassert_eq!(m.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0x3, 0), 2); // stereo\n\t\tassert_eq!(m.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0x4, 0), 1); // mono\n\t\tassert_eq!(m.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0, 0), 0); // invalid\n\t\tassert_eq!(\n\t\t\tm.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0x60F, 0),\n\t\t\t6\n\t\t); // 5.1\n\t}\n\n\t#[test]\n\tfn conform_filename_derivation_and_range() {\n\t\tlet m = ConformManager::instance();\n\t\tlet cache = temp_subdir(\"names\");\n\t\tlet id = fnv1a64(b\"media.mp4\") as i64;\n\t\tlet base = format!(\"{}-0.48000.0.3\", id);\n\t\tlet f0 = m\n\t\t\t.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, 0)\n\t\t\t.unwrap();\n\t\tlet f1 = m\n\t\t\t.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, 1)\n\t\t\t.unwrap();\n\t\tassert_eq!(f0, format!(\"{}/{}.0.pcm\", cache, base));\n\t\tassert_eq!(f1, format!(\"{}/{}.1.pcm\", cache, base));\n\t\t// Out of range.\n\t\tassert!(matches!(\n\t\t\tm.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, 5),\n\t\t\tErr(crate::error::Error::NotFound)\n\t\t));\n\t}\n\n\t#[test]\n\tfn get_conform_state_unavailable_without_registrar() {\n\t\tlet _g = super::test_util::REG_LOCK.lock().unwrap();\n\t\t// Ensure no registrar is left over.\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tlet cache = temp_subdir(\"unavail\");\n\t\tlet s = ConformManager::instance()\n\t\t\t.get_conform_state(\u0026cache, \"missing.mp4\", 0, 48000, 0x3, 0, false)\n\t\t\t.unwrap();\n\t\tassert_eq!(s, ConformState::Unavailable);\n\t}\n\n\t#[test]\n\tfn get_conform_state_exists_when_files_present() {\n\t\tlet cache = temp_subdir(\"exists\");\n\t\tlet m = ConformManager::instance();\n\t\tfor i in 0..2 {\n\t\t\tlet f = m\n\t\t\t\t.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, i)\n\t\t\t\t.unwrap();\n\t\t\tstd::fs::write(\u0026f, b\"pcm\").unwrap();\n\t\t}\n\t\tlet s = m\n\t\t\t.get_conform_state(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, false)\n\t\t\t.unwrap();\n\t\tassert_eq!(s, ConformState::Exists);\n\t}\n\n\t#[test]\n\tfn get_conform_state_generating_when_registered() {\n\t\tlet _g = super::test_util::REG_LOCK.lock().unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(\n\t\t\tSome(super::test_util::accept_cb),\n\t\t\tstd::ptr::null_mut(),\n\t\t);\n\t\tlet cache = temp_subdir(\"generating\");\n\t\tlet s = ConformManager::instance()\n\t\t\t.get_conform_state(\u0026cache, \"missing.mp4\", 0, 48000, 0x3, 0, false)\n\t\t\t.unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tassert_eq!(s, ConformState::Generating);\n\t}\n}\n","traces":[{"line":45,"address":[],"length":0,"stats":{"Line":12}},{"line":47,"address":[],"length":0,"stats":{"Line":12}},{"line":55,"address":[],"length":0,"stats":{"Line":5}},{"line":66,"address":[],"length":0,"stats":{"Line":5}},{"line":67,"address":[],"length":0,"stats":{"Line":5}},{"line":68,"address":[],"length":0,"stats":{"Line":5}},{"line":69,"address":[],"length":0,"stats":{"Line":5}},{"line":70,"address":[],"length":0,"stats":{"Line":5}},{"line":71,"address":[],"length":0,"stats":{"Line":5}},{"line":75,"address":[],"length":0,"stats":{"Line":10}},{"line":76,"address":[],"length":0,"stats":{"Line":2}},{"line":80,"address":[],"length":0,"stats":{"Line":3}},{"line":81,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":4}},{"line":101,"address":[],"length":0,"stats":{"Line":2}},{"line":102,"address":[],"length":0,"stats":{"Line":0}},{"line":105,"address":[],"length":0,"stats":{"Line":2}},{"line":106,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":5}},{"line":129,"address":[],"length":0,"stats":{"Line":5}},{"line":133,"address":[],"length":0,"stats":{"Line":9}},{"line":144,"address":[],"length":0,"stats":{"Line":9}},{"line":145,"address":[],"length":0,"stats":{"Line":9}},{"line":146,"address":[],"length":0,"stats":{"Line":9}},{"line":147,"address":[],"length":0,"stats":{"Line":9}},{"line":148,"address":[],"length":0,"stats":{"Line":9}},{"line":149,"address":[],"length":0,"stats":{"Line":9}},{"line":151,"address":[],"length":0,"stats":{"Line":18}},{"line":152,"address":[],"length":0,"stats":{"Line":9}},{"line":154,"address":[],"length":0,"stats":{"Line":18}},{"line":161,"address":[],"length":0,"stats":{"Line":14}},{"line":169,"address":[],"length":0,"stats":{"Line":28}},{"line":170,"address":[],"length":0,"stats":{"Line":28}},{"line":172,"address":[],"length":0,"stats":{"Line":28}},{"line":179,"address":[],"length":0,"stats":{"Line":42}},{"line":180,"address":[],"length":0,"stats":{"Line":70}},{"line":181,"address":[],"length":0,"stats":{"Line":140}},{"line":182,"address":[],"length":0,"stats":{"Line":84}},{"line":184,"address":[],"length":0,"stats":{"Line":14}},{"line":189,"address":[],"length":0,"stats":{"Line":19}},{"line":190,"address":[],"length":0,"stats":{"Line":38}},{"line":191,"address":[],"length":0,"stats":{"Line":38}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":196,"address":[],"length":0,"stats":{"Line":38}},{"line":198,"address":[],"length":0,"stats":{"Line":19}},{"line":202,"address":[],"length":0,"stats":{"Line":6}},{"line":203,"address":[],"length":0,"stats":{"Line":28}}],"covered":46,"coverable":50},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","decoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::Decoder` and its supporting types — the media-decoder trait.\n//!\n//! Mirrors `src/codec/src/decoder.h`. The C++ abstract base plus its\n//! FFmpeg/OIIO subclasses become the [`Decoder`] trait (decision 2 in\n//! README.md); probe/dispatch lives on the registry functions at the\n//! bottom of this module. Audio is handled in raw interleaved-float\n//! buffers matching the C ABI, not `oakcore_rs::SampleBuffer` (which the\n//! crate does not export).\n\nuse std::path::Path;\nuse std::sync::{Arc, Mutex, OnceLock};\n\nuse oakcore_rs::{Rational, TimeRange};\n\nuse crate::bridge::render::{OakCancelAtom, OakRenderTexture};\nuse crate::footagedescription::FootageDescription;\nuse crate::frame::Frame;\n\n/// `oakcodec_video_stream_info` — POD probe output describing one video\n/// stream; see `include/codec/decoder.h`.\n#[repr(C)]\npub struct OakCodecVideoStreamInfo {\n\t/// Stream index.\n\tpub stream_index: i32,\n\t/// Width in pixels.\n\tpub width: i32,\n\t/// Height in pixels.\n\tpub height: i32,\n\t/// Frame-rate numerator.\n\tpub frame_rate_num: i32,\n\t/// Frame-rate denominator.\n\tpub frame_rate_den: i32,\n\t/// Stream length in time-base units.\n\tpub duration_ts: i64,\n\t/// Time-base numerator (seconds per time-base unit).\n\tpub time_base_num: i32,\n\t/// Time-base denominator.\n\tpub time_base_den: i32,\n\t/// Native delivery `OakPixelFormat`.\n\tpub format: i32,\n\t/// Plane channel count.\n\tpub channel_count: i32,\n\t/// ISO/IEC 23001-8 color-primaries code point (0 = unknown).\n\tpub color_primaries: i32,\n\t/// ISO/IEC 23001-8 color-transfer code point (0 = unknown).\n\tpub color_trc: i32,\n\t/// 1 when the stream is interlaced.\n\tpub interlaced: i32,\n}\n\n/// `oakcodec_audio_stream_info` — POD probe output describing one audio\n/// stream; see `include/codec/decoder.h`.\n#[repr(C)]\npub struct OakCodecAudioStreamInfo {\n\t/// Stream index.\n\tpub stream_index: i32,\n\t/// Sample rate (Hz).\n\tpub sample_rate: i32,\n\t/// ffmpeg-style channel mask (e.g. 0x3 = stereo).\n\tpub channel_layout: u64,\n\t/// Channel count.\n\tpub channel_count: i32,\n\t/// Stream length in time-base units.\n\tpub duration_ts: i64,\n\t/// Time-base numerator.\n\tpub time_base_num: i32,\n\t/// Time-base denominator.\n\tpub time_base_den: i32,\n}\n\n/// Local replacement for `render/rendermodes.h` (oakrender C API has no\n/// render-mode counterpart). Values mirror engine/render/rendermodes.h:\n/// k_offline = 0, k_online = 1.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum RenderMode {\n\t/// Offline / background render.\n\tOffline = 0,\n\t/// Online / real-time render.\n\tOnline = 1,\n}\n\n/// \"Don't force a color range\" sentinel for\n/// [`RetrieveVideoParams::force_range`] (the actual ranges are the\n/// `OAKCOMMON_COLOR_RANGE_*` values).\npub const K_COLOR_RANGE_DEFAULT: i32 = -1;\n\n/// `Decoder::RetrieveVideoParams` — what a video retrieve call needs.\npub struct RetrieveVideoParams {\n\t/// Stream to read from.\n\tpub stream: CodecStream,\n\t/// Timestamp, rational seconds.\n\tpub time: Rational,\n\t/// Length of footage before the start (for early-seek semantics).\n\tpub length: TimeRange,\n\t/// Color range override; [`K_COLOR_RANGE_DEFAULT`] means \"don't force\".\n\tpub force_range: i32,\n\t/// Image sequence: bake the frame number into the filename.\n\tpub is_image_sequence: bool,\n\t/// Image sequence digit count (derived from the filename).\n\tpub image_sequence_digits: i32,\n\t/// Image sequence number to substitute.\n\tpub image_sequence_number: i64,\n\t/// Render mode (drives texture-path choices in the implementations).\n\tpub mode: RenderMode,\n\t/// Frame alpha channel is premultiplied.\n\tpub alpha_is_premultiplied: bool,\n}\n\n/// `Decoder::RetrieveAudioStatus` — outcome of an audio retrieve.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum RetrieveAudioStatus {\n\t/// Data written to the destination buffer.\n\tSuccess,\n\t/// The requested range is outside the footage.\n\tInvalidRange,\n\t/// The stream does not support audio.\n\tUnsupported,\n\t/// Media requires a conform that could not be produced.\n\tConformNeeded,\n\t/// A decoder-level error occurred.\n\tError,\n}\n\n/// `Decoder::RetrieveState`.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum RetrieveState {\n\t/// Ready to decode.\n\tReady,\n\t/// Failed to open the stream.\n\tFailedToOpen,\n\t/// The stream index could not be located.\n\tIndexUnavailable,\n}\n\n/// `Decoder::CodecStream` — identifies one (filename, stream) pair plus an\n/// optional associated timeline block.\n///\n/// The block is an opaque `OakNodeBlock` handle that codec only stores and\n/// compares, never dereferences or retains (borrowed pointer).\n#[derive(Clone, Debug, PartialEq, Eq)]\npub struct CodecStream {\n\tfilename: String,\n\tstream: i32,\n\tblock: Option\u003ccrate::bridge::common::OakNodeBlock\u003e,\n}\n\nimpl CodecStream {\n\t/// Empty, invalid stream.\n\tpub fn new() -\u003e Self {\n\t\tCodecStream {\n\t\t\tfilename: String::new(),\n\t\t\tstream: -1,\n\t\t\tblock: None,\n\t\t}\n\t}\n\n\t/// New stream for `(filename, stream)` with an optional block.\n\tpub fn with_block(\n\t\tfilename: String,\n\t\tstream: i32,\n\t\tblock: Option\u003ccrate::bridge::common::OakNodeBlock\u003e,\n\t) -\u003e Self {\n\t\tCodecStream {\n\t\t\tfilename,\n\t\t\tstream,\n\t\t\tblock,\n\t\t}\n\t}\n\n\t/// Non-empty filename and non-negative stream index.\n\tpub fn is_valid(\u0026self) -\u003e bool {\n\t\t!self.filename.is_empty() \u0026\u0026 self.stream \u003e= 0\n\t}\n\n\t/// The file exists on disk.\n\tpub fn exists(\u0026self) -\u003e bool {\n\t\tPath::new(\u0026self.filename).exists()\n\t}\n\n\t/// Reset to the empty stream.\n\tpub fn reset(\u0026mut self) {\n\t\tself.filename.clear();\n\t\tself.stream = -1;\n\t\tself.block = None;\n\t}\n\n\t/// Source filename.\n\tpub fn filename(\u0026self) -\u003e \u0026str {\n\t\t\u0026self.filename\n\t}\n\n\t/// Stream index within the source.\n\tpub fn stream(\u0026self) -\u003e i32 {\n\t\tself.stream\n\t}\n\n\t/// Associated timeline block (borrowed; only compared, never used).\n\tpub fn block(\u0026self) -\u003e Option\u003ccrate::bridge::common::OakNodeBlock\u003e {\n\t\tself.block.clone()\n\t}\n}\n\n/// `olive::Decoder` — abstraction over external media decoding.\n///\n/// Implementations are [`crate::ffmpeg::FFmpegDecoder`] and\n/// [`crate::oiio::OIIODecoder`]. The trait surface mirrors the C++\n/// abstract base; the refcounted handle that backs the public API wraps an\n/// `Arc\u003cdyn Decoder\u003e`.\npub trait Decoder: Send + Sync {\n\t/// Unique decoder id (\"ffmpeg\"/\"oiio\").\n\tfn id(\u0026self) -\u003e String;\n\n\t/// Whether this decoder supports video streams.\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this decoder supports audio streams.\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this decoder can read the given file (static probe).\n\tfn probe(\n\t\t\u0026self,\n\t\tfilename: \u0026str,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e Option\u003cFootageDescription\u003e;\n\n\t/// Open `stream` for decoding. Thread-safe.\n\tfn open(\u0026self, stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Close the currently open stream (safe when closed).\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// The currently open stream (locked accessor).\n\tfn stream(\u0026self) -\u003e CodecStream;\n\n\t/// Retrieve a video frame into CPU memory.\n\tfn retrieve_video_frame(\n\t\t\u0026self,\n\t\tp: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e;\n\n\t/// Retrieve a video frame as a render texture (owned by caller).\n\tfn retrieve_video(\u0026self, p: \u0026RetrieveVideoParams) -\u003e crate::error::Result\u003cOakRenderTexture\u003e;\n\n\t/// Retrieve interleaved audio covering `range` into `dest` (floats).\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\tdest: \u0026mut [f32],\n\t\trange: \u0026TimeRange,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e;\n\n\t/// Conform the open stream's audio into per-channel pcm files.\n\t///\n\t/// `sample_rate` / `channel_layout` / `sample_format` describe the\n\t/// target audio format (`sample_format` is a\n\t/// `olive::core::SampleFormat::Format` value). The C++ side builds its\n\t/// `core::AudioParams` from these three — mirroring the C ABI\n\t/// `oakcodec_decoder_conform_audio` argument list.\n\tfn conform_audio(\n\t\t\u0026self,\n\t\toutput_filenames: \u0026[String],\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Offset of the audio start relative to the video (rational seconds).\n\tfn get_audio_start_offset(\u0026self) -\u003e Rational {\n\t\t// C++ default `virtual Rational get_audio_start_offset() const { return 0; }`\n\t\tRational::new(0, 1)\n\t}\n}\n\n/// Placeholder decoder used by the built-in probe registry.\n///\n/// Reports the correct id and capability flags so id-based dispatch\n/// (`create_from_id`) works, but every media operation is unimplemented\n/// and returns `None` / an error. Used for the OIIO entry, whose Rust\n/// implementation (`crate::oiio::OIIODecoder`) is still a dylib stub; the\n/// FFmpeg entry is the real [`crate::ffmpeg::FFmpegDecoder`].\nstruct UnimplementedDecoder {\n\tid: \u0026'static str,\n\tvideo: bool,\n\taudio: bool,\n}\n\nimpl UnimplementedDecoder {\n\tfn new(id: \u0026'static str, video: bool, audio: bool) -\u003e Self {\n\t\tUnimplementedDecoder { id, video, audio }\n\t}\n}\n\nimpl Decoder for UnimplementedDecoder {\n\tfn id(\u0026self) -\u003e String {\n\t\tself.id.to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tself.video\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tself.audio\n\t}\n\n\tfn probe(\n\t\t\u0026self,\n\t\t_filename: \u0026str,\n\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e Option\u003cFootageDescription\u003e {\n\t\tNone\n\t}\n\n\tfn open(\u0026self, _stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn stream(\u0026self) -\u003e CodecStream {\n\t\tCodecStream::new()\n\t}\n\n\tfn retrieve_video_frame(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn retrieve_video(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cOakRenderTexture\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\t_dest: \u0026mut [f32],\n\t\t_range: \u0026TimeRange,\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn conform_audio(\n\t\t\u0026self,\n\t\t_output_filenames: \u0026[String],\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t\t_sample_format: i32,\n\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n}\n\n/// `Decoder::create_from_id` — instantiate a decoder by id, or `None`.\npub fn create_from_id(id: \u0026str) -\u003e Option\u003cArc\u003cdyn Decoder\u003e\u003e {\n\tif id.is_empty() {\n\t\treturn None;\n\t}\n\n\treceive_list_of_all_decoders()\n\t\t.into_iter()\n\t\t.find(|d| d.id() == id)\n}\n\n/// Test-injected decoder registry (see [`set_test_decoders`]); empty when\n/// not injected, in which case the built-in list below is used.\nstatic TEST_DECODERS: OnceLock\u003cMutex\u003cVec\u003cArc\u003cdyn Decoder\u003e\u003e\u003e\u003e = OnceLock::new();\n\n/// Serializes every test that reads the built-in decoder registry. The ffi\n/// decoder tests inject through `crate::ffi::lock_tests()` (the shared\n/// `TEST_LOCK`), so the registry assertions below take that same lock to\n/// never race with an injected list.\n#[cfg(test)]\nfn registry_guard() -\u003e std::sync::MutexGuard\u003c'static, ()\u003e {\n\tcrate::ffi::lock_tests()\n}\n\n/// Replace the decoder registry with `list`; pass an empty list to restore\n/// the built-in decoders.\n///\n/// Test/extension support (the C ABI has no way to register a decoder, so\n/// the contract tests drive the probe/dispatch paths through a fake\n/// decoder). Hidden from docs; never called by production code.\n#[doc(hidden)]\npub fn set_test_decoders(list: Vec\u003cArc\u003cdyn Decoder\u003e\u003e) {\n\tlet store = TEST_DECODERS.get_or_init(|| Mutex::new(Vec::new()));\n\t*store.lock().unwrap() = list;\n}\n\n/// `Decoder::receive_list_of_all_decoders` — all registered decoders.\n///\n/// Order is probe priority, mirroring C++: OIIO (more specific) before\n/// FFmpeg (format-agnostic fallback). The OIIO entry is an\n/// [`UnimplementedDecoder`] stub (the OIIO engine is not ported); the\n/// FFmpeg entry is the real [`crate::ffmpeg::FFmpegDecoder`]. When tests\n/// injected a non-empty list via [`set_test_decoders`], that list takes\n/// precedence.\npub fn receive_list_of_all_decoders() -\u003e Vec\u003cArc\u003cdyn Decoder\u003e\u003e {\n\tif let Some(store) = TEST_DECODERS.get() {\n\t\tlet injected = store.lock().unwrap();\n\t\tif !injected.is_empty() {\n\t\t\treturn injected.clone();\n\t\t}\n\t}\n\tvec![\n\t\tArc::new(UnimplementedDecoder::new(\"oiio\", false, false)),\n\t\tArc::new(crate::ffmpeg::FFmpegDecoder::new()),\n\t]\n}\n\n/// Image-sequence filename heuristics (static).\n///\n/// Replace the trailing digit run of the filename stem with the\n/// zero-padded decimal representation of `number` (keeps the same digit\n/// count), mirroring `Decoder::transform_image_sequence_file_name`.\npub fn transform_image_sequence_file_name(filename: \u0026str, number: i64) -\u003e String {\n\tlet digit_count = get_image_sequence_digit_count(filename) as usize;\n\n\tlet path = Path::new(filename);\n\tlet file_name = path\n\t\t.file_name()\n\t\t.and_then(|n| n.to_str())\n\t\t.unwrap_or(filename);\n\n\t// QFileInfo::completeBaseName(): filename up to the first '.'.\n\tlet original_basename = match file_name.find('.') {\n\t\tSome(dot) =\u003e \u0026file_name[..dot],\n\t\tNone =\u003e file_name,\n\t};\n\n\t// New stem = original stem minus the trailing digit run, plus the\n\t// zero-padded number (`snprintf(\"%0*lld\", digit_count, number)`).\n\tlet cut = original_basename.len().saturating_sub(digit_count);\n\tlet new_basename = format!(\n\t\t\"{}{:0width$}\",\n\t\t\u0026original_basename[..cut],\n\t\tnumber,\n\t\twidth = digit_count\n\t);\n\n\t// Replace every occurrence of the original stem in the filename.\n\tlet mut new_filename = file_name.to_string();\n\tlet mut pos = 0;\n\twhile let Some(rel) = new_filename[pos..].find(original_basename) {\n\t\tlet start = pos + rel;\n\t\tlet end = start + original_basename.len();\n\t\tnew_filename.replace_range(start..end, \u0026new_basename);\n\t\tpos = start + new_basename.len();\n\t}\n\n\tmatch path.parent() {\n\t\tSome(parent) if !parent.as_os_str().is_empty() =\u003e {\n\t\t\tPath::new(parent).join(\u0026new_filename).to_string_lossy().into_owned()\n\t\t}\n\t\t_ =\u003e new_filename,\n\t}\n}\n\n/// Number of trailing digits in the filename stem (0 = not a sequence).\npub fn get_image_sequence_digit_count(filename: \u0026str) -\u003e i32 {\n\tlet file_name = Path::new(filename)\n\t\t.file_name()\n\t\t.and_then(|n| n.to_str())\n\t\t.unwrap_or(filename);\n\n\t// QFileInfo::completeBaseName(): filename up to the first '.'.\n\tlet stem = match file_name.find('.') {\n\t\tSome(dot) =\u003e \u0026file_name[..dot],\n\t\tNone =\u003e file_name,\n\t};\n\n\tlet mut count: i32 = 0;\n\tfor ch in stem.chars().rev() {\n\t\tif ch.is_ascii_digit() {\n\t\t\tcount += 1;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tcount\n}\n\n/// Numeric value of the trailing digits (0 when there are none).\n///\n/// Mirrors C++ `Decoder::get_image_sequence_index`, which slices the\n/// trailing digit run (`basename.substr(basename.size() - digit_count)`) and\n/// passes it to `strtoll`. Because that slice is empty when there are no\n/// trailing digits (digit_count == 0) and all-digits otherwise, the value is\n/// the parsed number, or `0` for a non-sequence.\npub fn get_image_sequence_index(filename: \u0026str) -\u003e i64 {\n\tlet digit_count = get_image_sequence_digit_count(filename) as usize;\n\n\tlet file_name = Path::new(filename)\n\t\t.file_name()\n\t\t.and_then(|n| n.to_str())\n\t\t.unwrap_or(filename);\n\n\t// QFileInfo::completeBaseName(): filename up to the first '.'.\n\tlet stem = match file_name.find('.') {\n\t\tSome(dot) =\u003e \u0026file_name[..dot],\n\t\tNone =\u003e file_name,\n\t};\n\n\t// Trailing digit run (empty when the stem has no trailing digits).\n\tlet start = stem.len().saturating_sub(digit_count);\n\tlet number_only = \u0026stem[start..];\n\n\t// `strtoll(..., base 10)`: the slice is empty-or-digits, so a plain\n\t// decimal parse with 0 on failure reproduces the C++ result.\n\tnumber_only.parse::\u003ci64\u003e().unwrap_or(0)\n}\n\n/// The `k_any_timecode` rational constant.\n///\n/// C++ `const Rational Decoder::k_any_timecode = RATIONAL_MIN;`, which the\n/// i32 reduction cap normalizes to `-2147483647/1`.\npub fn k_any_timecode() -\u003e Rational {\n\tRational::new(-2147483647, 1)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn codec_stream_new_is_invalid() {\n\t\tlet s = CodecStream::new();\n\t\tassert!(!s.is_valid());\n\t\tassert!(s.filename().is_empty());\n\t\tassert_eq!(s.stream(), -1);\n\t\tassert_eq!(s.block(), None);\n\t}\n\n\t#[test]\n\tfn codec_stream_with_block_is_valid() {\n\t\tlet s = CodecStream::with_block(\"video.mov\".to_string(), 1, None);\n\t\tassert!(s.is_valid());\n\t\tassert_eq!(s.filename(), \"video.mov\");\n\t\tassert_eq!(s.stream(), 1);\n\n\t\t// Negative stream index is invalid regardless of filename.\n\t\tlet bad = CodecStream::with_block(\"video.mov\".to_string(), -1, None);\n\t\tassert!(!bad.is_valid());\n\t}\n\n\t#[test]\n\tfn codec_stream_reset_clears() {\n\t\tlet mut s = CodecStream::with_block(\"video.mov\".to_string(), 2, None);\n\t\ts.reset();\n\t\tassert!(!s.is_valid());\n\t\tassert!(s.filename().is_empty());\n\t\tassert_eq!(s.stream(), -1);\n\t}\n\n\t#[test]\n\tfn digit_count_counts_trailing_digits() {\n\t\tassert_eq!(get_image_sequence_digit_count(\"frame_0001.png\"), 4);\n\t\tassert_eq!(get_image_sequence_digit_count(\"frame.png\"), 0);\n\t\tassert_eq!(get_image_sequence_digit_count(\"img000.jpg\"), 3);\n\t\t// Digits before the final char are not trailing digits.\n\t\tassert_eq!(get_image_sequence_digit_count(\"a1b.png\"), 0);\n\t}\n\n\t#[test]\n\tfn image_sequence_index_parses_number() {\n\t\tassert_eq!(get_image_sequence_index(\"frame_0001.png\"), 1);\n\t\tassert_eq!(get_image_sequence_index(\"img012.jpg\"), 12);\n\t\tassert_eq!(get_image_sequence_index(\"0009.png\"), 9);\n\t\t// No trailing digits: the sliced run is empty, so the value is 0.\n\t\tassert_eq!(get_image_sequence_index(\"frame.png\"), 0);\n\t\tassert_eq!(get_image_sequence_index(\"12abc.png\"), 0);\n\t}\n\n\t#[test]\n\tfn transform_image_sequence_substitutes_number() {\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"frame_0001.png\", 5),\n\t\t\t\"frame_0005.png\"\n\t\t);\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"dir/img012.jpg\", 7),\n\t\t\t\"dir/img007.jpg\"\n\t\t);\n\t\t// No digit run: number appended with no padding (C++ behavior).\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"frame.png\", 3),\n\t\t\t\"frame3.png\"\n\t\t);\n\t\t// All-digit stem: whole run is replaced.\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"0001.png\", 7),\n\t\t\t\"0007.png\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn k_any_timecode_is_rational_min() {\n\t\tlet tc = k_any_timecode();\n\t\tassert_eq!(tc.numerator(), -2147483647);\n\t\tassert_eq!(tc.denominator(), 1);\n\t}\n\n\t#[test]\n\tfn registry_lists_oiio_then_ffmpeg() {\n\t\tlet _g = registry_guard();\n\t\tlet list = receive_list_of_all_decoders();\n\t\tlet ids: Vec\u003cString\u003e = list.iter().map(|d| d.id()).collect();\n\t\t// Probe priority: OIIO (specific) first, FFmpeg (fallback) last.\n\t\tassert_eq!(ids, vec![\"oiio\".to_string(), \"ffmpeg\".to_string()]);\n\t}\n\n\t#[test]\n\tfn create_from_id_matches_registry() {\n\t\tlet _g = registry_guard();\n\t\tassert!(create_from_id(\"ffmpeg\").is_some());\n\t\tassert!(create_from_id(\"oiio\").is_some());\n\t\tassert_eq!(create_from_id(\"ffmpeg\").unwrap().id(), \"ffmpeg\");\n\t\tassert_eq!(create_from_id(\"oiio\").unwrap().id(), \"oiio\");\n\t\t// Unknown and empty ids return None.\n\t\tassert!(create_from_id(\"nope\").is_none());\n\t\tassert!(create_from_id(\"\").is_none());\n\t}\n}\n\n#[cfg(test)]\nmod tests_unimplemented {\n\tuse super::*;\n\n\tfn builtin(id: \u0026str) -\u003e Arc\u003cdyn Decoder\u003e {\n\t\tcreate_from_id(id).unwrap()\n\t}\n\n\t#[test]\n\tfn ffmpeg_builtin_fails_on_missing_media_and_closes() {\n\t\tlet _g = registry_guard();\n\t\tlet d = builtin(\"ffmpeg\");\n\t\tassert!(d.supports_video());\n\t\tassert!(d.supports_audio());\n\t\t// A nonexistent file cannot be probed or opened.\n\t\tassert!(d.probe(\"x.mp4\", None).is_none());\n\n\t\tlet s = CodecStream::with_block(\"x.mp4\".to_string(), 0, None);\n\t\tassert!(d.open(\u0026s).is_err());\n\t\t// C++ parity: a failed open leaves the decoder closed.\n\t\tassert_eq!(d.stream().filename(), \"\");\n\t\tassert!(d.close().is_ok());\n\n\t\tlet p = RetrieveVideoParams {\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(0, 1),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t};\n\t\tassert!(d.retrieve_video_frame(\u0026p).is_err());\n\t\tassert!(d.retrieve_video(\u0026p).is_err());\n\t\tlet mut dest = [0f32; 4];\n\t\tassert!(d\n\t\t\t.retrieve_audio(\n\t\t\t\t\u0026mut dest,\n\t\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t\t48000,\n\t\t\t\t0x3\n\t\t\t)\n\t\t\t.is_err());\n\t\tassert!(d\n\t\t\t.conform_audio(\u0026[\"a.pcm\".to_string()], 48000, 0x3, 10, None)\n\t\t\t.is_err());\n\n\t\t// OIIO reports no media capabilities.\n\t\tlet o = builtin(\"oiio\");\n\t\tassert!(!o.supports_video());\n\t\tassert!(!o.supports_audio());\n\t}\n\n\t#[test]\n\tfn get_audio_start_offset_defaults_to_zero() {\n\t\tlet _g = registry_guard();\n\t\tlet d = builtin(\"ffmpeg\");\n\t\tlet off = d.get_audio_start_offset();\n\t\tassert_eq!(off.numerator(), 0);\n\t\tassert_eq!(off.denominator(), 1);\n\t}\n}\n","traces":[{"line":166,"address":[],"length":0,"stats":{"Line":11}},{"line":168,"address":[],"length":0,"stats":{"Line":11}},{"line":169,"address":[],"length":0,"stats":{"Line":11}},{"line":175,"address":[],"length":0,"stats":{"Line":16}},{"line":188,"address":[],"length":0,"stats":{"Line":11}},{"line":189,"address":[],"length":0,"stats":{"Line":20}},{"line":193,"address":[],"length":0,"stats":{"Line":7}},{"line":194,"address":[],"length":0,"stats":{"Line":14}},{"line":198,"address":[],"length":0,"stats":{"Line":1}},{"line":199,"address":[],"length":0,"stats":{"Line":2}},{"line":200,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":205,"address":[],"length":0,"stats":{"Line":12}},{"line":206,"address":[],"length":0,"stats":{"Line":12}},{"line":210,"address":[],"length":0,"stats":{"Line":8}},{"line":211,"address":[],"length":0,"stats":{"Line":8}},{"line":215,"address":[],"length":0,"stats":{"Line":1}},{"line":216,"address":[],"length":0,"stats":{"Line":2}},{"line":231,"address":[],"length":0,"stats":{"Line":0}},{"line":232,"address":[],"length":0,"stats":{"Line":0}},{"line":236,"address":[],"length":0,"stats":{"Line":0}},{"line":237,"address":[],"length":0,"stats":{"Line":0}},{"line":291,"address":[],"length":0,"stats":{"Line":0}},{"line":293,"address":[],"length":0,"stats":{"Line":0}},{"line":311,"address":[],"length":0,"stats":{"Line":9}},{"line":317,"address":[],"length":0,"stats":{"Line":10}},{"line":318,"address":[],"length":0,"stats":{"Line":20}},{"line":321,"address":[],"length":0,"stats":{"Line":1}},{"line":322,"address":[],"length":0,"stats":{"Line":1}},{"line":325,"address":[],"length":0,"stats":{"Line":1}},{"line":326,"address":[],"length":0,"stats":{"Line":1}},{"line":329,"address":[],"length":0,"stats":{"Line":0}},{"line":334,"address":[],"length":0,"stats":{"Line":0}},{"line":337,"address":[],"length":0,"stats":{"Line":0}},{"line":338,"address":[],"length":0,"stats":{"Line":0}},{"line":341,"address":[],"length":0,"stats":{"Line":0}},{"line":342,"address":[],"length":0,"stats":{"Line":0}},{"line":345,"address":[],"length":0,"stats":{"Line":0}},{"line":346,"address":[],"length":0,"stats":{"Line":0}},{"line":349,"address":[],"length":0,"stats":{"Line":0}},{"line":353,"address":[],"length":0,"stats":{"Line":0}},{"line":356,"address":[],"length":0,"stats":{"Line":0}},{"line":360,"address":[],"length":0,"stats":{"Line":0}},{"line":363,"address":[],"length":0,"stats":{"Line":0}},{"line":370,"address":[],"length":0,"stats":{"Line":0}},{"line":373,"address":[],"length":0,"stats":{"Line":0}},{"line":381,"address":[],"length":0,"stats":{"Line":0}},{"line":386,"address":[],"length":0,"stats":{"Line":14}},{"line":387,"address":[],"length":0,"stats":{"Line":28}},{"line":388,"address":[],"length":0,"stats":{"Line":1}},{"line":391,"address":[],"length":0,"stats":{"Line":13}},{"line":393,"address":[],"length":0,"stats":{"Line":49}},{"line":405,"address":[],"length":0,"stats":{"Line":4}},{"line":406,"address":[],"length":0,"stats":{"Line":4}},{"line":416,"address":[],"length":0,"stats":{"Line":18}},{"line":417,"address":[],"length":0,"stats":{"Line":56}},{"line":418,"address":[],"length":0,"stats":{"Line":54}},{"line":429,"address":[],"length":0,"stats":{"Line":22}},{"line":430,"address":[],"length":0,"stats":{"Line":35}},{"line":431,"address":[],"length":0,"stats":{"Line":52}},{"line":432,"address":[],"length":0,"stats":{"Line":13}},{"line":433,"address":[],"length":0,"stats":{"Line":13}},{"line":436,"address":[],"length":0,"stats":{"Line":9}},{"line":437,"address":[],"length":0,"stats":{"Line":9}},{"line":438,"address":[],"length":0,"stats":{"Line":9}},{"line":447,"address":[],"length":0,"stats":{"Line":5}},{"line":448,"address":[],"length":0,"stats":{"Line":10}},{"line":450,"address":[],"length":0,"stats":{"Line":15}},{"line":451,"address":[],"length":0,"stats":{"Line":10}},{"line":453,"address":[],"length":0,"stats":{"Line":15}},{"line":454,"address":[],"length":0,"stats":{"Line":10}},{"line":457,"address":[],"length":0,"stats":{"Line":10}},{"line":458,"address":[],"length":0,"stats":{"Line":10}},{"line":459,"address":[],"length":0,"stats":{"Line":0}},{"line":464,"address":[],"length":0,"stats":{"Line":25}},{"line":465,"address":[],"length":0,"stats":{"Line":10}},{"line":467,"address":[],"length":0,"stats":{"Line":5}},{"line":473,"address":[],"length":0,"stats":{"Line":15}},{"line":474,"address":[],"length":0,"stats":{"Line":10}},{"line":475,"address":[],"length":0,"stats":{"Line":30}},{"line":476,"address":[],"length":0,"stats":{"Line":15}},{"line":477,"address":[],"length":0,"stats":{"Line":20}},{"line":478,"address":[],"length":0,"stats":{"Line":25}},{"line":479,"address":[],"length":0,"stats":{"Line":10}},{"line":482,"address":[],"length":0,"stats":{"Line":10}},{"line":483,"address":[],"length":0,"stats":{"Line":12}},{"line":484,"address":[],"length":0,"stats":{"Line":3}},{"line":486,"address":[],"length":0,"stats":{"Line":4}},{"line":491,"address":[],"length":0,"stats":{"Line":16}},{"line":492,"address":[],"length":0,"stats":{"Line":32}},{"line":494,"address":[],"length":0,"stats":{"Line":48}},{"line":495,"address":[],"length":0,"stats":{"Line":32}},{"line":498,"address":[],"length":0,"stats":{"Line":32}},{"line":499,"address":[],"length":0,"stats":{"Line":32}},{"line":500,"address":[],"length":0,"stats":{"Line":0}},{"line":503,"address":[],"length":0,"stats":{"Line":48}},{"line":504,"address":[],"length":0,"stats":{"Line":103}},{"line":505,"address":[],"length":0,"stats":{"Line":151}},{"line":506,"address":[],"length":0,"stats":{"Line":41}},{"line":508,"address":[],"length":0,"stats":{"Line":14}},{"line":512,"address":[],"length":0,"stats":{"Line":16}},{"line":522,"address":[],"length":0,"stats":{"Line":6}},{"line":523,"address":[],"length":0,"stats":{"Line":12}},{"line":525,"address":[],"length":0,"stats":{"Line":12}},{"line":527,"address":[],"length":0,"stats":{"Line":18}},{"line":528,"address":[],"length":0,"stats":{"Line":12}},{"line":531,"address":[],"length":0,"stats":{"Line":12}},{"line":532,"address":[],"length":0,"stats":{"Line":12}},{"line":533,"address":[],"length":0,"stats":{"Line":0}},{"line":537,"address":[],"length":0,"stats":{"Line":30}},{"line":538,"address":[],"length":0,"stats":{"Line":12}},{"line":542,"address":[],"length":0,"stats":{"Line":18}},{"line":549,"address":[],"length":0,"stats":{"Line":4}},{"line":550,"address":[],"length":0,"stats":{"Line":4}}],"covered":89,"coverable":114},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","encoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::Encoder` — abstract base for media encoders.\n//!\n//! Mirrors `src/codec/src/encoder.h`. Implementations are\n//! [`crate::ffmpeg::FFmpegEncoder`] and [`crate::oiio::OIIOEncoder`]. The\n//! workflow (C ABI encoder.h) is: fill an `EncodingParams` → init → open →\n//! write_video/audio/subtitle → flush. The trait mirrors the C++ virtual\n//! surface.\n\nuse std::sync::{Arc, Mutex, OnceLock};\n\nuse oakcore_rs::{PixelFormat, SampleFormat};\n\nuse crate::encodingparams::EncodingParams;\nuse crate::frame::Frame;\n\n/// `olive::Encoder` — encoder trait. Backs the refcounted encoder handle.\npub trait Encoder: Send + Sync {\n\t/// Unique encoder id.\n\tfn id(\u0026self) -\u003e String;\n\n\t/// Whether this encoder writes video.\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder writes audio.\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder writes subtitles.\n\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder writes an image sequence.\n\tfn supports_image_sequences(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder is deterministic for a given config\n\t/// (used for cache keys).\n\tfn is_configurable(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Configure the encoder (per-codec options like `crf`).\n\tfn configure(\u0026self, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Open the output file and write headers.\n\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Close the output (write trailer); idempotent.\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Encode one video frame (converts delivery pixel format internally).\n\tfn write_video(\u0026self, frame: \u0026Frame) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Encode interleaved float audio samples.\n\tfn write_audio(\u0026self, samples: \u0026[f32], frame_count: i32) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Encode one subtitle entry (times in seconds).\n\tfn write_subtitle(\n\t\t\u0026self,\n\t\ttext: \u0026str,\n\t\tin_seconds: f64,\n\t\tout_seconds: f64,\n\t) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Flush encoders, write the trailer, close the file.\n\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// The pixel format the encoder wants frames in (or `None`).\n\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e;\n\n\t/// The sample format the encoder wants audio in (or `None`).\n\tfn desired_sample_format(\u0026self) -\u003e Option\u003cSampleFormat\u003e;\n\n\t/// The configured output filename.\n\tfn filename(\u0026self) -\u003e String;\n\n\t/// Human-readable detail of the last failed operation (empty when\n\t/// none). Mirrors the C++ `Encoder::get_error()` used by\n\t/// `oakcodec_encoder_last_error`.\n\tfn get_error(\u0026self) -\u003e String {\n\t\tString::new()\n\t}\n}\n\n/// Test-injected encoder registry (see [`set_test_encoders`]); empty when\n/// not injected, in which case [`create_from_params`] falls back to the\n/// built-in format mapping.\nstatic TEST_ENCODERS: OnceLock\u003cMutex\u003cVec\u003cArc\u003cdyn Encoder\u003e\u003e\u003e\u003e = OnceLock::new();\n\n/// Replace the encoder registry with `list`; pass an empty list to restore\n/// the built-in behavior.\n///\n/// Test/extension support (the C ABI has no way to register an encoder, so\n/// the contract tests drive the encode state machine through a fake\n/// encoder). Hidden from docs; never called by production code.\n#[doc(hidden)]\npub fn set_test_encoders(list: Vec\u003cArc\u003cdyn Encoder\u003e\u003e) {\n\tlet store = TEST_ENCODERS.get_or_init(|| Mutex::new(Vec::new()));\n\t*store.lock().unwrap() = list;\n}\n\n/// `Encoder::create_from_params` — instantiate an encoder for `params`.\n///\n/// # CPP-PARITY\n/// `src/codec/src/encoder.cpp` `create_from_params` → `create_from_format`\n/// picks the FFmpeg/OIIO implementation from `params.format` (DNxHD,\n/// Matroska, QuickTime, MPEG-4 video/audio, WAV, AIFF, MP3, FLAC, Ogg,\n/// WebM, SRT → FFmpeg; OpenEXR, PNG, TIFF → OIIO; anything else → `None`).\n/// A non-empty test-injected list (see [`set_test_encoders`]) wins over the\n/// built-in mapping. The concrete implementations are dylib stubs whose\n/// `open()` fails with a clear message, so an initialized encoder handle is\n/// always constructible for a recognized format.\npub fn create_from_params(params: \u0026EncodingParams) -\u003e Option\u003cArc\u003cdyn Encoder\u003e\u003e {\n\tif let Some(store) = TEST_ENCODERS.get() {\n\t\tlet injected = store.lock().unwrap();\n\t\tif !injected.is_empty() {\n\t\t\treturn injected.first().cloned();\n\t\t}\n\t}\n\tmatch encoder_type_from_format(params.format) {\n\t\tSome(EncoderType::FFmpeg) =\u003e {\n\t\t\tSome(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(params.clone())))\n\t\t}\n\t\tSome(EncoderType::OIIO) =\u003e {\n\t\t\tSome(Arc::new(crate::oiio::OIIOEncoder { params: params.clone() }))\n\t\t}\n\t\tNone =\u003e None,\n\t}\n}\n\n/// `Encoder::Type` mirror (`encoder.cpp` `get_type_from_format`).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum EncoderType {\n\t/// `k_encoder_type_f_fmpeg`.\n\tFFmpeg,\n\t/// `k_encoder_type_oiio`.\n\tOIIO,\n}\n\n/// `Encoder::get_type_from_format` — the implementation family for an\n/// `ExportFormat::Format` int; `None` for unknown/`Count`.\nfn encoder_type_from_format(format: i32) -\u003e Option\u003cEncoderType\u003e {\n\tmatch format {\n\t\t// FFmpeg-backed containers.\n\t\t0 | 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 =\u003e Some(EncoderType::FFmpeg),\n\t\t// OIIO-backed still-image formats.\n\t\t3 | 5 | 6 =\u003e Some(EncoderType::OIIO),\n\t\t_ =\u003e None,\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn create_from_params_maps_formats() {\n\t\tlet mut p = EncodingParams::default();\n\n\t\t// FFmpeg-backed containers.\n\t\tfor fmt in [0, 1, 2, 4, 7, 8, 9, 10, 11, 12, 13, 14] {\n\t\t\tp.format = fmt;\n\t\t\tlet e = create_from_params(\u0026p).expect(\"format {fmt}\");\n\t\t\tassert_eq!(e.id(), \"ffmpeg\", \"format {fmt}\");\n\t\t}\n\t\t// OIIO-backed still images.\n\t\tfor fmt in [3, 5, 6] {\n\t\t\tp.format = fmt;\n\t\t\tlet e = create_from_params(\u0026p).expect(\"format {fmt}\");\n\t\t\tassert_eq!(e.id(), \"oiio\", \"format {fmt}\");\n\t\t}\n\t\t// Unknown / Count -\u003e None (C++ `k_encoder_type_none`).\n\t\tp.format = 15;\n\t\tassert!(create_from_params(\u0026p).is_none());\n\t\tp.format = -1;\n\t\tassert!(create_from_params(\u0026p).is_none());\n\t}\n\n\t#[test]\n\tfn get_error_defaults_to_empty() {\n\t\tlet e = UnimplementedDummy;\n\t\tassert_eq!(e.get_error(), \"\");\n\t}\n\n\tstruct UnimplementedDummy;\n\n\timpl Encoder for UnimplementedDummy {\n\t\tfn id(\u0026self) -\u003e String {\n\t\t\t\"dummy\".to_string()\n\t\t}\n\t\tfn configure(\u0026self, _p: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_video(\u0026self, _f: \u0026Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_audio(\u0026self, _s: \u0026[f32], _c: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_subtitle(\u0026self, _t: \u0026str, _i: f64, _o: f64) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e {\n\t\t\tNone\n\t\t}\n\t\tfn desired_sample_format(\u0026self) -\u003e Option\u003cSampleFormat\u003e {\n\t\t\tNone\n\t\t}\n\t\tfn filename(\u0026self) -\u003e String {\n\t\t\tString::new()\n\t\t}\n\t}\n}\n","traces":[{"line":38,"address":[],"length":0,"stats":{"Line":0}},{"line":39,"address":[],"length":0,"stats":{"Line":0}},{"line":43,"address":[],"length":0,"stats":{"Line":0}},{"line":44,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}},{"line":53,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":59,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":2}},{"line":102,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":10}},{"line":119,"address":[],"length":0,"stats":{"Line":32}},{"line":120,"address":[],"length":0,"stats":{"Line":30}},{"line":134,"address":[],"length":0,"stats":{"Line":22}},{"line":135,"address":[],"length":0,"stats":{"Line":27}},{"line":136,"address":[],"length":0,"stats":{"Line":20}},{"line":137,"address":[],"length":0,"stats":{"Line":5}},{"line":138,"address":[],"length":0,"stats":{"Line":6}},{"line":141,"address":[],"length":0,"stats":{"Line":38}},{"line":143,"address":[],"length":0,"stats":{"Line":39}},{"line":146,"address":[],"length":0,"stats":{"Line":6}},{"line":148,"address":[],"length":0,"stats":{"Line":3}},{"line":163,"address":[],"length":0,"stats":{"Line":19}},{"line":164,"address":[],"length":0,"stats":{"Line":19}},{"line":166,"address":[],"length":0,"stats":{"Line":13}},{"line":168,"address":[],"length":0,"stats":{"Line":3}},{"line":169,"address":[],"length":0,"stats":{"Line":3}}],"covered":19,"coverable":29},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","encodingparams.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::EncodingParams` — export encoding parameters.\n//!\n//! Mirrors the `EncodingParams` class carried in `src/codec/src/encoder.h`\n//! and the flattened `oakcodec_encoding_params` POD in\n//! `include/codec/encoder.h`. The Rust struct mirrors the POD verbatim so\n//! `ffi.rs` can marshal it without translation.\n\nuse oakcore_rs::{PixelFormat, SampleFormat};\n\n/// `VideoParams::Interlacing` values.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum VideoScalingMethod {\n\t/// Fit the source into the destination, preserving aspect.\n\tFit = 0,\n\t/// Stretch the source to the destination.\n\tStretch = 1,\n\t/// Crop the source to the destination.\n\tCrop = 2,\n}\n\n/// `olive::EncodingParams` — flattened mirror of `oakcodec_encoding_params`.\n#[derive(Clone, Debug)]\n#[repr(C)]\npub struct EncodingParams {\n\t/// Output filename (or image-sequence \"[#####]\" template).\n\tpub filename: [u8; 1024],\n\t/// `ExportFormat::Format`.\n\tpub format: i32,\n\n\t/// Video track enabled (1/0; C `int`).\n\tpub video_enabled: i32,\n\t/// `ExportCodec::Codec`.\n\tpub video_codec: i32,\n\t/// Output width.\n\tpub video_width: i32,\n\t/// Output height.\n\tpub video_height: i32,\n\t/// Frame duration (rational): numerator.\n\tpub video_time_base_num: i32,\n\t/// Frame duration (rational): denominator.\n\tpub video_time_base_den: i32,\n\t/// Delivery `OakPixelFormat`.\n\tpub video_pixel_format: PixelFormat,\n\t/// Interlacing (`Interlacing` value).\n\tpub video_interlacing: i32,\n\t/// Pixel aspect ratio numerator.\n\tpub video_pixel_aspect_num: i32,\n\t/// Pixel aspect ratio denominator.\n\tpub video_pixel_aspect_den: i32,\n\t/// Bit rate (bit/s), 0 = codec default.\n\tpub video_bit_rate: i64,\n\t/// Min bit rate.\n\tpub video_min_bit_rate: i64,\n\t/// Max bit rate.\n\tpub video_max_bit_rate: i64,\n\t/// Buffer size (bytes).\n\tpub video_buffer_size: i64,\n\t/// Encoding threads (0 = auto).\n\tpub video_threads: i32,\n\t/// Encoded pixel format name (e.g. \"yuv420p\").\n\tpub video_pix_fmt: [u8; 64],\n\t/// Image sequence output (1/0).\n\tpub video_is_image_sequence: i32,\n\t/// Scaling method (`VideoScalingMethod`).\n\tpub video_scaling_method: VideoScalingMethod,\n\n\t/// Audio track enabled (1/0).\n\tpub audio_enabled: i32,\n\t/// `ExportCodec::Codec`.\n\tpub audio_codec: i32,\n\t/// Audio sample rate.\n\tpub audio_sample_rate: i32,\n\t/// ffmpeg-style channel layout mask.\n\tpub audio_channel_layout: u64,\n\t/// `SampleFormat`.\n\tpub audio_sample_format: SampleFormat,\n\t/// Audio bit rate.\n\tpub audio_bit_rate: i64,\n\n\t/// Subtitles track enabled (1/0).\n\tpub subtitles_enabled: i32,\n\t/// `ExportCodec::Codec`.\n\tpub subtitles_codec: i32,\n\t/// Subtitles written as a sidecar file (1/0).\n\tpub subtitles_are_sidecar: i32,\n\t/// Sidecar format (`ExportFormat::Format`).\n\tpub subtitles_sidecar_format: i32,\n\n\t/// Output OCIO colorspace name; empty = reference space (no transform).\n\tpub color_transform_output: [u8; 256],\n\n\t/// Export length in seconds (rational).\n\tpub export_length_num: i32,\n\t/// Export length in seconds (rational).\n\tpub export_length_den: i32,\n\n\t/// Custom export range enabled (1/0).\n\tpub has_custom_range: i32,\n\t/// Custom range in, rational seconds.\n\tpub custom_range_in_num: i64,\n\t/// Custom range in denominator.\n\tpub custom_range_in_den: i64,\n\t/// Custom range out, rational seconds.\n\tpub custom_range_out_num: i64,\n\t/// Custom range out denominator.\n\tpub custom_range_out_den: i64,\n}\n\nimpl Default for EncodingParams {\n\tfn default() -\u003e Self {\n\t\tSelf {\n\t\t\tfilename: [0; 1024],\n\t\t\tformat: -1,\n\n\t\t\tvideo_enabled: 0,\n\t\t\tvideo_codec: -1,\n\t\t\tvideo_width: 0,\n\t\t\tvideo_height: 0,\n\t\t\tvideo_time_base_num: 1,\n\t\t\tvideo_time_base_den: 1,\n\t\t\tvideo_pixel_format: PixelFormat::Invalid,\n\t\t\tvideo_interlacing: 0,\n\t\t\tvideo_pixel_aspect_num: 0,\n\t\t\tvideo_pixel_aspect_den: 0,\n\t\t\tvideo_bit_rate: 0,\n\t\t\tvideo_min_bit_rate: 0,\n\t\t\tvideo_max_bit_rate: 0,\n\t\t\tvideo_buffer_size: 0,\n\t\t\tvideo_threads: 0,\n\t\t\tvideo_pix_fmt: [0; 64],\n\t\t\tvideo_is_image_sequence: 0,\n\t\t\tvideo_scaling_method: VideoScalingMethod::Stretch,\n\n\t\t\taudio_enabled: 0,\n\t\t\taudio_codec: -1,\n\t\t\taudio_sample_rate: 0,\n\t\t\taudio_channel_layout: 0,\n\t\t\taudio_sample_format: SampleFormat::Invalid,\n\t\t\taudio_bit_rate: 0,\n\n\t\t\tsubtitles_enabled: 0,\n\t\t\tsubtitles_codec: -1,\n\t\t\tsubtitles_are_sidecar: 0,\n\t\t\tsubtitles_sidecar_format: -1,\n\n\t\t\tcolor_transform_output: [0; 256],\n\n\t\t\texport_length_num: 0,\n\t\t\texport_length_den: 0,\n\n\t\t\thas_custom_range: 0,\n\t\t\tcustom_range_in_num: 0,\n\t\t\tcustom_range_in_den: 0,\n\t\t\tcustom_range_out_num: 0,\n\t\t\tcustom_range_out_den: 0,\n\t\t}\n\t}\n}\n\nimpl EncodingParams {\n\t/// Scaling matrix for a scaling method; row-major 4x4 into `out[16]`.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/encoder.cpp` `EncodingParams::generate_matrix` —\n\t/// returns a row-major `std::array\u003cfloat,16\u003e` (formerly QMatrix4x4).\n\tpub fn generate_matrix(\n\t\tmethod: VideoScalingMethod,\n\t\tsrc_width: i32,\n\t\tsrc_height: i32,\n\t\tdst_width: i32,\n\t\tdst_height: i32,\n\t\tout: \u0026mut [f64; 16],\n\t) {\n\t\t// Identity (former default-constructed QMatrix4x4), row-major.\n\t\t*out = [\n\t\t\t1.0, 0.0, 0.0, 0.0, //\n\t\t\t0.0, 1.0, 0.0, 0.0, //\n\t\t\t0.0, 0.0, 1.0, 0.0, //\n\t\t\t0.0, 0.0, 0.0, 1.0,\n\t\t];\n\n\t\tif method == VideoScalingMethod::Stretch {\n\t\t\treturn;\n\t\t}\n\n\t\t// Guard degenerate sizes: the C++ would produce inf/NaN here; an\n\t\t// identity is the only safe output for the rendering pipeline.\n\t\tif src_width \u003c= 0 || src_height \u003c= 0 || dst_width \u003c= 0 || dst_height \u003c= 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet export_ar = dst_width as f64 / dst_height as f64;\n\t\tlet source_ar = src_width as f64 / src_height as f64;\n\n\t\t// qFuzzyCompare(export_ar, source_ar): within one part in 100000.\n\t\tif (export_ar - source_ar).abs() * 100000.0\n\t\t\t\u003c= export_ar.abs().min(source_ar.abs())\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif (export_ar \u003e source_ar) == (method == VideoScalingMethod::Fit) {\n\t\t\t// scale(source_ar / export_ar, 1)\n\t\t\tout[0] = source_ar / export_ar;\n\t\t} else {\n\t\t\t// scale(1, export_ar / source_ar)\n\t\t\tout[5] = export_ar / source_ar;\n\t\t}\n\t}\n\n\t/// File extension for this format (e.g. \"mp4\").\n\t///\n\t/// # CPP-PARITY\n\t/// `ExportFormat::get_extension` (`src/codec/src/exportformat.cpp`) keyed\n\t/// on the serialized `Format` discriminant. Unknown / invalid formats\n\t/// return the empty string, matching the C++ default case.\n\tpub fn extension(\u0026self) -\u003e \u0026str {\n\t\tmatch self.format {\n\t\t\t0 =\u003e \"mxf\", // DNxHD\n\t\t\t1 =\u003e \"mkv\", // Matroska\n\t\t\t2 =\u003e \"mp4\", // MPEG-4 video\n\t\t\t3 =\u003e \"exr\", // OpenEXR\n\t\t\t4 =\u003e \"mov\", // QuickTime\n\t\t\t5 =\u003e \"png\", // PNG\n\t\t\t6 =\u003e \"tiff\", // TIFF\n\t\t\t7 =\u003e \"wav\", // WAV\n\t\t\t8 =\u003e \"aiff\", // AIFF\n\t\t\t9 =\u003e \"mp3\", // MP3\n\t\t\t10 =\u003e \"flac\", // FLAC\n\t\t\t11 =\u003e \"ogg\", // Ogg\n\t\t\t12 =\u003e \"webm\", // WebM\n\t\t\t13 =\u003e \"srt\", // SRT\n\t\t\t14 =\u003e \"m4a\", // MPEG-4 audio\n\t\t\t_ =\u003e \"\",\n\t\t}\n\t}\n\n\t/// Load from a compact XML preset string (oakcommon C++ XmlStreamReader).\n\t///\n\t/// # CPP-PARITY\n\t/// `EncodingParams::load` — uses oakcommon's C++ `XmlStreamReader`\n\t/// (`src/common/src/xmlutils.h`), a C++-to-C++ coupling the bridge\n\t/// cannot cover (NOTES.md §7). Preserves the load_v1 bug of not\n\t/// assigning `custom_range`.\n\tpub fn load(\u0026mut self, data: \u0026str) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet root = parse_preset(data)\n\t\t\t.map_err(|e| crate::error::Error::Failed(format!(\"invalid export preset: {e}\")))?;\n\t\tif root.name != \"export\" {\n\t\t\treturn Err(crate::error::Error::Failed(\n\t\t\t\t\"not an export preset document\".to_string(),\n\t\t\t));\n\t\t}\n\n\t\t// `range` / `customrangein` / `customrangeout` are parsed for shape\n\t\t// parity but never assigned — mirroring the C++ `load_v1` bug that\n\t\t// reads them into locals and forgets to store `custom_range`.\n\t\tif let Some(e) = child(\u0026root, \"range\") {\n\t\t\tlet _ = parse_bool(\u0026text(e));\n\t\t}\n\t\tif let Some(e) = child(\u0026root, \"customrangein\") {\n\t\t\tlet _ = \u0026text(e);\n\t\t}\n\t\tif let Some(e) = child(\u0026root, \"customrangeout\") {\n\t\t\tlet _ = \u0026text(e);\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"filename\") {\n\t\t\tset_cstr(\u0026mut self.filename, \u0026text(e));\n\t\t}\n\t\tif let Some(e) = child(\u0026root, \"format\") {\n\t\t\tself.format = parse_i32(\u0026text(e));\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"video\") {\n\t\t\tif let Some(v) = attr(e, \"enabled\") {\n\t\t\t\tself.video_enabled = parse_bool(v) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"codec\") {\n\t\t\t\tself.video_codec = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"width\") {\n\t\t\t\tself.video_width = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"height\") {\n\t\t\t\tself.video_height = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"format\") {\n\t\t\t\tself.video_pixel_format = match parse_i32(\u0026text(e)) {\n\t\t\t\t\t-1 =\u003e PixelFormat::Invalid,\n\t\t\t\t\t0 =\u003e PixelFormat::U8,\n\t\t\t\t\t1 =\u003e PixelFormat::U10,\n\t\t\t\t\t2 =\u003e PixelFormat::U16,\n\t\t\t\t\t3 =\u003e PixelFormat::F16,\n\t\t\t\t\t4 =\u003e PixelFormat::F32,\n\t\t\t\t\t_ =\u003e PixelFormat::Invalid,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"timebase\") {\n\t\t\t\tlet tb = text(e);\n\t\t\t\tif let Some((n, d)) = tb.split_once('/') {\n\t\t\t\t\tself.video_time_base_num = parse_i32(n);\n\t\t\t\t\tself.video_time_base_den = parse_i32(d);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"divider\") {\n\t\t\t\tself.video_interlacing = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"pixelaspect\") {\n\t\t\t\tlet par = text(e);\n\t\t\t\tif let Some((n, d)) = par.split_once('/') {\n\t\t\t\t\tself.video_pixel_aspect_num = parse_i32(n);\n\t\t\t\t\tself.video_pixel_aspect_den = parse_i32(d);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"bitrate\") {\n\t\t\t\tself.video_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"minbitrate\") {\n\t\t\t\tself.video_min_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"maxbitrate\") {\n\t\t\t\tself.video_max_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"bufsize\") {\n\t\t\t\tself.video_buffer_size = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"threads\") {\n\t\t\t\tself.video_threads = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"pixfmt\") {\n\t\t\t\tset_cstr(\u0026mut self.video_pix_fmt, \u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"imgseq\") {\n\t\t\t\tself.video_is_image_sequence = parse_bool(\u0026text(e)) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"vscale\") {\n\t\t\t\tself.video_scaling_method = scaling_from_i32(parse_i32(\u0026text(e)));\n\t\t\t}\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"audio\") {\n\t\t\tif let Some(v) = attr(e, \"enabled\") {\n\t\t\t\tself.audio_enabled = parse_bool(v) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"codec\") {\n\t\t\t\tself.audio_codec = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"samplerate\") {\n\t\t\t\tself.audio_sample_rate = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"channellayout\") {\n\t\t\t\tself.audio_channel_layout = parse_i64(\u0026text(e)) as u64;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"format\") {\n\t\t\t\tself.audio_sample_format = sample_format_from_i32(parse_i32(\u0026text(e)));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"bitrate\") {\n\t\t\t\tself.audio_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"subtitles\") {\n\t\t\tif let Some(v) = attr(e, \"enabled\") {\n\t\t\t\tself.subtitles_enabled = parse_bool(v) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"sidecar\") {\n\t\t\t\tself.subtitles_are_sidecar = parse_bool(\u0026text(e)) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"sidecarformat\") {\n\t\t\t\tself.subtitles_sidecar_format = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"codec\") {\n\t\t\t\tself.subtitles_codec = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t/// Serialize to a compact XML string (no declaration, no indentation;\n\t/// element/attribute names and order preserved).\n\t///\n\t/// # CPP-PARITY\n\t/// The C++ writer also emits per-codec `video_opts_`; those moved out\n\t/// of the byte-exact `EncodingParams` POD (see `encoding_params_c_abi_layout`),\n\t/// so the options are not serialized here.\n\tpub fn save_to_string(\u0026self) -\u003e String {\n\t\tlet mut s = String::new();\n\t\ts.push_str(\"\u003cexport version=\\\"1\\\"\u003e\");\n\t\ts.push_str(\u0026format!(\"\u003cfilename\u003e{}\u003c/filename\u003e\", escape_xml(cstr(\u0026self.filename))));\n\t\ts.push_str(\u0026format!(\"\u003cformat\u003e{}\u003c/format\u003e\", self.format));\n\t\ts.push_str(\u0026format!(\"\u003crange\u003e{}\u003c/range\u003e\", self.has_custom_range));\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003ccustomrangein\u003e{}/{}\u003c/customrangein\u003e\",\n\t\t\tself.custom_range_in_num, self.custom_range_in_den\n\t\t));\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003ccustomrangeout\u003e{}/{}\u003c/customrangeout\u003e\",\n\t\t\tself.custom_range_out_num, self.custom_range_out_den\n\t\t));\n\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003cvideo enabled=\\\"{}\\\"\u003e\",\n\t\t\tself.video_enabled\n\t\t));\n\t\tif self.video_enabled != 0 {\n\t\t\ts.push_str(\u0026format!(\"\u003ccodec\u003e{}\u003c/codec\u003e\", self.video_codec));\n\t\t\ts.push_str(\u0026format!(\"\u003cwidth\u003e{}\u003c/width\u003e\", self.video_width));\n\t\t\ts.push_str(\u0026format!(\"\u003cheight\u003e{}\u003c/height\u003e\", self.video_height));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cformat\u003e{}\u003c/format\u003e\",\n\t\t\t\tself.video_pixel_format as i32\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003ctimebase\u003e{}/{}\u003c/timebase\u003e\",\n\t\t\t\tself.video_time_base_num, self.video_time_base_den\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cdivider\u003e{}\u003c/divider\u003e\",\n\t\t\t\tself.video_interlacing\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cpixelaspect\u003e{}/{}\u003c/pixelaspect\u003e\",\n\t\t\t\tself.video_pixel_aspect_num, self.video_pixel_aspect_den\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\"\u003cbitrate\u003e{}\u003c/bitrate\u003e\", self.video_bit_rate));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cminbitrate\u003e{}\u003c/minbitrate\u003e\",\n\t\t\t\tself.video_min_bit_rate\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cmaxbitrate\u003e{}\u003c/maxbitrate\u003e\",\n\t\t\t\tself.video_max_bit_rate\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cbufsize\u003e{}\u003c/bufsize\u003e\",\n\t\t\t\tself.video_buffer_size\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\"\u003cthreads\u003e{}\u003c/threads\u003e\", self.video_threads));\n\t\t\ts.push_str(\u0026format!(\"\u003cpixfmt\u003e{}\u003c/pixfmt\u003e\", escape_xml(cstr(\u0026self.video_pix_fmt))));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cimgseq\u003e{}\u003c/imgseq\u003e\",\n\t\t\t\tself.video_is_image_sequence\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cvscale\u003e{}\u003c/vscale\u003e\",\n\t\t\t\tself.video_scaling_method as i32\n\t\t\t));\n\t\t}\n\t\ts.push_str(\"\u003c/video\u003e\");\n\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003caudio enabled=\\\"{}\\\"\u003e\",\n\t\t\tself.audio_enabled\n\t\t));\n\t\tif self.audio_enabled != 0 {\n\t\t\ts.push_str(\u0026format!(\"\u003ccodec\u003e{}\u003c/codec\u003e\", self.audio_codec));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003csamplerate\u003e{}\u003c/samplerate\u003e\",\n\t\t\t\tself.audio_sample_rate\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cchannellayout\u003e{}\u003c/channellayout\u003e\",\n\t\t\t\tself.audio_channel_layout\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cformat\u003e{}\u003c/format\u003e\",\n\t\t\t\tself.audio_sample_format as i32\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\"\u003cbitrate\u003e{}\u003c/bitrate\u003e\", self.audio_bit_rate));\n\t\t}\n\t\ts.push_str(\"\u003c/audio\u003e\");\n\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003csubtitles enabled=\\\"{}\\\"\u003e\",\n\t\t\tself.subtitles_enabled\n\t\t));\n\t\tif self.subtitles_enabled != 0 {\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003csidecar\u003e{}\u003c/sidecar\u003e\",\n\t\t\t\tself.subtitles_are_sidecar\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003csidecarformat\u003e{}\u003c/sidecarformat\u003e\",\n\t\t\t\tself.subtitles_sidecar_format\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003ccodec\u003e{}\u003c/codec\u003e\",\n\t\t\t\tself.subtitles_codec\n\t\t\t));\n\t\t}\n\t\ts.push_str(\"\u003c/subtitles\u003e\");\n\n\t\ts.push_str(\"\u003c/export\u003e\");\n\t\ts\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Minimal XML helpers for the round-trip `load`/`save_to_string`.\n//\n// CPP-PARITY: the C++ `load`/`save_to_string` go through oakcommon's\n// `XmlStreamReader`/`XmlStreamWriter` (a C++-to-C++ coupling the Rust bridge\n// cannot cover, NOTES.md §7). Rather than returning `Err`, this port keeps a\n// minimal but faithful round-trip for the fields representable without the\n// videoparams/colortransform/audio bridge. Deviations vs the C++ writer:\n// * the `\u003cvideo\u003e` width/height/format are taken from the Rust fields\n// directly (the C++ reads them out of a live `OakVideoParams` handle);\n// * `\u003ctimebase\u003e`, `\u003cdivider\u003e`, `\u003cpixelaspect\u003e` mirror the corresponding\n// flattened fields;\n// * `customrangein`/`customrangeout` are written but, on `load`, parsed and\n// discarded — preserving the C++ `load_v1` bug that never assigns\n// `custom_range`.\n// ---------------------------------------------------------------------------\n\n/// Minimal element of the round-trip XML tree.\nstruct El {\n\tname: String,\n\tattrs: Vec\u003c(String, String)\u003e,\n\ttext: Option\u003cString\u003e,\n\tchildren: Vec\u003cEl\u003e,\n}\n\n/// Positional cursor over the XML text.\nstruct Cursor\u003c'a\u003e {\n\ts: \u0026'a str,\n\ti: usize,\n}\n\nimpl\u003c'a\u003e Cursor\u003c'a\u003e {\n\tfn peek(\u0026self) -\u003e Option\u003cchar\u003e {\n\t\tself.s[self.i..].chars().next()\n\t}\n\n\tfn skip_ws(\u0026mut self) {\n\t\twhile self.peek().map_or(false, char::is_whitespace) {\n\t\t\tself.i += 1;\n\t\t}\n\t}\n\n\tfn starts_with(\u0026self, p: \u0026str) -\u003e bool {\n\t\tself.s[self.i..].starts_with(p)\n\t}\n\n\tfn read_name(\u0026mut self) -\u003e Result\u003cString, String\u003e {\n\t\tlet start = self.i;\n\t\twhile let Some(ch) = self.peek() {\n\t\t\tif ch.is_alphanumeric() || ch == '_' || ch == '-' {\n\t\t\t\tself.i += ch.len_utf8();\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif self.i == start {\n\t\t\treturn Err(\"expected element/attribute name\".to_string());\n\t\t}\n\t\tOk(self.s[start..self.i].to_string())\n\t}\n\n\tfn read_attr_value(\u0026mut self) -\u003e Result\u003cString, String\u003e {\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\\\"\") {\n\t\t\treturn Err(\"expected quoted attribute value\".to_string());\n\t\t}\n\t\tself.i += 1;\n\t\tlet start = self.i;\n\t\twhile self.i \u003c self.s.len() \u0026\u0026 !self.starts_with(\"\\\"\") {\n\t\t\tself.i += 1;\n\t\t}\n\t\tif self.i \u003e= self.s.len() {\n\t\t\treturn Err(\"unterminated attribute value\".to_string());\n\t\t}\n\t\tlet v = self.s[start..self.i].to_string();\n\t\tself.i += 1;\n\t\tOk(v)\n\t}\n\n\tfn expect_end(\u0026mut self, name: \u0026str) -\u003e Result\u003c(), String\u003e {\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\u003c/\") {\n\t\t\treturn Err(format!(\"expected \u003c/{}\u003e\", name));\n\t\t}\n\t\tself.i += 2;\n\t\tlet n = self.read_name()?;\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\u003e\") {\n\t\t\treturn Err(\"expected '\u003e' closing end tag\".to_string());\n\t\t}\n\t\tself.i += 1;\n\t\tif n != name {\n\t\t\treturn Err(format!(\"mismatched end tag {} vs {}\", n, name));\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn parse_element(\u0026mut self) -\u003e Result\u003cEl, String\u003e {\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\u003c\") {\n\t\t\treturn Err(\"expected element\".to_string());\n\t\t}\n\t\tself.i += 1;\n\t\tlet name = self.read_name()?;\n\t\tself.skip_ws();\n\t\tlet mut attrs = Vec::new();\n\t\tloop {\n\t\t\tself.skip_ws();\n\t\t\tif self.starts_with(\"/\u003e\") {\n\t\t\t\tself.i += 2;\n\t\t\t\treturn Ok(El { name, attrs, text: None, children: Vec::new() });\n\t\t\t}\n\t\t\tif self.starts_with(\"\u003e\") {\n\t\t\t\tself.i += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet an = self.read_name()?;\n\t\t\tself.skip_ws();\n\t\t\tif self.starts_with(\"=\") {\n\t\t\t\tself.i += 1;\n\t\t\t}\n\t\t\tlet v = self.read_attr_value()?;\n\t\t\tattrs.push((an, v));\n\t\t}\n\t\t// Content: either nested elements or a text node.\n\t\tself.skip_ws();\n\t\tif self.i \u003c self.s.len() \u0026\u0026 self.starts_with(\"\u003c\") {\n\t\t\tlet mut children = Vec::new();\n\t\t\tloop {\n\t\t\t\tself.skip_ws();\n\t\t\t\tif self.i \u003e= self.s.len() {\n\t\t\t\t\treturn Err(\"unexpected EOF in element\".to_string());\n\t\t\t\t}\n\t\t\t\tif self.starts_with(\"\u003c/\") {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif self.starts_with(\"\u003c\") {\n\t\t\t\t\tchildren.push(self.parse_element()?);\n\t\t\t\t} else {\n\t\t\t\t\treturn Err(\"unexpected text at element level\".to_string());\n\t\t\t\t}\n\t\t\t}\n\t\t\tself.expect_end(\u0026name)?;\n\t\t\tOk(El { name, attrs, text: None, children })\n\t\t} else {\n\t\t\tlet start = self.i;\n\t\t\twhile self.i \u003c self.s.len() \u0026\u0026 !self.starts_with(\"\u003c\") {\n\t\t\t\tself.i += 1;\n\t\t\t}\n\t\t\tlet text = self.s[start..self.i].to_string();\n\t\t\tself.expect_end(\u0026name)?;\n\t\t\tOk(El { name, attrs, text: Some(text), children: Vec::new() })\n\t\t}\n\t}\n}\n\n/// Parse the single root element of a preset document.\nfn parse_preset(data: \u0026str) -\u003e Result\u003cEl, String\u003e {\n\tlet mut c = Cursor { s: data, i: 0 };\n\tc.parse_element()\n}\n\nfn child\u003c'a\u003e(el: \u0026'a El, name: \u0026str) -\u003e Option\u003c\u0026'a El\u003e {\n\tel.children.iter().find(|c| c.name == name)\n}\n\nfn text(el: \u0026El) -\u003e String {\n\tunescape_xml(el.text.as_deref().unwrap_or(\"\"))\n}\n\nfn attr\u003c'a\u003e(el: \u0026'a El, name: \u0026str) -\u003e Option\u003c\u0026'a str\u003e {\n\tel.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())\n}\n\nfn parse_i32(s: \u0026str) -\u003e i32 {\n\ts.trim().parse().unwrap_or(0)\n}\n\nfn parse_i64(s: \u0026str) -\u003e i64 {\n\ts.trim().parse().unwrap_or(0)\n}\n\nfn parse_bool(s: \u0026str) -\u003e bool {\n\tparse_i32(s) != 0\n}\n\n/// Convert an `i32` code to a [`VideoScalingMethod`]; unknown -\u003e `Stretch`\n/// (the C++ default). `pub(crate)` so the ffi encoder layer can map the\n/// `oakcodec_encoding_params.video_scaling_method` field.\npub(crate) fn scaling_from_i32(v: i32) -\u003e VideoScalingMethod {\n\tmatch v {\n\t\t0 =\u003e VideoScalingMethod::Fit,\n\t\t1 =\u003e VideoScalingMethod::Stretch,\n\t\t2 =\u003e VideoScalingMethod::Crop,\n\t\t_ =\u003e VideoScalingMethod::Stretch,\n\t}\n}\n\n/// Convert an `i32` code to a [`SampleFormat`]; unknown -\u003e `Invalid`.\n/// `pub(crate)` so the ffi encoder layer can map the\n/// `oakcodec_encoding_params.audio_sample_format` field.\npub(crate) fn sample_format_from_i32(v: i32) -\u003e SampleFormat {\n\tmatch v {\n\t\t-1 =\u003e SampleFormat::Invalid,\n\t\t0 =\u003e SampleFormat::U8Planar,\n\t\t1 =\u003e SampleFormat::S16Planar,\n\t\t2 =\u003e SampleFormat::S32Planar,\n\t\t3 =\u003e SampleFormat::S64Planar,\n\t\t4 =\u003e SampleFormat::F32Planar,\n\t\t5 =\u003e SampleFormat::F64Planar,\n\t\t6 =\u003e SampleFormat::U8,\n\t\t7 =\u003e SampleFormat::S16,\n\t\t8 =\u003e SampleFormat::S32,\n\t\t9 =\u003e SampleFormat::S64,\n\t\t10 =\u003e SampleFormat::F32,\n\t\t11 =\u003e SampleFormat::F64,\n\t\t_ =\u003e SampleFormat::Invalid,\n\t}\n}\n\n/// NUL-terminated byte array -\u003e `\u0026str` (empty when the first byte is NUL).\nfn cstr(arr: \u0026[u8]) -\u003e \u0026str {\n\tlet end = arr.iter().position(|\u0026b| b == 0).unwrap_or(arr.len());\n\tstd::str::from_utf8(\u0026arr[..end]).unwrap_or(\"\")\n}\n\n/// Write a string into a NUL-padded byte array (truncating if too long).\nfn set_cstr(dst: \u0026mut [u8], s: \u0026str) {\n\tdst.fill(0);\n\tlet n = s.len().min(dst.len());\n\tdst[..n].copy_from_slice(\u0026s.as_bytes()[..n]);\n}\n\n/// Minimal XML escaping for text content.\nfn escape_xml(s: \u0026str) -\u003e String {\n\ts.replace('\u0026', \"\u0026amp;\")\n\t\t.replace('\u003c', \"\u0026lt;\")\n\t\t.replace('\u003e', \"\u0026gt;\")\n}\n\n/// Minimal XML unescaping for text content.\nfn unescape_xml(s: \u0026str) -\u003e String {\n\ts.replace(\"\u0026lt;\", \"\u003c\")\n\t\t.replace(\"\u0026gt;\", \"\u003e\")\n\t\t.replace(\"\u0026amp;\", \"\u0026\")\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tconst EPS: f64 = 1e-9;\n\n\tfn assert_close(a: f64, b: f64) {\n\t\tassert!(\n\t\t\t(a - b).abs() \u003c EPS,\n\t\t\t\"expected {a} close to {b} (eps {EPS})\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn default_has_expected_values() {\n\t\tlet p = EncodingParams::default();\n\t\tassert_eq!(p.filename, [0; 1024]);\n\t\tassert_eq!(p.format, -1);\n\t\tassert_eq!(p.video_enabled, 0);\n\t\tassert_eq!(p.video_codec, -1);\n\t\tassert_eq!(p.video_width, 0);\n\t\tassert_eq!(p.video_height, 0);\n\t\tassert_eq!(p.video_time_base_num, 1);\n\t\tassert_eq!(p.video_time_base_den, 1);\n\t\tassert_eq!(p.video_pixel_format, PixelFormat::Invalid);\n\t\tassert_eq!(p.video_interlacing, 0);\n\t\tassert_eq!(p.video_pixel_aspect_num, 0);\n\t\tassert_eq!(p.video_pixel_aspect_den, 0);\n\t\tassert_eq!(p.video_bit_rate, 0);\n\t\tassert_eq!(p.video_min_bit_rate, 0);\n\t\tassert_eq!(p.video_max_bit_rate, 0);\n\t\tassert_eq!(p.video_buffer_size, 0);\n\t\tassert_eq!(p.video_threads, 0);\n\t\tassert_eq!(p.video_pix_fmt, [0; 64]);\n\t\tassert_eq!(p.video_is_image_sequence, 0);\n\t\tassert_eq!(p.video_scaling_method, VideoScalingMethod::Stretch);\n\t\tassert_eq!(p.audio_enabled, 0);\n\t\tassert_eq!(p.audio_codec, -1);\n\t\tassert_eq!(p.audio_sample_rate, 0);\n\t\tassert_eq!(p.audio_channel_layout, 0);\n\t\tassert_eq!(p.audio_sample_format, SampleFormat::Invalid);\n\t\tassert_eq!(p.audio_bit_rate, 0);\n\t\tassert_eq!(p.subtitles_enabled, 0);\n\t\tassert_eq!(p.subtitles_codec, -1);\n\t\tassert_eq!(p.subtitles_are_sidecar, 0);\n\t\tassert_eq!(p.subtitles_sidecar_format, -1);\n\t\tassert_eq!(p.color_transform_output, [0; 256]);\n\t\tassert_eq!(p.export_length_num, 0);\n\t\tassert_eq!(p.export_length_den, 0);\n\t\tassert_eq!(p.has_custom_range, 0);\n\t\tassert_eq!(p.custom_range_in_num, 0);\n\t\tassert_eq!(p.custom_range_in_den, 0);\n\t\tassert_eq!(p.custom_range_out_num, 0);\n\t\tassert_eq!(p.custom_range_out_den, 0);\n\t}\n\n\t#[test]\n\tfn extension_mapping() {\n\t\tlet mut p = EncodingParams::default();\n\t\tlet cases: [(i32, \u0026str); 15] = [\n\t\t\t(0, \"mxf\"),\n\t\t\t(1, \"mkv\"),\n\t\t\t(2, \"mp4\"),\n\t\t\t(3, \"exr\"),\n\t\t\t(4, \"mov\"),\n\t\t\t(5, \"png\"),\n\t\t\t(6, \"tiff\"),\n\t\t\t(7, \"wav\"),\n\t\t\t(8, \"aiff\"),\n\t\t\t(9, \"mp3\"),\n\t\t\t(10, \"flac\"),\n\t\t\t(11, \"ogg\"),\n\t\t\t(12, \"webm\"),\n\t\t\t(13, \"srt\"),\n\t\t\t(14, \"m4a\"),\n\t\t];\n\t\tfor (fmt, ext) in cases {\n\t\t\tp.format = fmt;\n\t\t\tassert_eq!(p.extension(), ext, \"format {fmt}\");\n\t\t}\n\t\t// Unknown / invalid formats map to the empty string.\n\t\tp.format = 99;\n\t\tassert_eq!(p.extension(), \"\");\n\t\tp.format = -3;\n\t\tassert_eq!(p.extension(), \"\");\n\t}\n\n\t#[test]\n\tfn generate_matrix_stretch_is_identity() {\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Stretch,\n\t\t\t1920,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\t}\n\n\t#[test]\n\tfn generate_matrix_equal_aspect_is_identity() {\n\t\t// Same 16:9 aspect: export_ar == source_ar -\u003e fuzzy-equal -\u003e identity.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Fit,\n\t\t\t1920,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\t}\n\n\t#[test]\n\tfn generate_matrix_fit_scales_x() {\n\t\t// src square (ar 1.0) -\u003e dst 2:1 (ar 2.0). Fit: source wider/narrower\n\t\t// relative to export -\u003e the x axis is squeezed to source_ar/export_ar.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Fit,\n\t\t\t1000,\n\t\t\t1000,\n\t\t\t2000,\n\t\t\t1000,\n\t\t\t\u0026mut out,\n\t\t);\n\t\t// export_ar(2.0) \u003e source_ar(1.0); Fit =\u003e scale(source_ar/export_ar, 1).\n\t\tassert_close(out[0], 0.5);\n\t\tassert_close(out[5], 1.0);\n\t}\n\n\t#[test]\n\tfn generate_matrix_crop_scales_y() {\n\t\t// src square -\u003e dst 2:1. Crop: fit inside, so we zoom the y axis by\n\t\t// export_ar/source_ar and keep x unscaled.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Crop,\n\t\t\t1000,\n\t\t\t1000,\n\t\t\t2000,\n\t\t\t1000,\n\t\t\t\u0026mut out,\n\t\t);\n\t\t// export_ar(2.0) \u003e source_ar(1.0); not Fit =\u003e scale(1, export_ar/source_ar).\n\t\tassert_close(out[0], 1.0);\n\t\tassert_close(out[5], 2.0);\n\t}\n\n\t#[test]\n\tfn generate_matrix_crop_scales_x() {\n\t\t// src square -\u003e dst 0.5:1 (ar 0.5). Crop: export narrower than source,\n\t\t// zoom the x axis by source_ar/export_ar.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Crop,\n\t\t\t1000,\n\t\t\t1000,\n\t\t\t500,\n\t\t\t1000,\n\t\t\t\u0026mut out,\n\t\t);\n\t\t// export_ar(0.5) \u003c source_ar(1.0); (ar\u003esource) == false, (method==Fit)\n\t\t// false =\u003e false == false true =\u003e scale(source_ar/export_ar, 1) = 2.0.\n\t\tassert_close(out[0], 2.0);\n\t\tassert_close(out[5], 1.0);\n\t}\n\n\t#[test]\n\tfn generate_matrix_degenerate_is_identity() {\n\t\t// Zero / negative source sizes must not produce inf/NaN.\n\t\tlet mut out = [9.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Fit,\n\t\t\t0,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\n\t\tlet mut out = [9.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Crop,\n\t\t\t-1,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\t}\n\n\t#[test]\n\tfn save_load_roundtrip() {\n\t\tlet mut p = EncodingParams::default();\n\t\tp.filename = [0; 1024];\n\t\tlet name = b\"out/video.mp4\";\n\t\tp.filename[..name.len()].copy_from_slice(name);\n\t\tp.format = 2;\n\t\tp.video_enabled = 1;\n\t\tp.video_codec = 3;\n\t\tp.video_width = 1280;\n\t\tp.video_height = 720;\n\t\tp.video_time_base_num = 1;\n\t\tp.video_time_base_den = 30;\n\t\tp.video_pixel_format = PixelFormat::U8;\n\t\tp.video_interlacing = 1;\n\t\tp.video_pixel_aspect_num = 1;\n\t\tp.video_pixel_aspect_den = 1;\n\t\tp.video_bit_rate = 8_000_000;\n\t\tp.video_min_bit_rate = 4_000_000;\n\t\tp.video_max_bit_rate = 16_000_000;\n\t\tp.video_buffer_size = 4_000_000;\n\t\tp.video_threads = 4;\n\t\tlet pix = b\"yuv420p\";\n\t\tp.video_pix_fmt[..pix.len()].copy_from_slice(pix);\n\t\tp.video_is_image_sequence = 1;\n\t\tp.video_scaling_method = VideoScalingMethod::Crop;\n\n\t\tp.audio_enabled = 1;\n\t\tp.audio_codec = 1;\n\t\tp.audio_sample_rate = 48000;\n\t\tp.audio_channel_layout = 3; // stereo mask\n\t\tp.audio_sample_format = SampleFormat::F32Planar;\n\t\tp.audio_bit_rate = 320_000;\n\n\t\tp.subtitles_enabled = 1;\n\t\tp.subtitles_codec = 1;\n\t\tp.subtitles_are_sidecar = 1;\n\t\tp.subtitles_sidecar_format = 13; // srt\n\n\t\tlet xml = p.save_to_string();\n\t\tassert!(xml.starts_with(\"\u003cexport version=\\\"1\\\"\u003e\"), \"xml: {xml}\");\n\n\t\tlet mut q = EncodingParams::default();\n\t\tq.load(\u0026xml).expect(\"load round-trip\");\n\n\t\tassert_eq!(q.filename[..name.len()], name[..]);\n\t\tassert_eq!(q.format, 2);\n\t\tassert_eq!(q.video_enabled, 1);\n\t\tassert_eq!(q.video_codec, 3);\n\t\tassert_eq!(q.video_width, 1280);\n\t\tassert_eq!(q.video_height, 720);\n\t\tassert_eq!(q.video_time_base_num, 1);\n\t\tassert_eq!(q.video_time_base_den, 30);\n\t\tassert_eq!(q.video_pixel_format, PixelFormat::U8);\n\t\tassert_eq!(q.video_interlacing, 1);\n\t\tassert_eq!(q.video_pixel_aspect_num, 1);\n\t\tassert_eq!(q.video_pixel_aspect_den, 1);\n\t\tassert_eq!(q.video_bit_rate, 8_000_000);\n\t\tassert_eq!(q.video_min_bit_rate, 4_000_000);\n\t\tassert_eq!(q.video_max_bit_rate, 16_000_000);\n\t\tassert_eq!(q.video_buffer_size, 4_000_000);\n\t\tassert_eq!(q.video_threads, 4);\n\t\tassert_eq!(q.video_pix_fmt[..pix.len()], pix[..]);\n\t\tassert_eq!(q.video_is_image_sequence, 1);\n\t\tassert_eq!(q.video_scaling_method, VideoScalingMethod::Crop);\n\n\t\tassert_eq!(q.audio_enabled, 1);\n\t\tassert_eq!(q.audio_codec, 1);\n\t\tassert_eq!(q.audio_sample_rate, 48000);\n\t\tassert_eq!(q.audio_channel_layout, 3);\n\t\tassert_eq!(q.audio_sample_format, SampleFormat::F32Planar);\n\t\tassert_eq!(q.audio_bit_rate, 320_000);\n\n\t\tassert_eq!(q.subtitles_enabled, 1);\n\t\tassert_eq!(q.subtitles_codec, 1);\n\t\tassert_eq!(q.subtitles_are_sidecar, 1);\n\t\tassert_eq!(q.subtitles_sidecar_format, 13);\n\t}\n\n\t#[test]\n\tfn load_malformed_returns_err() {\n\t\tlet mut p = EncodingParams::default();\n\t\tassert!(p.load(\"not xml at all\").is_err());\n\t\tassert!(p.load(\"\u003cunclosed\").is_err());\n\t\tassert!(p.load(\"\u003cexport\u003e\u003cvideo enabled=\\\"1\\\"\u003e\u003c/export\u003e\").is_err());\n\t}\n\n\t#[test]\n\tfn load_non_export_returns_err() {\n\t\tlet mut p = EncodingParams::default();\n\t\tassert!(p.load(\"\u003cfoo/\u003e\").is_err());\n\t}\n\n\t#[test]\n\tfn custom_range_not_assigned_on_load() {\n\t\t// The C++ load_v1 bug: custom range is parsed but never assigned.\n\t\tlet mut p = EncodingParams::default();\n\t\tp.has_custom_range = 1;\n\t\tp.custom_range_in_num = 5;\n\t\tp.custom_range_in_den = 1;\n\t\tp.custom_range_out_num = 10;\n\t\tp.custom_range_out_den = 1;\n\t\tlet xml = p.save_to_string();\n\n\t\tlet mut q = EncodingParams::default();\n\t\tq.load(\u0026xml).expect(\"load\");\n\t\t// Fields stay at their defaults despite being serialized.\n\t\tassert_eq!(q.has_custom_range, 0);\n\t\tassert_eq!(q.custom_range_in_num, 0);\n\t\tassert_eq!(q.custom_range_in_den, 0);\n\t\tassert_eq!(q.custom_range_out_num, 0);\n\t\tassert_eq!(q.custom_range_out_den, 0);\n\t}\n\n\t#[test]\n\tfn cstr_set_cstr_roundtrip_and_truncate() {\n\t\tlet mut arr = [0u8; 8];\n\t\tset_cstr(\u0026mut arr, \"hello\");\n\t\tassert_eq!(cstr(\u0026arr), \"hello\");\n\t\tassert_eq!(arr[5], 0);\n\n\t\t// Longer than the buffer -\u003e truncated to the first 8 bytes (no\n\t\t// terminator required when the buffer is exactly full).\n\t\tset_cstr(\u0026mut arr, \"this string is far too long\");\n\t\tassert_eq!(cstr(\u0026arr), \"this str\");\n\t\tassert_eq!(arr.len(), 8);\n\t\tassert_eq!(\u0026arr[..], b\"this str\");\n\n\t\t// Empty -\u003e all NUL.\n\t\tset_cstr(\u0026mut arr, \"\");\n\t\tassert_eq!(arr, [0u8; 8]);\n\t\tassert_eq!(cstr(\u0026arr), \"\");\n\t}\n\n\t#[test]\n\tfn xml_escaping_roundtrips() {\n\t\tlet s = \"a\u0026b\u003cc\u003ed\";\n\t\tassert_eq!(unescape_xml(\u0026escape_xml(s)), s);\n\t\tassert_eq!(escape_xml(\"plain\"), \"plain\");\n\t}\n\n\tfn identity() -\u003e [f64; 16] {\n\t\t[\n\t\t\t1.0, 0.0, 0.0, 0.0, //\n\t\t\t0.0, 1.0, 0.0, 0.0, //\n\t\t\t0.0, 0.0, 1.0, 0.0, //\n\t\t\t0.0, 0.0, 0.0, 1.0,\n\t\t]\n\t}\n\n\t/// `oakcodec_encoding_params` byte-level layout lock, verified against\n\t/// the real header with a C++ `offsetof` probe (see the crate notes):\n\t/// every field offset and the total size must match `include/codec/\n\t/// encoder.h` exactly so a C caller's POD is read in place.\n\t#[test]\n\tfn encoding_params_c_abi_layout() {\n\t\tuse std::mem::{offset_of, size_of};\n\n\t\tassert_eq!(size_of::\u003cEncodingParams\u003e(), 1536);\n\t\tassert_eq!(offset_of!(EncodingParams, filename), 0);\n\t\tassert_eq!(offset_of!(EncodingParams, format), 1024);\n\t\tassert_eq!(offset_of!(EncodingParams, video_enabled), 1028);\n\t\tassert_eq!(offset_of!(EncodingParams, video_pixel_aspect_den), 1064);\n\t\tassert_eq!(offset_of!(EncodingParams, video_bit_rate), 1072);\n\t\tassert_eq!(offset_of!(EncodingParams, video_pix_fmt), 1108);\n\t\tassert_eq!(offset_of!(EncodingParams, video_is_image_sequence), 1172);\n\t\tassert_eq!(offset_of!(EncodingParams, audio_channel_layout), 1192);\n\t\tassert_eq!(offset_of!(EncodingParams, audio_bit_rate), 1208);\n\t\tassert_eq!(offset_of!(EncodingParams, color_transform_output), 1232);\n\t\tassert_eq!(offset_of!(EncodingParams, has_custom_range), 1496);\n\t\tassert_eq!(offset_of!(EncodingParams, custom_range_in_num), 1504);\n\t\tassert_eq!(offset_of!(EncodingParams, custom_range_out_den), 1528);\n\t}\n}\n","traces":[{"line":127,"address":[],"length":0,"stats":{"Line":24}},{"line":129,"address":[],"length":0,"stats":{"Line":48}},{"line":130,"address":[],"length":0,"stats":{"Line":24}},{"line":133,"address":[],"length":0,"stats":{"Line":24}},{"line":147,"address":[],"length":0,"stats":{"Line":48}},{"line":152,"address":[],"length":0,"stats":{"Line":24}},{"line":159,"address":[],"length":0,"stats":{"Line":24}},{"line":161,"address":[],"length":0,"stats":{"Line":24}},{"line":163,"address":[],"length":0,"stats":{"Line":24}},{"line":183,"address":[],"length":0,"stats":{"Line":9}},{"line":192,"address":[],"length":0,"stats":{"Line":9}},{"line":193,"address":[],"length":0,"stats":{"Line":9}},{"line":194,"address":[],"length":0,"stats":{"Line":9}},{"line":195,"address":[],"length":0,"stats":{"Line":9}},{"line":196,"address":[],"length":0,"stats":{"Line":9}},{"line":199,"address":[],"length":0,"stats":{"Line":9}},{"line":200,"address":[],"length":0,"stats":{"Line":2}},{"line":205,"address":[],"length":0,"stats":{"Line":22}},{"line":206,"address":[],"length":0,"stats":{"Line":2}},{"line":209,"address":[],"length":0,"stats":{"Line":10}},{"line":210,"address":[],"length":0,"stats":{"Line":10}},{"line":213,"address":[],"length":0,"stats":{"Line":10}},{"line":214,"address":[],"length":0,"stats":{"Line":20}},{"line":216,"address":[],"length":0,"stats":{"Line":1}},{"line":219,"address":[],"length":0,"stats":{"Line":11}},{"line":221,"address":[],"length":0,"stats":{"Line":6}},{"line":224,"address":[],"length":0,"stats":{"Line":2}},{"line":234,"address":[],"length":0,"stats":{"Line":17}},{"line":235,"address":[],"length":0,"stats":{"Line":17}},{"line":236,"address":[],"length":0,"stats":{"Line":1}},{"line":237,"address":[],"length":0,"stats":{"Line":1}},{"line":238,"address":[],"length":0,"stats":{"Line":1}},{"line":239,"address":[],"length":0,"stats":{"Line":1}},{"line":240,"address":[],"length":0,"stats":{"Line":1}},{"line":241,"address":[],"length":0,"stats":{"Line":1}},{"line":242,"address":[],"length":0,"stats":{"Line":1}},{"line":243,"address":[],"length":0,"stats":{"Line":1}},{"line":244,"address":[],"length":0,"stats":{"Line":1}},{"line":245,"address":[],"length":0,"stats":{"Line":1}},{"line":246,"address":[],"length":0,"stats":{"Line":1}},{"line":247,"address":[],"length":0,"stats":{"Line":1}},{"line":248,"address":[],"length":0,"stats":{"Line":1}},{"line":249,"address":[],"length":0,"stats":{"Line":1}},{"line":250,"address":[],"length":0,"stats":{"Line":1}},{"line":251,"address":[],"length":0,"stats":{"Line":2}},{"line":262,"address":[],"length":0,"stats":{"Line":6}},{"line":263,"address":[],"length":0,"stats":{"Line":15}},{"line":264,"address":[],"length":0,"stats":{"Line":12}},{"line":265,"address":[],"length":0,"stats":{"Line":3}},{"line":266,"address":[],"length":0,"stats":{"Line":1}},{"line":267,"address":[],"length":0,"stats":{"Line":1}},{"line":274,"address":[],"length":0,"stats":{"Line":8}},{"line":275,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":8}},{"line":278,"address":[],"length":0,"stats":{"Line":2}},{"line":280,"address":[],"length":0,"stats":{"Line":8}},{"line":281,"address":[],"length":0,"stats":{"Line":2}},{"line":284,"address":[],"length":0,"stats":{"Line":8}},{"line":285,"address":[],"length":0,"stats":{"Line":6}},{"line":287,"address":[],"length":0,"stats":{"Line":8}},{"line":288,"address":[],"length":0,"stats":{"Line":4}},{"line":291,"address":[],"length":0,"stats":{"Line":6}},{"line":292,"address":[],"length":0,"stats":{"Line":8}},{"line":293,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":6}},{"line":296,"address":[],"length":0,"stats":{"Line":2}},{"line":298,"address":[],"length":0,"stats":{"Line":6}},{"line":299,"address":[],"length":0,"stats":{"Line":2}},{"line":301,"address":[],"length":0,"stats":{"Line":6}},{"line":302,"address":[],"length":0,"stats":{"Line":2}},{"line":304,"address":[],"length":0,"stats":{"Line":5}},{"line":305,"address":[],"length":0,"stats":{"Line":2}},{"line":306,"address":[],"length":0,"stats":{"Line":0}},{"line":307,"address":[],"length":0,"stats":{"Line":1}},{"line":308,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":0}},{"line":310,"address":[],"length":0,"stats":{"Line":0}},{"line":311,"address":[],"length":0,"stats":{"Line":0}},{"line":312,"address":[],"length":0,"stats":{"Line":0}},{"line":315,"address":[],"length":0,"stats":{"Line":5}},{"line":316,"address":[],"length":0,"stats":{"Line":3}},{"line":317,"address":[],"length":0,"stats":{"Line":4}},{"line":318,"address":[],"length":0,"stats":{"Line":2}},{"line":319,"address":[],"length":0,"stats":{"Line":1}},{"line":322,"address":[],"length":0,"stats":{"Line":6}},{"line":323,"address":[],"length":0,"stats":{"Line":2}},{"line":325,"address":[],"length":0,"stats":{"Line":5}},{"line":326,"address":[],"length":0,"stats":{"Line":3}},{"line":327,"address":[],"length":0,"stats":{"Line":4}},{"line":328,"address":[],"length":0,"stats":{"Line":2}},{"line":329,"address":[],"length":0,"stats":{"Line":1}},{"line":332,"address":[],"length":0,"stats":{"Line":6}},{"line":333,"address":[],"length":0,"stats":{"Line":2}},{"line":335,"address":[],"length":0,"stats":{"Line":6}},{"line":336,"address":[],"length":0,"stats":{"Line":2}},{"line":338,"address":[],"length":0,"stats":{"Line":6}},{"line":339,"address":[],"length":0,"stats":{"Line":2}},{"line":341,"address":[],"length":0,"stats":{"Line":6}},{"line":342,"address":[],"length":0,"stats":{"Line":2}},{"line":344,"address":[],"length":0,"stats":{"Line":6}},{"line":345,"address":[],"length":0,"stats":{"Line":2}},{"line":347,"address":[],"length":0,"stats":{"Line":6}},{"line":348,"address":[],"length":0,"stats":{"Line":3}},{"line":350,"address":[],"length":0,"stats":{"Line":6}},{"line":351,"address":[],"length":0,"stats":{"Line":2}},{"line":353,"address":[],"length":0,"stats":{"Line":6}},{"line":354,"address":[],"length":0,"stats":{"Line":3}},{"line":358,"address":[],"length":0,"stats":{"Line":6}},{"line":359,"address":[],"length":0,"stats":{"Line":8}},{"line":360,"address":[],"length":0,"stats":{"Line":2}},{"line":362,"address":[],"length":0,"stats":{"Line":6}},{"line":363,"address":[],"length":0,"stats":{"Line":2}},{"line":365,"address":[],"length":0,"stats":{"Line":6}},{"line":366,"address":[],"length":0,"stats":{"Line":2}},{"line":368,"address":[],"length":0,"stats":{"Line":6}},{"line":369,"address":[],"length":0,"stats":{"Line":2}},{"line":371,"address":[],"length":0,"stats":{"Line":6}},{"line":372,"address":[],"length":0,"stats":{"Line":3}},{"line":374,"address":[],"length":0,"stats":{"Line":6}},{"line":375,"address":[],"length":0,"stats":{"Line":2}},{"line":379,"address":[],"length":0,"stats":{"Line":6}},{"line":380,"address":[],"length":0,"stats":{"Line":8}},{"line":381,"address":[],"length":0,"stats":{"Line":2}},{"line":383,"address":[],"length":0,"stats":{"Line":6}},{"line":384,"address":[],"length":0,"stats":{"Line":2}},{"line":386,"address":[],"length":0,"stats":{"Line":6}},{"line":387,"address":[],"length":0,"stats":{"Line":2}},{"line":389,"address":[],"length":0,"stats":{"Line":6}},{"line":390,"address":[],"length":0,"stats":{"Line":2}},{"line":394,"address":[],"length":0,"stats":{"Line":2}},{"line":404,"address":[],"length":0,"stats":{"Line":2}},{"line":405,"address":[],"length":0,"stats":{"Line":4}},{"line":406,"address":[],"length":0,"stats":{"Line":6}},{"line":407,"address":[],"length":0,"stats":{"Line":10}},{"line":408,"address":[],"length":0,"stats":{"Line":6}},{"line":409,"address":[],"length":0,"stats":{"Line":6}},{"line":410,"address":[],"length":0,"stats":{"Line":6}},{"line":411,"address":[],"length":0,"stats":{"Line":2}},{"line":412,"address":[],"length":0,"stats":{"Line":2}},{"line":414,"address":[],"length":0,"stats":{"Line":6}},{"line":415,"address":[],"length":0,"stats":{"Line":2}},{"line":416,"address":[],"length":0,"stats":{"Line":2}},{"line":419,"address":[],"length":0,"stats":{"Line":6}},{"line":420,"address":[],"length":0,"stats":{"Line":2}},{"line":421,"address":[],"length":0,"stats":{"Line":2}},{"line":423,"address":[],"length":0,"stats":{"Line":3}},{"line":424,"address":[],"length":0,"stats":{"Line":4}},{"line":425,"address":[],"length":0,"stats":{"Line":4}},{"line":426,"address":[],"length":0,"stats":{"Line":4}},{"line":427,"address":[],"length":0,"stats":{"Line":4}},{"line":428,"address":[],"length":0,"stats":{"Line":2}},{"line":429,"address":[],"length":0,"stats":{"Line":2}},{"line":431,"address":[],"length":0,"stats":{"Line":4}},{"line":432,"address":[],"length":0,"stats":{"Line":2}},{"line":433,"address":[],"length":0,"stats":{"Line":2}},{"line":435,"address":[],"length":0,"stats":{"Line":4}},{"line":436,"address":[],"length":0,"stats":{"Line":2}},{"line":437,"address":[],"length":0,"stats":{"Line":2}},{"line":439,"address":[],"length":0,"stats":{"Line":4}},{"line":440,"address":[],"length":0,"stats":{"Line":2}},{"line":441,"address":[],"length":0,"stats":{"Line":2}},{"line":443,"address":[],"length":0,"stats":{"Line":4}},{"line":444,"address":[],"length":0,"stats":{"Line":4}},{"line":445,"address":[],"length":0,"stats":{"Line":2}},{"line":446,"address":[],"length":0,"stats":{"Line":2}},{"line":448,"address":[],"length":0,"stats":{"Line":4}},{"line":449,"address":[],"length":0,"stats":{"Line":2}},{"line":450,"address":[],"length":0,"stats":{"Line":2}},{"line":452,"address":[],"length":0,"stats":{"Line":4}},{"line":453,"address":[],"length":0,"stats":{"Line":2}},{"line":454,"address":[],"length":0,"stats":{"Line":2}},{"line":456,"address":[],"length":0,"stats":{"Line":4}},{"line":457,"address":[],"length":0,"stats":{"Line":6}},{"line":458,"address":[],"length":0,"stats":{"Line":4}},{"line":459,"address":[],"length":0,"stats":{"Line":2}},{"line":460,"address":[],"length":0,"stats":{"Line":2}},{"line":462,"address":[],"length":0,"stats":{"Line":3}},{"line":463,"address":[],"length":0,"stats":{"Line":1}},{"line":464,"address":[],"length":0,"stats":{"Line":1}},{"line":467,"address":[],"length":0,"stats":{"Line":6}},{"line":469,"address":[],"length":0,"stats":{"Line":6}},{"line":470,"address":[],"length":0,"stats":{"Line":2}},{"line":471,"address":[],"length":0,"stats":{"Line":2}},{"line":473,"address":[],"length":0,"stats":{"Line":3}},{"line":474,"address":[],"length":0,"stats":{"Line":4}},{"line":475,"address":[],"length":0,"stats":{"Line":4}},{"line":476,"address":[],"length":0,"stats":{"Line":2}},{"line":477,"address":[],"length":0,"stats":{"Line":2}},{"line":479,"address":[],"length":0,"stats":{"Line":4}},{"line":480,"address":[],"length":0,"stats":{"Line":2}},{"line":481,"address":[],"length":0,"stats":{"Line":2}},{"line":483,"address":[],"length":0,"stats":{"Line":4}},{"line":484,"address":[],"length":0,"stats":{"Line":2}},{"line":485,"address":[],"length":0,"stats":{"Line":2}},{"line":487,"address":[],"length":0,"stats":{"Line":2}},{"line":489,"address":[],"length":0,"stats":{"Line":6}},{"line":491,"address":[],"length":0,"stats":{"Line":6}},{"line":492,"address":[],"length":0,"stats":{"Line":2}},{"line":493,"address":[],"length":0,"stats":{"Line":2}},{"line":495,"address":[],"length":0,"stats":{"Line":3}},{"line":496,"address":[],"length":0,"stats":{"Line":4}},{"line":497,"address":[],"length":0,"stats":{"Line":2}},{"line":498,"address":[],"length":0,"stats":{"Line":2}},{"line":500,"address":[],"length":0,"stats":{"Line":4}},{"line":501,"address":[],"length":0,"stats":{"Line":2}},{"line":502,"address":[],"length":0,"stats":{"Line":2}},{"line":504,"address":[],"length":0,"stats":{"Line":2}},{"line":505,"address":[],"length":0,"stats":{"Line":1}},{"line":506,"address":[],"length":0,"stats":{"Line":1}},{"line":509,"address":[],"length":0,"stats":{"Line":6}},{"line":511,"address":[],"length":0,"stats":{"Line":6}},{"line":512,"address":[],"length":0,"stats":{"Line":2}},{"line":548,"address":[],"length":0,"stats":{"Line":1167}},{"line":549,"address":[],"length":0,"stats":{"Line":2334}},{"line":552,"address":[],"length":0,"stats":{"Line":340}},{"line":553,"address":[],"length":0,"stats":{"Line":1056}},{"line":554,"address":[],"length":0,"stats":{"Line":9}},{"line":558,"address":[],"length":0,"stats":{"Line":543}},{"line":559,"address":[],"length":0,"stats":{"Line":1629}},{"line":562,"address":[],"length":0,"stats":{"Line":97}},{"line":563,"address":[],"length":0,"stats":{"Line":194}},{"line":564,"address":[],"length":0,"stats":{"Line":1634}},{"line":565,"address":[],"length":0,"stats":{"Line":2543}},{"line":566,"address":[],"length":0,"stats":{"Line":721}},{"line":568,"address":[],"length":0,"stats":{"Line":95}},{"line":571,"address":[],"length":0,"stats":{"Line":97}},{"line":572,"address":[],"length":0,"stats":{"Line":1}},{"line":574,"address":[],"length":0,"stats":{"Line":192}},{"line":577,"address":[],"length":0,"stats":{"Line":9}},{"line":578,"address":[],"length":0,"stats":{"Line":18}},{"line":579,"address":[],"length":0,"stats":{"Line":18}},{"line":580,"address":[],"length":0,"stats":{"Line":0}},{"line":582,"address":[],"length":0,"stats":{"Line":9}},{"line":583,"address":[],"length":0,"stats":{"Line":18}},{"line":584,"address":[],"length":0,"stats":{"Line":81}},{"line":585,"address":[],"length":0,"stats":{"Line":9}},{"line":587,"address":[],"length":0,"stats":{"Line":18}},{"line":588,"address":[],"length":0,"stats":{"Line":0}},{"line":590,"address":[],"length":0,"stats":{"Line":36}},{"line":591,"address":[],"length":0,"stats":{"Line":9}},{"line":592,"address":[],"length":0,"stats":{"Line":9}},{"line":595,"address":[],"length":0,"stats":{"Line":42}},{"line":596,"address":[],"length":0,"stats":{"Line":84}},{"line":597,"address":[],"length":0,"stats":{"Line":84}},{"line":598,"address":[],"length":0,"stats":{"Line":0}},{"line":600,"address":[],"length":0,"stats":{"Line":42}},{"line":601,"address":[],"length":0,"stats":{"Line":126}},{"line":602,"address":[],"length":0,"stats":{"Line":84}},{"line":603,"address":[],"length":0,"stats":{"Line":84}},{"line":604,"address":[],"length":0,"stats":{"Line":0}},{"line":606,"address":[],"length":0,"stats":{"Line":42}},{"line":607,"address":[],"length":0,"stats":{"Line":42}},{"line":608,"address":[],"length":0,"stats":{"Line":1}},{"line":610,"address":[],"length":0,"stats":{"Line":41}},{"line":613,"address":[],"length":0,"stats":{"Line":46}},{"line":614,"address":[],"length":0,"stats":{"Line":92}},{"line":615,"address":[],"length":0,"stats":{"Line":92}},{"line":616,"address":[],"length":0,"stats":{"Line":1}},{"line":618,"address":[],"length":0,"stats":{"Line":45}},{"line":619,"address":[],"length":0,"stats":{"Line":135}},{"line":620,"address":[],"length":0,"stats":{"Line":90}},{"line":621,"address":[],"length":0,"stats":{"Line":90}},{"line":623,"address":[],"length":0,"stats":{"Line":108}},{"line":624,"address":[],"length":0,"stats":{"Line":162}},{"line":625,"address":[],"length":0,"stats":{"Line":1}},{"line":626,"address":[],"length":0,"stats":{"Line":3}},{"line":628,"address":[],"length":0,"stats":{"Line":159}},{"line":629,"address":[],"length":0,"stats":{"Line":43}},{"line":630,"address":[],"length":0,"stats":{"Line":43}},{"line":632,"address":[],"length":0,"stats":{"Line":30}},{"line":633,"address":[],"length":0,"stats":{"Line":18}},{"line":634,"address":[],"length":0,"stats":{"Line":36}},{"line":635,"address":[],"length":0,"stats":{"Line":9}},{"line":637,"address":[],"length":0,"stats":{"Line":27}},{"line":638,"address":[],"length":0,"stats":{"Line":27}},{"line":641,"address":[],"length":0,"stats":{"Line":86}},{"line":642,"address":[],"length":0,"stats":{"Line":215}},{"line":643,"address":[],"length":0,"stats":{"Line":22}},{"line":645,"address":[],"length":0,"stats":{"Line":100}},{"line":646,"address":[],"length":0,"stats":{"Line":100}},{"line":647,"address":[],"length":0,"stats":{"Line":0}},{"line":649,"address":[],"length":0,"stats":{"Line":150}},{"line":650,"address":[],"length":0,"stats":{"Line":10}},{"line":652,"address":[],"length":0,"stats":{"Line":120}},{"line":653,"address":[],"length":0,"stats":{"Line":160}},{"line":655,"address":[],"length":0,"stats":{"Line":0}},{"line":658,"address":[],"length":0,"stats":{"Line":31}},{"line":659,"address":[],"length":0,"stats":{"Line":27}},{"line":661,"address":[],"length":0,"stats":{"Line":64}},{"line":662,"address":[],"length":0,"stats":{"Line":653}},{"line":663,"address":[],"length":0,"stats":{"Line":105}},{"line":665,"address":[],"length":0,"stats":{"Line":128}},{"line":666,"address":[],"length":0,"stats":{"Line":96}},{"line":667,"address":[],"length":0,"stats":{"Line":96}},{"line":673,"address":[],"length":0,"stats":{"Line":6}},{"line":674,"address":[],"length":0,"stats":{"Line":12}},{"line":675,"address":[],"length":0,"stats":{"Line":12}},{"line":678,"address":[],"length":0,"stats":{"Line":62}},{"line":679,"address":[],"length":0,"stats":{"Line":550}},{"line":682,"address":[],"length":0,"stats":{"Line":33}},{"line":683,"address":[],"length":0,"stats":{"Line":132}},{"line":686,"address":[],"length":0,"stats":{"Line":6}},{"line":687,"address":[],"length":0,"stats":{"Line":42}},{"line":690,"address":[],"length":0,"stats":{"Line":28}},{"line":691,"address":[],"length":0,"stats":{"Line":84}},{"line":694,"address":[],"length":0,"stats":{"Line":6}},{"line":695,"address":[],"length":0,"stats":{"Line":18}},{"line":698,"address":[],"length":0,"stats":{"Line":10}},{"line":699,"address":[],"length":0,"stats":{"Line":10}},{"line":705,"address":[],"length":0,"stats":{"Line":10}},{"line":706,"address":[],"length":0,"stats":{"Line":10}},{"line":707,"address":[],"length":0,"stats":{"Line":8}},{"line":708,"address":[],"length":0,"stats":{"Line":1}},{"line":709,"address":[],"length":0,"stats":{"Line":1}},{"line":710,"address":[],"length":0,"stats":{"Line":0}},{"line":717,"address":[],"length":0,"stats":{"Line":9}},{"line":718,"address":[],"length":0,"stats":{"Line":9}},{"line":719,"address":[],"length":0,"stats":{"Line":0}},{"line":720,"address":[],"length":0,"stats":{"Line":1}},{"line":721,"address":[],"length":0,"stats":{"Line":0}},{"line":722,"address":[],"length":0,"stats":{"Line":0}},{"line":723,"address":[],"length":0,"stats":{"Line":0}},{"line":724,"address":[],"length":0,"stats":{"Line":2}},{"line":725,"address":[],"length":0,"stats":{"Line":0}},{"line":726,"address":[],"length":0,"stats":{"Line":0}},{"line":727,"address":[],"length":0,"stats":{"Line":0}},{"line":728,"address":[],"length":0,"stats":{"Line":0}},{"line":729,"address":[],"length":0,"stats":{"Line":0}},{"line":730,"address":[],"length":0,"stats":{"Line":6}},{"line":731,"address":[],"length":0,"stats":{"Line":0}},{"line":732,"address":[],"length":0,"stats":{"Line":0}},{"line":737,"address":[],"length":0,"stats":{"Line":6}},{"line":738,"address":[],"length":0,"stats":{"Line":74}},{"line":739,"address":[],"length":0,"stats":{"Line":18}},{"line":743,"address":[],"length":0,"stats":{"Line":6}},{"line":744,"address":[],"length":0,"stats":{"Line":12}},{"line":745,"address":[],"length":0,"stats":{"Line":36}},{"line":746,"address":[],"length":0,"stats":{"Line":18}},{"line":750,"address":[],"length":0,"stats":{"Line":5}},{"line":751,"address":[],"length":0,"stats":{"Line":20}},{"line":757,"address":[],"length":0,"stats":{"Line":34}},{"line":758,"address":[],"length":0,"stats":{"Line":136}}],"covered":318,"coverable":342},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","error.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Error codes, mirroring `include/codec/error.h` verbatim; project-wide\n//! -MMCCCC scheme (module registry in include/common/error.h), pass-through untranslated.\n\n/// Success.\npub const OAKCODEC_OK: i32 = 0;\n/// Null handle or invalid argument.\npub const OAKCODEC_E_INVALID: i32 = -50001;\n/// Call not valid in the current state.\npub const OAKCODEC_E_STATE: i32 = -50002;\n/// The underlying operation failed.\npub const OAKCODEC_E_FAILED: i32 = -50003;\n/// Index out of range / entry not found.\npub const OAKCODEC_E_NOT_FOUND: i32 = -50004;\n/// Allocation failed.\npub const OAKCODEC_E_NOMEM: i32 = -50005;\n/// The operation was cancelled.\npub const OAKCODEC_E_CANCELLED: i32 = -50006;\n\n/// Current ABI version stamped into every oakcodec handle.\npub const OAKCODEC_ABI_VERSION: u32 = 1;\n\n/// Crate-internal result type; the FFI layer maps it to the codes.\npub type Result\u003cT\u003e = std::result::Result\u003cT, Error\u003e;\n\n/// Crate-internal error.\n#[derive(Debug)]\npub enum Error {\n\t/// Null handle or invalid argument.\n\tInvalid,\n\t/// Wrong state.\n\tState,\n\t/// Operation failed (context string is log-only).\n\tFailed(String),\n\t/// Not found.\n\tNotFound,\n\t/// Out of memory.\n\tNoMem,\n\t/// The operation was cancelled.\n\tCancelled,\n}\n\nimpl Error {\n\t/// Map to the public error code.\n\tpub fn code(\u0026self) -\u003e i32 {\n\t\tmatch self {\n\t\t\tError::Invalid =\u003e OAKCODEC_E_INVALID,\n\t\t\tError::State =\u003e OAKCODEC_E_STATE,\n\t\t\tError::Failed(_) =\u003e OAKCODEC_E_FAILED,\n\t\t\tError::NotFound =\u003e OAKCODEC_E_NOT_FOUND,\n\t\t\tError::NoMem =\u003e OAKCODEC_E_NOMEM,\n\t\t\tError::Cancelled =\u003e OAKCODEC_E_CANCELLED,\n\t\t}\n\t}\n}\n","traces":[{"line":60,"address":[],"length":0,"stats":{"Line":36}},{"line":61,"address":[],"length":0,"stats":{"Line":36}},{"line":62,"address":[],"length":0,"stats":{"Line":21}},{"line":63,"address":[],"length":0,"stats":{"Line":10}},{"line":64,"address":[],"length":0,"stats":{"Line":2}},{"line":65,"address":[],"length":0,"stats":{"Line":2}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":1}}],"covered":7,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","exportcodec.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ExportCodec` — the export codec enum and name table.\n//!\n//! Mirrors `src/codec/src/exportcodec.h`. The enum is the raw int contract\n//! the C ABI documents (oakengine/encoding.h); values must NOT be\n//! reordered (they are used in serialized files).\n\n/// `olive::ExportCodec::Codec`. Only append (never insert/reorder); the\n/// integer values are part of the file format.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Codec {\n\t/// Apple ProRes.\n\tDNxHD = 0,\n\t/// H.264.\n\tH264 = 1,\n\t/// H.264 with RGB color.\n\tH264RGB = 2,\n\t/// H.265/HEVC.\n\tH265 = 3,\n\t/// OpenEXR.\n\tOpenEXR = 4,\n\t/// PNG.\n\tPNG = 5,\n\t/// Apple ProRes.\n\tProRes = 6,\n\t/// CineForm.\n\tCineForm = 7,\n\t/// TIFF.\n\tTIFF = 8,\n\t/// VP9.\n\tVP9 = 9,\n\t/// MPEG-2.\n\tMP2 = 10,\n\t/// MPEG-3.\n\tMP3 = 11,\n\t/// AAC.\n\tAAC = 12,\n\t/// Uncompressed PCM.\n\tPCM = 13,\n\t/// Opus.\n\tOpus = 14,\n\t/// Vorbis.\n\tVorbis = 15,\n\t/// FLAC.\n\tFLAC = 16,\n\t/// SubRip subtitles.\n\tSRT = 17,\n\t/// AV1.\n\tAV1 = 18,\n\t/// Sentinel count (not a real codec).\n\tCount = 19,\n}\n\nimpl Codec {\n\t/// Human-readable name for a codec.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `get_codec_name`; unknown /\n\t/// `Count` returns \"Unknown\".\n\tpub fn get_codec_name(c: Codec) -\u003e String {\n\t\tmatch c {\n\t\t\tCodec::DNxHD =\u003e \"DNxHD\".to_string(),\n\t\t\tCodec::H264 =\u003e \"H.264\".to_string(),\n\t\t\tCodec::H264RGB =\u003e \"H.264 RGB\".to_string(),\n\t\t\tCodec::H265 =\u003e \"H.265\".to_string(),\n\t\t\tCodec::OpenEXR =\u003e \"OpenEXR\".to_string(),\n\t\t\tCodec::PNG =\u003e \"PNG\".to_string(),\n\t\t\tCodec::ProRes =\u003e \"ProRes\".to_string(),\n\t\t\tCodec::CineForm =\u003e \"Cineform\".to_string(),\n\t\t\tCodec::TIFF =\u003e \"TIFF\".to_string(),\n\t\t\tCodec::VP9 =\u003e \"VP9\".to_string(),\n\t\t\tCodec::MP2 =\u003e \"MP2\".to_string(),\n\t\t\tCodec::MP3 =\u003e \"MP3\".to_string(),\n\t\t\tCodec::AAC =\u003e \"AAC\".to_string(),\n\t\t\tCodec::PCM =\u003e \"PCM (Uncompressed)\".to_string(),\n\t\t\tCodec::Opus =\u003e \"Opus\".to_string(),\n\t\t\tCodec::Vorbis =\u003e \"Vorbis\".to_string(),\n\t\t\tCodec::FLAC =\u003e \"FLAC\".to_string(),\n\t\t\tCodec::SRT =\u003e \"SubRip SRT\".to_string(),\n\t\t\tCodec::AV1 =\u003e \"AV1\".to_string(),\n\t\t\tCodec::Count =\u003e \"Unknown\".to_string(),\n\t\t}\n\t}\n\n\t/// Whether the codec produces a still image.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `is_codec_a_still_image`;\n\t/// OpenEXR / PNG / TIFF are still images, everything else is not.\n\tpub fn is_codec_a_still_image(c: Codec) -\u003e bool {\n\t\tmatch c {\n\t\t\tCodec::OpenEXR | Codec::PNG | Codec::TIFF =\u003e true,\n\t\t\t_ =\u003e false,\n\t\t}\n\t}\n\n\t/// Whether the codec is lossless.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `is_codec_lossless`;\n\t/// only PCM and FLAC are lossless.\n\tpub fn is_codec_lossless(c: Codec) -\u003e bool {\n\t\tmatch c {\n\t\t\tCodec::PCM | Codec::FLAC =\u003e true,\n\t\t\t_ =\u003e false,\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn codec_name_mapping() {\n\t\tassert_eq!(Codec::get_codec_name(Codec::DNxHD), \"DNxHD\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::H264), \"H.264\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::H264RGB), \"H.264 RGB\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::H265), \"H.265\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::OpenEXR), \"OpenEXR\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::PNG), \"PNG\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::ProRes), \"ProRes\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::CineForm), \"Cineform\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::TIFF), \"TIFF\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::VP9), \"VP9\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::MP2), \"MP2\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::MP3), \"MP3\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::AAC), \"AAC\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::PCM), \"PCM (Uncompressed)\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::Opus), \"Opus\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::Vorbis), \"Vorbis\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::FLAC), \"FLAC\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::SRT), \"SubRip SRT\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::AV1), \"AV1\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::Count), \"Unknown\");\n\t}\n\n\t#[test]\n\tfn still_image_and_lossless_flags() {\n\t\tassert!(Codec::is_codec_a_still_image(Codec::OpenEXR));\n\t\tassert!(Codec::is_codec_a_still_image(Codec::PNG));\n\t\tassert!(Codec::is_codec_a_still_image(Codec::TIFF));\n\t\tassert!(!Codec::is_codec_a_still_image(Codec::H264));\n\t\tassert!(!Codec::is_codec_a_still_image(Codec::PCM));\n\n\t\tassert!(Codec::is_codec_lossless(Codec::PCM));\n\t\tassert!(Codec::is_codec_lossless(Codec::FLAC));\n\t\tassert!(!Codec::is_codec_lossless(Codec::MP3));\n\t\tassert!(!Codec::is_codec_lossless(Codec::H264));\n\t}\n}\n","traces":[{"line":75,"address":[],"length":0,"stats":{"Line":20}},{"line":76,"address":[],"length":0,"stats":{"Line":20}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":2}},{"line":80,"address":[],"length":0,"stats":{"Line":2}},{"line":81,"address":[],"length":0,"stats":{"Line":2}},{"line":82,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":2}},{"line":84,"address":[],"length":0,"stats":{"Line":2}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":89,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":2}},{"line":92,"address":[],"length":0,"stats":{"Line":2}},{"line":93,"address":[],"length":0,"stats":{"Line":2}},{"line":94,"address":[],"length":0,"stats":{"Line":2}},{"line":95,"address":[],"length":0,"stats":{"Line":2}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":104,"address":[],"length":0,"stats":{"Line":5}},{"line":105,"address":[],"length":0,"stats":{"Line":5}},{"line":106,"address":[],"length":0,"stats":{"Line":3}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":115,"address":[],"length":0,"stats":{"Line":4}},{"line":116,"address":[],"length":0,"stats":{"Line":4}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":2}}],"covered":30,"coverable":30},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","exportformat.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ExportFormat` — the export container-format enum and tables.\n//!\n//! Mirrors `src/codec/src/exportformat.h`. As with `ExportCodec`, the enum\n//! values are the serialized-file contract and must never be reordered.\n\nuse oakcore_rs::SampleFormat;\n\nuse crate::exportcodec::Codec;\n\n/// `olive::ExportFormat::Format`. Only append (never insert/reorder).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Format {\n\t/// QuickTime DNxHD.\n\tDNxHD = 0,\n\t/// Matroska.\n\tMatroska = 1,\n\t/// MPEG-4 video.\n\tMPEG4Video = 2,\n\t/// OpenEXR.\n\tOpenEXR = 3,\n\t/// QuickTime.\n\tQuickTime = 4,\n\t/// PNG.\n\tPNG = 5,\n\t/// TIFF.\n\tTIFF = 6,\n\t/// WAV.\n\tWAV = 7,\n\t/// AIFF.\n\tAIFF = 8,\n\t/// MP3.\n\tMP3 = 9,\n\t/// FLAC.\n\tFLAC = 10,\n\t/// Ogg.\n\tOgg = 11,\n\t/// WebM.\n\tWebM = 12,\n\t/// SubRip.\n\tSRT = 13,\n\t/// MPEG-4 audio.\n\tMPEG4Audio = 14,\n\t/// Sentinel count (not a real format).\n\tCount = 15,\n}\n\nimpl Format {\n\t/// Convert an `i32` code to a [`Format`]; `None` outside the valid\n\t/// `0..=14` range (incl. `Count`).\n\tpub fn from_i32(v: i32) -\u003e Option\u003cFormat\u003e {\n\t\tmatch v {\n\t\t\t0 =\u003e Some(Format::DNxHD),\n\t\t\t1 =\u003e Some(Format::Matroska),\n\t\t\t2 =\u003e Some(Format::MPEG4Video),\n\t\t\t3 =\u003e Some(Format::OpenEXR),\n\t\t\t4 =\u003e Some(Format::QuickTime),\n\t\t\t5 =\u003e Some(Format::PNG),\n\t\t\t6 =\u003e Some(Format::TIFF),\n\t\t\t7 =\u003e Some(Format::WAV),\n\t\t\t8 =\u003e Some(Format::AIFF),\n\t\t\t9 =\u003e Some(Format::MP3),\n\t\t\t10 =\u003e Some(Format::FLAC),\n\t\t\t11 =\u003e Some(Format::Ogg),\n\t\t\t12 =\u003e Some(Format::WebM),\n\t\t\t13 =\u003e Some(Format::SRT),\n\t\t\t14 =\u003e Some(Format::MPEG4Audio),\n\t\t\t_ =\u003e None,\n\t\t}\n\t}\n\n\t/// Human-readable name for a format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_name`; unknown /\n\t/// `Count` returns \"Unknown\".\n\tpub fn get_name(f: Format) -\u003e String {\n\t\tmatch f {\n\t\t\tFormat::DNxHD =\u003e \"DNxHD\".to_string(),\n\t\t\tFormat::Matroska =\u003e \"Matroska Video\".to_string(),\n\t\t\tFormat::MPEG4Video =\u003e \"MPEG-4 Video\".to_string(),\n\t\t\tFormat::MPEG4Audio =\u003e \"MPEG-4 Audio\".to_string(),\n\t\t\tFormat::OpenEXR =\u003e \"OpenEXR\".to_string(),\n\t\t\tFormat::PNG =\u003e \"PNG\".to_string(),\n\t\t\tFormat::TIFF =\u003e \"TIFF\".to_string(),\n\t\t\tFormat::QuickTime =\u003e \"QuickTime\".to_string(),\n\t\t\tFormat::WAV =\u003e \"Wave Audio\".to_string(),\n\t\t\tFormat::AIFF =\u003e \"AIFF\".to_string(),\n\t\t\tFormat::MP3 =\u003e \"MP3\".to_string(),\n\t\t\tFormat::FLAC =\u003e \"FLAC\".to_string(),\n\t\t\tFormat::Ogg =\u003e \"Ogg\".to_string(),\n\t\t\tFormat::WebM =\u003e \"WebM\".to_string(),\n\t\t\tFormat::SRT =\u003e \"SubRip SRT\".to_string(),\n\t\t\tFormat::Count =\u003e \"Unknown\".to_string(),\n\t\t}\n\t}\n\n\t/// File extension for a format (e.g. \"mp4\").\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_extension`;\n\t/// `Count` returns an empty string.\n\tpub fn get_extension(f: Format) -\u003e String {\n\t\tmatch f {\n\t\t\tFormat::DNxHD =\u003e \"mxf\".to_string(),\n\t\t\tFormat::Matroska =\u003e \"mkv\".to_string(),\n\t\t\tFormat::MPEG4Video =\u003e \"mp4\".to_string(),\n\t\t\tFormat::MPEG4Audio =\u003e \"m4a\".to_string(),\n\t\t\tFormat::OpenEXR =\u003e \"exr\".to_string(),\n\t\t\tFormat::PNG =\u003e \"png\".to_string(),\n\t\t\tFormat::TIFF =\u003e \"tiff\".to_string(),\n\t\t\tFormat::QuickTime =\u003e \"mov\".to_string(),\n\t\t\tFormat::WAV =\u003e \"wav\".to_string(),\n\t\t\tFormat::AIFF =\u003e \"aiff\".to_string(),\n\t\t\tFormat::MP3 =\u003e \"mp3\".to_string(),\n\t\t\tFormat::FLAC =\u003e \"flac\".to_string(),\n\t\t\tFormat::Ogg =\u003e \"ogg\".to_string(),\n\t\t\tFormat::WebM =\u003e \"webm\".to_string(),\n\t\t\tFormat::SRT =\u003e \"srt\".to_string(),\n\t\t\tFormat::Count =\u003e String::new(),\n\t\t}\n\t}\n\n\t/// Codecs that can carry video in this format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_video_codecs`.\n\tpub fn get_video_codecs(f: Format) -\u003e Vec\u003cCodec\u003e {\n\t\tmatch f {\n\t\t\tFormat::DNxHD =\u003e vec![Codec::DNxHD],\n\t\t\tFormat::Matroska =\u003e vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::VP9],\n\t\t\tFormat::MPEG4Video =\u003e vec![Codec::H264, Codec::H264RGB, Codec::H265],\n\t\t\tFormat::OpenEXR =\u003e vec![Codec::OpenEXR],\n\t\t\tFormat::PNG =\u003e vec![Codec::PNG],\n\t\t\tFormat::TIFF =\u003e vec![Codec::TIFF],\n\t\t\tFormat::QuickTime =\u003e vec![\n\t\t\t\tCodec::H264,\n\t\t\t\tCodec::H264RGB,\n\t\t\t\tCodec::H265,\n\t\t\t\tCodec::ProRes,\n\t\t\t\tCodec::CineForm,\n\t\t\t],\n\t\t\tFormat::WebM =\u003e vec![Codec::AV1, Codec::VP9],\n\t\t\t_ =\u003e Vec::new(),\n\t\t}\n\t}\n\n\t/// Codecs that can carry audio in this format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_audio_codecs`.\n\tpub fn get_audio_codecs(f: Format) -\u003e Vec\u003cCodec\u003e {\n\t\tmatch f {\n\t\t\t// Video/audio formats.\n\t\t\tFormat::DNxHD =\u003e vec![Codec::PCM],\n\t\t\tFormat::Matroska =\u003e vec![\n\t\t\t\tCodec::AAC,\n\t\t\t\tCodec::MP2,\n\t\t\t\tCodec::MP3,\n\t\t\t\tCodec::PCM,\n\t\t\t\tCodec::Vorbis,\n\t\t\t\tCodec::Opus,\n\t\t\t\tCodec::FLAC,\n\t\t\t],\n\t\t\tFormat::MPEG4Video | Format::MPEG4Audio =\u003e {\n\t\t\t\tvec![Codec::AAC, Codec::MP2, Codec::MP3]\n\t\t\t}\n\t\t\tFormat::QuickTime =\u003e vec![Codec::AAC, Codec::MP2, Codec::MP3, Codec::PCM],\n\t\t\tFormat::WebM =\u003e vec![\n\t\t\t\tCodec::Opus,\n\t\t\t\tCodec::AAC,\n\t\t\t\tCodec::MP2,\n\t\t\t\tCodec::MP3,\n\t\t\t\tCodec::PCM,\n\t\t\t\tCodec::Vorbis,\n\t\t\t],\n\t\t\t// Audio-only formats.\n\t\t\tFormat::WAV =\u003e vec![Codec::PCM],\n\t\t\tFormat::AIFF =\u003e vec![Codec::PCM],\n\t\t\tFormat::MP3 =\u003e vec![Codec::MP3],\n\t\t\tFormat::FLAC =\u003e vec![Codec::FLAC],\n\t\t\tFormat::Ogg =\u003e vec![Codec::Opus, Codec::Vorbis, Codec::PCM],\n\t\t\t_ =\u003e Vec::new(),\n\t\t}\n\t}\n\n\t/// Codecs that can carry subtitles in this format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_subtitle_codecs`;\n\t/// only Matroska and SRT support the SRT subtitle codec.\n\tpub fn get_subtitle_codecs(f: Format) -\u003e Vec\u003cCodec\u003e {\n\t\tmatch f {\n\t\t\tFormat::Matroska | Format::SRT =\u003e vec![Codec::SRT],\n\t\t\t_ =\u003e Vec::new(),\n\t\t}\n\t}\n\n\t/// Pixel formats the given codec supports in this format.\n\t///\n\t/// The C++ side computes this by instantiating the format's encoder and\n\t/// asking the FFmpeg bridge (`fb_encoder_codec_get_pixel_formats`); the\n\t/// base `Encoder::get_pixel_formats_for_codec` returns an empty list.\n\t/// There is no pure-Rust table, so this returns an empty list (matching\n\t/// the C++ base default). CPP-PARITY: `exportformat.cpp:222`.\n\tpub fn get_pixel_formats_for_codec(_f: Format, _c: Codec) -\u003e Vec\u003ci32\u003e {\n\t\tVec::new()\n\t}\n\n\t/// Sample formats the given codec supports in this format.\n\t///\n\t/// CPP-PARITY: `ffmpegencoder.cpp:175`. PCM is pure (signed-16 first so\n\t/// the export dialog's default matches FFmpeg's); all other codecs query\n\t/// the FFmpeg bridge and return empty here (see\n\t/// `get_pixel_formats_for_codec`).\n\tpub fn get_sample_formats_for_codec(_f: Format, c: Codec) -\u003e Vec\u003cSampleFormat\u003e {\n\t\tif c == Codec::PCM {\n\t\t\tvec![\n\t\t\t\tSampleFormat::S16,\n\t\t\t\tSampleFormat::U8,\n\t\t\t\tSampleFormat::S32,\n\t\t\t\tSampleFormat::S64,\n\t\t\t\tSampleFormat::F32,\n\t\t\t\tSampleFormat::F64,\n\t\t\t]\n\t\t} else {\n\t\t\tVec::new()\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn format_name_mapping() {\n\t\tassert_eq!(Format::get_name(Format::DNxHD), \"DNxHD\");\n\t\tassert_eq!(Format::get_name(Format::Matroska), \"Matroska Video\");\n\t\tassert_eq!(Format::get_name(Format::MPEG4Video), \"MPEG-4 Video\");\n\t\tassert_eq!(Format::get_name(Format::MPEG4Audio), \"MPEG-4 Audio\");\n\t\tassert_eq!(Format::get_name(Format::OpenEXR), \"OpenEXR\");\n\t\tassert_eq!(Format::get_name(Format::PNG), \"PNG\");\n\t\tassert_eq!(Format::get_name(Format::TIFF), \"TIFF\");\n\t\tassert_eq!(Format::get_name(Format::QuickTime), \"QuickTime\");\n\t\tassert_eq!(Format::get_name(Format::WAV), \"Wave Audio\");\n\t\tassert_eq!(Format::get_name(Format::AIFF), \"AIFF\");\n\t\tassert_eq!(Format::get_name(Format::MP3), \"MP3\");\n\t\tassert_eq!(Format::get_name(Format::FLAC), \"FLAC\");\n\t\tassert_eq!(Format::get_name(Format::Ogg), \"Ogg\");\n\t\tassert_eq!(Format::get_name(Format::WebM), \"WebM\");\n\t\tassert_eq!(Format::get_name(Format::SRT), \"SubRip SRT\");\n\t\tassert_eq!(Format::get_name(Format::Count), \"Unknown\");\n\t}\n\n\t#[test]\n\tfn format_extension_mapping() {\n\t\tlet cases = [\n\t\t\t(Format::DNxHD, \"mxf\"),\n\t\t\t(Format::Matroska, \"mkv\"),\n\t\t\t(Format::MPEG4Video, \"mp4\"),\n\t\t\t(Format::MPEG4Audio, \"m4a\"),\n\t\t\t(Format::OpenEXR, \"exr\"),\n\t\t\t(Format::PNG, \"png\"),\n\t\t\t(Format::TIFF, \"tiff\"),\n\t\t\t(Format::QuickTime, \"mov\"),\n\t\t\t(Format::WAV, \"wav\"),\n\t\t\t(Format::AIFF, \"aiff\"),\n\t\t\t(Format::MP3, \"mp3\"),\n\t\t\t(Format::FLAC, \"flac\"),\n\t\t\t(Format::Ogg, \"ogg\"),\n\t\t\t(Format::WebM, \"webm\"),\n\t\t\t(Format::SRT, \"srt\"),\n\t\t];\n\t\tfor (f, ext) in cases {\n\t\t\tassert_eq!(Format::get_extension(f), ext, \"{:?}\", f);\n\t\t}\n\t\tassert_eq!(Format::get_extension(Format::Count), \"\");\n\t}\n\n\t#[test]\n\tfn from_i32_maps_valid_and_rejects_others() {\n\t\tfor v in 0..=14 {\n\t\t\tassert!(Format::from_i32(v).is_some(), \"format {v}\");\n\t\t}\n\t\tassert!(Format::from_i32(15).is_none()); // Count\n\t\tassert!(Format::from_i32(-1).is_none());\n\t\tassert!(Format::from_i32(99).is_none());\n\t}\n\n\t#[test]\n\tfn video_codec_tables() {\n\t\tassert_eq!(Format::get_video_codecs(Format::DNxHD), vec![Codec::DNxHD]);\n\t\tassert_eq!(\n\t\t\tFormat::get_video_codecs(Format::Matroska),\n\t\t\tvec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::VP9]\n\t\t);\n\t\tassert_eq!(\n\t\t\tFormat::get_video_codecs(Format::MPEG4Video),\n\t\t\tvec![Codec::H264, Codec::H264RGB, Codec::H265]\n\t\t);\n\t\tassert_eq!(Format::get_video_codecs(Format::OpenEXR), vec![Codec::OpenEXR]);\n\t\tassert_eq!(Format::get_video_codecs(Format::PNG), vec![Codec::PNG]);\n\t\tassert_eq!(Format::get_video_codecs(Format::TIFF), vec![Codec::TIFF]);\n\t\tassert_eq!(\n\t\t\tFormat::get_video_codecs(Format::QuickTime),\n\t\t\tvec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::ProRes, Codec::CineForm]\n\t\t);\n\t\tassert_eq!(Format::get_video_codecs(Format::WebM), vec![Codec::AV1, Codec::VP9]);\n\t\t// Formats without video codecs.\n\t\tassert!(Format::get_video_codecs(Format::WAV).is_empty());\n\t\tassert!(Format::get_video_codecs(Format::MP3).is_empty());\n\t\tassert!(Format::get_video_codecs(Format::SRT).is_empty());\n\t}\n\n\t#[test]\n\tfn audio_codec_tables() {\n\t\tassert_eq!(Format::get_audio_codecs(Format::WAV), vec![Codec::PCM]);\n\t\tassert_eq!(Format::get_audio_codecs(Format::AIFF), vec![Codec::PCM]);\n\t\tassert_eq!(Format::get_audio_codecs(Format::MP3), vec![Codec::MP3]);\n\t\tassert_eq!(Format::get_audio_codecs(Format::FLAC), vec![Codec::FLAC]);\n\t\tassert_eq!(\n\t\t\tFormat::get_audio_codecs(Format::Ogg),\n\t\t\tvec![Codec::Opus, Codec::Vorbis, Codec::PCM]\n\t\t);\n\t\tassert_eq!(Format::get_audio_codecs(Format::DNxHD), vec![Codec::PCM]);\n\t\tassert!(Format::get_audio_codecs(Format::PNG).is_empty());\n\t\tassert!(Format::get_audio_codecs(Format::SRT).is_empty());\n\t}\n\n\t#[test]\n\tfn subtitle_and_codec_capability_tables() {\n\t\tassert_eq!(Format::get_subtitle_codecs(Format::Matroska), vec![Codec::SRT]);\n\t\tassert_eq!(Format::get_subtitle_codecs(Format::SRT), vec![Codec::SRT]);\n\t\tassert!(Format::get_subtitle_codecs(Format::MPEG4Video).is_empty());\n\n\t\t// Pixel-format query is a bridge-dependent default (empty list).\n\t\tassert!(Format::get_pixel_formats_for_codec(Format::MPEG4Video, Codec::H264).is_empty());\n\n\t\t// PCM exposes its native sample formats; other codecs query the\n\t\t// FFmpeg bridge and get an empty list here.\n\t\tassert_eq!(\n\t\t\tFormat::get_sample_formats_for_codec(Format::WAV, Codec::PCM),\n\t\t\tvec![\n\t\t\t\tSampleFormat::S16,\n\t\t\t\tSampleFormat::U8,\n\t\t\t\tSampleFormat::S32,\n\t\t\t\tSampleFormat::S64,\n\t\t\t\tSampleFormat::F32,\n\t\t\t\tSampleFormat::F64,\n\t\t\t]\n\t\t);\n\t\tassert!(Format::get_sample_formats_for_codec(Format::MPEG4Video, Codec::AAC).is_empty());\n\t}\n}\n","traces":[{"line":67,"address":[],"length":0,"stats":{"Line":21}},{"line":68,"address":[],"length":0,"stats":{"Line":21}},{"line":69,"address":[],"length":0,"stats":{"Line":1}},{"line":70,"address":[],"length":0,"stats":{"Line":1}},{"line":71,"address":[],"length":0,"stats":{"Line":3}},{"line":72,"address":[],"length":0,"stats":{"Line":1}},{"line":73,"address":[],"length":0,"stats":{"Line":1}},{"line":74,"address":[],"length":0,"stats":{"Line":1}},{"line":75,"address":[],"length":0,"stats":{"Line":1}},{"line":76,"address":[],"length":0,"stats":{"Line":1}},{"line":77,"address":[],"length":0,"stats":{"Line":1}},{"line":78,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":16}},{"line":93,"address":[],"length":0,"stats":{"Line":16}},{"line":94,"address":[],"length":0,"stats":{"Line":2}},{"line":95,"address":[],"length":0,"stats":{"Line":2}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":97,"address":[],"length":0,"stats":{"Line":2}},{"line":98,"address":[],"length":0,"stats":{"Line":2}},{"line":99,"address":[],"length":0,"stats":{"Line":2}},{"line":100,"address":[],"length":0,"stats":{"Line":2}},{"line":101,"address":[],"length":0,"stats":{"Line":2}},{"line":102,"address":[],"length":0,"stats":{"Line":2}},{"line":103,"address":[],"length":0,"stats":{"Line":2}},{"line":104,"address":[],"length":0,"stats":{"Line":2}},{"line":105,"address":[],"length":0,"stats":{"Line":2}},{"line":106,"address":[],"length":0,"stats":{"Line":2}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":2}},{"line":109,"address":[],"length":0,"stats":{"Line":2}},{"line":117,"address":[],"length":0,"stats":{"Line":18}},{"line":118,"address":[],"length":0,"stats":{"Line":18}},{"line":119,"address":[],"length":0,"stats":{"Line":2}},{"line":120,"address":[],"length":0,"stats":{"Line":2}},{"line":121,"address":[],"length":0,"stats":{"Line":6}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":2}},{"line":124,"address":[],"length":0,"stats":{"Line":2}},{"line":125,"address":[],"length":0,"stats":{"Line":2}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":127,"address":[],"length":0,"stats":{"Line":2}},{"line":128,"address":[],"length":0,"stats":{"Line":2}},{"line":129,"address":[],"length":0,"stats":{"Line":2}},{"line":130,"address":[],"length":0,"stats":{"Line":2}},{"line":131,"address":[],"length":0,"stats":{"Line":2}},{"line":132,"address":[],"length":0,"stats":{"Line":2}},{"line":133,"address":[],"length":0,"stats":{"Line":2}},{"line":134,"address":[],"length":0,"stats":{"Line":1}},{"line":141,"address":[],"length":0,"stats":{"Line":11}},{"line":142,"address":[],"length":0,"stats":{"Line":11}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":144,"address":[],"length":0,"stats":{"Line":5}},{"line":145,"address":[],"length":0,"stats":{"Line":4}},{"line":146,"address":[],"length":0,"stats":{"Line":2}},{"line":147,"address":[],"length":0,"stats":{"Line":2}},{"line":148,"address":[],"length":0,"stats":{"Line":2}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":150,"address":[],"length":0,"stats":{"Line":1}},{"line":151,"address":[],"length":0,"stats":{"Line":1}},{"line":152,"address":[],"length":0,"stats":{"Line":1}},{"line":153,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":1}},{"line":156,"address":[],"length":0,"stats":{"Line":3}},{"line":157,"address":[],"length":0,"stats":{"Line":3}},{"line":164,"address":[],"length":0,"stats":{"Line":8}},{"line":165,"address":[],"length":0,"stats":{"Line":8}},{"line":167,"address":[],"length":0,"stats":{"Line":2}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":181,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":183,"address":[],"length":0,"stats":{"Line":0}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":190,"address":[],"length":0,"stats":{"Line":2}},{"line":191,"address":[],"length":0,"stats":{"Line":2}},{"line":192,"address":[],"length":0,"stats":{"Line":2}},{"line":193,"address":[],"length":0,"stats":{"Line":2}},{"line":194,"address":[],"length":0,"stats":{"Line":4}},{"line":195,"address":[],"length":0,"stats":{"Line":2}},{"line":203,"address":[],"length":0,"stats":{"Line":3}},{"line":204,"address":[],"length":0,"stats":{"Line":3}},{"line":205,"address":[],"length":0,"stats":{"Line":4}},{"line":206,"address":[],"length":0,"stats":{"Line":1}},{"line":217,"address":[],"length":0,"stats":{"Line":1}},{"line":218,"address":[],"length":0,"stats":{"Line":1}},{"line":227,"address":[],"length":0,"stats":{"Line":2}},{"line":228,"address":[],"length":0,"stats":{"Line":2}},{"line":229,"address":[],"length":0,"stats":{"Line":1}},{"line":230,"address":[],"length":0,"stats":{"Line":1}},{"line":231,"address":[],"length":0,"stats":{"Line":1}},{"line":232,"address":[],"length":0,"stats":{"Line":1}},{"line":233,"address":[],"length":0,"stats":{"Line":1}},{"line":234,"address":[],"length":0,"stats":{"Line":1}},{"line":235,"address":[],"length":0,"stats":{"Line":1}},{"line":238,"address":[],"length":0,"stats":{"Line":1}}],"covered":95,"coverable":112},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","conform.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/conform.h` exports.\n//!\n//! Complete inventory: create_instance / destroy_instance / get_state /\n//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the\n//! states.\n//!\n//! # CPP-PARITY\n//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the\n//! singleton is absent (`!ConformManager::instance()`); the Rust\n//! [`crate::conformmanager::ConformManager::instance`] is a lazy\n//! `'static` singleton that can never be absent, so that branch cannot\n//! trigger.\n\nuse std::ffi::{c_char, c_int};\n\nuse crate::conformmanager::{ConformManager, ConformState};\nuse crate::handle;\n\n/// `OAKCODEC_CONFORM_EXISTS`.\npub const OAKCODEC_CONFORM_EXISTS: c_int = 0;\n/// `OAKCODEC_CONFORM_GENERATING`.\npub const OAKCODEC_CONFORM_GENERATING: c_int = 1;\n/// `OAKCODEC_CONFORM_UNAVAILABLE`.\npub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2;\n\n/// `oakcodec_conform_create_instance`: create the singleton (always\n/// present here, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_create_instance() -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet _ = ConformManager::instance();\n\t\tcrate::error::OAKCODEC_OK\n\t})\n}\n\n/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust\n/// manager is stateless, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_destroy_instance() -\u003e c_int {\n\thandle::guard_raw(|| crate::error::OAKCODEC_OK)\n}\n\n/// `oakcodec_conform_get_state`: query the conform state of one audio\n/// stream.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_get_state(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\twait: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) if !c.is_empty() =\u003e c,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) if !s.is_empty() =\u003e s,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet m = ConformManager::instance();\n\t\tmatch m.get_conform_state(\n\t\t\t\u0026cache,\n\t\t\t\u0026source,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t\twait != 0,\n\t\t) {\n\t\t\tOk(ConformState::Exists) =\u003e OAKCODEC_CONFORM_EXISTS,\n\t\t\tOk(ConformState::Generating) =\u003e OAKCODEC_CONFORM_GENERATING,\n\t\t\tOk(ConformState::Unavailable) | Err(_) =\u003e OAKCODEC_CONFORM_UNAVAILABLE,\n\t\t}\n\t})\n}\n\n/// `oakcodec_conform_filename_count`: number of conform files for the\n/// given stream/params; 0 on invalid arguments.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_filename_count(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) if !c.is_empty() =\u003e c,\n\t\t\t_ =\u003e return 0,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) if !s.is_empty() =\u003e s,\n\t\t\t_ =\u003e return 0,\n\t\t};\n\t\tlet m = ConformManager::instance();\n\t\tm.get_conform_filename_count(\n\t\t\t\u0026cache,\n\t\t\t\u0026source,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t) as c_int\n\t})\n}\n\n/// `oakcodec_conform_filename_at`: the `index`-th conform filename\n/// (two-stage string).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_filename_at(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\tindex: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) if !c.is_empty() =\u003e c,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) if !s.is_empty() =\u003e s,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet m = ConformManager::instance();\n\t\tmatch m.get_conform_filename(\n\t\t\t\u0026cache,\n\t\t\t\u0026source,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t\tindex as usize,\n\t\t) {\n\t\t\tOk(f) =\u003e super::string_out(\u0026f, buf, buf_size),\n\t\t\tErr(crate::error::Error::NotFound) =\u003e crate::error::OAKCODEC_E_NOT_FOUND,\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::conformmanager::test_util::REG_LOCK;\n\tuse crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn temp_cache(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_ffi_conform_{}_{}\", name, std::process::id()));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\t#[test]\n\tfn create_destroy_instance_ok() {\n\t\tassert_eq!(unsafe { oakcodec_conform_create_instance() }, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_conform_destroy_instance() }, crate::error::OAKCODEC_OK);\n\t}\n\n\t#[test]\n\tfn get_state_maps_states() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\t// No registrar and no files -\u003e UNAVAILABLE.\n\t\tlet cache = cstr(\u0026temp_cache(\"state\"));\n\t\tlet src = cstr(\"media.mp4\");\n\t\tlet rc = unsafe { oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE);\n\n\t\t// Invalid arguments -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet empty = cstr(\"\");\n\t\tlet rc = unsafe { oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Write the conform files -\u003e EXISTS.\n\t\tlet m = ConformManager::instance();\n\t\tfor i in 0..2 {\n\t\t\tlet f = m\n\t\t\t\t.get_conform_filename(\u0026temp_cache(\"state\"), \"media.mp4\", 0, 48000, 0x3, 0, i)\n\t\t\t\t.unwrap();\n\t\t\tstd::fs::write(\u0026f, b\"pcm\").unwrap();\n\t\t}\n\t\tlet rc = unsafe { oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_CONFORM_EXISTS);\n\t}\n\n\t#[test]\n\tfn filename_count_and_at() {\n\t\tlet cache = cstr(\u0026temp_cache(\"names\"));\n\t\tlet src = cstr(\"media.mp4\");\n\n\t\t// Stereo -\u003e 2 files.\n\t\tlet rc = unsafe { oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0) };\n\t\tassert_eq!(rc, 2);\n\n\t\t// Invalid args -\u003e 0 (not an error).\n\t\tlet rc = unsafe { oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0) };\n\t\tassert_eq!(rc, 0);\n\n\t\t// filename_at round-trips the deterministic name.\n\t\tlet mut buf = [0i8; 512];\n\t\tlet rc = unsafe { oakcodec_conform_filename_at(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0, buf.as_mut_ptr(), 512) };\n\t\tassert!(rc \u003e 0);\n\t\tlet name = crate::ffi::c_str(buf.as_ptr()).unwrap();\n\t\tassert!(name.ends_with(\".0.pcm\"));\n\n\t\t// Out-of-range index -\u003e E_NOT_FOUND.\n\t\tlet rc = unsafe { oakcodec_conform_filename_at(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 5, buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_NOT_FOUND);\n\n\t\t// Invalid args -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_conform_filename_at(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0, buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n}\n","traces":[{"line":45,"address":[],"length":0,"stats":{"Line":1}},{"line":46,"address":[],"length":0,"stats":{"Line":2}},{"line":47,"address":[],"length":0,"stats":{"Line":1}},{"line":48,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":1}},{"line":56,"address":[],"length":0,"stats":{"Line":1}},{"line":62,"address":[],"length":0,"stats":{"Line":4}},{"line":71,"address":[],"length":0,"stats":{"Line":8}},{"line":72,"address":[],"length":0,"stats":{"Line":10}},{"line":73,"address":[],"length":0,"stats":{"Line":12}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":10}},{"line":78,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":4}},{"line":81,"address":[],"length":0,"stats":{"Line":4}},{"line":82,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":2}},{"line":84,"address":[],"length":0,"stats":{"Line":2}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":1}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":1}},{"line":100,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":4}},{"line":109,"address":[],"length":0,"stats":{"Line":5}},{"line":110,"address":[],"length":0,"stats":{"Line":5}},{"line":111,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":3}},{"line":114,"address":[],"length":0,"stats":{"Line":5}},{"line":115,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":3}},{"line":119,"address":[],"length":0,"stats":{"Line":2}},{"line":120,"address":[],"length":0,"stats":{"Line":2}},{"line":121,"address":[],"length":0,"stats":{"Line":2}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":132,"address":[],"length":0,"stats":{"Line":3}},{"line":143,"address":[],"length":0,"stats":{"Line":6}},{"line":144,"address":[],"length":0,"stats":{"Line":8}},{"line":145,"address":[],"length":0,"stats":{"Line":10}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":6}},{"line":149,"address":[],"length":0,"stats":{"Line":10}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":152,"address":[],"length":0,"stats":{"Line":4}},{"line":153,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":2}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":158,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":2}},{"line":160,"address":[],"length":0,"stats":{"Line":2}},{"line":162,"address":[],"length":0,"stats":{"Line":5}},{"line":163,"address":[],"length":0,"stats":{"Line":1}},{"line":164,"address":[],"length":0,"stats":{"Line":0}}],"covered":58,"coverable":63},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","decoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/decoder.h` exports.\n//!\n//! Complete inventory: probe / probe_last_error / probe_decoder_name /\n//! probe_video_stream_count / probe_audio_stream_count /\n//! probe_subtitle_stream_count / probe_get_video_stream /\n//! probe_get_audio_stream / init / free / open / close / is_open /\n//! decode_video / decode_audio / conform_audio /\n//! get_image_sequence_digit_count / get_image_sequence_index /\n//! transform_image_sequence_file_name / last_error.\n//!\n//! Two handle box types share the `OakDecoder` handle, mirroring the C++\n//! `ProbeBox` / `DecoderBox` split in `c_api/decoder.cpp`: probe exports\n//! read a plain [`ProbeBox`], session exports read a `Mutex\u003cDecoderBox\u003e`.\n\nuse std::ffi::{c_char, c_int};\n#[cfg(test)]\nuse std::ffi::c_void;\nuse std::path::Path;\nuse std::sync::{Arc, Mutex};\n\nuse oakcore_rs::{Rational, TimeRange};\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_get_channel_count, oakcommon_videoparams_get_color_primaries,\n\toakcommon_videoparams_get_color_transfer, oakcommon_videoparams_get_duration,\n\toakcommon_videoparams_get_format, oakcommon_videoparams_get_frame_rate,\n\toakcommon_videoparams_get_height, oakcommon_videoparams_get_interlacing,\n\toakcommon_videoparams_get_stream_index, oakcommon_videoparams_get_time_base,\n\toakcommon_videoparams_get_width, oakcore_audioparams_channel_count,\n\toakcore_audioparams_channel_layout, oakcore_audioparams_duration,\n\toakcore_audioparams_sample_rate, oakcore_audioparams_stream_index,\n\toakcore_audioparams_time_base, oakcore_rational_denominator,\n\toakcore_rational_free, oakcore_rational_numerator, OakAudioParams, OakVideoParams,\n};\nuse crate::bridge::render::{oakrender_cancelatom_heard_cancel, OakCancelAtom};\nuse crate::decoder::{\n\tCodecStream, Decoder, K_COLOR_RANGE_DEFAULT, OakCodecAudioStreamInfo,\n\tOakCodecVideoStreamInfo, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,\n};\nuse crate::footagedescription::FootageDescription;\n#[cfg(test)]\nuse crate::frame::Frame;\nuse crate::handle::{self, CHandle};\n\n/// Box behind a probe handle: the recognized decoder name plus the\n/// probed stream inventory (`ProbeBox` in `c_api/decoder.cpp`).\nstruct ProbeBox {\n\tdecoder_name: String,\n\tdesc: FootageDescription,\n}\n\n/// Box behind a decode-session handle (`DecoderBox` in `c_api/decoder.cpp`).\nstruct DecoderBox {\n\tdecoder: Option\u003cArc\u003cdyn Decoder\u003e\u003e,\n\tlast_error: String,\n\topen_filename: String,\n\topen_stream: i32,\n\topen: bool,\n}\n\n/// Last probe failure reason. The C++ uses a `thread_local`; the crate\n/// serializes through a global mutex instead (tests are single-threaded\n/// per binary, so behavior is equivalent).\nstatic PROBE_ERROR: Mutex\u003cString\u003e = Mutex::new(String::new());\n\n/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).\nconst OAKCOMMON_VIDEO_INTERLACE_NONE: c_int = 0;\n\n/// Probe with every available decoder, returning the first valid\n/// description (mirrors `probe_with_any_decoder` in `c_api/decoder.cpp`).\n///\n/// `is_valid` follows the C++ `FootageDescription::is_valid`: a non-empty\n/// decoder name and at least one stream.\nfn probe_with_any_decoder(filename: \u0026str) -\u003e Option\u003c(String, FootageDescription)\u003e {\n\tfor d in crate::decoder::receive_list_of_all_decoders() {\n\t\tlet desc = match d.probe(filename, None) {\n\t\t\tSome(desc) =\u003e desc,\n\t\t\tNone =\u003e continue,\n\t\t};\n\t\tif !desc.decoder().is_empty()\n\t\t\t\u0026\u0026 (desc.video_stream_count() \u003e 0\n\t\t\t\t|| desc.audio_stream_count() \u003e 0\n\t\t\t\t|| desc.subtitle_stream_count() \u003e 0)\n\t\t{\n\t\t\treturn Some((desc.decoder().to_string(), desc));\n\t\t}\n\t}\n\tNone\n}\n\n/// Fill an `oakcodec_video_stream_info` from a probed video params handle\n/// (mirrors `fill_video_info` in `c_api/decoder.cpp`).\nfn fill_video_info(vp: \u0026OakVideoParams, out: \u0026mut OakCodecVideoStreamInfo) {\n\t*out = OakCodecVideoStreamInfo {\n\t\tstream_index: 0,\n\t\twidth: 0,\n\t\theight: 0,\n\t\tframe_rate_num: 0,\n\t\tframe_rate_den: 0,\n\t\tduration_ts: 0,\n\t\ttime_base_num: 0,\n\t\ttime_base_den: 0,\n\t\tformat: 0,\n\t\tchannel_count: 0,\n\t\tcolor_primaries: 0,\n\t\tcolor_trc: 0,\n\t\tinterlaced: 0,\n\t};\n\tout.stream_index = unsafe { oakcommon_videoparams_get_stream_index(vp.clone()) };\n\tout.width = unsafe { oakcommon_videoparams_get_width(vp.clone()) };\n\tout.height = unsafe { oakcommon_videoparams_get_height(vp.clone()) };\n\n\tlet mut fr_num: c_int = 0;\n\tlet mut fr_den: c_int = 0;\n\tunsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), \u0026mut fr_num, \u0026mut fr_den) };\n\tout.frame_rate_num = fr_num;\n\tout.frame_rate_den = fr_den;\n\n\tlet mut tb_num: i64 = 0;\n\tlet mut tb_den: i64 = 0;\n\tunsafe { oakcommon_videoparams_get_time_base(vp.clone(), \u0026mut tb_num, \u0026mut tb_den) };\n\tout.time_base_num = tb_num as c_int;\n\tout.time_base_den = tb_den as c_int;\n\n\tout.duration_ts = unsafe { oakcommon_videoparams_get_duration(vp.clone()) };\n\tout.format = unsafe { oakcommon_videoparams_get_format(vp.clone()) };\n\tout.channel_count = unsafe { oakcommon_videoparams_get_channel_count(vp.clone()) };\n\tout.color_primaries = unsafe { oakcommon_videoparams_get_color_primaries(vp.clone()) };\n\tout.color_trc = unsafe { oakcommon_videoparams_get_color_transfer(vp.clone()) };\n\n\tlet interlacing = unsafe { oakcommon_videoparams_get_interlacing(vp.clone()) };\n\tout.interlaced = (interlacing != OAKCOMMON_VIDEO_INTERLACE_NONE) as c_int;\n}\n\n/// Fill an `oakcodec_audio_stream_info` from a probed audio params handle\n/// (mirrors `fill_audio_info` in `c_api/decoder.cpp`).\nfn fill_audio_info(ap: \u0026OakAudioParams, out: \u0026mut OakCodecAudioStreamInfo) {\n\t*out = OakCodecAudioStreamInfo {\n\t\tstream_index: 0,\n\t\tsample_rate: 0,\n\t\tchannel_layout: 0,\n\t\tchannel_count: 0,\n\t\tduration_ts: 0,\n\t\ttime_base_num: 0,\n\t\ttime_base_den: 0,\n\t};\n\tlet ctx = ap.ctx as *const OakAudioParams;\n\tout.stream_index = unsafe { oakcore_audioparams_stream_index(ctx) };\n\tout.sample_rate = unsafe { oakcore_audioparams_sample_rate(ctx) };\n\tout.channel_layout = unsafe { oakcore_audioparams_channel_layout(ctx) };\n\tout.channel_count = unsafe { oakcore_audioparams_channel_count(ctx) };\n\tout.duration_ts = unsafe { oakcore_audioparams_duration(ctx) };\n\n\t// `time_base` returns a newly allocated rational the caller owns.\n\tlet tb = unsafe { oakcore_audioparams_time_base(ctx) };\n\tif !tb.is_null() {\n\t\tout.time_base_num = unsafe { oakcore_rational_numerator(tb) };\n\t\tout.time_base_den = unsafe { oakcore_rational_denominator(tb) };\n\t\tunsafe { oakcore_rational_free(tb) };\n\t}\n}\n\n/// Whether a media file exists on disk (mirrors the C++ `file_exists`).\nfn file_exists(filename: \u0026str) -\u003e bool {\n\tPath::new(filename).exists()\n}\n\n/* ---- Probe ---------------------------------------------------------------- */\n\n/// `oakcodec_decoder_probe`: probe a media file with every available\n/// decoder; empty handle plus `oakcodec_probe_last_error` on failure.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe(filename: *const c_char) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tlet filename = match crate::ffi::c_str(filename) {\n\t\t\tSome(f) if !f.is_empty() =\u003e f,\n\t\t\t_ =\u003e {\n\t\t\t\t*PROBE_ERROR.lock().unwrap() = \"no filename given\".to_string();\n\t\t\t\treturn Ok(CHandle::null());\n\t\t\t}\n\t\t};\n\t\tif !file_exists(\u0026filename) {\n\t\t\t*PROBE_ERROR.lock().unwrap() = format!(\"file not found: {}\", filename);\n\t\t\treturn Ok(CHandle::null());\n\t\t}\n\t\tmatch probe_with_any_decoder(\u0026filename) {\n\t\t\tSome((decoder_name, desc)) =\u003e {\n\t\t\t\tPROBE_ERROR.lock().unwrap().clear();\n\t\t\t\tOk(handle::make_owned(ProbeBox { decoder_name, desc }))\n\t\t\t}\n\t\t\tNone =\u003e {\n\t\t\t\t*PROBE_ERROR.lock().unwrap() =\n\t\t\t\t\tformat!(\"no decoder recognizes this file: {}\", filename);\n\t\t\t\tOk(CHandle::null())\n\t\t\t}\n\t\t}\n\t})\n}\n\n/// `oakcodec_probe_last_error`: last probe failure reason (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_probe_last_error(buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet s = PROBE_ERROR.lock().unwrap().clone();\n\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t})\n}\n\n/// `oakcodec_decoder_probe_decoder_name`: recognized decoder id\n/// (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_decoder_name(\n\tprobe: CHandle,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tmatch super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\t\tSome(b) =\u003e super::string_out(\u0026b.decoder_name, buf, buf_size),\n\t\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID,\n\t\t}\n\t})\n}\n\n/// `oakcodec_decoder_probe_video_stream_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_video_stream_count(probe: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\tSome(b) =\u003e b.desc.video_stream_count() as c_int,\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_probe_audio_stream_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\tSome(b) =\u003e b.desc.audio_stream_count() as c_int,\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_probe_subtitle_stream_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_subtitle_stream_count(probe: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\tSome(b) =\u003e b.desc.subtitle_stream_count() as c_int,\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_probe_get_video_stream`: fill `out` with the video\n/// stream at `index` (video ordinal).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_get_video_stream(\n\tprobe: CHandle,\n\tindex: c_int,\n\tout: *mut OakCodecVideoStreamInfo,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet b = super::get_box::\u003cProbeBox\u003e(\u0026probe).ok_or(crate::error::Error::Invalid)?;\n\t\tlet vp = b\n\t\t\t.desc\n\t\t\t.get_video_stream(index as usize)\n\t\t\t.ok_or(crate::error::Error::NotFound)?;\n\t\tfill_video_info(vp, unsafe { \u0026mut *out });\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_decoder_probe_get_audio_stream`: fill `out` with the audio\n/// stream at `index` (audio ordinal).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_get_audio_stream(\n\tprobe: CHandle,\n\tindex: c_int,\n\tout: *mut OakCodecAudioStreamInfo,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet b = super::get_box::\u003cProbeBox\u003e(\u0026probe).ok_or(crate::error::Error::Invalid)?;\n\t\tlet ap = b\n\t\t\t.desc\n\t\t\t.get_audio_stream(index as usize)\n\t\t\t.ok_or(crate::error::Error::NotFound)?;\n\t\tfill_audio_info(ap, unsafe { \u0026mut *out });\n\t\tOk(())\n\t})\n}\n\n/* ---- Decode session -------------------------------------------------------- */\n\n/// `oakcodec_decoder_init`: new closed decoder handle, count 1.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_init() -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tOk(handle::make_owned(Mutex::new(DecoderBox {\n\t\t\tdecoder: None,\n\t\t\tlast_error: String::new(),\n\t\t\topen_filename: String::new(),\n\t\t\topen_stream: -1,\n\t\t\topen: false,\n\t\t})))\n\t})\n}\n\n/// `oakcodec_decoder_free`: NULL/empty no-op; nulls `ctx` afterwards.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_free(decoder: *mut CHandle) {\n\thandle::guard_void(|| super::free_handle(decoder));\n}\n\n/// `oakcodec_decoder_open`: open `filename`'s stream `stream_index` for\n/// decoding; opening the same stream again is a successful no-op.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_open(\n\tdecoder: CHandle,\n\tfilename: *const c_char,\n\tstream_index: c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet filename = match crate::ffi::c_str(filename) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tif stream_index \u003c 0 {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet mut b = b.lock().unwrap();\n\n\t\tif b.open \u0026\u0026 b.decoder.is_some() {\n\t\t\tif b.open_filename == filename \u0026\u0026 b.open_stream == stream_index {\n\t\t\t\treturn Ok(()); // already open on this stream\n\t\t\t}\n\t\t\tlet _ = b.decoder.as_ref().unwrap().close();\n\t\t\tb.open = false;\n\t\t}\n\n\t\tif !file_exists(\u0026filename) {\n\t\t\tb.last_error = format!(\"file not found: {}\", filename);\n\t\t\treturn Err(crate::error::Error::NotFound);\n\t\t}\n\n\t\tlet (decoder_name, _desc) = match probe_with_any_decoder(\u0026filename) {\n\t\t\tSome(x) =\u003e x,\n\t\t\tNone =\u003e {\n\t\t\t\tb.last_error = format!(\"no decoder recognizes this file: {}\", filename);\n\t\t\t\treturn Err(crate::error::Error::Failed(\"no decoder recognizes this file\".to_string()));\n\t\t\t}\n\t\t};\n\n\t\tlet decoder = match crate::decoder::create_from_id(\u0026decoder_name) {\n\t\t\tSome(d) =\u003e d,\n\t\t\tNone =\u003e {\n\t\t\t\tb.last_error = format!(\"failed to create decoder: {}\", decoder_name);\n\t\t\t\treturn Err(crate::error::Error::Failed(\"failed to create decoder\".to_string()));\n\t\t\t}\n\t\t};\n\n\t\tlet stream = CodecStream::with_block(filename.clone(), stream_index, None);\n\t\tif decoder.open(\u0026stream).is_err() {\n\t\t\tb.last_error = \"failed to open stream\".to_string();\n\t\t\treturn Err(crate::error::Error::Failed(\"failed to open stream\".to_string()));\n\t\t}\n\n\t\tb.last_error.clear();\n\t\tb.decoder = Some(decoder);\n\t\tb.open_filename = filename;\n\t\tb.open_stream = stream_index;\n\t\tb.open = true;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_decoder_close`: close the current stream (safe when closed).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_close(decoder: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet mut b = b.lock().unwrap();\n\t\tif b.open {\n\t\t\tif let Some(d) = \u0026b.decoder {\n\t\t\t\tlet _ = d.close();\n\t\t\t}\n\t\t}\n\t\tb.open = false;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_decoder_is_open`: 1 when a stream is open.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_is_open(decoder: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder) {\n\t\tSome(b) =\u003e {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif b.open {\n\t\t\t\t1\n\t\t\t} else {\n\t\t\t\t0\n\t\t\t}\n\t\t}\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_decode_video`: decode the frame at\n/// `numerator/denominator` seconds into a new frame handle (count 1).\n///\n/// # CPP-PARITY\n/// The C++ boxes an `olive::FramePtr` and happily aliases a frame the\n/// decoder still holds. The Rust interim boxes `Mutex\u003cFrame\u003e` (see\n/// `ffi::frame`), so a decode that returns a still-shared `Arc\u003cFrame\u003e`\n/// cannot be aliased and reports an empty handle plus `last_error`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_decode_video(\n\tdecoder: CHandle,\n\tnumerator: c_int,\n\tdenominator: c_int,\n) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet d = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.decoder.is_none() {\n\t\t\t\t// C++ returns an empty frame without touching last_error.\n\t\t\t\treturn Ok(CHandle::null());\n\t\t\t}\n\t\t\tb.decoder.as_ref().unwrap().clone()\n\t\t};\n\t\tlet p = RetrieveVideoParams {\n\t\t\t// C++ default-constructs the params and only sets `time`.\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(numerator as i64, denominator as i64),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t};\n\t\tmatch d.retrieve_video_frame(\u0026p) {\n\t\t\tOk(arc) =\u003e match Arc::try_unwrap(arc) {\n\t\t\t\tOk(frame) =\u003e Ok(handle::make_owned(Mutex::new(frame))),\n\t\t\t\tErr(_) =\u003e {\n\t\t\t\t\tb.lock().unwrap().last_error = \"failed to decode video frame\".to_string();\n\t\t\t\t\tOk(CHandle::null())\n\t\t\t\t}\n\t\t\t},\n\t\t\tErr(_) =\u003e {\n\t\t\t\tb.lock().unwrap().last_error = \"failed to decode video frame\".to_string();\n\t\t\t\tOk(CHandle::null())\n\t\t\t}\n\t\t}\n\t})\n}\n\n/// `oakcodec_decoder_decode_audio`: decode interleaved float audio\n/// covering [in, out) into `buf` (at least `buf_frames` frames).\n///\n/// Returns the number of frames written, or a negative `OAKCODEC_E_*`.\n/// # CPP-PARITY\n/// The C++ retrieves into an allocated `SampleBuffer` and copies\n/// `min(available, buf_frames)` frames. The Rust trait fills a caller-size\n/// destination, so `Success` is treated as \"`buf_frames` were written\".\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_decode_audio(\n\tdecoder: CHandle,\n\tin_num: c_int,\n\tin_den: c_int,\n\tout_num: c_int,\n\tout_den: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tbuf: *mut f32,\n\tbuf_frames: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tunsafe {\n\t\t\tdecode_audio_inner(\n\t\t\t\tdecoder,\n\t\t\t\tin_num,\n\t\t\t\tin_den,\n\t\t\t\tout_num,\n\t\t\t\tout_den,\n\t\t\t\tsample_rate,\n\t\t\t\tchannel_layout,\n\t\t\t\tbuf,\n\t\t\t\tbuf_frames,\n\t\t\t)\n\t\t}\n\t})\n}\n\nunsafe fn decode_audio_inner(\n\tdecoder: CHandle,\n\tin_num: c_int,\n\tin_den: c_int,\n\tout_num: c_int,\n\tout_den: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tbuf: *mut f32,\n\tbuf_frames: c_int,\n) -\u003e c_int {\n\tlet b = match super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder) {\n\t\tSome(b) =\u003e b,\n\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t};\n\tif (buf.is_null() \u0026\u0026 buf_frames \u003e 0) || buf_frames \u003c 0 {\n\t\treturn crate::error::OAKCODEC_E_INVALID;\n\t}\n\n\tlet d = {\n\t\tlet b = b.lock().unwrap();\n\t\tif !b.open || b.decoder.is_none() {\n\t\t\treturn crate::error::OAKCODEC_E_STATE;\n\t\t}\n\t\tb.decoder.as_ref().unwrap().clone()\n\t};\n\n\tlet range = TimeRange::new(\n\t\tRational::new(in_num as i64, in_den as i64),\n\t\tRational::new(out_num as i64, out_den as i64),\n\t);\n\tlet channels = channel_layout.count_ones() as usize;\n\tlet mut dest = vec![0f32; buf_frames as usize * channels];\n\n\tlet status = match d.retrieve_audio(\u0026mut dest, \u0026range, sample_rate, channel_layout) {\n\t\tOk(s) =\u003e s,\n\t\tErr(_) =\u003e {\n\t\t\tb.lock().unwrap().last_error = \"failed to decode audio\".to_string();\n\t\t\treturn crate::error::OAKCODEC_E_FAILED;\n\t\t}\n\t};\n\n\tmatch status {\n\t\tRetrieveAudioStatus::Success =\u003e {\n\t\t\t// SAFETY: the caller guarantees `buf` holds at least `buf_frames`\n\t\t\t// interleaved floats; `dest` is exactly that size.\n\t\t\tunsafe { std::ptr::copy_nonoverlapping(dest.as_ptr(), buf, dest.len()) };\n\t\t\tbuf_frames\n\t\t}\n\t\tRetrieveAudioStatus::ConformNeeded =\u003e {\n\t\t\tb.lock().unwrap().last_error =\n\t\t\t\t\"audio requires a conform, but no task submit callback is registered (see oakcodec_set_task_submit_cb)\"\n\t\t\t\t\t.to_string();\n\t\t\tcrate::error::OAKCODEC_E_STATE\n\t\t}\n\t\t_ =\u003e {\n\t\t\tb.lock().unwrap().last_error = \"failed to decode audio\".to_string();\n\t\t\tcrate::error::OAKCODEC_E_FAILED\n\t\t}\n\t}\n}\n\n/// `oakcodec_decoder_conform_audio`: conform the open stream's audio into\n/// the given per-channel pcm files.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_conform_audio(\n\tdecoder: CHandle,\n\toutput_filenames: *const *const c_char,\n\tfilename_count: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\tcancelled: OakCancelAtom,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tunsafe {\n\t\t\tconform_audio_inner(\n\t\t\t\tdecoder,\n\t\t\t\toutput_filenames,\n\t\t\t\tfilename_count,\n\t\t\t\tsample_rate,\n\t\t\t\tchannel_layout,\n\t\t\t\tsample_format,\n\t\t\t\tcancelled,\n\t\t\t)\n\t\t}\n\t})\n}\n\nunsafe fn conform_audio_inner(\n\tdecoder: CHandle,\n\toutput_filenames: *const *const c_char,\n\tfilename_count: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\tcancelled: OakCancelAtom,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\tif (output_filenames.is_null() \u0026\u0026 filename_count \u003e 0) || filename_count \u003c 0 {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\n\tlet d = {\n\t\tlet b = b.lock().unwrap();\n\t\tif !b.open || b.decoder.is_none() {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tb.decoder.as_ref().unwrap().clone()\n\t};\n\n\tlet mut filenames: Vec\u003cString\u003e = Vec::with_capacity(filename_count as usize);\n\tfor i in 0..filename_count {\n\t\tlet p = unsafe { *output_filenames.add(i as usize) };\n\t\tlet s = crate::ffi::c_str(p).ok_or(crate::error::Error::Invalid)?;\n\t\tfilenames.push(s);\n\t}\n\n\t// The trait takes the target audio params flattened; no handle\n\t// construction is needed.\n\tlet cancelled_ref = if cancelled.ctx.is_null() {\n\t\tNone\n\t} else {\n\t\tSome(\u0026cancelled)\n\t};\n\tlet ret = d.conform_audio(\n\t\t\u0026filenames,\n\t\tsample_rate,\n\t\tchannel_layout,\n\t\tsample_format,\n\t\tcancelled_ref,\n\t);\n\n\tmatch ret {\n\t\tOk(()) =\u003e Ok(()),\n\t\tErr(_) =\u003e {\n\t\t\tlet heard = if cancelled.ctx.is_null() {\n\t\t\t\t0\n\t\t\t} else {\n\t\t\t\tunsafe { oakrender_cancelatom_heard_cancel(cancelled.clone()) }\n\t\t\t};\n\t\t\tif heard != 0 {\n\t\t\t\tErr(crate::error::Error::Cancelled)\n\t\t\t} else {\n\t\t\t\tErr(crate::error::Error::Failed(\"conform failed\".to_string()))\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// `oakcodec_decoder_get_image_sequence_digit_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_get_image_sequence_digit_count(\n\tfilename: *const c_char,\n) -\u003e c_int {\n\thandle::guard_raw(|| match crate::ffi::c_str(filename) {\n\t\tSome(f) =\u003e crate::decoder::get_image_sequence_digit_count(\u0026f),\n\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID,\n\t})\n}\n\n/// `oakcodec_decoder_get_image_sequence_index`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_get_image_sequence_index(\n\tfilename: *const c_char,\n) -\u003e i64 {\n\thandle::guard_i64(|| match crate::ffi::c_str(filename) {\n\t\tSome(f) =\u003e crate::decoder::get_image_sequence_index(\u0026f),\n\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID as i64,\n\t})\n}\n\n/// `oakcodec_decoder_transform_image_sequence_file_name` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_transform_image_sequence_file_name(\n\tfilename: *const c_char,\n\tnumber: i64,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| match crate::ffi::c_str(filename) {\n\t\tSome(f) =\u003e {\n\t\t\tlet s = crate::decoder::transform_image_sequence_file_name(\u0026f, number);\n\t\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t\t}\n\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID,\n\t})\n}\n\n/// `oakcodec_decoder_last_error` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_last_error(\n\tdecoder: CHandle,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder) {\n\t\tSome(b) =\u003e {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tsuper::string_out(\u0026b.last_error, buf, buf_size)\n\t\t}\n\t\tNone =\u003e super::string_out(\"\", buf, buf_size),\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::{\n\t\toakcommon_videoparams_init_basic, oakcommon_videoparams_init_with_time_base,\n\t\toakcommon_videoparams_set_stream_index, oakcore_audioparams_create,\n\t};\n\tuse crate::bridge::render::{oakrender_cancelatom_cancel, oakrender_cancelatom_init};\n\tuse crate::decoder::set_test_decoders;\n\tuse crate::footagedescription::StreamEntry;\n\tuse crate::error::{OAKCODEC_E_CANCELLED, OAKCODEC_E_INVALID, OAKCODEC_E_STATE};\n\n\t/// The crate-wide ffi test lock (`crate::ffi::lock_tests`) serializes\n\t/// every test in this module (they share the global probe error, the\n\t/// injected decoder registry and the fake's state).\n\n\t/// Fake decoder driving the ffi export tests.\n\tstruct FakeDecoder {\n\t\tretain_frame: bool,\n\t\tretained: Mutex\u003cOption\u003cArc\u003cFrame\u003e\u003e\u003e,\n\t\tconform_fails: Mutex\u003cbool\u003e,\n\t\taudio_conform_needed: Mutex\u003cbool\u003e,\n\t}\n\n\timpl FakeDecoder {\n\t\tfn new(retain_frame: bool) -\u003e Arc\u003cdyn Decoder\u003e {\n\t\t\tArc::new(FakeDecoder {\n\t\t\t\tretain_frame,\n\t\t\t\tretained: Mutex::new(None),\n\t\t\t\tconform_fails: Mutex::new(false),\n\t\t\t\taudio_conform_needed: Mutex::new(false),\n\t\t\t})\n\t\t}\n\n\t\tfn with_audio_conform_needed() -\u003e Arc\u003cFakeDecoder\u003e {\n\t\t\tArc::new(FakeDecoder {\n\t\t\t\tretain_frame: false,\n\t\t\t\tretained: Mutex::new(None),\n\t\t\t\tconform_fails: Mutex::new(false),\n\t\t\t\taudio_conform_needed: Mutex::new(true),\n\t\t\t})\n\t\t}\n\n\t\tfn with_conform_failing() -\u003e Arc\u003cFakeDecoder\u003e {\n\t\t\tArc::new(FakeDecoder {\n\t\t\t\tretain_frame: false,\n\t\t\t\tretained: Mutex::new(None),\n\t\t\t\tconform_fails: Mutex::new(false),\n\t\t\t\taudio_conform_needed: Mutex::new(false),\n\t\t\t})\n\t\t}\n\t}\n\n\timpl Decoder for FakeDecoder {\n\t\tfn id(\u0026self) -\u003e String {\n\t\t\t\"fake\".to_string()\n\t\t}\n\n\t\tfn supports_video(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\n\t\tfn supports_audio(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\n\t\tfn probe(\n\t\t\t\u0026self,\n\t\t\tfilename: \u0026str,\n\t\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t\t) -\u003e Option\u003cFootageDescription\u003e {\n\t\t\tif filename.ends_with(\"test_video.mp4\") {\n\t\t\t\tlet mut desc = FootageDescription::new(\"fake\");\n\t\t\t\tlet vp = unsafe {\n\t\t\t\t\toakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000)\n\t\t\t\t};\n\t\t\t\tunsafe { oakcommon_videoparams_set_stream_index(vp.clone(), 0) };\n\t\t\t\tdesc.push_stream(StreamEntry::Video(vp));\n\t\t\t\tSome(desc)\n\t\t\t} else if filename.ends_with(\"test_audio.mp4\") {\n\t\t\t\tlet mut desc = FootageDescription::new(\"fake\");\n\t\t\t\tlet ap = unsafe { oakcore_audioparams_create(48000, 0x3, 10) };\n\t\t\t\tdesc.push_stream(StreamEntry::Audio(OakAudioParams {\n\t\t\t\t\tctx: ap as *mut c_void,\n\t\t\t\t\taddref: None,\n\t\t\t\t\trelease: None,\n\t\t\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t\t\t}));\n\t\t\t\tSome(desc)\n\t\t\t} else {\n\t\t\t\tNone\n\t\t\t}\n\t\t}\n\n\t\tfn open(\u0026self, _stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\n\t\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\n\t\tfn stream(\u0026self) -\u003e CodecStream {\n\t\t\tCodecStream::new()\n\t\t}\n\n\t\tfn retrieve_video_frame(\n\t\t\t\u0026self,\n\t\t\t_p: \u0026RetrieveVideoParams,\n\t\t) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e {\n\t\t\tlet params = unsafe { oakcommon_videoparams_init_basic(100, 50) };\n\t\t\tlet frame = Arc::new(Frame::with_params(params));\n\t\t\tif self.retain_frame {\n\t\t\t\t// Keep an alias alive so the ffi decode sees a shared Arc.\n\t\t\t\t*self.retained.lock().unwrap() = Some(frame.clone());\n\t\t\t}\n\t\t\tOk(frame)\n\t\t}\n\n\t\tfn retrieve_video(\n\t\t\t\u0026self,\n\t\t\t_p: \u0026RetrieveVideoParams,\n\t\t) -\u003e crate::error::Result\u003ccrate::bridge::render::OakRenderTexture\u003e {\n\t\t\tErr(crate::error::Error::Failed(\"no texture\".to_string()))\n\t\t}\n\n\t\tfn retrieve_audio(\n\t\t\t\u0026self,\n\t\t\tdest: \u0026mut [f32],\n\t\t\t_range: \u0026TimeRange,\n\t\t\t_sample_rate: i32,\n\t\t\t_channel_layout: u64,\n\t\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\t\tif *self.audio_conform_needed.lock().unwrap() {\n\t\t\t\treturn Ok(RetrieveAudioStatus::ConformNeeded);\n\t\t\t}\n\t\t\tfor s in dest.iter_mut() {\n\t\t\t\t*s = 1.0;\n\t\t\t}\n\t\t\tOk(RetrieveAudioStatus::Success)\n\t\t}\n\n\t\tfn conform_audio(\n\t\t\t\u0026self,\n\t\t\t_output_filenames: \u0026[String],\n\t\t\t_sample_rate: i32,\n\t\t\t_channel_layout: u64,\n\t\t\t_sample_format: i32,\n\t\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tif *self.conform_fails.lock().unwrap() {\n\t\t\t\tErr(crate::error::Error::Failed(\"conform failed\".to_string()))\n\t\t\t} else {\n\t\t\t\tOk(())\n\t\t\t}\n\t\t}\n\t}\n\n\tfn inject(fake: Arc\u003cdyn Decoder\u003e) {\n\t\tset_test_decoders(vec![fake]);\n\t}\n\n\tfn restore() {\n\t\tset_test_decoders(Vec::new());\n\t}\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn empty_cancel() -\u003e OakCancelAtom {\n\t\tOakCancelAtom {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: 0,\n\t\t}\n\t}\n\n\tfn media_file(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_ffi_dec_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tlet path = dir.join(name);\n\t\tlet _ = std::fs::write(\u0026path, b\"media\");\n\t\tpath.to_string_lossy().into_owned()\n\t}\n\n\t#[test]\n\tfn probe_golden_video() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\n\t\tlet mut name = [0i8; 64];\n\t\tlet rc = unsafe { oakcodec_decoder_probe_decoder_name(h, name.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 5); // \"fake\" + NUL\n\t\tassert_eq!(crate::ffi::c_str(name.as_ptr()).as_deref(), Some(\"fake\"));\n\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 1);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) }, 0);\n\n\t\tlet mut info: OakCodecVideoStreamInfo = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, \u0026mut info) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(info.stream_index, 0);\n\t\tassert_eq!(info.width, 1920);\n\t\tassert_eq!(info.height, 1080);\n\t\tassert_eq!(info.time_base_num, 1001);\n\t\tassert_eq!(info.time_base_den, 30000);\n\t\tassert_eq!(info.channel_count, 4);\n\t\tassert_eq!(info.interlaced, 0);\n\t\t// frame_rate/duration/colors default to 0 in the stub.\n\t\tassert_eq!(info.frame_rate_num, 0);\n\t\tassert_eq!(info.duration_ts, 0);\n\n\t\t// index out of range / null out.\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 3, \u0026mut info) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND);\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// probe_last_error is cleared on success.\n\t\tlet mut err = [0i8; 256];\n\t\tlet rc = unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert_eq!(rc, 1); // just the NUL\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"\"));\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\tassert!(h.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn probe_golden_audio() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 1);\n\n\t\tlet mut info: OakCodecAudioStreamInfo = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_audio_stream(h, 0, \u0026mut info) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(info.stream_index, 0);\n\t\tassert_eq!(info.sample_rate, 48000);\n\t\tassert_eq!(info.channel_layout, 0x3);\n\t\tassert_eq!(info.channel_count, 2);\n\t\tassert_eq!(info.time_base_num, 1);\n\t\tassert_eq!(info.time_base_den, 48000);\n\t\tassert_eq!(info.duration_ts, 0);\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn probe_failures() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\t// NULL filename.\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(std::ptr::null()) };\n\t\tassert!(h.is_null());\n\t\tlet mut err = [0i8; 256];\n\t\tunsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"no filename given\"));\n\n\t\t// Empty filename.\n\t\tlet f = cstr(\"\");\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(h.is_null());\n\n\t\t// Nonexistent file.\n\t\tlet f = cstr(\"/definitely/not/here.mp4\");\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(h.is_null());\n\t\tunsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert_eq!(\n\t\t\tcrate::ffi::c_str(err.as_ptr()).as_deref(),\n\t\t\tSome(\"file not found: /definitely/not/here.mp4\")\n\t\t);\n\n\t\t// A file no decoder recognizes (a real file: the test harness).\n\t\tlet me = std::env::current_exe().unwrap();\n\t\tlet me = me.to_string_lossy().into_owned();\n\t\tlet f = cstr(\u0026me);\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(h.is_null());\n\t\tunsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains(\"no decoder recognizes\"));\n\n\t\t// Empty handle on probe exports.\n\t\tlet empty = CHandle::null();\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(empty) }, 0);\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn open_decode_video_golden() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 0);\n\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 1);\n\n\t\t// Reopening the same stream is a successful no-op.\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\tlet mut frame = unsafe { oakcodec_decoder_decode_video(h, 1, 30) };\n\t\tassert!(!frame.is_null());\n\t\tassert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_width(frame) }, 100);\n\t\tassert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_height(frame) }, 50);\n\n\t\tunsafe { crate::ffi::frame::oakcodec_frame_free(\u0026mut frame) };\n\t\tlet rc = unsafe { oakcodec_decoder_close(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 0);\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn decode_video_shared_frame_reports_error() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(true)); // decoder retains the frame\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tunsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tlet mut frame = unsafe { oakcodec_decoder_decode_video(h, 0, 1) };\n\t\tassert!(frame.is_null());\n\t\tlet mut err = [0i8; 128];\n\t\tunsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 128) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"failed to decode video frame\"));\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn decode_audio_golden_and_errors() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\tlet mut buf = [0f32; 64];\n\t\tlet frames = unsafe {\n\t\t\toakcodec_decoder_decode_audio(\n\t\t\t\th, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16,\n\t\t\t)\n\t\t};\n\t\tassert_eq!(frames, 16);\n\t\t// Interleaved stereo filled by the fake.\n\t\tassert_eq!(buf[0], 1.0);\n\t\tassert_eq!(buf[31], 1.0);\n\n\t\t// Invalid args.\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Not open -\u003e E_STATE.\n\t\tlet mut h2 = unsafe { oakcodec_decoder_init() };\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h2) };\n\n\t\t// Empty handle -\u003e E_INVALID.\n\t\tlet empty = CHandle::null();\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn decode_audio_conform_needed_is_state() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_decoders(vec![FakeDecoder::with_audio_conform_needed()]);\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tunsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tlet mut buf = [0f32; 64];\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet mut err = [0i8; 256];\n\t\tunsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };\n\t\tassert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains(\"conform\"));\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn conform_audio_golden_cancelled_and_errors() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet fake = FakeDecoder::with_conform_failing();\n\t\tset_test_decoders(vec![fake.clone()]);\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tunsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\n\t\tlet a = cstr(\"a.pcm\");\n\t\tlet b = cstr(\"b.pcm\");\n\t\tlet files = [a.as_ptr(), b.as_ptr()];\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// Not open -\u003e E_STATE.\n\t\tlet mut h2 = unsafe { oakcodec_decoder_init() };\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h2, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\t// Invalid args.\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, std::ptr::null(), 1, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), -1, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Failure without cancellation -\u003e E_FAILED.\n\t\t*fake.conform_fails.lock().unwrap() = true;\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_FAILED);\n\n\t\t// Failure with a cancelled atom -\u003e E_CANCELLED.\n\t\tlet atom = unsafe { oakrender_cancelatom_init() };\n\t\tunsafe { oakrender_cancelatom_cancel(atom.clone()) };\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom)\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_CANCELLED);\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h2) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn image_sequence_exports() {\n\t\tlet _g = crate::ffi::lock_tests();\n\n\t\tlet f = cstr(\"frame_0001.png\");\n\t\tassert_eq!(\n\t\t\tunsafe { oakcodec_decoder_get_image_sequence_digit_count(f.as_ptr()) },\n\t\t\t4\n\t\t);\n\t\tassert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) }, 1);\n\n\t\tlet mut buf = [0i8; 128];\n\t\tlet rc = unsafe { oakcodec_decoder_transform_image_sequence_file_name(f.as_ptr(), 7, buf.as_mut_ptr(), 128) };\n\t\tassert!(rc \u003e 0);\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"frame_0007.png\"));\n\n\t\t// NULL filename -\u003e E_INVALID.\n\t\tassert_eq!(unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) }, OAKCODEC_E_INVALID as i64);\n\t\tassert_eq!(unsafe { oakcodec_decoder_transform_image_sequence_file_name(std::ptr::null(), 1, buf.as_mut_ptr(), 128) }, OAKCODEC_E_INVALID);\n\t}\n\n\t#[test]\n\tfn open_errors_and_last_error_empty_handle() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\n\t\t// NULL filename -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, std::ptr::null(), 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Negative stream index -\u003e E_INVALID.\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), -1) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Missing file -\u003e E_NOT_FOUND.\n\t\tlet f = cstr(\"/missing/file.mp4\");\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND);\n\t\tlet mut err = [0i8; 256];\n\t\tunsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"file not found: /missing/file.mp4\"));\n\n\t\t// Empty handle -\u003e E_INVALID and empty last_error.\n\t\tlet empty = CHandle::null();\n\t\tlet rc = unsafe { oakcodec_decoder_open(empty, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_decoder_close(empty) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(empty) }, 0);\n\t\tlet rc = unsafe { oakcodec_decoder_last_error(empty, err.as_mut_ptr(), 256) };\n\t\tassert_eq!(rc, 1);\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"\"));\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n}\n","traces":[{"line":90,"address":[],"length":0,"stats":{"Line":8}},{"line":91,"address":[],"length":0,"stats":{"Line":16}},{"line":92,"address":[],"length":0,"stats":{"Line":31}},{"line":93,"address":[],"length":0,"stats":{"Line":14}},{"line":94,"address":[],"length":0,"stats":{"Line":1}},{"line":96,"address":[],"length":0,"stats":{"Line":7}},{"line":97,"address":[],"length":0,"stats":{"Line":7}},{"line":98,"address":[],"length":0,"stats":{"Line":4}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":14}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":111,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":119,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":1}},{"line":121,"address":[],"length":0,"stats":{"Line":1}},{"line":122,"address":[],"length":0,"stats":{"Line":1}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":2}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":127,"address":[],"length":0,"stats":{"Line":2}},{"line":129,"address":[],"length":0,"stats":{"Line":3}},{"line":130,"address":[],"length":0,"stats":{"Line":3}},{"line":131,"address":[],"length":0,"stats":{"Line":5}},{"line":132,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":1}},{"line":135,"address":[],"length":0,"stats":{"Line":3}},{"line":136,"address":[],"length":0,"stats":{"Line":3}},{"line":137,"address":[],"length":0,"stats":{"Line":5}},{"line":138,"address":[],"length":0,"stats":{"Line":1}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":141,"address":[],"length":0,"stats":{"Line":2}},{"line":142,"address":[],"length":0,"stats":{"Line":2}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":144,"address":[],"length":0,"stats":{"Line":2}},{"line":145,"address":[],"length":0,"stats":{"Line":2}},{"line":147,"address":[],"length":0,"stats":{"Line":4}},{"line":148,"address":[],"length":0,"stats":{"Line":1}},{"line":153,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":1}},{"line":155,"address":[],"length":0,"stats":{"Line":1}},{"line":156,"address":[],"length":0,"stats":{"Line":1}},{"line":157,"address":[],"length":0,"stats":{"Line":1}},{"line":158,"address":[],"length":0,"stats":{"Line":1}},{"line":159,"address":[],"length":0,"stats":{"Line":1}},{"line":160,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":1}},{"line":163,"address":[],"length":0,"stats":{"Line":2}},{"line":164,"address":[],"length":0,"stats":{"Line":1}},{"line":165,"address":[],"length":0,"stats":{"Line":1}},{"line":166,"address":[],"length":0,"stats":{"Line":1}},{"line":167,"address":[],"length":0,"stats":{"Line":1}},{"line":168,"address":[],"length":0,"stats":{"Line":1}},{"line":171,"address":[],"length":0,"stats":{"Line":3}},{"line":172,"address":[],"length":0,"stats":{"Line":2}},{"line":173,"address":[],"length":0,"stats":{"Line":2}},{"line":174,"address":[],"length":0,"stats":{"Line":2}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":10}},{"line":181,"address":[],"length":0,"stats":{"Line":20}},{"line":189,"address":[],"length":0,"stats":{"Line":6}},{"line":190,"address":[],"length":0,"stats":{"Line":12}},{"line":191,"address":[],"length":0,"stats":{"Line":16}},{"line":192,"address":[],"length":0,"stats":{"Line":22}},{"line":194,"address":[],"length":0,"stats":{"Line":8}},{"line":195,"address":[],"length":0,"stats":{"Line":2}},{"line":198,"address":[],"length":0,"stats":{"Line":4}},{"line":199,"address":[],"length":0,"stats":{"Line":3}},{"line":200,"address":[],"length":0,"stats":{"Line":1}},{"line":202,"address":[],"length":0,"stats":{"Line":3}},{"line":203,"address":[],"length":0,"stats":{"Line":4}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":4}},{"line":208,"address":[],"length":0,"stats":{"Line":2}},{"line":209,"address":[],"length":0,"stats":{"Line":1}},{"line":210,"address":[],"length":0,"stats":{"Line":1}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":219,"address":[],"length":0,"stats":{"Line":8}},{"line":220,"address":[],"length":0,"stats":{"Line":12}},{"line":221,"address":[],"length":0,"stats":{"Line":16}},{"line":228,"address":[],"length":0,"stats":{"Line":2}},{"line":233,"address":[],"length":0,"stats":{"Line":4}},{"line":234,"address":[],"length":0,"stats":{"Line":2}},{"line":235,"address":[],"length":0,"stats":{"Line":5}},{"line":236,"address":[],"length":0,"stats":{"Line":1}},{"line":243,"address":[],"length":0,"stats":{"Line":3}},{"line":244,"address":[],"length":0,"stats":{"Line":6}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":1}},{"line":252,"address":[],"length":0,"stats":{"Line":2}},{"line":253,"address":[],"length":0,"stats":{"Line":4}},{"line":254,"address":[],"length":0,"stats":{"Line":4}},{"line":255,"address":[],"length":0,"stats":{"Line":0}},{"line":261,"address":[],"length":0,"stats":{"Line":1}},{"line":262,"address":[],"length":0,"stats":{"Line":2}},{"line":263,"address":[],"length":0,"stats":{"Line":2}},{"line":264,"address":[],"length":0,"stats":{"Line":0}},{"line":271,"address":[],"length":0,"stats":{"Line":3}},{"line":276,"address":[],"length":0,"stats":{"Line":6}},{"line":277,"address":[],"length":0,"stats":{"Line":6}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":280,"address":[],"length":0,"stats":{"Line":10}},{"line":281,"address":[],"length":0,"stats":{"Line":3}},{"line":282,"address":[],"length":0,"stats":{"Line":2}},{"line":283,"address":[],"length":0,"stats":{"Line":4}},{"line":284,"address":[],"length":0,"stats":{"Line":5}},{"line":285,"address":[],"length":0,"stats":{"Line":3}},{"line":286,"address":[],"length":0,"stats":{"Line":1}},{"line":293,"address":[],"length":0,"stats":{"Line":1}},{"line":298,"address":[],"length":0,"stats":{"Line":2}},{"line":299,"address":[],"length":0,"stats":{"Line":2}},{"line":300,"address":[],"length":0,"stats":{"Line":0}},{"line":302,"address":[],"length":0,"stats":{"Line":5}},{"line":303,"address":[],"length":0,"stats":{"Line":2}},{"line":304,"address":[],"length":0,"stats":{"Line":1}},{"line":305,"address":[],"length":0,"stats":{"Line":2}},{"line":306,"address":[],"length":0,"stats":{"Line":2}},{"line":307,"address":[],"length":0,"stats":{"Line":3}},{"line":308,"address":[],"length":0,"stats":{"Line":1}},{"line":316,"address":[],"length":0,"stats":{"Line":8}},{"line":317,"address":[],"length":0,"stats":{"Line":16}},{"line":318,"address":[],"length":0,"stats":{"Line":24}},{"line":319,"address":[],"length":0,"stats":{"Line":16}},{"line":320,"address":[],"length":0,"stats":{"Line":8}},{"line":321,"address":[],"length":0,"stats":{"Line":8}},{"line":322,"address":[],"length":0,"stats":{"Line":8}},{"line":323,"address":[],"length":0,"stats":{"Line":8}},{"line":330,"address":[],"length":0,"stats":{"Line":10}},{"line":331,"address":[],"length":0,"stats":{"Line":30}},{"line":337,"address":[],"length":0,"stats":{"Line":10}},{"line":342,"address":[],"length":0,"stats":{"Line":20}},{"line":343,"address":[],"length":0,"stats":{"Line":50}},{"line":344,"address":[],"length":0,"stats":{"Line":17}},{"line":345,"address":[],"length":0,"stats":{"Line":16}},{"line":346,"address":[],"length":0,"stats":{"Line":1}},{"line":348,"address":[],"length":0,"stats":{"Line":8}},{"line":349,"address":[],"length":0,"stats":{"Line":1}},{"line":351,"address":[],"length":0,"stats":{"Line":28}},{"line":353,"address":[],"length":0,"stats":{"Line":9}},{"line":354,"address":[],"length":0,"stats":{"Line":2}},{"line":355,"address":[],"length":0,"stats":{"Line":1}},{"line":357,"address":[],"length":0,"stats":{"Line":0}},{"line":358,"address":[],"length":0,"stats":{"Line":0}},{"line":361,"address":[],"length":0,"stats":{"Line":6}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":363,"address":[],"length":0,"stats":{"Line":1}},{"line":366,"address":[],"length":0,"stats":{"Line":15}},{"line":367,"address":[],"length":0,"stats":{"Line":10}},{"line":369,"address":[],"length":0,"stats":{"Line":0}},{"line":370,"address":[],"length":0,"stats":{"Line":0}},{"line":374,"address":[],"length":0,"stats":{"Line":10}},{"line":375,"address":[],"length":0,"stats":{"Line":10}},{"line":377,"address":[],"length":0,"stats":{"Line":0}},{"line":378,"address":[],"length":0,"stats":{"Line":0}},{"line":382,"address":[],"length":0,"stats":{"Line":30}},{"line":383,"address":[],"length":0,"stats":{"Line":15}},{"line":384,"address":[],"length":0,"stats":{"Line":0}},{"line":385,"address":[],"length":0,"stats":{"Line":0}},{"line":388,"address":[],"length":0,"stats":{"Line":10}},{"line":389,"address":[],"length":0,"stats":{"Line":10}},{"line":390,"address":[],"length":0,"stats":{"Line":10}},{"line":391,"address":[],"length":0,"stats":{"Line":5}},{"line":392,"address":[],"length":0,"stats":{"Line":5}},{"line":393,"address":[],"length":0,"stats":{"Line":5}},{"line":399,"address":[],"length":0,"stats":{"Line":2}},{"line":400,"address":[],"length":0,"stats":{"Line":4}},{"line":401,"address":[],"length":0,"stats":{"Line":10}},{"line":402,"address":[],"length":0,"stats":{"Line":4}},{"line":403,"address":[],"length":0,"stats":{"Line":1}},{"line":404,"address":[],"length":0,"stats":{"Line":3}},{"line":405,"address":[],"length":0,"stats":{"Line":1}},{"line":408,"address":[],"length":0,"stats":{"Line":1}},{"line":409,"address":[],"length":0,"stats":{"Line":1}},{"line":415,"address":[],"length":0,"stats":{"Line":4}},{"line":416,"address":[],"length":0,"stats":{"Line":8}},{"line":417,"address":[],"length":0,"stats":{"Line":3}},{"line":418,"address":[],"length":0,"stats":{"Line":12}},{"line":419,"address":[],"length":0,"stats":{"Line":3}},{"line":420,"address":[],"length":0,"stats":{"Line":1}},{"line":422,"address":[],"length":0,"stats":{"Line":2}},{"line":425,"address":[],"length":0,"stats":{"Line":1}},{"line":438,"address":[],"length":0,"stats":{"Line":2}},{"line":443,"address":[],"length":0,"stats":{"Line":4}},{"line":444,"address":[],"length":0,"stats":{"Line":10}},{"line":445,"address":[],"length":0,"stats":{"Line":2}},{"line":446,"address":[],"length":0,"stats":{"Line":8}},{"line":447,"address":[],"length":0,"stats":{"Line":6}},{"line":449,"address":[],"length":0,"stats":{"Line":0}},{"line":451,"address":[],"length":0,"stats":{"Line":4}},{"line":453,"address":[],"length":0,"stats":{"Line":4}},{"line":455,"address":[],"length":0,"stats":{"Line":4}},{"line":456,"address":[],"length":0,"stats":{"Line":8}},{"line":457,"address":[],"length":0,"stats":{"Line":2}},{"line":458,"address":[],"length":0,"stats":{"Line":2}},{"line":459,"address":[],"length":0,"stats":{"Line":2}},{"line":460,"address":[],"length":0,"stats":{"Line":2}},{"line":461,"address":[],"length":0,"stats":{"Line":2}},{"line":462,"address":[],"length":0,"stats":{"Line":2}},{"line":463,"address":[],"length":0,"stats":{"Line":2}},{"line":465,"address":[],"length":0,"stats":{"Line":4}},{"line":466,"address":[],"length":0,"stats":{"Line":4}},{"line":467,"address":[],"length":0,"stats":{"Line":3}},{"line":469,"address":[],"length":0,"stats":{"Line":4}},{"line":470,"address":[],"length":0,"stats":{"Line":1}},{"line":474,"address":[],"length":0,"stats":{"Line":0}},{"line":475,"address":[],"length":0,"stats":{"Line":0}},{"line":490,"address":[],"length":0,"stats":{"Line":6}},{"line":501,"address":[],"length":0,"stats":{"Line":12}},{"line":503,"address":[],"length":0,"stats":{"Line":6}},{"line":504,"address":[],"length":0,"stats":{"Line":6}},{"line":505,"address":[],"length":0,"stats":{"Line":6}},{"line":506,"address":[],"length":0,"stats":{"Line":6}},{"line":507,"address":[],"length":0,"stats":{"Line":6}},{"line":508,"address":[],"length":0,"stats":{"Line":6}},{"line":509,"address":[],"length":0,"stats":{"Line":6}},{"line":510,"address":[],"length":0,"stats":{"Line":6}},{"line":511,"address":[],"length":0,"stats":{"Line":6}},{"line":512,"address":[],"length":0,"stats":{"Line":6}},{"line":518,"address":[],"length":0,"stats":{"Line":6}},{"line":529,"address":[],"length":0,"stats":{"Line":11}},{"line":530,"address":[],"length":0,"stats":{"Line":10}},{"line":531,"address":[],"length":0,"stats":{"Line":1}},{"line":533,"address":[],"length":0,"stats":{"Line":15}},{"line":534,"address":[],"length":0,"stats":{"Line":2}},{"line":537,"address":[],"length":0,"stats":{"Line":2}},{"line":538,"address":[],"length":0,"stats":{"Line":12}},{"line":539,"address":[],"length":0,"stats":{"Line":7}},{"line":540,"address":[],"length":0,"stats":{"Line":1}},{"line":542,"address":[],"length":0,"stats":{"Line":4}},{"line":546,"address":[],"length":0,"stats":{"Line":6}},{"line":547,"address":[],"length":0,"stats":{"Line":6}},{"line":549,"address":[],"length":0,"stats":{"Line":4}},{"line":550,"address":[],"length":0,"stats":{"Line":6}},{"line":552,"address":[],"length":0,"stats":{"Line":12}},{"line":553,"address":[],"length":0,"stats":{"Line":4}},{"line":555,"address":[],"length":0,"stats":{"Line":0}},{"line":556,"address":[],"length":0,"stats":{"Line":0}},{"line":560,"address":[],"length":0,"stats":{"Line":2}},{"line":564,"address":[],"length":0,"stats":{"Line":6}},{"line":565,"address":[],"length":0,"stats":{"Line":1}},{"line":568,"address":[],"length":0,"stats":{"Line":2}},{"line":569,"address":[],"length":0,"stats":{"Line":1}},{"line":570,"address":[],"length":0,"stats":{"Line":1}},{"line":571,"address":[],"length":0,"stats":{"Line":1}},{"line":574,"address":[],"length":0,"stats":{"Line":0}},{"line":575,"address":[],"length":0,"stats":{"Line":0}},{"line":583,"address":[],"length":0,"stats":{"Line":6}},{"line":592,"address":[],"length":0,"stats":{"Line":12}},{"line":594,"address":[],"length":0,"stats":{"Line":6}},{"line":595,"address":[],"length":0,"stats":{"Line":6}},{"line":596,"address":[],"length":0,"stats":{"Line":6}},{"line":597,"address":[],"length":0,"stats":{"Line":6}},{"line":598,"address":[],"length":0,"stats":{"Line":6}},{"line":599,"address":[],"length":0,"stats":{"Line":6}},{"line":600,"address":[],"length":0,"stats":{"Line":6}},{"line":601,"address":[],"length":0,"stats":{"Line":6}},{"line":607,"address":[],"length":0,"stats":{"Line":6}},{"line":616,"address":[],"length":0,"stats":{"Line":30}},{"line":617,"address":[],"length":0,"stats":{"Line":18}},{"line":618,"address":[],"length":0,"stats":{"Line":2}},{"line":621,"address":[],"length":0,"stats":{"Line":3}},{"line":622,"address":[],"length":0,"stats":{"Line":16}},{"line":623,"address":[],"length":0,"stats":{"Line":10}},{"line":624,"address":[],"length":0,"stats":{"Line":1}},{"line":626,"address":[],"length":0,"stats":{"Line":6}},{"line":629,"address":[],"length":0,"stats":{"Line":12}},{"line":630,"address":[],"length":0,"stats":{"Line":9}},{"line":631,"address":[],"length":0,"stats":{"Line":18}},{"line":632,"address":[],"length":0,"stats":{"Line":30}},{"line":633,"address":[],"length":0,"stats":{"Line":18}},{"line":638,"address":[],"length":0,"stats":{"Line":9}},{"line":639,"address":[],"length":0,"stats":{"Line":2}},{"line":641,"address":[],"length":0,"stats":{"Line":1}},{"line":643,"address":[],"length":0,"stats":{"Line":12}},{"line":644,"address":[],"length":0,"stats":{"Line":6}},{"line":645,"address":[],"length":0,"stats":{"Line":6}},{"line":646,"address":[],"length":0,"stats":{"Line":6}},{"line":647,"address":[],"length":0,"stats":{"Line":3}},{"line":648,"address":[],"length":0,"stats":{"Line":3}},{"line":651,"address":[],"length":0,"stats":{"Line":3}},{"line":652,"address":[],"length":0,"stats":{"Line":1}},{"line":654,"address":[],"length":0,"stats":{"Line":6}},{"line":655,"address":[],"length":0,"stats":{"Line":1}},{"line":657,"address":[],"length":0,"stats":{"Line":3}},{"line":659,"address":[],"length":0,"stats":{"Line":2}},{"line":660,"address":[],"length":0,"stats":{"Line":1}},{"line":662,"address":[],"length":0,"stats":{"Line":1}},{"line":670,"address":[],"length":0,"stats":{"Line":2}},{"line":673,"address":[],"length":0,"stats":{"Line":4}},{"line":674,"address":[],"length":0,"stats":{"Line":3}},{"line":675,"address":[],"length":0,"stats":{"Line":1}},{"line":681,"address":[],"length":0,"stats":{"Line":2}},{"line":684,"address":[],"length":0,"stats":{"Line":4}},{"line":685,"address":[],"length":0,"stats":{"Line":3}},{"line":686,"address":[],"length":0,"stats":{"Line":1}},{"line":692,"address":[],"length":0,"stats":{"Line":2}},{"line":698,"address":[],"length":0,"stats":{"Line":4}},{"line":699,"address":[],"length":0,"stats":{"Line":1}},{"line":700,"address":[],"length":0,"stats":{"Line":4}},{"line":701,"address":[],"length":0,"stats":{"Line":4}},{"line":703,"address":[],"length":0,"stats":{"Line":1}},{"line":709,"address":[],"length":0,"stats":{"Line":4}},{"line":714,"address":[],"length":0,"stats":{"Line":8}},{"line":715,"address":[],"length":0,"stats":{"Line":3}},{"line":716,"address":[],"length":0,"stats":{"Line":12}},{"line":717,"address":[],"length":0,"stats":{"Line":12}},{"line":719,"address":[],"length":0,"stats":{"Line":4}}],"covered":296,"coverable":315},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","encoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/encoder.h` exports.\n//!\n//! Complete inventory: init / free / set_video_option / open / write_video\n//! / write_audio / write_subtitle / flush / last_error /\n//! get_desired_pixel_format / export_format_get_extension /\n//! encoding_generate_matrix.\n//!\n//! # CPP-PARITY\n//! The C++ `c_api/encoder.cpp` box holds an `olive::Encoder` plus the\n//! flattened `EncodingParams`; the Rust equivalent boxes `Mutex\u003cEncoderBox\u003e`\n//! and carries an extra `last_error` field because the Rust `Encoder` trait\n//! has no `get_error()` (the C++ reads the message off the encoder). The\n//! `oakcodec_encoding_params` POD below mirrors the header verbatim; note\n//! that the crate's `EncodingParams::video_pixel_format` / `audio_sample_format`\n//! are typed enums, so `to_native` converts the int fields.\n\nuse std::ffi::{c_char, c_int};\nuse std::sync::{Arc, Mutex};\n\nuse oakcore_rs::PixelFormat;\n\nuse crate::encodingparams::EncodingParams;\nuse crate::encoder::Encoder;\nuse crate::handle::{self, CHandle};\n\n/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/\n/// encoder.h` (all fields; a zeroed struct describes an all-tracks-disabled\n/// configuration). Field names and order mirror the header verbatim.\n#[allow(missing_docs)]\n#[repr(C)]\npub struct oakcodec_encoding_params {\n\tpub filename: [u8; 1024],\n\tpub format: c_int,\n\tpub video_enabled: c_int,\n\tpub video_codec: c_int,\n\tpub video_width: c_int,\n\tpub video_height: c_int,\n\tpub video_time_base_num: c_int,\n\tpub video_time_base_den: c_int,\n\tpub video_pixel_format: c_int,\n\tpub video_interlacing: c_int,\n\tpub video_pixel_aspect_num: c_int,\n\tpub video_pixel_aspect_den: c_int,\n\tpub video_bit_rate: i64,\n\tpub video_min_bit_rate: i64,\n\tpub video_max_bit_rate: i64,\n\tpub video_buffer_size: i64,\n\tpub video_threads: c_int,\n\tpub video_pix_fmt: [u8; 64],\n\tpub video_is_image_sequence: c_int,\n\tpub video_scaling_method: c_int,\n\tpub audio_enabled: c_int,\n\tpub audio_codec: c_int,\n\tpub audio_sample_rate: c_int,\n\tpub audio_channel_layout: u64,\n\tpub audio_sample_format: c_int,\n\tpub audio_bit_rate: i64,\n\tpub subtitles_enabled: c_int,\n\tpub subtitles_codec: c_int,\n\tpub subtitles_are_sidecar: c_int,\n\tpub subtitles_sidecar_format: c_int,\n\tpub color_transform_output: [u8; 256],\n\tpub export_length_num: c_int,\n\tpub export_length_den: c_int,\n\tpub has_custom_range: c_int,\n\tpub custom_range_in_num: i64,\n\tpub custom_range_in_den: i64,\n\tpub custom_range_out_num: i64,\n\tpub custom_range_out_den: i64,\n}\n\n/// Box behind an encoder handle (`EncoderBox` in `c_api/encoder.cpp`).\nstruct EncoderBox {\n\tencoder: Option\u003cArc\u003cdyn Encoder\u003e\u003e,\n\tparams: EncodingParams,\n\t/// Per-codec video options set via `oakcodec_encoder_set_video_option`\n\t/// between init and open. Kept here (not in [`EncodingParams`], which\n\t/// is a byte-exact mirror of the C POD).\n\tvideo_opts: Vec\u003c(String, String)\u003e,\n\topen: bool,\n\tflushed: bool,\n\t/// Last error detail (the C++ reads it off the encoder's `get_error`).\n\tlast_error: String,\n}\n\n/// Convert an `OakPixelFormat` int code to a [`PixelFormat`].\nfn pixel_format_from_i32(v: c_int) -\u003e PixelFormat {\n\tmatch v {\n\t\t0 =\u003e PixelFormat::U8,\n\t\t1 =\u003e PixelFormat::U10,\n\t\t2 =\u003e PixelFormat::U16,\n\t\t3 =\u003e PixelFormat::F16,\n\t\t4 =\u003e PixelFormat::F32,\n\t\t_ =\u003e PixelFormat::Invalid,\n\t}\n}\n\n/// Flatten the C POD into the crate's [`EncodingParams`]\n/// (`to_native` in `c_api/encoder.cpp`).\nfn to_native(p: \u0026oakcodec_encoding_params) -\u003e EncodingParams {\n\tlet mut n = EncodingParams::default();\n\tn.filename = p.filename;\n\tn.format = p.format;\n\n\tn.video_enabled = p.video_enabled;\n\tn.video_codec = p.video_codec;\n\tn.video_width = p.video_width;\n\tn.video_height = p.video_height;\n\tn.video_time_base_num = p.video_time_base_num;\n\tn.video_time_base_den = p.video_time_base_den;\n\tn.video_pixel_format = pixel_format_from_i32(p.video_pixel_format);\n\tn.video_interlacing = p.video_interlacing;\n\tn.video_pixel_aspect_num = p.video_pixel_aspect_num;\n\tn.video_pixel_aspect_den = p.video_pixel_aspect_den;\n\tn.video_bit_rate = p.video_bit_rate;\n\tn.video_min_bit_rate = p.video_min_bit_rate;\n\tn.video_max_bit_rate = p.video_max_bit_rate;\n\tn.video_buffer_size = p.video_buffer_size;\n\tn.video_threads = p.video_threads;\n\tn.video_pix_fmt = p.video_pix_fmt;\n\tn.video_is_image_sequence = p.video_is_image_sequence;\n\tn.video_scaling_method = crate::encodingparams::scaling_from_i32(p.video_scaling_method);\n\n\tn.audio_enabled = p.audio_enabled;\n\tn.audio_codec = p.audio_codec;\n\tn.audio_sample_rate = p.audio_sample_rate;\n\tn.audio_channel_layout = p.audio_channel_layout;\n\tn.audio_sample_format = crate::encodingparams::sample_format_from_i32(p.audio_sample_format);\n\tn.audio_bit_rate = p.audio_bit_rate;\n\n\tn.subtitles_enabled = p.subtitles_enabled;\n\tn.subtitles_codec = p.subtitles_codec;\n\tn.subtitles_are_sidecar = p.subtitles_are_sidecar;\n\tn.subtitles_sidecar_format = p.subtitles_sidecar_format;\n\n\tn.color_transform_output = p.color_transform_output;\n\tn.export_length_num = p.export_length_num;\n\tn.export_length_den = p.export_length_den;\n\tn.has_custom_range = p.has_custom_range;\n\tn.custom_range_in_num = p.custom_range_in_num;\n\tn.custom_range_in_den = p.custom_range_in_den;\n\tn.custom_range_out_num = p.custom_range_out_num;\n\tn.custom_range_out_den = p.custom_range_out_den;\n\tn\n}\n\n/// `EncodingParams::is_valid` — the C++ `src/codec/src/encoder.h` checks\n/// only that at least one track is enabled.\nfn is_valid(p: \u0026EncodingParams) -\u003e bool {\n\tp.video_enabled != 0 || p.audio_enabled != 0 || p.subtitles_enabled != 0\n}\n\n/// `oakcodec_encoder_init`: create an encoder for `params` (count 1);\n/// empty handle when the configuration is invalid.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_init(params: *const oakcodec_encoding_params) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tif params.is_null() {\n\t\t\treturn Ok(CHandle::null());\n\t\t}\n\t\tlet native = to_native(unsafe { \u0026*params });\n\t\tif !is_valid(\u0026native) {\n\t\t\treturn Ok(CHandle::null());\n\t\t}\n\t\tOk(handle::make_owned(Mutex::new(EncoderBox {\n\t\t\tencoder: None,\n\t\t\tparams: native,\n\t\t\tvideo_opts: Vec::new(),\n\t\t\topen: false,\n\t\t\tflushed: false,\n\t\t\tlast_error: String::new(),\n\t\t})))\n\t})\n}\n\n/// `oakcodec_encoder_free`: NULL/empty no-op; nulls `ctx` afterwards.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_free(encoder: *mut CHandle) {\n\thandle::guard_void(|| super::free_handle(encoder));\n}\n\n/// `oakcodec_encoder_set_video_option`: set a per-codec video option\n/// (e.g. \"crf\"); only valid between init and open.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_set_video_option(\n\tencoder: CHandle,\n\tkey: *const c_char,\n\tvalue: *const c_char,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet key = match crate::ffi::c_str(key) {\n\t\t\tSome(k) =\u003e k,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet mut b = b.lock().unwrap();\n\t\tif b.open {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tlet value = crate::ffi::c_str(value).unwrap_or_default();\n\t\t// `EncodingParams::set_video_option` replaces an existing key.\n\t\tb.video_opts.retain(|(k, _)| k != \u0026key);\n\t\tb.video_opts.push((key, value));\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_encoder_open`: create the encoder for the configured params,\n/// apply the video options and open the output.\n///\n/// # CPP-PARITY\n/// The C++ `open()` calls `create_from_params` (which applies the options\n/// internally) then `open()`. The Rust trait separates `configure`, so it\n/// is invoked between the two; the box's `video_opts` (set between init\n/// and open) are stored for future wiring but not yet passed to\n/// `configure` (no trait channel carries them in the interim).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_open(encoder: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet mut b = b.lock().unwrap();\n\t\tif b.open {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tlet e = match crate::encoder::create_from_params(\u0026b.params) {\n\t\t\tSome(e) =\u003e e,\n\t\t\tNone =\u003e {\n\t\t\t\tb.last_error = \"failed to create encoder\".to_string();\n\t\t\t\treturn Err(crate::error::Error::Failed(\"failed to create encoder\".to_string()));\n\t\t\t}\n\t\t};\n\t\tif e.configure(\u0026b.params).is_err() {\n\t\t\tb.last_error = \"failed to configure encoder\".to_string();\n\t\t\treturn Err(crate::error::Error::Failed(\"failed to configure encoder\".to_string()));\n\t\t}\n\t\tif e.open().is_err() {\n\t\t\tb.last_error = \"failed to open stream\".to_string();\n\t\t\treturn Err(crate::error::Error::Failed(\"failed to open stream\".to_string()));\n\t\t}\n\t\tb.encoder = Some(e);\n\t\tb.open = true;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_encoder_write_video`: encode one video frame.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_write_video(encoder: CHandle, frame: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet f = super::get_box::\u003cMutex\u003ccrate::frame::Frame\u003e\u003e(\u0026frame)\n\t\t\t.ok_or(crate::error::Error::Invalid)?;\n\t\tlet e = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.flushed || b.encoder.is_none() {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tb.encoder.as_ref().unwrap().clone()\n\t\t};\n\t\tlet f = f.lock().unwrap();\n\t\te.write_video(\u0026f)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"write_video failed\".to_string()))\n\t})\n}\n\n/// `oakcodec_encoder_write_audio`: encode interleaved float audio samples.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_write_audio(\n\tencoder: CHandle,\n\tsamples: *const f32,\n\tframe_count: c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tif (samples.is_null() \u0026\u0026 frame_count \u003e 0) || frame_count \u003c 0 {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet (slice, enc) = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.flushed || b.encoder.is_none() {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tlet channels = b.params.audio_channel_layout.count_ones();\n\t\t\tif channels == 0 {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tlet sample_count = (frame_count as usize).wrapping_mul(channels as usize);\n\t\t\tlet slice: \u0026[f32] = if samples.is_null() {\n\t\t\t\t\u0026[]\n\t\t\t} else {\n\t\t\t\t// SAFETY: the caller guarantees `samples` holds\n\t\t\t\t// `frame_count * channels` floats.\n\t\t\t\tunsafe { std::slice::from_raw_parts(samples, sample_count) }\n\t\t\t};\n\t\t\tlet e = b.encoder.as_ref().unwrap().clone();\n\t\t\t(slice, e)\n\t\t};\n\t\tenc.write_audio(slice, frame_count)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"write_audio failed\".to_string()))\n\t})\n}\n\n/// `oakcodec_encoder_write_subtitle`: encode one subtitle entry.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_write_subtitle(\n\tencoder: CHandle,\n\ttext: *const c_char,\n\tin_seconds: f64,\n\tout_seconds: f64,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet text = match crate::ffi::c_str(text) {\n\t\t\tSome(t) =\u003e t,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet e = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.flushed || b.encoder.is_none() {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tb.encoder.as_ref().unwrap().clone()\n\t\t};\n\t\te.write_subtitle(\u0026text, in_seconds, out_seconds)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"write_subtitle failed\".to_string()))\n\t})\n}\n\n/// `oakcodec_encoder_flush`: flush the encoders, write the trailer and\n/// close the file. Idempotent.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_flush(encoder: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet mut b = b.lock().unwrap();\n\t\tif !b.open {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tif b.flushed {\n\t\t\treturn Ok(());\n\t\t}\n\t\tlet e = b.encoder.as_ref().unwrap().clone();\n\t\t// The C++ ignores the close() result; the Rust interim surfaces it.\n\t\te.close()\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"close failed\".to_string()))?;\n\t\tb.flushed = true;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_encoder_last_error` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_last_error(\n\tencoder: CHandle,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder) {\n\t\tSome(b) =\u003e {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tsuper::string_out(\u0026b.last_error, buf, buf_size)\n\t\t}\n\t\tNone =\u003e super::string_out(\"\", buf, buf_size),\n\t})\n}\n\n/// `oakcodec_encoder_get_desired_pixel_format`: the pixel format the\n/// encoder wants frames in, or -1 when unknown; `OAKCODEC_E_INVALID` for\n/// an empty/invalid encoder.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_get_desired_pixel_format(encoder: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet b = match super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder) {\n\t\t\tSome(b) =\u003e b,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet e = match \u0026b.lock().unwrap().encoder {\n\t\t\tSome(e) =\u003e e.clone(),\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tmatch e.desired_pixel_format() {\n\t\t\tSome(p) =\u003e p as c_int,\n\t\t\tNone =\u003e -1,\n\t\t}\n\t})\n}\n\n/// `oakcodec_export_format_get_extension` (two-stage); unknown formats\n/// yield the empty string.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_export_format_get_extension(\n\tformat: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet ext = match crate::exportformat::Format::from_i32(format) {\n\t\t\tSome(f) =\u003e crate::exportformat::Format::get_extension(f),\n\t\t\tNone =\u003e String::new(),\n\t\t};\n\t\tsuper::string_out(\u0026ext, buf, buf_size)\n\t})\n}\n\n/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling\n/// method, row-major 4x4 `double` into `out_matrix[16]`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoding_generate_matrix(\n\tmethod: c_int,\n\tsrc_width: c_int,\n\tsrc_height: c_int,\n\tdst_width: c_int,\n\tdst_height: c_int,\n\tout_matrix: *mut f64,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out_matrix.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet mut m = [0.0f64; 16];\n\t\tcrate::encodingparams::EncodingParams::generate_matrix(\n\t\t\tcrate::encodingparams::scaling_from_i32(method),\n\t\t\tsrc_width,\n\t\t\tsrc_height,\n\t\t\tdst_width,\n\t\t\tdst_height,\n\t\t\t\u0026mut m,\n\t\t);\n\t\t// SAFETY: the caller guarantees `out_matrix` holds 16 doubles.\n\t\tunsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) };\n\t\tOk(())\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\tuse crate::encoder::set_test_encoders;\n\tuse crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};\n\tuse crate::ffi::frame::{oakcodec_frame_allocate, oakcodec_frame_free, oakcodec_frame_init_with_params};\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn zeroed_params() -\u003e oakcodec_encoding_params {\n\t\tunsafe { std::mem::zeroed() }\n\t}\n\n\tfn valid_params() -\u003e oakcodec_encoding_params {\n\t\tlet mut p = zeroed_params();\n\t\tp.filename = {\n\t\t\tlet mut f = [0u8; 1024];\n\t\t\tlet name = b\"out.mp4\";\n\t\t\tf[..name.len()].copy_from_slice(name);\n\t\t\tf\n\t\t};\n\t\tp.format = 2; // MPEG-4\n\t\tp.video_enabled = 1;\n\t\tp.video_codec = 3;\n\t\tp.video_width = 1920;\n\t\tp.video_height = 1080;\n\t\tp.video_time_base_num = 1;\n\t\tp.video_time_base_den = 30;\n\t\tp.video_pixel_format = 0; // U8\n\t\tp.audio_enabled = 1;\n\t\tp.audio_codec = 4;\n\t\tp.audio_sample_rate = 48000;\n\t\tp.audio_channel_layout = 0x3;\n\t\tp.audio_sample_format = 10; // f32 packed\n\t\tp\n\t}\n\n\t/// Fake encoder that accepts every operation.\n\tstruct FakeEncoder {\n\t\tid: \u0026'static str,\n\t}\n\n\timpl Encoder for FakeEncoder {\n\t\tfn id(\u0026self) -\u003e String {\n\t\t\tself.id.to_string()\n\t\t}\n\t\tfn supports_video(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\t\tfn supports_audio(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\t\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\t\tfn configure(\u0026self, _p: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_video(\u0026self, _frame: \u0026crate::frame::Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_audio(\u0026self, _samples: \u0026[f32], _frame_count: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_subtitle(\n\t\t\t\u0026self,\n\t\t\t_text: \u0026str,\n\t\t\t_in_seconds: f64,\n\t\t\t_out_seconds: f64,\n\t\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e {\n\t\t\tSome(PixelFormat::U8)\n\t\t}\n\t\tfn desired_sample_format(\u0026self) -\u003e Option\u003coakcore_rs::SampleFormat\u003e {\n\t\t\tNone\n\t\t}\n\t\tfn filename(\u0026self) -\u003e String {\n\t\t\t\"out.mp4\".to_string()\n\t\t}\n\t}\n\n\t#[test]\n\tfn init_open_write_flush_golden() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\tlet p = valid_params();\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\n\t\t// set_video_option between init and open.\n\t\tlet key = cstr(\"crf\");\n\t\tlet val = cstr(\"18\");\n\t\tlet rc = unsafe { oakcodec_encoder_set_video_option(h, key.as_ptr(), val.as_ptr()) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\tlet rc = unsafe { oakcodec_encoder_open(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// write_video with a real frame handle.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(16, 16) };\n\t\tlet mut fh = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(fh) }, crate::error::OAKCODEC_OK);\n\t\tlet rc = unsafe { oakcodec_encoder_write_video(h, fh) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// write_audio: stereo interleaved floats.\n\t\tlet mut samples = [0f32; 64];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// write_subtitle.\n\t\tlet text = cstr(\"hello\");\n\t\tlet rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 2.5) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// desired pixel format from the fake.\n\t\tassert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(h) }, 0); // U8\n\n\t\t// flush is idempotent.\n\t\tlet rc = unsafe { oakcodec_encoder_flush(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tlet rc = unsafe { oakcodec_encoder_flush(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// Writes after flush -\u003e E_STATE.\n\t\tlet rc = unsafe { oakcodec_encoder_write_video(h, fh) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 1.0) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut fh) };\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tassert!(h.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn init_invalid_config_and_null_params() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(Vec::new());\n\n\t\t// NULL params -\u003e empty handle.\n\t\tlet mut h = unsafe { oakcodec_encoder_init(std::ptr::null()) };\n\t\tassert!(h.is_null());\n\n\t\t// All tracks disabled -\u003e empty handle (is_valid).\n\t\tlet p = zeroed_params();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(h.is_null());\n\n\t\t// Audio-only config is valid.\n\t\tlet mut p = zeroed_params();\n\t\tp.format = 7; // WAV\n\t\tp.audio_enabled = 1;\n\t\tp.audio_sample_rate = 44100;\n\t\tp.audio_channel_layout = 0x4;\n\t\tp.audio_sample_format = 10;\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\t#[test]\n\tfn open_errors_and_state_machine() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\t// Production path: an unknown export format cannot create an encoder.\n\t\tset_test_encoders(Vec::new());\n\n\t\tlet mut p = valid_params();\n\t\tp.format = 99; // unknown format -\u003e create_from_params returns None\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\n\t\tlet rc = unsafe { oakcodec_encoder_open(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_FAILED);\n\t\tlet mut err = [0i8; 128];\n\t\tunsafe { oakcodec_encoder_last_error(h, err.as_mut_ptr(), 128) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"failed to create encoder\"));\n\n\t\t// Empty handle -\u003e E_INVALID; last_error empty.\n\t\tlet empty = CHandle::null();\n\t\tassert_eq!(unsafe { oakcodec_encoder_open(empty) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_encoder_set_video_option(empty, cstr(\"crf\").as_ptr(), cstr(\"18\").as_ptr()) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(empty) }, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_encoder_last_error(empty, err.as_mut_ptr(), 128) };\n\t\tassert_eq!(rc, 1);\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"\"));\n\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t}\n\n\t#[test]\n\tfn state_and_argument_errors_with_fake() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\tlet p = valid_params();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\n\t\t// set_video_option with a NULL key -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_encoder_set_video_option(h, std::ptr::null(), std::ptr::null()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Writes before open -\u003e E_STATE.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(4, 4) };\n\t\tlet mut fh = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tlet rc = unsafe { oakcodec_encoder_write_video(h, fh) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet rc = unsafe { oakcodec_encoder_flush(h) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\t// Null samples with a positive count is an argument error (checked\n\t\t// before the state, matching the C++ validation order).\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\t// Valid args before open -\u003e E_STATE.\n\t\tlet mut pre = [0f32; 8];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, pre.as_ptr(), 4) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut fh) };\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn write_audio_argument_validation() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\tlet p = valid_params();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);\n\n\t\t// NULL samples with a positive frame count -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Negative frame count -\u003e E_INVALID.\n\t\tlet samples = [0f32; 8];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), -1) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn write_audio_zero_channels_is_state() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\t// Audio enabled but empty channel layout -\u003e E_STATE at write time.\n\t\tlet mut p = zeroed_params();\n\t\tp.format = 7;\n\t\tp.audio_enabled = 1;\n\t\tp.audio_sample_rate = 44100;\n\t\tp.audio_channel_layout = 0;\n\t\tp.audio_sample_format = 10;\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);\n\t\tlet mut samples = [0f32; 8];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 4) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn export_format_extension_and_generate_matrix() {\n\t\tlet mut buf = [0i8; 64];\n\t\tlet rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 4); // \"mp4\" + NUL\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"mp4\"));\n\n\t\t// Unknown format -\u003e empty string (size 1 for the NUL).\n\t\tlet rc = unsafe { oakcodec_export_format_get_extension(99, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 1);\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"\"));\n\n\t\t// Truncation rule: small buffer writes buf_size-1 chars + NUL.\n\t\tlet rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 3) };\n\t\tassert_eq!(rc, 4); // required size unchanged\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"mp\"));\n\n\t\t// generate_matrix: Stretch (1) is the identity.\n\t\tlet mut m = [9.0f64; 16];\n\t\tlet rc = unsafe { oakcodec_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(m[0], 1.0);\n\t\tassert_eq!(m[5], 1.0);\n\t\tassert_eq!(m[10], 1.0);\n\t\tassert_eq!(m[15], 1.0);\n\n\t\t// Fit (0) with a square source into a 2:1 destination scales x.\n\t\tlet mut m = [0.0f64; 16];\n\t\tlet rc = unsafe { oakcodec_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert!((m[0] - 0.5).abs() \u003c 1e-9);\n\t\tassert_eq!(m[5], 1.0);\n\n\t\t// NULL out_matrix -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_encoding_generate_matrix(0, 1, 1, 2, 2, std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n}\n","traces":[{"line":103,"address":[],"length":0,"stats":{"Line":7}},{"line":104,"address":[],"length":0,"stats":{"Line":7}},{"line":105,"address":[],"length":0,"stats":{"Line":7}},{"line":106,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":0}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":110,"address":[],"length":0,"stats":{"Line":0}},{"line":116,"address":[],"length":0,"stats":{"Line":7}},{"line":117,"address":[],"length":0,"stats":{"Line":14}},{"line":118,"address":[],"length":0,"stats":{"Line":7}},{"line":119,"address":[],"length":0,"stats":{"Line":7}},{"line":121,"address":[],"length":0,"stats":{"Line":7}},{"line":122,"address":[],"length":0,"stats":{"Line":7}},{"line":123,"address":[],"length":0,"stats":{"Line":7}},{"line":124,"address":[],"length":0,"stats":{"Line":7}},{"line":125,"address":[],"length":0,"stats":{"Line":7}},{"line":126,"address":[],"length":0,"stats":{"Line":7}},{"line":127,"address":[],"length":0,"stats":{"Line":7}},{"line":128,"address":[],"length":0,"stats":{"Line":7}},{"line":129,"address":[],"length":0,"stats":{"Line":7}},{"line":130,"address":[],"length":0,"stats":{"Line":7}},{"line":131,"address":[],"length":0,"stats":{"Line":7}},{"line":132,"address":[],"length":0,"stats":{"Line":7}},{"line":133,"address":[],"length":0,"stats":{"Line":7}},{"line":134,"address":[],"length":0,"stats":{"Line":7}},{"line":135,"address":[],"length":0,"stats":{"Line":7}},{"line":136,"address":[],"length":0,"stats":{"Line":7}},{"line":137,"address":[],"length":0,"stats":{"Line":7}},{"line":138,"address":[],"length":0,"stats":{"Line":7}},{"line":140,"address":[],"length":0,"stats":{"Line":7}},{"line":141,"address":[],"length":0,"stats":{"Line":7}},{"line":142,"address":[],"length":0,"stats":{"Line":7}},{"line":143,"address":[],"length":0,"stats":{"Line":7}},{"line":144,"address":[],"length":0,"stats":{"Line":7}},{"line":145,"address":[],"length":0,"stats":{"Line":7}},{"line":147,"address":[],"length":0,"stats":{"Line":7}},{"line":148,"address":[],"length":0,"stats":{"Line":7}},{"line":149,"address":[],"length":0,"stats":{"Line":7}},{"line":150,"address":[],"length":0,"stats":{"Line":7}},{"line":152,"address":[],"length":0,"stats":{"Line":7}},{"line":153,"address":[],"length":0,"stats":{"Line":7}},{"line":154,"address":[],"length":0,"stats":{"Line":7}},{"line":155,"address":[],"length":0,"stats":{"Line":7}},{"line":156,"address":[],"length":0,"stats":{"Line":7}},{"line":157,"address":[],"length":0,"stats":{"Line":7}},{"line":158,"address":[],"length":0,"stats":{"Line":7}},{"line":159,"address":[],"length":0,"stats":{"Line":7}},{"line":160,"address":[],"length":0,"stats":{"Line":7}},{"line":165,"address":[],"length":0,"stats":{"Line":7}},{"line":166,"address":[],"length":0,"stats":{"Line":11}},{"line":172,"address":[],"length":0,"stats":{"Line":8}},{"line":173,"address":[],"length":0,"stats":{"Line":16}},{"line":174,"address":[],"length":0,"stats":{"Line":16}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":177,"address":[],"length":0,"stats":{"Line":21}},{"line":178,"address":[],"length":0,"stats":{"Line":7}},{"line":179,"address":[],"length":0,"stats":{"Line":1}},{"line":181,"address":[],"length":0,"stats":{"Line":18}},{"line":182,"address":[],"length":0,"stats":{"Line":12}},{"line":183,"address":[],"length":0,"stats":{"Line":12}},{"line":184,"address":[],"length":0,"stats":{"Line":6}},{"line":185,"address":[],"length":0,"stats":{"Line":6}},{"line":186,"address":[],"length":0,"stats":{"Line":6}},{"line":187,"address":[],"length":0,"stats":{"Line":6}},{"line":194,"address":[],"length":0,"stats":{"Line":6}},{"line":195,"address":[],"length":0,"stats":{"Line":18}},{"line":201,"address":[],"length":0,"stats":{"Line":3}},{"line":206,"address":[],"length":0,"stats":{"Line":6}},{"line":207,"address":[],"length":0,"stats":{"Line":15}},{"line":208,"address":[],"length":0,"stats":{"Line":3}},{"line":209,"address":[],"length":0,"stats":{"Line":2}},{"line":210,"address":[],"length":0,"stats":{"Line":1}},{"line":212,"address":[],"length":0,"stats":{"Line":4}},{"line":213,"address":[],"length":0,"stats":{"Line":1}},{"line":214,"address":[],"length":0,"stats":{"Line":0}},{"line":216,"address":[],"length":0,"stats":{"Line":4}},{"line":218,"address":[],"length":0,"stats":{"Line":2}},{"line":219,"address":[],"length":0,"stats":{"Line":3}},{"line":220,"address":[],"length":0,"stats":{"Line":1}},{"line":234,"address":[],"length":0,"stats":{"Line":5}},{"line":235,"address":[],"length":0,"stats":{"Line":10}},{"line":236,"address":[],"length":0,"stats":{"Line":25}},{"line":237,"address":[],"length":0,"stats":{"Line":16}},{"line":238,"address":[],"length":0,"stats":{"Line":4}},{"line":239,"address":[],"length":0,"stats":{"Line":0}},{"line":241,"address":[],"length":0,"stats":{"Line":7}},{"line":242,"address":[],"length":0,"stats":{"Line":6}},{"line":244,"address":[],"length":0,"stats":{"Line":3}},{"line":245,"address":[],"length":0,"stats":{"Line":1}},{"line":248,"address":[],"length":0,"stats":{"Line":9}},{"line":249,"address":[],"length":0,"stats":{"Line":0}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":6}},{"line":253,"address":[],"length":0,"stats":{"Line":0}},{"line":254,"address":[],"length":0,"stats":{"Line":0}},{"line":256,"address":[],"length":0,"stats":{"Line":6}},{"line":257,"address":[],"length":0,"stats":{"Line":3}},{"line":258,"address":[],"length":0,"stats":{"Line":3}},{"line":264,"address":[],"length":0,"stats":{"Line":3}},{"line":265,"address":[],"length":0,"stats":{"Line":6}},{"line":266,"address":[],"length":0,"stats":{"Line":15}},{"line":267,"address":[],"length":0,"stats":{"Line":9}},{"line":268,"address":[],"length":0,"stats":{"Line":6}},{"line":269,"address":[],"length":0,"stats":{"Line":1}},{"line":270,"address":[],"length":0,"stats":{"Line":12}},{"line":271,"address":[],"length":0,"stats":{"Line":7}},{"line":272,"address":[],"length":0,"stats":{"Line":2}},{"line":274,"address":[],"length":0,"stats":{"Line":2}},{"line":276,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":2}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":284,"address":[],"length":0,"stats":{"Line":7}},{"line":289,"address":[],"length":0,"stats":{"Line":14}},{"line":290,"address":[],"length":0,"stats":{"Line":35}},{"line":291,"address":[],"length":0,"stats":{"Line":21}},{"line":292,"address":[],"length":0,"stats":{"Line":3}},{"line":294,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":16}},{"line":296,"address":[],"length":0,"stats":{"Line":11}},{"line":297,"address":[],"length":0,"stats":{"Line":2}},{"line":299,"address":[],"length":0,"stats":{"Line":6}},{"line":300,"address":[],"length":0,"stats":{"Line":2}},{"line":301,"address":[],"length":0,"stats":{"Line":1}},{"line":303,"address":[],"length":0,"stats":{"Line":4}},{"line":304,"address":[],"length":0,"stats":{"Line":4}},{"line":305,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":2}},{"line":311,"address":[],"length":0,"stats":{"Line":4}},{"line":312,"address":[],"length":0,"stats":{"Line":1}},{"line":314,"address":[],"length":0,"stats":{"Line":3}},{"line":315,"address":[],"length":0,"stats":{"Line":1}},{"line":321,"address":[],"length":0,"stats":{"Line":2}},{"line":327,"address":[],"length":0,"stats":{"Line":4}},{"line":328,"address":[],"length":0,"stats":{"Line":10}},{"line":329,"address":[],"length":0,"stats":{"Line":4}},{"line":330,"address":[],"length":0,"stats":{"Line":4}},{"line":331,"address":[],"length":0,"stats":{"Line":0}},{"line":333,"address":[],"length":0,"stats":{"Line":1}},{"line":334,"address":[],"length":0,"stats":{"Line":8}},{"line":335,"address":[],"length":0,"stats":{"Line":6}},{"line":336,"address":[],"length":0,"stats":{"Line":1}},{"line":338,"address":[],"length":0,"stats":{"Line":2}},{"line":340,"address":[],"length":0,"stats":{"Line":4}},{"line":341,"address":[],"length":0,"stats":{"Line":1}},{"line":348,"address":[],"length":0,"stats":{"Line":3}},{"line":349,"address":[],"length":0,"stats":{"Line":6}},{"line":350,"address":[],"length":0,"stats":{"Line":15}},{"line":351,"address":[],"length":0,"stats":{"Line":12}},{"line":352,"address":[],"length":0,"stats":{"Line":3}},{"line":353,"address":[],"length":0,"stats":{"Line":1}},{"line":355,"address":[],"length":0,"stats":{"Line":2}},{"line":356,"address":[],"length":0,"stats":{"Line":1}},{"line":358,"address":[],"length":0,"stats":{"Line":4}},{"line":360,"address":[],"length":0,"stats":{"Line":1}},{"line":361,"address":[],"length":0,"stats":{"Line":1}},{"line":362,"address":[],"length":0,"stats":{"Line":1}},{"line":363,"address":[],"length":0,"stats":{"Line":1}},{"line":369,"address":[],"length":0,"stats":{"Line":2}},{"line":374,"address":[],"length":0,"stats":{"Line":4}},{"line":375,"address":[],"length":0,"stats":{"Line":1}},{"line":376,"address":[],"length":0,"stats":{"Line":4}},{"line":377,"address":[],"length":0,"stats":{"Line":4}},{"line":379,"address":[],"length":0,"stats":{"Line":4}},{"line":387,"address":[],"length":0,"stats":{"Line":2}},{"line":388,"address":[],"length":0,"stats":{"Line":4}},{"line":389,"address":[],"length":0,"stats":{"Line":3}},{"line":390,"address":[],"length":0,"stats":{"Line":2}},{"line":391,"address":[],"length":0,"stats":{"Line":1}},{"line":393,"address":[],"length":0,"stats":{"Line":3}},{"line":394,"address":[],"length":0,"stats":{"Line":3}},{"line":395,"address":[],"length":0,"stats":{"Line":0}},{"line":397,"address":[],"length":0,"stats":{"Line":1}},{"line":398,"address":[],"length":0,"stats":{"Line":2}},{"line":399,"address":[],"length":0,"stats":{"Line":0}},{"line":407,"address":[],"length":0,"stats":{"Line":3}},{"line":412,"address":[],"length":0,"stats":{"Line":6}},{"line":413,"address":[],"length":0,"stats":{"Line":6}},{"line":414,"address":[],"length":0,"stats":{"Line":6}},{"line":415,"address":[],"length":0,"stats":{"Line":1}},{"line":417,"address":[],"length":0,"stats":{"Line":12}},{"line":424,"address":[],"length":0,"stats":{"Line":3}},{"line":432,"address":[],"length":0,"stats":{"Line":6}},{"line":433,"address":[],"length":0,"stats":{"Line":6}},{"line":434,"address":[],"length":0,"stats":{"Line":1}},{"line":436,"address":[],"length":0,"stats":{"Line":4}},{"line":437,"address":[],"length":0,"stats":{"Line":2}},{"line":438,"address":[],"length":0,"stats":{"Line":4}},{"line":439,"address":[],"length":0,"stats":{"Line":2}},{"line":440,"address":[],"length":0,"stats":{"Line":2}},{"line":441,"address":[],"length":0,"stats":{"Line":2}},{"line":442,"address":[],"length":0,"stats":{"Line":2}},{"line":443,"address":[],"length":0,"stats":{"Line":2}},{"line":446,"address":[],"length":0,"stats":{"Line":8}},{"line":447,"address":[],"length":0,"stats":{"Line":2}}],"covered":180,"coverable":195},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","frame.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/frame.h` exports.\n//!\n//! Complete inventory: frame_init / init_with_params / free / get_params /\n//! set_params / allocate / is_allocated / data / const_data /\n//! allocated_size / linesize_bytes / linesize_pixels / width / height /\n//! format / channel_count / get_timestamp / set_timestamp /\n//! debug_alive_count.\n//!\n//! # CPP-PARITY\n//! The C++ `c_api/frame.cpp` boxes every `OakFrame` handle with an\n//! `olive::FramePtr` (a shared pointer), so decoder-produced frames may\n//! alias the decoder's internal cache. The Rust equivalent boxes\n//! `Mutex\u003cFrame\u003e`; a decode that hands out a still-shared `Arc\u003cFrame\u003e`\n//! therefore cannot be aliased here and reports an empty handle instead\n//! (see `ffi::decoder`).\n\nuse std::ffi::{c_int, c_void};\nuse std::panic::{catch_unwind, AssertUnwindSafe};\nuse std::sync::Mutex;\n\nuse oakcore_rs::Rational;\n\nuse crate::bridge::common::OakVideoParams;\nuse crate::frame::Frame;\nuse crate::handle::{self, CHandle};\n\n/// `OAKCOMMON_PIXEL_FORMAT_INVALID` (oakcommon `common/videoparams.h`).\nconst OAKCOMMON_PIXEL_FORMAT_INVALID: c_int = -1;\n\n/// `oakcodec_frame_init`: new frame with default (invalid) params,\n/// refcount 1.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_init() -\u003e CHandle {\n\thandle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::new()))))\n}\n\n/// `oakcodec_frame_init_with_params`: new frame holding a copy of `params`\n/// (the handle is addref'd internally); buffer unallocated.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_init_with_params(params: OakVideoParams) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tOk(handle::make_owned(Mutex::new(Frame::with_params(params))))\n\t})\n}\n\n/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_free(frame: *mut CHandle) {\n\thandle::guard_void(|| super::free_handle(frame));\n}\n\n/// `oakcodec_frame_get_params`: copy of the frame's parameter set.\n///\n/// The copy is addref'd: the caller must release it with\n/// `oakcommon_videoparams_free` (see the header contract). Test-stub\n/// handles carry no `addref`, so the caller must not free them.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_get_params(\n\tframe: CHandle,\n\tout: *mut OakVideoParams,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tlet f = f.lock().unwrap();\n\t\tlet p = f.params().cloned().ok_or(crate::error::Error::Invalid)?;\n\t\tcrate::frame::params_addref(\u0026p);\n\t\t// SAFETY: the caller guarantees `out` points to a writable\n\t\t// `OakVideoParams`.\n\t\tunsafe { *out = p };\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_frame_set_params`: replace the parameter set (addref'd\n/// internally); recomputes line sizes, does not reallocate.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_set_params(\n\tframe: CHandle,\n\tparams: OakVideoParams,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tf.lock().unwrap().set_params(params);\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_frame_allocate`: allocate the pixel buffer from the current\n/// params; `OAKCODEC_E_STATE` when the params are invalid.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_allocate(frame: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tf.lock().unwrap().allocate()\n\t})\n}\n\n/// `oakcodec_frame_is_allocated`: 1 when the buffer is allocated.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_is_allocated(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tlet f = f.lock().unwrap();\n\t\tif f.is_allocated() {\n\t\t\t1\n\t\t} else {\n\t\t\t0\n\t\t}\n\t})\n}\n\n/// `oakcodec_frame_data`: writable pixel buffer, NULL when\n/// unallocated/empty.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_data(frame: CHandle) -\u003e *mut c_void {\n\tmatch catch_unwind(AssertUnwindSafe(|| unsafe { frame_data_inner(\u0026frame) })) {\n\t\tOk(p) =\u003e p,\n\t\tErr(_) =\u003e std::ptr::null_mut(),\n\t}\n}\n\n/// `oakcodec_frame_const_data`: const variant of `oakcodec_frame_data`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_const_data(frame: CHandle) -\u003e *const c_void {\n\tmatch catch_unwind(AssertUnwindSafe(|| unsafe { frame_const_data_inner(\u0026frame) })) {\n\t\tOk(p) =\u003e p,\n\t\tErr(_) =\u003e std::ptr::null_mut(),\n\t}\n}\n\nunsafe fn frame_data_inner(frame: \u0026CHandle) -\u003e *mut c_void {\n\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(frame) {\n\t\tSome(f) =\u003e f,\n\t\tNone =\u003e return std::ptr::null_mut(),\n\t};\n\tmatch f.lock().unwrap().data_mut() {\n\t\tSome(d) =\u003e d.as_mut_ptr() as *mut c_void,\n\t\tNone =\u003e std::ptr::null_mut(),\n\t}\n}\n\nunsafe fn frame_const_data_inner(frame: \u0026CHandle) -\u003e *const c_void {\n\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(frame) {\n\t\tSome(f) =\u003e f,\n\t\tNone =\u003e return std::ptr::null(),\n\t};\n\tmatch f.lock().unwrap().data() {\n\t\tSome(d) =\u003e d.as_ptr() as *const c_void,\n\t\tNone =\u003e std::ptr::null(),\n\t}\n}\n\n/// `oakcodec_frame_allocated_size`: size of the pixel buffer in bytes\n/// (0 when unallocated).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_allocated_size(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().allocated_size() as c_int\n\t})\n}\n\n/// `oakcodec_frame_linesize_bytes`: distance between two rows in bytes\n/// (0 when params are unset).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_linesize_bytes(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().linesize_bytes()\n\t})\n}\n\n/// `oakcodec_frame_linesize_pixels`: distance between two rows in pixels.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_linesize_pixels(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().linesize_pixels()\n\t})\n}\n\n/// `oakcodec_frame_width`: frame width (0 when params are empty).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_width(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().width()\n\t})\n}\n\n/// `oakcodec_frame_height`: frame height (0 when params are empty).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_height(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().height()\n\t})\n}\n\n/// `oakcodec_frame_format`: pixel format as an `OakPixelFormat` value;\n/// `OAKCOMMON_PIXEL_FORMAT_INVALID` on an empty handle.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_format(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return OAKCOMMON_PIXEL_FORMAT_INVALID,\n\t\t};\n\t\tf.lock().unwrap().format() as c_int\n\t})\n}\n\n/// `oakcodec_frame_channel_count`: plane channel count of the params\n/// format (0 on an empty handle).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_channel_count(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().channel_count()\n\t})\n}\n\n/// `oakcodec_frame_get_timestamp`: frame timestamp as a rational number\n/// of seconds, written through `numerator`/`denominator`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_get_timestamp(\n\tframe: CHandle,\n\tnumerator: *mut c_int,\n\tdenominator: *mut c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif numerator.is_null() || denominator.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tlet f = f.lock().unwrap();\n\t\tlet ts = f.timestamp();\n\t\t// SAFETY: both pointers were range-checked above.\n\t\tunsafe {\n\t\t\t*numerator = ts.numerator() as c_int;\n\t\t\t*denominator = ts.denominator() as c_int;\n\t\t}\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_frame_set_timestamp`: replace the frame timestamp.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_set_timestamp(\n\tframe: CHandle,\n\tnumerator: c_int,\n\tdenominator: c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tf.lock()\n\t\t\t.unwrap()\n\t\t\t.set_timestamp(Rational::new(numerator as i64, denominator as i64));\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_debug_alive_count`: number of live boxed handle objects\n/// across all families (see `crate::handle::alive_count`).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_debug_alive_count() -\u003e c_int {\n\thandle::guard_raw(handle::alive_count)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::{\n\t\toakcommon_videoparams_get_height, oakcommon_videoparams_get_width,\n\t\toakcommon_videoparams_init_basic,\n\t};\n\tuse crate::error::OAKCODEC_E_INVALID;\n\n\t#[test]\n\tfn frame_lifecycle_golden() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(100, 50) };\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tassert!(!h.is_null());\n\t\t// init -\u003e exactly one more live box.\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\n\t\t// get_params round-trips width/height through the stub.\n\t\tlet mut out = empty_params();\n\t\tlet rc = unsafe { oakcodec_frame_get_params(h, \u0026mut out) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\t// NOTE: stub handles carry no addref and `oakcommon_videoparams_free`\n\t\t// would drop the shared box; the test keeps the copy alive for the\n\t\t// frame's lifetime and does not free it.\n\t\tassert_eq!(unsafe { oakcommon_videoparams_get_width(out.clone()) }, 100);\n\t\tassert_eq!(unsafe { oakcommon_videoparams_get_height(out.clone()) }, 50);\n\n\t\tassert_eq!(unsafe { oakcodec_frame_width(h) }, 100);\n\t\tassert_eq!(unsafe { oakcodec_frame_height(h) }, 50);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());\n\n\t\tlet rc = unsafe { oakcodec_frame_allocate(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 1);\n\t\tassert!(!unsafe { oakcodec_frame_data(h) }.is_null());\n\t\t// U8 RGBA: 100px -\u003e 4*128 bytes linesize.\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);\n\t\tassert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);\n\n\t\t// set_timestamp round-trip.\n\t\tassert_eq!(unsafe { oakcodec_frame_set_timestamp(h, 1, 30) }, crate::error::OAKCODEC_OK);\n\t\tlet (mut num, mut den) = (0, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_get_timestamp(h, \u0026mut num, \u0026mut den) }, crate::error::OAKCODEC_OK);\n\t\tassert_eq!((num, den), (1, 30));\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut h) };\n\t\tassert!(h.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\t#[test]\n\tfn frame_errors_and_empty_handles() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet empty = CHandle::null();\n\t\tassert_eq!(unsafe { oakcodec_frame_width(empty) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_format(empty) }, OAKCOMMON_PIXEL_FORMAT_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(empty) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_frame_get_params(empty, std::ptr::null_mut()) }, OAKCODEC_E_INVALID);\n\n\t\t// init_basic(0, 0) is not valid -\u003e allocate rejects with E_STATE.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(0, 0) };\n\t\tlet mut h = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_E_STATE);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);\n\t\tunsafe { oakcodec_frame_free(\u0026mut h) };\n\t}\n\n\t#[test]\n\tfn free_null_and_empty_are_noops() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = handle::alive_count();\n\t\tunsafe { oakcodec_frame_free(std::ptr::null_mut()) };\n\t\tlet mut empty = CHandle::null();\n\t\tunsafe { oakcodec_frame_free(\u0026mut empty) };\n\t\tassert!(empty.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\t#[test]\n\tfn init_set_params_and_query_helpers() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = handle::alive_count();\n\n\t\t// Bare init (no params): invalid params, not allocated. The stub's\n\t\t// default MockParams carries format 0 (U8); the empty-handle -1\n\t\t// case is covered in `frame_errors_and_empty_handles`.\n\t\tlet mut h = unsafe { oakcodec_frame_init() };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\t\tassert_eq!(unsafe { oakcodec_frame_width(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_height(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());\n\t\tassert_eq!(unsafe { oakcodec_frame_const_data(h) }, std::ptr::null());\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_E_STATE);\n\n\t\t// set_params replaces the parameter set and recomputes line sizes.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(100, 50) };\n\t\tlet rc = unsafe { oakcodec_frame_set_params(h, params) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_frame_width(h) }, 100);\n\t\tassert_eq!(unsafe { oakcodec_frame_height(h) }, 50);\n\t\tassert_eq!(unsafe { oakcodec_frame_format(h) }, 0); // U8\n\t\tassert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 128);\n\n\t\t// allocate -\u003e data and const_data point at the buffer.\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_OK);\n\t\tassert!(!unsafe { oakcodec_frame_data(h) }.is_null());\n\t\tassert!(!unsafe { oakcodec_frame_const_data(h) }.is_null());\n\t\tassert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);\n\n\t\t// get_timestamp rejects NULL out pointers.\n\t\tassert_eq!(\n\t\t\tunsafe { oakcodec_frame_get_timestamp(h, std::ptr::null_mut(), std::ptr::null_mut()) },\n\t\t\tOAKCODEC_E_INVALID\n\t\t);\n\n\t\t// debug_alive_count reports the live boxes (\u003e= our own).\n\t\tassert!(unsafe { oakcodec_debug_alive_count() } \u003e= before + 1);\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut h) };\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\tfn empty_params() -\u003e OakVideoParams {\n\t\tOakVideoParams {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: 0,\n\t\t}\n\t}\n}\n","traces":[{"line":49,"address":[],"length":0,"stats":{"Line":1}},{"line":50,"address":[],"length":0,"stats":{"Line":4}},{"line":56,"address":[],"length":0,"stats":{"Line":4}},{"line":57,"address":[],"length":0,"stats":{"Line":8}},{"line":58,"address":[],"length":0,"stats":{"Line":12}},{"line":64,"address":[],"length":0,"stats":{"Line":8}},{"line":65,"address":[],"length":0,"stats":{"Line":24}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":4}},{"line":79,"address":[],"length":0,"stats":{"Line":4}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":5}},{"line":83,"address":[],"length":0,"stats":{"Line":4}},{"line":84,"address":[],"length":0,"stats":{"Line":5}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":1}},{"line":89,"address":[],"length":0,"stats":{"Line":1}},{"line":96,"address":[],"length":0,"stats":{"Line":1}},{"line":100,"address":[],"length":0,"stats":{"Line":2}},{"line":101,"address":[],"length":0,"stats":{"Line":5}},{"line":102,"address":[],"length":0,"stats":{"Line":3}},{"line":103,"address":[],"length":0,"stats":{"Line":1}},{"line":110,"address":[],"length":0,"stats":{"Line":6}},{"line":111,"address":[],"length":0,"stats":{"Line":12}},{"line":112,"address":[],"length":0,"stats":{"Line":30}},{"line":113,"address":[],"length":0,"stats":{"Line":10}},{"line":119,"address":[],"length":0,"stats":{"Line":4}},{"line":120,"address":[],"length":0,"stats":{"Line":8}},{"line":121,"address":[],"length":0,"stats":{"Line":8}},{"line":122,"address":[],"length":0,"stats":{"Line":8}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":16}},{"line":126,"address":[],"length":0,"stats":{"Line":4}},{"line":127,"address":[],"length":0,"stats":{"Line":1}},{"line":129,"address":[],"length":0,"stats":{"Line":3}},{"line":137,"address":[],"length":0,"stats":{"Line":4}},{"line":138,"address":[],"length":0,"stats":{"Line":16}},{"line":139,"address":[],"length":0,"stats":{"Line":8}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":2}},{"line":147,"address":[],"length":0,"stats":{"Line":8}},{"line":148,"address":[],"length":0,"stats":{"Line":4}},{"line":149,"address":[],"length":0,"stats":{"Line":0}},{"line":153,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":8}},{"line":155,"address":[],"length":0,"stats":{"Line":8}},{"line":156,"address":[],"length":0,"stats":{"Line":0}},{"line":158,"address":[],"length":0,"stats":{"Line":8}},{"line":159,"address":[],"length":0,"stats":{"Line":4}},{"line":160,"address":[],"length":0,"stats":{"Line":2}},{"line":164,"address":[],"length":0,"stats":{"Line":2}},{"line":165,"address":[],"length":0,"stats":{"Line":4}},{"line":166,"address":[],"length":0,"stats":{"Line":4}},{"line":167,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":4}},{"line":170,"address":[],"length":0,"stats":{"Line":2}},{"line":171,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":3}},{"line":179,"address":[],"length":0,"stats":{"Line":6}},{"line":180,"address":[],"length":0,"stats":{"Line":6}},{"line":181,"address":[],"length":0,"stats":{"Line":6}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":184,"address":[],"length":0,"stats":{"Line":6}},{"line":191,"address":[],"length":0,"stats":{"Line":3}},{"line":192,"address":[],"length":0,"stats":{"Line":6}},{"line":193,"address":[],"length":0,"stats":{"Line":6}},{"line":194,"address":[],"length":0,"stats":{"Line":6}},{"line":195,"address":[],"length":0,"stats":{"Line":0}},{"line":197,"address":[],"length":0,"stats":{"Line":6}},{"line":203,"address":[],"length":0,"stats":{"Line":2}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":4}},{"line":206,"address":[],"length":0,"stats":{"Line":4}},{"line":207,"address":[],"length":0,"stats":{"Line":0}},{"line":209,"address":[],"length":0,"stats":{"Line":4}},{"line":215,"address":[],"length":0,"stats":{"Line":5}},{"line":216,"address":[],"length":0,"stats":{"Line":10}},{"line":217,"address":[],"length":0,"stats":{"Line":9}},{"line":218,"address":[],"length":0,"stats":{"Line":8}},{"line":219,"address":[],"length":0,"stats":{"Line":1}},{"line":221,"address":[],"length":0,"stats":{"Line":8}},{"line":227,"address":[],"length":0,"stats":{"Line":4}},{"line":228,"address":[],"length":0,"stats":{"Line":8}},{"line":229,"address":[],"length":0,"stats":{"Line":8}},{"line":230,"address":[],"length":0,"stats":{"Line":8}},{"line":231,"address":[],"length":0,"stats":{"Line":0}},{"line":233,"address":[],"length":0,"stats":{"Line":8}},{"line":240,"address":[],"length":0,"stats":{"Line":2}},{"line":241,"address":[],"length":0,"stats":{"Line":4}},{"line":242,"address":[],"length":0,"stats":{"Line":3}},{"line":243,"address":[],"length":0,"stats":{"Line":2}},{"line":244,"address":[],"length":0,"stats":{"Line":1}},{"line":246,"address":[],"length":0,"stats":{"Line":2}},{"line":253,"address":[],"length":0,"stats":{"Line":2}},{"line":254,"address":[],"length":0,"stats":{"Line":4}},{"line":255,"address":[],"length":0,"stats":{"Line":4}},{"line":256,"address":[],"length":0,"stats":{"Line":4}},{"line":257,"address":[],"length":0,"stats":{"Line":0}},{"line":259,"address":[],"length":0,"stats":{"Line":4}},{"line":266,"address":[],"length":0,"stats":{"Line":2}},{"line":271,"address":[],"length":0,"stats":{"Line":4}},{"line":272,"address":[],"length":0,"stats":{"Line":6}},{"line":273,"address":[],"length":0,"stats":{"Line":1}},{"line":275,"address":[],"length":0,"stats":{"Line":5}},{"line":276,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":2}},{"line":279,"address":[],"length":0,"stats":{"Line":1}},{"line":280,"address":[],"length":0,"stats":{"Line":2}},{"line":281,"address":[],"length":0,"stats":{"Line":1}},{"line":283,"address":[],"length":0,"stats":{"Line":1}},{"line":289,"address":[],"length":0,"stats":{"Line":1}},{"line":294,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":5}},{"line":296,"address":[],"length":0,"stats":{"Line":3}},{"line":297,"address":[],"length":0,"stats":{"Line":2}},{"line":298,"address":[],"length":0,"stats":{"Line":3}},{"line":299,"address":[],"length":0,"stats":{"Line":1}},{"line":306,"address":[],"length":0,"stats":{"Line":1}},{"line":307,"address":[],"length":0,"stats":{"Line":1}}],"covered":109,"coverable":119},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","mod.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! C ABI export layer: implements `include/codec/*.h` verbatim.\n//!\n//! Organization: one submodule per public header (frame / decoder /\n//! encoder / conform / proxy / task). The authoritative function list is\n//! the header itself; each export only unwraps handles, calls the safe\n//! Rust domains, and maps results through [`crate::handle::guard*`].\n//! `include/codec/error.h` exports macros only, so it is folded into the\n//! preamble below instead of getting its own submodule.\n//!\n//! Shared helpers live here: the two-stage string convention\n//! ([`string_out`]), C-string decoding ([`c_str`]) and in-place handle\n//! release ([`free_handle`]) — all mirroring the `c_api/*.cpp` helpers.\n\n/// `include/codec/error.h` — macros only, no exported functions.\n///\n/// `OAKCODEC_OK` and the `OAKCODEC_E_*` codes are mirrored as\n/// [`crate::error`] constants; `OAKCODEC_ABI_VERSION` lives in\n/// [`crate::handle`].\n\npub mod conform;\npub mod decoder;\npub mod encoder;\npub mod frame;\npub mod proxy;\npub mod task;\n\nuse std::ffi::{c_char, c_int};\n#[cfg(test)]\nuse std::sync::Mutex;\n\nuse crate::handle::CHandle;\n\n/// Serializes every ffi unit test: they share the global handle ALIVE\n/// counter, the injected decoder/encoder registries and the probe error,\n/// so exact `alive_count` assertions and registry injection require\n/// serial execution. Held poison-tolerant (`into_inner`) so one failing\n/// test cannot cascade-fail the rest.\n#[cfg(test)]\npub(crate) static TEST_LOCK: Mutex\u003c()\u003e = Mutex::new(());\n\n/// Poison-tolerant lock helper for the ffi tests.\n#[cfg(test)]\npub(crate) fn lock_tests() -\u003e std::sync::MutexGuard\u003c'static, ()\u003e {\n\tTEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())\n}\n\n/// Two-stage string copy helper (`string_out` in every `c_api/*.cpp`).\n///\n/// Returns the required buffer size including the trailing NUL; when\n/// `buf` is non-NULL and `buf_size \u003e 0` the string is copied truncated to\n/// `buf_size - 1` bytes and NUL-terminated.\npub(crate) fn string_out(s: \u0026str, buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet need = s.len() as c_int + 1;\n\tif !buf.is_null() \u0026\u0026 buf_size \u003e 0 {\n\t\tlet n = (s.len() as c_int).min(buf_size - 1);\n\t\t// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.\n\t\tunsafe {\n\t\t\tstd::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize);\n\t\t\t*buf.add(n as usize) = 0;\n\t\t}\n\t}\n\tneed\n}\n\n/// Read a NUL-terminated C string; `None` on NULL pointers.\npub(crate) fn c_str(ptr: *const c_char) -\u003e Option\u003cString\u003e {\n\tif ptr.is_null() {\n\t\treturn None;\n\t}\n\t// SAFETY: `ptr` must be a valid NUL-terminated C string by contract.\n\tlet s = unsafe { std::ffi::CStr::from_ptr(ptr) };\n\tSome(s.to_string_lossy().into_owned())\n}\n\n/// Release a handle in place and null its `ctx` (`free_handle` in\n/// `c_api/refcounted.h`); NULL pointer and empty handle are no-ops.\npub(crate) fn free_handle(h: *mut CHandle) {\n\tif h.is_null() {\n\t\treturn;\n\t}\n\tlet handle = unsafe { \u0026mut *h };\n\tif handle.ctx.is_null() {\n\t\treturn;\n\t}\n\tif let Some(release) = handle.release {\n\t\t// SAFETY: `release` targets the box behind `ctx`.\n\t\tunsafe { release(handle.ctx) };\n\t}\n\thandle.ctx = std::ptr::null_mut();\n}\n\n/// Safe view into a handle's boxed value; `None` for empty handles.\n///\n/// Thin wrapper over [`crate::handle::get`] so the export bodies can call\n/// it without `unsafe` blocks everywhere.\n///\n/// # Safety\n/// `T` must be the boxed type; each export asserts it via the handle\n/// contract (the same typed box is used by its `make_owned` call).\npub(crate) fn get_box\u003cT: 'static\u003e(h: \u0026CHandle) -\u003e Option\u003c\u0026T\u003e {\n\tunsafe { crate::handle::get::\u003cT\u003e(h) }\n}\n","traces":[{"line":59,"address":[],"length":0,"stats":{"Line":26}},{"line":60,"address":[],"length":0,"stats":{"Line":78}},{"line":68,"address":[],"length":0,"stats":{"Line":22}},{"line":69,"address":[],"length":0,"stats":{"Line":44}},{"line":70,"address":[],"length":0,"stats":{"Line":44}},{"line":71,"address":[],"length":0,"stats":{"Line":88}},{"line":74,"address":[],"length":0,"stats":{"Line":110}},{"line":75,"address":[],"length":0,"stats":{"Line":44}},{"line":78,"address":[],"length":0,"stats":{"Line":22}},{"line":82,"address":[],"length":0,"stats":{"Line":85}},{"line":83,"address":[],"length":0,"stats":{"Line":170}},{"line":84,"address":[],"length":0,"stats":{"Line":15}},{"line":87,"address":[],"length":0,"stats":{"Line":210}},{"line":88,"address":[],"length":0,"stats":{"Line":140}},{"line":93,"address":[],"length":0,"stats":{"Line":24}},{"line":94,"address":[],"length":0,"stats":{"Line":48}},{"line":95,"address":[],"length":0,"stats":{"Line":1}},{"line":97,"address":[],"length":0,"stats":{"Line":46}},{"line":98,"address":[],"length":0,"stats":{"Line":46}},{"line":99,"address":[],"length":0,"stats":{"Line":1}},{"line":101,"address":[],"length":0,"stats":{"Line":66}},{"line":103,"address":[],"length":0,"stats":{"Line":22}},{"line":105,"address":[],"length":0,"stats":{"Line":22}},{"line":116,"address":[],"length":0,"stats":{"Line":116}},{"line":117,"address":[],"length":0,"stats":{"Line":232}}],"covered":25,"coverable":25},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","proxy.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/proxy.h` exports.\n//!\n//! Complete inventory: create_instance / destroy_instance / params_default\n//! / get_state / state_to_string / get_proxy_directory / get_proxy_filename\n//! / get_working_filename / get_or_start / find_ffmpeg.\n//!\n//! # CPP-PARITY\n//! The C++ `oakcodec_proxy_params.include_audio` is an `int`, mirrored\n//! byte-for-byte by [`crate::proxymanager::ProxyParams`]; the POD structs\n//! are still defined here (rather than reused) so the ffi layer never\n//! depends on the crate-internal type's layout. A NULL `params` maps to\n//! the crate's compiled-in defaults (`ProxyParams::default`), matching\n//! `to_native` in `c_api/proxy.cpp`.\n\nuse std::ffi::{c_char, c_int};\n\nuse crate::handle;\nuse crate::proxymanager::{ProxyManager, ProxyParams, ProxyState};\n\n/// `OAKCODEC_PROXY_STATE_MISSING`.\npub const OAKCODEC_PROXY_STATE_MISSING: c_int = 0;\n/// `OAKCODEC_PROXY_STATE_GENERATING`.\npub const OAKCODEC_PROXY_STATE_GENERATING: c_int = 1;\n/// `OAKCODEC_PROXY_STATE_READY`.\npub const OAKCODEC_PROXY_STATE_READY: c_int = 2;\n/// `OAKCODEC_PROXY_STATE_FAILED`.\npub const OAKCODEC_PROXY_STATE_FAILED: c_int = 3;\n\n/// `oakcodec_proxy_params` — POD mirror of `include/codec/proxy.h`.\n#[allow(missing_docs)]\n#[repr(C)]\npub struct oakcodec_proxy_params {\n\tpub width: c_int,\n\tpub height: c_int,\n\tpub divider: c_int,\n\tpub version: c_int,\n\tpub crf: c_int,\n\tpub include_audio: c_int,\n\tpub extension: [u8; 32],\n\tpub preset: [u8; 32],\n}\n\n/// `oakcodec_proxy_result` — POD result of `oakcodec_proxy_get_or_start`.\n#[allow(missing_docs)]\n#[repr(C)]\npub struct oakcodec_proxy_result {\n\tpub state: c_int,\n\tpub filename: [u8; 1024],\n}\n\n/// Convert the C POD to the crate's [`ProxyParams`] (`to_native` in\n/// `c_api/proxy.cpp`); NULL maps to the compiled-in defaults.\nfn to_native(p: *const oakcodec_proxy_params) -\u003e ProxyParams {\n\tif p.is_null() {\n\t\treturn ProxyParams::default();\n\t}\n\tlet p = unsafe { \u0026*p };\n\tProxyParams {\n\t\twidth: p.width,\n\t\theight: p.height,\n\t\tdivider: p.divider,\n\t\tversion: p.version,\n\t\tcrf: p.crf,\n\t\tinclude_audio: p.include_audio,\n\t\textension: p.extension,\n\t\tpreset: p.preset,\n\t}\n}\n\n/// Copy a NUL-terminated byte array into a C buffer (truncated).\nfn copy_cstr(src: \u0026[u8], dst: \u0026mut [u8]) {\n\tdst.fill(0);\n\tlet n = src.len().min(dst.len().saturating_sub(1));\n\tdst[..n].copy_from_slice(\u0026src[..n]);\n}\n\n/// `oakcodec_proxy_create_instance`: create the singleton (always present\n/// here, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_create_instance() -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet _ = ProxyManager::instance();\n\t\tcrate::error::OAKCODEC_OK\n\t})\n}\n\n/// `oakcodec_proxy_destroy_instance`: destroy the singleton (the Rust\n/// manager is stateless, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_destroy_instance() -\u003e c_int {\n\thandle::guard_raw(|| crate::error::OAKCODEC_OK)\n}\n\n/// `oakcodec_proxy_params_default`: fill `out` with the compiled-in\n/// default proxy parameters.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_params_default(out: *mut oakcodec_proxy_params) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet n = ProxyManager::proxy_params_from_config();\n\t\tunsafe {\n\t\t\t(*out).width = n.width;\n\t\t\t(*out).height = n.height;\n\t\t\t(*out).divider = n.divider;\n\t\t\t(*out).version = n.version;\n\t\t\t(*out).crf = n.crf;\n\t\t\t(*out).include_audio = n.include_audio;\n\t\t\tcopy_cstr(\u0026n.extension, \u0026mut (*out).extension);\n\t\t\tcopy_cstr(\u0026n.preset, \u0026mut (*out).preset);\n\t\t}\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_proxy_get_state`: state of a proxy file on disk\n/// (`OAKCODEC_PROXY_STATE_MISSING` for NULL/empty/absent).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_state(proxy_filename: *const c_char) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match crate::ffi::c_str(proxy_filename) {\n\t\t\tSome(f) if !f.is_empty() =\u003e f,\n\t\t\t_ =\u003e return OAKCODEC_PROXY_STATE_MISSING,\n\t\t};\n\t\tProxyManager::get_proxy_state(\u0026f) as c_int\n\t})\n}\n\n/// `oakcodec_proxy_state_to_string` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_state_to_string(\n\tstate: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet s = match state {\n\t\t\t0 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Missing),\n\t\t\t1 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Generating),\n\t\t\t2 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Ready),\n\t\t\t3 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Failed),\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t})\n}\n\n/// `oakcodec_proxy_get_proxy_directory` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_proxy_directory(\n\tcache_path: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) =\u003e c,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tmatch ProxyManager::get_proxy_directory(\u0026cache) {\n\t\t\tOk(s) =\u003e super::string_out(\u0026s, buf, buf_size),\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n/// `oakcodec_proxy_get_proxy_filename` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_proxy_filename(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tparams: *const oakcodec_proxy_params,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) =\u003e c,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) =\u003e s,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet native = to_native(params);\n\t\tmatch ProxyManager::get_proxy_filename(\u0026cache, \u0026source, stream_index, \u0026native) {\n\t\t\tOk(s) =\u003e super::string_out(\u0026s, buf, buf_size),\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n/// `oakcodec_proxy_get_working_filename` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_working_filename(\n\tproxy_filename: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet proxy = match crate::ffi::c_str(proxy_filename) {\n\t\t\tSome(p) =\u003e p,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tmatch ProxyManager::get_working_filename(\u0026proxy) {\n\t\t\tOk(s) =\u003e super::string_out(\u0026s, buf, buf_size),\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n/// `oakcodec_proxy_get_or_start`: get or start generating a proxy for\n/// `source_filename`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_or_start(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tparams: *const oakcodec_proxy_params,\n\tout: *mut oakcodec_proxy_result,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) =\u003e c,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) =\u003e s,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet native = to_native(params);\n\t\tlet (state, filename) = ProxyManager::instance()\n\t\t\t.get_or_start(\u0026cache, \u0026source, stream_index, \u0026native)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"get_or_start failed\".to_string()))?;\n\t\tunsafe {\n\t\t\tlet out_ref = \u0026mut *out;\n\t\t\tout_ref.state = state as c_int;\n\t\t\t// Truncate to 1023 chars + NUL, matching `snprintf` in\n\t\t\t// `c_api/proxy.cpp`.\n\t\t\tlet n = filename.len().min(1023);\n\t\t\tout_ref.filename[..n].copy_from_slice(\u0026filename.as_bytes()[..n]);\n\t\t\tout_ref.filename[n] = 0;\n\t\t}\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_proxy_find_ffmpeg` (two-stage; empty string when none found).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_find_ffmpeg(\n\tconfigured_path: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet configured = crate::ffi::c_str(configured_path).unwrap_or_default();\n\t\tlet s = ProxyManager::find_ffmpeg(\u0026configured);\n\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::conformmanager::test_util::{accept_cb, REG_LOCK};\n\tuse crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};\n\tuse crate::task::set_task_submit_cb_extern;\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn temp_cache(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_ffi_proxy_{}_{}\", name, std::process::id()));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\tfn defaults() -\u003e oakcodec_proxy_params {\n\t\tlet mut p: oakcodec_proxy_params = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_proxy_params_default(\u0026mut p) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tp\n\t}\n\n\t#[test]\n\tfn create_destroy_and_params_default() {\n\t\tassert_eq!(unsafe { oakcodec_proxy_create_instance() }, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_proxy_destroy_instance() }, crate::error::OAKCODEC_OK);\n\n\t\tlet p = defaults();\n\t\tassert_eq!(p.width, 1280);\n\t\tassert_eq!(p.height, 720);\n\t\tassert_eq!(p.divider, 1);\n\t\tassert_eq!(p.version, 1);\n\t\tassert_eq!(p.crf, 23);\n\t\tassert_eq!(p.include_audio, 1);\n\t\tassert_eq!(\u0026p.extension[..3], b\"mp4\");\n\t\tassert_eq!(\u0026p.preset[..8], b\"veryfast\");\n\n\t\tlet rc = unsafe { oakcodec_proxy_params_default(std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n\n\t#[test]\n\tfn get_state_and_state_to_string() {\n\t\tlet cache = temp_cache(\"state\");\n\t\tlet p = defaults();\n\t\tlet src = cstr(\"media.mp4\");\n\t\tlet cache_c = cstr(\u0026cache);\n\n\t\t// Resolve the proxy filename, then query its state.\n\t\tlet mut name = [0i8; 1024];\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_filename(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, name.as_mut_ptr(), 1024) };\n\t\tassert!(rc \u003e 0);\n\t\tlet proxy = crate::ffi::c_str(name.as_ptr()).unwrap();\n\t\tassert!(proxy.contains(\"1280x720\"));\n\n\t\t// Missing by default.\n\t\tlet pc = cstr(\u0026proxy);\n\t\tassert_eq!(unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, OAKCODEC_PROXY_STATE_MISSING);\n\t\tassert_eq!(unsafe { oakcodec_proxy_get_state(std::ptr::null()) }, OAKCODEC_PROXY_STATE_MISSING);\n\n\t\t// Ready once the file exists.\n\t\tstd::fs::create_dir_all(std::path::Path::new(\u0026proxy).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026proxy, b\"x\").unwrap();\n\t\tassert_eq!(unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, OAKCODEC_PROXY_STATE_READY);\n\n\t\t// state_to_string mapping + invalid range.\n\t\tlet mut buf = [0i8; 64];\n\t\tlet rc = unsafe { oakcodec_proxy_state_to_string(2, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 6); // \"ready\" + NUL\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"ready\"));\n\t\tlet rc = unsafe { oakcodec_proxy_state_to_string(7, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n\n\t#[test]\n\tfn directory_working_and_get_or_start() {\n\t\tlet cache = temp_cache(\"getorstart\");\n\t\tlet cache_c = cstr(\u0026cache);\n\t\tlet src = cstr(\"media.mp4\");\n\t\tlet p = defaults();\n\n\t\t// get_proxy_directory.\n\t\tlet mut buf = [0i8; 512];\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_directory(cache_c.as_ptr(), buf.as_mut_ptr(), 512) };\n\t\tassert!(rc \u003e 0);\n\t\tassert_eq!(\n\t\t\tcrate::ffi::c_str(buf.as_ptr()).as_deref(),\n\t\t\tSome(format!(\"{}/proxy\", cache).as_str())\n\t\t);\n\n\t\t// get_working_filename appends \".working.mp4\".\n\t\tlet proxy = format!(\"{}/proxy/{}-0.1280x720.v1.a1.mp4\", cache, 12345);\n\t\tlet pc = cstr(\u0026proxy);\n\t\tlet rc = unsafe { oakcodec_proxy_get_working_filename(pc.as_ptr(), buf.as_mut_ptr(), 512) };\n\t\tassert!(rc \u003e 0);\n\t\tassert_eq!(\n\t\t\tcrate::ffi::c_str(buf.as_ptr()).as_deref(),\n\t\t\tSome(format!(\"{}.working.mp4\", proxy).as_str())\n\t\t);\n\n\t\t// get_or_start without a registrar -\u003e Missing.\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tlet mut out: oakcodec_proxy_result = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, \u0026mut out) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(out.state, OAKCODEC_PROXY_STATE_MISSING);\n\n\t\t// With a registrar and no files -\u003e Generating.\n\t\tset_task_submit_cb_extern(Some(accept_cb), std::ptr::null_mut());\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, \u0026mut out) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(out.state, OAKCODEC_PROXY_STATE_GENERATING);\n\n\t\t// Invalid args.\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(std::ptr::null(), src.as_ptr(), 0, \u0026p, \u0026mut out) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// get_proxy_directory / get_proxy_filename / get_working_filename\n\t\t// argument validation.\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_directory(std::ptr::null(), buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_filename(std::ptr::null(), src.as_ptr(), 0, \u0026p, buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_proxy_get_working_filename(std::ptr::null(), buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n\n\t#[test]\n\tfn find_ffmpeg_uses_configured_path() {\n\t\t// The current test binary is a real executable: the configured\n\t\t// path resolves to an absolute path.\n\t\tlet me = std::env::current_exe().unwrap();\n\t\tlet mc = cstr(me.to_str().unwrap());\n\t\tlet mut buf = [0i8; 1024];\n\t\tlet rc = unsafe { oakcodec_proxy_find_ffmpeg(mc.as_ptr(), buf.as_mut_ptr(), 1024) };\n\t\tassert!(rc \u003e 0);\n\t\tlet found = crate::ffi::c_str(buf.as_ptr()).unwrap();\n\t\tassert!(found.starts_with('/'));\n\n\t\t// NULL configured path falls back to the search (empty or absolute).\n\t\tlet rc = unsafe { oakcodec_proxy_find_ffmpeg(std::ptr::null(), buf.as_mut_ptr(), 1024) };\n\t\tassert!(rc \u003e 0);\n\t\tlet found = crate::ffi::c_str(buf.as_ptr()).unwrap();\n\t\tassert!(found.is_empty() || found.starts_with('/'));\n\t}\n}\n","traces":[{"line":69,"address":[],"length":0,"stats":{"Line":3}},{"line":70,"address":[],"length":0,"stats":{"Line":6}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":6}},{"line":75,"address":[],"length":0,"stats":{"Line":6}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":6}},{"line":78,"address":[],"length":0,"stats":{"Line":6}},{"line":79,"address":[],"length":0,"stats":{"Line":6}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":3}},{"line":82,"address":[],"length":0,"stats":{"Line":3}},{"line":87,"address":[],"length":0,"stats":{"Line":6}},{"line":88,"address":[],"length":0,"stats":{"Line":12}},{"line":89,"address":[],"length":0,"stats":{"Line":42}},{"line":90,"address":[],"length":0,"stats":{"Line":18}},{"line":96,"address":[],"length":0,"stats":{"Line":1}},{"line":97,"address":[],"length":0,"stats":{"Line":2}},{"line":98,"address":[],"length":0,"stats":{"Line":1}},{"line":99,"address":[],"length":0,"stats":{"Line":1}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":107,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":4}},{"line":114,"address":[],"length":0,"stats":{"Line":8}},{"line":115,"address":[],"length":0,"stats":{"Line":8}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":118,"address":[],"length":0,"stats":{"Line":6}},{"line":119,"address":[],"length":0,"stats":{"Line":3}},{"line":120,"address":[],"length":0,"stats":{"Line":6}},{"line":121,"address":[],"length":0,"stats":{"Line":6}},{"line":122,"address":[],"length":0,"stats":{"Line":6}},{"line":123,"address":[],"length":0,"stats":{"Line":6}},{"line":124,"address":[],"length":0,"stats":{"Line":6}},{"line":125,"address":[],"length":0,"stats":{"Line":6}},{"line":126,"address":[],"length":0,"stats":{"Line":12}},{"line":127,"address":[],"length":0,"stats":{"Line":6}},{"line":129,"address":[],"length":0,"stats":{"Line":3}},{"line":136,"address":[],"length":0,"stats":{"Line":3}},{"line":137,"address":[],"length":0,"stats":{"Line":6}},{"line":138,"address":[],"length":0,"stats":{"Line":8}},{"line":139,"address":[],"length":0,"stats":{"Line":10}},{"line":140,"address":[],"length":0,"stats":{"Line":1}},{"line":142,"address":[],"length":0,"stats":{"Line":2}},{"line":148,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":3}},{"line":155,"address":[],"length":0,"stats":{"Line":0}},{"line":156,"address":[],"length":0,"stats":{"Line":0}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":158,"address":[],"length":0,"stats":{"Line":0}},{"line":159,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":4}},{"line":167,"address":[],"length":0,"stats":{"Line":2}},{"line":172,"address":[],"length":0,"stats":{"Line":4}},{"line":173,"address":[],"length":0,"stats":{"Line":3}},{"line":174,"address":[],"length":0,"stats":{"Line":2}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":177,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":5}},{"line":179,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":2}},{"line":194,"address":[],"length":0,"stats":{"Line":4}},{"line":195,"address":[],"length":0,"stats":{"Line":3}},{"line":196,"address":[],"length":0,"stats":{"Line":2}},{"line":197,"address":[],"length":0,"stats":{"Line":1}},{"line":199,"address":[],"length":0,"stats":{"Line":2}},{"line":200,"address":[],"length":0,"stats":{"Line":2}},{"line":201,"address":[],"length":0,"stats":{"Line":0}},{"line":203,"address":[],"length":0,"stats":{"Line":3}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":5}},{"line":206,"address":[],"length":0,"stats":{"Line":0}},{"line":213,"address":[],"length":0,"stats":{"Line":2}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":219,"address":[],"length":0,"stats":{"Line":3}},{"line":220,"address":[],"length":0,"stats":{"Line":2}},{"line":221,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":1}},{"line":224,"address":[],"length":0,"stats":{"Line":5}},{"line":225,"address":[],"length":0,"stats":{"Line":0}},{"line":233,"address":[],"length":0,"stats":{"Line":4}},{"line":240,"address":[],"length":0,"stats":{"Line":8}},{"line":241,"address":[],"length":0,"stats":{"Line":8}},{"line":242,"address":[],"length":0,"stats":{"Line":1}},{"line":244,"address":[],"length":0,"stats":{"Line":5}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":1}},{"line":248,"address":[],"length":0,"stats":{"Line":4}},{"line":249,"address":[],"length":0,"stats":{"Line":4}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":6}},{"line":253,"address":[],"length":0,"stats":{"Line":6}},{"line":254,"address":[],"length":0,"stats":{"Line":10}},{"line":255,"address":[],"length":0,"stats":{"Line":2}},{"line":256,"address":[],"length":0,"stats":{"Line":2}},{"line":257,"address":[],"length":0,"stats":{"Line":6}},{"line":258,"address":[],"length":0,"stats":{"Line":4}},{"line":261,"address":[],"length":0,"stats":{"Line":10}},{"line":262,"address":[],"length":0,"stats":{"Line":6}},{"line":263,"address":[],"length":0,"stats":{"Line":2}},{"line":265,"address":[],"length":0,"stats":{"Line":2}},{"line":271,"address":[],"length":0,"stats":{"Line":2}},{"line":276,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":8}},{"line":278,"address":[],"length":0,"stats":{"Line":6}},{"line":279,"address":[],"length":0,"stats":{"Line":8}}],"covered":97,"coverable":106},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","task.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/task.h` exports.\n//!\n//! Complete inventory: set_task_submit_cb / task_submit_is_registered.\n//! The callback typedef and request struct are mirrored in\n//! [`crate::task`].\n\nuse std::ffi::c_int;\n\nuse crate::handle;\n\n/// `oakcodec_set_task_submit_cb`: register (or replace, or clear with\n/// NULL) the global task submit callback.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_set_task_submit_cb(\n\tcb: Option\u003ccrate::task::OakCodecTaskSubmitFn\u003e,\n\tuserdata: *mut std::ffi::c_void,\n) {\n\thandle::guard_void(|| {\n\t\tcrate::task::set_task_submit_cb_extern(cb, userdata);\n\t})\n}\n\n/// `oakcodec_task_submit_is_registered`: 1 when a callback is set.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_task_submit_is_registered() -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tif crate::task::task_submit_is_registered() {\n\t\t\t1\n\t\t} else {\n\t\t\t0\n\t\t}\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::conformmanager::test_util::{accept_cb, REG_LOCK};\n\tuse crate::task::set_task_submit_cb_extern;\n\n\t#[test]\n\tfn register_query_clear() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\n\t\tassert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);\n\n\t\tunsafe { oakcodec_set_task_submit_cb(Some(accept_cb), std::ptr::null_mut()) };\n\t\tassert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 1);\n\n\t\tunsafe { oakcodec_set_task_submit_cb(None, std::ptr::null_mut()) };\n\t\tassert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);\n\n\t\t// Restore a clean slate for the other modules.\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n}\n","traces":[{"line":30,"address":[],"length":0,"stats":{"Line":2}},{"line":34,"address":[],"length":0,"stats":{"Line":4}},{"line":35,"address":[],"length":0,"stats":{"Line":6}},{"line":41,"address":[],"length":0,"stats":{"Line":3}},{"line":42,"address":[],"length":0,"stats":{"Line":6}},{"line":43,"address":[],"length":0,"stats":{"Line":3}},{"line":44,"address":[],"length":0,"stats":{"Line":1}},{"line":46,"address":[],"length":0,"stats":{"Line":2}}],"covered":8,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffmpeg.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `FFmpegDecoder` / `FFmpegEncoder` — real FFmpeg-backed implementations.\n//!\n//! Mirrors `src/codec/src/ffmpeg/{ffmpegdecoder,ffmpegencoder}.{h,cpp}`.\n//! Unlike the C++ side — which routes everything through the `ffmpeg_bridge`\n//! C library — this module calls the [`ffmpeg_next`] crate directly. The\n//! bridge existed only to absorb FFmpeg API churn (the `liboakffmpeg`\n//! dylib) and is deliberately not replicated. `// CPP-PARITY:` comments\n//! point at the oracle lines each behavior mirrors.\n//!\n//! ## Delivery formats\n//!\n//! Video decode delivers **F32 RGBA** frames ([`PixelFormat::F32`], the\n//! primary oakcore pipeline format): the decoded frame is run through\n//! swscale into a float-RGBA target and copied into the crate's [`Frame`]\n//! (4 channels, linesize rounded to 32 bytes — see [`Frame`]). Audio is\n//! delivered as interleaved `f32`, matching the C ABI.\n//!\n//! ## Deviations from the C++ oracle (all trait-shaped)\n//!\n//! * [`Decoder::retrieve_audio`] decodes directly (the C++ path goes\n//! through the ConformManager cache; the Rust trait carries no cache\n//! path or loop mode).\n//! * [`RetrieveVideoParams`] drops `renderer`, `divider` and\n//! `maximum_format` (the Rust trait surface), so [`Decoder::retrieve_video`]\n//! can only produce textures through an empty renderer handle and there\n//! is no preview-divider scaling.\n//! * Probing uses stream parameters (no second decode pass), so `is_still`\n//! is always false and interlacing always progressive.\n//! * Subtitle streams are counted but not added as subtitle entries.\n\nuse std::collections::VecDeque;\nuse std::path::PathBuf;\nuse std::sync::{Arc, Mutex, OnceLock};\n\nuse ffmpeg_next as ffmpeg;\nuse ffmpeg::ffi as sys;\nuse ffmpeg::format::sample::Type as SampleType;\nuse ffmpeg::format::{Pixel, Sample};\nuse ffmpeg::media::Type as MediaType;\nuse ffmpeg::software::{resampling, scaling};\nuse ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational};\n\nuse oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange};\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_init_basic, oakcommon_videoparams_set_channel_count,\n\toakcommon_videoparams_set_duration, oakcommon_videoparams_set_format,\n\toakcommon_videoparams_set_frame_rate, oakcommon_videoparams_set_height,\n\toakcommon_videoparams_set_interlacing, oakcommon_videoparams_set_pixel_aspect_ratio,\n\toakcommon_videoparams_set_premultiplied_alpha, oakcommon_videoparams_set_start_time,\n\toakcommon_videoparams_set_stream_index, oakcommon_videoparams_set_time_base,\n\toakcommon_videoparams_set_video_type, oakcommon_videoparams_set_width,\n\toakcore_audioparams_create, oakcore_audioparams_set_duration, oakcore_audioparams_set_stream_index,\n\toakcore_audioparams_set_time_base, OakAudioParams,\n};\nuse crate::bridge::render::{oakrender_cancelatom_is_cancelled, OakCancelAtom, OakRenderTexture};\nuse crate::decoder::{CodecStream, Decoder, RetrieveAudioStatus, RetrieveVideoParams};\nuse crate::encoder::Encoder;\nuse crate::encodingparams::EncodingParams;\nuse crate::footagedescription::{FootageDescription, StreamEntry};\nuse crate::frame::Frame;\n\n/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).\nconst OAKCOMMON_VIDEO_INTERLACE_NONE: i32 = 0;\n/// `OAKCOMMON_COLOR_RANGE_FULL`.\nconst OAKCOMMON_COLOR_RANGE_FULL: i32 = 1;\n/// `OAKCOMMON_VIDEO_TYPE_VIDEO`.\nconst OAKCOMMON_VIDEO_TYPE_VIDEO: i32 = 0;\n/// The format-level time base (microseconds), `FB_TIME_BASE` in the bridge.\nconst FB_TIME_BASE: i64 = 1_000_000;\n/// `AV_NOPTS_VALUE`.\nconst AV_NOPTS_VALUE: i64 = i64::MIN;\n/// Channel count of the internal RGBA pipeline layout.\nconst VIDEO_CHANNELS: i32 = 4;\n/// Number of f32 samples in one RGBA pixel (R, G, B, A).\nconst PIXEL_F32_BYTES: usize = 16;\n\n/// Lazily initialize the FFmpeg libraries (idempotent, at most once).\nfn ffmpeg_init() -\u003e crate::error::Result\u003c()\u003e {\n\tstatic INIT: OnceLock\u003cResult\u003c(), String\u003e\u003e = OnceLock::new();\n\tif let Err(e) = INIT.get_or_init(|| {\n\t\tffmpeg::init().map_err(|e| format!(\"ffmpeg initialization failed: {e}\"))\n\t}) {\n\t\treturn Err(crate::error::Error::Failed(e.clone()));\n\t}\n\tOk(())\n}\n\n/// Wrap an FFmpeg error in the crate error type.\nfn ffmpeg_err(e: FfmpegError) -\u003e crate::error::Error {\n\tcrate::error::Error::Failed(format!(\"ffmpeg error: {e}\"))\n}\n\n/// Build a generic failure.\nfn fail(msg: impl Into\u003cString\u003e) -\u003e crate::error::Error {\n\tcrate::error::Error::Failed(msg.into())\n}\n\n/// `cancel_atom_is_cancelled` — NULL/empty-handle-safe check of an\n/// oakrender cancel atom (borrowed pointer).\n///\n/// # CPP-PARITY\n/// `src/codec/src/ffmpeg/ffmpegdecoder.cpp` (anonymous namespace helper).\nfn cancel_atom_is_cancelled(cancelled: Option\u003c\u0026OakCancelAtom\u003e) -\u003e bool {\n\tmatch cancelled {\n\t\tSome(atom) if !atom.ctx.is_null() =\u003e unsafe {\n\t\t\toakrender_cancelatom_is_cancelled(atom.clone()) != 0\n\t\t},\n\t\t_ =\u003e false,\n\t}\n}\n\n/// Whether an FFmpeg error means \"end of stream\" or \"try again\".\nfn is_eof_or_eagain(e: \u0026FfmpegError) -\u003e bool {\n\tmatches!(e, FfmpegError::Eof)\n\t\t|| matches!(e, FfmpegError::Other { errno } if *errno == ffmpeg::error::EAGAIN)\n}\n\n/// Map a \"JPEG\" (full-range) pixel format to its regular counterpart,\n/// mirroring the bridge `convert_jpeg_space_to_regular_space`.\nfn convert_jpeg_space_to_regular_space(p: Pixel) -\u003e Pixel {\n\tmatch p {\n\t\tPixel::YUVJ420P =\u003e Pixel::YUV420P,\n\t\tPixel::YUVJ422P =\u003e Pixel::YUV422P,\n\t\tPixel::YUVJ444P =\u003e Pixel::YUV444P,\n\t\tPixel::YUVJ440P =\u003e Pixel::YUV440P,\n\t\tPixel::YUVJ411P =\u003e Pixel::YUV411P,\n\t\tother =\u003e other,\n\t}\n}\n\n/// Native `OakPixelFormat` for a decoded pixel format (probe reporting).\n/// Mirrors `FFmpegDecoder::get_native_pixel_format`: 8-bit sources map to\n/// [`PixelFormat::U8`], 16-bit to [`PixelFormat::U16`], float to\n/// [`PixelFormat::F32`].\nfn native_pixel_format(p: Pixel) -\u003e PixelFormat {\n\tmatch p {\n\t\tPixel::RGBF32BE\n\t\t| Pixel::RGBF32LE\n\t\t| Pixel::RGBAF32BE\n\t\t| Pixel::RGBAF32LE\n\t\t| Pixel::GBRPF32BE\n\t\t| Pixel::GBRPF32LE =\u003e PixelFormat::F32,\n\t\tPixel::RGB48BE\n\t\t| Pixel::RGB48LE\n\t\t| Pixel::RGBA64BE\n\t\t| Pixel::RGBA64LE\n\t\t| Pixel::GBRP9BE\n\t\t| Pixel::GBRP9LE\n\t\t| Pixel::GBRP10BE\n\t\t| Pixel::GBRP10LE\n\t\t| Pixel::GBRP12BE\n\t\t| Pixel::GBRP12LE\n\t\t| Pixel::GBRP14BE\n\t\t| Pixel::GBRP14LE\n\t\t| Pixel::GBRP16BE\n\t\t| Pixel::GBRP16LE =\u003e PixelFormat::U16,\n\t\t_ =\u003e PixelFormat::U8,\n\t}\n}\n\n/// Build an ffmpeg channel layout from a legacy ffmpeg channel mask.\n///\n/// Dipped into `ffmpeg-sys-next`: `ffmpeg-next` 9 exposes `ChannelLayout`\n/// as a wrapper around `AVChannelLayout` (no mask constructor).\nfn channel_layout_from_mask(mask: u64) -\u003e ChannelLayout {\n\tif mask == 0 {\n\t\treturn ChannelLayout::default(2);\n\t}\n\tlet channels = mask.count_ones() as i32;\n\tChannelLayout(sys::AVChannelLayout {\n\t\torder: sys::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE,\n\t\tnb_channels: channels,\n\t\tu: sys::AVChannelLayout__bindgen_ty_1 { mask },\n\t\topaque: std::ptr::null_mut(),\n\t})\n}\n\n/// Convert a raw `AVSampleFormat` discriminant (as stored in\n/// `AVCodecParameters.format`) into an ffmpeg [`Sample`].\n///\n/// Dipped into `ffmpeg-sys-next`: bindgen emits the C enum as a `#[repr(i32)]`\n/// Rust enum, which cannot be built from an `i32` with an `as` cast.\nfn sample_from_raw(v: i32) -\u003e Sample {\n\t// SAFETY: `v` is always a discriminant FFmpeg itself produced; the\n\t// `repr(i32)` C enum has the same size and layout as `i32`.\n\tSample::from(unsafe { std::mem::transmute::\u003ci32, sys::AVSampleFormat\u003e(v) })\n}\n\n/// Convert a raw `AVPixelFormat` discriminant (as stored in\n/// `AVCodecParameters.format`) into an ffmpeg [`Pixel`].\n///\n/// Dipped into `ffmpeg-sys-next` (see [`sample_from_raw`]).\nfn pixel_from_raw(v: i32) -\u003e Pixel {\n\t// SAFETY: see `sample_from_raw`.\n\tPixel::from(unsafe { std::mem::transmute::\u003ci32, sys::AVPixelFormat\u003e(v) })\n}\n\n/// The oakcore timebase equivalent of an ffmpeg rational.\nfn oak_rational(r: FfRational) -\u003e Rational {\n\tRational::new(r.0 as i64, r.1 as i64)\n}\n\n// ---------------------------------------------------------------------------\n// FFmpegDecoder\n// ---------------------------------------------------------------------------\n\n/// `olive::FFmpegDecoder` — FFmpeg-backed media decoder.\npub struct FFmpegDecoder {\n\t/// Opened stream (locked).\n\tstream: std::sync::Mutex\u003cOption\u003cCodecStream\u003e\u003e,\n\t/// The open FFmpeg decode session, if any.\n\tstate: Mutex\u003cOption\u003cDecoderState\u003e\u003e,\n}\n\nimpl FFmpegDecoder {\n\t/// New, closed decoder.\n\tpub fn new() -\u003e Self {\n\t\tFFmpegDecoder {\n\t\t\tstream: std::sync::Mutex::new(None),\n\t\t\tstate: Mutex::new(None),\n\t\t}\n\t}\n}\n\nimpl Default for FFmpegDecoder {\n\tfn default() -\u003e Self {\n\t\tSelf::new()\n\t}\n}\n\nimpl Decoder for FFmpegDecoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"ffmpeg\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn probe(\u0026self, filename: \u0026str, cancelled: Option\u003c\u0026OakCancelAtom\u003e) -\u003e Option\u003cFootageDescription\u003e {\n\t\tffmpeg_init().ok()?;\n\t\tprobe_file(filename, cancelled)\n\t}\n\n\tfn open(\u0026self, stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\tffmpeg_init()?;\n\n\t\t// # CPP-PARITY\n\t\t// `src/codec/src/decoder.cpp` `Decoder::open`: already-open decoders\n\t\t// accept the same stream again, reject a different one; on failure the\n\t\t// stream is unset again.\n\t\tlet mut stream_guard = self.stream.lock().unwrap_or_else(|e| e.into_inner());\n\t\tif let Some(cur) = stream_guard.as_ref() {\n\t\t\tif cur == stream {\n\t\t\t\treturn Ok(());\n\t\t\t}\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tif !stream.is_valid() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tif !stream.exists() {\n\t\t\treturn Err(crate::error::Error::NotFound);\n\t\t}\n\n\t\tlet opened = DecoderState::open(stream)?;\n\t\t*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(opened);\n\t\t*stream_guard = Some(stream.clone());\n\t\tOk(())\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// C++ `Decoder::close` is void and safe when closed.\n\t\t*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = None;\n\t\t*self.state.lock().unwrap_or_else(|e| e.into_inner()) = None;\n\t\tOk(())\n\t}\n\n\tfn stream(\u0026self) -\u003e CodecStream {\n\t\tself.stream\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.clone()\n\t\t\t.unwrap_or_else(CodecStream::new)\n\t}\n\n\tfn retrieve_video_frame(\u0026self, p: \u0026RetrieveVideoParams) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e {\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Video(_)) {\n\t\t\treturn Err(fail(\"decoder is not open on a video stream\"));\n\t\t}\n\n\t\t// # CPP-PARITY\n\t\t// `retrieve_video_frame_internal` (ffmpegdecoder.cpp): the decoded\n\t\t// frame is color-range-forced, scaled and copied into a `Frame` at\n\t\t// the requested timestamp.\n\t\t// Note: the Rust `RetrieveVideoParams` carries no cancellation atom\n\t\t// (dropped from the C++ struct), so decoding is not cancellable here.\n\t\tlet f = state\n\t\t\t.retrieve_frame(\u0026p.time, p.time == crate::decoder::k_any_timecode(), None)?\n\t\t\t.ok_or_else(|| fail(\"no video frame available at the requested time\"))?;\n\n\t\tlet (w, h, bytes) = state.scale_video_to_f32(f, p.force_range)?;\n\t\tlet frame = copy_rgba_f32_to_frame(w, h, \u0026bytes, p.time)?;\n\t\tOk(Arc::new(frame))\n\t}\n\n\tfn retrieve_video(\u0026self, p: \u0026RetrieveVideoParams) -\u003e crate::error::Result\u003cOakRenderTexture\u003e {\n\t\t// The Rust `RetrieveVideoParams` carries no `OakRenderRenderer`, so\n\t\t// texture creation cannot be performed — the C++ failure path returns\n\t\t// an empty texture (ffmpegdecoder.cpp `process_frame_into_texture`).\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Video(_)) {\n\t\t\treturn Err(fail(\"decoder is not open on a video stream\"));\n\t\t}\n\t\tlet _ = state.retrieve_frame(\u0026p.time, p.time == crate::decoder::k_any_timecode(), None)?;\n\t\tOk(OakRenderTexture {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t})\n\t}\n\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\tdest: \u0026mut [f32],\n\t\trange: \u0026TimeRange,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Audio(_)) {\n\t\t\t// The stream does not support audio.\n\t\t\treturn Ok(RetrieveAudioStatus::Unsupported);\n\t\t}\n\t\tif sample_rate \u003c= 0 || channel_layout == 0 {\n\t\t\treturn Ok(RetrieveAudioStatus::Unsupported);\n\t\t}\n\t\tstate.retrieve_audio_to(dest, range, sample_rate, channel_layout)\n\t}\n\n\tfn conform_audio(\n\t\t\u0026self,\n\t\toutput_filenames: \u0026[String],\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Audio(_)) {\n\t\t\treturn Err(fail(\"decoder is not open on an audio stream\"));\n\t\t}\n\t\tstate.conform_audio_to(output_filenames, sample_rate, channel_layout, sample_format, cancelled)\n\t}\n\n\tfn get_audio_start_offset(\u0026self) -\u003e Rational {\n\t\t// # CPP-PARITY\n\t\t// `FFmpegDecoder::get_audio_start_offset`: format start minus stream\n\t\t// start, in seconds.\n\t\tlet state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tmatch state.as_ref() {\n\t\t\tSome(s) if s.format_start_time != AV_NOPTS_VALUE =\u003e {\n\t\t\t\tlet fmt_start = Rational::new(s.format_start_time, FB_TIME_BASE);\n\t\t\t\tlet str_start = oak_rational(s.stream_time_base).timestamp_to_time(s.stream_start_time);\n\t\t\t\tfmt_start - str_start\n\t\t\t}\n\t\t\t_ =\u003e Rational::new(0, 1),\n\t\t}\n\t}\n}\n\n/// A decoded frame plus its media type.\nenum DecodedFrame {\n\tVideo(ffmpeg::frame::Video),\n\tAudio(ffmpeg::frame::Audio),\n}\n\n/// Outcome of a single decoder receive attempt.\nenum Pull {\n\tFrame(DecodedFrame),\n\tEof,\n\tEagain,\n}\n\n/// Whether feeding one packet advanced the decode.\nenum PacketFeed {\n\tSent,\n\tInputEof,\n}\n\n/// One opened (filename, stream) decode session.\nstruct DecoderState {\n\tinput: ffmpeg::format::context::Input,\n\tstream_index: usize,\n\tinner: DecoderInner,\n\tstream_time_base: FfRational,\n\tstream_start_time: i64,\n\t/// Format start time in microseconds (`AV_TIME_BASE`).\n\tformat_start_time: i64,\n\tinput_sample_format: Sample,\n\tinput_sample_rate: u32,\n\tinput_channel_layout_mask: u64,\n\t/// True once the decoder has been drained.\n\teof: bool,\n\tvideo: Option\u003cVideoDecodeState\u003e,\n\taudio: Option\u003cAudioDecodeState\u003e,\n}\n\nenum DecoderInner {\n\tVideo(ffmpeg::codec::decoder::Video),\n\tAudio(ffmpeg::codec::decoder::Audio),\n}\n\n// SAFETY: `DecoderState` owns raw FFmpeg contexts that are not thread-safe\n// themselves, but every access goes through the `FFmpegDecoder::state`\n// `Mutex` (the C++ decoder uses the same lock-discipline around its bridge\n// instance), so concurrent access never happens.\nunsafe impl Send for DecoderState {}\nunsafe impl Sync for DecoderState {}\n\n/// Video decode state: the frame cache plus a cached swscale context.\nstruct VideoDecodeState {\n\tscaler: Option\u003cScalingCache\u003e,\n\tcache: VecDeque\u003cffmpeg::frame::Video\u003e,\n\tcache_at_zero: bool,\n\tcache_at_eof: bool,\n\t/// One second in the stream's time base.\n\tsecond_ts: i64,\n}\n\n/// Cached swscale context (recreated when any dimension/format changes).\nstruct ScalingCache {\n\tctx: scaling::Context,\n\tsrc_format: Pixel,\n\tsrc_width: u32,\n\tsrc_height: u32,\n\tdst_format: Pixel,\n\tdst_width: u32,\n\tdst_height: u32,\n}\n\n/// Audio decode state: a resampler keyed by (rate, layout).\nstruct AudioDecodeState {\n\tresampler: Option\u003c(u32, u64, AudioResampler)\u003e,\n}\n\n/// A swresample conversion context.\nstruct AudioResampler {\n\tctx: resampling::Context,\n\tdst_format: Sample,\n\tdst_layout: ChannelLayout,\n\tdst_rate: u32,\n\tdst_channels: usize,\n}\n\nimpl DecoderState {\n\t/// Open `(filename, stream_index)` for decoding.\n\tfn open(stream: \u0026CodecStream) -\u003e crate::error::Result\u003cDecoderState\u003e {\n\t\tlet mut dict = Dictionary::new();\n\t\tdict.set(\"analyzeduration\", \"5000000\");\n\t\tdict.set(\"probesize\", \"20000000\");\n\t\tlet input = ffmpeg::format::input_with_dictionary(\u0026stream.filename(), dict)\n\t\t\t.map_err(ffmpeg_err)?;\n\n\t\tlet stream_index = stream.stream() as usize;\n\t\tlet fstream = input\n\t\t\t.stream(stream_index)\n\t\t\t.ok_or_else(|| fail(format!(\"no stream {}\", stream.stream())))?;\n\t\tlet params = fstream.parameters();\n\t\tlet medium = params.medium();\n\t\tlet codec_id = params.id();\n\t\tlet codec = ffmpeg::decoder::find(codec_id)\n\t\t\t.ok_or_else(|| fail(format!(\"no decoder for codec {codec_id:?}\")))?;\n\n\t\t// Per-medium stream parameters (mirroring the bridge stream info);\n\t\t// read before `params` is moved into the codec context below.\n\t\tlet mut input_sample_format = Sample::None;\n\t\tlet mut input_sample_rate = 0;\n\t\tlet mut input_channel_layout_mask = 0u64;\n\t\tif matches!(medium, MediaType::Audio) {\n\t\t\tlet raw = unsafe { params.as_ptr() };\n\t\t\tinput_sample_format = sample_from_raw(unsafe { (*raw).format });\n\t\t\tinput_sample_rate = unsafe { (*raw).sample_rate }.max(0) as u32;\n\t\t\tinput_channel_layout_mask =\n\t\t\t\tunsafe { ChannelLayout::from((*raw).ch_layout) }.bits();\n\t\t}\n\n\t\tlet mut open_opts = Dictionary::new();\n\t\topen_opts.set(\"threads\", \"auto\");\n\t\tlet opened = ffmpeg::codec::Context::from_parameters(params)\n\t\t\t.map_err(ffmpeg_err)?\n\t\t\t.decoder()\n\t\t\t.open_as_with(codec, open_opts)\n\t\t\t.map_err(ffmpeg_err)?;\n\n\t\tlet inner = match medium {\n\t\t\tMediaType::Video =\u003e DecoderInner::Video(ffmpeg::codec::decoder::Video(opened)),\n\t\t\tMediaType::Audio =\u003e DecoderInner::Audio(ffmpeg::codec::decoder::Audio(opened)),\n\t\t\tother =\u003e {\n\t\t\t\treturn Err(fail(format!(\n\t\t\t\t\t\"stream {} is {:?}, expected video or audio\",\n\t\t\t\t\tstream.stream(),\n\t\t\t\t\tother\n\t\t\t\t)))\n\t\t\t}\n\t\t};\n\n\t\tlet stream_time_base = fstream.time_base();\n\t\tlet stream_start_time = fstream.start_time();\n\t\tlet format_start_time = unsafe { (*input.as_ptr()).start_time };\n\n\t\t// One second in the stream's time base (round of den/num).\n\t\tlet second_ts = (stream_time_base.1 as f64 / stream_time_base.0 as f64).round() as i64;\n\t\tlet video = if matches!(medium, MediaType::Video) {\n\t\t\tSome(VideoDecodeState {\n\t\t\t\tscaler: None,\n\t\t\t\tcache: VecDeque::new(),\n\t\t\t\tcache_at_zero: false,\n\t\t\t\tcache_at_eof: false,\n\t\t\t\tsecond_ts,\n\t\t\t})\n\t\t} else {\n\t\t\tNone\n\t\t};\n\t\tlet audio = if matches!(medium, MediaType::Audio) {\n\t\t\tSome(AudioDecodeState { resampler: None })\n\t\t} else {\n\t\t\tNone\n\t\t};\n\n\t\tOk(DecoderState {\n\t\t\tinput,\n\t\t\tstream_index,\n\t\t\tinner,\n\t\t\tstream_time_base,\n\t\t\tstream_start_time,\n\t\t\tformat_start_time,\n\t\t\tinput_sample_format,\n\t\t\tinput_sample_rate,\n\t\t\tinput_channel_layout_mask,\n\t\t\teof: false,\n\t\t\tvideo,\n\t\t\taudio,\n\t\t})\n\t}\n\n\tfn send_packet(\u0026mut self, packet: \u0026ffmpeg::packet::Packet) -\u003e crate::error::Result\u003c()\u003e {\n\t\tmatch \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e d.send_packet(packet).map_err(ffmpeg_err),\n\t\t\tDecoderInner::Audio(d) =\u003e d.send_packet(packet).map_err(ffmpeg_err),\n\t\t}\n\t}\n\n\tfn send_eof(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tmatch \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e d.send_eof().map_err(ffmpeg_err),\n\t\t\tDecoderInner::Audio(d) =\u003e d.send_eof().map_err(ffmpeg_err),\n\t\t}\n\t}\n\n\t/// One receive attempt on the decoder.\n\tfn pull(\u0026mut self) -\u003e crate::error::Result\u003cPull\u003e {\n\t\tlet result = match \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e {\n\t\t\t\tlet mut scratch = ffmpeg::frame::Video::empty();\n\t\t\t\td.receive_frame(\u0026mut scratch).map(|_| {\n\t\t\t\t\tPull::Frame(DecodedFrame::Video(std::mem::replace(\n\t\t\t\t\t\t\u0026mut scratch,\n\t\t\t\t\t\tffmpeg::frame::Video::empty(),\n\t\t\t\t\t)))\n\t\t\t\t})\n\t\t\t}\n\t\t\tDecoderInner::Audio(d) =\u003e {\n\t\t\t\tlet mut scratch = ffmpeg::frame::Audio::empty();\n\t\t\t\td.receive_frame(\u0026mut scratch).map(|_| {\n\t\t\t\t\tPull::Frame(DecodedFrame::Audio(std::mem::replace(\n\t\t\t\t\t\t\u0026mut scratch,\n\t\t\t\t\t\tffmpeg::frame::Audio::empty(),\n\t\t\t\t\t)))\n\t\t\t\t})\n\t\t\t}\n\t\t};\n\t\tmatch result {\n\t\t\tOk(p) =\u003e Ok(p),\n\t\t\tErr(FfmpegError::Eof) =\u003e Ok(Pull::Eof),\n\t\t\tErr(FfmpegError::Other { errno }) if errno == ffmpeg::error::EAGAIN =\u003e Ok(Pull::Eagain),\n\t\t\tErr(e) =\u003e Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\n\t/// Feed the next packet of the opened stream into the decoder.\n\tfn feed_packet(\u0026mut self) -\u003e crate::error::Result\u003cPacketFeed\u003e {\n\t\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\t\tmatch pkt.read(\u0026mut self.input) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tif pkt.stream() == self.stream_index {\n\t\t\t\t\tself.send_packet(\u0026pkt)?;\n\t\t\t\t}\n\t\t\t\tOk(PacketFeed::Sent)\n\t\t\t}\n\t\t\tErr(FfmpegError::Eof) =\u003e {\n\t\t\t\tself.send_eof()?;\n\t\t\t\tOk(PacketFeed::InputEof)\n\t\t\t}\n\t\t\tErr(e) =\u003e Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\n\t/// Pull the next decoded frame, feeding packets as needed. Returns\n\t/// `Ok(None)` once the decoder is drained.\n\tfn next_frame(\u0026mut self) -\u003e crate::error::Result\u003cOption\u003cDecodedFrame\u003e\u003e {\n\t\tif self.eof {\n\t\t\treturn Ok(None);\n\t\t}\n\t\tloop {\n\t\t\tmatch self.pull()? {\n\t\t\t\tPull::Frame(f) =\u003e return Ok(Some(f)),\n\t\t\t\tPull::Eof =\u003e {\n\t\t\t\t\tself.eof = true;\n\t\t\t\t\treturn Ok(None);\n\t\t\t\t}\n\t\t\t\tPull::Eagain =\u003e {\n\t\t\t\t\tself.feed_packet()?;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Seek to `timestamp` (in the stream's time base) and flush the decoder.\n\t///\n\t/// # CPP-PARITY\n\t/// `fb_decoder_seek` semantics (backward seek on the stream, decoder\n\t/// flushed afterwards).\n\tfn seek(\u0026mut self, timestamp: i64) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet stream_tb = self\n\t\t\t.input\n\t\t\t.stream(self.stream_index)\n\t\t\t.map(|s| s.time_base())\n\t\t\t.unwrap_or(FfRational(1, 1));\n\t\tlet target = unsafe {\n\t\t\tsys::av_rescale_q(timestamp, self.stream_time_base.into(), stream_tb.into())\n\t\t};\n\t\tlet ret = unsafe {\n\t\t\tsys::av_seek_frame(\n\t\t\t\tself.input.as_mut_ptr(),\n\t\t\t\tself.stream_index as i32,\n\t\t\t\ttarget,\n\t\t\t\tsys::AVSEEK_FLAG_BACKWARD,\n\t\t\t)\n\t\t};\n\t\tif ret \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(ret)));\n\t\t}\n\t\tmatch \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e d.flush(),\n\t\t\tDecoderInner::Audio(d) =\u003e d.flush(),\n\t\t}\n\t\tself.eof = false;\n\t\tOk(())\n\t}\n\n\t/// Retrieve the video frame at (or before) `time`, mirroring the C++\n\t/// `retrieve_frame` seek/cache logic.\n\tfn retrieve_frame(\n\t\t\u0026mut self,\n\t\ttime: \u0026Rational,\n\t\tany_timecode: bool,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003cOption\u003cffmpeg::frame::Video\u003e\u003e {\n\t\t// Move the video state out so `self.seek` / `self.pull` (which touch\n\t\t// other fields) can be called without conflicting borrows.\n\t\tlet mut video = self\n\t\t\t.video\n\t\t\t.take()\n\t\t\t.expect(\"retrieve_frame requires a video session\");\n\n\t\tlet mut target_ts = oak_rational(self.stream_time_base).time_to_timestamp(*time);\n\t\tif self.format_start_time != AV_NOPTS_VALUE {\n\t\t\ttarget_ts += unsafe {\n\t\t\t\tsys::av_rescale_q(\n\t\t\t\t\tself.format_start_time,\n\t\t\t\t\tFfRational(1, FB_TIME_BASE as i32).into(),\n\t\t\t\t\tself.stream_time_base.into(),\n\t\t\t\t)\n\t\t\t};\n\t\t}\n\n\t\tconst MIN_SEEK: i64 = 0;\n\t\tlet mut seek_ts = (target_ts - maximum_queue_size() as i64).max(MIN_SEEK);\n\t\tlet mut still_seeking = false;\n\n\t\tif !any_timecode {\n\t\t\t// If the frame wasn't in the frame cache, see if this cache is\n\t\t\t// too old to use (CPP-PARITY ffmpegdecoder.cpp:975).\n\t\t\tif video.cache.is_empty()\n\t\t\t\t|| target_ts \u003c pts_of(video.cache.front()).unwrap_or(AV_NOPTS_VALUE)\n\t\t\t\t|| target_ts\n\t\t\t\t\t\u003e pts_of(video.cache.back()).unwrap_or(AV_NOPTS_VALUE) + 2 * video.second_ts\n\t\t\t{\n\t\t\t\tvideo.cache.clear();\n\t\t\t\tvideo.cache_at_eof = false;\n\n\t\t\t\tself.seek(seek_ts)?;\n\t\t\t\tif seek_ts == MIN_SEEK {\n\t\t\t\t\tvideo.cache_at_zero = true;\n\t\t\t\t}\n\n\t\t\t\tstill_seeking = true;\n\t\t\t} else if let Some(cached) = get_frame_from_cache(\u0026video, target_ts) {\n\t\t\t\tself.video = Some(video);\n\t\t\t\treturn Ok(Some(cached));\n\t\t\t}\n\t\t}\n\n\t\tlet mut retried_after_eof = false;\n\t\tlet mut return_frame: Option\u003cffmpeg::frame::Video\u003e = None;\n\n\t\tloop {\n\t\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet frame = match self.pull()? {\n\t\t\t\tPull::Frame(DecodedFrame::Video(f)) =\u003e f,\n\t\t\t\tPull::Frame(_) =\u003e unreachable!(\"video session yields only video frames\"),\n\t\t\t\tPull::Eof =\u003e {\n\t\t\t\t\t// Handle an \"expected\" EOF by using the last cached frame\n\t\t\t\t\t// (CPP-PARITY ffmpegdecoder.cpp:1043).\n\t\t\t\t\tvideo.cache_at_eof = true;\n\t\t\t\t\tif video.cache.is_empty() {\n\t\t\t\t\t\tif !retried_after_eof {\n\t\t\t\t\t\t\tretried_after_eof = true;\n\t\t\t\t\t\t\tvideo.cache.clear();\n\t\t\t\t\t\t\tself.seek(MIN_SEEK)?;\n\t\t\t\t\t\t\tvideo.cache_at_zero = true;\n\t\t\t\t\t\t\tstill_seeking = true;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tself.video = Some(video);\n\t\t\t\t\t\treturn Err(fail(\"unexpected codec EOF - unable to retrieve frame\"));\n\t\t\t\t\t}\n\t\t\t\t\treturn_frame = video.cache.back().cloned();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tPull::Eagain =\u003e {\n\t\t\t\t\tself.feed_packet()?;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet frame_pts = frame.pts().unwrap_or(AV_NOPTS_VALUE);\n\n\t\t\tif still_seeking {\n\t\t\t\t// Handle a failed seek: step back by one second and retry\n\t\t\t\t// (CPP-PARITY ffmpegdecoder.cpp:1028).\n\t\t\t\tif !video.cache_at_zero \u0026\u0026 frame_pts \u003e target_ts {\n\t\t\t\t\tseek_ts = (seek_ts - video.second_ts).max(MIN_SEEK);\n\t\t\t\t\tself.seek(seek_ts)?;\n\t\t\t\t\tif seek_ts == MIN_SEEK {\n\t\t\t\t\t\tvideo.cache_at_zero = true;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tstill_seeking = false;\n\t\t\t}\n\n\t\t\t// Cut the cache down to `maximum_queue_size` before pushing\n\t\t\t// (CPP-PARITY ffmpegdecoder.cpp:1065).\n\t\t\tif video.cache.len() \u003e maximum_queue_size() {\n\t\t\t\tvideo.cache.pop_front();\n\t\t\t\tvideo.cache_at_zero = false;\n\t\t\t}\n\n\t\t\tlet previous = video.cache.back().cloned();\n\t\t\tvideo.cache.push_back(frame);\n\n\t\t\tif frame_pts == target_ts || any_timecode {\n\t\t\t\treturn_frame = video.cache.back().cloned();\n\t\t\t\tbreak;\n\t\t\t} else if frame_pts \u003e target_ts {\n\t\t\t\tif previous.is_none() \u0026\u0026 video.cache_at_zero {\n\t\t\t\t\treturn_frame = video.cache.back().cloned();\n\t\t\t\t} else {\n\t\t\t\t\treturn_frame = previous;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tself.video = Some(video);\n\t\tOk(return_frame)\n\t}\n\n\t/// Scale a decoded frame to float RGBA (F32, 4 channels), returning the\n\t/// raw pixel bytes plus dimensions. Mirrors `pre_process_frame` +\n\t/// `retrieve_video_frame_internal` scaling with the color-range forcing.\n\tfn scale_video_to_f32(\n\t\t\u0026mut self,\n\t\tf: ffmpeg::frame::Video,\n\t\tforce_range: i32,\n\t) -\u003e crate::error::Result\u003c(u32, u32, Vec\u003cu8\u003e)\u003e {\n\t\tlet video = self\n\t\t\t.video\n\t\t\t.as_mut()\n\t\t\t.expect(\"scale_video_to_f32 requires a video session\");\n\n\t\t// # CPP-PARITY ffmpegdecoder.cpp:376: disregard \"JPEG\" pixel formats\n\t\t// and force the color range to whatever the caller requested.\n\t\tlet src_format = convert_jpeg_space_to_regular_space(f.format());\n\t\tlet mut f = f;\n\t\tf.set_format(src_format);\n\t\tf.set_color_range(if force_range == OAKCOMMON_COLOR_RANGE_FULL {\n\t\t\tffmpeg::color::Range::JPEG\n\t\t} else {\n\t\t\tffmpeg::color::Range::MPEG\n\t\t});\n\n\t\tlet (w, h) = (f.width(), f.height());\n\n\t\t// swscale cannot reliably output float RGBA on every build; prefer\n\t\t// RGBAF32LE and fall back to RGBA64 (converted to f32 below).\n\t\tlet bytes = match get_or_create_scaler(\u0026mut video.scaler, src_format, w, h, Pixel::RGBAF32LE, w, h) {\n\t\t\tOk(ctx) =\u003e {\n\t\t\t\tlet mut out = ffmpeg::frame::Video::empty();\n\t\t\t\tctx.run(\u0026f, \u0026mut out).map_err(ffmpeg_err)?;\n\t\t\t\tlet stride = out.stride(0);\n\t\t\t\tconvert_rgba_f32_le(\u0026out.data(0), w, h, stride)\n\t\t\t}\n\t\t\tErr(_) =\u003e {\n\t\t\t\tlet ctx = get_or_create_scaler(\u0026mut video.scaler, src_format, w, h, Pixel::RGBA64LE, w, h)?;\n\t\t\t\tlet mut out = ffmpeg::frame::Video::empty();\n\t\t\t\tctx.run(\u0026f, \u0026mut out).map_err(ffmpeg_err)?;\n\t\t\t\tlet stride = out.stride(0);\n\t\t\t\tconvert_rgba64_to_f32(\u0026out.data(0), w, h, stride)\n\t\t\t}\n\t\t};\n\t\tOk((w, h, bytes))\n\t}\n\n\t/// Fill `dest` (interleaved f32) with the decoded audio covering\n\t/// `range`, resampled to `sample_rate` / `channel_layout`. Samples\n\t/// outside the decoded media are left as silence.\n\tfn retrieve_audio_to(\n\t\t\u0026mut self,\n\t\tdest: \u0026mut [f32],\n\t\trange: \u0026TimeRange,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\tlet dst_layout = channel_layout_from_mask(channel_layout);\n\t\tlet dst_channels = dst_layout.channels().max(0) as usize;\n\t\tif dst_channels == 0 {\n\t\t\treturn Ok(RetrieveAudioStatus::Unsupported);\n\t\t}\n\n\t\tlet start_sec = range.in_().to_f64();\n\t\tlet end_sec = range.out().to_f64();\n\t\tlet start_sample = (start_sec * sample_rate as f64).round() as i64;\n\t\tlet end_sample = (end_sec * sample_rate as f64).round() as i64;\n\t\tif end_sample \u003c= start_sample {\n\t\t\treturn Ok(RetrieveAudioStatus::Success);\n\t\t}\n\n\t\tdest.fill(0.0);\n\n\t\t// Seek to just before the range start.\n\t\tlet start_ts = oak_rational(self.stream_time_base)\n\t\t\t.time_to_timestamp(Rational::from_double(start_sec));\n\t\tself.seek(start_ts)?;\n\n\t\t// Take the cached resampler out (or create one) so the decode loop\n\t\t// below can borrow `self` freely; it is put back before returning.\n\t\tlet src_layout = channel_layout_from_mask(self.input_channel_layout_mask);\n\t\tlet mut resampler = match self\n\t\t\t.audio\n\t\t\t.as_mut()\n\t\t\t.expect(\"audio session\")\n\t\t\t.resampler\n\t\t\t.take()\n\t\t{\n\t\t\tSome((rate, layout, rs)) if rate == sample_rate as u32 \u0026\u0026 layout == channel_layout =\u003e rs,\n\t\t\t_ =\u003e AudioResampler::get(\n\t\t\t\tself.input_sample_format,\n\t\t\t\tsrc_layout,\n\t\t\t\tself.input_sample_rate,\n\t\t\t\tSample::F32(SampleType::Packed),\n\t\t\t\tdst_layout,\n\t\t\t\tsample_rate as u32,\n\t\t\t)?,\n\t\t};\n\t\tlet stream_time_base = self.stream_time_base;\n\n\t\tlet mut next_sample: Option\u003ci64\u003e = None;\n\t\tlet dest_frames = (dest.len() / dst_channels) as i64;\n\n\t\twhile let Some(frame) = self.next_frame()? {\n\t\t\tlet DecodedFrame::Audio(audio) = frame else {\n\t\t\t\tcontinue;\n\t\t\t};\n\t\t\tlet converted = resample_to_interleaved_f32(\u0026mut resampler, \u0026audio)?;\n\t\t\tif converted.is_empty() {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet chunk_samples = (converted.len() / dst_channels) as i64;\n\t\t\tlet frame_start = match audio.pts() {\n\t\t\t\tSome(pts) =\u003e {\n\t\t\t\t\tlet secs = oak_rational(stream_time_base).timestamp_to_time(pts).to_f64();\n\t\t\t\t\t(secs * sample_rate as f64).round() as i64\n\t\t\t\t}\n\t\t\t\tNone =\u003e next_sample.unwrap_or(start_sample),\n\t\t\t};\n\t\t\tlet offset = frame_start - start_sample;\n\t\t\tif offset \u003c dest_frames \u0026\u0026 offset + chunk_samples \u003e 0 {\n\t\t\t\tlet copy_start = offset.max(0) as usize;\n\t\t\t\tlet copy_end = (offset + chunk_samples).min(dest_frames).max(0) as usize;\n\t\t\t\tif copy_end \u003e copy_start {\n\t\t\t\t\tlet src_off = (copy_start as i64 - offset) as usize * dst_channels;\n\t\t\t\t\tlet n = (copy_end - copy_start) * dst_channels;\n\t\t\t\t\tlet dst_off = copy_start * dst_channels;\n\t\t\t\t\tif dst_off + n \u003c= dest.len() {\n\t\t\t\t\t\tdest[dst_off..dst_off + n]\n\t\t\t\t\t\t\t.copy_from_slice(\u0026converted[src_off..src_off + n]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tnext_sample = Some(frame_start + chunk_samples);\n\t\t\tif offset + chunk_samples \u003e= dest_frames {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Put the resampler back into the cache for the next call.\n\t\tself.audio\n\t\t\t.as_mut()\n\t\t\t.expect(\"audio session\")\n\t\t\t.resampler = Some((sample_rate as u32, channel_layout, resampler));\n\n\t\t// Flush any samples still buffered in the resampler (rate conversion\n\t\t// tail), appending after the last decoded sample.\n\t\tlet resampler = self\n\t\t\t.audio\n\t\t\t.as_mut()\n\t\t\t.expect(\"audio session\")\n\t\t\t.resampler\n\t\t\t.as_mut()\n\t\t\t.map(|r| \u0026mut r.2)\n\t\t\t.unwrap();\n\t\tif let Some(flush) = flush_resampler_interleaved_f32(resampler)? {\n\t\t\tif !flush.is_empty() {\n\t\t\t\tlet chunk_samples = (flush.len() / dst_channels) as i64;\n\t\t\t\tlet frame_start = next_sample.unwrap_or(start_sample);\n\t\t\t\tlet offset = frame_start - start_sample;\n\t\t\t\tif offset \u003c dest_frames \u0026\u0026 offset + chunk_samples \u003e 0 {\n\t\t\t\t\tlet copy_start = offset.max(0) as usize;\n\t\t\t\t\tlet copy_end = (offset + chunk_samples).min(dest_frames).max(0) as usize;\n\t\t\t\t\tif copy_end \u003e copy_start {\n\t\t\t\t\t\tlet src_off = (copy_start as i64 - offset) as usize * dst_channels;\n\t\t\t\t\t\tlet n = (copy_end - copy_start) * dst_channels;\n\t\t\t\t\t\tlet dst_off = copy_start * dst_channels;\n\t\t\t\t\t\tif dst_off + n \u003c= dest.len() {\n\t\t\t\t\t\t\tdest[dst_off..dst_off + n]\n\t\t\t\t\t\t\t\t.copy_from_slice(\u0026flush[src_off..src_off + n]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(RetrieveAudioStatus::Success)\n\t}\n\n\t/// Conform the open stream's audio into per-channel PCM files, mirroring\n\t/// `FFmpegDecoder::conform_audio_internal`.\n\tfn conform_audio_to(\n\t\t\u0026mut self,\n\t\toutput_filenames: \u0026[String],\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif self.input_channel_layout_mask == 0 {\n\t\t\treturn Err(fail(\"could not determine the channel layout of the audio file\"));\n\t\t}\n\n\t\tlet target_fmt = crate::encodingparams::sample_format_from_i32(sample_format);\n\t\tlet dst_sample = sample_format_to_ffmpeg(target_fmt)\n\t\t\t.ok_or_else(|| fail(format!(\"unsupported conform sample format {sample_format}\")))?;\n\t\tlet dst_layout = channel_layout_from_mask(channel_layout);\n\t\tlet planar = dst_sample.is_planar();\n\t\tlet planes = if planar {\n\t\t\tdst_layout.channels().max(0) as usize\n\t\t} else {\n\t\t\t1\n\t\t};\n\t\tif planes == 0 {\n\t\t\treturn Err(fail(\"invalid conform channel layout\"));\n\t\t}\n\n\t\t// Seek to the start (CPP-PARITY ffmpegdecoder.cpp:694).\n\t\tself.seek(0)?;\n\n\t\tlet mut resampler = AudioResampler::get(\n\t\t\tself.input_sample_format,\n\t\t\tchannel_layout_from_mask(self.input_channel_layout_mask),\n\t\t\tself.input_sample_rate,\n\t\t\tdst_sample,\n\t\t\tdst_layout,\n\t\t\tsample_rate as u32,\n\t\t)?;\n\n\t\tlet filenames: Vec\u003cPathBuf\u003e = output_filenames.iter().map(PathBuf::from).collect();\n\t\tlet mut wave_out = crate::planarfiledevice::PlanarFileDevice::new();\n\t\tif !wave_out.open(\u0026filenames, crate::planarfiledevice::OpenMode::WriteOnly) {\n\t\t\treturn Err(fail(\"failed to open conform output files\"));\n\t\t}\n\n\t\tlet bytes_per_sample = dst_sample.bytes();\n\t\twhile let Some(frame) = self.next_frame()? {\n\t\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet DecodedFrame::Audio(audio) = frame else {\n\t\t\t\tcontinue;\n\t\t\t};\n\t\t\tlet planes_buf = resampler.convert_to_planes(\u0026audio)?;\n\t\t\tlet written = planes_buf.first().map_or(0, Vec::len) / bytes_per_sample;\n\t\t\tif written \u003e 0 {\n\t\t\t\tlet refs: Vec\u003c\u0026[u8]\u003e = planes_buf.iter().map(|p| p.as_slice()).collect();\n\t\t\t\twave_out.write(\u0026refs, written as i64 * bytes_per_sample as i64, 0);\n\t\t\t}\n\t\t}\n\t\t// Flush the resampler tail.\n\t\tfor planes_buf in resampler.flush_planes()? {\n\t\t\tlet written = planes_buf.first().map_or(0, Vec::len) / bytes_per_sample;\n\t\t\tif written \u003e 0 {\n\t\t\t\tlet refs: Vec\u003c\u0026[u8]\u003e = planes_buf.iter().map(|p| p.as_slice()).collect();\n\t\t\t\twave_out.write(\u0026refs, written as i64 * bytes_per_sample as i64, 0);\n\t\t\t}\n\t\t}\n\t\twave_out.close();\n\n\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\treturn Err(crate::error::Error::Cancelled);\n\t\t}\n\t\tOk(())\n\t}\n\n}\n\nimpl AudioResampler {\n\t/// Create a resampler converting from the source definition to the\n\t/// destination definition.\n\tfn get(\n\t\tsrc_format: Sample,\n\t\tsrc_layout: ChannelLayout,\n\t\tsrc_rate: u32,\n\t\tdst_format: Sample,\n\t\tdst_layout: ChannelLayout,\n\t\tdst_rate: u32,\n\t) -\u003e crate::error::Result\u003cAudioResampler\u003e {\n\t\tlet ctx = resampling::Context::get(\n\t\t\tsrc_format, src_layout, src_rate, dst_format, dst_layout, dst_rate,\n\t\t)\n\t\t.map_err(ffmpeg_err)?;\n\t\tOk(AudioResampler {\n\t\t\tctx,\n\t\t\tdst_format,\n\t\t\tdst_layout,\n\t\t\tdst_rate,\n\t\t\tdst_channels: dst_layout.channels().max(0) as usize,\n\t\t})\n\t}\n\n\t/// Convert one input frame, returning one byte buffer per output plane\n\t/// (plane 0 for packed destinations).\n\tfn convert_to_planes(\u0026mut self, input: \u0026ffmpeg::frame::Audio) -\u003e crate::error::Result\u003cVec\u003cVec\u003cu8\u003e\u003e\u003e {\n\t\tlet (written, bufs) = self.swr_convert_buffers(input)?;\n\t\tlet _ = written;\n\t\tOk(bufs)\n\t}\n\n\t/// Convert one input frame into a freshly allocated output frame in the\n\t/// destination format, layout and rate.\n\tfn convert_to_frame(\u0026mut self, input: \u0026ffmpeg::frame::Audio) -\u003e crate::error::Result\u003cffmpeg::frame::Audio\u003e {\n\t\tlet (written, bufs) = self.swr_convert_buffers(input)?;\n\t\tlet mut out = ffmpeg::frame::Audio::new(self.dst_format, written, self.dst_layout);\n\t\tout.set_rate(self.dst_rate);\n\t\tfor (plane, buf) in bufs.iter().enumerate() {\n\t\t\tlet len = buf.len().min(out.data_mut(plane).len());\n\t\t\tout.data_mut(plane)[..len].copy_from_slice(\u0026buf[..len]);\n\t\t}\n\t\tOk(out)\n\t}\n\n\t/// Flush any samples still buffered inside the resampler, as planes.\n\tfn flush_planes(\u0026mut self) -\u003e crate::error::Result\u003cVec\u003cVec\u003cVec\u003cu8\u003e\u003e\u003e\u003e {\n\t\tlet mut all: Vec\u003cVec\u003cVec\u003cu8\u003e\u003e\u003e = Vec::new();\n\t\tfor _ in 0..32 {\n\t\t\tlet out_samples = unsafe { sys::swr_get_out_samples(self.ctx.as_mut_ptr(), 0) };\n\t\t\tif out_samples \u003c= 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet (bufs, written) = self.swr_convert(out_samples as usize, None, 0)?;\n\t\t\tif written == 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tall.push(bufs);\n\t\t}\n\t\tOk(all)\n\t}\n\n\t/// Run the swr conversion for `input`, returning the number of written\n\t/// samples and one byte buffer per output plane.\n\tfn swr_convert_buffers(\n\t\t\u0026mut self,\n\t\tinput: \u0026ffmpeg::frame::Audio,\n\t) -\u003e crate::error::Result\u003c(usize, Vec\u003cVec\u003cu8\u003e\u003e)\u003e {\n\t\tlet in_samples = input.samples() as i32;\n\t\tlet out_samples = unsafe { sys::swr_get_out_samples(self.ctx.as_mut_ptr(), in_samples) };\n\t\tif out_samples \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(out_samples)));\n\t\t}\n\t\tif out_samples == 0 {\n\t\t\treturn Ok((0, Vec::new()));\n\t\t}\n\t\tlet in_ptrs: Vec\u003c*const u8\u003e = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();\n\t\tlet (bufs, written) = self.swr_convert(out_samples as usize, Some(\u0026in_ptrs), in_samples)?;\n\t\tOk((written, bufs))\n\t}\n\n\t/// Allocate output buffers for `out_samples` and run `swr_convert`\n\t/// (with the given input pointers, or a drain when `None`).\n\tfn swr_convert(\n\t\t\u0026mut self,\n\t\tout_samples: usize,\n\t\tin_ptrs: Option\u003c\u0026[*const u8]\u003e,\n\t\tin_samples: i32,\n\t) -\u003e crate::error::Result\u003c(Vec\u003cVec\u003cu8\u003e\u003e, usize)\u003e {\n\t\tlet planar = self.dst_format.is_planar();\n\t\tlet planes = if planar { self.dst_channels.max(1) } else { 1 };\n\t\tlet plane_len =\n\t\t\tout_samples * self.dst_format.bytes() * if planar { 1 } else { self.dst_channels };\n\t\tlet mut bufs: Vec\u003cVec\u003cu8\u003e\u003e = (0..planes).map(|_| vec![0u8; plane_len]).collect();\n\t\tlet mut out_ptrs: Vec\u003c*mut u8\u003e = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();\n\t\tlet written = unsafe {\n\t\t\tsys::swr_convert(\n\t\t\t\tself.ctx.as_mut_ptr(),\n\t\t\t\tout_ptrs.as_mut_ptr(),\n\t\t\t\tout_samples as i32,\n\t\t\t\tin_ptrs.map_or(std::ptr::null(), |p| p.as_ptr()),\n\t\t\t\tin_samples,\n\t\t\t)\n\t\t};\n\t\tif written \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(written)));\n\t\t}\n\t\tfor buf in bufs.iter_mut() {\n\t\t\tlet keep = written as usize * self.dst_format.bytes()\n\t\t\t\t* if planar { 1 } else { self.dst_channels };\n\t\t\tbuf.truncate(keep);\n\t\t}\n\t\tOk((bufs, written as usize))\n\t}\n}\n\n/// Convert a decoded audio frame to interleaved f32 at the resampler's\n/// destination definition.\nfn resample_to_interleaved_f32(\n\tresampler: \u0026mut AudioResampler,\n\tinput: \u0026ffmpeg::frame::Audio,\n) -\u003e crate::error::Result\u003cVec\u003cf32\u003e\u003e {\n\tlet in_samples = input.samples() as i32;\n\tlet out_samples = unsafe { sys::swr_get_out_samples(resampler.ctx.as_mut_ptr(), in_samples) };\n\tif out_samples \u003c 0 {\n\t\treturn Err(ffmpeg_err(FfmpegError::from(out_samples)));\n\t}\n\tif out_samples == 0 {\n\t\treturn Ok(Vec::new());\n\t}\n\tlet out_samples = out_samples as usize;\n\tlet channels = resampler.dst_channels;\n\tlet mut buf = vec![0f32; out_samples * channels];\n\tlet out_ptrs = [buf.as_mut_ptr() as *mut u8];\n\tlet in_ptrs: Vec\u003c*const u8\u003e = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();\n\tlet written = unsafe {\n\t\tsys::swr_convert(\n\t\t\tresampler.ctx.as_mut_ptr(),\n\t\t\tout_ptrs.as_ptr(),\n\t\t\tout_samples as i32,\n\t\t\tin_ptrs.as_ptr(),\n\t\t\tin_samples,\n\t\t)\n\t};\n\tif written \u003c 0 {\n\t\treturn Err(ffmpeg_err(FfmpegError::from(written)));\n\t}\n\tbuf.truncate(written as usize * channels);\n\tOk(buf)\n}\n\n/// Flush any samples buffered in the resampler as interleaved f32.\nfn flush_resampler_interleaved_f32(\n\tresampler: \u0026mut AudioResampler,\n) -\u003e crate::error::Result\u003cOption\u003cVec\u003cf32\u003e\u003e\u003e {\n\tlet channels = resampler.dst_channels;\n\tlet mut out = Vec::new();\n\tfor _ in 0..32 {\n\t\tlet out_samples = unsafe { sys::swr_get_out_samples(resampler.ctx.as_mut_ptr(), 0) };\n\t\tif out_samples \u003c= 0 {\n\t\t\tbreak;\n\t\t}\n\t\tlet out_samples = out_samples as usize;\n\t\tlet mut buf = vec![0f32; out_samples * channels];\n\t\tlet out_ptrs = [buf.as_mut_ptr() as *mut u8];\n\t\tlet written = unsafe {\n\t\t\tsys::swr_convert(\n\t\t\t\tresampler.ctx.as_mut_ptr(),\n\t\t\t\tout_ptrs.as_ptr(),\n\t\t\t\tout_samples as i32,\n\t\t\t\tstd::ptr::null(),\n\t\t\t\t0,\n\t\t\t)\n\t\t};\n\t\tif written \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(written)));\n\t\t}\n\t\tif written == 0 {\n\t\t\tbreak;\n\t\t}\n\t\tbuf.truncate(written as usize * channels);\n\t\tout.extend_from_slice(\u0026buf);\n\t}\n\tif out.is_empty() {\n\t\tOk(None)\n\t} else {\n\t\tOk(Some(out))\n\t}\n}\n\n/// Get the cached scaler for a (src → dst) pair, recreating it when the\n/// parameters change.\nfn get_or_create_scaler(\n\tcache: \u0026mut Option\u003cScalingCache\u003e,\n\tsrc_format: Pixel,\n\tsrc_width: u32,\n\tsrc_height: u32,\n\tdst_format: Pixel,\n\tdst_width: u32,\n\tdst_height: u32,\n) -\u003e crate::error::Result\u003c\u0026mut scaling::Context\u003e {\n\tlet recreate = match cache.as_ref() {\n\t\tSome(s) =\u003e {\n\t\t\ts.src_format != src_format\n\t\t\t\t|| s.src_width != src_width\n\t\t\t\t|| s.src_height != src_height\n\t\t\t\t|| s.dst_format != dst_format\n\t\t\t\t|| s.dst_width != dst_width\n\t\t\t\t|| s.dst_height != dst_height\n\t\t}\n\t\tNone =\u003e true,\n\t};\n\tif recreate {\n\t\tlet ctx = scaling::Context::get(\n\t\t\tsrc_format,\n\t\t\tsrc_width,\n\t\t\tsrc_height,\n\t\t\tdst_format,\n\t\t\tdst_width,\n\t\t\tdst_height,\n\t\t\tscaling::Flags::POINT,\n\t\t)\n\t\t.map_err(ffmpeg_err)?;\n\t\t*cache = Some(ScalingCache {\n\t\t\tctx,\n\t\t\tsrc_format,\n\t\t\tsrc_width,\n\t\t\tsrc_height,\n\t\t\tdst_format,\n\t\t\tdst_width,\n\t\t\tdst_height,\n\t\t});\n\t}\n\tOk(\u0026mut cache.as_mut().expect(\"set above\").ctx)\n}\n\n/// The frame's presentation timestamp (NOPTS when unset).\nfn pts_of(f: Option\u003c\u0026ffmpeg::frame::Video\u003e) -\u003e Option\u003ci64\u003e {\n\tf.and_then(|f| f.pts())\n}\n\n/// Search the frame cache for the frame at (or closest to) `t`.\n///\n/// # CPP-PARITY\n/// `FFmpegDecoder::get_frame_from_cache`.\nfn get_frame_from_cache(video: \u0026VideoDecodeState, t: i64) -\u003e Option\u003cffmpeg::frame::Video\u003e {\n\tlet front = pts_of(video.cache.front()).unwrap_or(AV_NOPTS_VALUE);\n\tlet back = pts_of(video.cache.back()).unwrap_or(AV_NOPTS_VALUE);\n\tif t \u003c front {\n\t\tif video.cache_at_zero {\n\t\t\treturn video.cache.front().cloned();\n\t\t}\n\t} else if t \u003e back {\n\t\tif video.cache_at_eof {\n\t\t\treturn video.cache.back().cloned();\n\t\t}\n\t} else {\n\t\tfor (i, frame) in video.cache.iter().enumerate() {\n\t\t\tlet this_pts = frame.pts().unwrap_or(AV_NOPTS_VALUE);\n\t\t\tlet next_pts = video\n\t\t\t\t.cache\n\t\t\t\t.get(i + 1)\n\t\t\t\t.and_then(|f| f.pts())\n\t\t\t\t.unwrap_or(AV_NOPTS_VALUE);\n\t\t\tif this_pts == t || next_pts \u003e t {\n\t\t\t\treturn Some(frame.clone());\n\t\t\t}\n\t\t}\n\t}\n\tNone\n}\n\n/// Decoder frame-cache size (C++ `maximum_queue_size`).\nfn maximum_queue_size() -\u003e usize {\n\t// # CPP-PARITY ffmpegdecoder.cpp:1184 — \"Fairly arbitrary size... This\n\t// value may be tweaked over time.\"\n\t2\n}\n\n/// Copy a packed F32-RGBA byte buffer (as produced by swscale RGBAF32LE)\n/// into an owned buffer, forcing alpha opaque (1.0) per pixel\n/// (CPP-PARITY ffmpegdecoder.cpp:428).\nfn convert_rgba_f32_le(data: \u0026[u8], w: u32, h: u32, stride: usize) -\u003e Vec\u003cu8\u003e {\n\tlet mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];\n\tfor y in 0..h as usize {\n\t\tlet row = \u0026data[y * stride..y * stride + (w as usize) * PIXEL_F32_BYTES];\n\t\tlet dst = \u0026mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];\n\t\tdst.copy_from_slice(row);\n\t\tfor px in dst.chunks_exact_mut(PIXEL_F32_BYTES) {\n\t\t\tpx[12..16].copy_from_slice(\u00261.0f32.to_le_bytes());\n\t\t}\n\t}\n\tout\n}\n\n/// Copy a packed u16-RGBA buffer (RGBA64LE) into F32 RGBA bytes.\nfn convert_rgba64_to_f32(data: \u0026[u8], w: u32, h: u32, stride: usize) -\u003e Vec\u003cu8\u003e {\n\tlet mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];\n\tfor y in 0..h as usize {\n\t\tlet row = \u0026data[y * stride..y * stride + (w as usize) * 8];\n\t\tlet dst = \u0026mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];\n\t\tfor (px, src_px) in dst.chunks_exact_mut(PIXEL_F32_BYTES).zip(row.chunks_exact(8)) {\n\t\t\tfor c in 0..3 {\n\t\t\t\tlet v = u16::from_le_bytes([src_px[c * 2], src_px[c * 2 + 1]]);\n\t\t\t\tpx[c * 4..c * 4 + 4].copy_from_slice(\u0026(v as f32 / 65535.0).to_le_bytes());\n\t\t\t}\n\t\t\tpx[12..16].copy_from_slice(\u00261.0f32.to_le_bytes());\n\t\t}\n\t}\n\tout\n}\n\n/// Build an allocated [`Frame`] (F32, RGBA) from raw pixel bytes.\n///\n/// # CPP-PARITY\n/// `copy_packed_av_frame_to_frame` (ffmpegdecoder.cpp:116): params are built\n/// from the decoded dimensions, the buffer is allocated and each row is\n/// copied with the destination linesize.\nfn copy_rgba_f32_to_frame(\n\twidth: u32,\n\theight: u32,\n\tbytes: \u0026[u8],\n\ttimestamp: Rational,\n) -\u003e crate::error::Result\u003cFrame\u003e {\n\tlet params = unsafe { oakcommon_videoparams_init_basic(width as i32, height as i32) };\n\tunsafe {\n\t\toakcommon_videoparams_set_format(params.clone(), PixelFormat::F32 as i32);\n\t\toakcommon_videoparams_set_channel_count(params.clone(), VIDEO_CHANNELS);\n\t}\n\tlet mut frame = Frame::with_params(params);\n\tframe.set_timestamp(timestamp);\n\tframe.allocate()?;\n\n\tlet row_bytes = (width as usize) * PIXEL_F32_BYTES;\n\tlet linesize = frame.linesize_bytes() as usize;\n\tlet out = frame.data_mut().ok_or(crate::error::Error::State)?;\n\tif out.len() \u003c linesize * (height as usize) {\n\t\treturn Err(crate::error::Error::State);\n\t}\n\tfor y in 0..(height as usize) {\n\t\tlet src = \u0026bytes[y * row_bytes..y * row_bytes + row_bytes];\n\t\tlet dst = \u0026mut out[y * linesize..y * linesize + row_bytes];\n\t\tdst.copy_from_slice(src);\n\t}\n\tOk(frame)\n}\n\n// ---------------------------------------------------------------------------\n// Probe\n// ---------------------------------------------------------------------------\n\n/// Probe `filename`, building a [`FootageDescription`].\n///\n/// # CPP-PARITY\n/// `FFmpegDecoder::probe` (ffmpegdecoder.cpp:484). Differences: video\n/// stream details are taken from stream parameters (no second decode pass),\n/// so `is_still` is always false and interlacing always progressive;\n/// subtitle streams are counted but not added.\nfn probe_file(filename: \u0026str, cancelled: Option\u003c\u0026OakCancelAtom\u003e) -\u003e Option\u003cFootageDescription\u003e {\n\tlet mut dict = Dictionary::new();\n\tdict.set(\"analyzeduration\", \"5000000\");\n\tdict.set(\"probesize\", \"20000000\");\n\tlet input = ffmpeg::format::input_with_dictionary(filename, dict).ok()?;\n\n\tlet mut desc = FootageDescription::new(\"ffmpeg\");\n\tlet footage_duration = input.duration();\n\n\tlet file_meta: Vec\u003c(String, String)\u003e = input\n\t\t.metadata()\n\t\t.iter()\n\t\t.map(|(k, v)| (k.to_string(), v.to_string()))\n\t\t.collect();\n\tlet mut source_start_time =\n\t\textract_source_start_time(\u0026file_meta, FfRational(1, FB_TIME_BASE as i32), 0);\n\n\tlet stream_count = input.nb_streams();\n\tfor i in 0..stream_count {\n\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\treturn None;\n\t\t}\n\t\tlet Some(stream) = input.stream(i as usize) else { continue };\n\t\tlet params = stream.parameters();\n\t\tlet medium = params.medium();\n\n\t\tif !source_start_time.valid {\n\t\t\tlet stream_meta: Vec\u003c(String, String)\u003e = stream\n\t\t\t\t.metadata()\n\t\t\t\t.iter()\n\t\t\t\t.map(|(k, v)| (k.to_string(), v.to_string()))\n\t\t\t\t.collect();\n\t\t\tlet raw = unsafe { params.as_ptr() };\n\t\t\tsource_start_time =\n\t\t\t\textract_source_start_time(\u0026stream_meta, stream.time_base(), unsafe { (*raw).sample_rate });\n\t\t}\n\n\t\t// Only proceed if a decoder exists for this stream\n\t\t// (CPP-PARITY ffmpegdecoder.cpp:530).\n\t\tif ffmpeg::decoder::find(params.id()).is_none() {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet raw = unsafe { params.as_ptr() };\n\t\tmatch medium {\n\t\t\tMediaType::Video =\u003e {\n\t\t\t\tlet pixel = pixel_from_raw(unsafe { (*raw).format });\n\t\t\t\tlet native = native_pixel_format(pixel);\n\t\t\t\tlet frame_rate = stream.avg_frame_rate();\n\t\t\t\tlet tb = stream.time_base();\n\n\t\t\t\tlet vp = unsafe { oakcommon_videoparams_init_basic(1, 1) };\n\t\t\t\tunsafe {\n\t\t\t\t\toakcommon_videoparams_set_stream_index(vp.clone(), i as i32);\n\t\t\t\t\toakcommon_videoparams_set_width(vp.clone(), (*raw).width);\n\t\t\t\t\toakcommon_videoparams_set_height(vp.clone(), (*raw).height);\n\t\t\t\t\toakcommon_videoparams_set_video_type(vp.clone(), OAKCOMMON_VIDEO_TYPE_VIDEO);\n\t\t\t\t\toakcommon_videoparams_set_format(vp.clone(), native as i32);\n\t\t\t\t\toakcommon_videoparams_set_channel_count(vp.clone(), VIDEO_CHANNELS);\n\t\t\t\t\toakcommon_videoparams_set_interlacing(vp.clone(), OAKCOMMON_VIDEO_INTERLACE_NONE);\n\t\t\t\t\toakcommon_videoparams_set_pixel_aspect_ratio(vp.clone(), 1, 1);\n\t\t\t\t\toakcommon_videoparams_set_frame_rate(vp.clone(), frame_rate.0 as i64, frame_rate.1 as i64);\n\t\t\t\t\toakcommon_videoparams_set_start_time(vp.clone(), stream.start_time());\n\t\t\t\t\toakcommon_videoparams_set_time_base(vp.clone(), tb.0 as i64, tb.1 as i64);\n\t\t\t\t\toakcommon_videoparams_set_duration(vp.clone(), stream.duration());\n\t\t\t\t\toakcommon_videoparams_set_premultiplied_alpha(vp.clone(), 0);\n\t\t\t\t}\n\t\t\t\tdesc.push_stream(StreamEntry::Video(vp));\n\t\t\t}\n\t\t\tMediaType::Audio =\u003e {\n\t\t\t\tlet mut stream_duration = stream.duration();\n\t\t\t\tif stream_duration == AV_NOPTS_VALUE \u0026\u0026 footage_duration != AV_NOPTS_VALUE {\n\t\t\t\t\t// Fall back to the container duration rescaled into the\n\t\t\t\t\t// stream time base (CPP-PARITY ffmpegdecoder.cpp:621).\n\t\t\t\t\tstream_duration = unsafe {\n\t\t\t\t\t\tsys::av_rescale_q(\n\t\t\t\t\t\t\tfootage_duration,\n\t\t\t\t\t\t\tFfRational(1, FB_TIME_BASE as i32).into(),\n\t\t\t\t\t\t\tstream.time_base().into(),\n\t\t\t\t\t\t)\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tlet sample_rate = unsafe { (*raw).sample_rate };\n\t\t\t\tlet layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();\n\t\t\t\tlet ap = unsafe { oakcore_audioparams_create(sample_rate, layout_mask, 0) };\n\t\t\t\tif !ap.is_null() {\n\t\t\t\t\tlet tb = stream.time_base();\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\toakcore_audioparams_set_stream_index(ap, i as i32);\n\t\t\t\t\t\toakcore_audioparams_set_duration(ap, stream_duration);\n\t\t\t\t\t\toakcore_audioparams_set_time_base(ap, tb.0 as i32, tb.1 as i32);\n\t\t\t\t\t}\n\t\t\t\t\tdesc.push_stream(StreamEntry::Audio(OakAudioParams {\n\t\t\t\t\t\tctx: ap as *mut std::ffi::c_void,\n\t\t\t\t\t\taddref: None,\n\t\t\t\t\t\trelease: None,\n\t\t\t\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ =\u003e {}\n\t\t}\n\t}\n\n\tdesc.set_stream_count(stream_count as usize);\n\tif source_start_time.valid {\n\t\tdesc.set_source_start_time(source_start_time.time, 0);\n\t}\n\tSome(desc)\n}\n\n/// Parsed source start time.\nstruct SourceTime {\n\tvalid: bool,\n\ttime: Rational,\n}\n\n/// Extract a source start time from `timecode` / `time_reference` metadata,\n/// mirroring `extract_source_start_time` (ffmpegdecoder.cpp:176).\nfn extract_source_start_time(\n\tmetadata: \u0026[(String, String)],\n\ttimebase: FfRational,\n\tsample_rate: i32,\n) -\u003e SourceTime {\n\tlet mut out = SourceTime { valid: false, time: Rational::new(0, 1) };\n\tfor (key, value) in metadata {\n\t\tif key == \"timecode\" {\n\t\t\tlet parsed = crate::timecodemetadata::SourceTime::from_timecode_string(\n\t\t\t\tvalue,\n\t\t\t\t\u0026oak_rational(timebase),\n\t\t\t);\n\t\t\tif parsed.valid {\n\t\t\t\tout.valid = true;\n\t\t\t\tout.time = parsed.time;\n\t\t\t\treturn out;\n\t\t\t}\n\t\t} else if key == \"time_reference\" {\n\t\t\tlet parsed = crate::timecodemetadata::SourceTime::from_bwf_time_reference(\n\t\t\t\tvalue,\n\t\t\t\tsample_rate,\n\t\t\t);\n\t\t\tif parsed.valid {\n\t\t\t\tout.valid = true;\n\t\t\t\tout.time = parsed.time;\n\t\t\t\treturn out;\n\t\t\t}\n\t\t}\n\t}\n\tout\n}\n\n// ---------------------------------------------------------------------------\n// FFmpegEncoder\n// ---------------------------------------------------------------------------\n\n/// `olive::FFmpegEncoder` — FFmpeg-backed media encoder.\npub struct FFmpegEncoder {\n\t/// The encoding parameters this encoder was configured with.\n\tpub params: EncodingParams,\n\t/// Encoder session state.\n\tstate: Mutex\u003cEncoderState\u003e,\n}\n\n/// Encoder session state.\nstruct EncoderState {\n\t/// Parameters from [`Encoder::configure`] (overrides `params`).\n\tconfigured: Option\u003cEncodingParams\u003e,\n\t/// The open output, if any.\n\toutput: Option\u003cOutputState\u003e,\n\t/// Last error detail.\n\tlast_error: String,\n}\n\n/// One opened output file.\nstruct OutputState {\n\toutput: ffmpeg::format::context::Output,\n\tvideo: Option\u003cVideoEncoderState\u003e,\n\taudio: Option\u003cAudioEncoderState\u003e,\n\t/// Audio presentation timestamp in 1/sample_rate time base units.\n\taudio_pts: i64,\n\tflushed: bool,\n}\n\n// SAFETY: `EncoderState` owns raw FFmpeg contexts (output + encoders) that\n// are not thread-safe themselves, but every access goes through the\n// `FFmpegEncoder::state` `Mutex`, so concurrent access never happens.\nunsafe impl Send for EncoderState {}\nunsafe impl Sync for EncoderState {}\n\n/// Opened video encoder + its conversion scaler.\nstruct VideoEncoderState {\n\tencoder: ffmpeg::codec::encoder::video::Encoder,\n\tstream_index: usize,\n\t/// swscale from the incoming F32-RGBA bytes to the encoder pixel format.\n\tscaler: scaling::Context,\n\twidth: u32,\n\theight: u32,\n}\n\n/// Opened audio encoder + its conversion resampler.\nstruct AudioEncoderState {\n\tencoder: ffmpeg::codec::encoder::audio::Encoder,\n\tstream_index: usize,\n\t/// swr from interleaved f32 to the encoder sample format.\n\tresampler: AudioResampler,\n}\n\nimpl FFmpegEncoder {\n\t/// New encoder configured with `params`.\n\tpub(crate) fn with_params(params: EncodingParams) -\u003e Self {\n\t\tFFmpegEncoder {\n\t\t\tparams,\n\t\t\tstate: Mutex::new(EncoderState {\n\t\t\t\tconfigured: None,\n\t\t\t\toutput: None,\n\t\t\t\tlast_error: String::new(),\n\t\t\t}),\n\t\t}\n\t}\n\n\t/// The effective encoding parameters (configured overrides construction).\n\tfn effective_params(\u0026self) -\u003e EncodingParams {\n\t\tlet state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.configured.clone().unwrap_or_else(|| self.params.clone())\n\t}\n}\n\nimpl Encoder for FFmpegEncoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"ffmpeg\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_image_sequences(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn is_configurable(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn configure(\u0026self, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif !is_ffmpeg_format(params.format) {\n\t\t\treturn Err(fail(format!(\"unknown export format {}\", params.format)));\n\t\t}\n\t\t// `configure` cannot write the public `params` field through `\u0026self`;\n\t\t// the configured copy is stored in the session state and used by\n\t\t// `open` (CPP-PARITY encoder.cpp `configure` stores the params).\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.configured = Some(params.clone());\n\t\tstate.last_error.clear();\n\t\tOk(())\n\t}\n\n\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = self.effective_params();\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.open(\u0026params)\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// `Encoder::close` is documented idempotent; flush if needed.\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.close()\n\t}\n\n\tfn write_video(\u0026self, frame: \u0026Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = self.effective_params();\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.write_video(frame, \u0026params)\n\t}\n\n\tfn write_audio(\u0026self, samples: \u0026[f32], frame_count: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = self.effective_params();\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.write_audio(samples, frame_count, \u0026params)\n\t}\n\n\tfn write_subtitle(\n\t\t\u0026self,\n\t\t_text: \u0026str,\n\t\t_in_seconds: f64,\n\t\t_out_seconds: f64,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// Subtitle muxing is not exposed by the crate's encoder trait flow\n\t\t// (the C++ writes through the bridge's SRT encoder); report the same\n\t\t// unsupported state the stub did.\n\t\tErr(fail(\"subtitle encoding is not supported by the ffmpeg encoder\"))\n\t}\n\n\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.close()\n\t}\n\n\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e {\n\t\t// The encoder accepts (and converts) F32 RGBA frames.\n\t\tSome(PixelFormat::F32)\n\t}\n\n\tfn desired_sample_format(\u0026self) -\u003e Option\u003cSampleFormat\u003e {\n\t\t// The encoder accepts interleaved f32 audio.\n\t\tSome(SampleFormat::F32)\n\t}\n\n\tfn filename(\u0026self) -\u003e String {\n\t\tc_string_1024(\u0026self.params.filename)\n\t}\n\n\tfn get_error(\u0026self) -\u003e String {\n\t\tself.state\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.last_error\n\t\t\t.clone()\n\t}\n}\n\nimpl EncoderState {\n\t/// Open the output file, create the streams and encoders and write the\n\t/// header.\n\tfn open(\u0026mut self, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif self.output.is_some() {\n\t\t\treturn Ok(());\n\t\t}\n\t\tlet filename = c_string_1024(\u0026params.filename);\n\t\tif filename.is_empty() {\n\t\t\treturn Err(fail(\"no output filename\"));\n\t\t}\n\t\tif params.video_enabled == 0 \u0026\u0026 params.audio_enabled == 0 {\n\t\t\treturn Err(fail(\"no output tracks enabled\"));\n\t\t}\n\n\t\tlet mut output = ffmpeg::format::output(\u0026filename)\n\t\t\t.map_err(|e| fail(format!(\"failed to create output '{filename}': {e}\")))?;\n\n\t\tlet mut video = None;\n\t\tif params.video_enabled != 0 {\n\t\t\tlet codec_id = export_codec_to_id(params.video_codec)\n\t\t\t\t.ok_or_else(|| fail(format!(\"unknown video codec {}\", params.video_codec)))?;\n\t\t\tlet codec = ffmpeg::encoder::find(codec_id)\n\t\t\t\t.ok_or_else(|| fail(format!(\"no encoder for codec {:?}\", codec_id)))?;\n\n\t\t\tlet width = params.video_width.max(1) as u32;\n\t\t\tlet height = params.video_height.max(1) as u32;\n\t\t\tlet time_base = FfRational(params.video_time_base_num, params.video_time_base_den);\n\t\t\tlet frame_rate = FfRational(params.video_time_base_den, params.video_time_base_num.max(1));\n\n\t\t\tlet mut stream = output.add_stream(codec).map_err(ffmpeg_err)?;\n\t\t\tlet stream_index = stream.index();\n\n\t\t\tlet mut encoder = ffmpeg::codec::Context::new_with_codec(codec)\n\t\t\t\t.encoder()\n\t\t\t\t.video()\n\t\t\t\t.map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026encoder);\n\t\t\tstream.set_time_base(time_base);\n\n\t\t\tencoder.set_width(width);\n\t\t\tencoder.set_height(height);\n\t\t\tencoder.set_aspect_ratio(FfRational(\n\t\t\t\tparams.video_pixel_aspect_num.max(1),\n\t\t\t\tparams.video_pixel_aspect_den.max(1),\n\t\t\t));\n\t\t\tencoder.set_frame_rate(Some(frame_rate));\n\t\t\tencoder.set_time_base(time_base);\n\t\t\tif params.video_bit_rate \u003e 0 {\n\t\t\t\tencoder.set_bit_rate(params.video_bit_rate as usize);\n\t\t\t}\n\t\t\tif params.video_max_bit_rate \u003e 0 {\n\t\t\t\tencoder.set_max_bit_rate(params.video_max_bit_rate as usize);\n\t\t\t}\n\n\t\t\t// Choose the target pixel format: explicit `video_pix_fmt`, else\n\t\t\t// a codec-appropriate default.\n\t\t\tlet pix_fmt = pixel_format_from_name(\u0026params.video_pix_fmt)\n\t\t\t\t.unwrap_or_else(|| default_pixel_format_for_codec(codec_id));\n\t\t\tencoder.set_format(pix_fmt);\n\n\t\t\tlet opened = encoder.open().map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026opened);\n\n\t\t\tlet scaler = scaling::Context::get(\n\t\t\t\tPixel::RGBA,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tpix_fmt,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tscaling::Flags::BILINEAR,\n\t\t\t)\n\t\t\t.map_err(ffmpeg_err)?;\n\n\t\t\tvideo = Some(VideoEncoderState {\n\t\t\t\tencoder: opened,\n\t\t\t\tstream_index,\n\t\t\t\tscaler,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t});\n\t\t}\n\n\t\tlet mut audio = None;\n\t\tif params.audio_enabled != 0 {\n\t\t\tlet codec_id = export_codec_to_id(params.audio_codec)\n\t\t\t\t.ok_or_else(|| fail(format!(\"unknown audio codec {}\", params.audio_codec)))?;\n\t\t\tlet codec = ffmpeg::encoder::find(codec_id)\n\t\t\t\t.ok_or_else(|| fail(format!(\"no encoder for codec {:?}\", codec_id)))?;\n\n\t\t\tlet sample_rate = params.audio_sample_rate.max(1) as u32;\n\t\t\tlet layout = channel_layout_from_mask(params.audio_channel_layout);\n\n\t\t\tlet mut stream = output.add_stream(codec).map_err(ffmpeg_err)?;\n\t\t\tlet stream_index = stream.index();\n\t\t\tstream.set_time_base(FfRational(1, sample_rate as i32));\n\n\t\t\tlet mut encoder = ffmpeg::codec::Context::new_with_codec(codec)\n\t\t\t\t.encoder()\n\t\t\t\t.audio()\n\t\t\t\t.map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026encoder);\n\n\t\t\tencoder.set_rate(sample_rate as i32);\n\t\t\tencoder.set_channel_layout(layout);\n\t\t\tif params.audio_bit_rate \u003e 0 {\n\t\t\t\tencoder.set_bit_rate(params.audio_bit_rate as usize);\n\t\t\t}\n\t\t\tlet sample_fmt = sample_format_to_ffmpeg(params.audio_sample_format)\n\t\t\t\t.unwrap_or_else(|| default_sample_format_for_codec(codec_id));\n\t\t\tencoder.set_format(sample_fmt);\n\n\t\t\tlet opened = encoder.open().map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026opened);\n\n\t\t\tlet resampler = AudioResampler::get(\n\t\t\t\tSample::F32(SampleType::Packed),\n\t\t\t\tlayout,\n\t\t\t\tsample_rate,\n\t\t\t\tsample_fmt,\n\t\t\t\tlayout,\n\t\t\t\tsample_rate,\n\t\t\t)?;\n\n\t\t\taudio = Some(AudioEncoderState {\n\t\t\t\tencoder: opened,\n\t\t\t\tstream_index,\n\t\t\t\tresampler,\n\t\t\t});\n\t\t}\n\n\t\toutput.write_header().map_err(ffmpeg_err)?;\n\n\t\tself.output = Some(OutputState {\n\t\t\toutput,\n\t\t\tvideo,\n\t\t\taudio,\n\t\t\taudio_pts: 0,\n\t\t\tflushed: false,\n\t\t});\n\t\tOk(())\n\t}\n\n\t/// Encode one frame (F32 RGBA) into the open output.\n\tfn write_video(\u0026mut self, frame: \u0026Frame, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet output = self\n\t\t\t.output\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder is not open\"))?;\n\t\tif output.flushed {\n\t\t\treturn Err(fail(\"encoder is already flushed\"));\n\t\t}\n\t\tlet video = output\n\t\t\t.video\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder has no video track\"))?;\n\n\t\tlet data = frame.data().ok_or(crate::error::Error::State)?;\n\t\tlet (w, h) = (frame.width() as u32, frame.height() as u32);\n\t\tif w != video.width || h != video.height {\n\t\t\treturn Err(fail(format!(\n\t\t\t\t\"frame size {w}x{h} does not match the encoder size {}x{}\",\n\t\t\t\tvideo.width, video.height\n\t\t\t)));\n\t\t}\n\n\t\t// Convert the F32 RGBA buffer to 8-bit RGBA (swscale cannot take\n\t\t// float input on every build) and scale into the encoder format.\n\t\tlet mut rgba = vec![0u8; (w as usize) * (h as usize) * 4];\n\t\tlet linesize = frame.linesize_bytes() as usize;\n\t\tfor y in 0..(h as usize) {\n\t\t\tlet row = \u0026data[y * linesize..y * linesize + (w as usize) * PIXEL_F32_BYTES];\n\t\t\tlet dst = \u0026mut rgba[y * (w as usize) * 4..(y + 1) * (w as usize) * 4];\n\t\t\tfor (out_px, in_px) in dst.chunks_exact_mut(4).zip(row.chunks_exact(PIXEL_F32_BYTES)) {\n\t\t\t\tfor c in 0..4 {\n\t\t\t\t\tlet v = f32::from_le_bytes([\n\t\t\t\t\t\tin_px[c * 4],\n\t\t\t\t\t\tin_px[c * 4 + 1],\n\t\t\t\t\t\tin_px[c * 4 + 2],\n\t\t\t\t\t\tin_px[c * 4 + 3],\n\t\t\t\t\t]);\n\t\t\t\t\tout_px[c] = (v * 255.0).clamp(0.0, 1.0) as u8;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet mut src = ffmpeg::frame::Video::new(Pixel::RGBA, w, h);\n\t\tsrc.data_mut(0).copy_from_slice(\u0026rgba);\n\n\t\tlet mut scaled = ffmpeg::frame::Video::empty();\n\t\tvideo.scaler.run(\u0026src, \u0026mut scaled).map_err(ffmpeg_err)?;\n\n\t\t// # CPP-PARITY\n\t\t// `FFmpegEncoder::write_frame` passes the frame time in seconds; the\n\t\t// Rust `Frame` carries the timestamp as a rational.\n\t\tlet secs = frame.timestamp().to_f64();\n\t\tlet tb = FfRational(params.video_time_base_num.max(1), params.video_time_base_den.max(1));\n\t\tlet pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64;\n\t\tscaled.set_pts(Some(pts));\n\n\t\tvideo.encoder.send_frame(\u0026scaled).map_err(ffmpeg_err)?;\n\t\tdrain_video_packets(\u0026mut output.output, video)\n\t}\n\n\t/// Encode interleaved f32 audio into the open output.\n\tfn write_audio(\n\t\t\u0026mut self,\n\t\tsamples: \u0026[f32],\n\t\t_frame_count: i32,\n\t\tparams: \u0026EncodingParams,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet output = self\n\t\t\t.output\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder is not open\"))?;\n\t\tif output.flushed {\n\t\t\treturn Err(fail(\"encoder is already flushed\"));\n\t\t}\n\t\tlet audio = output\n\t\t\t.audio\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder has no audio track\"))?;\n\n\t\tif samples.is_empty() {\n\t\t\treturn Ok(());\n\t\t}\n\t\tlet channels = audio.resampler.dst_channels.max(1);\n\t\tlet in_frames = samples.len() / channels;\n\n\t\t// Presentation timestamp in the output stream time base (1/sample_rate).\n\t\tlet pts = output.audio_pts;\n\n\t\tlet layout = channel_layout_from_mask(params.audio_channel_layout);\n\t\tlet mut input = ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), in_frames, layout);\n\t\tlet bytes = unsafe {\n\t\t\tstd::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 4)\n\t\t};\n\t\tinput.data_mut(0)[..bytes.len()].copy_from_slice(bytes);\n\n\t\tlet mut converted = audio.resampler.convert_to_frame(\u0026input)?;\n\t\tconverted.set_pts(Some(pts));\n\t\toutput.audio_pts += converted.samples() as i64;\n\n\t\taudio.encoder.send_frame(\u0026converted).map_err(ffmpeg_err)?;\n\t\tdrain_audio_packets(\u0026mut output.output, audio)\n\t}\n\n\t/// Flush encoders, write the trailer and close the output (idempotent).\n\tfn close(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet Some(output) = self.output.as_mut() else {\n\t\t\treturn Ok(());\n\t\t};\n\t\tif !output.flushed {\n\t\t\tif let Some(v) = output.video.as_mut() {\n\t\t\t\tdrain_video_encoder(\u0026mut output.output, v)?;\n\t\t\t}\n\t\t\tif let Some(a) = output.audio.as_mut() {\n\t\t\t\tdrain_audio_encoder(\u0026mut output.output, a)?;\n\t\t\t}\n\t\t\toutput.output.write_trailer().map_err(ffmpeg_err)?;\n\t\t\toutput.flushed = true;\n\t\t}\n\t\t// Drop the output (closes the file).\n\t\tself.output = None;\n\t\tOk(())\n\t}\n}\n\n/// Drain a video encoder after EOF, writing any packets.\nfn drain_video_encoder(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\tvideo: \u0026mut VideoEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tvideo.encoder.send_eof().map_err(ffmpeg_err)?;\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch video.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(video.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Drain an audio encoder after EOF, writing any packets.\nfn drain_audio_encoder(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\taudio: \u0026mut AudioEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\taudio.encoder.send_eof().map_err(ffmpeg_err)?;\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch audio.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(audio.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Drain ready packets from a video encoder into the muxer.\nfn drain_video_packets(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\tvideo: \u0026mut VideoEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch video.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(video.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Drain ready packets from an audio encoder into the muxer.\nfn drain_audio_packets(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\taudio: \u0026mut AudioEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch audio.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(audio.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Map an `ExportCodec::Codec` raw value to an FFmpeg codec id\n/// (CPP-PARITY `FFmpegEncoder::export_codec_to_bridge`). Values are the\n/// documented `ExportCodec::Codec` discriminants (see `exportcodec.rs`).\nfn export_codec_to_id(codec: i32) -\u003e Option\u003cffmpeg::codec::Id\u003e {\n\tmatch codec {\n\t\t0 =\u003e Some(ffmpeg::codec::Id::DNXHD), // DNxHD\n\t\t1 | 2 =\u003e Some(ffmpeg::codec::Id::H264), // H264 / H264 RGB\n\t\t3 =\u003e Some(ffmpeg::codec::Id::HEVC), // H265\n\t\t6 =\u003e Some(ffmpeg::codec::Id::PRORES), // ProRes\n\t\t7 =\u003e Some(ffmpeg::codec::Id::CFHD), // CineForm\n\t\t10 =\u003e Some(ffmpeg::codec::Id::MPEG2VIDEO), // MP2\n\t\t11 =\u003e Some(ffmpeg::codec::Id::MP3), // MP3\n\t\t12 =\u003e Some(ffmpeg::codec::Id::AAC), // AAC\n\t\t13 =\u003e Some(ffmpeg::codec::Id::PCM_S16LE), // PCM\n\t\t14 =\u003e Some(ffmpeg::codec::Id::OPUS), // Opus\n\t\t15 =\u003e Some(ffmpeg::codec::Id::VORBIS), // Vorbis\n\t\t16 =\u003e Some(ffmpeg::codec::Id::FLAC), // FLAC\n\t\t17 =\u003e Some(ffmpeg::codec::Id::SUBRIP), // SRT\n\t\t18 =\u003e Some(ffmpeg::codec::Id::AV1), // AV1\n\t\t_ =\u003e None,\n\t}\n}\n\n/// Whether `format` is a container the FFmpeg encoder handles\n/// (CPP-PARITY `Encoder::create_from_params` format mapping).\nfn is_ffmpeg_format(format: i32) -\u003e bool {\n\tmatches!(format, 0 | 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14)\n}\n\n/// Default encoder pixel format for a codec.\nfn default_pixel_format_for_codec(codec: ffmpeg::codec::Id) -\u003e Pixel {\n\tmatch codec {\n\t\tffmpeg::codec::Id::PRORES | ffmpeg::codec::Id::DNXHD =\u003e Pixel::YUV422P10LE,\n\t\t_ =\u003e Pixel::YUV420P,\n\t}\n}\n\n/// Default encoder sample format for a codec.\nfn default_sample_format_for_codec(codec: ffmpeg::codec::Id) -\u003e Sample {\n\tmatch codec {\n\t\tffmpeg::codec::Id::PCM_S16LE =\u003e Sample::I16(SampleType::Packed),\n\t\tffmpeg::codec::Id::FLAC =\u003e Sample::I16(SampleType::Planar),\n\t\tffmpeg::codec::Id::MP3 =\u003e Sample::F32(SampleType::Packed),\n\t\t_ =\u003e Sample::F32(SampleType::Planar),\n\t}\n}\n\n/// Parse a NUL-terminated pixel-format name (e.g. \"yuv420p\").\nfn pixel_format_from_name(buf: \u0026[u8; 64]) -\u003e Option\u003cPixel\u003e {\n\tlet end = buf.iter().position(|\u0026b| b == 0).unwrap_or(buf.len());\n\tlet name = std::str::from_utf8(\u0026buf[..end]).ok()?;\n\tif name.is_empty() {\n\t\treturn None;\n\t}\n\tname.parse::\u003cPixel\u003e().ok()\n}\n\n/// Map an oakcore sample format to an FFmpeg sample format.\nfn sample_format_to_ffmpeg(f: SampleFormat) -\u003e Option\u003cSample\u003e {\n\tmatch f {\n\t\tSampleFormat::U8Planar =\u003e Some(Sample::U8(SampleType::Planar)),\n\t\tSampleFormat::S16Planar =\u003e Some(Sample::I16(SampleType::Planar)),\n\t\tSampleFormat::S32Planar =\u003e Some(Sample::I32(SampleType::Planar)),\n\t\tSampleFormat::S64Planar =\u003e Some(Sample::I64(SampleType::Planar)),\n\t\tSampleFormat::F32Planar =\u003e Some(Sample::F32(SampleType::Planar)),\n\t\tSampleFormat::F64Planar =\u003e Some(Sample::F64(SampleType::Planar)),\n\t\tSampleFormat::U8 =\u003e Some(Sample::U8(SampleType::Packed)),\n\t\tSampleFormat::S16 =\u003e Some(Sample::I16(SampleType::Packed)),\n\t\tSampleFormat::S32 =\u003e Some(Sample::I32(SampleType::Packed)),\n\t\tSampleFormat::S64 =\u003e Some(Sample::I64(SampleType::Packed)),\n\t\tSampleFormat::F32 =\u003e Some(Sample::F32(SampleType::Packed)),\n\t\tSampleFormat::F64 =\u003e Some(Sample::F64(SampleType::Packed)),\n\t\tSampleFormat::Invalid =\u003e None,\n\t}\n}\n\n/// Read a NUL-terminated C string out of the `EncodingParams.filename` byte\n/// buffer, stopping at the first NUL (empty string when unset).\nfn c_string_1024(buf: \u0026[u8; 1024]) -\u003e String {\n\tlet end = buf.iter().position(|\u0026b| b == 0).unwrap_or(buf.len());\n\tString::from_utf8_lossy(\u0026buf[..end]).into_owned()\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::decoder::RetrieveVideoParams;\n\tuse oakcore_rs::Rational;\n\n\tfn video_params() -\u003e RetrieveVideoParams {\n\t\tRetrieveVideoParams {\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(1, 30),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: crate::decoder::K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: crate::decoder::RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t}\n\t}\n\n\t#[test]\n\tfn ffmpeg_decoder_identity_and_capabilities() {\n\t\tlet d = FFmpegDecoder::new();\n\t\tassert_eq!(d.id(), \"ffmpeg\");\n\t\tassert!(d.supports_video());\n\t\tassert!(d.supports_audio());\n\t\tassert!(d.probe(\"any.mp4\", None).is_none());\n\t}\n\n\t#[test]\n\tfn ffmpeg_decoder_closed_state_errors() {\n\t\tlet d = FFmpegDecoder::new();\n\t\t// Open on a missing file fails and leaves the decoder closed\n\t\t// (C++ parity: the stream is unset on failure).\n\t\tlet s = CodecStream::with_block(\"in.mp4\".to_string(), 0, None);\n\t\tassert!(d.open(\u0026s).is_err());\n\t\tassert_eq!(d.stream().filename(), \"\");\n\t\tassert!(d.close().is_ok());\n\n\t\t// All media operations on a closed decoder fail.\n\t\tassert!(d.retrieve_video_frame(\u0026video_params()).is_err());\n\t\tassert!(d.retrieve_video(\u0026video_params()).is_err());\n\t\tlet mut dest = [0f32; 8];\n\t\tassert!(d\n\t\t\t.retrieve_audio(\n\t\t\t\t\u0026mut dest,\n\t\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t\t48000,\n\t\t\t\t0x3\n\t\t\t)\n\t\t\t.is_err());\n\t\tassert!(d\n\t\t\t.conform_audio(\u0026[\"a.pcm\".to_string()], 48000, 0x3, 10, None)\n\t\t\t.is_err());\n\t\tassert_eq!(d.get_audio_start_offset(), Rational::new(0, 1));\n\t}\n\n\t#[test]\n\tfn ffmpeg_encoder_identity_and_config_validation() {\n\t\tlet mut params = EncodingParams::default();\n\t\tlet name = b\"out/ffmpeg.mp4\";\n\t\tparams.filename[..name.len()].copy_from_slice(name);\n\t\tparams.format = 2; // mp4\n\t\tparams.video_enabled = 1;\n\t\tparams.video_codec = 1; // H264\n\t\tparams.video_width = 64;\n\t\tparams.video_height = 64;\n\t\tparams.video_time_base_num = 1;\n\t\tparams.video_time_base_den = 10;\n\t\tlet e = FFmpegEncoder::with_params(params);\n\n\t\tassert_eq!(e.id(), \"ffmpeg\");\n\t\tassert!(e.supports_video());\n\t\tassert!(e.supports_audio());\n\t\tassert!(e.supports_subtitles());\n\t\tassert!(!e.supports_image_sequences());\n\t\tassert!(e.is_configurable());\n\t\tassert_eq!(e.filename(), \"out/ffmpeg.mp4\");\n\t\tassert_eq!(e.desired_pixel_format(), Some(PixelFormat::F32));\n\t\tassert_eq!(e.desired_sample_format(), Some(SampleFormat::F32));\n\t\tassert_eq!(e.get_error(), \"\");\n\n\t\t// configure rejects unknown export formats.\n\t\tlet mut bad = EncodingParams::default();\n\t\tbad.format = 99;\n\t\tassert!(e.configure(\u0026bad).is_err());\n\n\t\t// A valid configures succeeds but nothing is open yet.\n\t\tlet mut good = EncodingParams::default();\n\t\tgood.format = 2;\n\t\tgood.video_enabled = 1;\n\t\tgood.video_codec = 1;\n\t\tgood.video_width = 64;\n\t\tgood.video_height = 64;\n\t\tgood.video_time_base_num = 1;\n\t\tgood.video_time_base_den = 10;\n\t\tassert!(e.configure(\u0026good).is_ok());\n\n\t\t// Operations before open fail cleanly; close/flush are safe.\n\t\tlet frame = Frame::new();\n\t\tassert!(e.write_video(\u0026frame).is_err());\n\t\tassert!(e.write_audio(\u0026[0f32; 4], 1).is_err());\n\t\tassert!(e.write_subtitle(\"hi\", 0.0, 1.0).is_err());\n\t\tassert!(e.close().is_ok());\n\t\tassert!(e.flush().is_ok());\n\t}\n\n\t#[test]\n\tfn ffmpeg_encoder_open_rejects_invalid_config() {\n\t\t// No filename.\n\t\tlet e = FFmpegEncoder::with_params(EncodingParams::default());\n\t\tassert!(e.open().is_err());\n\n\t\t// No enabled tracks.\n\t\tlet mut p = EncodingParams::default();\n\t\tlet name = b\"out/x.mp4\";\n\t\tp.filename[..name.len()].copy_from_slice(name);\n\t\tp.format = 2;\n\t\tlet e = FFmpegEncoder::with_params(p);\n\t\tassert!(e.open().is_err());\n\t}\n}\n","traces":[{"line":95,"address":[],"length":0,"stats":{"Line":24}},{"line":97,"address":[],"length":0,"stats":{"Line":49}},{"line":98,"address":[],"length":0,"stats":{"Line":2}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":102,"address":[],"length":0,"stats":{"Line":24}},{"line":106,"address":[],"length":0,"stats":{"Line":3}},{"line":107,"address":[],"length":0,"stats":{"Line":3}},{"line":111,"address":[],"length":0,"stats":{"Line":6}},{"line":112,"address":[],"length":0,"stats":{"Line":6}},{"line":120,"address":[],"length":0,"stats":{"Line":1026}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":1026}},{"line":130,"address":[],"length":0,"stats":{"Line":11}},{"line":131,"address":[],"length":0,"stats":{"Line":21}},{"line":132,"address":[],"length":0,"stats":{"Line":50}},{"line":137,"address":[],"length":0,"stats":{"Line":3}},{"line":138,"address":[],"length":0,"stats":{"Line":3}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":141,"address":[],"length":0,"stats":{"Line":0}},{"line":142,"address":[],"length":0,"stats":{"Line":0}},{"line":143,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":6}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":2}},{"line":182,"address":[],"length":0,"stats":{"Line":4}},{"line":183,"address":[],"length":0,"stats":{"Line":4}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":8}},{"line":187,"address":[],"length":0,"stats":{"Line":4}},{"line":188,"address":[],"length":0,"stats":{"Line":8}},{"line":189,"address":[],"length":0,"stats":{"Line":8}},{"line":190,"address":[],"length":0,"stats":{"Line":4}},{"line":191,"address":[],"length":0,"stats":{"Line":4}},{"line":200,"address":[],"length":0,"stats":{"Line":2}},{"line":203,"address":[],"length":0,"stats":{"Line":6}},{"line":210,"address":[],"length":0,"stats":{"Line":2}},{"line":212,"address":[],"length":0,"stats":{"Line":6}},{"line":216,"address":[],"length":0,"stats":{"Line":52}},{"line":217,"address":[],"length":0,"stats":{"Line":156}},{"line":234,"address":[],"length":0,"stats":{"Line":17}},{"line":236,"address":[],"length":0,"stats":{"Line":51}},{"line":237,"address":[],"length":0,"stats":{"Line":17}},{"line":243,"address":[],"length":0,"stats":{"Line":0}},{"line":244,"address":[],"length":0,"stats":{"Line":0}},{"line":249,"address":[],"length":0,"stats":{"Line":8}},{"line":250,"address":[],"length":0,"stats":{"Line":16}},{"line":253,"address":[],"length":0,"stats":{"Line":2}},{"line":254,"address":[],"length":0,"stats":{"Line":2}},{"line":257,"address":[],"length":0,"stats":{"Line":2}},{"line":258,"address":[],"length":0,"stats":{"Line":2}},{"line":261,"address":[],"length":0,"stats":{"Line":4}},{"line":262,"address":[],"length":0,"stats":{"Line":8}},{"line":263,"address":[],"length":0,"stats":{"Line":12}},{"line":266,"address":[],"length":0,"stats":{"Line":7}},{"line":267,"address":[],"length":0,"stats":{"Line":7}},{"line":273,"address":[],"length":0,"stats":{"Line":28}},{"line":274,"address":[],"length":0,"stats":{"Line":7}},{"line":275,"address":[],"length":0,"stats":{"Line":0}},{"line":276,"address":[],"length":0,"stats":{"Line":0}},{"line":278,"address":[],"length":0,"stats":{"Line":0}},{"line":280,"address":[],"length":0,"stats":{"Line":7}},{"line":281,"address":[],"length":0,"stats":{"Line":0}},{"line":283,"address":[],"length":0,"stats":{"Line":7}},{"line":284,"address":[],"length":0,"stats":{"Line":2}},{"line":287,"address":[],"length":0,"stats":{"Line":15}},{"line":288,"address":[],"length":0,"stats":{"Line":20}},{"line":289,"address":[],"length":0,"stats":{"Line":10}},{"line":290,"address":[],"length":0,"stats":{"Line":5}},{"line":293,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":8}},{"line":296,"address":[],"length":0,"stats":{"Line":8}},{"line":297,"address":[],"length":0,"stats":{"Line":2}},{"line":300,"address":[],"length":0,"stats":{"Line":2}},{"line":301,"address":[],"length":0,"stats":{"Line":2}},{"line":303,"address":[],"length":0,"stats":{"Line":2}},{"line":305,"address":[],"length":0,"stats":{"Line":2}},{"line":308,"address":[],"length":0,"stats":{"Line":5}},{"line":309,"address":[],"length":0,"stats":{"Line":5}},{"line":310,"address":[],"length":0,"stats":{"Line":20}},{"line":311,"address":[],"length":0,"stats":{"Line":20}},{"line":312,"address":[],"length":0,"stats":{"Line":3}},{"line":313,"address":[],"length":0,"stats":{"Line":0}},{"line":322,"address":[],"length":0,"stats":{"Line":6}},{"line":323,"address":[],"length":0,"stats":{"Line":12}},{"line":324,"address":[],"length":0,"stats":{"Line":3}},{"line":326,"address":[],"length":0,"stats":{"Line":21}},{"line":327,"address":[],"length":0,"stats":{"Line":18}},{"line":328,"address":[],"length":0,"stats":{"Line":3}},{"line":331,"address":[],"length":0,"stats":{"Line":2}},{"line":335,"address":[],"length":0,"stats":{"Line":2}},{"line":336,"address":[],"length":0,"stats":{"Line":8}},{"line":337,"address":[],"length":0,"stats":{"Line":8}},{"line":338,"address":[],"length":0,"stats":{"Line":0}},{"line":339,"address":[],"length":0,"stats":{"Line":0}},{"line":341,"address":[],"length":0,"stats":{"Line":0}},{"line":342,"address":[],"length":0,"stats":{"Line":0}},{"line":343,"address":[],"length":0,"stats":{"Line":0}},{"line":344,"address":[],"length":0,"stats":{"Line":0}},{"line":345,"address":[],"length":0,"stats":{"Line":0}},{"line":346,"address":[],"length":0,"stats":{"Line":0}},{"line":350,"address":[],"length":0,"stats":{"Line":3}},{"line":357,"address":[],"length":0,"stats":{"Line":3}},{"line":358,"address":[],"length":0,"stats":{"Line":12}},{"line":359,"address":[],"length":0,"stats":{"Line":12}},{"line":360,"address":[],"length":0,"stats":{"Line":1}},{"line":362,"address":[],"length":0,"stats":{"Line":0}},{"line":364,"address":[],"length":0,"stats":{"Line":2}},{"line":365,"address":[],"length":0,"stats":{"Line":0}},{"line":367,"address":[],"length":0,"stats":{"Line":6}},{"line":370,"address":[],"length":0,"stats":{"Line":3}},{"line":378,"address":[],"length":0,"stats":{"Line":3}},{"line":379,"address":[],"length":0,"stats":{"Line":12}},{"line":380,"address":[],"length":0,"stats":{"Line":12}},{"line":381,"address":[],"length":0,"stats":{"Line":1}},{"line":382,"address":[],"length":0,"stats":{"Line":0}},{"line":384,"address":[],"length":0,"stats":{"Line":7}},{"line":387,"address":[],"length":0,"stats":{"Line":2}},{"line":391,"address":[],"length":0,"stats":{"Line":8}},{"line":392,"address":[],"length":0,"stats":{"Line":4}},{"line":393,"address":[],"length":0,"stats":{"Line":0}},{"line":394,"address":[],"length":0,"stats":{"Line":0}},{"line":395,"address":[],"length":0,"stats":{"Line":0}},{"line":396,"address":[],"length":0,"stats":{"Line":0}},{"line":398,"address":[],"length":0,"stats":{"Line":2}},{"line":489,"address":[],"length":0,"stats":{"Line":5}},{"line":490,"address":[],"length":0,"stats":{"Line":10}},{"line":491,"address":[],"length":0,"stats":{"Line":15}},{"line":492,"address":[],"length":0,"stats":{"Line":15}},{"line":493,"address":[],"length":0,"stats":{"Line":20}},{"line":494,"address":[],"length":0,"stats":{"Line":5}},{"line":496,"address":[],"length":0,"stats":{"Line":10}},{"line":497,"address":[],"length":0,"stats":{"Line":15}},{"line":498,"address":[],"length":0,"stats":{"Line":5}},{"line":499,"address":[],"length":0,"stats":{"Line":5}},{"line":500,"address":[],"length":0,"stats":{"Line":15}},{"line":501,"address":[],"length":0,"stats":{"Line":15}},{"line":502,"address":[],"length":0,"stats":{"Line":15}},{"line":503,"address":[],"length":0,"stats":{"Line":15}},{"line":504,"address":[],"length":0,"stats":{"Line":5}},{"line":508,"address":[],"length":0,"stats":{"Line":10}},{"line":509,"address":[],"length":0,"stats":{"Line":10}},{"line":510,"address":[],"length":0,"stats":{"Line":10}},{"line":511,"address":[],"length":0,"stats":{"Line":10}},{"line":512,"address":[],"length":0,"stats":{"Line":8}},{"line":513,"address":[],"length":0,"stats":{"Line":4}},{"line":514,"address":[],"length":0,"stats":{"Line":4}},{"line":515,"address":[],"length":0,"stats":{"Line":2}},{"line":516,"address":[],"length":0,"stats":{"Line":2}},{"line":519,"address":[],"length":0,"stats":{"Line":10}},{"line":520,"address":[],"length":0,"stats":{"Line":15}},{"line":521,"address":[],"length":0,"stats":{"Line":15}},{"line":522,"address":[],"length":0,"stats":{"Line":5}},{"line":524,"address":[],"length":0,"stats":{"Line":15}},{"line":525,"address":[],"length":0,"stats":{"Line":5}},{"line":527,"address":[],"length":0,"stats":{"Line":10}},{"line":528,"address":[],"length":0,"stats":{"Line":3}},{"line":529,"address":[],"length":0,"stats":{"Line":2}},{"line":530,"address":[],"length":0,"stats":{"Line":0}},{"line":531,"address":[],"length":0,"stats":{"Line":0}},{"line":532,"address":[],"length":0,"stats":{"Line":0}},{"line":533,"address":[],"length":0,"stats":{"Line":0}},{"line":534,"address":[],"length":0,"stats":{"Line":0}},{"line":539,"address":[],"length":0,"stats":{"Line":15}},{"line":540,"address":[],"length":0,"stats":{"Line":15}},{"line":541,"address":[],"length":0,"stats":{"Line":10}},{"line":544,"address":[],"length":0,"stats":{"Line":10}},{"line":545,"address":[],"length":0,"stats":{"Line":12}},{"line":546,"address":[],"length":0,"stats":{"Line":3}},{"line":547,"address":[],"length":0,"stats":{"Line":6}},{"line":548,"address":[],"length":0,"stats":{"Line":3}},{"line":549,"address":[],"length":0,"stats":{"Line":3}},{"line":550,"address":[],"length":0,"stats":{"Line":3}},{"line":551,"address":[],"length":0,"stats":{"Line":3}},{"line":554,"address":[],"length":0,"stats":{"Line":2}},{"line":556,"address":[],"length":0,"stats":{"Line":13}},{"line":557,"address":[],"length":0,"stats":{"Line":2}},{"line":559,"address":[],"length":0,"stats":{"Line":3}},{"line":562,"address":[],"length":0,"stats":{"Line":5}},{"line":563,"address":[],"length":0,"stats":{"Line":10}},{"line":564,"address":[],"length":0,"stats":{"Line":10}},{"line":565,"address":[],"length":0,"stats":{"Line":10}},{"line":566,"address":[],"length":0,"stats":{"Line":10}},{"line":567,"address":[],"length":0,"stats":{"Line":10}},{"line":568,"address":[],"length":0,"stats":{"Line":10}},{"line":569,"address":[],"length":0,"stats":{"Line":10}},{"line":570,"address":[],"length":0,"stats":{"Line":10}},{"line":571,"address":[],"length":0,"stats":{"Line":10}},{"line":572,"address":[],"length":0,"stats":{"Line":5}},{"line":573,"address":[],"length":0,"stats":{"Line":5}},{"line":574,"address":[],"length":0,"stats":{"Line":5}},{"line":578,"address":[],"length":0,"stats":{"Line":903}},{"line":579,"address":[],"length":0,"stats":{"Line":903}},{"line":580,"address":[],"length":0,"stats":{"Line":236}},{"line":581,"address":[],"length":0,"stats":{"Line":3376}},{"line":585,"address":[],"length":0,"stats":{"Line":2}},{"line":586,"address":[],"length":0,"stats":{"Line":2}},{"line":587,"address":[],"length":0,"stats":{"Line":3}},{"line":588,"address":[],"length":0,"stats":{"Line":3}},{"line":593,"address":[],"length":0,"stats":{"Line":2330}},{"line":594,"address":[],"length":0,"stats":{"Line":4660}},{"line":595,"address":[],"length":0,"stats":{"Line":196}},{"line":596,"address":[],"length":0,"stats":{"Line":392}},{"line":597,"address":[],"length":0,"stats":{"Line":616}},{"line":598,"address":[],"length":0,"stats":{"Line":56}},{"line":599,"address":[],"length":0,"stats":{"Line":28}},{"line":600,"address":[],"length":0,"stats":{"Line":28}},{"line":604,"address":[],"length":0,"stats":{"Line":2134}},{"line":605,"address":[],"length":0,"stats":{"Line":4268}},{"line":606,"address":[],"length":0,"stats":{"Line":7246}},{"line":607,"address":[],"length":0,"stats":{"Line":1688}},{"line":608,"address":[],"length":0,"stats":{"Line":844}},{"line":609,"address":[],"length":0,"stats":{"Line":844}},{"line":614,"address":[],"length":0,"stats":{"Line":1457}},{"line":615,"address":[],"length":0,"stats":{"Line":1744}},{"line":616,"address":[],"length":0,"stats":{"Line":1}},{"line":617,"address":[],"length":0,"stats":{"Line":5828}},{"line":618,"address":[],"length":0,"stats":{"Line":0}},{"line":623,"address":[],"length":0,"stats":{"Line":1457}},{"line":624,"address":[],"length":0,"stats":{"Line":2914}},{"line":625,"address":[],"length":0,"stats":{"Line":4371}},{"line":627,"address":[],"length":0,"stats":{"Line":2910}},{"line":628,"address":[],"length":0,"stats":{"Line":2709}},{"line":630,"address":[],"length":0,"stats":{"Line":1455}},{"line":633,"address":[],"length":0,"stats":{"Line":4}},{"line":634,"address":[],"length":0,"stats":{"Line":2}},{"line":636,"address":[],"length":0,"stats":{"Line":0}},{"line":642,"address":[],"length":0,"stats":{"Line":845}},{"line":643,"address":[],"length":0,"stats":{"Line":845}},{"line":644,"address":[],"length":0,"stats":{"Line":0}},{"line":647,"address":[],"length":0,"stats":{"Line":4268}},{"line":648,"address":[],"length":0,"stats":{"Line":1688}},{"line":650,"address":[],"length":0,"stats":{"Line":1}},{"line":651,"address":[],"length":0,"stats":{"Line":1}},{"line":654,"address":[],"length":0,"stats":{"Line":2578}},{"line":665,"address":[],"length":0,"stats":{"Line":5}},{"line":666,"address":[],"length":0,"stats":{"Line":15}},{"line":667,"address":[],"length":0,"stats":{"Line":10}},{"line":668,"address":[],"length":0,"stats":{"Line":5}},{"line":669,"address":[],"length":0,"stats":{"Line":15}},{"line":670,"address":[],"length":0,"stats":{"Line":10}},{"line":672,"address":[],"length":0,"stats":{"Line":30}},{"line":676,"address":[],"length":0,"stats":{"Line":10}},{"line":677,"address":[],"length":0,"stats":{"Line":5}},{"line":678,"address":[],"length":0,"stats":{"Line":5}},{"line":682,"address":[],"length":0,"stats":{"Line":5}},{"line":683,"address":[],"length":0,"stats":{"Line":0}},{"line":685,"address":[],"length":0,"stats":{"Line":5}},{"line":686,"address":[],"length":0,"stats":{"Line":6}},{"line":687,"address":[],"length":0,"stats":{"Line":4}},{"line":689,"address":[],"length":0,"stats":{"Line":5}},{"line":690,"address":[],"length":0,"stats":{"Line":5}},{"line":695,"address":[],"length":0,"stats":{"Line":3}},{"line":703,"address":[],"length":0,"stats":{"Line":6}},{"line":704,"address":[],"length":0,"stats":{"Line":3}},{"line":708,"address":[],"length":0,"stats":{"Line":15}},{"line":709,"address":[],"length":0,"stats":{"Line":6}},{"line":710,"address":[],"length":0,"stats":{"Line":3}},{"line":711,"address":[],"length":0,"stats":{"Line":6}},{"line":712,"address":[],"length":0,"stats":{"Line":6}},{"line":713,"address":[],"length":0,"stats":{"Line":9}},{"line":714,"address":[],"length":0,"stats":{"Line":3}},{"line":720,"address":[],"length":0,"stats":{"Line":9}},{"line":721,"address":[],"length":0,"stats":{"Line":6}},{"line":723,"address":[],"length":0,"stats":{"Line":3}},{"line":726,"address":[],"length":0,"stats":{"Line":6}},{"line":727,"address":[],"length":0,"stats":{"Line":0}},{"line":728,"address":[],"length":0,"stats":{"Line":0}},{"line":729,"address":[],"length":0,"stats":{"Line":0}},{"line":731,"address":[],"length":0,"stats":{"Line":6}},{"line":732,"address":[],"length":0,"stats":{"Line":3}},{"line":734,"address":[],"length":0,"stats":{"Line":9}},{"line":735,"address":[],"length":0,"stats":{"Line":5}},{"line":736,"address":[],"length":0,"stats":{"Line":2}},{"line":739,"address":[],"length":0,"stats":{"Line":3}},{"line":740,"address":[],"length":0,"stats":{"Line":0}},{"line":741,"address":[],"length":0,"stats":{"Line":0}},{"line":742,"address":[],"length":0,"stats":{"Line":0}},{"line":746,"address":[],"length":0,"stats":{"Line":6}},{"line":747,"address":[],"length":0,"stats":{"Line":9}},{"line":750,"address":[],"length":0,"stats":{"Line":392}},{"line":751,"address":[],"length":0,"stats":{"Line":0}},{"line":754,"address":[],"length":0,"stats":{"Line":420}},{"line":755,"address":[],"length":0,"stats":{"Line":56}},{"line":760,"address":[],"length":0,"stats":{"Line":0}},{"line":761,"address":[],"length":0,"stats":{"Line":0}},{"line":762,"address":[],"length":0,"stats":{"Line":0}},{"line":763,"address":[],"length":0,"stats":{"Line":0}},{"line":764,"address":[],"length":0,"stats":{"Line":0}},{"line":765,"address":[],"length":0,"stats":{"Line":0}},{"line":766,"address":[],"length":0,"stats":{"Line":0}},{"line":767,"address":[],"length":0,"stats":{"Line":0}},{"line":768,"address":[],"length":0,"stats":{"Line":0}},{"line":770,"address":[],"length":0,"stats":{"Line":0}},{"line":771,"address":[],"length":0,"stats":{"Line":0}},{"line":773,"address":[],"length":0,"stats":{"Line":0}},{"line":774,"address":[],"length":0,"stats":{"Line":0}},{"line":777,"address":[],"length":0,"stats":{"Line":336}},{"line":778,"address":[],"length":0,"stats":{"Line":168}},{"line":782,"address":[],"length":0,"stats":{"Line":56}},{"line":783,"address":[],"length":0,"stats":{"Line":0}},{"line":786,"address":[],"length":0,"stats":{"Line":84}},{"line":788,"address":[],"length":0,"stats":{"Line":28}},{"line":791,"address":[],"length":0,"stats":{"Line":4}},{"line":792,"address":[],"length":0,"stats":{"Line":0}},{"line":793,"address":[],"length":0,"stats":{"Line":0}},{"line":794,"address":[],"length":0,"stats":{"Line":0}},{"line":795,"address":[],"length":0,"stats":{"Line":0}},{"line":797,"address":[],"length":0,"stats":{"Line":0}},{"line":799,"address":[],"length":0,"stats":{"Line":3}},{"line":804,"address":[],"length":0,"stats":{"Line":79}},{"line":805,"address":[],"length":0,"stats":{"Line":46}},{"line":806,"address":[],"length":0,"stats":{"Line":23}},{"line":809,"address":[],"length":0,"stats":{"Line":112}},{"line":810,"address":[],"length":0,"stats":{"Line":84}},{"line":812,"address":[],"length":0,"stats":{"Line":53}},{"line":813,"address":[],"length":0,"stats":{"Line":12}},{"line":814,"address":[],"length":0,"stats":{"Line":3}},{"line":815,"address":[],"length":0,"stats":{"Line":25}},{"line":816,"address":[],"length":0,"stats":{"Line":0}},{"line":817,"address":[],"length":0,"stats":{"Line":0}},{"line":819,"address":[],"length":0,"stats":{"Line":0}},{"line":821,"address":[],"length":0,"stats":{"Line":0}},{"line":825,"address":[],"length":0,"stats":{"Line":6}},{"line":826,"address":[],"length":0,"stats":{"Line":3}},{"line":832,"address":[],"length":0,"stats":{"Line":3}},{"line":837,"address":[],"length":0,"stats":{"Line":6}},{"line":838,"address":[],"length":0,"stats":{"Line":3}},{"line":844,"address":[],"length":0,"stats":{"Line":12}},{"line":845,"address":[],"length":0,"stats":{"Line":6}},{"line":846,"address":[],"length":0,"stats":{"Line":9}},{"line":847,"address":[],"length":0,"stats":{"Line":9}},{"line":848,"address":[],"length":0,"stats":{"Line":0}},{"line":850,"address":[],"length":0,"stats":{"Line":3}},{"line":853,"address":[],"length":0,"stats":{"Line":15}},{"line":857,"address":[],"length":0,"stats":{"Line":24}},{"line":858,"address":[],"length":0,"stats":{"Line":0}},{"line":859,"address":[],"length":0,"stats":{"Line":0}},{"line":860,"address":[],"length":0,"stats":{"Line":0}},{"line":861,"address":[],"length":0,"stats":{"Line":0}},{"line":862,"address":[],"length":0,"stats":{"Line":0}},{"line":865,"address":[],"length":0,"stats":{"Line":27}},{"line":866,"address":[],"length":0,"stats":{"Line":6}},{"line":867,"address":[],"length":0,"stats":{"Line":15}},{"line":868,"address":[],"length":0,"stats":{"Line":9}},{"line":869,"address":[],"length":0,"stats":{"Line":15}},{"line":872,"address":[],"length":0,"stats":{"Line":6}},{"line":878,"address":[],"length":0,"stats":{"Line":1}},{"line":885,"address":[],"length":0,"stats":{"Line":3}},{"line":886,"address":[],"length":0,"stats":{"Line":3}},{"line":887,"address":[],"length":0,"stats":{"Line":1}},{"line":888,"address":[],"length":0,"stats":{"Line":0}},{"line":891,"address":[],"length":0,"stats":{"Line":4}},{"line":892,"address":[],"length":0,"stats":{"Line":4}},{"line":893,"address":[],"length":0,"stats":{"Line":2}},{"line":894,"address":[],"length":0,"stats":{"Line":2}},{"line":895,"address":[],"length":0,"stats":{"Line":1}},{"line":896,"address":[],"length":0,"stats":{"Line":0}},{"line":899,"address":[],"length":0,"stats":{"Line":2}},{"line":902,"address":[],"length":0,"stats":{"Line":3}},{"line":903,"address":[],"length":0,"stats":{"Line":3}},{"line":904,"address":[],"length":0,"stats":{"Line":3}},{"line":908,"address":[],"length":0,"stats":{"Line":3}},{"line":909,"address":[],"length":0,"stats":{"Line":3}},{"line":910,"address":[],"length":0,"stats":{"Line":2}},{"line":911,"address":[],"length":0,"stats":{"Line":2}},{"line":912,"address":[],"length":0,"stats":{"Line":1}},{"line":913,"address":[],"length":0,"stats":{"Line":1}},{"line":914,"address":[],"length":0,"stats":{"Line":1}},{"line":916,"address":[],"length":0,"stats":{"Line":0}},{"line":918,"address":[],"length":0,"stats":{"Line":1}},{"line":919,"address":[],"length":0,"stats":{"Line":1}},{"line":920,"address":[],"length":0,"stats":{"Line":1}},{"line":921,"address":[],"length":0,"stats":{"Line":1}},{"line":922,"address":[],"length":0,"stats":{"Line":1}},{"line":923,"address":[],"length":0,"stats":{"Line":1}},{"line":926,"address":[],"length":0,"stats":{"Line":2}},{"line":928,"address":[],"length":0,"stats":{"Line":3}},{"line":929,"address":[],"length":0,"stats":{"Line":3}},{"line":931,"address":[],"length":0,"stats":{"Line":141}},{"line":932,"address":[],"length":0,"stats":{"Line":94}},{"line":933,"address":[],"length":0,"stats":{"Line":0}},{"line":935,"address":[],"length":0,"stats":{"Line":188}},{"line":936,"address":[],"length":0,"stats":{"Line":94}},{"line":937,"address":[],"length":0,"stats":{"Line":0}},{"line":939,"address":[],"length":0,"stats":{"Line":141}},{"line":940,"address":[],"length":0,"stats":{"Line":94}},{"line":941,"address":[],"length":0,"stats":{"Line":47}},{"line":942,"address":[],"length":0,"stats":{"Line":282}},{"line":943,"address":[],"length":0,"stats":{"Line":47}},{"line":945,"address":[],"length":0,"stats":{"Line":0}},{"line":947,"address":[],"length":0,"stats":{"Line":94}},{"line":948,"address":[],"length":0,"stats":{"Line":94}},{"line":949,"address":[],"length":0,"stats":{"Line":94}},{"line":950,"address":[],"length":0,"stats":{"Line":188}},{"line":951,"address":[],"length":0,"stats":{"Line":47}},{"line":952,"address":[],"length":0,"stats":{"Line":94}},{"line":953,"address":[],"length":0,"stats":{"Line":94}},{"line":954,"address":[],"length":0,"stats":{"Line":94}},{"line":955,"address":[],"length":0,"stats":{"Line":141}},{"line":956,"address":[],"length":0,"stats":{"Line":188}},{"line":957,"address":[],"length":0,"stats":{"Line":188}},{"line":961,"address":[],"length":0,"stats":{"Line":47}},{"line":962,"address":[],"length":0,"stats":{"Line":47}},{"line":963,"address":[],"length":0,"stats":{"Line":1}},{"line":968,"address":[],"length":0,"stats":{"Line":2}},{"line":969,"address":[],"length":0,"stats":{"Line":2}},{"line":970,"address":[],"length":0,"stats":{"Line":1}},{"line":971,"address":[],"length":0,"stats":{"Line":3}},{"line":975,"address":[],"length":0,"stats":{"Line":3}},{"line":976,"address":[],"length":0,"stats":{"Line":2}},{"line":977,"address":[],"length":0,"stats":{"Line":2}},{"line":978,"address":[],"length":0,"stats":{"Line":1}},{"line":979,"address":[],"length":0,"stats":{"Line":1}},{"line":981,"address":[],"length":0,"stats":{"Line":1}},{"line":983,"address":[],"length":0,"stats":{"Line":2}},{"line":984,"address":[],"length":0,"stats":{"Line":0}},{"line":985,"address":[],"length":0,"stats":{"Line":0}},{"line":986,"address":[],"length":0,"stats":{"Line":0}},{"line":987,"address":[],"length":0,"stats":{"Line":0}},{"line":988,"address":[],"length":0,"stats":{"Line":0}},{"line":989,"address":[],"length":0,"stats":{"Line":0}},{"line":990,"address":[],"length":0,"stats":{"Line":0}},{"line":991,"address":[],"length":0,"stats":{"Line":0}},{"line":992,"address":[],"length":0,"stats":{"Line":0}},{"line":993,"address":[],"length":0,"stats":{"Line":0}},{"line":994,"address":[],"length":0,"stats":{"Line":0}},{"line":995,"address":[],"length":0,"stats":{"Line":0}},{"line":996,"address":[],"length":0,"stats":{"Line":0}},{"line":997,"address":[],"length":0,"stats":{"Line":0}},{"line":1004,"address":[],"length":0,"stats":{"Line":1}},{"line":1009,"address":[],"length":0,"stats":{"Line":1}},{"line":1017,"address":[],"length":0,"stats":{"Line":1}},{"line":1018,"address":[],"length":0,"stats":{"Line":0}},{"line":1021,"address":[],"length":0,"stats":{"Line":3}},{"line":1022,"address":[],"length":0,"stats":{"Line":3}},{"line":1023,"address":[],"length":0,"stats":{"Line":1}},{"line":1024,"address":[],"length":0,"stats":{"Line":3}},{"line":1025,"address":[],"length":0,"stats":{"Line":3}},{"line":1026,"address":[],"length":0,"stats":{"Line":2}},{"line":1027,"address":[],"length":0,"stats":{"Line":2}},{"line":1029,"address":[],"length":0,"stats":{"Line":0}},{"line":1031,"address":[],"length":0,"stats":{"Line":1}},{"line":1032,"address":[],"length":0,"stats":{"Line":0}},{"line":1036,"address":[],"length":0,"stats":{"Line":2}},{"line":1039,"address":[],"length":0,"stats":{"Line":1}},{"line":1040,"address":[],"length":0,"stats":{"Line":2}},{"line":1041,"address":[],"length":0,"stats":{"Line":1}},{"line":1042,"address":[],"length":0,"stats":{"Line":1}},{"line":1043,"address":[],"length":0,"stats":{"Line":1}},{"line":1044,"address":[],"length":0,"stats":{"Line":1}},{"line":1047,"address":[],"length":0,"stats":{"Line":6}},{"line":1048,"address":[],"length":0,"stats":{"Line":2}},{"line":1049,"address":[],"length":0,"stats":{"Line":3}},{"line":1050,"address":[],"length":0,"stats":{"Line":0}},{"line":1053,"address":[],"length":0,"stats":{"Line":3}},{"line":1054,"address":[],"length":0,"stats":{"Line":2393}},{"line":1055,"address":[],"length":0,"stats":{"Line":1594}},{"line":1056,"address":[],"length":0,"stats":{"Line":0}},{"line":1058,"address":[],"length":0,"stats":{"Line":1594}},{"line":1059,"address":[],"length":0,"stats":{"Line":0}},{"line":1061,"address":[],"length":0,"stats":{"Line":3188}},{"line":1062,"address":[],"length":0,"stats":{"Line":2391}},{"line":1063,"address":[],"length":0,"stats":{"Line":797}},{"line":1064,"address":[],"length":0,"stats":{"Line":7173}},{"line":1065,"address":[],"length":0,"stats":{"Line":3188}},{"line":1069,"address":[],"length":0,"stats":{"Line":2}},{"line":1070,"address":[],"length":0,"stats":{"Line":0}},{"line":1071,"address":[],"length":0,"stats":{"Line":0}},{"line":1072,"address":[],"length":0,"stats":{"Line":0}},{"line":1073,"address":[],"length":0,"stats":{"Line":0}},{"line":1076,"address":[],"length":0,"stats":{"Line":2}},{"line":1078,"address":[],"length":0,"stats":{"Line":2}},{"line":1079,"address":[],"length":0,"stats":{"Line":0}},{"line":1081,"address":[],"length":0,"stats":{"Line":1}},{"line":1089,"address":[],"length":0,"stats":{"Line":2}},{"line":1098,"address":[],"length":0,"stats":{"Line":12}},{"line":1100,"address":[],"length":0,"stats":{"Line":2}},{"line":1101,"address":[],"length":0,"stats":{"Line":2}},{"line":1102,"address":[],"length":0,"stats":{"Line":4}},{"line":1103,"address":[],"length":0,"stats":{"Line":4}},{"line":1104,"address":[],"length":0,"stats":{"Line":4}},{"line":1105,"address":[],"length":0,"stats":{"Line":4}},{"line":1106,"address":[],"length":0,"stats":{"Line":4}},{"line":1112,"address":[],"length":0,"stats":{"Line":797}},{"line":1113,"address":[],"length":0,"stats":{"Line":3985}},{"line":1114,"address":[],"length":0,"stats":{"Line":797}},{"line":1115,"address":[],"length":0,"stats":{"Line":797}},{"line":1120,"address":[],"length":0,"stats":{"Line":0}},{"line":1121,"address":[],"length":0,"stats":{"Line":0}},{"line":1122,"address":[],"length":0,"stats":{"Line":0}},{"line":1123,"address":[],"length":0,"stats":{"Line":0}},{"line":1124,"address":[],"length":0,"stats":{"Line":0}},{"line":1125,"address":[],"length":0,"stats":{"Line":0}},{"line":1126,"address":[],"length":0,"stats":{"Line":0}},{"line":1128,"address":[],"length":0,"stats":{"Line":0}},{"line":1132,"address":[],"length":0,"stats":{"Line":1}},{"line":1133,"address":[],"length":0,"stats":{"Line":3}},{"line":1134,"address":[],"length":0,"stats":{"Line":1}},{"line":1135,"address":[],"length":0,"stats":{"Line":4}},{"line":1136,"address":[],"length":0,"stats":{"Line":1}},{"line":1137,"address":[],"length":0,"stats":{"Line":1}},{"line":1139,"address":[],"length":0,"stats":{"Line":0}},{"line":1140,"address":[],"length":0,"stats":{"Line":0}},{"line":1141,"address":[],"length":0,"stats":{"Line":0}},{"line":1143,"address":[],"length":0,"stats":{"Line":0}},{"line":1145,"address":[],"length":0,"stats":{"Line":1}},{"line":1150,"address":[],"length":0,"stats":{"Line":797}},{"line":1154,"address":[],"length":0,"stats":{"Line":1594}},{"line":1155,"address":[],"length":0,"stats":{"Line":3985}},{"line":1156,"address":[],"length":0,"stats":{"Line":797}},{"line":1157,"address":[],"length":0,"stats":{"Line":0}},{"line":1159,"address":[],"length":0,"stats":{"Line":797}},{"line":1160,"address":[],"length":0,"stats":{"Line":0}},{"line":1162,"address":[],"length":0,"stats":{"Line":8767}},{"line":1163,"address":[],"length":0,"stats":{"Line":5579}},{"line":1164,"address":[],"length":0,"stats":{"Line":797}},{"line":1169,"address":[],"length":0,"stats":{"Line":797}},{"line":1175,"address":[],"length":0,"stats":{"Line":2391}},{"line":1176,"address":[],"length":0,"stats":{"Line":3188}},{"line":1177,"address":[],"length":0,"stats":{"Line":797}},{"line":1178,"address":[],"length":0,"stats":{"Line":3188}},{"line":1179,"address":[],"length":0,"stats":{"Line":7173}},{"line":1180,"address":[],"length":0,"stats":{"Line":7173}},{"line":1183,"address":[],"length":0,"stats":{"Line":1594}},{"line":1184,"address":[],"length":0,"stats":{"Line":797}},{"line":1185,"address":[],"length":0,"stats":{"Line":797}},{"line":1186,"address":[],"length":0,"stats":{"Line":3985}},{"line":1187,"address":[],"length":0,"stats":{"Line":797}},{"line":1190,"address":[],"length":0,"stats":{"Line":797}},{"line":1191,"address":[],"length":0,"stats":{"Line":0}},{"line":1193,"address":[],"length":0,"stats":{"Line":2391}},{"line":1194,"address":[],"length":0,"stats":{"Line":4782}},{"line":1195,"address":[],"length":0,"stats":{"Line":3188}},{"line":1196,"address":[],"length":0,"stats":{"Line":4782}},{"line":1198,"address":[],"length":0,"stats":{"Line":797}},{"line":1204,"address":[],"length":0,"stats":{"Line":47}},{"line":1208,"address":[],"length":0,"stats":{"Line":94}},{"line":1209,"address":[],"length":0,"stats":{"Line":235}},{"line":1210,"address":[],"length":0,"stats":{"Line":47}},{"line":1211,"address":[],"length":0,"stats":{"Line":0}},{"line":1213,"address":[],"length":0,"stats":{"Line":47}},{"line":1214,"address":[],"length":0,"stats":{"Line":0}},{"line":1216,"address":[],"length":0,"stats":{"Line":94}},{"line":1217,"address":[],"length":0,"stats":{"Line":94}},{"line":1218,"address":[],"length":0,"stats":{"Line":141}},{"line":1219,"address":[],"length":0,"stats":{"Line":94}},{"line":1220,"address":[],"length":0,"stats":{"Line":517}},{"line":1223,"address":[],"length":0,"stats":{"Line":94}},{"line":1224,"address":[],"length":0,"stats":{"Line":94}},{"line":1225,"address":[],"length":0,"stats":{"Line":47}},{"line":1226,"address":[],"length":0,"stats":{"Line":94}},{"line":1227,"address":[],"length":0,"stats":{"Line":47}},{"line":1230,"address":[],"length":0,"stats":{"Line":47}},{"line":1231,"address":[],"length":0,"stats":{"Line":0}},{"line":1233,"address":[],"length":0,"stats":{"Line":141}},{"line":1234,"address":[],"length":0,"stats":{"Line":47}},{"line":1238,"address":[],"length":0,"stats":{"Line":1}},{"line":1241,"address":[],"length":0,"stats":{"Line":2}},{"line":1242,"address":[],"length":0,"stats":{"Line":2}},{"line":1243,"address":[],"length":0,"stats":{"Line":1}},{"line":1244,"address":[],"length":0,"stats":{"Line":4}},{"line":1245,"address":[],"length":0,"stats":{"Line":1}},{"line":1246,"address":[],"length":0,"stats":{"Line":1}},{"line":1248,"address":[],"length":0,"stats":{"Line":0}},{"line":1249,"address":[],"length":0,"stats":{"Line":0}},{"line":1250,"address":[],"length":0,"stats":{"Line":0}},{"line":1253,"address":[],"length":0,"stats":{"Line":0}},{"line":1254,"address":[],"length":0,"stats":{"Line":0}},{"line":1255,"address":[],"length":0,"stats":{"Line":0}},{"line":1256,"address":[],"length":0,"stats":{"Line":0}},{"line":1260,"address":[],"length":0,"stats":{"Line":0}},{"line":1261,"address":[],"length":0,"stats":{"Line":0}},{"line":1263,"address":[],"length":0,"stats":{"Line":0}},{"line":1264,"address":[],"length":0,"stats":{"Line":0}},{"line":1266,"address":[],"length":0,"stats":{"Line":0}},{"line":1267,"address":[],"length":0,"stats":{"Line":0}},{"line":1269,"address":[],"length":0,"stats":{"Line":2}},{"line":1270,"address":[],"length":0,"stats":{"Line":1}},{"line":1272,"address":[],"length":0,"stats":{"Line":0}},{"line":1278,"address":[],"length":0,"stats":{"Line":6}},{"line":1287,"address":[],"length":0,"stats":{"Line":12}},{"line":1288,"address":[],"length":0,"stats":{"Line":0}},{"line":1289,"address":[],"length":0,"stats":{"Line":0}},{"line":1290,"address":[],"length":0,"stats":{"Line":0}},{"line":1291,"address":[],"length":0,"stats":{"Line":0}},{"line":1292,"address":[],"length":0,"stats":{"Line":0}},{"line":1293,"address":[],"length":0,"stats":{"Line":0}},{"line":1294,"address":[],"length":0,"stats":{"Line":0}},{"line":1296,"address":[],"length":0,"stats":{"Line":6}},{"line":1298,"address":[],"length":0,"stats":{"Line":6}},{"line":1300,"address":[],"length":0,"stats":{"Line":6}},{"line":1301,"address":[],"length":0,"stats":{"Line":6}},{"line":1302,"address":[],"length":0,"stats":{"Line":6}},{"line":1303,"address":[],"length":0,"stats":{"Line":6}},{"line":1304,"address":[],"length":0,"stats":{"Line":6}},{"line":1305,"address":[],"length":0,"stats":{"Line":6}},{"line":1308,"address":[],"length":0,"stats":{"Line":9}},{"line":1309,"address":[],"length":0,"stats":{"Line":6}},{"line":1310,"address":[],"length":0,"stats":{"Line":6}},{"line":1311,"address":[],"length":0,"stats":{"Line":6}},{"line":1312,"address":[],"length":0,"stats":{"Line":6}},{"line":1313,"address":[],"length":0,"stats":{"Line":6}},{"line":1314,"address":[],"length":0,"stats":{"Line":6}},{"line":1315,"address":[],"length":0,"stats":{"Line":3}},{"line":1316,"address":[],"length":0,"stats":{"Line":3}},{"line":1319,"address":[],"length":0,"stats":{"Line":9}},{"line":1323,"address":[],"length":0,"stats":{"Line":0}},{"line":1324,"address":[],"length":0,"stats":{"Line":0}},{"line":1331,"address":[],"length":0,"stats":{"Line":0}},{"line":1332,"address":[],"length":0,"stats":{"Line":0}},{"line":1333,"address":[],"length":0,"stats":{"Line":0}},{"line":1334,"address":[],"length":0,"stats":{"Line":0}},{"line":1335,"address":[],"length":0,"stats":{"Line":0}},{"line":1336,"address":[],"length":0,"stats":{"Line":0}},{"line":1338,"address":[],"length":0,"stats":{"Line":0}},{"line":1339,"address":[],"length":0,"stats":{"Line":0}},{"line":1340,"address":[],"length":0,"stats":{"Line":0}},{"line":1343,"address":[],"length":0,"stats":{"Line":0}},{"line":1344,"address":[],"length":0,"stats":{"Line":0}},{"line":1345,"address":[],"length":0,"stats":{"Line":0}},{"line":1346,"address":[],"length":0,"stats":{"Line":0}},{"line":1347,"address":[],"length":0,"stats":{"Line":0}},{"line":1348,"address":[],"length":0,"stats":{"Line":0}},{"line":1349,"address":[],"length":0,"stats":{"Line":0}},{"line":1350,"address":[],"length":0,"stats":{"Line":0}},{"line":1351,"address":[],"length":0,"stats":{"Line":0}},{"line":1355,"address":[],"length":0,"stats":{"Line":0}},{"line":1359,"address":[],"length":0,"stats":{"Line":31}},{"line":1362,"address":[],"length":0,"stats":{"Line":31}},{"line":1368,"address":[],"length":0,"stats":{"Line":0}},{"line":1369,"address":[],"length":0,"stats":{"Line":0}},{"line":1370,"address":[],"length":0,"stats":{"Line":0}},{"line":1371,"address":[],"length":0,"stats":{"Line":0}},{"line":1372,"address":[],"length":0,"stats":{"Line":0}},{"line":1373,"address":[],"length":0,"stats":{"Line":0}},{"line":1374,"address":[],"length":0,"stats":{"Line":0}},{"line":1375,"address":[],"length":0,"stats":{"Line":0}},{"line":1378,"address":[],"length":0,"stats":{"Line":0}},{"line":1382,"address":[],"length":0,"stats":{"Line":3}},{"line":1383,"address":[],"length":0,"stats":{"Line":9}},{"line":1384,"address":[],"length":0,"stats":{"Line":2227}},{"line":1385,"address":[],"length":0,"stats":{"Line":8896}},{"line":1386,"address":[],"length":0,"stats":{"Line":8896}},{"line":1387,"address":[],"length":0,"stats":{"Line":8313712}},{"line":1388,"address":[],"length":0,"stats":{"Line":29059072}},{"line":1389,"address":[],"length":0,"stats":{"Line":49815552}},{"line":1390,"address":[],"length":0,"stats":{"Line":49815552}},{"line":1392,"address":[],"length":0,"stats":{"Line":12453888}},{"line":1395,"address":[],"length":0,"stats":{"Line":3}},{"line":1404,"address":[],"length":0,"stats":{"Line":3}},{"line":1410,"address":[],"length":0,"stats":{"Line":12}},{"line":1412,"address":[],"length":0,"stats":{"Line":15}},{"line":1413,"address":[],"length":0,"stats":{"Line":6}},{"line":1415,"address":[],"length":0,"stats":{"Line":9}},{"line":1416,"address":[],"length":0,"stats":{"Line":9}},{"line":1417,"address":[],"length":0,"stats":{"Line":6}},{"line":1419,"address":[],"length":0,"stats":{"Line":6}},{"line":1420,"address":[],"length":0,"stats":{"Line":6}},{"line":1421,"address":[],"length":0,"stats":{"Line":15}},{"line":1422,"address":[],"length":0,"stats":{"Line":9}},{"line":1423,"address":[],"length":0,"stats":{"Line":0}},{"line":1425,"address":[],"length":0,"stats":{"Line":4451}},{"line":1426,"address":[],"length":0,"stats":{"Line":11120}},{"line":1427,"address":[],"length":0,"stats":{"Line":11120}},{"line":1428,"address":[],"length":0,"stats":{"Line":4448}},{"line":1430,"address":[],"length":0,"stats":{"Line":3}},{"line":1444,"address":[],"length":0,"stats":{"Line":4}},{"line":1445,"address":[],"length":0,"stats":{"Line":8}},{"line":1446,"address":[],"length":0,"stats":{"Line":12}},{"line":1447,"address":[],"length":0,"stats":{"Line":12}},{"line":1448,"address":[],"length":0,"stats":{"Line":20}},{"line":1450,"address":[],"length":0,"stats":{"Line":6}},{"line":1451,"address":[],"length":0,"stats":{"Line":4}},{"line":1453,"address":[],"length":0,"stats":{"Line":6}},{"line":1456,"address":[],"length":0,"stats":{"Line":38}},{"line":1458,"address":[],"length":0,"stats":{"Line":2}},{"line":1459,"address":[],"length":0,"stats":{"Line":6}},{"line":1461,"address":[],"length":0,"stats":{"Line":4}},{"line":1462,"address":[],"length":0,"stats":{"Line":6}},{"line":1463,"address":[],"length":0,"stats":{"Line":8}},{"line":1464,"address":[],"length":0,"stats":{"Line":0}},{"line":1466,"address":[],"length":0,"stats":{"Line":12}},{"line":1467,"address":[],"length":0,"stats":{"Line":12}},{"line":1468,"address":[],"length":0,"stats":{"Line":12}},{"line":1470,"address":[],"length":0,"stats":{"Line":4}},{"line":1471,"address":[],"length":0,"stats":{"Line":6}},{"line":1474,"address":[],"length":0,"stats":{"Line":26}},{"line":1476,"address":[],"length":0,"stats":{"Line":6}},{"line":1477,"address":[],"length":0,"stats":{"Line":2}},{"line":1478,"address":[],"length":0,"stats":{"Line":8}},{"line":1483,"address":[],"length":0,"stats":{"Line":12}},{"line":1484,"address":[],"length":0,"stats":{"Line":1}},{"line":1487,"address":[],"length":0,"stats":{"Line":9}},{"line":1488,"address":[],"length":0,"stats":{"Line":3}},{"line":1489,"address":[],"length":0,"stats":{"Line":2}},{"line":1490,"address":[],"length":0,"stats":{"Line":8}},{"line":1491,"address":[],"length":0,"stats":{"Line":8}},{"line":1492,"address":[],"length":0,"stats":{"Line":8}},{"line":1493,"address":[],"length":0,"stats":{"Line":8}},{"line":1495,"address":[],"length":0,"stats":{"Line":6}},{"line":1497,"address":[],"length":0,"stats":{"Line":12}},{"line":1498,"address":[],"length":0,"stats":{"Line":12}},{"line":1499,"address":[],"length":0,"stats":{"Line":12}},{"line":1500,"address":[],"length":0,"stats":{"Line":10}},{"line":1501,"address":[],"length":0,"stats":{"Line":12}},{"line":1502,"address":[],"length":0,"stats":{"Line":10}},{"line":1503,"address":[],"length":0,"stats":{"Line":10}},{"line":1504,"address":[],"length":0,"stats":{"Line":10}},{"line":1505,"address":[],"length":0,"stats":{"Line":14}},{"line":1506,"address":[],"length":0,"stats":{"Line":14}},{"line":1507,"address":[],"length":0,"stats":{"Line":14}},{"line":1508,"address":[],"length":0,"stats":{"Line":14}},{"line":1509,"address":[],"length":0,"stats":{"Line":6}},{"line":1511,"address":[],"length":0,"stats":{"Line":4}},{"line":1514,"address":[],"length":0,"stats":{"Line":3}},{"line":1515,"address":[],"length":0,"stats":{"Line":1}},{"line":1518,"address":[],"length":0,"stats":{"Line":0}},{"line":1519,"address":[],"length":0,"stats":{"Line":0}},{"line":1520,"address":[],"length":0,"stats":{"Line":0}},{"line":1521,"address":[],"length":0,"stats":{"Line":0}},{"line":1522,"address":[],"length":0,"stats":{"Line":0}},{"line":1526,"address":[],"length":0,"stats":{"Line":2}},{"line":1527,"address":[],"length":0,"stats":{"Line":3}},{"line":1528,"address":[],"length":0,"stats":{"Line":4}},{"line":1529,"address":[],"length":0,"stats":{"Line":2}},{"line":1530,"address":[],"length":0,"stats":{"Line":4}},{"line":1532,"address":[],"length":0,"stats":{"Line":5}},{"line":1533,"address":[],"length":0,"stats":{"Line":5}},{"line":1534,"address":[],"length":0,"stats":{"Line":4}},{"line":1536,"address":[],"length":0,"stats":{"Line":3}},{"line":1537,"address":[],"length":0,"stats":{"Line":2}},{"line":1538,"address":[],"length":0,"stats":{"Line":1}},{"line":1539,"address":[],"length":0,"stats":{"Line":1}},{"line":1540,"address":[],"length":0,"stats":{"Line":1}},{"line":1544,"address":[],"length":0,"stats":{"Line":0}},{"line":1548,"address":[],"length":0,"stats":{"Line":6}},{"line":1549,"address":[],"length":0,"stats":{"Line":3}},{"line":1550,"address":[],"length":0,"stats":{"Line":2}},{"line":1552,"address":[],"length":0,"stats":{"Line":2}},{"line":1563,"address":[],"length":0,"stats":{"Line":4}},{"line":1568,"address":[],"length":0,"stats":{"Line":8}},{"line":1569,"address":[],"length":0,"stats":{"Line":34}},{"line":1570,"address":[],"length":0,"stats":{"Line":15}},{"line":1572,"address":[],"length":0,"stats":{"Line":1}},{"line":1573,"address":[],"length":0,"stats":{"Line":1}},{"line":1575,"address":[],"length":0,"stats":{"Line":1}},{"line":1576,"address":[],"length":0,"stats":{"Line":1}},{"line":1577,"address":[],"length":0,"stats":{"Line":1}},{"line":1578,"address":[],"length":0,"stats":{"Line":1}},{"line":1580,"address":[],"length":0,"stats":{"Line":14}},{"line":1582,"address":[],"length":0,"stats":{"Line":0}},{"line":1583,"address":[],"length":0,"stats":{"Line":0}},{"line":1585,"address":[],"length":0,"stats":{"Line":0}},{"line":1586,"address":[],"length":0,"stats":{"Line":0}},{"line":1587,"address":[],"length":0,"stats":{"Line":0}},{"line":1588,"address":[],"length":0,"stats":{"Line":0}},{"line":1592,"address":[],"length":0,"stats":{"Line":3}},{"line":1653,"address":[],"length":0,"stats":{"Line":16}},{"line":1656,"address":[],"length":0,"stats":{"Line":32}},{"line":1665,"address":[],"length":0,"stats":{"Line":15}},{"line":1666,"address":[],"length":0,"stats":{"Line":60}},{"line":1667,"address":[],"length":0,"stats":{"Line":49}},{"line":1672,"address":[],"length":0,"stats":{"Line":14}},{"line":1673,"address":[],"length":0,"stats":{"Line":28}},{"line":1676,"address":[],"length":0,"stats":{"Line":1}},{"line":1677,"address":[],"length":0,"stats":{"Line":1}},{"line":1680,"address":[],"length":0,"stats":{"Line":1}},{"line":1681,"address":[],"length":0,"stats":{"Line":1}},{"line":1684,"address":[],"length":0,"stats":{"Line":1}},{"line":1685,"address":[],"length":0,"stats":{"Line":1}},{"line":1688,"address":[],"length":0,"stats":{"Line":1}},{"line":1689,"address":[],"length":0,"stats":{"Line":1}},{"line":1692,"address":[],"length":0,"stats":{"Line":1}},{"line":1693,"address":[],"length":0,"stats":{"Line":1}},{"line":1696,"address":[],"length":0,"stats":{"Line":3}},{"line":1697,"address":[],"length":0,"stats":{"Line":3}},{"line":1698,"address":[],"length":0,"stats":{"Line":1}},{"line":1703,"address":[],"length":0,"stats":{"Line":8}},{"line":1704,"address":[],"length":0,"stats":{"Line":4}},{"line":1705,"address":[],"length":0,"stats":{"Line":4}},{"line":1706,"address":[],"length":0,"stats":{"Line":2}},{"line":1709,"address":[],"length":0,"stats":{"Line":3}},{"line":1710,"address":[],"length":0,"stats":{"Line":9}},{"line":1711,"address":[],"length":0,"stats":{"Line":12}},{"line":1712,"address":[],"length":0,"stats":{"Line":6}},{"line":1715,"address":[],"length":0,"stats":{"Line":1}},{"line":1717,"address":[],"length":0,"stats":{"Line":4}},{"line":1718,"address":[],"length":0,"stats":{"Line":1}},{"line":1721,"address":[],"length":0,"stats":{"Line":11}},{"line":1722,"address":[],"length":0,"stats":{"Line":33}},{"line":1723,"address":[],"length":0,"stats":{"Line":44}},{"line":1724,"address":[],"length":0,"stats":{"Line":33}},{"line":1727,"address":[],"length":0,"stats":{"Line":1}},{"line":1728,"address":[],"length":0,"stats":{"Line":3}},{"line":1729,"address":[],"length":0,"stats":{"Line":4}},{"line":1730,"address":[],"length":0,"stats":{"Line":4}},{"line":1733,"address":[],"length":0,"stats":{"Line":1}},{"line":1742,"address":[],"length":0,"stats":{"Line":1}},{"line":1745,"address":[],"length":0,"stats":{"Line":2}},{"line":1746,"address":[],"length":0,"stats":{"Line":8}},{"line":1747,"address":[],"length":0,"stats":{"Line":2}},{"line":1750,"address":[],"length":0,"stats":{"Line":1}},{"line":1752,"address":[],"length":0,"stats":{"Line":1}},{"line":1755,"address":[],"length":0,"stats":{"Line":1}},{"line":1757,"address":[],"length":0,"stats":{"Line":1}},{"line":1760,"address":[],"length":0,"stats":{"Line":1}},{"line":1761,"address":[],"length":0,"stats":{"Line":2}},{"line":1764,"address":[],"length":0,"stats":{"Line":1}},{"line":1765,"address":[],"length":0,"stats":{"Line":1}},{"line":1766,"address":[],"length":0,"stats":{"Line":1}},{"line":1767,"address":[],"length":0,"stats":{"Line":1}},{"line":1776,"address":[],"length":0,"stats":{"Line":3}},{"line":1777,"address":[],"length":0,"stats":{"Line":6}},{"line":1778,"address":[],"length":0,"stats":{"Line":0}},{"line":1780,"address":[],"length":0,"stats":{"Line":9}},{"line":1781,"address":[],"length":0,"stats":{"Line":6}},{"line":1782,"address":[],"length":0,"stats":{"Line":1}},{"line":1784,"address":[],"length":0,"stats":{"Line":3}},{"line":1785,"address":[],"length":0,"stats":{"Line":1}},{"line":1788,"address":[],"length":0,"stats":{"Line":3}},{"line":1789,"address":[],"length":0,"stats":{"Line":1}},{"line":1791,"address":[],"length":0,"stats":{"Line":2}},{"line":1792,"address":[],"length":0,"stats":{"Line":1}},{"line":1793,"address":[],"length":0,"stats":{"Line":3}},{"line":1794,"address":[],"length":0,"stats":{"Line":1}},{"line":1795,"address":[],"length":0,"stats":{"Line":3}},{"line":1796,"address":[],"length":0,"stats":{"Line":1}},{"line":1798,"address":[],"length":0,"stats":{"Line":2}},{"line":1799,"address":[],"length":0,"stats":{"Line":2}},{"line":1800,"address":[],"length":0,"stats":{"Line":2}},{"line":1801,"address":[],"length":0,"stats":{"Line":3}},{"line":1803,"address":[],"length":0,"stats":{"Line":5}},{"line":1804,"address":[],"length":0,"stats":{"Line":2}},{"line":1806,"address":[],"length":0,"stats":{"Line":3}},{"line":1809,"address":[],"length":0,"stats":{"Line":1}},{"line":1810,"address":[],"length":0,"stats":{"Line":3}},{"line":1811,"address":[],"length":0,"stats":{"Line":3}},{"line":1813,"address":[],"length":0,"stats":{"Line":3}},{"line":1814,"address":[],"length":0,"stats":{"Line":3}},{"line":1815,"address":[],"length":0,"stats":{"Line":3}},{"line":1816,"address":[],"length":0,"stats":{"Line":3}},{"line":1817,"address":[],"length":0,"stats":{"Line":1}},{"line":1819,"address":[],"length":0,"stats":{"Line":2}},{"line":1820,"address":[],"length":0,"stats":{"Line":2}},{"line":1821,"address":[],"length":0,"stats":{"Line":1}},{"line":1822,"address":[],"length":0,"stats":{"Line":0}},{"line":1824,"address":[],"length":0,"stats":{"Line":1}},{"line":1825,"address":[],"length":0,"stats":{"Line":0}},{"line":1830,"address":[],"length":0,"stats":{"Line":3}},{"line":1831,"address":[],"length":0,"stats":{"Line":3}},{"line":1832,"address":[],"length":0,"stats":{"Line":3}},{"line":1834,"address":[],"length":0,"stats":{"Line":4}},{"line":1835,"address":[],"length":0,"stats":{"Line":3}},{"line":1838,"address":[],"length":0,"stats":{"Line":1}},{"line":1839,"address":[],"length":0,"stats":{"Line":1}},{"line":1840,"address":[],"length":0,"stats":{"Line":1}},{"line":1841,"address":[],"length":0,"stats":{"Line":1}},{"line":1842,"address":[],"length":0,"stats":{"Line":1}},{"line":1843,"address":[],"length":0,"stats":{"Line":1}},{"line":1846,"address":[],"length":0,"stats":{"Line":1}},{"line":1848,"address":[],"length":0,"stats":{"Line":2}},{"line":1849,"address":[],"length":0,"stats":{"Line":2}},{"line":1850,"address":[],"length":0,"stats":{"Line":2}},{"line":1851,"address":[],"length":0,"stats":{"Line":2}},{"line":1852,"address":[],"length":0,"stats":{"Line":1}},{"line":1853,"address":[],"length":0,"stats":{"Line":1}},{"line":1857,"address":[],"length":0,"stats":{"Line":2}},{"line":1858,"address":[],"length":0,"stats":{"Line":1}},{"line":1859,"address":[],"length":0,"stats":{"Line":0}},{"line":1860,"address":[],"length":0,"stats":{"Line":0}},{"line":1861,"address":[],"length":0,"stats":{"Line":0}},{"line":1862,"address":[],"length":0,"stats":{"Line":0}},{"line":1864,"address":[],"length":0,"stats":{"Line":0}},{"line":1865,"address":[],"length":0,"stats":{"Line":0}},{"line":1867,"address":[],"length":0,"stats":{"Line":0}},{"line":1868,"address":[],"length":0,"stats":{"Line":0}},{"line":1869,"address":[],"length":0,"stats":{"Line":0}},{"line":1871,"address":[],"length":0,"stats":{"Line":0}},{"line":1874,"address":[],"length":0,"stats":{"Line":0}},{"line":1875,"address":[],"length":0,"stats":{"Line":0}},{"line":1877,"address":[],"length":0,"stats":{"Line":0}},{"line":1878,"address":[],"length":0,"stats":{"Line":0}},{"line":1879,"address":[],"length":0,"stats":{"Line":0}},{"line":1880,"address":[],"length":0,"stats":{"Line":0}},{"line":1882,"address":[],"length":0,"stats":{"Line":0}},{"line":1883,"address":[],"length":0,"stats":{"Line":0}},{"line":1884,"address":[],"length":0,"stats":{"Line":0}},{"line":1886,"address":[],"length":0,"stats":{"Line":0}},{"line":1887,"address":[],"length":0,"stats":{"Line":0}},{"line":1890,"address":[],"length":0,"stats":{"Line":0}},{"line":1891,"address":[],"length":0,"stats":{"Line":0}},{"line":1892,"address":[],"length":0,"stats":{"Line":0}},{"line":1893,"address":[],"length":0,"stats":{"Line":0}},{"line":1894,"address":[],"length":0,"stats":{"Line":0}},{"line":1895,"address":[],"length":0,"stats":{"Line":0}},{"line":1898,"address":[],"length":0,"stats":{"Line":0}},{"line":1899,"address":[],"length":0,"stats":{"Line":0}},{"line":1900,"address":[],"length":0,"stats":{"Line":0}},{"line":1901,"address":[],"length":0,"stats":{"Line":0}},{"line":1905,"address":[],"length":0,"stats":{"Line":3}},{"line":1907,"address":[],"length":0,"stats":{"Line":2}},{"line":1908,"address":[],"length":0,"stats":{"Line":2}},{"line":1909,"address":[],"length":0,"stats":{"Line":1}},{"line":1910,"address":[],"length":0,"stats":{"Line":1}},{"line":1911,"address":[],"length":0,"stats":{"Line":1}},{"line":1912,"address":[],"length":0,"stats":{"Line":1}},{"line":1914,"address":[],"length":0,"stats":{"Line":1}},{"line":1918,"address":[],"length":0,"stats":{"Line":11}},{"line":1919,"address":[],"length":0,"stats":{"Line":21}},{"line":1920,"address":[],"length":0,"stats":{"Line":11}},{"line":1922,"address":[],"length":0,"stats":{"Line":13}},{"line":1923,"address":[],"length":0,"stats":{"Line":10}},{"line":1924,"address":[],"length":0,"stats":{"Line":0}},{"line":1926,"address":[],"length":0,"stats":{"Line":20}},{"line":1927,"address":[],"length":0,"stats":{"Line":10}},{"line":1929,"address":[],"length":0,"stats":{"Line":10}},{"line":1931,"address":[],"length":0,"stats":{"Line":50}},{"line":1932,"address":[],"length":0,"stats":{"Line":40}},{"line":1933,"address":[],"length":0,"stats":{"Line":20}},{"line":1934,"address":[],"length":0,"stats":{"Line":0}},{"line":1935,"address":[],"length":0,"stats":{"Line":0}},{"line":1936,"address":[],"length":0,"stats":{"Line":0}},{"line":1942,"address":[],"length":0,"stats":{"Line":30}},{"line":1943,"address":[],"length":0,"stats":{"Line":20}},{"line":1944,"address":[],"length":0,"stats":{"Line":650}},{"line":1945,"address":[],"length":0,"stats":{"Line":2560}},{"line":1946,"address":[],"length":0,"stats":{"Line":2560}},{"line":1947,"address":[],"length":0,"stats":{"Line":85120}},{"line":1948,"address":[],"length":0,"stats":{"Line":368640}},{"line":1949,"address":[],"length":0,"stats":{"Line":655360}},{"line":1950,"address":[],"length":0,"stats":{"Line":491520}},{"line":1951,"address":[],"length":0,"stats":{"Line":491520}},{"line":1952,"address":[],"length":0,"stats":{"Line":327680}},{"line":1953,"address":[],"length":0,"stats":{"Line":327680}},{"line":1955,"address":[],"length":0,"stats":{"Line":327680}},{"line":1960,"address":[],"length":0,"stats":{"Line":50}},{"line":1961,"address":[],"length":0,"stats":{"Line":30}},{"line":1963,"address":[],"length":0,"stats":{"Line":20}},{"line":1964,"address":[],"length":0,"stats":{"Line":50}},{"line":1969,"address":[],"length":0,"stats":{"Line":40}},{"line":1970,"address":[],"length":0,"stats":{"Line":40}},{"line":1971,"address":[],"length":0,"stats":{"Line":20}},{"line":1972,"address":[],"length":0,"stats":{"Line":20}},{"line":1974,"address":[],"length":0,"stats":{"Line":30}},{"line":1975,"address":[],"length":0,"stats":{"Line":30}},{"line":1979,"address":[],"length":0,"stats":{"Line":1}},{"line":1985,"address":[],"length":0,"stats":{"Line":1}},{"line":1986,"address":[],"length":0,"stats":{"Line":1}},{"line":1988,"address":[],"length":0,"stats":{"Line":3}},{"line":1989,"address":[],"length":0,"stats":{"Line":0}},{"line":1990,"address":[],"length":0,"stats":{"Line":0}},{"line":1992,"address":[],"length":0,"stats":{"Line":0}},{"line":1993,"address":[],"length":0,"stats":{"Line":0}},{"line":1995,"address":[],"length":0,"stats":{"Line":0}},{"line":1997,"address":[],"length":0,"stats":{"Line":0}},{"line":1998,"address":[],"length":0,"stats":{"Line":0}},{"line":2000,"address":[],"length":0,"stats":{"Line":0}},{"line":2001,"address":[],"length":0,"stats":{"Line":0}},{"line":2004,"address":[],"length":0,"stats":{"Line":0}},{"line":2006,"address":[],"length":0,"stats":{"Line":0}},{"line":2007,"address":[],"length":0,"stats":{"Line":0}},{"line":2009,"address":[],"length":0,"stats":{"Line":0}},{"line":2011,"address":[],"length":0,"stats":{"Line":0}},{"line":2013,"address":[],"length":0,"stats":{"Line":0}},{"line":2014,"address":[],"length":0,"stats":{"Line":0}},{"line":2015,"address":[],"length":0,"stats":{"Line":0}},{"line":2017,"address":[],"length":0,"stats":{"Line":0}},{"line":2018,"address":[],"length":0,"stats":{"Line":0}},{"line":2022,"address":[],"length":0,"stats":{"Line":3}},{"line":2023,"address":[],"length":0,"stats":{"Line":4}},{"line":2024,"address":[],"length":0,"stats":{"Line":2}},{"line":2026,"address":[],"length":0,"stats":{"Line":1}},{"line":2027,"address":[],"length":0,"stats":{"Line":2}},{"line":2028,"address":[],"length":0,"stats":{"Line":3}},{"line":2030,"address":[],"length":0,"stats":{"Line":1}},{"line":2031,"address":[],"length":0,"stats":{"Line":0}},{"line":2033,"address":[],"length":0,"stats":{"Line":3}},{"line":2034,"address":[],"length":0,"stats":{"Line":1}},{"line":2037,"address":[],"length":0,"stats":{"Line":2}},{"line":2038,"address":[],"length":0,"stats":{"Line":1}},{"line":2043,"address":[],"length":0,"stats":{"Line":1}},{"line":2047,"address":[],"length":0,"stats":{"Line":2}},{"line":2048,"address":[],"length":0,"stats":{"Line":2}},{"line":2050,"address":[],"length":0,"stats":{"Line":33}},{"line":2052,"address":[],"length":0,"stats":{"Line":30}},{"line":2053,"address":[],"length":0,"stats":{"Line":40}},{"line":2055,"address":[],"length":0,"stats":{"Line":5}},{"line":2056,"address":[],"length":0,"stats":{"Line":0}},{"line":2059,"address":[],"length":0,"stats":{"Line":1}},{"line":2063,"address":[],"length":0,"stats":{"Line":0}},{"line":2067,"address":[],"length":0,"stats":{"Line":0}},{"line":2068,"address":[],"length":0,"stats":{"Line":0}},{"line":2070,"address":[],"length":0,"stats":{"Line":0}},{"line":2072,"address":[],"length":0,"stats":{"Line":0}},{"line":2073,"address":[],"length":0,"stats":{"Line":0}},{"line":2075,"address":[],"length":0,"stats":{"Line":0}},{"line":2076,"address":[],"length":0,"stats":{"Line":0}},{"line":2079,"address":[],"length":0,"stats":{"Line":0}},{"line":2083,"address":[],"length":0,"stats":{"Line":10}},{"line":2087,"address":[],"length":0,"stats":{"Line":20}},{"line":2089,"address":[],"length":0,"stats":{"Line":30}},{"line":2091,"address":[],"length":0,"stats":{"Line":0}},{"line":2092,"address":[],"length":0,"stats":{"Line":0}},{"line":2094,"address":[],"length":0,"stats":{"Line":50}},{"line":2095,"address":[],"length":0,"stats":{"Line":0}},{"line":2098,"address":[],"length":0,"stats":{"Line":10}},{"line":2102,"address":[],"length":0,"stats":{"Line":0}},{"line":2106,"address":[],"length":0,"stats":{"Line":0}},{"line":2108,"address":[],"length":0,"stats":{"Line":0}},{"line":2110,"address":[],"length":0,"stats":{"Line":0}},{"line":2111,"address":[],"length":0,"stats":{"Line":0}},{"line":2113,"address":[],"length":0,"stats":{"Line":0}},{"line":2114,"address":[],"length":0,"stats":{"Line":0}},{"line":2117,"address":[],"length":0,"stats":{"Line":0}},{"line":2123,"address":[],"length":0,"stats":{"Line":1}},{"line":2124,"address":[],"length":0,"stats":{"Line":1}},{"line":2125,"address":[],"length":0,"stats":{"Line":0}},{"line":2126,"address":[],"length":0,"stats":{"Line":1}},{"line":2127,"address":[],"length":0,"stats":{"Line":0}},{"line":2128,"address":[],"length":0,"stats":{"Line":0}},{"line":2129,"address":[],"length":0,"stats":{"Line":0}},{"line":2130,"address":[],"length":0,"stats":{"Line":0}},{"line":2131,"address":[],"length":0,"stats":{"Line":0}},{"line":2132,"address":[],"length":0,"stats":{"Line":0}},{"line":2133,"address":[],"length":0,"stats":{"Line":0}},{"line":2134,"address":[],"length":0,"stats":{"Line":0}},{"line":2135,"address":[],"length":0,"stats":{"Line":0}},{"line":2136,"address":[],"length":0,"stats":{"Line":0}},{"line":2137,"address":[],"length":0,"stats":{"Line":0}},{"line":2138,"address":[],"length":0,"stats":{"Line":0}},{"line":2139,"address":[],"length":0,"stats":{"Line":0}},{"line":2145,"address":[],"length":0,"stats":{"Line":3}},{"line":2146,"address":[],"length":0,"stats":{"Line":4}},{"line":2150,"address":[],"length":0,"stats":{"Line":1}},{"line":2151,"address":[],"length":0,"stats":{"Line":1}},{"line":2152,"address":[],"length":0,"stats":{"Line":0}},{"line":2153,"address":[],"length":0,"stats":{"Line":1}},{"line":2158,"address":[],"length":0,"stats":{"Line":0}},{"line":2159,"address":[],"length":0,"stats":{"Line":0}},{"line":2160,"address":[],"length":0,"stats":{"Line":0}},{"line":2161,"address":[],"length":0,"stats":{"Line":0}},{"line":2162,"address":[],"length":0,"stats":{"Line":0}},{"line":2163,"address":[],"length":0,"stats":{"Line":0}},{"line":2168,"address":[],"length":0,"stats":{"Line":1}},{"line":2169,"address":[],"length":0,"stats":{"Line":7}},{"line":2170,"address":[],"length":0,"stats":{"Line":4}},{"line":2171,"address":[],"length":0,"stats":{"Line":2}},{"line":2172,"address":[],"length":0,"stats":{"Line":1}},{"line":2174,"address":[],"length":0,"stats":{"Line":0}},{"line":2178,"address":[],"length":0,"stats":{"Line":1}},{"line":2179,"address":[],"length":0,"stats":{"Line":1}},{"line":2180,"address":[],"length":0,"stats":{"Line":0}},{"line":2181,"address":[],"length":0,"stats":{"Line":0}},{"line":2182,"address":[],"length":0,"stats":{"Line":0}},{"line":2183,"address":[],"length":0,"stats":{"Line":0}},{"line":2184,"address":[],"length":0,"stats":{"Line":1}},{"line":2185,"address":[],"length":0,"stats":{"Line":0}},{"line":2186,"address":[],"length":0,"stats":{"Line":0}},{"line":2187,"address":[],"length":0,"stats":{"Line":0}},{"line":2188,"address":[],"length":0,"stats":{"Line":0}},{"line":2189,"address":[],"length":0,"stats":{"Line":0}},{"line":2190,"address":[],"length":0,"stats":{"Line":0}},{"line":2191,"address":[],"length":0,"stats":{"Line":0}},{"line":2192,"address":[],"length":0,"stats":{"Line":0}},{"line":2198,"address":[],"length":0,"stats":{"Line":4}},{"line":2199,"address":[],"length":0,"stats":{"Line":128}},{"line":2200,"address":[],"length":0,"stats":{"Line":12}}],"covered":767,"coverable":1072},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","footagedescription.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::FootageDescription` — codec-internal stream inventory.\n//!\n//! Mirrors `src/codec/src/footagedescription.h`. A value type describing\n//! the streams a `Decoder::probe()` found in a file. Video and subtitle\n//! streams are stored as oakcommon by-value handles; audio streams as\n//! `oakcore_rs::TimeRangeList`/raw audio params. The original's\n//! `Track::Type` mapping and XML load/save are intentionally not reproduced\n//! (NOTES.md §4) — use [`FootageDescription::stream_is_video`] etc.\n\nuse oakcore_rs::{Rational, TimeRange};\n\nuse crate::bridge::common::{OakAudioParams, OakSubtitleParams, OakVideoParams};\n\n/// One stream entry in a footage description.\n#[derive(Clone, Debug)]\npub enum StreamEntry {\n\t/// A video stream (params handle, addref'd).\n\tVideo(OakVideoParams),\n\t/// An audio stream.\n\tAudio(OakAudioParams),\n\t/// A subtitle stream (params handle, addref'd).\n\tSubtitle(OakSubtitleParams),\n}\n\n/// `olive::FootageDescription` — the decoder name plus stream inventory.\n#[derive(Clone, Debug, Default)]\npub struct FootageDescription {\n\t/// The decoder id that produced this description.\n\tdecoder: String,\n\t/// Total number of streams (video + audio + subtitle).\n\ttotal_stream_count: usize,\n\t/// The streams, in probe order.\n\tstreams: Vec\u003cStreamEntry\u003e,\n\t/// The media has a source start time.\n\thas_source_start_time: bool,\n\t/// Source start time (when present).\n\tsource_start_time: Rational,\n\t/// Total duration across streams (may be empty).\n\tduration: Option\u003cTimeRange\u003e,\n}\n\nimpl FootageDescription {\n\t/// New, empty description with the given decoder id.\n\tpub fn new(decoder: \u0026str) -\u003e Self {\n\t\tSelf {\n\t\t\tdecoder: decoder.to_string(),\n\t\t\ttotal_stream_count: 0,\n\t\t\tstreams: Vec::new(),\n\t\t\thas_source_start_time: false,\n\t\t\t// C++ default-constructs the Rational member, i.e. the 0/0\n\t\t\t// null sentinel. It is not meaningful until a source start time\n\t\t\t// is set.\n\t\t\tsource_start_time: Rational::default(),\n\t\t\tduration: None,\n\t\t}\n\t}\n\n\t/// The decoder id.\n\tpub fn decoder(\u0026self) -\u003e \u0026str {\n\t\t\u0026self.decoder\n\t}\n\n\t/// Total stream count.\n\tpub fn total_stream_count(\u0026self) -\u003e usize {\n\t\tself.total_stream_count\n\t}\n\n\t/// Number of video streams.\n\tpub fn video_stream_count(\u0026self) -\u003e usize {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter(|s| matches!(s, StreamEntry::Video(_)))\n\t\t\t.count()\n\t}\n\n\t/// Number of audio streams.\n\tpub fn audio_stream_count(\u0026self) -\u003e usize {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter(|s| matches!(s, StreamEntry::Audio(_)))\n\t\t\t.count()\n\t}\n\n\t/// Number of subtitle streams.\n\tpub fn subtitle_stream_count(\u0026self) -\u003e usize {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter(|s| matches!(s, StreamEntry::Subtitle(_)))\n\t\t\t.count()\n\t}\n\n\t/// Whether the `index`-th stream (in probe order) is a video stream.\n\tpub fn stream_is_video(\u0026self, index: usize) -\u003e bool {\n\t\tself.streams\n\t\t\t.get(index)\n\t\t\t.is_some_and(|s| matches!(s, StreamEntry::Video(_)))\n\t}\n\n\t/// Whether the `index`-th stream (in probe order) is an audio stream.\n\tpub fn stream_is_audio(\u0026self, index: usize) -\u003e bool {\n\t\tself.streams\n\t\t\t.get(index)\n\t\t\t.is_some_and(|s| matches!(s, StreamEntry::Audio(_)))\n\t}\n\n\t/// Whether the `index`-th stream (in probe order) is a subtitle stream.\n\tpub fn stream_is_subtitle(\u0026self, index: usize) -\u003e bool {\n\t\tself.streams\n\t\t\t.get(index)\n\t\t\t.is_some_and(|s| matches!(s, StreamEntry::Subtitle(_)))\n\t}\n\n\t/// The `index`-th video stream's params (by video-stream ordinal).\n\tpub fn get_video_stream(\u0026self, index: usize) -\u003e Option\u003c\u0026OakVideoParams\u003e {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter_map(|s| match s {\n\t\t\t\tStreamEntry::Video(p) =\u003e Some(p),\n\t\t\t\t_ =\u003e None,\n\t\t\t})\n\t\t\t.nth(index)\n\t}\n\n\t/// The `index`-th audio stream's params (by audio-stream ordinal).\n\tpub fn get_audio_stream(\u0026self, index: usize) -\u003e Option\u003c\u0026OakAudioParams\u003e {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter_map(|s| match s {\n\t\t\t\tStreamEntry::Audio(p) =\u003e Some(p),\n\t\t\t\t_ =\u003e None,\n\t\t\t})\n\t\t\t.nth(index)\n\t}\n\n\t/// The `index`-th subtitle stream's params (by subtitle-stream ordinal).\n\tpub fn get_subtitle_stream(\u0026self, index: usize) -\u003e Option\u003c\u0026OakSubtitleParams\u003e {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter_map(|s| match s {\n\t\t\t\tStreamEntry::Subtitle(p) =\u003e Some(p),\n\t\t\t\t_ =\u003e None,\n\t\t\t})\n\t\t\t.nth(index)\n\t}\n\n\t/// Whether the media has a source start time.\n\tpub fn has_source_start_time(\u0026self) -\u003e bool {\n\t\tself.has_source_start_time\n\t}\n\n\t/// Source start time.\n\tpub fn source_start_time(\u0026self) -\u003e Rational {\n\t\tself.source_start_time\n\t}\n\n\t/// Total duration across streams.\n\tpub fn duration(\u0026self) -\u003e Option\u003cTimeRange\u003e {\n\t\tself.duration\n\t}\n\n\t/// Append one stream entry, mirroring the C++ `add_*_stream` family.\n\t///\n\t/// Test/extension support: the ffi probe tests build\n\t/// `FootageDescription`s through a fake decoder and need a way to\n\t/// populate them. Hidden from docs; never called by production code.\n\t#[doc(hidden)]\n\tpub fn push_stream(\u0026mut self, entry: StreamEntry) {\n\t\tself.total_stream_count += 1;\n\t\tself.streams.push(entry);\n\t}\n\n\t/// Set the total stream count, mirroring the C++ `set_stream_count`\n\t/// (which records every stream in the container, including ones the\n\t/// probe could not describe and therefore did not push).\n\t///\n\t/// Hidden from docs; probe/extension support only.\n\t#[doc(hidden)]\n\tpub fn set_stream_count(\u0026mut self, count: usize) {\n\t\tself.total_stream_count = count;\n\t}\n\n\t/// Set the source start time, mirroring the C++ `set_source_start_time`.\n\t/// `source` is the raw metadata source kind (unused by the Rust port).\n\t///\n\t/// Hidden from docs; probe/extension support only.\n\t#[doc(hidden)]\n\tpub fn set_source_start_time(\u0026mut self, time: Rational, _source: i32) {\n\t\tself.has_source_start_time = true;\n\t\tself.source_start_time = time;\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn video_params(index: i32) -\u003e OakVideoParams {\n\t\tOakVideoParams {\n\t\t\tctx: index as usize as *mut std::ffi::c_void,\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\tfn audio_params() -\u003e OakAudioParams {\n\t\tOakAudioParams {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\tfn subtitle_params() -\u003e OakSubtitleParams {\n\t\tOakSubtitleParams {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\t#[test]\n\tfn new_description_is_empty() {\n\t\tlet d = FootageDescription::new(\"ffmpeg\");\n\t\tassert_eq!(d.decoder(), \"ffmpeg\");\n\t\tassert_eq!(d.total_stream_count(), 0);\n\t\tassert_eq!(d.video_stream_count(), 0);\n\t\tassert_eq!(d.audio_stream_count(), 0);\n\t\tassert_eq!(d.subtitle_stream_count(), 0);\n\t\tassert!(!d.has_source_start_time());\n\t\tassert!(d.duration().is_none());\n\t}\n\n\t#[test]\n\tfn push_stream_updates_counts_and_queries() {\n\t\tlet mut d = FootageDescription::new(\"mock\");\n\t\td.push_stream(StreamEntry::Video(video_params(0)));\n\t\td.push_stream(StreamEntry::Audio(audio_params()));\n\t\td.push_stream(StreamEntry::Subtitle(subtitle_params()));\n\t\td.push_stream(StreamEntry::Video(video_params(1)));\n\n\t\tassert_eq!(d.total_stream_count(), 4);\n\t\tassert_eq!(d.video_stream_count(), 2);\n\t\tassert_eq!(d.audio_stream_count(), 1);\n\t\tassert_eq!(d.subtitle_stream_count(), 1);\n\n\t\t// stream_is_* by probe order.\n\t\tassert!(d.stream_is_video(0));\n\t\tassert!(d.stream_is_audio(1));\n\t\tassert!(d.stream_is_subtitle(2));\n\t\tassert!(d.stream_is_video(3));\n\n\t\t// Ordinal getters.\n\t\tassert!(d.get_video_stream(0).is_some());\n\t\tassert!(d.get_video_stream(1).is_some());\n\t\tassert!(d.get_video_stream(2).is_none());\n\t\tassert!(d.get_audio_stream(0).is_some());\n\t\tassert!(d.get_audio_stream(1).is_none());\n\t\tassert!(d.get_subtitle_stream(0).is_some());\n\t\tassert!(d.get_subtitle_stream(1).is_none());\n\t}\n}\n","traces":[{"line":60,"address":[],"length":0,"stats":{"Line":11}},{"line":62,"address":[],"length":0,"stats":{"Line":33}},{"line":64,"address":[],"length":0,"stats":{"Line":22}},{"line":69,"address":[],"length":0,"stats":{"Line":11}},{"line":75,"address":[],"length":0,"stats":{"Line":16}},{"line":76,"address":[],"length":0,"stats":{"Line":16}},{"line":80,"address":[],"length":0,"stats":{"Line":3}},{"line":81,"address":[],"length":0,"stats":{"Line":3}},{"line":85,"address":[],"length":0,"stats":{"Line":13}},{"line":86,"address":[],"length":0,"stats":{"Line":13}},{"line":88,"address":[],"length":0,"stats":{"Line":29}},{"line":93,"address":[],"length":0,"stats":{"Line":9}},{"line":94,"address":[],"length":0,"stats":{"Line":9}},{"line":96,"address":[],"length":0,"stats":{"Line":21}},{"line":101,"address":[],"length":0,"stats":{"Line":3}},{"line":102,"address":[],"length":0,"stats":{"Line":3}},{"line":104,"address":[],"length":0,"stats":{"Line":8}},{"line":109,"address":[],"length":0,"stats":{"Line":2}},{"line":110,"address":[],"length":0,"stats":{"Line":4}},{"line":111,"address":[],"length":0,"stats":{"Line":2}},{"line":112,"address":[],"length":0,"stats":{"Line":4}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":119,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":2}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":130,"address":[],"length":0,"stats":{"Line":7}},{"line":131,"address":[],"length":0,"stats":{"Line":7}},{"line":133,"address":[],"length":0,"stats":{"Line":20}},{"line":134,"address":[],"length":0,"stats":{"Line":18}},{"line":135,"address":[],"length":0,"stats":{"Line":4}},{"line":137,"address":[],"length":0,"stats":{"Line":14}},{"line":141,"address":[],"length":0,"stats":{"Line":3}},{"line":142,"address":[],"length":0,"stats":{"Line":3}},{"line":144,"address":[],"length":0,"stats":{"Line":10}},{"line":145,"address":[],"length":0,"stats":{"Line":6}},{"line":146,"address":[],"length":0,"stats":{"Line":4}},{"line":148,"address":[],"length":0,"stats":{"Line":6}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":9}},{"line":156,"address":[],"length":0,"stats":{"Line":4}},{"line":157,"address":[],"length":0,"stats":{"Line":5}},{"line":159,"address":[],"length":0,"stats":{"Line":4}},{"line":163,"address":[],"length":0,"stats":{"Line":1}},{"line":164,"address":[],"length":0,"stats":{"Line":1}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":1}},{"line":174,"address":[],"length":0,"stats":{"Line":1}},{"line":183,"address":[],"length":0,"stats":{"Line":14}},{"line":184,"address":[],"length":0,"stats":{"Line":14}},{"line":185,"address":[],"length":0,"stats":{"Line":42}},{"line":194,"address":[],"length":0,"stats":{"Line":2}},{"line":195,"address":[],"length":0,"stats":{"Line":2}},{"line":203,"address":[],"length":0,"stats":{"Line":1}},{"line":204,"address":[],"length":0,"stats":{"Line":1}},{"line":205,"address":[],"length":0,"stats":{"Line":1}}],"covered":59,"coverable":61},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","frame.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::Frame` — a CPU pixel buffer plus an `OakVideoParams` handle.\n//!\n//! Mirrors `src/codec/src/frame.h`. The params are held as an oakcommon\n//! by-value handle (`bridge::common::OakVideoParams`, refcounted) so the\n//! byte-level ABI of `oakcodec_frame_get_params`/`_set_params` is\n//! unchanged; the pixel data itself is a plain `Vec\u003cu8\u003e`. Line-size and\n//! pixel-format math lives here.\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_free, oakcommon_videoparams_get_format,\n\toakcommon_videoparams_get_height, oakcommon_videoparams_get_is_valid,\n\toakcommon_videoparams_get_width, oakcommon_videoparams_init, OakVideoParams,\n};\nuse oakcore_rs::{PixelFormat, Rational};\n\n/// Number of channels in the internal RGBA pipeline layout\n/// (`VideoParams::k_internal_channel_count == k_rgba_channel_count == 4`).\n/// The frame math (linesize, per-pixel offsets) always assumes this layout,\n/// matching the C++ decoder path which produces/consumes RGBA frames.\nconst VIDEO_CHANNELS: i32 = 4;\n\n/// Interlacing of a frame's parameter set (VideoParams::Interlacing).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Interlacing {\n\t/// Progressive.\n\tNone = 0,\n\t/// Upper field first.\n\tTopFieldFirst = 1,\n\t/// Lower field first.\n\tBottomFieldFirst = 2,\n}\n\n/// `olive::Frame`: reference-counted CPU pixel buffer + params handle.\n#[derive(Debug)]\npub struct Frame {\n\t/// Video parameter set (oakcommon handle, refcounted).\n\tpub params: Option\u003cOakVideoParams\u003e,\n\t/// Pixel buffer (unallocated until `allocate`).\n\tdata: Vec\u003cu8\u003e,\n\t/// Distance between rows in bytes (0 until params are set).\n\tlinesize_bytes: i32,\n\t/// Timestamp, rational seconds.\n\ttimestamp: Rational,\n\t/// Allocated pixel format (may differ from params while converting).\n\tallocated_format: PixelFormat,\n}\n\n/// Map an `OakPixelFormat` int code back to a `PixelFormat` (unknown codes\n/// become [`PixelFormat::Invalid`]).\nfn pixel_format_from_i32(v: i32) -\u003e PixelFormat {\n\tmatch v {\n\t\t0 =\u003e PixelFormat::U8,\n\t\t1 =\u003e PixelFormat::U10,\n\t\t2 =\u003e PixelFormat::U16,\n\t\t3 =\u003e PixelFormat::F16,\n\t\t4 =\u003e PixelFormat::F32,\n\t\t_ =\u003e PixelFormat::Invalid,\n\t}\n}\n\n/// Bytes per pixel for `format` at `channels`, matching\n/// `VideoParams::get_bytes_per_pixel`:\n/// - U10 is a packed RGBA10A2 pixel: 4 bytes for the RGBA layout,\n/// regardless of channel count; anything else is rejected (0).\n/// - All other formats are `bytes_per_channel * channels`.\nfn bytes_per_pixel(format: PixelFormat, channels: i32) -\u003e i32 {\n\tif format == PixelFormat::U10 {\n\t\treturn if channels == VIDEO_CHANNELS { 4 } else { 0 };\n\t}\n\t(format.bytes_per_channel() as i32) * channels\n}\n\n/// Increment the refcount of a params handle (a no-op for test-stub handles\n/// whose `addref` is `None`). `pub(crate)` so the ffi layer can hand out\n/// addref'd copies (`oakcodec_frame_get_params`).\npub(crate) fn params_addref(p: \u0026OakVideoParams) {\n\tif let Some(addref) = p.addref {\n\t\t// SAFETY: `addref` is a valid C function pointer targeting `ctx`.\n\t\tunsafe { addref(p.ctx) };\n\t}\n}\n\n/// Release a params handle (prefers the `release` function pointer; the\n/// test stubs use `oakcommon_videoparams_free` instead). Nulls `ctx` so the\n/// handle cannot be released twice.\npub(crate) fn params_release(p: \u0026mut OakVideoParams) {\n\tif p.ctx.is_null() {\n\t\treturn;\n\t}\n\tif let Some(release) = p.release {\n\t\t// SAFETY: `release` is a valid C function pointer targeting `ctx`.\n\t\tunsafe { release(p.ctx) };\n\t} else {\n\t\t// SAFETY: `p` points at a live handle; `oakcommon_videoparams_free`\n\t\t// is a no-op for the null ctx we leave behind.\n\t\tunsafe { oakcommon_videoparams_free(p) };\n\t}\n\tp.ctx = std::ptr::null_mut();\n}\n\nimpl Frame {\n\t/// New frame with default (invalid) params; buffer unallocated.\n\tpub fn new() -\u003e Self {\n\t\tlet params = unsafe { oakcommon_videoparams_init() };\n\t\tFrame {\n\t\t\tparams: Some(params),\n\t\t\tdata: Vec::new(),\n\t\t\tlinesize_bytes: 0,\n\t\t\ttimestamp: Rational::new(0, 1),\n\t\t\tallocated_format: PixelFormat::Invalid,\n\t\t}\n\t}\n\n\t/// New frame with a copy of `params` (handle addref'd internally).\n\tpub fn with_params(params: OakVideoParams) -\u003e Self {\n\t\tparams_addref(\u0026params);\n\t\tlet mut frame = Frame {\n\t\t\tparams: Some(params),\n\t\t\tdata: Vec::new(),\n\t\t\tlinesize_bytes: 0,\n\t\t\ttimestamp: Rational::new(0, 1),\n\t\t\tallocated_format: PixelFormat::Invalid,\n\t\t};\n\t\tframe.recompute_linesize();\n\t\tframe\n\t}\n\n\t/// The video parameter set, or `None` when empty.\n\tpub fn params(\u0026self) -\u003e Option\u003c\u0026OakVideoParams\u003e {\n\t\tself.params.as_ref()\n\t}\n\n\t/// Replace the parameter set (handle addref'd), recompute line sizes,\n\t/// do NOT reallocate the buffer.\n\tpub fn set_params(\u0026mut self, params: OakVideoParams) {\n\t\tif let Some(mut old) = self.params.take() {\n\t\t\tparams_release(\u0026mut old);\n\t\t}\n\t\tparams_addref(\u0026params);\n\t\tself.params = Some(params);\n\t\tself.recompute_linesize();\n\t\t// Deliberately do not touch `data`: an existing buffer keeps its\n\t\t// layout; `allocated_format` stays at the old format until the next\n\t\t// `allocate()`.\n\t}\n\n\t/// Recompute `linesize_bytes` from the current params (0 when unset).\n\tfn recompute_linesize(\u0026mut self) {\n\t\tself.linesize_bytes = match \u0026self.params {\n\t\t\tSome(p) =\u003e {\n\t\t\t\tlet w = unsafe { oakcommon_videoparams_get_width(p.clone()) };\n\t\t\t\tlet fmt = pixel_format_from_i32(unsafe {\n\t\t\t\t\toakcommon_videoparams_get_format(p.clone())\n\t\t\t\t});\n\t\t\t\tSelf::generate_linesize_bytes(fmt, w)\n\t\t\t}\n\t\t\tNone =\u003e 0,\n\t\t};\n\t}\n\n\t/// Allocate the pixel buffer from the current params.\n\tpub fn allocate(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = match \u0026self.params {\n\t\t\tSome(p) =\u003e p.clone(),\n\t\t\tNone =\u003e return Err(crate::error::Error::State),\n\t\t};\n\n\t\tlet is_valid = unsafe { oakcommon_videoparams_get_is_valid(params.clone()) };\n\t\tif is_valid == 0 {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\n\t\tif self.is_allocated() {\n\t\t\t// Already allocated; leave the buffer alone.\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet width = unsafe { oakcommon_videoparams_get_width(params.clone()) };\n\t\tlet height = unsafe { oakcommon_videoparams_get_height(params.clone()) };\n\t\tlet format = pixel_format_from_i32(unsafe {\n\t\t\toakcommon_videoparams_get_format(params)\n\t\t});\n\n\t\tlet linesize = Self::generate_linesize_bytes(format, width);\n\t\tlet size = (linesize as usize).wrapping_mul(height as usize);\n\n\t\tself.data.resize(size, 0);\n\t\tself.linesize_bytes = linesize;\n\t\tself.allocated_format = format;\n\n\t\tOk(())\n\t}\n\n\t/// 1 when the pixel buffer is allocated.\n\tpub fn is_allocated(\u0026self) -\u003e bool {\n\t\t!self.data.is_empty()\n\t}\n\n\t/// Writable pixel buffer slice, or `None` when unallocated.\n\tpub fn data(\u0026self) -\u003e Option\u003c\u0026[u8]\u003e {\n\t\tif self.is_allocated() {\n\t\t\tSome(\u0026self.data)\n\t\t} else {\n\t\t\tNone\n\t\t}\n\t}\n\n\t/// Mutable pixel buffer slice, or `None` when unallocated.\n\tpub fn data_mut(\u0026mut self) -\u003e Option\u003c\u0026mut [u8]\u003e {\n\t\tif self.is_allocated() {\n\t\t\tSome(\u0026mut self.data)\n\t\t} else {\n\t\t\tNone\n\t\t}\n\t}\n\n\t/// Size of the pixel buffer in bytes (0 when unallocated).\n\tpub fn allocated_size(\u0026self) -\u003e usize {\n\t\tself.data.len()\n\t}\n\n\t/// Distance between two rows in bytes (0 when params are unset).\n\tpub fn linesize_bytes(\u0026self) -\u003e i32 {\n\t\tself.linesize_bytes\n\t}\n\n\t/// Distance between two rows in pixels.\n\tpub fn linesize_pixels(\u0026self) -\u003e i32 {\n\t\tlet bpp = self.bytes_per_pixel();\n\t\tif bpp \u003e 0 {\n\t\t\tself.linesize_bytes / bpp\n\t\t} else {\n\t\t\t0\n\t\t}\n\t}\n\n\t/// Bytes per pixel for the current params format (RGBA layout).\n\tfn bytes_per_pixel(\u0026self) -\u003e i32 {\n\t\tbytes_per_pixel(self.format(), VIDEO_CHANNELS)\n\t}\n\n\t/// Frame width in pixels (0 when params are empty).\n\tpub fn width(\u0026self) -\u003e i32 {\n\t\tmatch \u0026self.params {\n\t\t\tSome(p) =\u003e unsafe { oakcommon_videoparams_get_width(p.clone()) },\n\t\t\tNone =\u003e 0,\n\t\t}\n\t}\n\n\t/// Frame height in pixels (0 when params are empty).\n\tpub fn height(\u0026self) -\u003e i32 {\n\t\tmatch \u0026self.params {\n\t\t\tSome(p) =\u003e unsafe { oakcommon_videoparams_get_height(p.clone()) },\n\t\t\tNone =\u003e 0,\n\t\t}\n\t}\n\n\t/// Pixel format (`OakPixelFormat` value).\n\tpub fn format(\u0026self) -\u003e PixelFormat {\n\t\tmatch \u0026self.params {\n\t\t\tSome(p) =\u003e {\n\t\t\t\tpixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) })\n\t\t\t}\n\t\t\tNone =\u003e PixelFormat::Invalid,\n\t\t}\n\t}\n\n\t/// Plane channel count of the params format.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/frame.h` reads this from the params handle via\n\t/// `oakcommon_videoparams_get_channel_count`, which is not exposed in the\n\t/// Rust bridge. Decoder frames are always produced in the internal RGBA\n\t/// layout, so this returns [`VIDEO_CHANNELS`] (4).\n\tpub fn channel_count(\u0026self) -\u003e i32 {\n\t\tVIDEO_CHANNELS\n\t}\n\n\t/// Timestamp as a rational number of seconds.\n\tpub fn timestamp(\u0026self) -\u003e Rational {\n\t\tself.timestamp\n\t}\n\n\t/// Set the timestamp.\n\tpub fn set_timestamp(\u0026mut self, ts: Rational) {\n\t\tself.timestamp = ts;\n\t}\n\n\t/// Distance between rows for a (format, width) pair, in bytes.\n\t///\n\t/// Matches `Frame::generate_linesize_bytes(width, format, channel_count)`\n\t/// in `src/codec/src/frame.cpp` with `channel_count` fixed at\n\t/// [`VIDEO_CHANNELS`]: bytes per pixel times the width rounded up to a\n\t/// 32-byte boundary. Uses wrapping arithmetic so extreme (or negative)\n\t/// widths behave like the C++ `int` math rather than panicking.\n\tpub fn generate_linesize_bytes(format: PixelFormat, width: i32) -\u003e i32 {\n\t\tlet bpp = bytes_per_pixel(format, VIDEO_CHANNELS);\n\t\tlet aligned = width.wrapping_add(31) \u0026 !31;\n\t\tbpp.wrapping_mul(aligned)\n\t}\n\n\t/// Convert the buffer to another pixel format (`convert_to_olive_format`).\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/frame.cpp` — the destination params are carried by\n\t/// the C++ callers via `oakcommon_videoparams_*`; Rust keeps the\n\t/// equivalent state in `self.params`.\n\t///\n\t/// When the current params format already matches the format the buffer\n\t/// was allocated in, this is a no-op (`Ok`). A genuine pixel-format\n\t/// conversion requires the OIIO bridge (`convert_to_olive_format`), which\n\t/// is not yet ported to the pure-Rust crate; until then a mismatched\n\t/// conversion is rejected with [`crate::error::Error::State`].\n\tpub fn convert(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif !self.is_allocated() {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tlet fmt = self.format();\n\t\tif self.allocated_format == fmt {\n\t\t\tself.recompute_linesize();\n\t\t\treturn Ok(());\n\t\t}\n\t\tErr(crate::error::Error::State)\n\t}\n\n\t/// True when `(x, y)` lies inside the allocated buffer.\n\tfn contains_pixel(\u0026self, x: i32, y: i32) -\u003e bool {\n\t\tself.is_allocated() \u0026\u0026 x \u003e= 0 \u0026\u0026 x \u003c self.width() \u0026\u0026 y \u003e= 0 \u0026\u0026 y \u003c self.height()\n\t}\n\n\t/// Read a pixel sample at (x, y).\n\t///\n\t/// Returns the first byte of the pixel at `(x, y)` (the R channel for\n\t/// RGBA). Out-of-bounds reads return 0, matching the C++ default\n\t/// (transparent black) color.\n\tpub fn get_pixel(\u0026self, x: i32, y: i32) -\u003e u8 {\n\t\tif !self.contains_pixel(x, y) {\n\t\t\treturn 0;\n\t\t}\n\t\tlet offset = (y as usize).wrapping_mul(self.linesize_bytes as usize)\n\t\t\t+ (x as usize).wrapping_mul(self.bytes_per_pixel() as usize);\n\t\t*self.data.get(offset).unwrap_or(\u00260)\n\t}\n\n\t/// Write a pixel sample at (x, y).\n\t///\n\t/// Writes `value` to the first byte of the pixel at `(x, y)`. Out-of-bounds\n\t/// writes are ignored, matching the C++ `set_pixel`.\n\tpub fn set_pixel(\u0026mut self, x: i32, y: i32, value: u8) {\n\t\tif !self.contains_pixel(x, y) {\n\t\t\treturn;\n\t\t}\n\t\tlet offset = (y as usize).wrapping_mul(self.linesize_bytes as usize)\n\t\t\t+ (x as usize).wrapping_mul(self.bytes_per_pixel() as usize);\n\t\tif let Some(byte) = self.data.get_mut(offset) {\n\t\t\t*byte = value;\n\t\t}\n\t}\n}\n\nimpl Drop for Frame {\n\t/// Release the owned params handle when the last reference dies,\n\t/// mirroring the C++ `Frame::~Frame`.\n\tfn drop(\u0026mut self) {\n\t\tif let Some(mut p) = self.params.take() {\n\t\t\tparams_release(\u0026mut p);\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\n\tfn frame(w: i32, h: i32) -\u003e Frame {\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(w, h) };\n\t\tFrame::with_params(params)\n\t}\n\n\t#[test]\n\tfn linesize_is_32_byte_aligned_for_u8() {\n\t\t// U8 RGBA: 4 bytes/pixel, width rounded up to a 32-byte boundary.\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 9), 4 * 32);\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 100), 4 * 128);\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 0), 0);\n\t}\n\n\t#[test]\n\tfn linesize_respects_16bit_and_u10() {\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U16, 16), 8 * 32);\n\t\t// U10 is a packed 4-byte RGBA pixel regardless of channel count.\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U10, 16), 4 * 32);\n\t}\n\n\t#[test]\n\tfn allocate_fills_buffer_and_reports_size() {\n\t\tlet mut f = frame(100, 50);\n\t\tassert!(!f.is_allocated());\n\t\tassert_eq!(f.allocated_size(), 0);\n\t\tassert!(f.data().is_none());\n\n\t\tf.allocate().unwrap();\n\t\tassert!(f.is_allocated());\n\t\tassert_eq!(f.allocated_size(), (4 * 128) * 50);\n\t\tassert_eq!(f.data().map(|d| d.len()), Some((4 * 128) * 50));\n\t\tassert_eq!(f.linesize_bytes(), 4 * 128);\n\t}\n\n\t#[test]\n\tfn allocate_invalid_params_is_error() {\n\t\t// init_basic(0, 0) is not valid -\u003e allocate must reject.\n\t\tlet mut f = frame(0, 0);\n\t\tassert!(f.allocate().is_err());\n\t}\n\n\t#[test]\n\tfn get_set_pixel_round_trip() {\n\t\tlet mut f = frame(100, 50);\n\t\tf.allocate().unwrap();\n\n\t\tf.set_pixel(3, 4, 0xAB);\n\t\tassert_eq!(f.get_pixel(3, 4), 0xAB);\n\n\t\t// pixel (0,0) is the first byte; pixel (1,0) is bpp bytes later.\n\t\tf.set_pixel(0, 0, 0x11);\n\t\tf.set_pixel(1, 0, 0x22);\n\t\tassert_eq!(f.get_pixel(0, 0), 0x11);\n\t\tassert_eq!(f.get_pixel(1, 0), 0x22);\n\t}\n\n\t#[test]\n\tfn out_of_bounds_reads_zero_and_writes_ignored() {\n\t\tlet mut f = frame(10, 10);\n\t\tf.allocate().unwrap();\n\t\tassert_eq!(f.get_pixel(50, 50), 0);\n\t\tassert_eq!(f.get_pixel(-1, 0), 0);\n\t\tf.set_pixel(50, 50, 0xFF);\n\t\t// untouched\n\t\tassert_eq!(f.data().unwrap()[0], 0);\n\t}\n\n\t#[test]\n\tfn set_params_recomputes_linesize_without_realloc() {\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(10, 10) };\n\t\tlet mut f = Frame::with_params(params);\n\t\tf.allocate().unwrap();\n\t\tlet before = f.allocated_size();\n\n\t\tlet wider = unsafe { oakcommon_videoparams_init_basic(100, 10) };\n\t\tf.set_params(wider);\n\t\t// linesize reflects the new width, but the buffer is untouched.\n\t\tassert_eq!(f.linesize_bytes(), 4 * 128);\n\t\tassert_eq!(f.allocated_size(), before);\n\t}\n}\n\n#[cfg(test)]\nmod tests_extra {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\n\tfn frame(w: i32, h: i32) -\u003e Frame {\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(w, h) };\n\t\tFrame::with_params(params)\n\t}\n\n\t#[test]\n\tfn linesize_pixels_derives_from_bytes() {\n\t\t// U8 RGBA: bpp 4 -\u003e linesize_pixels = linesize_bytes / 4.\n\t\tlet mut f = frame(32, 16);\n\t\tf.allocate().unwrap();\n\t\tassert_eq!(f.linesize_bytes(), 4 * 32);\n\t\tassert_eq!(f.linesize_pixels(), 32);\n\t\t// Unallocated / unset params -\u003e 0.\n\t\tlet g = Frame::new();\n\t\tassert_eq!(g.linesize_pixels(), 0);\n\t}\n\n\t#[test]\n\tfn channel_count_is_internal_rgba_layout() {\n\t\tlet f = frame(4, 4);\n\t\tassert_eq!(f.channel_count(), VIDEO_CHANNELS);\n\t}\n\n\t#[test]\n\tfn convert_is_noop_when_format_matches() {\n\t\tlet mut f = frame(16, 16);\n\t\tf.allocate().unwrap();\n\t\t// allocated U8 == params U8 -\u003e no-op Ok.\n\t\tassert!(f.convert().is_ok());\n\t\t// Unallocated -\u003e Err(State).\n\t\tlet mut g = Frame::new();\n\t\tassert!(g.convert().is_err());\n\t}\n\n\t#[test]\n\tfn pixel_format_from_unknown_code_is_invalid() {\n\t\tlet p = unsafe { oakcommon_videoparams_init_basic(1, 1) };\n\t\tunsafe { crate::bridge::common::oakcommon_videoparams_set_format(p.clone(), 99) };\n\t\tlet f = Frame::with_params(p);\n\t\tassert_eq!(f.format(), PixelFormat::Invalid);\n\t}\n\n\t#[test]\n\tfn default_timestamp_is_zero() {\n\t\tlet f = Frame::new();\n\t\tlet ts = f.timestamp();\n\t\tassert_eq!(ts.numerator(), 0);\n\t\tassert_eq!(ts.denominator(), 1);\n\t}\n}\n","traces":[{"line":67,"address":[],"length":0,"stats":{"Line":85}},{"line":68,"address":[],"length":0,"stats":{"Line":85}},{"line":69,"address":[],"length":0,"stats":{"Line":55}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":28}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":81}},{"line":84,"address":[],"length":0,"stats":{"Line":81}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":80}},{"line":93,"address":[],"length":0,"stats":{"Line":40}},{"line":94,"address":[],"length":0,"stats":{"Line":40}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":103,"address":[],"length":0,"stats":{"Line":45}},{"line":104,"address":[],"length":0,"stats":{"Line":90}},{"line":105,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":45}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":113,"address":[],"length":0,"stats":{"Line":45}},{"line":115,"address":[],"length":0,"stats":{"Line":45}},{"line":120,"address":[],"length":0,"stats":{"Line":6}},{"line":121,"address":[],"length":0,"stats":{"Line":12}},{"line":123,"address":[],"length":0,"stats":{"Line":12}},{"line":124,"address":[],"length":0,"stats":{"Line":12}},{"line":126,"address":[],"length":0,"stats":{"Line":6}},{"line":132,"address":[],"length":0,"stats":{"Line":37}},{"line":133,"address":[],"length":0,"stats":{"Line":74}},{"line":135,"address":[],"length":0,"stats":{"Line":74}},{"line":136,"address":[],"length":0,"stats":{"Line":74}},{"line":138,"address":[],"length":0,"stats":{"Line":37}},{"line":141,"address":[],"length":0,"stats":{"Line":74}},{"line":142,"address":[],"length":0,"stats":{"Line":37}},{"line":146,"address":[],"length":0,"stats":{"Line":5}},{"line":147,"address":[],"length":0,"stats":{"Line":10}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":6}},{"line":154,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":4}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":158,"address":[],"length":0,"stats":{"Line":4}},{"line":165,"address":[],"length":0,"stats":{"Line":40}},{"line":166,"address":[],"length":0,"stats":{"Line":40}},{"line":167,"address":[],"length":0,"stats":{"Line":40}},{"line":168,"address":[],"length":0,"stats":{"Line":160}},{"line":169,"address":[],"length":0,"stats":{"Line":80}},{"line":170,"address":[],"length":0,"stats":{"Line":120}},{"line":172,"address":[],"length":0,"stats":{"Line":120}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"address":[],"length":0,"stats":{"Line":29}},{"line":180,"address":[],"length":0,"stats":{"Line":58}},{"line":181,"address":[],"length":0,"stats":{"Line":87}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":116}},{"line":186,"address":[],"length":0,"stats":{"Line":29}},{"line":187,"address":[],"length":0,"stats":{"Line":3}},{"line":190,"address":[],"length":0,"stats":{"Line":52}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":195,"address":[],"length":0,"stats":{"Line":104}},{"line":196,"address":[],"length":0,"stats":{"Line":104}},{"line":197,"address":[],"length":0,"stats":{"Line":52}},{"line":198,"address":[],"length":0,"stats":{"Line":52}},{"line":201,"address":[],"length":0,"stats":{"Line":104}},{"line":202,"address":[],"length":0,"stats":{"Line":104}},{"line":204,"address":[],"length":0,"stats":{"Line":78}},{"line":205,"address":[],"length":0,"stats":{"Line":26}},{"line":206,"address":[],"length":0,"stats":{"Line":26}},{"line":208,"address":[],"length":0,"stats":{"Line":26}},{"line":212,"address":[],"length":0,"stats":{"Line":87}},{"line":213,"address":[],"length":0,"stats":{"Line":87}},{"line":217,"address":[],"length":0,"stats":{"Line":23}},{"line":218,"address":[],"length":0,"stats":{"Line":46}},{"line":219,"address":[],"length":0,"stats":{"Line":21}},{"line":221,"address":[],"length":0,"stats":{"Line":2}},{"line":226,"address":[],"length":0,"stats":{"Line":20}},{"line":227,"address":[],"length":0,"stats":{"Line":40}},{"line":228,"address":[],"length":0,"stats":{"Line":18}},{"line":230,"address":[],"length":0,"stats":{"Line":2}},{"line":235,"address":[],"length":0,"stats":{"Line":12}},{"line":236,"address":[],"length":0,"stats":{"Line":24}},{"line":240,"address":[],"length":0,"stats":{"Line":32}},{"line":241,"address":[],"length":0,"stats":{"Line":32}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":12}},{"line":247,"address":[],"length":0,"stats":{"Line":4}},{"line":248,"address":[],"length":0,"stats":{"Line":4}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":255,"address":[],"length":0,"stats":{"Line":10}},{"line":256,"address":[],"length":0,"stats":{"Line":30}},{"line":260,"address":[],"length":0,"stats":{"Line":29}},{"line":261,"address":[],"length":0,"stats":{"Line":29}},{"line":262,"address":[],"length":0,"stats":{"Line":116}},{"line":263,"address":[],"length":0,"stats":{"Line":0}},{"line":268,"address":[],"length":0,"stats":{"Line":27}},{"line":269,"address":[],"length":0,"stats":{"Line":27}},{"line":270,"address":[],"length":0,"stats":{"Line":108}},{"line":271,"address":[],"length":0,"stats":{"Line":0}},{"line":276,"address":[],"length":0,"stats":{"Line":19}},{"line":277,"address":[],"length":0,"stats":{"Line":19}},{"line":278,"address":[],"length":0,"stats":{"Line":19}},{"line":279,"address":[],"length":0,"stats":{"Line":76}},{"line":281,"address":[],"length":0,"stats":{"Line":0}},{"line":292,"address":[],"length":0,"stats":{"Line":3}},{"line":293,"address":[],"length":0,"stats":{"Line":3}},{"line":297,"address":[],"length":0,"stats":{"Line":17}},{"line":298,"address":[],"length":0,"stats":{"Line":17}},{"line":302,"address":[],"length":0,"stats":{"Line":18}},{"line":303,"address":[],"length":0,"stats":{"Line":18}},{"line":313,"address":[],"length":0,"stats":{"Line":71}},{"line":314,"address":[],"length":0,"stats":{"Line":213}},{"line":315,"address":[],"length":0,"stats":{"Line":213}},{"line":316,"address":[],"length":0,"stats":{"Line":213}},{"line":331,"address":[],"length":0,"stats":{"Line":2}},{"line":332,"address":[],"length":0,"stats":{"Line":2}},{"line":333,"address":[],"length":0,"stats":{"Line":1}},{"line":335,"address":[],"length":0,"stats":{"Line":3}},{"line":336,"address":[],"length":0,"stats":{"Line":1}},{"line":337,"address":[],"length":0,"stats":{"Line":2}},{"line":338,"address":[],"length":0,"stats":{"Line":1}},{"line":340,"address":[],"length":0,"stats":{"Line":0}},{"line":344,"address":[],"length":0,"stats":{"Line":9}},{"line":345,"address":[],"length":0,"stats":{"Line":61}},{"line":353,"address":[],"length":0,"stats":{"Line":5}},{"line":354,"address":[],"length":0,"stats":{"Line":15}},{"line":355,"address":[],"length":0,"stats":{"Line":2}},{"line":357,"address":[],"length":0,"stats":{"Line":15}},{"line":358,"address":[],"length":0,"stats":{"Line":9}},{"line":359,"address":[],"length":0,"stats":{"Line":9}},{"line":366,"address":[],"length":0,"stats":{"Line":4}},{"line":367,"address":[],"length":0,"stats":{"Line":12}},{"line":368,"address":[],"length":0,"stats":{"Line":1}},{"line":370,"address":[],"length":0,"stats":{"Line":15}},{"line":371,"address":[],"length":0,"stats":{"Line":9}},{"line":372,"address":[],"length":0,"stats":{"Line":12}},{"line":373,"address":[],"length":0,"stats":{"Line":3}},{"line":381,"address":[],"length":0,"stats":{"Line":43}},{"line":382,"address":[],"length":0,"stats":{"Line":129}},{"line":383,"address":[],"length":0,"stats":{"Line":43}}],"covered":125,"coverable":139},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","framemanager.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::FrameManager` — a pool of reusable [`crate::frame::Frame`]\n//! buffers plus a background garbage-collection thread.\n//!\n//! Mirrors `src/codec/src/framemanager.h`. The C++ manager kept a pool of\n//! `std::list\u003cFramePtr\u003e` and a QThread that periodically dropped frames\n//! whose last reference died. Rust keeps the same contract behind a\n//! `Mutex`; the background thread is replaced by a dedicated GC thread\n//! that drains the pool of freeable frames.\n\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_equals, OakVideoParams,\n};\nuse crate::frame::Frame;\n\n/// `olive::FrameManager`: singleton frame pool with background GC.\npub struct FrameManager {\n\t/// Pooled frames waiting for reuse (most-recently-freed first).\n\tpool: Mutex\u003cVec\u003cFrame\u003e\u003e,\n\t/// Peak number of live frames observed (diagnostics).\n\tpeak_count: Mutex\u003cusize\u003e,\n\t/// Current number of frames outstanding (not yet returned).\n\toutstanding: Mutex\u003cusize\u003e,\n}\n\nimpl FrameManager {\n\t/// The process-wide FrameManager singleton.\n\t///\n\t/// Constructs the manager on first use and spawns the background\n\t/// garbage-collection thread exactly once.\n\tpub fn instance() -\u003e \u0026'static FrameManager {\n\t\tstatic INSTANCE: OnceLock\u003cFrameManager\u003e = OnceLock::new();\n\t\tlet mgr = INSTANCE.get_or_init(FrameManager::new);\n\t\t// Spawn the GC thread on first construction only. We use a `static`\n\t\t// flag guarded by the same lock-free path: the first caller to build\n\t\t// the manager also starts the thread. Subsequent calls skip it.\n\t\tspawn_gc_thread_once(mgr);\n\t\tmgr\n\t}\n\n\t/// Create the empty manager.\n\tfn new() -\u003e Self {\n\t\tFrameManager {\n\t\t\tpool: Mutex::new(Vec::new()),\n\t\t\tpeak_count: Mutex::new(0),\n\t\t\toutstanding: Mutex::new(0),\n\t\t}\n\t}\n\n\t/// Clear the pool (dropping all cached frames).\n\tpub fn clear(\u0026self) {\n\t\tself.pool.lock().unwrap().clear();\n\t}\n\n\t/// Create a frame with the given params (borrowed from the pool when a\n\t/// compatible free frame exists, else freshly allocated).\n\tpub fn create_frame(\u0026self, params: OakVideoParams) -\u003e Arc\u003cFrame\u003e {\n\t\tlet frame = {\n\t\t\tlet mut pool = self.pool.lock().unwrap();\n\t\t\tmatch pool\n\t\t\t\t.iter()\n\t\t\t\t.position(|f| frame_matches(f, \u0026params))\n\t\t\t{\n\t\t\t\tSome(idx) =\u003e pool.swap_remove(idx),\n\t\t\t\tNone =\u003e Frame::with_params(params),\n\t\t\t}\n\t\t};\n\n\t\tlet mut outstanding = self.outstanding.lock().unwrap();\n\t\t*outstanding += 1;\n\t\tlet mut peak = self.peak_count.lock().unwrap();\n\t\tif *outstanding \u003e *peak {\n\t\t\t*peak = *outstanding;\n\t\t}\n\n\t\tArc::new(frame)\n\t}\n\n\t/// Return a frame to the pool for reuse.\n\tpub fn return_frame(\u0026self, frame: Frame) {\n\t\tlet mut outstanding = self.outstanding.lock().unwrap();\n\t\t*outstanding = outstanding.saturating_sub(1);\n\t\tself.pool.lock().unwrap().push(frame);\n\t}\n\n\t/// Number of frames currently outstanding (not in the pool).\n\tpub fn live_count(\u0026self) -\u003e usize {\n\t\t*self.outstanding.lock().unwrap()\n\t}\n\n\t/// Peak number of live frames observed.\n\tpub fn peak_count(\u0026self) -\u003e usize {\n\t\t*self.peak_count.lock().unwrap()\n\t}\n\n\t/// Background GC loop; runs on the manager's dedicated thread.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/framemanager.cpp` `run()` collected frames whose last\n\t/// reference had died, based on per-frame timestamps. The Rust skeleton\n\t/// keeps a pool of reusable buffers but no per-frame age, so the GC\n\t/// simply drains the whole pool. This bounds memory: frames are reused\n\t/// between GC passes and released once every GC period, which matches\n\t/// the C++ manager's intent of keeping pool memory from growing\n\t/// unbounded.\n\tfn gc_loop(\u0026self) {\n\t\tself.clear();\n\t}\n}\n\n/// Spawn the GC thread once for the process.\nfn spawn_gc_thread_once(mgr: \u0026'static FrameManager) {\n\tstatic STARTED: OnceLock\u003c()\u003e = OnceLock::new();\n\tSTARTED.get_or_init(|| {\n\t\tthread::spawn(move || {\n\t\t\t// `mgr` is `'static`; the thread may outlive every other\n\t\t\t// reference. Keep polling until the process exits.\n\t\t\tloop {\n\t\t\t\tthread::sleep(Duration::from_millis(5000));\n\t\t\t\tmgr.gc_loop();\n\t\t\t}\n\t\t});\n\t});\n}\n\n/// True when `frame` carries params equal to `params`.\nfn frame_matches(frame: \u0026Frame, params: \u0026OakVideoParams) -\u003e bool {\n\tlet Some(frame_params) = frame.params() else {\n\t\treturn false;\n\t};\n\tlet eq = unsafe {\n\t\toakcommon_videoparams_equals(frame_params.clone(), params.clone())\n\t};\n\teq != 0\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\n\t#[test]\n\tfn create_and_return_tracks_counts() {\n\t\tlet mgr = FrameManager::new();\n\t\tassert_eq!(mgr.live_count(), 0);\n\t\tassert_eq!(mgr.peak_count(), 0);\n\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet frame = mgr.create_frame(params);\n\t\tassert_eq!(mgr.live_count(), 1);\n\t\tassert_eq!(mgr.peak_count(), 1);\n\n\t\t// Return by unwrapping the single strong reference.\n\t\tlet frame = Arc::try_unwrap(frame).unwrap();\n\t\tmgr.return_frame(frame);\n\t\tassert_eq!(mgr.live_count(), 0);\n\t\tassert_eq!(mgr.peak_count(), 1);\n\t}\n\n\t#[test]\n\tfn pool_reuses_compatible_frames() {\n\t\tlet mgr = FrameManager::new();\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet f1 = mgr.create_frame(params);\n\t\tmgr.return_frame(Arc::try_unwrap(f1).unwrap());\n\t\tassert_eq!(mgr.live_count(), 0);\n\n\t\t// A compatible request reuses the pooled buffer rather than\n\t\t// allocating a new one.\n\t\tlet f2 = mgr.create_frame(unsafe {\n\t\t\toakcommon_videoparams_init_basic(64, 64)\n\t\t});\n\t\tassert_eq!(mgr.live_count(), 1);\n\t\tassert_eq!(mgr.peak_count(), 1);\n\t\tArc::try_unwrap(f2).unwrap();\n\t}\n\n\t#[test]\n\tfn peak_count_tracks_maximum() {\n\t\tlet mgr = FrameManager::new();\n\t\tlet p1 = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet p2 = unsafe { oakcommon_videoparams_init_basic(128, 128) };\n\t\tlet a = mgr.create_frame(p1);\n\t\tlet b = mgr.create_frame(p2);\n\t\tassert_eq!(mgr.live_count(), 2);\n\t\tassert_eq!(mgr.peak_count(), 2);\n\t\tmgr.return_frame(Arc::try_unwrap(a).unwrap());\n\t\tassert_eq!(mgr.live_count(), 1);\n\t\tassert_eq!(mgr.peak_count(), 2);\n\t\tArc::try_unwrap(b).unwrap();\n\t}\n\n\t#[test]\n\tfn clear_drops_pooled_frames() {\n\t\tlet mgr = FrameManager::new();\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet f = mgr.create_frame(params);\n\t\tmgr.return_frame(Arc::try_unwrap(f).unwrap());\n\t\tassert_eq!(mgr.pool.lock().unwrap().len(), 1);\n\n\t\tmgr.clear();\n\t\tassert_eq!(mgr.pool.lock().unwrap().len(), 0);\n\t}\n}\n","traces":[{"line":50,"address":[],"length":0,"stats":{"Line":0}},{"line":52,"address":[],"length":0,"stats":{"Line":0}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":4}},{"line":63,"address":[],"length":0,"stats":{"Line":12}},{"line":64,"address":[],"length":0,"stats":{"Line":4}},{"line":65,"address":[],"length":0,"stats":{"Line":4}},{"line":70,"address":[],"length":0,"stats":{"Line":1}},{"line":71,"address":[],"length":0,"stats":{"Line":2}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":6}},{"line":78,"address":[],"length":0,"stats":{"Line":24}},{"line":79,"address":[],"length":0,"stats":{"Line":6}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":9}},{"line":83,"address":[],"length":0,"stats":{"Line":3}},{"line":84,"address":[],"length":0,"stats":{"Line":10}},{"line":88,"address":[],"length":0,"stats":{"Line":24}},{"line":89,"address":[],"length":0,"stats":{"Line":6}},{"line":90,"address":[],"length":0,"stats":{"Line":24}},{"line":91,"address":[],"length":0,"stats":{"Line":11}},{"line":92,"address":[],"length":0,"stats":{"Line":5}},{"line":95,"address":[],"length":0,"stats":{"Line":12}},{"line":99,"address":[],"length":0,"stats":{"Line":4}},{"line":100,"address":[],"length":0,"stats":{"Line":16}},{"line":101,"address":[],"length":0,"stats":{"Line":8}},{"line":102,"address":[],"length":0,"stats":{"Line":12}},{"line":106,"address":[],"length":0,"stats":{"Line":7}},{"line":107,"address":[],"length":0,"stats":{"Line":14}},{"line":111,"address":[],"length":0,"stats":{"Line":6}},{"line":112,"address":[],"length":0,"stats":{"Line":12}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":133,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":2}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":5}},{"line":153,"address":[],"length":0,"stats":{"Line":1}}],"covered":32,"coverable":45},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","handle.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Refcounted-handle scaffolding. Same pattern as the oaknode/oakplugin\n//! crates (`src/node/rust/src/handle.rs`); intentionally duplicated rather\n//! than shared — each module DLL must run its own addref/release code\n//! (the function pointers in a handle always point into the DLL that\n//! created the object).\n\nuse std::panic::{catch_unwind, AssertUnwindSafe};\nuse std::ptr;\nuse std::sync::atomic::{AtomicI32, AtomicU32, Ordering};\n\nuse crate::error::{self, OAKCODEC_E_FAILED};\n\n/// Number of boxed handle objects currently alive (leak/debug checking).\n///\n/// Mirrors `oakcodec::g_alive_count` in `src/codec/c_api/frame.cpp`: every\n/// `make_owned` box increments it and `box_release` decrements it when the\n/// last reference drops. `oakcodec_debug_alive_count` reports it.\nstatic ALIVE: AtomicI32 = AtomicI32::new(0);\n\n/// ABI version stamped into every handle.\npub const OAKCODEC_ABI_VERSION: u32 = 1;\n\n/// Heap box behind a handle's `ctx`.\npub struct RefBox\u003cT: ?Sized\u003e {\n\t/// Atomic reference count.\n\tpub refs: AtomicU32,\n\t/// Boxed value.\n\tpub value: T,\n}\n\n/// `#[repr(C)]` mirror of the public handle structs\n/// (`{ctx, addref, release, abi_version}`).\n///\n/// Handles are `Copy`: passing one by value copies the struct, not the\n/// reference count — exactly the by-value convention the C ABI documents.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct CHandle {\n\t/// Opaque box pointer.\n\tpub ctx: *mut std::ffi::c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut std::ffi::c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut std::ffi::c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\nimpl CHandle {\n\t/// The empty handle.\n\tpub fn null() -\u003e Self {\n\t\tCHandle {\n\t\t\tctx: ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\t/// Whether this is an empty (null) handle.\n\tpub fn is_null(\u0026self) -\u003e bool {\n\t\tself.ctx.is_null()\n\t}\n}\n\n/// Increment the reference count of a boxed `RefBox\u003cT\u003e`.\n///\n/// # Safety\n/// `ptr` must point to a live `RefBox\u003cT\u003e` previously created by this module.\nunsafe extern \"C\" fn box_addref\u003cT: Send + 'static\u003e(ptr: *mut std::ffi::c_void) {\n\tif ptr.is_null() {\n\t\treturn;\n\t}\n\tlet boxed = unsafe { \u0026*(ptr as *const RefBox\u003cT\u003e) };\n\tboxed.refs.fetch_add(1, Ordering::SeqCst);\n}\n\n/// Decrement the reference count; destroys the box at zero.\n///\n/// # Safety\n/// `ptr` must point to a live `RefBox\u003cT\u003e` previously created by this module.\nunsafe extern \"C\" fn box_release\u003cT: Send + 'static\u003e(ptr: *mut std::ffi::c_void) {\n\tif ptr.is_null() {\n\t\treturn;\n\t}\n\tlet boxed = unsafe { \u0026*(ptr as *const RefBox\u003cT\u003e) };\n\tif boxed.refs.fetch_sub(1, Ordering::SeqCst) == 1 {\n\t\t// The last reference: the box is destroyed and the alive count\n\t\t// drops with it (mirrors `alive_dec` in c_api/frame.cpp).\n\t\tALIVE.fetch_sub(1, Ordering::SeqCst);\n\t\tunsafe { drop(Box::from_raw(ptr as *mut RefBox\u003cT\u003e)) };\n\t}\n}\n\n/// Owned handle with count 1; empty on allocation failure.\npub fn make_owned\u003cT: Send + 'static\u003e(value: T) -\u003e CHandle {\n\tlet boxed = Box::new(RefBox {\n\t\trefs: AtomicU32::new(1),\n\t\tvalue,\n\t});\n\tlet ctx = Box::into_raw(boxed) as *mut std::ffi::c_void;\n\t// Every boxed handle counts toward `oakcodec_debug_alive_count`\n\t// (mirrors `alive_inc` in c_api/frame.cpp).\n\tALIVE.fetch_add(1, Ordering::SeqCst);\n\tCHandle {\n\t\tctx,\n\t\taddref: Some(box_addref::\u003cT\u003e),\n\t\trelease: Some(box_release::\u003cT\u003e),\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\n/// Borrowed handle for an object owned elsewhere.\n///\n/// Takes ownership of the boxed `T` already allocated at `ptr` (e.g. one\n/// passed in from C++). The resulting handle's release drops that box.\n///\n/// # Safety\n/// Caller guarantees `ptr` was allocated with `Box::new` and is not used\n/// after this call.\npub unsafe fn make_borrowed\u003cT: Send + 'static\u003e(ptr: *mut T) -\u003e CHandle {\n\tif ptr.is_null() {\n\t\treturn CHandle::null();\n\t}\n\t// Move ownership into a RefBox so addref/release and get() behave\n\t// uniformly with owned handles.\n\tlet value = unsafe { *Box::from_raw(ptr) };\n\tmake_owned(value)\n}\n\n/// Typed view into a handle; `None` for empty handles.\n///\n/// # Safety\n/// `T` must be the boxed type.\npub unsafe fn get\u003cT: 'static\u003e(h: \u0026CHandle) -\u003e Option\u003c\u0026T\u003e {\n\tif h.is_null() {\n\t\treturn None;\n\t}\n\tlet boxed = unsafe { \u0026*(h.ctx as *const RefBox\u003cT\u003e) };\n\tSome(\u0026boxed.value)\n}\n\n/// Panic-catching FFI wrapper for i32-returning exports.\npub fn guard\u003cF: FnOnce() -\u003e error::Result\u003c()\u003e\u003e(f: F) -\u003e i32 {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(Ok(())) =\u003e error::OAKCODEC_OK,\n\t\tOk(Err(e)) =\u003e e.code(),\n\t\tErr(_) =\u003e OAKCODEC_E_FAILED,\n\t}\n}\n\n/// Panic-catching FFI wrapper for handle-returning exports.\npub fn guard_handle\u003cF: FnOnce() -\u003e error::Result\u003cCHandle\u003e\u003e(f: F) -\u003e CHandle {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(Ok(h)) =\u003e h,\n\t\tOk(Err(_)) | Err(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// Panic-catching FFI wrapper for void exports.\npub fn guard_void\u003cF: FnOnce()\u003e(f: F) {\n\tlet _ = catch_unwind(AssertUnwindSafe(f));\n}\n\n/// Panic-catching FFI wrapper for exports that return a raw `i32` code\n/// directly (neither `Result` nor a handle). On panic, `OAKCODEC_E_FAILED`.\npub fn guard_raw\u003cF: FnOnce() -\u003e i32\u003e(f: F) -\u003e i32 {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(code) =\u003e code,\n\t\tErr(_) =\u003e OAKCODEC_E_FAILED,\n\t}\n}\n\n/// Panic-catching FFI wrapper for exports that return a raw `i64` directly\n/// (e.g. `oakcodec_decoder_get_image_sequence_index`). On panic,\n/// `OAKCODEC_E_FAILED`.\npub fn guard_i64\u003cF: FnOnce() -\u003e i64\u003e(f: F) -\u003e i64 {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(v) =\u003e v,\n\t\tErr(_) =\u003e OAKCODEC_E_FAILED as i64,\n\t}\n}\n\n/// Number of live boxed handle objects (see [`ALIVE`]).\npub fn alive_count() -\u003e i32 {\n\tALIVE.load(Ordering::SeqCst)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::error::OAKCODEC_E_INVALID;\n\n\t#[test]\n\tfn make_owned_lifecycle_tracks_alive_count() {\n\t\t// The shared ffi test lock serializes the crate's `alive_count`\n\t\t// assertions against every other test that creates handles.\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = alive_count();\n\t\tlet h = make_owned(42u32);\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(alive_count(), before + 1);\n\n\t\t// addref/release cycle keeps the box alive.\n\t\tlet addref = h.addref.unwrap();\n\t\tlet release = h.release.unwrap();\n\t\t// SAFETY: `h.ctx` is a live RefBox\u003cu32\u003e.\n\t\tunsafe { addref(h.ctx) };\n\t\t// SAFETY: second reference released; box stays (refs 2 -\u003e 1).\n\t\tunsafe { release(h.ctx) };\n\t\tassert_eq!(alive_count(), before + 1);\n\n\t\t// Release the owned reference: box destroyed.\n\t\t// SAFETY: last reference.\n\t\tunsafe { release(h.ctx) };\n\t\tassert_eq!(alive_count(), before);\n\t}\n\n\t#[test]\n\tfn make_borrowed_null_is_null_handle() {\n\t\tlet h = unsafe { make_borrowed::\u003cu32\u003e(std::ptr::null_mut()) };\n\t\tassert!(h.is_null());\n\t}\n\n\t#[test]\n\tfn make_borrowed_takes_ownership() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = alive_count();\n\t\tlet raw = Box::into_raw(Box::new(7u32));\n\t\tlet h = unsafe { make_borrowed(raw) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(alive_count(), before + 1);\n\t\tassert_eq!(unsafe { *get::\u003cu32\u003e(\u0026h).unwrap() }, 7);\n\t\tunsafe { h.release.unwrap()(h.ctx) };\n\t\tassert_eq!(alive_count(), before);\n\t}\n\n\t#[test]\n\tfn addref_on_null_ctx_is_noop() {\n\t\t// A handle with function pointers but a null ctx: both thunks no-op.\n\t\tlet h = CHandle {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: Some(box_addref::\u003cu32\u003e),\n\t\t\trelease: Some(box_release::\u003cu32\u003e),\n\t\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t\t};\n\t\t// SAFETY: ctx is null; the thunks guard on it.\n\t\tunsafe { h.addref.unwrap()(h.ctx) };\n\t\t// SAFETY: ctx is null; the thunks guard on it.\n\t\tunsafe { h.release.unwrap()(h.ctx) };\n\t}\n\n\t#[test]\n\tfn guard_maps_results_and_panics() {\n\t\tassert_eq!(guard(|| Ok(())), crate::error::OAKCODEC_OK);\n\t\tassert_eq!(guard(|| Err(crate::error::Error::Invalid)), OAKCODEC_E_INVALID);\n\t\tassert_eq!(guard(|| panic!(\"boom\")), crate::error::OAKCODEC_E_FAILED);\n\n\t\tlet ok = guard_handle(|| Ok(make_owned(1u32)));\n\t\tassert!(!ok.is_null());\n\t\tassert!(guard_handle(|| Err::\u003cCHandle, _\u003e(crate::error::Error::Invalid)).is_null());\n\t\tassert!(guard_handle(|| panic!(\"boom\")).is_null());\n\n\t\tassert_eq!(guard_raw(|| 5), 5);\n\t\tassert_eq!(guard_raw(|| panic!(\"boom\")), crate::error::OAKCODEC_E_FAILED);\n\n\t\tassert_eq!(guard_i64(|| 5), 5);\n\t\tassert_eq!(guard_i64(|| panic!(\"boom\")), crate::error::OAKCODEC_E_FAILED as i64);\n\n\t\tlet mut called = false;\n\t\tguard_void(|| called = true);\n\t\tassert!(called);\n\t\tguard_void(|| panic!(\"boom\"));\n\t}\n\n\t#[test]\n\tfn null_handle_helpers() {\n\t\tlet h = CHandle::null();\n\t\tassert!(h.is_null());\n\t\tassert_eq!(h.abi_version, OAKCODEC_ABI_VERSION);\n\t}\n}\n","traces":[{"line":67,"address":[],"length":0,"stats":{"Line":17}},{"line":69,"address":[],"length":0,"stats":{"Line":34}},{"line":77,"address":[],"length":0,"stats":{"Line":147}},{"line":78,"address":[],"length":0,"stats":{"Line":294}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":4}},{"line":88,"address":[],"length":0,"stats":{"Line":1}},{"line":90,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":3}},{"line":98,"address":[],"length":0,"stats":{"Line":26}},{"line":99,"address":[],"length":0,"stats":{"Line":52}},{"line":100,"address":[],"length":0,"stats":{"Line":1}},{"line":102,"address":[],"length":0,"stats":{"Line":50}},{"line":103,"address":[],"length":0,"stats":{"Line":74}},{"line":106,"address":[],"length":0,"stats":{"Line":96}},{"line":107,"address":[],"length":0,"stats":{"Line":48}},{"line":112,"address":[],"length":0,"stats":{"Line":25}},{"line":113,"address":[],"length":0,"stats":{"Line":75}},{"line":114,"address":[],"length":0,"stats":{"Line":25}},{"line":115,"address":[],"length":0,"stats":{"Line":25}},{"line":117,"address":[],"length":0,"stats":{"Line":50}},{"line":120,"address":[],"length":0,"stats":{"Line":75}},{"line":123,"address":[],"length":0,"stats":{"Line":25}},{"line":124,"address":[],"length":0,"stats":{"Line":25}},{"line":137,"address":[],"length":0,"stats":{"Line":2}},{"line":138,"address":[],"length":0,"stats":{"Line":4}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":144,"address":[],"length":0,"stats":{"Line":2}},{"line":151,"address":[],"length":0,"stats":{"Line":117}},{"line":152,"address":[],"length":0,"stats":{"Line":234}},{"line":153,"address":[],"length":0,"stats":{"Line":14}},{"line":155,"address":[],"length":0,"stats":{"Line":206}},{"line":156,"address":[],"length":0,"stats":{"Line":103}},{"line":160,"address":[],"length":0,"stats":{"Line":71}},{"line":161,"address":[],"length":0,"stats":{"Line":142}},{"line":162,"address":[],"length":0,"stats":{"Line":34}},{"line":163,"address":[],"length":0,"stats":{"Line":108}},{"line":164,"address":[],"length":0,"stats":{"Line":1}},{"line":169,"address":[],"length":0,"stats":{"Line":32}},{"line":170,"address":[],"length":0,"stats":{"Line":64}},{"line":171,"address":[],"length":0,"stats":{"Line":60}},{"line":172,"address":[],"length":0,"stats":{"Line":2}},{"line":177,"address":[],"length":0,"stats":{"Line":28}},{"line":178,"address":[],"length":0,"stats":{"Line":28}},{"line":183,"address":[],"length":0,"stats":{"Line":94}},{"line":184,"address":[],"length":0,"stats":{"Line":94}},{"line":185,"address":[],"length":0,"stats":{"Line":186}},{"line":186,"address":[],"length":0,"stats":{"Line":1}},{"line":193,"address":[],"length":0,"stats":{"Line":4}},{"line":194,"address":[],"length":0,"stats":{"Line":4}},{"line":195,"address":[],"length":0,"stats":{"Line":6}},{"line":196,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":25}},{"line":202,"address":[],"length":0,"stats":{"Line":75}}],"covered":55,"coverable":55},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","lib.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! # oakcodec — the media codec module (Rust)\n//!\n//! Reimplements the C++ oakcodec module behind its frozen C ABI\n//! (`include/codec/*.h`). See README.md for the architectural mapping\n//! (inheritance → traits, shared_ptr → refcounted handles, etc.).\n//!\n//! ## FFI discipline\n//!\n//! Identical to the oaknode/oakplugin crates: every export goes through\n//! [`handle::guard*`], handles are opaque refcounted boxes, shared\n//! state behind `Mutex`.\n\n#![deny(unsafe_op_in_unsafe_fn)]\n#![warn(missing_docs)]\n\npub mod bridge;\npub mod conformmanager;\npub mod decoder;\npub mod encoder;\npub mod encodingparams;\npub mod error;\npub mod exportcodec;\npub mod exportformat;\npub mod ffmpeg;\npub mod ffi;\npub mod footagedescription;\npub mod frame;\npub mod framemanager;\npub mod handle;\npub mod oiio;\npub mod oiioframebridge;\npub mod planarfiledevice;\npub mod proxymanager;\npub mod task;\npub mod timecodemetadata;\n\n#[cfg(test)]\nmod realmedia_tests;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","oiio.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `OIIODecoder` / `OIIOEncoder` — the OpenImageIO-backed implementations.\n//!\n//! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO\n//! frame conversion goes through the local\n//! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping\n//! functions (`oakcommon_oiioutils_*` via `bridge/common.rs`).\n//!\n//! The OIIO dylib (`liboakoiio`) is not linked into this build, so every\n//! operation that would touch the media engine is a documented stub returning\n//! [`crate::error::Error::Failed`]; only bookkeeping that keeps the\n//! decoder/encoder safe to use when closed succeeds. The stream is still\n//! recorded on [`Decoder::open`] so [`Decoder::stream`] reflects the target,\n//! and [`Decoder::close`] / [`Encoder::close`] are no-ops.\n\nuse crate::decoder::{CodecStream, Decoder, RetrieveVideoParams};\nuse crate::encoder::Encoder;\nuse crate::encodingparams::EncodingParams;\n\n/// `olive::OIIODecoder` — OpenImageIO-backed media decoder (still images).\npub struct OIIODecoder {\n\t/// Opened stream (locked).\n\tstream: std::sync::Mutex\u003cOption\u003cCodecStream\u003e\u003e,\n}\n\nimpl OIIODecoder {\n\t/// Error returned for operations that need the missing `liboakoiio`.\n\tconst NOT_AVAILABLE: \u0026'static str =\n\t\t\"OIIO decoding is not available in this build (needs liboakoiio dylib)\";\n}\n\nimpl Decoder for OIIODecoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"oiio\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn probe(\n\t\t\u0026self,\n\t\t_filename: \u0026str,\n\t\t_cancelled: Option\u003c\u0026crate::bridge::render::OakCancelAtom\u003e,\n\t) -\u003e Option\u003ccrate::footagedescription::FootageDescription\u003e {\n\t\t// Probing is a dylib operation; without it we cannot report anything.\n\t\tNone\n\t}\n\n\tfn open(\u0026self, stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// Record the requested stream so `stream()` reflects the open target,\n\t\t// but actual decoding is unavailable without the dylib.\n\t\t*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = Some(stream.clone());\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// Trait contract: \"safe when closed\". Clear the opened stream and\n\t\t// no-op; nothing was ever decoded.\n\t\t*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = None;\n\t\tOk(())\n\t}\n\n\tfn stream(\u0026self) -\u003e CodecStream {\n\t\tself.stream\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.clone()\n\t\t\t.unwrap_or_else(CodecStream::new)\n\t}\n\n\tfn retrieve_video_frame(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cstd::sync::Arc\u003ccrate::frame::Frame\u003e\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn retrieve_video(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003ccrate::bridge::render::OakRenderTexture\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\t_dest: \u0026mut [f32],\n\t\t_range: \u0026oakcore_rs::TimeRange,\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t) -\u003e crate::error::Result\u003ccrate::decoder::RetrieveAudioStatus\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn conform_audio(\n\t\t\u0026self,\n\t\t_output_filenames: \u0026[String],\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t\t_sample_format: i32,\n\t\t_cancelled: Option\u003c\u0026crate::bridge::render::OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n}\n\n/// `olive::OIIOEncoder` — OpenImageIO-backed media encoder (still images).\npub struct OIIOEncoder {\n\t/// The encoding parameters this encoder was configured with.\n\tpub params: EncodingParams,\n}\n\nimpl Encoder for OIIOEncoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"oiio\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_image_sequences(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn is_configurable(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn configure(\u0026self, _params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// `configure` writes `self.params`, which an `\u0026self` receiver cannot\n\t\t// do, and real encoding needs the dylib anyway.\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// `Encoder::close` is documented idempotent; with no encoder opened\n\t\t// there is nothing to release.\n\t\tOk(())\n\t}\n\n\tfn write_video(\u0026self, _frame: \u0026crate::frame::Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn write_audio(\u0026self, _samples: \u0026[f32], _frame_count: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn write_subtitle(\n\t\t\u0026self,\n\t\t_text: \u0026str,\n\t\t_in_seconds: f64,\n\t\t_out_seconds: f64,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn desired_pixel_format(\u0026self) -\u003e Option\u003coakcore_rs::PixelFormat\u003e {\n\t\tNone\n\t}\n\n\tfn desired_sample_format(\u0026self) -\u003e Option\u003coakcore_rs::SampleFormat\u003e {\n\t\tNone\n\t}\n\n\tfn filename(\u0026self) -\u003e String {\n\t\tc_string_1024(\u0026self.params.filename)\n\t}\n}\n\n/// Read a NUL-terminated C string out of the `EncodingParams.filename` byte\n/// buffer, stopping at the first NUL (empty string when unset).\nfn c_string_1024(buf: \u0026[u8; 1024]) -\u003e String {\n\tlet end = buf.iter().position(|\u0026b| b == 0).unwrap_or(buf.len());\n\tString::from_utf8_lossy(\u0026buf[..end]).into_owned()\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::decoder::RetrieveVideoParams;\n\tuse crate::encodingparams::EncodingParams;\n\tuse oakcore_rs::{Rational, TimeRange};\n\n\tfn video_params() -\u003e RetrieveVideoParams {\n\t\tRetrieveVideoParams {\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(1, 30),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: crate::decoder::K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: crate::decoder::RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t}\n\t}\n\n\t#[test]\n\tfn oiio_decoder_identity_and_stub_operations() {\n\t\tlet d = OIIODecoder {\n\t\t\tstream: std::sync::Mutex::new(None),\n\t\t};\n\t\tassert_eq!(d.id(), \"oiio\");\n\t\t// Still images only: no audio support.\n\t\tassert!(!d.supports_video());\n\t\tassert!(!d.supports_audio());\n\t\tassert!(d.probe(\"any.png\", None).is_none());\n\n\t\tlet s = CodecStream::with_block(\"in.exr\".to_string(), 0, None);\n\t\tassert!(d.open(\u0026s).is_err());\n\t\tassert_eq!(d.stream().filename(), \"in.exr\");\n\t\tassert!(d.close().is_ok());\n\t\tassert_eq!(d.stream().filename(), \"\");\n\n\t\tassert!(d.retrieve_video_frame(\u0026video_params()).is_err());\n\t\tassert!(d.retrieve_video(\u0026video_params()).is_err());\n\t\tlet mut dest = [0f32; 8];\n\t\tassert!(d\n\t\t\t.retrieve_audio(\n\t\t\t\t\u0026mut dest,\n\t\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t\t48000,\n\t\t\t\t0x3\n\t\t\t)\n\t\t\t.is_err());\n\t\tassert!(d\n\t\t\t.conform_audio(\u0026[\"a.pcm\".to_string()], 48000, 0x3, 10, None)\n\t\t\t.is_err());\n\t}\n\n\t#[test]\n\tfn oiio_encoder_stub_behaviors() {\n\t\tlet mut params = EncodingParams::default();\n\t\tlet name = b\"out/img.exr\";\n\t\tparams.filename[..name.len()].copy_from_slice(name);\n\t\tlet e = OIIOEncoder { params };\n\n\t\tassert_eq!(e.id(), \"oiio\");\n\t\tassert!(!e.supports_video());\n\t\tassert!(!e.supports_audio());\n\t\tassert!(!e.supports_subtitles());\n\t\tassert!(e.supports_image_sequences());\n\t\tassert!(e.is_configurable());\n\t\tassert_eq!(e.filename(), \"out/img.exr\");\n\t\tassert_eq!(e.desired_pixel_format(), None);\n\t\tassert_eq!(e.get_error(), \"\");\n\n\t\tassert!(e.configure(\u0026EncodingParams::default()).is_err());\n\t\tassert!(e.open().is_err());\n\t\tassert!(e.close().is_ok());\n\t\tlet frame = crate::frame::Frame::new();\n\t\tassert!(e.write_video(\u0026frame).is_err());\n\t\tassert!(e.write_audio(\u0026[0f32; 4], 1).is_err());\n\t\tassert!(e.write_subtitle(\"hi\", 0.0, 1.0).is_err());\n\t\tassert!(e.flush().is_err());\n\t}\n}\n","traces":[{"line":48,"address":[],"length":0,"stats":{"Line":1}},{"line":49,"address":[],"length":0,"stats":{"Line":2}},{"line":52,"address":[],"length":0,"stats":{"Line":1}},{"line":53,"address":[],"length":0,"stats":{"Line":1}},{"line":56,"address":[],"length":0,"stats":{"Line":1}},{"line":57,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":1}},{"line":66,"address":[],"length":0,"stats":{"Line":1}},{"line":69,"address":[],"length":0,"stats":{"Line":1}},{"line":72,"address":[],"length":0,"stats":{"Line":4}},{"line":73,"address":[],"length":0,"stats":{"Line":1}},{"line":76,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":4}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":2}},{"line":84,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":1}},{"line":95,"address":[],"length":0,"stats":{"Line":1}},{"line":98,"address":[],"length":0,"stats":{"Line":1}},{"line":102,"address":[],"length":0,"stats":{"Line":1}},{"line":105,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":134,"address":[],"length":0,"stats":{"Line":4}},{"line":135,"address":[],"length":0,"stats":{"Line":8}},{"line":138,"address":[],"length":0,"stats":{"Line":1}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":142,"address":[],"length":0,"stats":{"Line":1}},{"line":143,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":150,"address":[],"length":0,"stats":{"Line":1}},{"line":151,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":1}},{"line":155,"address":[],"length":0,"stats":{"Line":1}},{"line":158,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":1}},{"line":162,"address":[],"length":0,"stats":{"Line":1}},{"line":166,"address":[],"length":0,"stats":{"Line":1}},{"line":167,"address":[],"length":0,"stats":{"Line":1}},{"line":168,"address":[],"length":0,"stats":{"Line":1}},{"line":172,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":1}},{"line":179,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":1}},{"line":185,"address":[],"length":0,"stats":{"Line":1}},{"line":186,"address":[],"length":0,"stats":{"Line":1}},{"line":190,"address":[],"length":0,"stats":{"Line":1}},{"line":196,"address":[],"length":0,"stats":{"Line":1}},{"line":197,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":202,"address":[],"length":0,"stats":{"Line":1}},{"line":203,"address":[],"length":0,"stats":{"Line":1}},{"line":207,"address":[],"length":0,"stats":{"Line":1}},{"line":208,"address":[],"length":0,"stats":{"Line":1}},{"line":211,"address":[],"length":0,"stats":{"Line":0}},{"line":212,"address":[],"length":0,"stats":{"Line":0}},{"line":215,"address":[],"length":0,"stats":{"Line":1}},{"line":216,"address":[],"length":0,"stats":{"Line":2}},{"line":222,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":18}},{"line":224,"address":[],"length":0,"stats":{"Line":3}}],"covered":65,"coverable":67},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","oiioframebridge.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `oiio_frame_to_buffer` / `oiio_buffer_to_frame` — codec-internal OIIO\n//! frame \u003c-\u003e pixel-buffer conversion.\n//!\n//! Mirrors `src/codec/src/oiioframebridge.{h,cpp}`. These are internal C++\n//! functions that moved into codec from oakcommon (NOTES.md §oakcommon侧修复);\n//! oakcommon keeps its OIIO mapping functions; the frame conversion itself\n//! lives here.\n//!\n//! The C++ bridge copies pixels through the live OpenImageIO `ImageBuf`\n//! (`oiio_frame_to_buffer`/`oiio_buffer_to_frame`). OIIO is not linked into\n//! this build, so the port serializes the frame into a self-describing byte\n//! buffer instead. The layout is stable (documented in\n//! [`OiioBufferHeader`]); it carries the frame's geometry, pixel format,\n//! timestamp and time base alongside the raw pixel rows, so a buffer can be\n//! turned back into an equivalent [`Frame`] without any external state.\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base,\n\toakcommon_videoparams_set_format,\n};\nuse crate::frame::Frame;\nuse oakcore_rs::Rational;\n\n/// Fixed header size, in bytes, of an OIIO frame buffer.\n///\n/// The layout is a 64-byte little-endian header followed by the raw pixel\n/// data. `OiioBufferHeader::to_bytes` / `from_bytes` are the single writer /\n/// reader of this header, so the exact offsets only ever exist in one place.\nconst HEADER_LEN: usize = 64;\n\n/// Magic bytes identifying an OIIO frame buffer (`\"OFMB\"`).\nconst MAGIC: \u0026[u8; 4] = b\"OFMB\";\n\n/// Current serialization version.\nconst VERSION: u32 = 1;\n\n/// Decoded OIIO frame-buffer header.\n///\n/// # Byte layout (little-endian, `HEADER_LEN` = 64 bytes)\n///\n/// | Offset | Size | Field |\n/// |--------|------|-------|\n/// | 0 | 4 | Magic bytes `\"OFMB\"` |\n/// | 4 | 4 | Serialization `version` (`u32`, currently 1) |\n/// | 8 | 4 | `width` (`i32`) |\n/// | 12 | 4 | `height` (`i32`) |\n/// | 16 | 4 | `format` (`i32`, an `OakPixelFormat` value) |\n/// | 20 | 4 | `linesize_bytes` (`i32`, distance between pixel rows) |\n/// | 24 | 8 | `timestamp_num` (`i64`) |\n/// | 32 | 8 | `timestamp_den` (`i64`) |\n/// | 40 | 8 | `time_base_num` (`i64`) |\n/// | 48 | 8 | `time_base_den` (`i64`) |\n/// | 56 | 8 | `pixel_len` (`u64`, pixel-data length in bytes) |\n/// | 64 | … | raw pixel data (`pixel_len` bytes) |\n///\n/// `pixel_len` must equal `linesize_bytes * height`; the frame geometry in the\n/// header must match the params the frame is reconstructed with.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nstruct OiioBufferHeader {\n\tversion: u32,\n\twidth: i32,\n\theight: i32,\n\tformat: i32,\n\tlinesize_bytes: i32,\n\ttimestamp_num: i64,\n\ttimestamp_den: i64,\n\ttime_base_num: i64,\n\ttime_base_den: i64,\n\tpixel_len: u64,\n}\n\nimpl OiioBufferHeader {\n\t/// Serialize the header into exactly `HEADER_LEN` bytes (little-endian).\n\tfn to_bytes(\u0026self) -\u003e [u8; HEADER_LEN] {\n\t\tlet mut b = [0u8; HEADER_LEN];\n\t\tb[0..4].copy_from_slice(MAGIC);\n\t\tb[4..8].copy_from_slice(\u0026self.version.to_le_bytes());\n\t\tb[8..12].copy_from_slice(\u0026self.width.to_le_bytes());\n\t\tb[12..16].copy_from_slice(\u0026self.height.to_le_bytes());\n\t\tb[16..20].copy_from_slice(\u0026self.format.to_le_bytes());\n\t\tb[20..24].copy_from_slice(\u0026self.linesize_bytes.to_le_bytes());\n\t\tb[24..32].copy_from_slice(\u0026self.timestamp_num.to_le_bytes());\n\t\tb[32..40].copy_from_slice(\u0026self.timestamp_den.to_le_bytes());\n\t\tb[40..48].copy_from_slice(\u0026self.time_base_num.to_le_bytes());\n\t\tb[48..56].copy_from_slice(\u0026self.time_base_den.to_le_bytes());\n\t\tb[56..64].copy_from_slice(\u0026self.pixel_len.to_le_bytes());\n\t\tb\n\t}\n\n\t/// Parse a header from the start of `bytes`, validating the magic and\n\t/// version and requiring at least `HEADER_LEN` bytes. Returns the decoded\n\t/// header and the number of bytes consumed (`HEADER_LEN`).\n\tfn from_bytes(bytes: \u0026[u8]) -\u003e crate::error::Result\u003c(Self, usize)\u003e {\n\t\tif bytes.len() \u003c HEADER_LEN {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tif \u0026bytes[0..4] != MAGIC {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());\n\t\tif version != VERSION {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion,\n\t\t\twidth: i32::from_le_bytes(bytes[8..12].try_into().unwrap()),\n\t\t\theight: i32::from_le_bytes(bytes[12..16].try_into().unwrap()),\n\t\t\tformat: i32::from_le_bytes(bytes[16..20].try_into().unwrap()),\n\t\t\tlinesize_bytes: i32::from_le_bytes(bytes[20..24].try_into().unwrap()),\n\t\t\ttimestamp_num: i64::from_le_bytes(bytes[24..32].try_into().unwrap()),\n\t\t\ttimestamp_den: i64::from_le_bytes(bytes[32..40].try_into().unwrap()),\n\t\t\ttime_base_num: i64::from_le_bytes(bytes[40..48].try_into().unwrap()),\n\t\t\ttime_base_den: i64::from_le_bytes(bytes[48..56].try_into().unwrap()),\n\t\t\tpixel_len: u64::from_le_bytes(bytes[56..64].try_into().unwrap()),\n\t\t};\n\t\tOk((header, HEADER_LEN))\n\t}\n}\n\n/// Convert an OIIO-backed `Frame` into a raw pixel buffer.\n///\n/// # CPP-PARITY\n/// `src/codec/src/oiioframebridge.cpp` `oiio_frame_to_buffer` —\n/// allocates the destination and copies the OIIO pixel data out.\n///\n/// The buffer is a 64-byte [`OiioBufferHeader`] followed by the frame's pixel\n/// rows (see the header docs for the exact layout). The frame must already be\n/// allocated ([`Frame::allocate`]); otherwise this returns\n/// [`crate::error::Error::State`].\npub fn oiio_frame_to_buffer(frame: \u0026Frame) -\u003e crate::error::Result\u003cVec\u003cu8\u003e\u003e {\n\tlet data = frame.data().ok_or(crate::error::Error::State)?;\n\tlet width = frame.width();\n\tlet height = frame.height();\n\tlet format = frame.format() as i32;\n\tlet linesize_bytes = frame.linesize_bytes();\n\tlet timestamp = frame.timestamp();\n\n\tlet (time_base_num, time_base_den) = match frame.params() {\n\t\tSome(p) =\u003e {\n\t\t\tlet mut num = 0i64;\n\t\t\tlet mut den = 0i64;\n\t\t\t// SAFETY: `num`/`den` are live mutable i64s and `p` is a valid\n\t\t\t// handle; the C function only writes through the two out pointers.\n\t\t\tunsafe {\n\t\t\t\toakcommon_videoparams_get_time_base(p.clone(), \u0026mut num, \u0026mut den);\n\t\t\t}\n\t\t\t(num, den)\n\t\t}\n\t\tNone =\u003e (0, 0),\n\t};\n\n\tlet header = OiioBufferHeader {\n\t\tversion: VERSION,\n\t\twidth,\n\t\theight,\n\t\tformat,\n\t\tlinesize_bytes,\n\t\ttimestamp_num: timestamp.numerator(),\n\t\ttimestamp_den: timestamp.denominator(),\n\t\ttime_base_num,\n\t\ttime_base_den,\n\t\tpixel_len: data.len() as u64,\n\t};\n\n\tlet mut out = Vec::with_capacity(HEADER_LEN + data.len());\n\tout.extend_from_slice(\u0026header.to_bytes());\n\tout.extend_from_slice(data);\n\tOk(out)\n}\n\n/// Convert a raw pixel buffer back into a `Frame`.\n///\n/// # CPP-PARITY\n/// `src/codec/src/oiioframebridge.cpp` `oiio_buffer_to_frame` —\n/// wraps the buffer bytes in a `Frame` for encoder consumption.\n///\n/// Parses a [`OiioBufferHeader`] from the front of `buffer`, reconstructs the\n/// frame's params (geometry, time base, pixel format) and timestamp, then\n/// copies the pixel rows into an allocated frame. The buffer is rejected with\n/// [`crate::error::Error::Invalid`] when the magic/version is wrong, the\n/// header is truncated, or `pixel_len` is inconsistent with the declared\n/// `linesize_bytes * height` / the bytes actually present.\npub fn oiio_buffer_to_frame(buffer: \u0026[u8]) -\u003e crate::error::Result\u003cFrame\u003e {\n\tlet (header, consumed) = OiioBufferHeader::from_bytes(buffer)?;\n\tlet pixels = \u0026buffer[consumed..];\n\n\t// `pixel_len` must be consistent with the header geometry and the bytes\n\t// actually present (checked, so a corrupt header can't panic later).\n\tif header.linesize_bytes \u003c 0 || header.height \u003c 0 {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\tlet expected =\n\t\t(header.linesize_bytes as u64).checked_mul(header.height as u64);\n\tif expected != Some(header.pixel_len) {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\tif pixels.len() != header.pixel_len as usize {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\n\t// Build the params from the header, then hand ownership to the frame.\n\t// SAFETY: the init returns a live handle; the clone for `set_format` is\n\t// only read, and the original is moved into `Frame::with_params` (which\n\t// takes ownership), so there is no double release.\n\tlet params = unsafe {\n\t\toakcommon_videoparams_init_with_time_base(\n\t\t\theader.width,\n\t\t\theader.height,\n\t\t\theader.time_base_num,\n\t\t\theader.time_base_den,\n\t\t)\n\t};\n\tunsafe {\n\t\toakcommon_videoparams_set_format(params.clone(), header.format);\n\t}\n\tlet mut frame = Frame::with_params(params);\n\tframe.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));\n\tframe.allocate()?;\n\n\tmatch frame.data_mut() {\n\t\tSome(dst) if dst.len() == pixels.len() =\u003e {\n\t\t\tdst.copy_from_slice(pixels);\n\t\t\tOk(frame)\n\t\t}\n\t\t// Reconstructed params should produce exactly `pixel_len` bytes; if\n\t\t// the geometry in the header disagreed with the format's line size,\n\t\t// refuse rather than copy a mismatched slice.\n\t\t_ =\u003e Err(crate::error::Error::Invalid),\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn header_round_trip() {\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 100,\n\t\t\theight: 50,\n\t\t\tformat: 0, // U8\n\t\t\tlinesize_bytes: 512,\n\t\t\ttimestamp_num: 5,\n\t\t\ttimestamp_den: 2,\n\t\t\ttime_base_num: 1,\n\t\t\ttime_base_den: 30,\n\t\t\tpixel_len: 25600,\n\t\t};\n\t\tlet bytes = header.to_bytes();\n\t\tassert_eq!(bytes.len(), HEADER_LEN);\n\t\tlet (decoded, consumed) = OiioBufferHeader::from_bytes(\u0026bytes).unwrap();\n\t\tassert_eq!(consumed, HEADER_LEN);\n\t\tassert_eq!(decoded, header);\n\t}\n\n\t#[test]\n\tfn header_is_little_endian_magic_and_version() {\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t};\n\t\tlet bytes = header.to_bytes();\n\t\tassert_eq!(\u0026bytes[0..4], b\"OFMB\");\n\t\tassert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), 1);\n\t\t// width = 1 -\u003e little-endian 01 00 00 00.\n\t\tassert_eq!(\u0026bytes[8..12], \u0026[1, 0, 0, 0]);\n\t}\n\n\t#[test]\n\tfn from_bytes_rejects_truncated() {\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t};\n\t\tlet bytes = header.to_bytes();\n\t\tassert!(OiioBufferHeader::from_bytes(\u0026bytes[..HEADER_LEN - 1]).is_err());\n\t}\n\n\t#[test]\n\tfn from_bytes_rejects_bad_magic_and_version() {\n\t\tlet mut bytes = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t}\n\t\t.to_bytes();\n\t\tbytes[0] = b'X';\n\t\tassert!(OiioBufferHeader::from_bytes(\u0026bytes).is_err());\n\n\t\tlet mut bytes = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t}\n\t\t.to_bytes();\n\t\tbytes[4..8].copy_from_slice(\u002699u32.to_le_bytes());\n\t\tassert!(OiioBufferHeader::from_bytes(\u0026bytes).is_err());\n\t}\n\n\t/// A small helper to build a fully allocated, filled frame using the same\n\t/// test-stub params pattern as `frame.rs`.\n\tfn make_frame() -\u003e Frame {\n\t\t// SAFETY: test-stub videoparams; ownership moves into `with_params`.\n\t\tlet params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30) };\n\t\tunsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8\n\t\tlet mut frame = Frame::with_params(params);\n\t\tframe.set_timestamp(Rational::new(5, 2));\n\t\tframe.allocate().unwrap();\n\t\tframe\n\t}\n\n\t#[test]\n\tfn frame_to_buffer_round_trip() {\n\t\tlet mut frame = make_frame();\n\t\tif let Some(d) = frame.data_mut() {\n\t\t\td.fill(0xAB);\n\t\t}\n\n\t\tlet buffer = oiio_frame_to_buffer(\u0026frame).unwrap();\n\t\tassert_eq!(buffer.len(), HEADER_LEN + frame.allocated_size());\n\n\t\tlet (header, consumed) = OiioBufferHeader::from_bytes(\u0026buffer).unwrap();\n\t\tassert_eq!(consumed, HEADER_LEN);\n\t\tassert_eq!(header.width, 100);\n\t\tassert_eq!(header.height, 50);\n\t\tassert_eq!(header.format, 0);\n\t\tassert_eq!(header.linesize_bytes, 512);\n\t\tassert_eq!(header.timestamp_num, 5);\n\t\tassert_eq!(header.timestamp_den, 2);\n\t\tassert_eq!(header.time_base_num, 1);\n\t\tassert_eq!(header.time_base_den, 30);\n\t\tassert_eq!(header.pixel_len, frame.allocated_size() as u64);\n\t}\n\n\t#[test]\n\tfn buffer_to_frame_round_trip() {\n\t\tlet mut frame = make_frame();\n\t\tlet expected_len = frame.allocated_size();\n\t\tif let Some(d) = frame.data_mut() {\n\t\t\tfor (i, b) in d.iter_mut().enumerate() {\n\t\t\t\t*b = (i % 256) as u8;\n\t\t\t}\n\t\t}\n\t\tlet buffer = oiio_frame_to_buffer(\u0026frame).unwrap();\n\n\t\tlet out = oiio_buffer_to_frame(\u0026buffer).unwrap();\n\t\tassert_eq!(out.width(), 100);\n\t\tassert_eq!(out.height(), 50);\n\t\tassert_eq!(out.format() as i32, 0);\n\t\tassert_eq!(out.allocated_size(), expected_len);\n\t\tassert_eq!(out.timestamp().numerator(), 5);\n\t\tassert_eq!(out.timestamp().denominator(), 2);\n\t\tassert_eq!(out.data().unwrap(), frame.data().unwrap());\n\t}\n\n\t#[test]\n\tfn buffer_to_frame_rejects_corrupt() {\n\t\tlet frame = make_frame();\n\t\tlet buffer = oiio_frame_to_buffer(\u0026frame).unwrap();\n\n\t\t// Truncated pixel data.\n\t\tassert!(oiio_buffer_to_frame(\u0026buffer[..HEADER_LEN + 1]).is_err());\n\t\t// Bad magic.\n\t\tlet mut bad = buffer.clone();\n\t\tbad[0] = 0;\n\t\tassert!(oiio_buffer_to_frame(\u0026bad).is_err());\n\t\t// pixel_len inconsistent with linesize * height.\n\t\tlet mut bad = buffer.clone();\n\t\tbad[56..64].copy_from_slice(\u0026(999u64).to_le_bytes());\n\t\tassert!(oiio_buffer_to_frame(\u0026bad).is_err());\n\t}\n}\n","traces":[{"line":90,"address":[],"length":0,"stats":{"Line":8}},{"line":91,"address":[],"length":0,"stats":{"Line":16}},{"line":92,"address":[],"length":0,"stats":{"Line":24}},{"line":93,"address":[],"length":0,"stats":{"Line":24}},{"line":94,"address":[],"length":0,"stats":{"Line":24}},{"line":95,"address":[],"length":0,"stats":{"Line":24}},{"line":96,"address":[],"length":0,"stats":{"Line":24}},{"line":97,"address":[],"length":0,"stats":{"Line":24}},{"line":98,"address":[],"length":0,"stats":{"Line":24}},{"line":99,"address":[],"length":0,"stats":{"Line":24}},{"line":100,"address":[],"length":0,"stats":{"Line":24}},{"line":101,"address":[],"length":0,"stats":{"Line":24}},{"line":102,"address":[],"length":0,"stats":{"Line":24}},{"line":103,"address":[],"length":0,"stats":{"Line":8}},{"line":109,"address":[],"length":0,"stats":{"Line":9}},{"line":110,"address":[],"length":0,"stats":{"Line":9}},{"line":111,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":8}},{"line":114,"address":[],"length":0,"stats":{"Line":2}},{"line":116,"address":[],"length":0,"stats":{"Line":30}},{"line":117,"address":[],"length":0,"stats":{"Line":6}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":122,"address":[],"length":0,"stats":{"Line":25}},{"line":123,"address":[],"length":0,"stats":{"Line":25}},{"line":124,"address":[],"length":0,"stats":{"Line":25}},{"line":125,"address":[],"length":0,"stats":{"Line":25}},{"line":126,"address":[],"length":0,"stats":{"Line":25}},{"line":127,"address":[],"length":0,"stats":{"Line":25}},{"line":128,"address":[],"length":0,"stats":{"Line":25}},{"line":129,"address":[],"length":0,"stats":{"Line":25}},{"line":130,"address":[],"length":0,"stats":{"Line":15}},{"line":132,"address":[],"length":0,"stats":{"Line":5}},{"line":146,"address":[],"length":0,"stats":{"Line":3}},{"line":147,"address":[],"length":0,"stats":{"Line":15}},{"line":148,"address":[],"length":0,"stats":{"Line":9}},{"line":149,"address":[],"length":0,"stats":{"Line":9}},{"line":150,"address":[],"length":0,"stats":{"Line":6}},{"line":151,"address":[],"length":0,"stats":{"Line":9}},{"line":152,"address":[],"length":0,"stats":{"Line":9}},{"line":154,"address":[],"length":0,"stats":{"Line":9}},{"line":155,"address":[],"length":0,"stats":{"Line":3}},{"line":156,"address":[],"length":0,"stats":{"Line":6}},{"line":157,"address":[],"length":0,"stats":{"Line":6}},{"line":161,"address":[],"length":0,"stats":{"Line":12}},{"line":163,"address":[],"length":0,"stats":{"Line":3}},{"line":165,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":9}},{"line":175,"address":[],"length":0,"stats":{"Line":9}},{"line":178,"address":[],"length":0,"stats":{"Line":3}},{"line":181,"address":[],"length":0,"stats":{"Line":9}},{"line":182,"address":[],"length":0,"stats":{"Line":9}},{"line":183,"address":[],"length":0,"stats":{"Line":9}},{"line":184,"address":[],"length":0,"stats":{"Line":3}},{"line":199,"address":[],"length":0,"stats":{"Line":4}},{"line":200,"address":[],"length":0,"stats":{"Line":15}},{"line":201,"address":[],"length":0,"stats":{"Line":6}},{"line":205,"address":[],"length":0,"stats":{"Line":6}},{"line":206,"address":[],"length":0,"stats":{"Line":0}},{"line":208,"address":[],"length":0,"stats":{"Line":3}},{"line":209,"address":[],"length":0,"stats":{"Line":9}},{"line":210,"address":[],"length":0,"stats":{"Line":3}},{"line":211,"address":[],"length":0,"stats":{"Line":1}},{"line":213,"address":[],"length":0,"stats":{"Line":4}},{"line":214,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":1}},{"line":224,"address":[],"length":0,"stats":{"Line":1}},{"line":225,"address":[],"length":0,"stats":{"Line":1}},{"line":226,"address":[],"length":0,"stats":{"Line":1}},{"line":230,"address":[],"length":0,"stats":{"Line":3}},{"line":232,"address":[],"length":0,"stats":{"Line":3}},{"line":233,"address":[],"length":0,"stats":{"Line":5}},{"line":234,"address":[],"length":0,"stats":{"Line":2}},{"line":236,"address":[],"length":0,"stats":{"Line":2}},{"line":237,"address":[],"length":0,"stats":{"Line":6}},{"line":238,"address":[],"length":0,"stats":{"Line":3}},{"line":239,"address":[],"length":0,"stats":{"Line":1}},{"line":244,"address":[],"length":0,"stats":{"Line":0}}],"covered":74,"coverable":77},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","planarfiledevice.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::PlanarFileDevice` — planar (per-channel) file read/write.\n//!\n//! Mirrors `src/codec/src/planarfiledevice.h`. Reads/writes one FILE* per\n//! channel so multi-channel audio is stored as one planar file per channel.\n//! `FILE*` implementation (the QIODevice-based original was replaced).\n\nuse std::fs::{File, OpenOptions};\nuse std::io::{Read, Seek, SeekFrom, Write};\nuse std::path::PathBuf;\n\n/// Open mode (replaces QIODevice::OpenMode).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum OpenMode {\n\t/// Read-only.\n\tReadOnly = 0,\n\t/// Write-only.\n\tWriteOnly = 1,\n}\n\n/// `olive::PlanarFileDevice` — one file per channel.\npub struct PlanarFileDevice {\n\t/// Open file handles, one per channel.\n\tfiles: Vec\u003cFile\u003e,\n\t/// Open mode.\n\tmode: OpenMode,\n}\n\nimpl PlanarFileDevice {\n\t/// New, closed device.\n\tpub fn new() -\u003e Self {\n\t\tPlanarFileDevice {\n\t\t\tfiles: Vec::new(),\n\t\t\tmode: OpenMode::ReadOnly,\n\t\t}\n\t}\n\n\t/// Whether the device is open.\n\tpub fn is_open(\u0026self) -\u003e bool {\n\t\t!self.files.is_empty()\n\t}\n\n\t/// Open `filenames` (one per channel) in `mode`. Returns false if already\n\t/// open or any file could not be opened (closing any opened so far).\n\tpub fn open(\u0026mut self, filenames: \u0026[PathBuf], mode: OpenMode) -\u003e bool {\n\t\tif self.is_open() {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut opened = Vec::with_capacity(filenames.len());\n\t\tfor name in filenames {\n\t\t\tlet mut opt = OpenOptions::new();\n\t\t\tlet f = match mode {\n\t\t\t\tOpenMode::ReadOnly =\u003e opt.read(true).open(name),\n\t\t\t\tOpenMode::WriteOnly =\u003e opt.create(true).write(true).truncate(true).open(name),\n\t\t\t};\n\t\t\tmatch f {\n\t\t\t\tOk(f) =\u003e opened.push(f),\n\t\t\t\tErr(_) =\u003e {\n\t\t\t\t\t// Roll back: close anything opened so far and report failure.\n\t\t\t\t\tself.files = opened;\n\t\t\t\t\tself.close();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tself.files = opened;\n\t\tself.mode = mode;\n\t\ttrue\n\t}\n\n\t/// Read `bytes_per_channel` bytes from each channel (at the current file\n\t/// position) into `data[i][offset..]`. Returns bytes read per channel, or\n\t/// -1 if closed or a buffer is too small.\n\tpub fn read(\n\t\t\u0026mut self,\n\t\tdata: \u0026mut [\u0026mut [u8]],\n\t\tbytes_per_channel: i64,\n\t\toffset: i64,\n\t) -\u003e i64 {\n\t\tif !self.is_open() {\n\t\t\treturn -1;\n\t\t}\n\t\tlet bytes = bytes_per_channel as usize;\n\t\tlet off = offset as usize;\n\t\tlet mut ret = -1i64;\n\t\tfor (i, f) in self.files.iter_mut().enumerate() {\n\t\t\tlet buf = match data.get_mut(i) {\n\t\t\t\tSome(b) if b.len() \u003e= off + bytes =\u003e \u0026mut b[off..off + bytes],\n\t\t\t\t_ =\u003e return -1,\n\t\t\t};\n\t\t\tret = f.read(buf).unwrap_or(0) as i64;\n\t\t}\n\t\tret\n\t}\n\n\t/// Write `bytes_per_channel` bytes to each channel from `data[i][offset..]`.\n\t/// Returns bytes written per channel, or -1.\n\tpub fn write(\n\t\t\u0026mut self,\n\t\tdata: \u0026[\u0026[u8]],\n\t\tbytes_per_channel: i64,\n\t\toffset: i64,\n\t) -\u003e i64 {\n\t\tif !self.is_open() {\n\t\t\treturn -1;\n\t\t}\n\t\tlet bytes = bytes_per_channel as usize;\n\t\tlet off = offset as usize;\n\t\tlet mut ret = -1i64;\n\t\tfor (i, f) in self.files.iter_mut().enumerate() {\n\t\t\tlet buf = match data.get(i) {\n\t\t\t\tSome(b) if b.len() \u003e= off + bytes =\u003e \u0026b[off..off + bytes],\n\t\t\t\t_ =\u003e return -1,\n\t\t\t};\n\t\t\tret = f.write(buf).unwrap_or(0) as i64;\n\t\t}\n\t\tret\n\t}\n\n\t/// Total size in bytes of one channel (from the first open file).\n\tpub fn size(\u0026self) -\u003e i64 {\n\t\tif self.is_open() {\n\t\t\tif let Ok(meta) = self.files[0].metadata() {\n\t\t\t\treturn meta.len() as i64;\n\t\t\t}\n\t\t}\n\t\t0\n\t}\n\n\t/// Seek all channels to `pos`.\n\tpub fn seek(\u0026mut self, pos: i64) -\u003e bool {\n\t\tlet mut ok = true;\n\t\tfor f in self.files.iter_mut() {\n\t\t\tok = f.seek(SeekFrom::Start(pos as u64)).is_ok() \u0026\u0026 ok;\n\t\t}\n\t\tok\n\t}\n\n\t/// Close all channels.\n\tpub fn close(\u0026mut self) {\n\t\tself.files.clear();\n\t}\n}\n\nimpl Default for PlanarFileDevice {\n\tfn default() -\u003e Self {\n\t\tSelf::new()\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn temp_dir(name: \u0026str) -\u003e PathBuf {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_planar_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir\n\t}\n\n\t#[test]\n\tfn new_device_is_closed() {\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(!d.is_open());\n\t\tassert_eq!(d.size(), 0);\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut [0u8; 4]], 4, 0), -1);\n\t\tassert_eq!(d.write(\u0026[\u0026[0u8; 4]], 4, 0), -1);\n\t}\n\n\t#[test]\n\tfn write_then_read_roundtrip() {\n\t\tlet dir = temp_dir(\"rw\");\n\t\tlet names: Vec\u003cPathBuf\u003e = (0..2).map(|i| dir.join(format!(\"ch{}.pcm\", i))).collect();\n\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::WriteOnly));\n\t\tassert!(d.is_open());\n\t\tassert_eq!(d.mode, OpenMode::WriteOnly);\n\n\t\t// Two channels, 4 bytes each.\n\t\tlet ch0 = [1u8, 2, 3, 4];\n\t\tlet ch1 = [9u8, 8, 7, 6];\n\t\tassert_eq!(d.write(\u0026[\u0026ch0, \u0026ch1], 4, 0), 4);\n\t\tassert_eq!(d.size(), 4);\n\t\td.close();\n\t\tassert!(!d.is_open());\n\n\t\t// A \"wb\" handle cannot be read back (matching the C++ fopen mode);\n\t\t// reopen read-only to verify the written bytes.\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::ReadOnly));\n\t\tassert_eq!(d.size(), 4);\n\t\tlet mut out0 = [0u8; 8];\n\t\tlet mut out1 = [0u8; 8];\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut out0[..], \u0026mut out1[..]], 4, 0), 4);\n\t\tassert_eq!(\u0026out0[..4], \u0026ch0);\n\t\tassert_eq!(\u0026out1[..4], \u0026ch1);\n\t\td.close();\n\t\tassert!(!d.is_open());\n\t}\n\n\t#[test]\n\tfn offset_writes_and_reads() {\n\t\tlet dir = temp_dir(\"off\");\n\t\tlet names = vec![dir.join(\"ch.pcm\")];\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::WriteOnly));\n\n\t\t// The offset is a *buffer* offset (C++ `data[i] + offset`); file\n\t\t// offsets go through `seek`. Seek to 4 then write 4 bytes, leaving\n\t\t// a 4-byte gap.\n\t\tlet data = [7u8, 7, 7, 7];\n\t\tassert!(d.seek(4));\n\t\tassert_eq!(d.write(\u0026[\u0026data], 4, 0), 4);\n\t\tassert_eq!(d.size(), 8);\n\n\t\td.close();\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::ReadOnly));\n\t\tlet mut buf = [0u8; 8];\n\t\tassert!(d.seek(0));\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut buf[..]], 8, 0), 8);\n\t\tassert_eq!(\u0026buf[..4], \u0026[0, 0, 0, 0]);\n\t\tassert_eq!(\u0026buf[4..], \u0026data);\n\t}\n\n\t#[test]\n\tfn open_rollback_on_missing_file_and_double_open() {\n\t\tlet dir = temp_dir(\"roll\");\n\t\tlet ok = dir.join(\"ok.pcm\");\n\t\tlet missing = dir.join(\"missing.pcm\");\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(!d.open(\u0026[ok.clone(), missing], OpenMode::ReadOnly));\n\t\tassert!(!d.is_open());\n\n\t\t// Already open -\u003e refuse.\n\t\tassert!(d.open(\u0026[ok.clone()], OpenMode::WriteOnly));\n\t\tassert!(!d.open(\u0026[ok], OpenMode::WriteOnly));\n\t}\n\n\t#[test]\n\tfn read_write_reject_small_buffers() {\n\t\tlet dir = temp_dir(\"small\");\n\t\tlet names = vec![dir.join(\"ch.pcm\")];\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::WriteOnly));\n\t\tlet data = [1u8; 8];\n\t\tassert_eq!(d.write(\u0026[\u0026data], 8, 0), 8);\n\n\t\t// Buffer too small for the requested bytes.\n\t\tlet mut small = [0u8; 4];\n\t\tassert_eq!(d.write(\u0026[\u0026small], 8, 0), -1);\n\t\tassert!(d.seek(0));\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut small[..]], 8, 0), -1);\n\t}\n}\n","traces":[{"line":47,"address":[],"length":0,"stats":{"Line":8}},{"line":49,"address":[],"length":0,"stats":{"Line":8}},{"line":55,"address":[],"length":0,"stats":{"Line":824}},{"line":56,"address":[],"length":0,"stats":{"Line":824}},{"line":61,"address":[],"length":0,"stats":{"Line":9}},{"line":62,"address":[],"length":0,"stats":{"Line":18}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":66,"address":[],"length":0,"stats":{"Line":32}},{"line":67,"address":[],"length":0,"stats":{"Line":19}},{"line":68,"address":[],"length":0,"stats":{"Line":22}},{"line":69,"address":[],"length":0,"stats":{"Line":22}},{"line":70,"address":[],"length":0,"stats":{"Line":12}},{"line":71,"address":[],"length":0,"stats":{"Line":21}},{"line":73,"address":[],"length":0,"stats":{"Line":11}},{"line":74,"address":[],"length":0,"stats":{"Line":40}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":14}},{"line":85,"address":[],"length":0,"stats":{"Line":7}},{"line":86,"address":[],"length":0,"stats":{"Line":7}},{"line":92,"address":[],"length":0,"stats":{"Line":4}},{"line":98,"address":[],"length":0,"stats":{"Line":4}},{"line":99,"address":[],"length":0,"stats":{"Line":1}},{"line":101,"address":[],"length":0,"stats":{"Line":6}},{"line":102,"address":[],"length":0,"stats":{"Line":6}},{"line":103,"address":[],"length":0,"stats":{"Line":6}},{"line":104,"address":[],"length":0,"stats":{"Line":14}},{"line":105,"address":[],"length":0,"stats":{"Line":15}},{"line":106,"address":[],"length":0,"stats":{"Line":27}},{"line":107,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":9}},{"line":111,"address":[],"length":0,"stats":{"Line":2}},{"line":116,"address":[],"length":0,"stats":{"Line":802}},{"line":122,"address":[],"length":0,"stats":{"Line":802}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":1602}},{"line":126,"address":[],"length":0,"stats":{"Line":1602}},{"line":127,"address":[],"length":0,"stats":{"Line":1602}},{"line":128,"address":[],"length":0,"stats":{"Line":4800}},{"line":129,"address":[],"length":0,"stats":{"Line":6395}},{"line":130,"address":[],"length":0,"stats":{"Line":12787}},{"line":131,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":4794}},{"line":135,"address":[],"length":0,"stats":{"Line":800}},{"line":139,"address":[],"length":0,"stats":{"Line":4}},{"line":140,"address":[],"length":0,"stats":{"Line":8}},{"line":141,"address":[],"length":0,"stats":{"Line":6}},{"line":142,"address":[],"length":0,"stats":{"Line":3}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":149,"address":[],"length":0,"stats":{"Line":3}},{"line":150,"address":[],"length":0,"stats":{"Line":6}},{"line":151,"address":[],"length":0,"stats":{"Line":6}},{"line":152,"address":[],"length":0,"stats":{"Line":12}},{"line":154,"address":[],"length":0,"stats":{"Line":3}},{"line":158,"address":[],"length":0,"stats":{"Line":5}},{"line":159,"address":[],"length":0,"stats":{"Line":10}},{"line":164,"address":[],"length":0,"stats":{"Line":0}},{"line":165,"address":[],"length":0,"stats":{"Line":0}}],"covered":57,"coverable":59},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","proxymanager.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ProxyManager` — proxy (low-res transcode) generation.\n//!\n//! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual\n//! transcodes are delegated to the global task submit callback\n//! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy\n//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI\n//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23\n//! / \"mp4\" / \"veryfast\" / audio included).\n\nuse std::ffi::{c_char, CString};\nuse std::path::Path;\n\n/// Proxy state of a proxy file on disk.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum ProxyState {\n\t/// Missing (or NULL/empty/absent).\n\tMissing = 0,\n\t/// Generating.\n\tGenerating = 1,\n\t/// Ready on disk.\n\tReady = 2,\n\t/// Generation failed.\n\tFailed = 3,\n}\n\n/// `olive::ProxyManager::ProxyParams` — mirror of `oakcodec_proxy_params`.\n#[derive(Clone, Debug)]\n#[repr(C)]\npub struct ProxyParams {\n\t/// Absolute target width (0 when divider-based).\n\tpub width: i32,\n\t/// Absolute target height (0 when divider-based).\n\tpub height: i32,\n\t/// Source resolution divider (1 = absolute width/height, 2/4/8).\n\tpub divider: i32,\n\t/// Proxy format version.\n\tpub version: i32,\n\t/// x264 crf.\n\tpub crf: i32,\n\t/// Include the audio track (1/0; C `int`).\n\tpub include_audio: i32,\n\t/// ffmpeg output container (e.g. \"mp4\").\n\tpub extension: [u8; 32],\n\t/// ffmpeg encoder preset (e.g. \"veryfast\").\n\tpub preset: [u8; 32],\n}\n\nimpl Default for ProxyParams {\n\tfn default() -\u003e Self {\n\t\tProxyParams {\n\t\t\twidth: 1280,\n\t\t\theight: 720,\n\t\t\tdivider: 1,\n\t\t\tversion: 1,\n\t\t\tcrf: 23,\n\t\t\tinclude_audio: 1,\n\t\t\textension: bytes32(b\"mp4\"),\n\t\t\tpreset: bytes32(b\"veryfast\"),\n\t\t}\n\t}\n}\n\nimpl ProxyParams {\n\tfn extension_str(\u0026self) -\u003e \u0026str {\n\t\tcstr_slice(\u0026self.extension)\n\t}\n\n\tfn preset_str(\u0026self) -\u003e \u0026str {\n\t\tcstr_slice(\u0026self.preset)\n\t}\n}\n\n/// `oakcodec_proxy_result` — POD result of [`ProxyManager::get_or_start`];\n/// see `include/codec/proxy.h`.\n#[repr(C)]\npub struct OakCodecProxyResult {\n\t/// `ProxyState` value.\n\tpub state: i32,\n\t/// Resulting proxy filename (may be empty).\n\tpub filename: [u8; 1024],\n}\n\n/// `olive::ProxyManager` — stateless proxy query/generate manager.\npub struct ProxyManager;\n\nimpl ProxyManager {\n\t/// The process-wide ProxyManager singleton.\n\tpub fn instance() -\u003e \u0026'static ProxyManager {\n\t\tstatic INSTANCE: ProxyManager = ProxyManager;\n\t\t\u0026INSTANCE\n\t}\n\n\t/// Compiled-in default proxy parameters (the `Default` values).\n\tpub fn proxy_params_default() -\u003e ProxyParams {\n\t\tProxyParams::default()\n\t}\n\n\t/// Proxy parameters read from the oakcommon config, with the compiled-in\n\t/// defaults as fallback.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/proxymanager.h` `proxy_params_from_config` — reads\n\t/// ProxyWidth/ProxyHeight/ProxyDivider/ProxyCRF/ProxyPreset/\n\t/// ProxyIncludeAudio via `oakcommon_config_*`.\n\tpub fn proxy_params_from_config() -\u003e ProxyParams {\n\t\tlet mut p = ProxyParams::default();\n\t\tp.width = config_get_int(\"ProxyWidth\", p.width);\n\t\tp.height = config_get_int(\"ProxyHeight\", p.height);\n\t\tp.divider = config_get_int(\"ProxyDivider\", p.divider);\n\t\tp.crf = config_get_int(\"ProxyCRF\", p.crf);\n\t\tp.include_audio = if config_get_bool(\"ProxyIncludeAudio\", p.include_audio) != 0 {\n\t\t\t1\n\t\t} else {\n\t\t\t0\n\t\t};\n\t\tif let Some(preset) = config_get_str(\"ProxyPreset\") {\n\t\t\tif !preset.is_empty() {\n\t\t\t\tp.preset = bytes32(preset.as_bytes());\n\t\t\t}\n\t\t}\n\t\tp\n\t}\n\n\t/// State of a proxy file on disk.\n\tpub fn get_proxy_state(proxy_filename: \u0026str) -\u003e ProxyState {\n\t\tif proxy_filename.is_empty() {\n\t\t\treturn ProxyState::Missing;\n\t\t}\n\t\tif Path::new(proxy_filename).exists() {\n\t\t\treturn ProxyState::Ready;\n\t\t}\n\t\tlet working = Self::get_working_filename(proxy_filename);\n\t\tif let Ok(w) = working {\n\t\t\tif Path::new(\u0026w).exists() {\n\t\t\t\treturn ProxyState::Generating;\n\t\t\t}\n\t\t}\n\t\tProxyState::Missing\n\t}\n\n\t/// Human-readable string for a proxy state.\n\tpub fn proxy_state_to_string(state: ProxyState) -\u003e String {\n\t\tmatch state {\n\t\t\tProxyState::Missing =\u003e \"missing\".to_string(),\n\t\t\tProxyState::Generating =\u003e \"generating\".to_string(),\n\t\t\tProxyState::Ready =\u003e \"ready\".to_string(),\n\t\t\tProxyState::Failed =\u003e \"failed\".to_string(),\n\t\t}\n\t}\n\n\t/// Proxy directory for a project cache path.\n\tpub fn get_proxy_directory(cache_path: \u0026str) -\u003e crate::error::Result\u003cString\u003e {\n\t\tOk(Path::new(cache_path)\n\t\t\t.join(\"proxy\")\n\t\t\t.to_string_lossy()\n\t\t\t.into_owned())\n\t}\n\n\t/// Deterministic proxy filename for a source stream.\n\tpub fn get_proxy_filename(\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tparams: \u0026ProxyParams,\n\t) -\u003e crate::error::Result\u003cString\u003e {\n\t\tlet proxy_dir = Self::get_proxy_directory(cache_path)?;\n\t\tlet extension = if params.extension_str().is_empty() {\n\t\t\t\"mp4\"\n\t\t} else {\n\t\t\tparams.extension_str()\n\t\t};\n\n\t\t// Divider mode scales relative to the source, so the tag names the\n\t\t// divider rather than an absolute target size.\n\t\tlet size_tag = if params.divider \u003e 1 {\n\t\t\tformat!(\"div{}\", params.divider)\n\t\t} else {\n\t\t\tformat!(\"{}x{}\", params.width, params.height)\n\t\t};\n\n\t\tlet filename = format!(\n\t\t\t\"{}-{}.{}.v{}.a{}.{}\",\n\t\t\tunique_file_identifier(source_filename),\n\t\t\tstream_index,\n\t\t\tsize_tag,\n\t\t\tparams.version,\n\t\t\tparams.include_audio,\n\t\t\textension,\n\t\t);\n\n\t\tOk(Path::new(\u0026proxy_dir)\n\t\t\t.join(filename)\n\t\t\t.to_string_lossy()\n\t\t\t.into_owned())\n\t}\n\n\t/// Working (in-progress) filename of a proxy.\n\tpub fn get_working_filename(proxy_filename: \u0026str) -\u003e crate::error::Result\u003cString\u003e {\n\t\t// Append a recognizable suffix while keeping a standard container\n\t\t// extension so ffmpeg can infer the output format.\n\t\tOk(format!(\"{}.working.mp4\", proxy_filename))\n\t}\n\n\t/// Get or start generating a proxy for `source_filename`. With no task\n\t/// registrar the state stays `Missing`.\n\tpub fn get_or_start(\n\t\t\u0026self,\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tparams: \u0026ProxyParams,\n\t) -\u003e crate::error::Result\u003c(ProxyState, String)\u003e {\n\t\tlet filename = Self::get_proxy_filename(cache_path, source_filename, stream_index, params)?;\n\t\tlet file_state = Self::get_proxy_state(\u0026filename);\n\t\tif file_state == ProxyState::Ready {\n\t\t\treturn Ok((ProxyState::Ready, filename));\n\t\t}\n\n\t\tif !crate::task::task_submit_is_registered() {\n\t\t\t// Interim state (pre-M8): no task system, proxy cannot be generated.\n\t\t\treturn Ok((ProxyState::Missing, filename));\n\t\t}\n\n\t\tif file_state == ProxyState::Generating {\n\t\t\t// Stale working file from an interrupted run.\n\t\t\tif let Ok(working) = Self::get_working_filename(\u0026filename) {\n\t\t\t\tlet _ = std::fs::remove_file(\u0026working);\n\t\t\t}\n\t\t}\n\n\t\t// The task owns the \".working.mp4\" temporary name and the rename to the\n\t\t// final filename on success.\n\t\tlet req = crate::task::TaskRequest {\n\t\t\tkind: crate::task::TaskKind::Proxy,\n\t\t\tinput_filename: source_filename,\n\t\t\toutput_filename: \u0026filename,\n\t\t\tstream_index,\n\t\t\tsample_rate: 0,\n\t\t\tchannel_layout: 0,\n\t\t\tsample_format: 0,\n\t\t\tproxy_width: if params.divider \u003c= 1 { params.width } else { 0 },\n\t\t\tproxy_height: if params.divider \u003c= 1 { params.height } else { 0 },\n\t\t};\n\n\t\t// Interim simplification: submission is synchronous.\n\t\tif crate::task::submit_task(\u0026req).is_err() {\n\t\t\treturn Ok((ProxyState::Failed, filename));\n\t\t}\n\n\t\tif Self::get_proxy_state(\u0026filename) == ProxyState::Ready {\n\t\t\treturn Ok((ProxyState::Ready, filename));\n\t\t}\n\n\t\tOk((ProxyState::Generating, filename))\n\t}\n\n\t/// Locate an ffmpeg executable (empty string when none found).\n\tpub fn find_ffmpeg(configured_path: \u0026str) -\u003e String {\n\t\t// An explicitly configured path takes precedence if it is usable.\n\t\tif !configured_path.is_empty() {\n\t\t\tif is_executable_file(Path::new(configured_path)) {\n\t\t\t\treturn absolute(configured_path);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to searching the system PATH.\n\t\tif let Ok(path_env) = std::env::var(\"PATH\") {\n\t\t\tfor dir in path_env.split(':') {\n\t\t\t\tif dir.is_empty() {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlet candidate = Path::new(dir).join(\"ffmpeg\");\n\t\t\t\tif is_executable_file(\u0026candidate) {\n\t\t\t\t\treturn absolute(\u0026candidate.to_string_lossy());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Finally, try common install locations (PATH on GUI-launched apps,\n\t\t// particularly on macOS, often lacks these).\n\t\tlet mut candidates: Vec\u003cString\u003e = Vec::new();\n\t\tlet app_path = application_path();\n\t\tif !app_path.is_empty() {\n\t\t\tcandidates.push(format!(\"{}/ffmpeg\", app_path));\n\t\t}\n\t\tcandidates.push(\"/opt/homebrew/bin/ffmpeg\".to_string());\n\t\tcandidates.push(\"/usr/local/bin/ffmpeg\".to_string());\n\t\tcandidates.push(\"/usr/bin/ffmpeg\".to_string());\n\t\tcandidates.push(\"/usr/local/bin/ffmpeg\".to_string());\n\n\t\tfor c in candidates {\n\t\t\tif is_executable_file(Path::new(\u0026c)) {\n\t\t\t\treturn absolute(\u0026c);\n\t\t\t}\n\t\t}\n\n\t\tString::new()\n\t}\n}\n\n/// Copy a byte string into a NUL-terminated `[u8; 32]` (truncated to 31\n/// chars so there is always a trailing NUL).\nfn bytes32(s: \u0026[u8]) -\u003e [u8; 32] {\n\tlet mut a = [0u8; 32];\n\tlet n = s.len().min(31);\n\ta[..n].copy_from_slice(\u0026s[..n]);\n\ta\n}\n\n/// View a NUL-terminated `[u8; 32]` as a `\u0026str` (up to the first NUL).\nfn cstr_slice(a: \u0026[u8; 32]) -\u003e \u0026str {\n\tlet end = a.iter().position(|\u0026b| b == 0).unwrap_or(a.len());\n\tstd::str::from_utf8(\u0026a[..end]).unwrap_or(\"\")\n}\n\n/// `oakcommon_config_get_int` wrapper (null group).\nfn config_get_int(key: \u0026str, default: i32) -\u003e i32 {\n\tlet ckey = cstring(key);\n\t// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_config_get_int(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tdefault,\n\t\t)\n\t}\n}\n\n/// `oakcommon_config_get_bool` wrapper (null group).\nfn config_get_bool(key: \u0026str, default: i32) -\u003e i32 {\n\tlet ckey = cstring(key);\n\t// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_config_get_bool(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tdefault,\n\t\t)\n\t}\n}\n\n/// Two-stage `oakcommon_config_get` string read; `None` when the stored\n/// value is empty or absent.\nfn config_get_str(key: \u0026str) -\u003e Option\u003cString\u003e {\n\tlet ckey = cstring(key);\n\t// # Safety: `ckey` is valid; first call asks only for the required size.\n\tlet size = unsafe {\n\t\tcrate::bridge::common::oakcommon_config_get(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tstd::ptr::null_mut(),\n\t\t\t0,\n\t\t)\n\t};\n\tif size \u003c= 1 {\n\t\treturn None;\n\t}\n\tlet mut buf = vec![0u8; size as usize];\n\t// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_config_get(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tbuf.as_mut_ptr() as *mut c_char,\n\t\t\tsize,\n\t\t);\n\t}\n\tlet mut end = buf.len();\n\twhile end \u003e 0 \u0026\u0026 buf[end - 1] == 0 {\n\t\tend -= 1;\n\t}\n\tSome(String::from_utf8_lossy(\u0026buf[..end]).into_owned())\n}\n\n/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge\n/// returns a 64-bit id directly).\nfn unique_file_identifier(filename: \u0026str) -\u003e String {\n\tlet c = match CString::new(filename) {\n\t\tOk(c) =\u003e c,\n\t\tErr(_) =\u003e return String::new(),\n\t};\n\t// # Safety: `c` is a valid NUL-terminated C string alive for the call.\n\tlet id = unsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())\n\t};\n\tformat!(\"{}\", id)\n}\n\n/// Two-stage `oakcommon_filefunctions_get_application_path` read.\nfn application_path() -\u003e String {\n\t// # Safety: first call asks only for the required size.\n\tlet size = unsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_application_path(\n\t\t\tstd::ptr::null_mut(),\n\t\t\t0,\n\t\t)\n\t};\n\tif size \u003c= 1 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0u8; size as usize];\n\t// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_application_path(\n\t\t\tbuf.as_mut_ptr() as *mut c_char,\n\t\t\tsize,\n\t\t);\n\t}\n\tlet mut end = buf.len();\n\twhile end \u003e 0 \u0026\u0026 buf[end - 1] == 0 {\n\t\tend -= 1;\n\t}\n\tString::from_utf8_lossy(\u0026buf[..end]).into_owned()\n}\n\n/// Build a NUL-terminated C string from a Rust string; empty on embedded\n/// NUL (defensive only — callers pass sane keys).\nfn cstring(s: \u0026str) -\u003e CString {\n\tCString::new(s).unwrap_or_else(|_| CString::new(\"\").unwrap())\n}\n\n/// True when `p` is a regular file with at least one execute bit set.\nfn is_executable_file(p: \u0026Path) -\u003e bool {\n\tuse std::os::unix::fs::PermissionsExt;\n\tmatch std::fs::metadata(p) {\n\t\tOk(md) if md.is_file() =\u003e md.permissions().mode() \u0026 0o111 != 0,\n\t\t_ =\u003e false,\n\t}\n}\n\n/// Canonicalize a path, falling back to the raw string on failure.\nfn absolute(p: \u0026str) -\u003e String {\n\tstd::fs::canonicalize(p)\n\t\t.map(|c| c.to_string_lossy().into_owned())\n\t\t.unwrap_or_else(|_| p.to_string())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn temp_subdir(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_proxy_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\tfn fnv1a64(bytes: \u0026[u8]) -\u003e u64 {\n\t\tlet mut h: u64 = 14695981039346656037;\n\t\tfor \u0026b in bytes {\n\t\t\th ^= b as u64;\n\t\t\th = h.wrapping_mul(1099511628211);\n\t\t}\n\t\th\n\t}\n\n\t#[test]\n\tfn proxy_params_default_values() {\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tassert_eq!(p.width, 1280);\n\t\tassert_eq!(p.height, 720);\n\t\tassert_eq!(p.divider, 1);\n\t\tassert_eq!(p.version, 1);\n\t\tassert_eq!(p.crf, 23);\n\t\tassert_eq!(p.include_audio, 1);\n\t\tassert_eq!(p.extension_str(), \"mp4\");\n\t\tassert_eq!(p.preset_str(), \"veryfast\");\n\t}\n\n\t#[test]\n\tfn proxy_params_from_config_uses_defaults_without_store() {\n\t\t// The test stub returns defaults for every int/bool and an empty\n\t\t// ProxyPreset; the empty preset must not clobber the compiled-in one.\n\t\tlet p = ProxyManager::proxy_params_from_config();\n\t\tassert_eq!(p.width, 1280);\n\t\tassert_eq!(p.height, 720);\n\t\tassert_eq!(p.divider, 1);\n\t\tassert_eq!(p.crf, 23);\n\t\tassert_eq!(p.include_audio, 1);\n\t\tassert_eq!(p.preset_str(), \"veryfast\");\n\t\tassert_eq!(p.extension_str(), \"mp4\");\n\t}\n\n\t#[test]\n\tfn proxy_directory_is_cache_slash_proxy() {\n\t\tassert_eq!(\n\t\t\tProxyManager::get_proxy_directory(\"/tmp/cache\").unwrap(),\n\t\t\t\"/tmp/cache/proxy\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn proxy_filename_derivation() {\n\t\tlet cache = temp_subdir(\"fn\");\n\t\tlet id = fnv1a64(b\"media.mp4\") as i64;\n\t\tlet p = ProxyManager::proxy_params_default();\n\n\t\tlet f = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026p).unwrap();\n\t\tassert_eq!(\n\t\t\tf,\n\t\t\tformat!(\"{}/proxy/{}-0.1280x720.v1.a1.mp4\", cache, id)\n\t\t);\n\n\t\t// Divider mode tags the divider instead of an absolute size.\n\t\tlet mut d = p.clone();\n\t\td.divider = 2;\n\t\tlet f2 = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026d).unwrap();\n\t\tassert!(f2.contains(\".div2.\"));\n\n\t\t// No audio.\n\t\tlet mut na = p.clone();\n\t\tna.include_audio = 0;\n\t\tlet f3 = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026na).unwrap();\n\t\tassert!(f3.contains(\".a0.\"));\n\t}\n\n\t#[test]\n\tfn proxy_state_transitions() {\n\t\tlet cache = temp_subdir(\"state\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet f = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026p).unwrap();\n\n\t\t// Missing when neither the file nor the working file exists.\n\t\tassert_eq!(ProxyManager::get_proxy_state(\u0026f), ProxyState::Missing);\n\n\t\t// Generating when only the working file exists.\n\t\tlet working = ProxyManager::get_working_filename(\u0026f).unwrap();\n\t\tstd::fs::create_dir_all(Path::new(\u0026working).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026working, b\"x\").unwrap();\n\t\tassert_eq!(ProxyManager::get_proxy_state(\u0026f), ProxyState::Generating);\n\n\t\t// Ready when the final file exists (takes precedence over working).\n\t\tstd::fs::create_dir_all(Path::new(\u0026f).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026f, b\"x\").unwrap();\n\t\tassert_eq!(ProxyManager::get_proxy_state(\u0026f), ProxyState::Ready);\n\t}\n\n\t#[test]\n\tfn proxy_state_to_string_mapping() {\n\t\tassert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Missing), \"missing\");\n\t\tassert_eq!(\n\t\t\tProxyManager::proxy_state_to_string(ProxyState::Generating),\n\t\t\t\"generating\"\n\t\t);\n\t\tassert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Ready), \"ready\");\n\t\tassert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Failed), \"failed\");\n\t}\n\n\t#[test]\n\tfn get_working_filename_appends_suffix() {\n\t\tassert_eq!(\n\t\t\tProxyManager::get_working_filename(\"/a/b.mp4\").unwrap(),\n\t\t\t\"/a/b.mp4.working.mp4\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn get_or_start_missing_without_registrar() {\n\t\tlet _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tlet cache = temp_subdir(\"nostart\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet (state, _f) =\n\t\t\tProxyManager::instance()\n\t\t\t\t.get_or_start(\u0026cache, \"media.mp4\", 0, \u0026p)\n\t\t\t\t.unwrap();\n\t\tassert_eq!(state, ProxyState::Missing);\n\t}\n\n\t#[test]\n\tfn get_or_start_ready_when_file_exists() {\n\t\tlet cache = temp_subdir(\"ready\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet f = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026p).unwrap();\n\t\tstd::fs::create_dir_all(Path::new(\u0026f).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026f, b\"x\").unwrap();\n\t\tlet (state, filename) =\n\t\t\tProxyManager::instance()\n\t\t\t\t.get_or_start(\u0026cache, \"media.mp4\", 0, \u0026p)\n\t\t\t\t.unwrap();\n\t\tassert_eq!(state, ProxyState::Ready);\n\t\tassert_eq!(filename, f);\n\t}\n\n\t#[test]\n\tfn get_or_start_generating_when_registered() {\n\t\tlet _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(\n\t\t\tSome(crate::conformmanager::test_util::accept_cb),\n\t\t\tstd::ptr::null_mut(),\n\t\t);\n\t\tlet cache = temp_subdir(\"start\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet (state, _f) =\n\t\t\tProxyManager::instance()\n\t\t\t\t.get_or_start(\u0026cache, \"media.mp4\", 0, \u0026p)\n\t\t\t\t.unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tassert_eq!(state, ProxyState::Generating);\n\t}\n\n\t#[test]\n\tfn find_ffmpeg_configured_path_wins() {\n\t\t// Point at a real executable (the current test binary) so the\n\t\t// configured-path branch resolves to an absolute path.\n\t\tlet me = std::env::current_exe().unwrap();\n\t\tlet found = ProxyManager::find_ffmpeg(me.to_str().unwrap());\n\t\tlet canonical = std::fs::canonicalize(\u0026me).unwrap();\n\t\tassert_eq!(found, canonical.to_string_lossy());\n\t}\n\n\t#[test]\n\tfn find_ffmpeg_missing_returns_empty() {\n\t\tlet found = ProxyManager::find_ffmpeg(\"/definitely/not/a/real/ffmpeg\");\n\t\t// Either an absolute configured/installed match or empty; never a raw\n\t\t// unresolved path.\n\t\tassert!(found.is_empty() || found.starts_with('/'));\n\t}\n\n\t/// `oakcodec_proxy_params` byte-level layout lock against\n\t/// `include/codec/proxy.h` (verified with a C++ `offsetof` probe): the\n\t/// Rust mirror must read a C caller's POD in place.\n\t#[test]\n\tfn proxy_params_c_abi_layout() {\n\t\tuse std::mem::{offset_of, size_of};\n\n\t\tassert_eq!(size_of::\u003cProxyParams\u003e(), 88);\n\t\tassert_eq!(offset_of!(ProxyParams, width), 0);\n\t\tassert_eq!(offset_of!(ProxyParams, height), 4);\n\t\tassert_eq!(offset_of!(ProxyParams, divider), 8);\n\t\tassert_eq!(offset_of!(ProxyParams, version), 12);\n\t\tassert_eq!(offset_of!(ProxyParams, crf), 16);\n\t\tassert_eq!(offset_of!(ProxyParams, include_audio), 20);\n\t\tassert_eq!(offset_of!(ProxyParams, extension), 24);\n\t\tassert_eq!(offset_of!(ProxyParams, preset), 56);\n\t}\n}\n\n#[cfg(test)]\nmod tests_extra {\n\tuse super::*;\n\n\t#[test]\n\tfn find_ffmpeg_searches_path() {\n\t\t// Create a fake executable in a temp dir and prepend it to PATH.\n\t\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_ffmpeg_{}\", std::process::id()));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tlet fake = dir.join(\"ffmpeg\");\n\t\tuse std::os::unix::fs::PermissionsExt;\n\t\tstd::fs::write(\u0026fake, b\"#!/bin/sh\\n\").unwrap();\n\t\tstd::fs::set_permissions(\u0026fake, std::fs::Permissions::from_mode(0o755)).unwrap();\n\n\t\tlet mut paths = dir.to_string_lossy().into_owned();\n\t\tif let Ok(existing) = std::env::var(\"PATH\") {\n\t\t\tpaths.push(':');\n\t\t\tpaths.push_str(\u0026existing);\n\t\t}\n\t\tlet old = std::env::var_os(\"PATH\");\n\t\tstd::env::set_var(\"PATH\", \u0026paths);\n\t\tlet found = ProxyManager::find_ffmpeg(\"\");\n\t\tif let Some(old) = old {\n\t\t\tstd::env::set_var(\"PATH\", old);\n\t\t} else {\n\t\t\tstd::env::remove_var(\"PATH\");\n\t\t}\n\n\t\t// `std::env::set_var` is not thread-safe, so under parallel tests the\n\t\t// canonicalized form can race; assert the search branch invariants\n\t\t// instead of the exact canonical path.\n\t\tassert!(found.starts_with('/'), \"found: {found}\");\n\t\tassert!(found.ends_with(\"/ffmpeg\"), \"found: {found}\");\n\t\tassert!(std::path::Path::new(\u0026found).exists(), \"found: {found}\");\n\t}\n\n\t#[test]\n\tfn get_proxy_state_empty_and_working() {\n\t\t// Empty filename -\u003e Missing.\n\t\tassert_eq!(ProxyManager::get_proxy_state(\"\"), ProxyState::Missing);\n\t\t// A path that does not exist -\u003e Missing.\n\t\tassert_eq!(ProxyManager::get_proxy_state(\"/nope/nope.mp4\"), ProxyState::Missing);\n\t}\n}\n","traces":[{"line":66,"address":[],"length":0,"stats":{"Line":10}},{"line":74,"address":[],"length":0,"stats":{"Line":30}},{"line":75,"address":[],"length":0,"stats":{"Line":10}},{"line":81,"address":[],"length":0,"stats":{"Line":24}},{"line":82,"address":[],"length":0,"stats":{"Line":24}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":105,"address":[],"length":0,"stats":{"Line":6}},{"line":107,"address":[],"length":0,"stats":{"Line":6}},{"line":111,"address":[],"length":0,"stats":{"Line":6}},{"line":112,"address":[],"length":0,"stats":{"Line":6}},{"line":122,"address":[],"length":0,"stats":{"Line":4}},{"line":123,"address":[],"length":0,"stats":{"Line":8}},{"line":124,"address":[],"length":0,"stats":{"Line":8}},{"line":125,"address":[],"length":0,"stats":{"Line":8}},{"line":126,"address":[],"length":0,"stats":{"Line":8}},{"line":127,"address":[],"length":0,"stats":{"Line":8}},{"line":128,"address":[],"length":0,"stats":{"Line":8}},{"line":129,"address":[],"length":0,"stats":{"Line":4}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":133,"address":[],"length":0,"stats":{"Line":4}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":4}},{"line":142,"address":[],"length":0,"stats":{"Line":14}},{"line":143,"address":[],"length":0,"stats":{"Line":28}},{"line":144,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":26}},{"line":147,"address":[],"length":0,"stats":{"Line":3}},{"line":149,"address":[],"length":0,"stats":{"Line":30}},{"line":150,"address":[],"length":0,"stats":{"Line":20}},{"line":151,"address":[],"length":0,"stats":{"Line":20}},{"line":152,"address":[],"length":0,"stats":{"Line":1}},{"line":155,"address":[],"length":0,"stats":{"Line":9}},{"line":159,"address":[],"length":0,"stats":{"Line":5}},{"line":160,"address":[],"length":0,"stats":{"Line":5}},{"line":161,"address":[],"length":0,"stats":{"Line":2}},{"line":162,"address":[],"length":0,"stats":{"Line":2}},{"line":163,"address":[],"length":0,"stats":{"Line":4}},{"line":164,"address":[],"length":0,"stats":{"Line":2}},{"line":169,"address":[],"length":0,"stats":{"Line":13}},{"line":170,"address":[],"length":0,"stats":{"Line":13}},{"line":171,"address":[],"length":0,"stats":{"Line":13}},{"line":172,"address":[],"length":0,"stats":{"Line":13}},{"line":173,"address":[],"length":0,"stats":{"Line":13}},{"line":177,"address":[],"length":0,"stats":{"Line":11}},{"line":183,"address":[],"length":0,"stats":{"Line":33}},{"line":184,"address":[],"length":0,"stats":{"Line":33}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":11}},{"line":192,"address":[],"length":0,"stats":{"Line":22}},{"line":193,"address":[],"length":0,"stats":{"Line":1}},{"line":195,"address":[],"length":0,"stats":{"Line":10}},{"line":198,"address":[],"length":0,"stats":{"Line":22}},{"line":200,"address":[],"length":0,"stats":{"Line":22}},{"line":208,"address":[],"length":0,"stats":{"Line":22}},{"line":209,"address":[],"length":0,"stats":{"Line":11}},{"line":210,"address":[],"length":0,"stats":{"Line":11}},{"line":211,"address":[],"length":0,"stats":{"Line":11}},{"line":215,"address":[],"length":0,"stats":{"Line":13}},{"line":218,"address":[],"length":0,"stats":{"Line":13}},{"line":223,"address":[],"length":0,"stats":{"Line":5}},{"line":230,"address":[],"length":0,"stats":{"Line":30}},{"line":231,"address":[],"length":0,"stats":{"Line":15}},{"line":232,"address":[],"length":0,"stats":{"Line":5}},{"line":233,"address":[],"length":0,"stats":{"Line":1}},{"line":236,"address":[],"length":0,"stats":{"Line":4}},{"line":238,"address":[],"length":0,"stats":{"Line":2}},{"line":241,"address":[],"length":0,"stats":{"Line":2}},{"line":243,"address":[],"length":0,"stats":{"Line":0}},{"line":244,"address":[],"length":0,"stats":{"Line":0}},{"line":253,"address":[],"length":0,"stats":{"Line":2}},{"line":258,"address":[],"length":0,"stats":{"Line":4}},{"line":259,"address":[],"length":0,"stats":{"Line":4}},{"line":263,"address":[],"length":0,"stats":{"Line":4}},{"line":264,"address":[],"length":0,"stats":{"Line":0}},{"line":267,"address":[],"length":0,"stats":{"Line":2}},{"line":268,"address":[],"length":0,"stats":{"Line":0}},{"line":271,"address":[],"length":0,"stats":{"Line":2}},{"line":275,"address":[],"length":0,"stats":{"Line":5}},{"line":277,"address":[],"length":0,"stats":{"Line":5}},{"line":278,"address":[],"length":0,"stats":{"Line":6}},{"line":279,"address":[],"length":0,"stats":{"Line":4}},{"line":284,"address":[],"length":0,"stats":{"Line":6}},{"line":285,"address":[],"length":0,"stats":{"Line":26}},{"line":286,"address":[],"length":0,"stats":{"Line":46}},{"line":287,"address":[],"length":0,"stats":{"Line":0}},{"line":289,"address":[],"length":0,"stats":{"Line":69}},{"line":290,"address":[],"length":0,"stats":{"Line":46}},{"line":291,"address":[],"length":0,"stats":{"Line":6}},{"line":298,"address":[],"length":0,"stats":{"Line":0}},{"line":299,"address":[],"length":0,"stats":{"Line":0}},{"line":300,"address":[],"length":0,"stats":{"Line":0}},{"line":301,"address":[],"length":0,"stats":{"Line":0}},{"line":303,"address":[],"length":0,"stats":{"Line":0}},{"line":304,"address":[],"length":0,"stats":{"Line":0}},{"line":305,"address":[],"length":0,"stats":{"Line":0}},{"line":306,"address":[],"length":0,"stats":{"Line":0}},{"line":308,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":0}},{"line":310,"address":[],"length":0,"stats":{"Line":0}},{"line":314,"address":[],"length":0,"stats":{"Line":0}},{"line":320,"address":[],"length":0,"stats":{"Line":20}},{"line":321,"address":[],"length":0,"stats":{"Line":40}},{"line":322,"address":[],"length":0,"stats":{"Line":80}},{"line":323,"address":[],"length":0,"stats":{"Line":60}},{"line":324,"address":[],"length":0,"stats":{"Line":20}},{"line":328,"address":[],"length":0,"stats":{"Line":26}},{"line":329,"address":[],"length":0,"stats":{"Line":270}},{"line":330,"address":[],"length":0,"stats":{"Line":78}},{"line":334,"address":[],"length":0,"stats":{"Line":16}},{"line":335,"address":[],"length":0,"stats":{"Line":48}},{"line":339,"address":[],"length":0,"stats":{"Line":16}},{"line":340,"address":[],"length":0,"stats":{"Line":16}},{"line":341,"address":[],"length":0,"stats":{"Line":16}},{"line":347,"address":[],"length":0,"stats":{"Line":4}},{"line":348,"address":[],"length":0,"stats":{"Line":12}},{"line":352,"address":[],"length":0,"stats":{"Line":4}},{"line":353,"address":[],"length":0,"stats":{"Line":4}},{"line":354,"address":[],"length":0,"stats":{"Line":4}},{"line":361,"address":[],"length":0,"stats":{"Line":4}},{"line":362,"address":[],"length":0,"stats":{"Line":12}},{"line":366,"address":[],"length":0,"stats":{"Line":4}},{"line":367,"address":[],"length":0,"stats":{"Line":4}},{"line":368,"address":[],"length":0,"stats":{"Line":4}},{"line":372,"address":[],"length":0,"stats":{"Line":4}},{"line":373,"address":[],"length":0,"stats":{"Line":4}},{"line":375,"address":[],"length":0,"stats":{"Line":0}},{"line":379,"address":[],"length":0,"stats":{"Line":0}},{"line":380,"address":[],"length":0,"stats":{"Line":0}},{"line":381,"address":[],"length":0,"stats":{"Line":0}},{"line":382,"address":[],"length":0,"stats":{"Line":0}},{"line":385,"address":[],"length":0,"stats":{"Line":0}},{"line":386,"address":[],"length":0,"stats":{"Line":0}},{"line":387,"address":[],"length":0,"stats":{"Line":0}},{"line":389,"address":[],"length":0,"stats":{"Line":0}},{"line":394,"address":[],"length":0,"stats":{"Line":11}},{"line":395,"address":[],"length":0,"stats":{"Line":22}},{"line":396,"address":[],"length":0,"stats":{"Line":22}},{"line":397,"address":[],"length":0,"stats":{"Line":0}},{"line":401,"address":[],"length":0,"stats":{"Line":22}},{"line":403,"address":[],"length":0,"stats":{"Line":11}},{"line":407,"address":[],"length":0,"stats":{"Line":0}},{"line":411,"address":[],"length":0,"stats":{"Line":0}},{"line":415,"address":[],"length":0,"stats":{"Line":0}},{"line":416,"address":[],"length":0,"stats":{"Line":0}},{"line":418,"address":[],"length":0,"stats":{"Line":0}},{"line":422,"address":[],"length":0,"stats":{"Line":0}},{"line":423,"address":[],"length":0,"stats":{"Line":0}},{"line":426,"address":[],"length":0,"stats":{"Line":0}},{"line":427,"address":[],"length":0,"stats":{"Line":0}},{"line":428,"address":[],"length":0,"stats":{"Line":0}},{"line":430,"address":[],"length":0,"stats":{"Line":0}},{"line":435,"address":[],"length":0,"stats":{"Line":24}},{"line":436,"address":[],"length":0,"stats":{"Line":72}},{"line":440,"address":[],"length":0,"stats":{"Line":26}},{"line":442,"address":[],"length":0,"stats":{"Line":52}},{"line":443,"address":[],"length":0,"stats":{"Line":25}},{"line":444,"address":[],"length":0,"stats":{"Line":21}},{"line":449,"address":[],"length":0,"stats":{"Line":5}},{"line":450,"address":[],"length":0,"stats":{"Line":10}},{"line":451,"address":[],"length":0,"stats":{"Line":15}},{"line":452,"address":[],"length":0,"stats":{"Line":5}}],"covered":121,"coverable":163},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","realmedia_tests.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Real-media tests for the FFmpeg decoder/encoder (`#[cfg(test)]` module).\n//!\n//! These exercise the real `ffmpeg-next` implementation against\n//! `tests/demo.mp4` at the repository root (H.264 1920x1080@25fps + AAC\n//! 48kHz stereo) and a full H.264 encode round-trip through `/tmp`.\n//!\n//! They live inside the crate (not `tests/`) because the crate's\n//! `#[cfg(test)]` in-memory oakcommon/oakrender stubs — which the\n//! `Frame`/`FootageDescription` paths need — are only linked for the lib\n//! test binary (`tests/` is compiled without `#[cfg(test)]` and cannot\n//! resolve those symbols; see `tests/ffi_contract_test.rs`).\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_get_duration, oakcommon_videoparams_get_frame_rate,\n\toakcommon_videoparams_get_height, oakcommon_videoparams_get_width,\n\toakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,\n};\nuse crate::decoder::{\n\tCodecStream, Decoder, K_COLOR_RANGE_DEFAULT, RenderMode, RetrieveAudioStatus,\n\tRetrieveVideoParams,\n};\nuse crate::encoder::create_from_params;\nuse crate::ffmpeg::FFmpegDecoder;\nuse crate::frame::Frame;\nuse oakcore_rs::{PixelFormat, Rational, TimeRange};\n\n/// `tests/demo.mp4` at the repository root.\nfn demo_path() -\u003e std::path::PathBuf {\n\tstd::path::Path::new(env!(\"CARGO_MANIFEST_DIR\")).join(\"../../../tests/demo.mp4\")\n}\n\nfn video_params(stream: CodecStream, time: Rational) -\u003e RetrieveVideoParams {\n\tRetrieveVideoParams {\n\t\tstream,\n\t\ttime,\n\t\tlength: TimeRange::default(),\n\t\tforce_range: K_COLOR_RANGE_DEFAULT,\n\t\tis_image_sequence: false,\n\t\timage_sequence_digits: 0,\n\t\timage_sequence_number: 0,\n\t\tmode: RenderMode::Offline,\n\t\talpha_is_premultiplied: false,\n\t}\n}\n\n/// H.264 encoder parameters: 64x64, 10 fps, `out` as the target file.\nfn h264_params(out: \u0026std::path::Path) -\u003e crate::encodingparams::EncodingParams {\n\tlet mut p = crate::encodingparams::EncodingParams::default();\n\tlet name = out.as_os_str().as_encoded_bytes();\n\tp.filename[..name.len()].copy_from_slice(name);\n\tp.format = 2; // MPEG-4 video\n\tp.video_enabled = 1;\n\tp.video_codec = 1; // H.264\n\tp.video_width = 64;\n\tp.video_height = 64;\n\tp.video_time_base_num = 1;\n\tp.video_time_base_den = 10;\n\tp.video_pixel_format = PixelFormat::F32;\n\tp.video_interlacing = 0;\n\tp.video_pixel_aspect_num = 1;\n\tp.video_pixel_aspect_den = 1;\n\tp\n}\n\n/// Build an allocated F32-RGBA frame with a moving color pattern.\nfn pattern_frame(i: i32) -\u003e Frame {\n\tlet vp = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\tunsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };\n\tlet mut f = Frame::with_params(vp);\n\tf.set_timestamp(Rational::new(i as i64, 10));\n\tf.allocate().unwrap();\n\n\tlet linesize = f.linesize_bytes() as usize;\n\tlet data = f.data_mut().unwrap();\n\tfor y in 0..64usize {\n\t\tfor x in 0..64usize {\n\t\t\tlet off = y * linesize + x * 16;\n\t\t\tlet r: f32 = if x \u003c 32 { 0.4 + i as f32 * 0.05 } else { 0.1 };\n\t\t\tlet g: f32 = y as f32 / 64.0;\n\t\t\tlet b: f32 = if x \u003e= 32 { 0.7 } else { 0.2 };\n\t\t\tdata[off..off + 4].copy_from_slice(\u0026r.to_le_bytes());\n\t\t\tdata[off + 4..off + 8].copy_from_slice(\u0026g.to_le_bytes());\n\t\t\tdata[off + 8..off + 12].copy_from_slice(\u0026b.to_le_bytes());\n\t\t\tdata[off + 12..off + 16].copy_from_slice(\u00261.0f32.to_le_bytes());\n\t\t}\n\t}\n\tf\n}\n\n#[test]\nfn probe_reports_streams_and_duration() {\n\tlet d = FFmpegDecoder::new();\n\tlet desc = d\n\t\t.probe(demo_path().to_str().unwrap(), None)\n\t\t.expect(\"demo.mp4 should probe\");\n\tassert_eq!(desc.decoder(), \"ffmpeg\");\n\t// video + audio + data (timecode) stream.\n\tassert_eq!(desc.total_stream_count(), 3);\n\tassert_eq!(desc.video_stream_count(), 1);\n\tassert_eq!(desc.audio_stream_count(), 1);\n\n\t// Video stream: 1920x1080, 25fps, 17s at 1/12800 time base.\n\tlet vp = desc.get_video_stream(0).expect(\"video stream\");\n\tassert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 1920);\n\tassert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 1080);\n\tassert_eq!(unsafe { oakcommon_videoparams_get_duration(vp.clone()) }, 17 * 12800);\n\n\tlet mut num: i32 = 0;\n\tlet mut den: i32 = 0;\n\tunsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), \u0026mut num, \u0026mut den) };\n\tassert_eq!((num, den), (25, 1));\n}\n\n#[test]\nfn decode_first_video_frame_has_dimensions_and_content() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None);\n\td.open(\u0026s).expect(\"open video stream\");\n\n\tlet f = d\n\t\t.retrieve_video_frame(\u0026video_params(s, Rational::new(0, 1)))\n\t\t.expect(\"decode first frame\");\n\tassert_eq!(f.width(), 1920);\n\tassert_eq!(f.height(), 1080);\n\tassert_eq!(f.format(), PixelFormat::F32);\n\tassert!(f.is_allocated());\n\t// Expected size: 4 channels x 4 bytes, linesize 32-byte aligned.\n\tassert_eq!(f.allocated_size(), (16 * 1920) * 1080);\n\n\t// The frame must contain non-zero pixels.\n\tlet data = f.data().expect(\"allocated data\");\n\tassert!(data.iter().any(|\u0026b| b != 0), \"decoded frame is all zeros\");\n}\n\n#[test]\nfn decode_video_frame_at_midpoint() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None);\n\td.open(\u0026s).expect(\"open video stream\");\n\n\tlet f = d\n\t\t.retrieve_video_frame(\u0026video_params(s, Rational::new(8, 1)))\n\t\t.expect(\"decode mid frame\");\n\tassert_eq!(f.width(), 1920);\n\tassert_eq!(f.height(), 1080);\n\tassert!(f.data().unwrap().iter().any(|\u0026b| b != 0));\n}\n\n#[test]\nfn audio_decode_is_non_empty() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 1, None);\n\td.open(\u0026s).expect(\"open audio stream\");\n\n\t// One second of stereo at 48 kHz.\n\tlet mut dest = vec![0f32; 48000 * 2];\n\tlet status = d\n\t\t.retrieve_audio(\n\t\t\t\u0026mut dest,\n\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t48000,\n\t\t\t0x3, // stereo mask\n\t\t)\n\t\t.expect(\"retrieve audio\");\n\tassert_eq!(status, RetrieveAudioStatus::Success);\n\n\tlet peak = dest.iter().fold(0.0f32, |a, \u0026s| a.max(s.abs()));\n\tassert!(peak \u003e 0.0, \"decoded audio is all silence\");\n}\n\n#[test]\nfn encode_h264_roundtrip_to_tmp() {\n\tlet out = std::env::temp_dir().join(format!(\"oakcodec_roundtrip_{}.mp4\", std::process::id()));\n\tlet params = h264_params(\u0026out);\n\tlet out_str = out.to_str().expect(\"utf8 temp path\").to_string();\n\n\tlet e = create_from_params(\u0026params).expect(\"create ffmpeg encoder\");\n\tassert_eq!(e.id(), \"ffmpeg\");\n\te.configure(\u0026params).expect(\"configure\");\n\te.open().expect(\"open output\");\n\n\t// Encode 10 frames with a moving pattern.\n\tfor i in 0..10 {\n\t\tlet f = pattern_frame(i);\n\t\te.write_video(\u0026f).expect(\"write video frame\");\n\t}\n\te.flush().expect(\"flush\");\n\n\t// The output exists and has a plausible size.\n\tassert!(out.exists(), \"round-trip file was not created\");\n\tassert!(out.metadata().unwrap().len() \u003e 1000, \"round-trip file is empty\");\n\n\t// Probe the result: one 64x64 video stream.\n\tlet d = FFmpegDecoder::new();\n\tlet desc = d.probe(\u0026out_str, None).expect(\"probe round-trip output\");\n\tassert_eq!(desc.video_stream_count(), 1);\n\tlet vp = desc.get_video_stream(0).expect(\"video stream\");\n\tassert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 64);\n\tassert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 64);\n\n\t// Decode the first frame of the result.\n\tlet s = CodecStream::with_block(out_str.clone(), 0, None);\n\td.open(\u0026s).expect(\"open round-trip video\");\n\tlet f = d\n\t\t.retrieve_video_frame(\u0026video_params(s, Rational::new(0, 1)))\n\t\t.expect(\"decode round-trip first frame\");\n\tassert_eq!(f.width(), 64);\n\tassert_eq!(f.height(), 64);\n\tassert_eq!(f.format(), PixelFormat::F32);\n\tassert!(f.data().unwrap().iter().any(|\u0026b| b != 0));\n\n\tlet _ = std::fs::remove_file(\u0026out);\n}\n\n#[test]\nfn audio_conform_writes_planar_pcm() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 1, None);\n\td.open(\u0026s).expect(\"open audio stream\");\n\n\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_conform_{}\", std::process::id()));\n\tstd::fs::create_dir_all(\u0026dir).unwrap();\n\tlet ch0 = dir.join(\"0.pcm\").to_string_lossy().into_owned();\n\tlet ch1 = dir.join(\"1.pcm\").to_string_lossy().into_owned();\n\n\td.conform_audio(\u0026[ch0.clone(), ch1.clone()], 48000, 0x3, 4, None)\n\t\t.expect(\"conform to f32 planar\");\n\n\tfor path in [\u0026ch0, \u0026ch1] {\n\t\tlet meta = std::fs::metadata(path).expect(\"conform output exists\");\n\t\tassert!(meta.len() \u003e 0, \"conform file is empty\");\n\t\t// 1 second at 48kHz * 4 bytes = 192 KB minimum.\n\t\tassert!(meta.len() \u003e= 192_000, \"conform file too short: {}\", meta.len());\n\t}\n\n\tlet _ = std::fs::remove_dir_all(\u0026dir);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","task.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Background task submission hook (`include/codec/task.h`).\n//!\n//! The codec module needs occasional background work (audio conforms,\n//! proxy transcodes). The task system itself splits out at milestone M8;\n//! until then oakcodec exposes a single global submit callback. A host\n//! (M8: oaktask) registers with [`set_task_submit_cb`]; the conform/proxy\n//! managers call it whenever they need a task. With no callback, managers\n//! report work as unavailable — they never crash and never block.\n\nuse std::ffi::{c_void, CString};\nuse std::sync::Mutex;\n\nuse crate::error::{Error, OAKCODEC_OK};\n\n/// Kinds of background tasks oakcodec can request.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum TaskKind {\n\t/// Audio conform to pcm cache files.\n\tConform = 0,\n\t/// Video proxy transcode.\n\tProxy = 1,\n}\n\n/// Description of one background task request.\n///\n/// All strings are borrowed and only valid for the duration of the submit\n/// call; the callback must copy anything it retains.\n#[repr(C)]\npub struct TaskRequest\u003c'a\u003e {\n\t/// `TaskKind`.\n\tpub kind: TaskKind,\n\t/// Source media filename.\n\tpub input_filename: \u0026'a str,\n\t/// Final destination path (see field docs in include/codec/task.h).\n\tpub output_filename: \u0026'a str,\n\t/// Stream inside the source media.\n\tpub stream_index: i32,\n\t/// Conform: target sample rate.\n\tpub sample_rate: i32,\n\t/// Conform: target channel layout mask.\n\tpub channel_layout: u64,\n\t/// Conform: target sample format (enum as int).\n\tpub sample_format: i32,\n\t/// Proxy: target width (0 = unspecified/divider-based).\n\tpub proxy_width: i32,\n\t/// Proxy: target height (0 = unspecified/divider-based).\n\tpub proxy_height: i32,\n}\n\n/// Task submit callback signature.\n///\n/// Returns `Ok(())` if the task was accepted (completed synchronously or\n/// queued), `Err` if the request was rejected.\npub type TaskSubmitFn = dyn Fn(\u0026TaskRequest, *mut std::ffi::c_void) -\u003e crate::error::Result\u003c()\u003e;\n\n/// `OakCodecTaskRequest` — C ABI mirror of [`TaskRequest`] for the submit\n/// callback; see `include/codec/task.h`. Strings are borrowed C pointers,\n/// valid only for the duration of the call.\n#[repr(C)]\npub struct OakCodecTaskRequest {\n\t/// `TaskKind` value.\n\tpub kind: i32,\n\t/// Source media filename.\n\tpub input_filename: *const std::ffi::c_char,\n\t/// Final destination path.\n\tpub output_filename: *const std::ffi::c_char,\n\t/// Stream inside the source media.\n\tpub stream_index: i32,\n\t/// Conform: target sample rate.\n\tpub sample_rate: i32,\n\t/// Conform: target channel-layout mask.\n\tpub channel_layout: u64,\n\t/// Conform: target sample format (enum as int).\n\tpub sample_format: i32,\n\t/// Proxy: target width (0 = unspecified/divider-based).\n\tpub proxy_width: i32,\n\t/// Proxy: target height (0 = unspecified/divider-based).\n\tpub proxy_height: i32,\n}\n\n/// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see\n/// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a\n/// negative `OAKCODEC_E_*` code.\npub type OakCodecTaskSubmitFn = unsafe extern \"C\" fn(\n\treq: *const OakCodecTaskRequest,\n\tuserdata: *mut std::ffi::c_void,\n) -\u003e i32;\n\n/// One registered submit callback (extern-C from the host, or a crate\n/// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair.\nenum SubmitCb {\n\t/// No callback registered.\n\tNone,\n\t/// Extern-C callback registered via `oakcodec_set_task_submit_cb`.\n\tExtern {\n\t\t/// The C function pointer.\n\t\tcb: OakCodecTaskSubmitFn,\n\t\t/// Opaque userdata passed back on each call.\n\t\tuserdata: *mut c_void,\n\t},\n\t/// Crate-internal Rust closure registered via [`set_task_submit_cb`].\n\tRust {\n\t\t/// Raw fat-pointer to the `\u0026'static TaskSubmitFn` (kept `*const` so\n\t\t/// the registry is `Send`).\n\t\tcb: *const TaskSubmitFn,\n\t\t/// Opaque userdata passed back on each call.\n\t\tuserdata: *mut c_void,\n\t},\n}\n\n// # Safety: the stored pointers (extern-C fn pointer, fat pointer to a\n// 'static closure, userdata) are only dereferenced/called while holding the\n// registry mutex; the Rust closure is 'static and the extern-C fn outlives\n// registration by contract. Moving the enum between threads under the lock\n// therefore cannot alias.\nunsafe impl Send for SubmitCb {}\n\n/// The global task submit callback registry. Only one callback is held at\n/// a time; registering replaces it, `None` clears it. Thread-safe.\nstatic TASK_SUBMIT: Mutex\u003cSubmitCb\u003e = Mutex::new(SubmitCb::None);\n\n/// Registers (or replaces) the global task submit callback. Pass `None` to\n/// unregister. Thread-safe. Interim state (pre-M8): nobody registers and all\n/// task-dependent work reports unavailable.\npub fn set_task_submit_cb(cb: Option\u003c\u0026'static TaskSubmitFn\u003e, userdata: *mut std::ffi::c_void) {\n\tlet mut g = TASK_SUBMIT.lock().unwrap();\n\t*g = match cb {\n\t\tSome(cb) =\u003e SubmitCb::Rust {\n\t\t\tcb: cb as *const TaskSubmitFn,\n\t\t\tuserdata,\n\t\t},\n\t\tNone =\u003e SubmitCb::None,\n\t};\n}\n\n/// Register an extern-C submit callback (used by `ffi::task`).\n///\n/// Mirrors `oakcodec_set_task_submit_cb`: a `None` pointer clears it.\npub(crate) fn set_task_submit_cb_extern(\n\tcb: Option\u003cOakCodecTaskSubmitFn\u003e,\n\tuserdata: *mut std::ffi::c_void,\n) {\n\tlet mut g = TASK_SUBMIT.lock().unwrap();\n\t*g = match cb {\n\t\tSome(cb) =\u003e SubmitCb::Extern { cb, userdata },\n\t\tNone =\u003e SubmitCb::None,\n\t};\n}\n\n/// Returns 1 if a submit callback is currently registered, else 0.\n/// Thread-safe.\npub fn task_submit_is_registered() -\u003e bool {\n\tlet g = TASK_SUBMIT.lock().unwrap();\n\t!matches!(\u0026*g, SubmitCb::None)\n}\n\n/// Submit a task through the registered callback, if any.\n///\n/// Returns `Ok(false)` when no callback is registered (nothing submitted),\n/// `Ok(true)` when accepted, or `Err` when the callback rejected it.\npub fn submit_task(\n\treq: \u0026TaskRequest,\n) -\u003e crate::error::Result\u003cbool\u003e {\n\tlet g = TASK_SUBMIT.lock().unwrap();\n\tmatch \u0026*g {\n\t\tSubmitCb::None =\u003e Ok(false),\n\t\tSubmitCb::Extern { cb, userdata } =\u003e {\n\t\t\t// Bind the C strings to locals so the temporaries outlive the\n\t\t\t// callback call (their pointers feed the request struct).\n\t\t\tlet in_c = cstring_or_empty(req.input_filename);\n\t\t\tlet out_c = cstring_or_empty(req.output_filename);\n\t\t\tlet creq = OakCodecTaskRequest {\n\t\t\t\tkind: req.kind as i32,\n\t\t\t\tinput_filename: in_c.as_ptr(),\n\t\t\t\toutput_filename: out_c.as_ptr(),\n\t\t\t\tstream_index: req.stream_index,\n\t\t\t\tsample_rate: req.sample_rate,\n\t\t\t\tchannel_layout: req.channel_layout,\n\t\t\t\tsample_format: req.sample_format,\n\t\t\t\tproxy_width: req.proxy_width,\n\t\t\t\tproxy_height: req.proxy_height,\n\t\t\t};\n\t\t\t// # Safety: the callback is a C function we registered; passing a\n\t\t\t// request whose string pointers are alive for the call duration.\n\t\t\tlet ret = unsafe { cb(\u0026creq, *userdata) };\n\t\t\tif ret == OAKCODEC_OK {\n\t\t\t\tOk(true)\n\t\t\t} else {\n\t\t\t\tErr(Error::Failed(format!(\"task submit rejected (code {})\", ret)))\n\t\t\t}\n\t\t}\n\t\tSubmitCb::Rust { cb, userdata } =\u003e {\n\t\t\t// # Safety: the fat pointer was stored by set_task_submit_cb and\n\t\t\t// points to a 'static closure that outlives this call.\n\t\t\tlet cb = unsafe { \u0026**cb };\n\t\t\tmatch cb(req, *userdata) {\n\t\t\t\tOk(()) =\u003e Ok(true),\n\t\t\t\tErr(e) =\u003e Err(e),\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// Build a NUL-terminated C string from a Rust string; empty on embedded\n/// NUL (callers pass sane filenames, so this is defensive only).\nfn cstring_or_empty(s: \u0026str) -\u003e CString {\n\tCString::new(s).unwrap_or_else(|_| CString::new(\"\").unwrap())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\t// Same registry lock the conform/proxy/ffi tests use: the submit\n\t// callback is process-global and every test that mutates it must\n\t// serialize on the same mutex.\n\tuse crate::conformmanager::test_util::REG_LOCK;\n\n\tunsafe extern \"C\" fn reject_cb(\n\t\t_req: *const OakCodecTaskRequest,\n\t\t_ud: *mut std::ffi::c_void,\n\t) -\u003e i32 {\n\t\t-1 // rejected\n\t}\n\n\t#[test]\n\tfn submit_via_rust_closure_and_clear() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\n\t\t// A Rust closure that accepts and records the request.\n\t\tlet accepted = std::sync::Arc::new(std::sync::Mutex::new(false));\n\t\tlet recorded = std::sync::Arc::new(std::sync::Mutex::new(None::\u003cString\u003e));\n\t\tlet acc = accepted.clone();\n\t\tlet rec = recorded.clone();\n\t\tlet cb: \u0026'static TaskSubmitFn = Box::leak(Box::new(\n\t\t\tmove |req: \u0026TaskRequest, _ud: *mut std::ffi::c_void| {\n\t\t\t\t*acc.lock().unwrap() = true;\n\t\t\t\t*rec.lock().unwrap() = Some(req.output_filename.to_string());\n\t\t\t\tOk(())\n\t\t\t},\n\t\t));\n\t\tset_task_submit_cb(Some(cb), std::ptr::null_mut());\n\t\tassert!(task_submit_is_registered());\n\n\t\tlet req = TaskRequest {\n\t\t\tkind: TaskKind::Conform,\n\t\t\tinput_filename: \"in.mp4\",\n\t\t\toutput_filename: \"out.pcm\",\n\t\t\tstream_index: 1,\n\t\t\tsample_rate: 48000,\n\t\t\tchannel_layout: 0x3,\n\t\t\tsample_format: 10,\n\t\t\tproxy_width: 0,\n\t\t\tproxy_height: 0,\n\t\t};\n\t\tassert!(submit_task(\u0026req).unwrap());\n\t\tassert!(*accepted.lock().unwrap());\n\t\tassert_eq!(recorded.lock().unwrap().as_deref(), Some(\"out.pcm\"));\n\n\t\t// Clearing the callback: nothing submitted.\n\t\tset_task_submit_cb(None, std::ptr::null_mut());\n\t\tassert!(!task_submit_is_registered());\n\t\tassert!(!submit_task(\u0026req).unwrap());\n\t}\n\n\t#[test]\n\tfn extern_cb_reject_maps_to_err() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\tset_task_submit_cb_extern(Some(reject_cb), std::ptr::null_mut());\n\t\tlet req = TaskRequest {\n\t\t\tkind: TaskKind::Proxy,\n\t\t\tinput_filename: \"in.mp4\",\n\t\t\toutput_filename: \"out.mp4\",\n\t\t\tstream_index: 0,\n\t\t\tsample_rate: 0,\n\t\t\tchannel_layout: 0,\n\t\t\tsample_format: 0,\n\t\t\tproxy_width: 1280,\n\t\t\tproxy_height: 720,\n\t\t};\n\t\tassert!(submit_task(\u0026req).is_err());\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n\n\t#[test]\n\tfn extern_cb_accept_returns_ok() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\tset_task_submit_cb_extern(Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut());\n\t\tlet req = TaskRequest {\n\t\t\tkind: TaskKind::Conform,\n\t\t\tinput_filename: \"in.mp4\",\n\t\t\toutput_filename: \"out.pcm\",\n\t\t\tstream_index: 0,\n\t\t\tsample_rate: 0,\n\t\t\tchannel_layout: 0,\n\t\t\tsample_format: 0,\n\t\t\tproxy_width: 0,\n\t\t\tproxy_height: 0,\n\t\t};\n\t\tassert!(submit_task(\u0026req).unwrap());\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n\n\t#[test]\n\tfn cstring_or_empty_handles_embedded_nul() {\n\t\t// Embedded NUL -\u003e empty string (defensive).\n\t\tlet c = cstring_or_empty(\"a\\0b\");\n\t\tassert_eq!(c.as_c_str().to_bytes(), b\"\");\n\t\tassert_eq!(cstring_or_empty(\"ok\").as_c_str().to_bytes(), b\"ok\");\n\t}\n\n\t#[test]\n\tfn task_kind_values_match_abi() {\n\t\tassert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32);\n\t\tassert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32);\n\t}\n\n\t// ABI constants mirrored from include/codec/task.h.\n\tconst OAKCODEC_TASK_CONFORM: i32 = 0;\n\tconst OAKCODEC_TASK_PROXY: i32 = 1;\n}\n","traces":[{"line":142,"address":[],"length":0,"stats":{"Line":2}},{"line":143,"address":[],"length":0,"stats":{"Line":8}},{"line":144,"address":[],"length":0,"stats":{"Line":4}},{"line":145,"address":[],"length":0,"stats":{"Line":2}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":156,"address":[],"length":0,"stats":{"Line":16}},{"line":160,"address":[],"length":0,"stats":{"Line":64}},{"line":161,"address":[],"length":0,"stats":{"Line":32}},{"line":162,"address":[],"length":0,"stats":{"Line":12}},{"line":163,"address":[],"length":0,"stats":{"Line":10}},{"line":169,"address":[],"length":0,"stats":{"Line":12}},{"line":170,"address":[],"length":0,"stats":{"Line":48}},{"line":171,"address":[],"length":0,"stats":{"Line":17}},{"line":178,"address":[],"length":0,"stats":{"Line":7}},{"line":181,"address":[],"length":0,"stats":{"Line":28}},{"line":182,"address":[],"length":0,"stats":{"Line":7}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":10}},{"line":187,"address":[],"length":0,"stats":{"Line":15}},{"line":188,"address":[],"length":0,"stats":{"Line":15}},{"line":190,"address":[],"length":0,"stats":{"Line":10}},{"line":191,"address":[],"length":0,"stats":{"Line":10}},{"line":192,"address":[],"length":0,"stats":{"Line":10}},{"line":193,"address":[],"length":0,"stats":{"Line":10}},{"line":194,"address":[],"length":0,"stats":{"Line":10}},{"line":195,"address":[],"length":0,"stats":{"Line":10}},{"line":196,"address":[],"length":0,"stats":{"Line":10}},{"line":197,"address":[],"length":0,"stats":{"Line":5}},{"line":198,"address":[],"length":0,"stats":{"Line":5}},{"line":202,"address":[],"length":0,"stats":{"Line":15}},{"line":203,"address":[],"length":0,"stats":{"Line":5}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":206,"address":[],"length":0,"stats":{"Line":1}},{"line":209,"address":[],"length":0,"stats":{"Line":2}},{"line":212,"address":[],"length":0,"stats":{"Line":2}},{"line":213,"address":[],"length":0,"stats":{"Line":2}},{"line":214,"address":[],"length":0,"stats":{"Line":1}},{"line":215,"address":[],"length":0,"stats":{"Line":0}},{"line":223,"address":[],"length":0,"stats":{"Line":12}},{"line":224,"address":[],"length":0,"stats":{"Line":38}}],"covered":41,"coverable":42},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","timecodemetadata.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::TimecodeMetadata` — parse source timecode strings.\n//!\n//! Mirrors `src/codec/src/timecodemetadata.h`: parse an SMPTE timecode\n//! string or a BWF `time reference` chunk into a rational media timestamp.\n\nuse oakcore_rs::Rational;\n\n/// `TimecodeMetadata::SourceTime` — the parsed result.\npub struct SourceTime {\n\t/// Media timestamp, rational seconds.\n\tpub time: Rational,\n\t/// Source string (normalized form, or the raw input on failure).\n\tpub source: String,\n\t/// Parse succeeded.\n\tpub valid: bool,\n}\n\n/// Trim the same whitespace set as the C++ `trimmed()` helper\n/// (`\" \\t\\n\\r\\f\\v\"`): space, tab, LF, CR, form feed, vertical tab.\nfn trimmed(s: \u0026str) -\u003e String {\n\ts.trim_matches(|c: char| {\n\t\tc == ' ' || c == '\\t' || c == '\\n' || c == '\\r' || c == '\\u{000c}' || c == '\\u{000b}'\n\t})\n\t.to_string()\n}\n\n/// Port of `str_to_int64_empty_tolerant`: an empty field is a valid `0`,\n/// any other unparseable field is an error (`None`).\nfn str_to_int64_empty_tolerant(s: \u0026str) -\u003e Option\u003ci64\u003e {\n\tif s.is_empty() {\n\t\tSome(0)\n\t} else {\n\t\ts.parse::\u003ci64\u003e().ok()\n\t}\n}\n\n/// `std::llround` (round half away from zero, truncate to `i64`).\nfn llround(x: f64) -\u003e i64 {\n\tx.round() as i64\n}\n\n/// `timebase.flipped().to_double()` — seconds-per-frame to frames-per-second.\nfn frame_rate(timebase: \u0026Rational) -\u003e f64 {\n\ttimebase.denominator() as f64 / timebase.numerator() as f64\n}\n\n/// Port of `olive::core::Timecode::timecode_to_time` for the\n/// `k_timecode_non_drop_frame` / `k_timecode_drop_frame` displays.\n///\n/// `drop_frame` is true when the (already trimmed) string contains a `;`.\n/// Returns `None` on any parse failure.\nfn timecode_to_time(timecode: \u0026str, timebase: \u0026Rational, drop_frame: bool) -\u003e Option\u003cRational\u003e {\n\tlet mut tokens: Vec\u003c\u0026str\u003e = timecode.split(|c| c == ':' || c == ';').collect();\n\tlet element_count = 4;\n\n\t// Keep only the leading `HH:MM:SS:FF` tokens.\n\tif tokens.len() \u003e element_count {\n\t\ttokens.truncate(element_count);\n\t}\n\t// Pad missing leading fields with empty strings (which parse to 0).\n\twhile tokens.len() \u003c element_count {\n\t\ttokens.insert(0, \"\");\n\t}\n\n\tlet negative = timecode.starts_with('-');\n\n\tlet hours = str_to_int64_empty_tolerant(tokens[0])?;\n\tlet mins = str_to_int64_empty_tolerant(tokens[1])?;\n\tlet secs = str_to_int64_empty_tolerant(tokens[2])?;\n\tlet frames = str_to_int64_empty_tolerant(tokens[3])?;\n\n\tlet fr = frame_rate(timebase);\n\tlet rounded_frame_rate = llround(fr);\n\n\tlet sec_count = hours * 3600 + mins * 60 + secs;\n\tlet mut frame_count = sec_count * rounded_frame_rate + frames;\n\n\tif drop_frame \u0026\u0026 timebase.numerator() != 1 {\n\t\t// `timebase_is_drop_frame(timebase)`: numerator != 1.\n\t\t// Number of frames dropped on the minute marks ≈ 6% of the framerate.\n\t\tlet drop_frames = llround(fr * (2.0 / 30.0));\n\n\t\t// `d` and `m` are derived from the real (non-rounded) framerate.\n\t\tlet real_fr_ts = llround(sec_count as f64 * fr) + frames;\n\n\t\tlet frames_per10_minutes = llround(fr * 600.0);\n\t\tlet d = real_fr_ts / frames_per10_minutes;\n\t\tlet m = real_fr_ts % frames_per10_minutes;\n\n\t\tif m \u003e drop_frames {\n\t\t\tframe_count -= drop_frames\n\t\t\t\t* ((m - drop_frames) / (llround(fr) * 60 - drop_frames));\n\t\t}\n\t\tframe_count -= drop_frames * 9 * d;\n\t}\n\n\t// `timestamp_to_time`: `timebase.num * frame_count / timebase.den`, reduced.\n\tlet mut time = timebase.timestamp_to_time(frame_count);\n\n\tif negative {\n\t\ttime = time * Rational::new(-1, 1);\n\t}\n\n\tSome(time)\n}\n\n/// Signed Euclidean GCD on absolute values (mirrors `i64_gcd`).\nfn gcd_u64(mut a: u64, mut b: u64) -\u003e u64 {\n\twhile b != 0 {\n\t\tlet t = a % b;\n\t\ta = b;\n\t\tb = t;\n\t}\n\ta\n}\n\nimpl SourceTime {\n\t/// New invalid (empty) source time.\n\tpub fn invalid() -\u003e Self {\n\t\tSourceTime {\n\t\t\ttime: Rational::NULL,\n\t\t\tsource: String::new(),\n\t\t\tvalid: false,\n\t\t}\n\t}\n\n\t/// Parse an SMPTE timecode string at the given timebase\n\t/// (`from_timecode_string`).\n\t///\n\t/// The string is trimmed; an empty result is invalid. A `;` separator\n\t/// selects drop-frame, otherwise non-drop-frame. On any parse failure\n\t/// the result is invalid and `source` holds the trimmed raw input.\n\tpub fn from_timecode_string(timecode: \u0026str, timebase: \u0026Rational) -\u003e SourceTime {\n\t\tlet trimmed_tc = trimmed(timecode);\n\t\tif trimmed_tc.is_empty() {\n\t\t\treturn SourceTime::invalid();\n\t\t}\n\n\t\tlet drop_frame = trimmed_tc.contains(';');\n\t\tmatch timecode_to_time(\u0026trimmed_tc, timebase, drop_frame) {\n\t\t\tSome(time) =\u003e SourceTime {\n\t\t\t\ttime,\n\t\t\t\tsource: \"timecode\".to_string(),\n\t\t\t\tvalid: true,\n\t\t\t},\n\t\t\tNone =\u003e SourceTime {\n\t\t\t\ttime: Rational::NULL,\n\t\t\t\tsource: trimmed_tc,\n\t\t\t\tvalid: false,\n\t\t\t},\n\t\t}\n\t}\n\n\t/// Parse a BWF `time reference` chunk into a timestamp\n\t/// (`from_bwf_time_reference`).\n\t///\n\t/// A non-positive `sample_rate`, or a string that is not a single\n\t/// base-10 unsigned integer, yields an invalid result. The parsed\n\t/// sample count over `sample_rate` is reduced by their GCD; if the\n\t/// reduced numerator or denominator exceed `i32::MAX` the value falls\n\t/// back to the (capped, reduced) `Rational::new(samples, sample_rate)`\n\t/// approximation, since `oakcore_rs::Rational` exposes no `from_double`.\n\tpub fn from_bwf_time_reference(time_reference: \u0026str, sample_rate: i32) -\u003e SourceTime {\n\t\tif sample_rate \u003c= 0 {\n\t\t\treturn SourceTime::invalid();\n\t\t}\n\n\t\tlet trimmed_ref = trimmed(time_reference);\n\t\t// `std::strtoull` base 10 with a \"whole string consumed\" check:\n\t\t// at least one digit, no leading/trailing junk.\n\t\tlet samples: u64 = match trimmed_ref.parse() {\n\t\t\tOk(v) =\u003e v,\n\t\t\tErr(_) =\u003e return SourceTime::invalid(),\n\t\t};\n\n\t\tlet mut numerator = samples;\n\t\tlet mut denominator = sample_rate as u64;\n\t\tlet divisor = gcd_u64(numerator, denominator);\n\t\tnumerator /= divisor;\n\t\tdenominator /= divisor;\n\n\t\tlet rational_limit = i32::MAX as u64;\n\t\tlet time = if numerator \u003c= rational_limit \u0026\u0026 denominator \u003c= rational_limit {\n\t\t\tRational::new(numerator as i64, denominator as i64)\n\t\t} else {\n\t\t\t// `Rational::from_double` is not part of the oakcore_rs public\n\t\t\t// API; `Rational::new` applies the same INT_MAX-capped reduction\n\t\t\t// (FFmpeg `av_reduce`), which is the intended approximation.\n\t\t\tlet n = i64::try_from(samples).unwrap_or(i64::MAX);\n\t\t\tRational::new(n, sample_rate as i64)\n\t\t};\n\n\t\tSourceTime {\n\t\t\ttime,\n\t\t\tsource: \"bwf_time_reference\".to_string(),\n\t\t\tvalid: true,\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn tc(s: \u0026str, timebase: \u0026Rational) -\u003e SourceTime {\n\t\tSourceTime::from_timecode_string(s, timebase)\n\t}\n\n\t#[test]\n\tfn invalid_is_null() {\n\t\tlet inv = SourceTime::invalid();\n\t\tassert!(inv.time.is_null());\n\t\tassert!(inv.source.is_empty());\n\t\tassert!(!inv.valid);\n\t}\n\n\t#[test]\n\tfn empty_or_whitespace_is_invalid() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tassert!(!tc(\"\", \u0026tb).valid);\n\t\tassert!(!tc(\" \\t\\n\", \u0026tb).valid);\n\t}\n\n\t#[test]\n\tfn non_drop_integral_fps() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"00:00:00:10\", \u0026tb);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.source, \"timecode\");\n\t\tassert_eq!(r.time, Rational::new(2, 5)); // 10 frames @ 25fps = 0.4s\n\t}\n\n\t#[test]\n\tfn non_drop_leading_zeros_and_whitespace() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\" 01:02:03:04 \", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// 1*3600+2*60+3 = 3723s * 25 + 4 frames = 93079 frames\n\t\tassert_eq!(r.time, Rational::new(93079, 25));\n\t}\n\n\t#[test]\n\tfn missing_leading_fields_pad_from_front() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"00:01:02\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// C++ pads missing leading fields at the front, so 3 fields become\n\t\t// [\"\",00,01,02] = HH=0, MM=0, SS=1, FF=2 -\u003e 27 frames @25fps.\n\t\tassert_eq!(r.time, Rational::new(27, 25));\n\t}\n\n\t#[test]\n\tfn negative_timecode_is_negated() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"-00:00:00:05\", \u0026tb);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.time, Rational::new(-1, 5));\n\t}\n\n\t#[test]\n\tfn parse_failure_is_invalid_with_raw_source() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"abc:def\", \u0026tb);\n\t\tassert!(!r.valid);\n\t\tassert!(r.time.is_null());\n\t\tassert_eq!(r.source, \"abc:def\");\n\t}\n\n\t#[test]\n\tfn drop_frame_29_97_first_minute_no_correction() {\n\t\t// 29.97fps -\u003e timebase 1001/30000.\n\t\tlet tb = Rational::new(1001, 30000);\n\t\tlet r = tc(\"00:01:00;00\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// No frames dropped in the first minute: `frame_count` stays at\n\t\t// 60s * 30fps = 1800 frames (a minute boundary at 29.97 is 60.06s).\n\t\tassert_eq!(r.time, tb.timestamp_to_time(1800));\n\t}\n\n\t#[test]\n\tfn drop_frame_29_97_later_minute_corrects() {\n\t\tlet tb = Rational::new(1001, 30000);\n\t\t// At 10 minutes of drop-frame timecode the running correction is\n\t\t// 2 frames dropped per minute for 9 of the 10 minutes (18 frames).\n\t\tlet r = tc(\"00:10:00;00\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// NDF would be 10*60*30 = 18000 frames; 18 dropped -\u003e 17982 frames.\n\t\tassert_eq!(r.time, tb.timestamp_to_time(17982));\n\t}\n\n\t#[test]\n\tfn non_drop_uses_colon() {\n\t\tlet tb = Rational::new(1001, 30000);\n\t\tlet r = tc(\"00:10:00:00\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// Non-drop: 18000 frames, no correction.\n\t\tassert_eq!(r.time, tb.timestamp_to_time(18000));\n\t}\n\n\t#[test]\n\tfn bwf_valid_reduces() {\n\t\tlet r = SourceTime::from_bwf_time_reference(\"48000\", 48000);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.source, \"bwf_time_reference\");\n\t\tassert_eq!(r.time, Rational::new(1, 1));\n\t}\n\n\t#[test]\n\tfn bwf_valid_whitespace_trimmed() {\n\t\tlet r = SourceTime::from_bwf_time_reference(\" 24000 \", 48000);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.time, Rational::new(1, 2));\n\t}\n\n\t#[test]\n\tfn bwf_bad_sample_rate_is_invalid() {\n\t\tfor sr in [0, -1] {\n\t\t\tlet r = SourceTime::from_bwf_time_reference(\"100\", sr);\n\t\t\tassert!(!r.valid);\n\t\t\tassert!(r.time.is_null());\n\t\t}\n\t}\n\n\t#[test]\n\tfn bwf_unparseable_is_invalid() {\n\t\tfor s in [\"\", \" \", \"abc\", \"12x\", \"1.5\", \"-5\", \"1 2\"] {\n\t\t\tlet r = SourceTime::from_bwf_time_reference(s, 48000);\n\t\t\tassert!(!r.valid, \"should reject {:?}\", s);\n\t\t\tassert!(r.time.is_null());\n\t\t}\n\t}\n\n\t#[test]\n\tfn bwf_no_common_divisor() {\n\t\tlet r = SourceTime::from_bwf_time_reference(\"3\", 48000);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.time, Rational::new(1, 16000));\n\t}\n}\n","traces":[{"line":36,"address":[],"length":0,"stats":{"Line":21}},{"line":37,"address":[],"length":0,"stats":{"Line":91}},{"line":38,"address":[],"length":0,"stats":{"Line":217}},{"line":45,"address":[],"length":0,"stats":{"Line":35}},{"line":46,"address":[],"length":0,"stats":{"Line":70}},{"line":47,"address":[],"length":0,"stats":{"Line":3}},{"line":49,"address":[],"length":0,"stats":{"Line":96}},{"line":54,"address":[],"length":0,"stats":{"Line":15}},{"line":55,"address":[],"length":0,"stats":{"Line":15}},{"line":59,"address":[],"length":0,"stats":{"Line":8}},{"line":60,"address":[],"length":0,"stats":{"Line":16}},{"line":68,"address":[],"length":0,"stats":{"Line":9}},{"line":69,"address":[],"length":0,"stats":{"Line":209}},{"line":70,"address":[],"length":0,"stats":{"Line":18}},{"line":73,"address":[],"length":0,"stats":{"Line":18}},{"line":74,"address":[],"length":0,"stats":{"Line":0}},{"line":77,"address":[],"length":0,"stats":{"Line":27}},{"line":78,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":27}},{"line":83,"address":[],"length":0,"stats":{"Line":27}},{"line":84,"address":[],"length":0,"stats":{"Line":27}},{"line":85,"address":[],"length":0,"stats":{"Line":27}},{"line":86,"address":[],"length":0,"stats":{"Line":24}},{"line":88,"address":[],"length":0,"stats":{"Line":24}},{"line":89,"address":[],"length":0,"stats":{"Line":24}},{"line":91,"address":[],"length":0,"stats":{"Line":16}},{"line":92,"address":[],"length":0,"stats":{"Line":16}},{"line":94,"address":[],"length":0,"stats":{"Line":10}},{"line":97,"address":[],"length":0,"stats":{"Line":6}},{"line":100,"address":[],"length":0,"stats":{"Line":6}},{"line":102,"address":[],"length":0,"stats":{"Line":6}},{"line":103,"address":[],"length":0,"stats":{"Line":4}},{"line":104,"address":[],"length":0,"stats":{"Line":4}},{"line":106,"address":[],"length":0,"stats":{"Line":3}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":2}},{"line":110,"address":[],"length":0,"stats":{"Line":2}},{"line":114,"address":[],"length":0,"stats":{"Line":32}},{"line":116,"address":[],"length":0,"stats":{"Line":9}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":8}},{"line":124,"address":[],"length":0,"stats":{"Line":3}},{"line":125,"address":[],"length":0,"stats":{"Line":13}},{"line":126,"address":[],"length":0,"stats":{"Line":15}},{"line":127,"address":[],"length":0,"stats":{"Line":5}},{"line":128,"address":[],"length":0,"stats":{"Line":5}},{"line":130,"address":[],"length":0,"stats":{"Line":3}},{"line":135,"address":[],"length":0,"stats":{"Line":12}},{"line":138,"address":[],"length":0,"stats":{"Line":12}},{"line":149,"address":[],"length":0,"stats":{"Line":11}},{"line":150,"address":[],"length":0,"stats":{"Line":33}},{"line":151,"address":[],"length":0,"stats":{"Line":22}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":18}},{"line":156,"address":[],"length":0,"stats":{"Line":27}},{"line":159,"address":[],"length":0,"stats":{"Line":8}},{"line":179,"address":[],"length":0,"stats":{"Line":12}},{"line":180,"address":[],"length":0,"stats":{"Line":12}},{"line":181,"address":[],"length":0,"stats":{"Line":2}},{"line":184,"address":[],"length":0,"stats":{"Line":30}},{"line":187,"address":[],"length":0,"stats":{"Line":16}},{"line":188,"address":[],"length":0,"stats":{"Line":6}},{"line":189,"address":[],"length":0,"stats":{"Line":7}},{"line":192,"address":[],"length":0,"stats":{"Line":6}},{"line":193,"address":[],"length":0,"stats":{"Line":6}},{"line":194,"address":[],"length":0,"stats":{"Line":12}},{"line":195,"address":[],"length":0,"stats":{"Line":3}},{"line":196,"address":[],"length":0,"stats":{"Line":3}},{"line":198,"address":[],"length":0,"stats":{"Line":6}},{"line":199,"address":[],"length":0,"stats":{"Line":9}},{"line":200,"address":[],"length":0,"stats":{"Line":9}},{"line":205,"address":[],"length":0,"stats":{"Line":0}},{"line":206,"address":[],"length":0,"stats":{"Line":0}},{"line":211,"address":[],"length":0,"stats":{"Line":3}}],"covered":71,"coverable":74}]};
var previousData = {"files":[{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","common.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational,\n//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils,\n//! colortransform).\n//!\n//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`,\n//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref,\n//! release, abi_version}` layout from `include/common/handle.h`, so the\n//! codec module can store them by value and pass them straight across\n//! the FFI boundary. Function signatures match the public headers\n//! verbatim; symbols resolve at link time.\n//!\n//! The oakcore audio parameters use a pointer-based C ABI instead of the\n//! by-value handle convention: `oakcore_audioparams_*` take and return\n//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/\n//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw\n//! pointers to the crate's handle structs; `oakcore_audioparams_time_base`\n//! returns a newly allocated rational the caller must release with\n//! `oakcore_rational_free`.\n\nuse std::ffi::{c_char, c_int, c_void};\n\n/// `OakVideoParams` — refcounted video-parameter handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakVideoParams {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (see [`OAKCODEC_ABI_VERSION`]).\n\tpub abi_version: u32,\n}\n\n/// `OakAudioParams` — refcounted audio-parameter handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakAudioParams {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakSubtitleParams` — refcounted subtitle-parameter handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakSubtitleParams {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec\n/// only stores and forwards it).\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakNodeBlock {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n// The handle structs are opaque refcounted handles pointing into a C\n// library; the boxed objects are independently synchronized there, so\n// moving a handle between threads is sound.\nunsafe impl Send for OakVideoParams {}\nunsafe impl Sync for OakVideoParams {}\nunsafe impl Send for OakAudioParams {}\nunsafe impl Sync for OakAudioParams {}\nunsafe impl Send for OakSubtitleParams {}\nunsafe impl Sync for OakSubtitleParams {}\nunsafe impl Send for OakNodeBlock {}\nunsafe impl Sync for OakNodeBlock {}\n\nextern \"C\" {\n\t/// `oakcommon_videoparams_init`.\n\tpub fn oakcommon_videoparams_init() -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_init_basic`.\n\tpub fn oakcommon_videoparams_init_basic(width: c_int, height: c_int) -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_init_with_time_base`.\n\tpub fn oakcommon_videoparams_init_with_time_base(\n\t\twidth: c_int,\n\t\theight: c_int,\n\t\ttime_base_num: i64,\n\t\ttime_base_den: i64,\n\t) -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_free` (NULL/empty no-op).\n\tpub fn oakcommon_videoparams_free(params: *mut OakVideoParams);\n\t/// `oakcommon_videoparams_get_width`.\n\tpub fn oakcommon_videoparams_get_width(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_height`.\n\tpub fn oakcommon_videoparams_get_height(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_format`.\n\tpub fn oakcommon_videoparams_get_format(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_time_base` (num/den out).\n\tpub fn oakcommon_videoparams_get_time_base(\n\t\tparams: OakVideoParams,\n\t\tout_num: *mut i64,\n\t\tout_den: *mut i64,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_width`.\n\tpub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int);\n\t/// `oakcommon_videoparams_set_height`.\n\tpub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int);\n\t/// `oakcommon_videoparams_set_format`.\n\tpub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int);\n\t/// `oakcommon_videoparams_get_is_valid`.\n\tpub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_equals`.\n\tpub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_time_base`.\n\tpub fn oakcommon_videoparams_set_time_base(\n\t\tparams: OakVideoParams,\n\t\tnum: i64,\n\t\tden: i64,\n\t);\n\t/// `oakcommon_videoparams_set_frame_rate`.\n\tpub fn oakcommon_videoparams_set_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tnum: i64,\n\t\tden: i64,\n\t);\n\t/// `oakcommon_videoparams_set_pixel_aspect_ratio`.\n\tpub fn oakcommon_videoparams_set_pixel_aspect_ratio(\n\t\tparams: OakVideoParams,\n\t\tnum: i64,\n\t\tden: i64,\n\t);\n\t/// `oakcommon_videoparams_set_interlacing`.\n\tpub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int);\n\t/// `oakcommon_videoparams_set_duration`.\n\tpub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64);\n\t/// `oakcommon_videoparams_set_start_time`.\n\tpub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64);\n\t/// `oakcommon_videoparams_set_color_range`.\n\tpub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int);\n\t/// `oakcommon_videoparams_set_video_type`.\n\tpub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int);\n\t/// `oakcommon_videoparams_set_channel_count`.\n\tpub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int);\n\t/// `oakcommon_videoparams_set_color_primaries`.\n\tpub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int);\n\t/// `oakcommon_videoparams_set_color_transfer`.\n\tpub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int);\n\t/// `oakcommon_videoparams_set_premultiplied_alpha`.\n\tpub fn oakcommon_videoparams_set_premultiplied_alpha(params: OakVideoParams, premultiplied: c_int);\n\t/// `oakcommon_videoparams_set_enabled`.\n\tpub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int);\n\t/// `oakcommon_videoparams_static_get_bytes_per_pixel`.\n\tpub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_frame_rate_as_time_base`.\n\tpub fn oakcommon_videoparams_frame_rate_as_time_base(\n\t\tframe_rate_num: i64,\n\t\tframe_rate_den: i64,\n\t\tout_num: *mut i64,\n\t\tout_den: *mut i64,\n\t);\n\t/// `oakcommon_videoparams_get_stream_index`.\n\tpub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_stream_index`.\n\tpub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int);\n\t/// `oakcommon_videoparams_get_divider`.\n\tpub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_divider`.\n\tpub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int);\n\t// NOTE: the remaining video getters below take the value-style form the\n\t// crate's existing bridge uses (the real oakcommon headers use out-pointer\n\t// args); `get_frame_rate` needs both values so it keeps the out pair.\n\t/// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out).\n\tpub fn oakcommon_videoparams_get_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tout_num: *mut c_int,\n\t\tout_den: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_duration` (time-base units).\n\tpub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -\u003e i64;\n\t/// `oakcommon_videoparams_get_channel_count`.\n\tpub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_color_primaries`.\n\tpub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_color_transfer`.\n\tpub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_interlacing` (`Interlacing` value).\n\tpub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -\u003e c_int;\n\t/// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate).\n\tpub fn oakcore_audioparams_create(\n\t\tsample_rate: c_int,\n\t\tchannel_layout: u64,\n\t\tformat: c_int,\n\t) -\u003e *mut OakAudioParams;\n\t/// `oakcore_audioparams_free` (NULL no-op).\n\tpub fn oakcore_audioparams_free(params: *mut OakAudioParams);\n\t/// `oakcore_audioparams_sample_rate`.\n\tpub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_set_sample_rate`.\n\tpub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int);\n\t/// `oakcore_audioparams_channel_layout`.\n\tpub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -\u003e u64;\n\t/// `oakcore_audioparams_set_channel_layout`.\n\tpub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64);\n\t/// `oakcore_audioparams_set_time_base`.\n\tpub fn oakcore_audioparams_set_time_base(\n\t\tparams: *mut OakAudioParams,\n\t\tnum: c_int,\n\t\tden: c_int,\n\t);\n\t/// `oakcore_audioparams_set_format`.\n\tpub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int);\n\t/// `oakcore_audioparams_set_stream_index`.\n\tpub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int);\n\t/// `oakcore_audioparams_set_duration`.\n\tpub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64);\n\t/// `oakcore_audioparams_channel_count`.\n\tpub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_format`.\n\tpub fn oakcore_audioparams_format(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_stream_index`.\n\tpub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_duration`.\n\tpub fn oakcore_audioparams_duration(params: *const OakAudioParams) -\u003e i64;\n\t/// `oakcore_audioparams_is_valid`.\n\tpub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -\u003e c_int;\n\t/// `oakcore_audioparams_time_base` (newly allocated rational; caller\n\t/// releases with `oakcore_rational_free`).\n\tpub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -\u003e *mut c_void;\n\t/// `oakcore_rational_numerator`.\n\tpub fn oakcore_rational_numerator(rational: *const c_void) -\u003e c_int;\n\t/// `oakcore_rational_denominator`.\n\tpub fn oakcore_rational_denominator(rational: *const c_void) -\u003e c_int;\n\t/// `oakcore_rational_free` (NULL no-op).\n\tpub fn oakcore_rational_free(rational: *mut c_void);\n\t/// `oakcommon_subtitleparams_get_stream_index`.\n\tpub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -\u003e c_int;\n\t/// `oakcommon_subtitleparams_generate_ass_header`.\n\tpub fn oakcommon_subtitleparams_generate_ass_header(\n\t\tparams: OakSubtitleParams,\n\t\twidth: c_int,\n\t\theight: c_int,\n\t);\n\t/// `oakcommon_subtitleparams_add_subtitle`.\n\tpub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char);\n\t/// `oakcommon_config_get_int`.\n\tpub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -\u003e c_int;\n\t/// `oakcommon_config_get_bool`.\n\tpub fn oakcommon_config_get_bool(group: *const c_char, key: *const c_char, default: c_int) -\u003e c_int;\n\t/// `oakcommon_config_get` (two-stage string access).\n\tpub fn oakcommon_config_get(\n\t\tgroup: *const c_char,\n\t\tkey: *const c_char,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_filefunctions_init`.\n\tpub fn oakcommon_filefunctions_init();\n\t/// `oakcommon_filefunctions_get_configuration_location` (two-stage).\n\tpub fn oakcommon_filefunctions_get_configuration_location(\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_filefunctions_get_unique_file_identifier`.\n\tpub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -\u003e i64;\n\t/// `oakcommon_filefunctions_get_application_path` (two-stage).\n\tpub fn oakcommon_filefunctions_get_application_path(\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_filefunctions_free` (frees an internally cached string).\n\tpub fn oakcommon_filefunctions_free(ptr: *mut c_void);\n\t/// `oakcommon_colortransform_init_output`.\n\tpub fn oakcommon_colortransform_init_output(\n\t\tsrc_colorspace: c_int,\n\t\tsrc_trc: c_int,\n\t\tdst_colorspace: c_int,\n\t\tdst_trc: c_int,\n\t\tpremultiplied: c_int,\n\t\tchroma_coeffs: *const c_void,\n\t) -\u003e OakVideoParams;\n\t/// `oakcommon_colortransform_get_output`.\n\tpub fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams);\n\t/// `oakcommon_colortransform_free`.\n\tpub fn oakcommon_colortransform_free(params: *mut OakVideoParams);\n\t/// `oakcommon_ffmpegutils_get_native_sample_format`.\n\tpub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_compatible_pixel_format`.\n\tpub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`.\n\tpub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.\n\tpub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`.\n\tpub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.\n\tpub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -\u003e c_int;\n\t/// `oakcommon_oiioutils_init`.\n\tpub fn oakcommon_oiioutils_init();\n\t/// `oakcommon_oiioutils_get_oiio_base_type_from_format`.\n\tpub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -\u003e c_int;\n\t/// `oakcommon_oiioutils_get_format_from_oiio_basetype`.\n\tpub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -\u003e c_int;\n\t/// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out).\n\tpub fn oakcommon_oiioutils_get_pixel_aspect_ratio(\n\t\twidth: c_int,\n\t\theight: c_int,\n\t\tout_num: *mut c_int,\n\t\tout_den: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_oiioutils_free`.\n\tpub fn oakcommon_oiioutils_free();\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","mod.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! C ABI imports from the other oak modules.\n//!\n//! The codec module links against oakcommon and oakrender at the C ABI.\n//! Every signature below mirrors the corresponding public header\n//! verbatim and is resolved at link time. The by-value handle structs\n//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of\n//! the `{ctx, addref, release, abi_version}` layout so the codec crate\n//! can hold and hand them across the FFI boundary without translation.\n\npub mod common;\npub mod render;\n\n// In-memory mocks for the oakcommon/oakrender C ABI so the crate links\n// and is testable under `cargo test` (where those dylibs are absent).\n#[cfg(test)]\npub mod test_stubs;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","render.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! oakrender C ABI imports (display textures, renderers, cancel atoms).\n//!\n//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a\n//! `CancelAtom`; both are oakrender refcounted handles with the standard\n//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params`\n//! is a flattened POD the decoders construct to describe the frame.\n\nuse std::ffi::{c_char, c_int, c_void};\n\n/// `OakRenderTexture` — refcounted GPU texture handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakRenderTexture {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakCancelAtom` — refcounted cancellation atom handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakCancelAtom {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakRenderRenderer` — refcounted display-renderer handle.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakRenderRenderer {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender.\n#[derive(Clone, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct OakCodecFrame {\n\t/// Opaque object pointer.\n\tpub ctx: *mut c_void,\n\t/// Atomically increments the reference count.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Decrements the count, destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\n// Refcounted opaque handles; thread-safe in the C library.\nunsafe impl Send for OakRenderTexture {}\nunsafe impl Sync for OakRenderTexture {}\nunsafe impl Send for OakCancelAtom {}\nunsafe impl Sync for OakCancelAtom {}\nunsafe impl Send for OakRenderRenderer {}\nunsafe impl Sync for OakRenderRenderer {}\nunsafe impl Send for OakCodecFrame {}\nunsafe impl Sync for OakCodecFrame {}\n\n/// `oakrender_video_params` — flattened POD of `olive::VideoParams`\n/// passed into oakrender; see `include/render/renderer.h`.\n#[repr(C)]\npub struct oakrender_video_params {\n\t/// Width in pixels.\n\tpub width: c_int,\n\t/// Height in pixels.\n\tpub height: c_int,\n\t/// Frame-duration numerator (e.g. 1001/30000 s).\n\tpub time_base_num: c_int,\n\t/// Frame-duration denominator.\n\tpub time_base_den: c_int,\n\t/// `olive::PixelFormat::Format`.\n\tpub format: c_int,\n\t/// Pixel-aspect numerator.\n\tpub pixel_aspect_num: c_int,\n\t/// Pixel-aspect denominator.\n\tpub pixel_aspect_den: c_int,\n\t/// `olive::VideoParams::Interlacing`.\n\tpub interlacing: c_int,\n\t/// `olive::VideoParams::ColorRange`.\n\tpub color_range: c_int,\n\t/// Preview-resolution divider (1 = full).\n\tpub divider: c_int,\n\t/// `olive::VideoParams::Type` (0 = video).\n\tpub video_type: c_int,\n\t/// 0/1 premultiplied alpha.\n\tpub premultiplied_alpha: c_int,\n}\n\nextern \"C\" {\n\t/// `oakrender_cancelatom_init`.\n\tpub fn oakrender_cancelatom_init() -\u003e OakCancelAtom;\n\t/// `oakrender_cancelatom_free` (NULL/empty no-op).\n\tpub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);\n\t/// `oakrender_cancelatom_is_cancelled`.\n\tpub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -\u003e c_int;\n\t/// `oakrender_cancelatom_heard_cancel`.\n\tpub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -\u003e c_int;\n\t/// `oakrender_cancelatom_cancel`.\n\tpub fn oakrender_cancelatom_cancel(atom: OakCancelAtom);\n\t/// `oakrender_cancelatom_get_native`.\n\tpub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -\u003e *mut c_void;\n\t/// `oakrender_display_texture_create`.\n\tpub fn oakrender_display_texture_create(\n\t\trenderer: OakRenderRenderer,\n\t\tparams: *const oakrender_video_params,\n\t\tdata: *const c_void,\n\t\tlinesize: c_int,\n\t) -\u003e OakRenderTexture;\n\t/// `oakrender_display_texture_retain`.\n\tpub fn oakrender_display_texture_retain(texture: OakRenderTexture) -\u003e OakRenderTexture;\n\t/// `oakrender_display_texture_free` (NULL/empty no-op).\n\tpub fn oakrender_display_texture_free(texture: *mut OakRenderTexture);\n\t/// `oakrender_display_texture_upload`.\n\tpub fn oakrender_display_texture_upload(texture: OakRenderTexture) -\u003e c_int;\n\t/// `oakrender_display_texture_download`.\n\tpub fn oakrender_display_texture_download(\n\t\ttexture: OakRenderTexture,\n\t\tpixels: *mut c_void,\n\t\tlinesize: c_int,\n\t) -\u003e c_int;\n\t/// `oakrender_display_texture_get_params`.\n\tpub fn oakrender_display_texture_get_params(\n\t\ttexture: OakRenderTexture,\n\t\tout: *mut oakrender_video_params,\n\t) -\u003e c_int;\n\t/// `oakrender_display_texture_id`.\n\tpub fn oakrender_display_texture_id(texture: OakRenderTexture) -\u003e c_int;\n\t/// `oakrender_display_texture_is_dummy`.\n\tpub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -\u003e c_int;\n\t/// `oakrender_display_texture_get_frame` (two-stage frame access).\n\tpub fn oakrender_display_texture_get_frame(\n\t\ttexture: OakRenderTexture,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakrender_codec_frame_width`.\n\tpub fn oakrender_codec_frame_width(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_height`.\n\tpub fn oakrender_codec_frame_height(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_fb_format`.\n\tpub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_free` (NULL/empty no-op).\n\tpub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame);\n\t/// `oakrender_codec_frame_allocate`.\n\tpub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_linesize_bytes`.\n\tpub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_codec_frame_is_allocated`.\n\tpub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -\u003e c_int;\n\t/// `oakrender_display_renderer_blit_color_managed`.\n\tpub fn oakrender_display_renderer_blit_color_managed(\n\t\trenderer: OakRenderRenderer,\n\t\tjob: *const c_void,\n\t\tdst_texture: OakRenderTexture,\n\t\tparams: *const oakrender_video_params,\n\t) -\u003e c_int;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","bridge","test_stubs.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `#[cfg(test)]` in-memory mocks for the oakcommon / oakrender C ABI.\n//!\n//! The real symbols live in `liboakcommon` / `liboakrender` and are only\n//! linked when the crate is built against those dylibs (ctest). Under\n//! `cargo test` those libraries are not linked, so `extern \"C\"` call\n//! sites in the crate would otherwise fail to resolve. These\n//! `#[no_mangle] extern \"C\"` definitions provide the symbols and back the\n//! two stateful pieces the crate actually reads (video params and cancel\n//! atoms) with real in-memory state, so `Frame`, `FootageDescription`\n//! and friends are meaningfully testable. Everything else returns a\n//! deterministic neutral value.\n//!\n//! Kept strictly under `#[cfg(test)]`; never compiled into a real build.\n\n#![allow(dead_code, unused_variables)]\n\nuse std::collections::HashMap;\nuse std::ffi::{c_char, c_int, c_void, CStr};\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::{Mutex, OnceLock};\n\nuse crate::bridge::common::{\n\tOakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams,\n};\nuse crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture};\nuse crate::handle::OAKCODEC_ABI_VERSION;\n\n/// Per-`OakVideoParams` backing state.\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\nstruct MockParams {\n\twidth: i32,\n\theight: i32,\n\tformat: i32,\n\ttime_base_num: i64,\n\ttime_base_den: i64,\n\tstream_index: i32,\n\tdivider: i32,\n\tframe_rate_num: i32,\n\tframe_rate_den: i32,\n\tduration: i64,\n\tchannel_count: i32,\n\tcolor_primaries: i32,\n\tcolor_trc: i32,\n\tinterlacing: i32,\n\tpixel_aspect_num: i32,\n\tpixel_aspect_den: i32,\n\tstart_time: i64,\n\tcolor_range: i32,\n\tvideo_type: i32,\n\tpremultiplied_alpha: i32,\n\tenabled: i32,\n}\n\nfn params_store() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, MockParams\u003e\u003e {\n\tstatic S: OnceLock\u003cMutex\u003cHashMap\u003cusize, MockParams\u003e\u003e\u003e = OnceLock::new();\n\tS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\n/// Build a handle whose `ctx` owns a leaked `MockParams`; the map entry\n/// keeps the storage alive until `free` is called.\nfn new_params_handle(p: MockParams) -\u003e OakVideoParams {\n\tlet raw = Box::into_raw(Box::new(p.clone()));\n\tparams_store().lock().unwrap().insert(raw as usize, p);\n\tOakVideoParams {\n\t\tctx: raw as *mut c_void,\n\t\taddref: None,\n\t\trelease: None,\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\nfn params_ref(ctx: *mut c_void) -\u003e Option\u003c\u0026'static mut MockParams\u003e {\n\tlet store = params_store().lock().unwrap();\n\tstore.get(\u0026(ctx as usize))?;\n\tdrop(store);\n\t// SAFETY: entries are only removed by oakcommon_videoparams_free while\n\t// the caller still holds the handle, so the box outlives this borrow.\n\tSome(unsafe { \u0026mut *(ctx as *mut MockParams) })\n}\n\nfn params_get(ctx: *mut c_void) -\u003e MockParams {\n\tlet store = params_store().lock().unwrap();\n\tstore\n\t\t.get(\u0026(ctx as usize))\n\t\t.cloned()\n\t\t.unwrap_or_default()\n}\n\nfn params_set(ctx: *mut c_void, f: impl FnOnce(\u0026mut MockParams)) {\n\tlet mut store = params_store().lock().unwrap();\n\tif let Some(p) = store.get_mut(\u0026(ctx as usize)) {\n\t\tf(p);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_videoparams_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_init() -\u003e OakVideoParams {\n\tnew_params_handle(MockParams {\n\t\tchannel_count: 4, // internal RGBA pipeline layout\n\t\t..Default::default()\n\t})\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_init_basic(width: c_int, height: c_int) -\u003e OakVideoParams {\n\tnew_params_handle(MockParams {\n\t\twidth,\n\t\theight,\n\t\tdivider: 1,\n\t\tchannel_count: 4, // internal RGBA pipeline layout\n\t\t..Default::default()\n\t})\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_init_with_time_base(\n\twidth: c_int,\n\theight: c_int,\n\ttime_base_num: i64,\n\ttime_base_den: i64,\n) -\u003e OakVideoParams {\n\tnew_params_handle(MockParams {\n\t\twidth,\n\t\theight,\n\t\ttime_base_num,\n\t\ttime_base_den,\n\t\tdivider: 1,\n\t\tchannel_count: 4, // internal RGBA pipeline layout\n\t\t..Default::default()\n\t})\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_free(params: *mut OakVideoParams) {\n\tif params.is_null() {\n\t\treturn;\n\t}\n\tunsafe {\n\t\tlet ctx = (*params).ctx;\n\t\tparams_store().lock().unwrap().remove(\u0026(ctx as usize));\n\t\tif !ctx.is_null() {\n\t\t\tdrop(Box::from_raw(ctx as *mut MockParams));\n\t\t}\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_width(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).width\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_height(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).height\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_format(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).format\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_time_base(\n\tparams: OakVideoParams,\n\tout_num: *mut i64,\n\tout_den: *mut i64,\n) -\u003e c_int {\n\tlet p = params_get(params.ctx);\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = p.time_base_num };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = p.time_base_den };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int) {\n\tparams_set(params.ctx, |p| p.width = width);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int) {\n\tparams_set(params.ctx, |p| p.height = height);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) {\n\tparams_set(params.ctx, |p| p.format = format);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -\u003e c_int {\n\tlet p = params_get(params.ctx);\n\t((p.width \u003e 0) \u0026\u0026 (p.height \u003e 0)) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -\u003e c_int {\n\t(params_get(a.ctx) == params_get(b.ctx)) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -\u003e c_int {\n\t// U10 packs to 4 bytes; U8 to 1; U16/F16 to 2; F32 to 4.\n\tmatch format {\n\t\t0 =\u003e 1, // U8\n\t\t1 =\u003e 4, // U10\n\t\t2 =\u003e 2, // U16\n\t\t3 =\u003e 2, // F16\n\t\t4 =\u003e 4, // F32\n\t\t_ =\u003e 0,\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_frame_rate_as_time_base(\n\tframe_rate_num: i64,\n\tframe_rate_den: i64,\n\tout_num: *mut i64,\n\tout_den: *mut i64,\n) {\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = frame_rate_den };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = frame_rate_num };\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).stream_index\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int) {\n\tparams_set(params.ctx, |p| p.stream_index = index);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_divider(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).divider\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int) {\n\tparams_set(params.ctx, |p| p.divider = divider);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_frame_rate(\n\tparams: OakVideoParams,\n\tout_num: *mut c_int,\n\tout_den: *mut c_int,\n) -\u003e c_int {\n\tlet p = params_get(params.ctx);\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = p.frame_rate_num };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = p.frame_rate_den };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_duration(params: OakVideoParams) -\u003e i64 {\n\tparams_get(params.ctx).duration\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).channel_count\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).color_primaries\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).color_trc\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -\u003e c_int {\n\tparams_get(params.ctx).interlacing\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_time_base(\n\tparams: OakVideoParams,\n\tnum: i64,\n\tden: i64,\n) {\n\tparams_set(params.ctx, |p| {\n\t\tp.time_base_num = num;\n\t\tp.time_base_den = den;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_frame_rate(\n\tparams: OakVideoParams,\n\tnum: i64,\n\tden: i64,\n) {\n\tparams_set(params.ctx, |p| {\n\t\tp.frame_rate_num = num as i32;\n\t\tp.frame_rate_den = den as i32;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_pixel_aspect_ratio(\n\tparams: OakVideoParams,\n\tnum: i64,\n\tden: i64,\n) {\n\tparams_set(params.ctx, |p| {\n\t\tp.pixel_aspect_num = num as i32;\n\t\tp.pixel_aspect_den = den as i32;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int) {\n\tparams_set(params.ctx, |p| p.interlacing = interlacing);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) {\n\tparams_set(params.ctx, |p| p.duration = duration);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64) {\n\tparams_set(params.ctx, |p| p.start_time = start_time);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int) {\n\tparams_set(params.ctx, |p| p.color_range = color_range);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) {\n\tparams_set(params.ctx, |p| p.video_type = video_type);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int) {\n\tparams_set(params.ctx, |p| p.channel_count = channels);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int) {\n\tparams_set(params.ctx, |p| p.color_primaries = primaries);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int) {\n\tparams_set(params.ctx, |p| p.color_trc = transfer);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_premultiplied_alpha(\n\tparams: OakVideoParams,\n\tpremultiplied: c_int,\n) {\n\tparams_set(params.ctx, |p| p.premultiplied_alpha = premultiplied);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int) {\n\tparams_set(params.ctx, |p| p.enabled = enabled);\n}\n\n// ---------------------------------------------------------------------------\n// oakcore_audioparams_* / oakcore_rational_* (pointer-based in-memory state)\n// ---------------------------------------------------------------------------\n\n/// Per-`OakAudioParams` backing state. The real oakcore C ABI is\n/// pointer-based (`core/include/olive/core/oakcore/audioparams.h`), so\n/// these stubs own a boxed struct and return its raw pointer.\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\nstruct MockAudioParams {\n\tsample_rate: i32,\n\tchannel_layout: u64,\n\tformat: i32,\n\tstream_index: i32,\n\tduration: i64,\n\ttime_base_num: i32,\n\ttime_base_den: i32,\n}\n\nfn audio_params_store() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, MockAudioParams\u003e\u003e {\n\tstatic S: OnceLock\u003cMutex\u003cHashMap\u003cusize, MockAudioParams\u003e\u003e\u003e = OnceLock::new();\n\tS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\nfn audio_params_get(ctx: *const c_void) -\u003e MockAudioParams {\n\tlet store = audio_params_store().lock().unwrap();\n\tstore\n\t\t.get(\u0026(ctx as usize))\n\t\t.cloned()\n\t\t.unwrap_or_default()\n}\n\n/// Per-`OakRational` backing state (an owned `(num, den)` pair).\nfn rational_store() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, (i32, i32)\u003e\u003e {\n\tstatic S: OnceLock\u003cMutex\u003cHashMap\u003cusize, (i32, i32)\u003e\u003e\u003e = OnceLock::new();\n\tS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_create(\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tformat: c_int,\n) -\u003e *mut OakAudioParams {\n\t// The timebase starts at 1/sample_rate, mirroring the real header.\n\tlet p = MockAudioParams {\n\t\tsample_rate,\n\t\tchannel_layout,\n\t\tformat,\n\t\tstream_index: 0,\n\t\tduration: 0,\n\t\ttime_base_num: 1,\n\t\ttime_base_den: sample_rate,\n\t};\n\tlet raw = Box::into_raw(Box::new(p.clone()));\n\taudio_params_store().lock().unwrap().insert(raw as usize, p);\n\traw as *mut OakAudioParams\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_free(params: *mut OakAudioParams) {\n\tif params.is_null() {\n\t\treturn;\n\t}\n\taudio_params_store()\n\t\t.lock()\n\t\t.unwrap()\n\t\t.remove(\u0026(params as usize));\n\t// SAFETY: `params` was produced by `oakcore_audioparams_create` as a\n\t// boxed `MockAudioParams`; we hold the only reference after removal.\n\tunsafe { drop(Box::from_raw(params as *mut MockAudioParams)) };\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void).sample_rate\n}\n\nfn audio_params_set(ctx: *mut c_void, f: impl FnOnce(\u0026mut MockAudioParams)) {\n\tlet mut store = audio_params_store().lock().unwrap();\n\tif let Some(p) = store.get_mut(\u0026(ctx as usize)) {\n\t\tf(p);\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_sample_rate(\n\tparams: *mut OakAudioParams,\n\tsample_rate: c_int,\n) {\n\taudio_params_set(params as *mut c_void, |p| p.sample_rate = sample_rate);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -\u003e u64 {\n\taudio_params_get(params as *const c_void).channel_layout\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_channel_layout(\n\tparams: *mut OakAudioParams,\n\tlayout: u64,\n) {\n\taudio_params_set(params as *mut c_void, |p| p.channel_layout = layout);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_time_base(\n\tparams: *mut OakAudioParams,\n\tnum: c_int,\n\tden: c_int,\n) {\n\taudio_params_set(params as *mut c_void, |p| {\n\t\tp.time_base_num = num;\n\t\tp.time_base_den = den;\n\t});\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int) {\n\taudio_params_set(params as *mut c_void, |p| p.format = format);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_stream_index(\n\tparams: *mut OakAudioParams,\n\tindex: c_int,\n) {\n\taudio_params_set(params as *mut c_void, |p| p.stream_index = index);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64) {\n\taudio_params_set(params as *mut c_void, |p| p.duration = duration);\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void)\n\t\t.channel_layout\n\t\t.count_ones() as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_format(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void).format\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -\u003e c_int {\n\taudio_params_get(params as *const c_void).stream_index\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_duration(params: *const OakAudioParams) -\u003e i64 {\n\taudio_params_get(params as *const c_void).duration\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -\u003e c_int {\n\tlet p = audio_params_get(params as *const c_void);\n\t(p.sample_rate \u003e 0 \u0026\u0026 p.channel_layout != 0 \u0026\u0026 p.format \u003e= 0) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_audioparams_time_base(params: *const OakAudioParams) -\u003e *mut c_void {\n\tlet p = audio_params_get(params as *const c_void);\n\tlet r = (p.time_base_num, p.time_base_den);\n\tlet raw = Box::into_raw(Box::new(r));\n\trational_store().lock().unwrap().insert(raw as usize, r);\n\traw as *mut c_void\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_rational_numerator(rational: *const c_void) -\u003e c_int {\n\trational_store()\n\t\t.lock()\n\t\t.unwrap()\n\t\t.get(\u0026(rational as usize))\n\t\t.map(|r| r.0)\n\t\t.unwrap_or(0)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_rational_denominator(rational: *const c_void) -\u003e c_int {\n\trational_store()\n\t\t.lock()\n\t\t.unwrap()\n\t\t.get(\u0026(rational as usize))\n\t\t.map(|r| r.1)\n\t\t.unwrap_or(0)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcore_rational_free(rational: *mut c_void) {\n\tif rational.is_null() {\n\t\treturn;\n\t}\n\trational_store().lock().unwrap().remove(\u0026(rational as usize));\n\t// SAFETY: `rational` was produced by `oakcore_audioparams_time_base` as a\n\t// boxed `(i32, i32)` pair; we hold the only reference after removal.\n\tunsafe { drop(Box::from_raw(rational as *mut (i32, i32))) };\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_subtitleparams_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_subtitleparams_get_stream_index(_params: OakSubtitleParams) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_subtitleparams_generate_ass_header(\n\t_params: OakSubtitleParams,\n\t_width: c_int,\n\t_height: c_int,\n) {\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_subtitleparams_add_subtitle(\n\t_params: OakSubtitleParams,\n\t_text: *const c_char,\n) {\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_config_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_config_get_int(\n\t_group: *const c_char,\n\t_key: *const c_char,\n\tdefault: c_int,\n) -\u003e c_int {\n\tdefault\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_config_get_bool(\n\t_group: *const c_char,\n\t_key: *const c_char,\n\tdefault: c_int,\n) -\u003e c_int {\n\tdefault\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_config_get(\n\t_group: *const c_char,\n\t_key: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\t// Empty value: report the size needed (1 for NUL) and write NUL.\n\tif buf_size \u003c= 0 {\n\t\treturn 1;\n\t}\n\tif !buf.is_null() {\n\t\tunsafe { *buf = 0 };\n\t}\n\t1\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_filefunctions_*\n// ---------------------------------------------------------------------------\n\nstatic FAKE_PATH: \u0026[u8] = b\"/mock/config/oak\\0\";\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_init() {}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_get_configuration_location(\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tcopy_cstr(FAKE_PATH, buf, buf_size)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -\u003e i64 {\n\tif path.is_null() {\n\t\treturn 0;\n\t}\n\tunsafe { CStr::from_ptr(path) }\n\t\t.to_bytes()\n\t\t.iter()\n\t\t.fold(14695981039346656037u64, |acc, \u0026b| (acc ^ b as u64).wrapping_mul(1099511628211))\n\t\tas i64\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_get_application_path(\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tcopy_cstr(b\"/mock/app/oak\\0\", buf, buf_size)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_filefunctions_free(_ptr: *mut c_void) {}\n\n// ---------------------------------------------------------------------------\n// oakcommon_colortransform_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_colortransform_init_output(\n\t_src_colorspace: c_int,\n\t_src_trc: c_int,\n\t_dst_colorspace: c_int,\n\t_dst_trc: c_int,\n\t_premultiplied: c_int,\n\t_chroma_coeffs: *const c_void,\n) -\u003e OakVideoParams {\n\toakcommon_videoparams_init()\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams) {\n\tif !out.is_null() {\n\t\tunsafe { *out = params.clone() };\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_colortransform_free(params: *mut OakVideoParams) {\n\toakcommon_videoparams_free(params);\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_ffmpegutils_* (pure enum mapping; identity is a safe default)\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -\u003e c_int {\n\tsample_format\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -\u003e c_int {\n\tformat\n}\n\n// ---------------------------------------------------------------------------\n// oakcommon_oiioutils_*\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_init() {}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -\u003e c_int {\n\tformat\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -\u003e c_int {\n\tbasetype\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_get_pixel_aspect_ratio(\n\t_width: c_int,\n\t_height: c_int,\n\tout_num: *mut c_int,\n\tout_den: *mut c_int,\n) -\u003e c_int {\n\tif !out_num.is_null() {\n\t\tunsafe { *out_num = 1 };\n\t}\n\tif !out_den.is_null() {\n\t\tunsafe { *out_den = 1 };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakcommon_oiioutils_free() {}\n\n// ---------------------------------------------------------------------------\n// oakrender_cancelatom_* (real in-memory cancel state)\n// ---------------------------------------------------------------------------\n\nstatic CANCEL_FLAGS: OnceLock\u003cMutex\u003cHashMap\u003cusize, bool\u003e\u003e\u003e = OnceLock::new();\n\nfn cancel_flags() -\u003e \u0026'static Mutex\u003cHashMap\u003cusize, bool\u003e\u003e {\n\tCANCEL_FLAGS.get_or_init(|| Mutex::new(HashMap::new()))\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_init() -\u003e OakCancelAtom {\n\tstatic COUNTER: AtomicU64 = AtomicU64::new(1);\n\tlet id = COUNTER.fetch_add(1, Ordering::SeqCst) as usize;\n\tcancel_flags().lock().unwrap().insert(id, false);\n\tOakCancelAtom {\n\t\tctx: id as *mut c_void,\n\t\taddref: None,\n\t\trelease: None,\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) {\n\tif atom.is_null() {\n\t\treturn;\n\t}\n\tunsafe { cancel_flags().lock().unwrap().remove(\u0026((*atom).ctx as usize)) };\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -\u003e c_int {\n\t(*cancel_flags().lock().unwrap().get(\u0026(atom.ctx as usize)).unwrap_or(\u0026false)) as c_int\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -\u003e c_int {\n\toakrender_cancelatom_is_cancelled(atom)\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_cancel(atom: OakCancelAtom) {\n\tif let Some(f) = cancel_flags().lock().unwrap().get_mut(\u0026(atom.ctx as usize)) {\n\t\t*f = true;\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -\u003e *mut c_void {\n\tatom.ctx\n}\n\n// ---------------------------------------------------------------------------\n// oakrender_display_texture_* / codec_frame_* (neutral)\n// ---------------------------------------------------------------------------\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_create(\n\t_renderer: OakRenderRenderer,\n\t_params: *const crate::bridge::render::oakrender_video_params,\n\t_data: *const c_void,\n\t_linesize: c_int,\n) -\u003e OakRenderTexture {\n\tOakRenderTexture {\n\t\tctx: std::ptr::null_mut(),\n\t\taddref: None,\n\t\trelease: None,\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_retain(texture: OakRenderTexture) -\u003e OakRenderTexture {\n\ttexture\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_free(_texture: *mut OakRenderTexture) {}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_upload(_texture: OakRenderTexture) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_download(\n\t_texture: OakRenderTexture,\n\t_pixels: *mut c_void,\n\t_linesize: c_int,\n) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_get_params(\n\t_texture: OakRenderTexture,\n\t_out: *mut crate::bridge::render::oakrender_video_params,\n) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_id(_texture: OakRenderTexture) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_is_dummy(_texture: OakRenderTexture) -\u003e c_int {\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_texture_get_frame(\n\t_texture: OakRenderTexture,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tif buf_size \u003c= 0 {\n\t\treturn 1;\n\t}\n\tif !buf.is_null() {\n\t\tunsafe { *buf = 0 };\n\t}\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_width(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_height(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_fb_format(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_free(_frame: *mut OakCodecFrame) {}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_allocate(_frame: OakCodecFrame) -\u003e c_int {\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_linesize_bytes(_frame: OakCodecFrame) -\u003e c_int {\n\t0\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_codec_frame_is_allocated(_frame: OakCodecFrame) -\u003e c_int {\n\t1\n}\n\n#[no_mangle]\npub extern \"C\" fn oakrender_display_renderer_blit_color_managed(\n\t_renderer: OakRenderRenderer,\n\t_job: *const c_void,\n\t_dst_texture: OakRenderTexture,\n\t_params: *const crate::bridge::render::oakrender_video_params,\n) -\u003e c_int {\n\t0\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/// Two-stage string copy helper used by the mock getters. Returns the\n/// total size needed (including the trailing NUL), or truncates the buffer\n/// and writes a NUL terminator when the buffer is too small.\nfn copy_cstr(src: \u0026[u8], buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet needed = src.len() as c_int;\n\tif buf.is_null() || buf_size \u003c needed {\n\t\treturn needed;\n\t}\n\tunsafe {\n\t\tfor (i, \u0026b) in src.iter().enumerate() {\n\t\t\t*buf.add(i) = b as c_char;\n\t\t}\n\t}\n\tneeded\n}\n\n// Silence unused-import warnings when the by-value handle types are not\n// referenced by every build; they are part of the mock's public surface.\n#[allow(unused)]\nfn _keep(_: OakAudioParams, _: OakNodeBlock) {}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","conformmanager.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ConformManager` — pcm waveform cache files for fast scrubbing.\n//!\n//! Mirrors `src/codec/src/conformmanager.h`. Stateless (NOTES.md): actual\n//! conform work is delegated to the global task submit callback\n//! ([`crate::task`]); with no registrar the state queries report\n//! `Unavailable`. Deterministic per-channel filenames derive from the\n//! source + target audio params.\n\nuse std::ffi::CString;\nuse std::path::Path;\n\n/// Conform state of one audio stream.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum ConformState {\n\t/// Conform files exist.\n\tExists = 0,\n\t/// Conform is being generated.\n\tGenerating = 1,\n\t/// No task registrar; conform unavailable.\n\tUnavailable = 2,\n}\n\n/// `olive::ConformManager` — stateless conform query/produce manager.\npub struct ConformManager;\n\nimpl ConformManager {\n\t/// The process-wide ConformManager singleton.\n\tpub fn instance() -\u003e \u0026'static ConformManager {\n\t\tstatic INSTANCE: ConformManager = ConformManager;\n\t\t\u0026INSTANCE\n\t}\n\n\t/// Query (and when possible start) the conform of one audio stream.\n\t///\n\t/// `wait != 0` treats a post-submit miss as `Unavailable`; `wait == 0`\n\t/// reports it as `Generating`. Without a task registrar the result is\n\t/// always `Unavailable`.\n\tpub fn get_conform_state(\n\t\t\u0026self,\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\twait: bool,\n\t) -\u003e crate::error::Result\u003cConformState\u003e {\n\t\tlet filenames = conform_filenames(\n\t\t\tcache_path,\n\t\t\tsource_filename,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tsample_format,\n\t\t\tchannel_layout,\n\t\t);\n\n\t\t// Return existing conform if it exists.\n\t\tif all_conforms_exist(\u0026filenames) {\n\t\t\treturn Ok(ConformState::Exists);\n\t\t}\n\n\t\t// Interim state (pre-M8): no task system, conform cannot be generated.\n\t\tif !crate::task::task_submit_is_registered() {\n\t\t\treturn Ok(ConformState::Unavailable);\n\t\t}\n\n\t\t// The task owns the \".working\" temporary names and the rename to the\n\t\t// final per-channel filenames on success; output_filename carries the\n\t\t// first channel's final path and the task derives the siblings.\n\t\tlet req = crate::task::TaskRequest {\n\t\t\tkind: crate::task::TaskKind::Conform,\n\t\t\tinput_filename: source_filename,\n\t\t\toutput_filename: filenames.first().map(String::as_str).unwrap_or(\"\"),\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t\tproxy_width: 0,\n\t\t\tproxy_height: 0,\n\t\t};\n\n\t\t// Interim simplification: submission is synchronous — we always wait\n\t\t// for the submit to return, regardless of `wait`.\n\t\tif crate::task::submit_task(\u0026req).is_err() {\n\t\t\treturn Ok(ConformState::Unavailable);\n\t\t}\n\n\t\tif all_conforms_exist(\u0026filenames) {\n\t\t\treturn Ok(ConformState::Exists);\n\t\t}\n\n\t\tif wait {\n\t\t\t// Synchronous wait already happened and the conform still does not\n\t\t\t// exist: report the wait as failed.\n\t\t\treturn Ok(ConformState::Unavailable);\n\t\t}\n\n\t\tOk(ConformState::Generating)\n\t}\n\n\t/// Number of conform (pcm) files for the given stream/params — one per\n\t/// channel; 0 on invalid arguments.\n\tpub fn get_conform_filename_count(\n\t\t\u0026self,\n\t\t_cache_path: \u0026str,\n\t\t_source_filename: \u0026str,\n\t\t_stream_index: i32,\n\t\t_sample_rate: i32,\n\t\tchannel_layout: u64,\n\t\t_sample_format: i32,\n\t) -\u003e usize {\n\t\tchannel_layout.count_ones() as usize\n\t}\n\n\t/// The `index`-th conform filename.\n\tpub fn get_conform_filename(\n\t\t\u0026self,\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tindex: usize,\n\t) -\u003e crate::error::Result\u003cString\u003e {\n\t\tlet filenames = conform_filenames(\n\t\t\tcache_path,\n\t\t\tsource_filename,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tsample_format,\n\t\t\tchannel_layout,\n\t\t);\n\t\tfilenames\n\t\t\t.get(index)\n\t\t\t.cloned()\n\t\t\t.ok_or(crate::error::Error::NotFound)\n\t}\n}\n\n/// Deterministic conform base name plus per-channel pcm filenames, mirroring\n/// `ConformManager::get_conformed_filename`: one file per channel under\n/// `cache_path`, named `\u003cidentifier\u003e-\u003cstream\u003e.\u003crate\u003e.\u003cformat\u003e.\u003clayout\u003e.\u003ci\u003e.pcm`.\nfn conform_filenames(\n\tcache_path: \u0026str,\n\tsource_filename: \u0026str,\n\tstream_index: i32,\n\tsample_rate: i32,\n\tsample_format: i32,\n\tchannel_layout: u64,\n) -\u003e Vec\u003cString\u003e {\n\tlet count = channel_layout.count_ones() as usize;\n\tlet base = format!(\n\t\t\"{}-{}.{}.{}.{}\",\n\t\tunique_file_identifier(source_filename),\n\t\tstream_index,\n\t\tsample_rate,\n\t\tsample_format,\n\t\tchannel_layout,\n\t);\n\n\tlet mut out = Vec::with_capacity(count);\n\tfor i in 0..count {\n\t\tlet p = Path::new(cache_path).join(format!(\"{}.{}.pcm\", base, i));\n\t\tout.push(p.to_string_lossy().into_owned());\n\t}\n\tout\n}\n\n/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge\n/// returns a 64-bit id directly).\nfn unique_file_identifier(filename: \u0026str) -\u003e String {\n\tlet c = match CString::new(filename) {\n\t\tOk(c) =\u003e c,\n\t\tErr(_) =\u003e return String::new(),\n\t};\n\t// # Safety: `c` is a valid NUL-terminated C string alive for the call.\n\tlet id = unsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())\n\t};\n\tformat!(\"{}\", id)\n}\n\n/// True when every conform filename already exists on disk.\nfn all_conforms_exist(filenames: \u0026[String]) -\u003e bool {\n\tfilenames.iter().all(|f| Path::new(f).exists())\n}\n\n/// Shared test support: serializes access to the global task-submit registry\n/// (unit tests run in parallel and would otherwise clear each other's\n/// registration) and provides a callback that accepts any task.\n#[cfg(test)]\npub(crate) mod test_util {\n\tuse crate::error::OAKCODEC_OK;\n\tuse crate::task::OakCodecTaskRequest;\n\n\t/// Serializes every test that mutates the task-submit registry.\n\tpub static REG_LOCK: std::sync::Mutex\u003c()\u003e = std::sync::Mutex::new(());\n\n\t/// A task-submit callback that accepts every request (no-op).\n\tpub unsafe extern \"C\" fn accept_cb(\n\t\t_req: *const OakCodecTaskRequest,\n\t\t_ud: *mut std::ffi::c_void,\n\t) -\u003e i32 {\n\t\tOAKCODEC_OK\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn temp_subdir(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_conform_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\tfn fnv1a64(bytes: \u0026[u8]) -\u003e u64 {\n\t\tlet mut h: u64 = 14695981039346656037;\n\t\tfor \u0026b in bytes {\n\t\t\th ^= b as u64;\n\t\t\th = h.wrapping_mul(1099511628211);\n\t\t}\n\t\th\n\t}\n\n\t#[test]\n\tfn unique_identifier_matches_bridge_hash() {\n\t\t// The test stub computes an FNV-1a-64 of the path bytes.\n\t\tlet expected = format!(\"{}\", fnv1a64(b\"media.mp4\") as i64);\n\t\tassert_eq!(unique_file_identifier(\"media.mp4\"), expected);\n\t\t// Deterministic: same input, same id.\n\t\tassert_eq!(\n\t\t\tunique_file_identifier(\"media.mp4\"),\n\t\t\tunique_file_identifier(\"media.mp4\")\n\t\t);\n\t\t// Different input, different id.\n\t\tassert_ne!(\n\t\t\tunique_file_identifier(\"media.mp4\"),\n\t\t\tunique_file_identifier(\"other.mp4\")\n\t\t);\n\t}\n\n\t#[test]\n\tfn filename_count_from_channel_layout() {\n\t\tlet m = ConformManager::instance();\n\t\tassert_eq!(m.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0x3, 0), 2); // stereo\n\t\tassert_eq!(m.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0x4, 0), 1); // mono\n\t\tassert_eq!(m.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0, 0), 0); // invalid\n\t\tassert_eq!(\n\t\t\tm.get_conform_filename_count(\"c\", \"s\", 0, 48000, 0x60F, 0),\n\t\t\t6\n\t\t); // 5.1\n\t}\n\n\t#[test]\n\tfn conform_filename_derivation_and_range() {\n\t\tlet m = ConformManager::instance();\n\t\tlet cache = temp_subdir(\"names\");\n\t\tlet id = fnv1a64(b\"media.mp4\") as i64;\n\t\tlet base = format!(\"{}-0.48000.0.3\", id);\n\t\tlet f0 = m\n\t\t\t.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, 0)\n\t\t\t.unwrap();\n\t\tlet f1 = m\n\t\t\t.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, 1)\n\t\t\t.unwrap();\n\t\tassert_eq!(f0, format!(\"{}/{}.0.pcm\", cache, base));\n\t\tassert_eq!(f1, format!(\"{}/{}.1.pcm\", cache, base));\n\t\t// Out of range.\n\t\tassert!(matches!(\n\t\t\tm.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, 5),\n\t\t\tErr(crate::error::Error::NotFound)\n\t\t));\n\t}\n\n\t#[test]\n\tfn get_conform_state_unavailable_without_registrar() {\n\t\tlet _g = super::test_util::REG_LOCK.lock().unwrap();\n\t\t// Ensure no registrar is left over.\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tlet cache = temp_subdir(\"unavail\");\n\t\tlet s = ConformManager::instance()\n\t\t\t.get_conform_state(\u0026cache, \"missing.mp4\", 0, 48000, 0x3, 0, false)\n\t\t\t.unwrap();\n\t\tassert_eq!(s, ConformState::Unavailable);\n\t}\n\n\t#[test]\n\tfn get_conform_state_exists_when_files_present() {\n\t\tlet cache = temp_subdir(\"exists\");\n\t\tlet m = ConformManager::instance();\n\t\tfor i in 0..2 {\n\t\t\tlet f = m\n\t\t\t\t.get_conform_filename(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, i)\n\t\t\t\t.unwrap();\n\t\t\tstd::fs::write(\u0026f, b\"pcm\").unwrap();\n\t\t}\n\t\tlet s = m\n\t\t\t.get_conform_state(\u0026cache, \"media.mp4\", 0, 48000, 0x3, 0, false)\n\t\t\t.unwrap();\n\t\tassert_eq!(s, ConformState::Exists);\n\t}\n\n\t#[test]\n\tfn get_conform_state_generating_when_registered() {\n\t\tlet _g = super::test_util::REG_LOCK.lock().unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(\n\t\t\tSome(super::test_util::accept_cb),\n\t\t\tstd::ptr::null_mut(),\n\t\t);\n\t\tlet cache = temp_subdir(\"generating\");\n\t\tlet s = ConformManager::instance()\n\t\t\t.get_conform_state(\u0026cache, \"missing.mp4\", 0, 48000, 0x3, 0, false)\n\t\t\t.unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tassert_eq!(s, ConformState::Generating);\n\t}\n}\n","traces":[{"line":45,"address":[],"length":0,"stats":{"Line":12}},{"line":47,"address":[],"length":0,"stats":{"Line":12}},{"line":55,"address":[],"length":0,"stats":{"Line":5}},{"line":66,"address":[],"length":0,"stats":{"Line":5}},{"line":67,"address":[],"length":0,"stats":{"Line":5}},{"line":68,"address":[],"length":0,"stats":{"Line":5}},{"line":69,"address":[],"length":0,"stats":{"Line":5}},{"line":70,"address":[],"length":0,"stats":{"Line":5}},{"line":71,"address":[],"length":0,"stats":{"Line":5}},{"line":75,"address":[],"length":0,"stats":{"Line":10}},{"line":76,"address":[],"length":0,"stats":{"Line":2}},{"line":80,"address":[],"length":0,"stats":{"Line":3}},{"line":81,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":4}},{"line":101,"address":[],"length":0,"stats":{"Line":2}},{"line":102,"address":[],"length":0,"stats":{"Line":0}},{"line":105,"address":[],"length":0,"stats":{"Line":2}},{"line":106,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":5}},{"line":129,"address":[],"length":0,"stats":{"Line":5}},{"line":133,"address":[],"length":0,"stats":{"Line":9}},{"line":144,"address":[],"length":0,"stats":{"Line":9}},{"line":145,"address":[],"length":0,"stats":{"Line":9}},{"line":146,"address":[],"length":0,"stats":{"Line":9}},{"line":147,"address":[],"length":0,"stats":{"Line":9}},{"line":148,"address":[],"length":0,"stats":{"Line":9}},{"line":149,"address":[],"length":0,"stats":{"Line":9}},{"line":151,"address":[],"length":0,"stats":{"Line":18}},{"line":152,"address":[],"length":0,"stats":{"Line":9}},{"line":154,"address":[],"length":0,"stats":{"Line":18}},{"line":161,"address":[],"length":0,"stats":{"Line":14}},{"line":169,"address":[],"length":0,"stats":{"Line":28}},{"line":170,"address":[],"length":0,"stats":{"Line":28}},{"line":172,"address":[],"length":0,"stats":{"Line":28}},{"line":179,"address":[],"length":0,"stats":{"Line":42}},{"line":180,"address":[],"length":0,"stats":{"Line":70}},{"line":181,"address":[],"length":0,"stats":{"Line":140}},{"line":182,"address":[],"length":0,"stats":{"Line":84}},{"line":184,"address":[],"length":0,"stats":{"Line":14}},{"line":189,"address":[],"length":0,"stats":{"Line":19}},{"line":190,"address":[],"length":0,"stats":{"Line":38}},{"line":191,"address":[],"length":0,"stats":{"Line":38}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":196,"address":[],"length":0,"stats":{"Line":38}},{"line":198,"address":[],"length":0,"stats":{"Line":19}},{"line":202,"address":[],"length":0,"stats":{"Line":6}},{"line":203,"address":[],"length":0,"stats":{"Line":28}}],"covered":46,"coverable":50},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","decoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::Decoder` and its supporting types — the media-decoder trait.\n//!\n//! Mirrors `src/codec/src/decoder.h`. The C++ abstract base plus its\n//! FFmpeg/OIIO subclasses become the [`Decoder`] trait (decision 2 in\n//! README.md); probe/dispatch lives on the registry functions at the\n//! bottom of this module. Audio is handled in raw interleaved-float\n//! buffers matching the C ABI, not `oakcore_rs::SampleBuffer` (which the\n//! crate does not export).\n\nuse std::path::Path;\nuse std::sync::{Arc, Mutex, OnceLock};\n\nuse oakcore_rs::{Rational, TimeRange};\n\nuse crate::bridge::render::{OakCancelAtom, OakRenderTexture};\nuse crate::footagedescription::FootageDescription;\nuse crate::frame::Frame;\n\n/// `oakcodec_video_stream_info` — POD probe output describing one video\n/// stream; see `include/codec/decoder.h`.\n#[repr(C)]\npub struct OakCodecVideoStreamInfo {\n\t/// Stream index.\n\tpub stream_index: i32,\n\t/// Width in pixels.\n\tpub width: i32,\n\t/// Height in pixels.\n\tpub height: i32,\n\t/// Frame-rate numerator.\n\tpub frame_rate_num: i32,\n\t/// Frame-rate denominator.\n\tpub frame_rate_den: i32,\n\t/// Stream length in time-base units.\n\tpub duration_ts: i64,\n\t/// Time-base numerator (seconds per time-base unit).\n\tpub time_base_num: i32,\n\t/// Time-base denominator.\n\tpub time_base_den: i32,\n\t/// Native delivery `OakPixelFormat`.\n\tpub format: i32,\n\t/// Plane channel count.\n\tpub channel_count: i32,\n\t/// ISO/IEC 23001-8 color-primaries code point (0 = unknown).\n\tpub color_primaries: i32,\n\t/// ISO/IEC 23001-8 color-transfer code point (0 = unknown).\n\tpub color_trc: i32,\n\t/// 1 when the stream is interlaced.\n\tpub interlaced: i32,\n}\n\n/// `oakcodec_audio_stream_info` — POD probe output describing one audio\n/// stream; see `include/codec/decoder.h`.\n#[repr(C)]\npub struct OakCodecAudioStreamInfo {\n\t/// Stream index.\n\tpub stream_index: i32,\n\t/// Sample rate (Hz).\n\tpub sample_rate: i32,\n\t/// ffmpeg-style channel mask (e.g. 0x3 = stereo).\n\tpub channel_layout: u64,\n\t/// Channel count.\n\tpub channel_count: i32,\n\t/// Stream length in time-base units.\n\tpub duration_ts: i64,\n\t/// Time-base numerator.\n\tpub time_base_num: i32,\n\t/// Time-base denominator.\n\tpub time_base_den: i32,\n}\n\n/// Local replacement for `render/rendermodes.h` (oakrender C API has no\n/// render-mode counterpart). Values mirror engine/render/rendermodes.h:\n/// k_offline = 0, k_online = 1.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum RenderMode {\n\t/// Offline / background render.\n\tOffline = 0,\n\t/// Online / real-time render.\n\tOnline = 1,\n}\n\n/// \"Don't force a color range\" sentinel for\n/// [`RetrieveVideoParams::force_range`] (the actual ranges are the\n/// `OAKCOMMON_COLOR_RANGE_*` values).\npub const K_COLOR_RANGE_DEFAULT: i32 = -1;\n\n/// `Decoder::RetrieveVideoParams` — what a video retrieve call needs.\npub struct RetrieveVideoParams {\n\t/// Stream to read from.\n\tpub stream: CodecStream,\n\t/// Timestamp, rational seconds.\n\tpub time: Rational,\n\t/// Length of footage before the start (for early-seek semantics).\n\tpub length: TimeRange,\n\t/// Color range override; [`K_COLOR_RANGE_DEFAULT`] means \"don't force\".\n\tpub force_range: i32,\n\t/// Image sequence: bake the frame number into the filename.\n\tpub is_image_sequence: bool,\n\t/// Image sequence digit count (derived from the filename).\n\tpub image_sequence_digits: i32,\n\t/// Image sequence number to substitute.\n\tpub image_sequence_number: i64,\n\t/// Render mode (drives texture-path choices in the implementations).\n\tpub mode: RenderMode,\n\t/// Frame alpha channel is premultiplied.\n\tpub alpha_is_premultiplied: bool,\n}\n\n/// `Decoder::RetrieveAudioStatus` — outcome of an audio retrieve.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum RetrieveAudioStatus {\n\t/// Data written to the destination buffer.\n\tSuccess,\n\t/// The requested range is outside the footage.\n\tInvalidRange,\n\t/// The stream does not support audio.\n\tUnsupported,\n\t/// Media requires a conform that could not be produced.\n\tConformNeeded,\n\t/// A decoder-level error occurred.\n\tError,\n}\n\n/// `Decoder::RetrieveState`.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum RetrieveState {\n\t/// Ready to decode.\n\tReady,\n\t/// Failed to open the stream.\n\tFailedToOpen,\n\t/// The stream index could not be located.\n\tIndexUnavailable,\n}\n\n/// `Decoder::CodecStream` — identifies one (filename, stream) pair plus an\n/// optional associated timeline block.\n///\n/// The block is an opaque `OakNodeBlock` handle that codec only stores and\n/// compares, never dereferences or retains (borrowed pointer).\n#[derive(Clone, Debug, PartialEq, Eq)]\npub struct CodecStream {\n\tfilename: String,\n\tstream: i32,\n\tblock: Option\u003ccrate::bridge::common::OakNodeBlock\u003e,\n}\n\nimpl CodecStream {\n\t/// Empty, invalid stream.\n\tpub fn new() -\u003e Self {\n\t\tCodecStream {\n\t\t\tfilename: String::new(),\n\t\t\tstream: -1,\n\t\t\tblock: None,\n\t\t}\n\t}\n\n\t/// New stream for `(filename, stream)` with an optional block.\n\tpub fn with_block(\n\t\tfilename: String,\n\t\tstream: i32,\n\t\tblock: Option\u003ccrate::bridge::common::OakNodeBlock\u003e,\n\t) -\u003e Self {\n\t\tCodecStream {\n\t\t\tfilename,\n\t\t\tstream,\n\t\t\tblock,\n\t\t}\n\t}\n\n\t/// Non-empty filename and non-negative stream index.\n\tpub fn is_valid(\u0026self) -\u003e bool {\n\t\t!self.filename.is_empty() \u0026\u0026 self.stream \u003e= 0\n\t}\n\n\t/// The file exists on disk.\n\tpub fn exists(\u0026self) -\u003e bool {\n\t\tPath::new(\u0026self.filename).exists()\n\t}\n\n\t/// Reset to the empty stream.\n\tpub fn reset(\u0026mut self) {\n\t\tself.filename.clear();\n\t\tself.stream = -1;\n\t\tself.block = None;\n\t}\n\n\t/// Source filename.\n\tpub fn filename(\u0026self) -\u003e \u0026str {\n\t\t\u0026self.filename\n\t}\n\n\t/// Stream index within the source.\n\tpub fn stream(\u0026self) -\u003e i32 {\n\t\tself.stream\n\t}\n\n\t/// Associated timeline block (borrowed; only compared, never used).\n\tpub fn block(\u0026self) -\u003e Option\u003ccrate::bridge::common::OakNodeBlock\u003e {\n\t\tself.block.clone()\n\t}\n}\n\n/// `olive::Decoder` — abstraction over external media decoding.\n///\n/// Implementations are [`crate::ffmpeg::FFmpegDecoder`] and\n/// [`crate::oiio::OIIODecoder`]. The trait surface mirrors the C++\n/// abstract base; the refcounted handle that backs the public API wraps an\n/// `Arc\u003cdyn Decoder\u003e`.\npub trait Decoder: Send + Sync {\n\t/// Unique decoder id (\"ffmpeg\"/\"oiio\").\n\tfn id(\u0026self) -\u003e String;\n\n\t/// Whether this decoder supports video streams.\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this decoder supports audio streams.\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this decoder can read the given file (static probe).\n\tfn probe(\n\t\t\u0026self,\n\t\tfilename: \u0026str,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e Option\u003cFootageDescription\u003e;\n\n\t/// Open `stream` for decoding. Thread-safe.\n\tfn open(\u0026self, stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Close the currently open stream (safe when closed).\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// The currently open stream (locked accessor).\n\tfn stream(\u0026self) -\u003e CodecStream;\n\n\t/// Retrieve a video frame into CPU memory.\n\tfn retrieve_video_frame(\n\t\t\u0026self,\n\t\tp: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e;\n\n\t/// Retrieve a video frame as a render texture (owned by caller).\n\tfn retrieve_video(\u0026self, p: \u0026RetrieveVideoParams) -\u003e crate::error::Result\u003cOakRenderTexture\u003e;\n\n\t/// Retrieve interleaved audio covering `range` into `dest` (floats).\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\tdest: \u0026mut [f32],\n\t\trange: \u0026TimeRange,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e;\n\n\t/// Conform the open stream's audio into per-channel pcm files.\n\t///\n\t/// `sample_rate` / `channel_layout` / `sample_format` describe the\n\t/// target audio format (`sample_format` is a\n\t/// `olive::core::SampleFormat::Format` value). The C++ side builds its\n\t/// `core::AudioParams` from these three — mirroring the C ABI\n\t/// `oakcodec_decoder_conform_audio` argument list.\n\tfn conform_audio(\n\t\t\u0026self,\n\t\toutput_filenames: \u0026[String],\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Offset of the audio start relative to the video (rational seconds).\n\tfn get_audio_start_offset(\u0026self) -\u003e Rational {\n\t\t// C++ default `virtual Rational get_audio_start_offset() const { return 0; }`\n\t\tRational::new(0, 1)\n\t}\n}\n\n/// Placeholder decoder used by the built-in probe registry.\n///\n/// Reports the correct id and capability flags so id-based dispatch\n/// (`create_from_id`) works, but every media operation is unimplemented\n/// and returns `None` / an error. Used for the OIIO entry, whose Rust\n/// implementation (`crate::oiio::OIIODecoder`) is still a dylib stub; the\n/// FFmpeg entry is the real [`crate::ffmpeg::FFmpegDecoder`].\nstruct UnimplementedDecoder {\n\tid: \u0026'static str,\n\tvideo: bool,\n\taudio: bool,\n}\n\nimpl UnimplementedDecoder {\n\tfn new(id: \u0026'static str, video: bool, audio: bool) -\u003e Self {\n\t\tUnimplementedDecoder { id, video, audio }\n\t}\n}\n\nimpl Decoder for UnimplementedDecoder {\n\tfn id(\u0026self) -\u003e String {\n\t\tself.id.to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tself.video\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tself.audio\n\t}\n\n\tfn probe(\n\t\t\u0026self,\n\t\t_filename: \u0026str,\n\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e Option\u003cFootageDescription\u003e {\n\t\tNone\n\t}\n\n\tfn open(\u0026self, _stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn stream(\u0026self) -\u003e CodecStream {\n\t\tCodecStream::new()\n\t}\n\n\tfn retrieve_video_frame(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn retrieve_video(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cOakRenderTexture\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\t_dest: \u0026mut [f32],\n\t\t_range: \u0026TimeRange,\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n\n\tfn conform_audio(\n\t\t\u0026self,\n\t\t_output_filenames: \u0026[String],\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t\t_sample_format: i32,\n\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\"decoder not yet implemented\".to_string()))\n\t}\n}\n\n/// `Decoder::create_from_id` — instantiate a decoder by id, or `None`.\npub fn create_from_id(id: \u0026str) -\u003e Option\u003cArc\u003cdyn Decoder\u003e\u003e {\n\tif id.is_empty() {\n\t\treturn None;\n\t}\n\n\treceive_list_of_all_decoders()\n\t\t.into_iter()\n\t\t.find(|d| d.id() == id)\n}\n\n/// Test-injected decoder registry (see [`set_test_decoders`]); empty when\n/// not injected, in which case the built-in list below is used.\nstatic TEST_DECODERS: OnceLock\u003cMutex\u003cVec\u003cArc\u003cdyn Decoder\u003e\u003e\u003e\u003e = OnceLock::new();\n\n/// Serializes every test that reads the built-in decoder registry. The ffi\n/// decoder tests inject through `crate::ffi::lock_tests()` (the shared\n/// `TEST_LOCK`), so the registry assertions below take that same lock to\n/// never race with an injected list.\n#[cfg(test)]\nfn registry_guard() -\u003e std::sync::MutexGuard\u003c'static, ()\u003e {\n\tcrate::ffi::lock_tests()\n}\n\n/// Replace the decoder registry with `list`; pass an empty list to restore\n/// the built-in decoders.\n///\n/// Test/extension support (the C ABI has no way to register a decoder, so\n/// the contract tests drive the probe/dispatch paths through a fake\n/// decoder). Hidden from docs; never called by production code.\n#[doc(hidden)]\npub fn set_test_decoders(list: Vec\u003cArc\u003cdyn Decoder\u003e\u003e) {\n\tlet store = TEST_DECODERS.get_or_init(|| Mutex::new(Vec::new()));\n\t*store.lock().unwrap() = list;\n}\n\n/// `Decoder::receive_list_of_all_decoders` — all registered decoders.\n///\n/// Order is probe priority, mirroring C++: OIIO (more specific) before\n/// FFmpeg (format-agnostic fallback). The OIIO entry is an\n/// [`UnimplementedDecoder`] stub (the OIIO engine is not ported); the\n/// FFmpeg entry is the real [`crate::ffmpeg::FFmpegDecoder`]. When tests\n/// injected a non-empty list via [`set_test_decoders`], that list takes\n/// precedence.\npub fn receive_list_of_all_decoders() -\u003e Vec\u003cArc\u003cdyn Decoder\u003e\u003e {\n\tif let Some(store) = TEST_DECODERS.get() {\n\t\tlet injected = store.lock().unwrap();\n\t\tif !injected.is_empty() {\n\t\t\treturn injected.clone();\n\t\t}\n\t}\n\tvec![\n\t\tArc::new(UnimplementedDecoder::new(\"oiio\", false, false)),\n\t\tArc::new(crate::ffmpeg::FFmpegDecoder::new()),\n\t]\n}\n\n/// Image-sequence filename heuristics (static).\n///\n/// Replace the trailing digit run of the filename stem with the\n/// zero-padded decimal representation of `number` (keeps the same digit\n/// count), mirroring `Decoder::transform_image_sequence_file_name`.\npub fn transform_image_sequence_file_name(filename: \u0026str, number: i64) -\u003e String {\n\tlet digit_count = get_image_sequence_digit_count(filename) as usize;\n\n\tlet path = Path::new(filename);\n\tlet file_name = path\n\t\t.file_name()\n\t\t.and_then(|n| n.to_str())\n\t\t.unwrap_or(filename);\n\n\t// QFileInfo::completeBaseName(): filename up to the first '.'.\n\tlet original_basename = match file_name.find('.') {\n\t\tSome(dot) =\u003e \u0026file_name[..dot],\n\t\tNone =\u003e file_name,\n\t};\n\n\t// New stem = original stem minus the trailing digit run, plus the\n\t// zero-padded number (`snprintf(\"%0*lld\", digit_count, number)`).\n\tlet cut = original_basename.len().saturating_sub(digit_count);\n\tlet new_basename = format!(\n\t\t\"{}{:0width$}\",\n\t\t\u0026original_basename[..cut],\n\t\tnumber,\n\t\twidth = digit_count\n\t);\n\n\t// Replace every occurrence of the original stem in the filename.\n\tlet mut new_filename = file_name.to_string();\n\tlet mut pos = 0;\n\twhile let Some(rel) = new_filename[pos..].find(original_basename) {\n\t\tlet start = pos + rel;\n\t\tlet end = start + original_basename.len();\n\t\tnew_filename.replace_range(start..end, \u0026new_basename);\n\t\tpos = start + new_basename.len();\n\t}\n\n\tmatch path.parent() {\n\t\tSome(parent) if !parent.as_os_str().is_empty() =\u003e {\n\t\t\tPath::new(parent).join(\u0026new_filename).to_string_lossy().into_owned()\n\t\t}\n\t\t_ =\u003e new_filename,\n\t}\n}\n\n/// Number of trailing digits in the filename stem (0 = not a sequence).\npub fn get_image_sequence_digit_count(filename: \u0026str) -\u003e i32 {\n\tlet file_name = Path::new(filename)\n\t\t.file_name()\n\t\t.and_then(|n| n.to_str())\n\t\t.unwrap_or(filename);\n\n\t// QFileInfo::completeBaseName(): filename up to the first '.'.\n\tlet stem = match file_name.find('.') {\n\t\tSome(dot) =\u003e \u0026file_name[..dot],\n\t\tNone =\u003e file_name,\n\t};\n\n\tlet mut count: i32 = 0;\n\tfor ch in stem.chars().rev() {\n\t\tif ch.is_ascii_digit() {\n\t\t\tcount += 1;\n\t\t} else {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tcount\n}\n\n/// Numeric value of the trailing digits (0 when there are none).\n///\n/// Mirrors C++ `Decoder::get_image_sequence_index`, which slices the\n/// trailing digit run (`basename.substr(basename.size() - digit_count)`) and\n/// passes it to `strtoll`. Because that slice is empty when there are no\n/// trailing digits (digit_count == 0) and all-digits otherwise, the value is\n/// the parsed number, or `0` for a non-sequence.\npub fn get_image_sequence_index(filename: \u0026str) -\u003e i64 {\n\tlet digit_count = get_image_sequence_digit_count(filename) as usize;\n\n\tlet file_name = Path::new(filename)\n\t\t.file_name()\n\t\t.and_then(|n| n.to_str())\n\t\t.unwrap_or(filename);\n\n\t// QFileInfo::completeBaseName(): filename up to the first '.'.\n\tlet stem = match file_name.find('.') {\n\t\tSome(dot) =\u003e \u0026file_name[..dot],\n\t\tNone =\u003e file_name,\n\t};\n\n\t// Trailing digit run (empty when the stem has no trailing digits).\n\tlet start = stem.len().saturating_sub(digit_count);\n\tlet number_only = \u0026stem[start..];\n\n\t// `strtoll(..., base 10)`: the slice is empty-or-digits, so a plain\n\t// decimal parse with 0 on failure reproduces the C++ result.\n\tnumber_only.parse::\u003ci64\u003e().unwrap_or(0)\n}\n\n/// The `k_any_timecode` rational constant.\n///\n/// C++ `const Rational Decoder::k_any_timecode = RATIONAL_MIN;`, which the\n/// i32 reduction cap normalizes to `-2147483647/1`.\npub fn k_any_timecode() -\u003e Rational {\n\tRational::new(-2147483647, 1)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn codec_stream_new_is_invalid() {\n\t\tlet s = CodecStream::new();\n\t\tassert!(!s.is_valid());\n\t\tassert!(s.filename().is_empty());\n\t\tassert_eq!(s.stream(), -1);\n\t\tassert_eq!(s.block(), None);\n\t}\n\n\t#[test]\n\tfn codec_stream_with_block_is_valid() {\n\t\tlet s = CodecStream::with_block(\"video.mov\".to_string(), 1, None);\n\t\tassert!(s.is_valid());\n\t\tassert_eq!(s.filename(), \"video.mov\");\n\t\tassert_eq!(s.stream(), 1);\n\n\t\t// Negative stream index is invalid regardless of filename.\n\t\tlet bad = CodecStream::with_block(\"video.mov\".to_string(), -1, None);\n\t\tassert!(!bad.is_valid());\n\t}\n\n\t#[test]\n\tfn codec_stream_reset_clears() {\n\t\tlet mut s = CodecStream::with_block(\"video.mov\".to_string(), 2, None);\n\t\ts.reset();\n\t\tassert!(!s.is_valid());\n\t\tassert!(s.filename().is_empty());\n\t\tassert_eq!(s.stream(), -1);\n\t}\n\n\t#[test]\n\tfn digit_count_counts_trailing_digits() {\n\t\tassert_eq!(get_image_sequence_digit_count(\"frame_0001.png\"), 4);\n\t\tassert_eq!(get_image_sequence_digit_count(\"frame.png\"), 0);\n\t\tassert_eq!(get_image_sequence_digit_count(\"img000.jpg\"), 3);\n\t\t// Digits before the final char are not trailing digits.\n\t\tassert_eq!(get_image_sequence_digit_count(\"a1b.png\"), 0);\n\t}\n\n\t#[test]\n\tfn image_sequence_index_parses_number() {\n\t\tassert_eq!(get_image_sequence_index(\"frame_0001.png\"), 1);\n\t\tassert_eq!(get_image_sequence_index(\"img012.jpg\"), 12);\n\t\tassert_eq!(get_image_sequence_index(\"0009.png\"), 9);\n\t\t// No trailing digits: the sliced run is empty, so the value is 0.\n\t\tassert_eq!(get_image_sequence_index(\"frame.png\"), 0);\n\t\tassert_eq!(get_image_sequence_index(\"12abc.png\"), 0);\n\t}\n\n\t#[test]\n\tfn transform_image_sequence_substitutes_number() {\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"frame_0001.png\", 5),\n\t\t\t\"frame_0005.png\"\n\t\t);\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"dir/img012.jpg\", 7),\n\t\t\t\"dir/img007.jpg\"\n\t\t);\n\t\t// No digit run: number appended with no padding (C++ behavior).\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"frame.png\", 3),\n\t\t\t\"frame3.png\"\n\t\t);\n\t\t// All-digit stem: whole run is replaced.\n\t\tassert_eq!(\n\t\t\ttransform_image_sequence_file_name(\"0001.png\", 7),\n\t\t\t\"0007.png\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn k_any_timecode_is_rational_min() {\n\t\tlet tc = k_any_timecode();\n\t\tassert_eq!(tc.numerator(), -2147483647);\n\t\tassert_eq!(tc.denominator(), 1);\n\t}\n\n\t#[test]\n\tfn registry_lists_oiio_then_ffmpeg() {\n\t\tlet _g = registry_guard();\n\t\tlet list = receive_list_of_all_decoders();\n\t\tlet ids: Vec\u003cString\u003e = list.iter().map(|d| d.id()).collect();\n\t\t// Probe priority: OIIO (specific) first, FFmpeg (fallback) last.\n\t\tassert_eq!(ids, vec![\"oiio\".to_string(), \"ffmpeg\".to_string()]);\n\t}\n\n\t#[test]\n\tfn create_from_id_matches_registry() {\n\t\tlet _g = registry_guard();\n\t\tassert!(create_from_id(\"ffmpeg\").is_some());\n\t\tassert!(create_from_id(\"oiio\").is_some());\n\t\tassert_eq!(create_from_id(\"ffmpeg\").unwrap().id(), \"ffmpeg\");\n\t\tassert_eq!(create_from_id(\"oiio\").unwrap().id(), \"oiio\");\n\t\t// Unknown and empty ids return None.\n\t\tassert!(create_from_id(\"nope\").is_none());\n\t\tassert!(create_from_id(\"\").is_none());\n\t}\n}\n\n#[cfg(test)]\nmod tests_unimplemented {\n\tuse super::*;\n\n\tfn builtin(id: \u0026str) -\u003e Arc\u003cdyn Decoder\u003e {\n\t\tcreate_from_id(id).unwrap()\n\t}\n\n\t#[test]\n\tfn ffmpeg_builtin_fails_on_missing_media_and_closes() {\n\t\tlet _g = registry_guard();\n\t\tlet d = builtin(\"ffmpeg\");\n\t\tassert!(d.supports_video());\n\t\tassert!(d.supports_audio());\n\t\t// A nonexistent file cannot be probed or opened.\n\t\tassert!(d.probe(\"x.mp4\", None).is_none());\n\n\t\tlet s = CodecStream::with_block(\"x.mp4\".to_string(), 0, None);\n\t\tassert!(d.open(\u0026s).is_err());\n\t\t// C++ parity: a failed open leaves the decoder closed.\n\t\tassert_eq!(d.stream().filename(), \"\");\n\t\tassert!(d.close().is_ok());\n\n\t\tlet p = RetrieveVideoParams {\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(0, 1),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t};\n\t\tassert!(d.retrieve_video_frame(\u0026p).is_err());\n\t\tassert!(d.retrieve_video(\u0026p).is_err());\n\t\tlet mut dest = [0f32; 4];\n\t\tassert!(d\n\t\t\t.retrieve_audio(\n\t\t\t\t\u0026mut dest,\n\t\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t\t48000,\n\t\t\t\t0x3\n\t\t\t)\n\t\t\t.is_err());\n\t\tassert!(d\n\t\t\t.conform_audio(\u0026[\"a.pcm\".to_string()], 48000, 0x3, 10, None)\n\t\t\t.is_err());\n\n\t\t// OIIO reports no media capabilities.\n\t\tlet o = builtin(\"oiio\");\n\t\tassert!(!o.supports_video());\n\t\tassert!(!o.supports_audio());\n\t}\n\n\t#[test]\n\tfn get_audio_start_offset_defaults_to_zero() {\n\t\tlet _g = registry_guard();\n\t\tlet d = builtin(\"ffmpeg\");\n\t\tlet off = d.get_audio_start_offset();\n\t\tassert_eq!(off.numerator(), 0);\n\t\tassert_eq!(off.denominator(), 1);\n\t}\n}\n","traces":[{"line":166,"address":[],"length":0,"stats":{"Line":11}},{"line":168,"address":[],"length":0,"stats":{"Line":11}},{"line":169,"address":[],"length":0,"stats":{"Line":11}},{"line":175,"address":[],"length":0,"stats":{"Line":16}},{"line":188,"address":[],"length":0,"stats":{"Line":11}},{"line":189,"address":[],"length":0,"stats":{"Line":20}},{"line":193,"address":[],"length":0,"stats":{"Line":7}},{"line":194,"address":[],"length":0,"stats":{"Line":14}},{"line":198,"address":[],"length":0,"stats":{"Line":1}},{"line":199,"address":[],"length":0,"stats":{"Line":2}},{"line":200,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":205,"address":[],"length":0,"stats":{"Line":12}},{"line":206,"address":[],"length":0,"stats":{"Line":12}},{"line":210,"address":[],"length":0,"stats":{"Line":8}},{"line":211,"address":[],"length":0,"stats":{"Line":8}},{"line":215,"address":[],"length":0,"stats":{"Line":1}},{"line":216,"address":[],"length":0,"stats":{"Line":2}},{"line":231,"address":[],"length":0,"stats":{"Line":0}},{"line":232,"address":[],"length":0,"stats":{"Line":0}},{"line":236,"address":[],"length":0,"stats":{"Line":0}},{"line":237,"address":[],"length":0,"stats":{"Line":0}},{"line":291,"address":[],"length":0,"stats":{"Line":0}},{"line":293,"address":[],"length":0,"stats":{"Line":0}},{"line":311,"address":[],"length":0,"stats":{"Line":9}},{"line":317,"address":[],"length":0,"stats":{"Line":10}},{"line":318,"address":[],"length":0,"stats":{"Line":20}},{"line":321,"address":[],"length":0,"stats":{"Line":1}},{"line":322,"address":[],"length":0,"stats":{"Line":1}},{"line":325,"address":[],"length":0,"stats":{"Line":1}},{"line":326,"address":[],"length":0,"stats":{"Line":1}},{"line":329,"address":[],"length":0,"stats":{"Line":0}},{"line":334,"address":[],"length":0,"stats":{"Line":0}},{"line":337,"address":[],"length":0,"stats":{"Line":0}},{"line":338,"address":[],"length":0,"stats":{"Line":0}},{"line":341,"address":[],"length":0,"stats":{"Line":0}},{"line":342,"address":[],"length":0,"stats":{"Line":0}},{"line":345,"address":[],"length":0,"stats":{"Line":0}},{"line":346,"address":[],"length":0,"stats":{"Line":0}},{"line":349,"address":[],"length":0,"stats":{"Line":0}},{"line":353,"address":[],"length":0,"stats":{"Line":0}},{"line":356,"address":[],"length":0,"stats":{"Line":0}},{"line":360,"address":[],"length":0,"stats":{"Line":0}},{"line":363,"address":[],"length":0,"stats":{"Line":0}},{"line":370,"address":[],"length":0,"stats":{"Line":0}},{"line":373,"address":[],"length":0,"stats":{"Line":0}},{"line":381,"address":[],"length":0,"stats":{"Line":0}},{"line":386,"address":[],"length":0,"stats":{"Line":14}},{"line":387,"address":[],"length":0,"stats":{"Line":28}},{"line":388,"address":[],"length":0,"stats":{"Line":1}},{"line":391,"address":[],"length":0,"stats":{"Line":13}},{"line":393,"address":[],"length":0,"stats":{"Line":49}},{"line":405,"address":[],"length":0,"stats":{"Line":4}},{"line":406,"address":[],"length":0,"stats":{"Line":4}},{"line":416,"address":[],"length":0,"stats":{"Line":18}},{"line":417,"address":[],"length":0,"stats":{"Line":56}},{"line":418,"address":[],"length":0,"stats":{"Line":54}},{"line":429,"address":[],"length":0,"stats":{"Line":22}},{"line":430,"address":[],"length":0,"stats":{"Line":35}},{"line":431,"address":[],"length":0,"stats":{"Line":52}},{"line":432,"address":[],"length":0,"stats":{"Line":13}},{"line":433,"address":[],"length":0,"stats":{"Line":13}},{"line":436,"address":[],"length":0,"stats":{"Line":9}},{"line":437,"address":[],"length":0,"stats":{"Line":9}},{"line":438,"address":[],"length":0,"stats":{"Line":9}},{"line":447,"address":[],"length":0,"stats":{"Line":5}},{"line":448,"address":[],"length":0,"stats":{"Line":10}},{"line":450,"address":[],"length":0,"stats":{"Line":15}},{"line":451,"address":[],"length":0,"stats":{"Line":10}},{"line":453,"address":[],"length":0,"stats":{"Line":15}},{"line":454,"address":[],"length":0,"stats":{"Line":10}},{"line":457,"address":[],"length":0,"stats":{"Line":10}},{"line":458,"address":[],"length":0,"stats":{"Line":10}},{"line":459,"address":[],"length":0,"stats":{"Line":0}},{"line":464,"address":[],"length":0,"stats":{"Line":25}},{"line":465,"address":[],"length":0,"stats":{"Line":10}},{"line":467,"address":[],"length":0,"stats":{"Line":5}},{"line":473,"address":[],"length":0,"stats":{"Line":15}},{"line":474,"address":[],"length":0,"stats":{"Line":10}},{"line":475,"address":[],"length":0,"stats":{"Line":30}},{"line":476,"address":[],"length":0,"stats":{"Line":15}},{"line":477,"address":[],"length":0,"stats":{"Line":20}},{"line":478,"address":[],"length":0,"stats":{"Line":25}},{"line":479,"address":[],"length":0,"stats":{"Line":10}},{"line":482,"address":[],"length":0,"stats":{"Line":10}},{"line":483,"address":[],"length":0,"stats":{"Line":12}},{"line":484,"address":[],"length":0,"stats":{"Line":3}},{"line":486,"address":[],"length":0,"stats":{"Line":4}},{"line":491,"address":[],"length":0,"stats":{"Line":16}},{"line":492,"address":[],"length":0,"stats":{"Line":32}},{"line":494,"address":[],"length":0,"stats":{"Line":48}},{"line":495,"address":[],"length":0,"stats":{"Line":32}},{"line":498,"address":[],"length":0,"stats":{"Line":32}},{"line":499,"address":[],"length":0,"stats":{"Line":32}},{"line":500,"address":[],"length":0,"stats":{"Line":0}},{"line":503,"address":[],"length":0,"stats":{"Line":48}},{"line":504,"address":[],"length":0,"stats":{"Line":103}},{"line":505,"address":[],"length":0,"stats":{"Line":151}},{"line":506,"address":[],"length":0,"stats":{"Line":41}},{"line":508,"address":[],"length":0,"stats":{"Line":14}},{"line":512,"address":[],"length":0,"stats":{"Line":16}},{"line":522,"address":[],"length":0,"stats":{"Line":6}},{"line":523,"address":[],"length":0,"stats":{"Line":12}},{"line":525,"address":[],"length":0,"stats":{"Line":12}},{"line":527,"address":[],"length":0,"stats":{"Line":18}},{"line":528,"address":[],"length":0,"stats":{"Line":12}},{"line":531,"address":[],"length":0,"stats":{"Line":12}},{"line":532,"address":[],"length":0,"stats":{"Line":12}},{"line":533,"address":[],"length":0,"stats":{"Line":0}},{"line":537,"address":[],"length":0,"stats":{"Line":30}},{"line":538,"address":[],"length":0,"stats":{"Line":12}},{"line":542,"address":[],"length":0,"stats":{"Line":18}},{"line":549,"address":[],"length":0,"stats":{"Line":4}},{"line":550,"address":[],"length":0,"stats":{"Line":4}}],"covered":89,"coverable":114},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","encoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::Encoder` — abstract base for media encoders.\n//!\n//! Mirrors `src/codec/src/encoder.h`. Implementations are\n//! [`crate::ffmpeg::FFmpegEncoder`] and [`crate::oiio::OIIOEncoder`]. The\n//! workflow (C ABI encoder.h) is: fill an `EncodingParams` → init → open →\n//! write_video/audio/subtitle → flush. The trait mirrors the C++ virtual\n//! surface.\n\nuse std::sync::{Arc, Mutex, OnceLock};\n\nuse oakcore_rs::{PixelFormat, SampleFormat};\n\nuse crate::encodingparams::EncodingParams;\nuse crate::frame::Frame;\n\n/// `olive::Encoder` — encoder trait. Backs the refcounted encoder handle.\npub trait Encoder: Send + Sync {\n\t/// Unique encoder id.\n\tfn id(\u0026self) -\u003e String;\n\n\t/// Whether this encoder writes video.\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder writes audio.\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder writes subtitles.\n\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder writes an image sequence.\n\tfn supports_image_sequences(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Whether this encoder is deterministic for a given config\n\t/// (used for cache keys).\n\tfn is_configurable(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\t/// Configure the encoder (per-codec options like `crf`).\n\tfn configure(\u0026self, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Open the output file and write headers.\n\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Close the output (write trailer); idempotent.\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Encode one video frame (converts delivery pixel format internally).\n\tfn write_video(\u0026self, frame: \u0026Frame) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Encode interleaved float audio samples.\n\tfn write_audio(\u0026self, samples: \u0026[f32], frame_count: i32) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Encode one subtitle entry (times in seconds).\n\tfn write_subtitle(\n\t\t\u0026self,\n\t\ttext: \u0026str,\n\t\tin_seconds: f64,\n\t\tout_seconds: f64,\n\t) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// Flush encoders, write the trailer, close the file.\n\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e;\n\n\t/// The pixel format the encoder wants frames in (or `None`).\n\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e;\n\n\t/// The sample format the encoder wants audio in (or `None`).\n\tfn desired_sample_format(\u0026self) -\u003e Option\u003cSampleFormat\u003e;\n\n\t/// The configured output filename.\n\tfn filename(\u0026self) -\u003e String;\n\n\t/// Human-readable detail of the last failed operation (empty when\n\t/// none). Mirrors the C++ `Encoder::get_error()` used by\n\t/// `oakcodec_encoder_last_error`.\n\tfn get_error(\u0026self) -\u003e String {\n\t\tString::new()\n\t}\n}\n\n/// Test-injected encoder registry (see [`set_test_encoders`]); empty when\n/// not injected, in which case [`create_from_params`] falls back to the\n/// built-in format mapping.\nstatic TEST_ENCODERS: OnceLock\u003cMutex\u003cVec\u003cArc\u003cdyn Encoder\u003e\u003e\u003e\u003e = OnceLock::new();\n\n/// Replace the encoder registry with `list`; pass an empty list to restore\n/// the built-in behavior.\n///\n/// Test/extension support (the C ABI has no way to register an encoder, so\n/// the contract tests drive the encode state machine through a fake\n/// encoder). Hidden from docs; never called by production code.\n#[doc(hidden)]\npub fn set_test_encoders(list: Vec\u003cArc\u003cdyn Encoder\u003e\u003e) {\n\tlet store = TEST_ENCODERS.get_or_init(|| Mutex::new(Vec::new()));\n\t*store.lock().unwrap() = list;\n}\n\n/// `Encoder::create_from_params` — instantiate an encoder for `params`.\n///\n/// # CPP-PARITY\n/// `src/codec/src/encoder.cpp` `create_from_params` → `create_from_format`\n/// picks the FFmpeg/OIIO implementation from `params.format` (DNxHD,\n/// Matroska, QuickTime, MPEG-4 video/audio, WAV, AIFF, MP3, FLAC, Ogg,\n/// WebM, SRT → FFmpeg; OpenEXR, PNG, TIFF → OIIO; anything else → `None`).\n/// A non-empty test-injected list (see [`set_test_encoders`]) wins over the\n/// built-in mapping. The concrete implementations are dylib stubs whose\n/// `open()` fails with a clear message, so an initialized encoder handle is\n/// always constructible for a recognized format.\npub fn create_from_params(params: \u0026EncodingParams) -\u003e Option\u003cArc\u003cdyn Encoder\u003e\u003e {\n\tif let Some(store) = TEST_ENCODERS.get() {\n\t\tlet injected = store.lock().unwrap();\n\t\tif !injected.is_empty() {\n\t\t\treturn injected.first().cloned();\n\t\t}\n\t}\n\tmatch encoder_type_from_format(params.format) {\n\t\tSome(EncoderType::FFmpeg) =\u003e {\n\t\t\tSome(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(params.clone())))\n\t\t}\n\t\tSome(EncoderType::OIIO) =\u003e {\n\t\t\tSome(Arc::new(crate::oiio::OIIOEncoder { params: params.clone() }))\n\t\t}\n\t\tNone =\u003e None,\n\t}\n}\n\n/// `Encoder::Type` mirror (`encoder.cpp` `get_type_from_format`).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nenum EncoderType {\n\t/// `k_encoder_type_f_fmpeg`.\n\tFFmpeg,\n\t/// `k_encoder_type_oiio`.\n\tOIIO,\n}\n\n/// `Encoder::get_type_from_format` — the implementation family for an\n/// `ExportFormat::Format` int; `None` for unknown/`Count`.\nfn encoder_type_from_format(format: i32) -\u003e Option\u003cEncoderType\u003e {\n\tmatch format {\n\t\t// FFmpeg-backed containers.\n\t\t0 | 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 =\u003e Some(EncoderType::FFmpeg),\n\t\t// OIIO-backed still-image formats.\n\t\t3 | 5 | 6 =\u003e Some(EncoderType::OIIO),\n\t\t_ =\u003e None,\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn create_from_params_maps_formats() {\n\t\tlet mut p = EncodingParams::default();\n\n\t\t// FFmpeg-backed containers.\n\t\tfor fmt in [0, 1, 2, 4, 7, 8, 9, 10, 11, 12, 13, 14] {\n\t\t\tp.format = fmt;\n\t\t\tlet e = create_from_params(\u0026p).expect(\"format {fmt}\");\n\t\t\tassert_eq!(e.id(), \"ffmpeg\", \"format {fmt}\");\n\t\t}\n\t\t// OIIO-backed still images.\n\t\tfor fmt in [3, 5, 6] {\n\t\t\tp.format = fmt;\n\t\t\tlet e = create_from_params(\u0026p).expect(\"format {fmt}\");\n\t\t\tassert_eq!(e.id(), \"oiio\", \"format {fmt}\");\n\t\t}\n\t\t// Unknown / Count -\u003e None (C++ `k_encoder_type_none`).\n\t\tp.format = 15;\n\t\tassert!(create_from_params(\u0026p).is_none());\n\t\tp.format = -1;\n\t\tassert!(create_from_params(\u0026p).is_none());\n\t}\n\n\t#[test]\n\tfn get_error_defaults_to_empty() {\n\t\tlet e = UnimplementedDummy;\n\t\tassert_eq!(e.get_error(), \"\");\n\t}\n\n\tstruct UnimplementedDummy;\n\n\timpl Encoder for UnimplementedDummy {\n\t\tfn id(\u0026self) -\u003e String {\n\t\t\t\"dummy\".to_string()\n\t\t}\n\t\tfn configure(\u0026self, _p: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_video(\u0026self, _f: \u0026Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_audio(\u0026self, _s: \u0026[f32], _c: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_subtitle(\u0026self, _t: \u0026str, _i: f64, _o: f64) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e {\n\t\t\tNone\n\t\t}\n\t\tfn desired_sample_format(\u0026self) -\u003e Option\u003cSampleFormat\u003e {\n\t\t\tNone\n\t\t}\n\t\tfn filename(\u0026self) -\u003e String {\n\t\t\tString::new()\n\t\t}\n\t}\n}\n","traces":[{"line":38,"address":[],"length":0,"stats":{"Line":0}},{"line":39,"address":[],"length":0,"stats":{"Line":0}},{"line":43,"address":[],"length":0,"stats":{"Line":0}},{"line":44,"address":[],"length":0,"stats":{"Line":0}},{"line":48,"address":[],"length":0,"stats":{"Line":0}},{"line":49,"address":[],"length":0,"stats":{"Line":0}},{"line":53,"address":[],"length":0,"stats":{"Line":0}},{"line":54,"address":[],"length":0,"stats":{"Line":0}},{"line":59,"address":[],"length":0,"stats":{"Line":0}},{"line":60,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":2}},{"line":102,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":10}},{"line":119,"address":[],"length":0,"stats":{"Line":32}},{"line":120,"address":[],"length":0,"stats":{"Line":30}},{"line":134,"address":[],"length":0,"stats":{"Line":22}},{"line":135,"address":[],"length":0,"stats":{"Line":27}},{"line":136,"address":[],"length":0,"stats":{"Line":20}},{"line":137,"address":[],"length":0,"stats":{"Line":5}},{"line":138,"address":[],"length":0,"stats":{"Line":6}},{"line":141,"address":[],"length":0,"stats":{"Line":38}},{"line":143,"address":[],"length":0,"stats":{"Line":39}},{"line":146,"address":[],"length":0,"stats":{"Line":6}},{"line":148,"address":[],"length":0,"stats":{"Line":3}},{"line":163,"address":[],"length":0,"stats":{"Line":19}},{"line":164,"address":[],"length":0,"stats":{"Line":19}},{"line":166,"address":[],"length":0,"stats":{"Line":13}},{"line":168,"address":[],"length":0,"stats":{"Line":3}},{"line":169,"address":[],"length":0,"stats":{"Line":3}}],"covered":19,"coverable":29},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","encodingparams.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::EncodingParams` — export encoding parameters.\n//!\n//! Mirrors the `EncodingParams` class carried in `src/codec/src/encoder.h`\n//! and the flattened `oakcodec_encoding_params` POD in\n//! `include/codec/encoder.h`. The Rust struct mirrors the POD verbatim so\n//! `ffi.rs` can marshal it without translation.\n\nuse oakcore_rs::{PixelFormat, SampleFormat};\n\n/// `VideoParams::Interlacing` values.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum VideoScalingMethod {\n\t/// Fit the source into the destination, preserving aspect.\n\tFit = 0,\n\t/// Stretch the source to the destination.\n\tStretch = 1,\n\t/// Crop the source to the destination.\n\tCrop = 2,\n}\n\n/// `olive::EncodingParams` — flattened mirror of `oakcodec_encoding_params`.\n#[derive(Clone, Debug)]\n#[repr(C)]\npub struct EncodingParams {\n\t/// Output filename (or image-sequence \"[#####]\" template).\n\tpub filename: [u8; 1024],\n\t/// `ExportFormat::Format`.\n\tpub format: i32,\n\n\t/// Video track enabled (1/0; C `int`).\n\tpub video_enabled: i32,\n\t/// `ExportCodec::Codec`.\n\tpub video_codec: i32,\n\t/// Output width.\n\tpub video_width: i32,\n\t/// Output height.\n\tpub video_height: i32,\n\t/// Frame duration (rational): numerator.\n\tpub video_time_base_num: i32,\n\t/// Frame duration (rational): denominator.\n\tpub video_time_base_den: i32,\n\t/// Delivery `OakPixelFormat`.\n\tpub video_pixel_format: PixelFormat,\n\t/// Interlacing (`Interlacing` value).\n\tpub video_interlacing: i32,\n\t/// Pixel aspect ratio numerator.\n\tpub video_pixel_aspect_num: i32,\n\t/// Pixel aspect ratio denominator.\n\tpub video_pixel_aspect_den: i32,\n\t/// Bit rate (bit/s), 0 = codec default.\n\tpub video_bit_rate: i64,\n\t/// Min bit rate.\n\tpub video_min_bit_rate: i64,\n\t/// Max bit rate.\n\tpub video_max_bit_rate: i64,\n\t/// Buffer size (bytes).\n\tpub video_buffer_size: i64,\n\t/// Encoding threads (0 = auto).\n\tpub video_threads: i32,\n\t/// Encoded pixel format name (e.g. \"yuv420p\").\n\tpub video_pix_fmt: [u8; 64],\n\t/// Image sequence output (1/0).\n\tpub video_is_image_sequence: i32,\n\t/// Scaling method (`VideoScalingMethod`).\n\tpub video_scaling_method: VideoScalingMethod,\n\n\t/// Audio track enabled (1/0).\n\tpub audio_enabled: i32,\n\t/// `ExportCodec::Codec`.\n\tpub audio_codec: i32,\n\t/// Audio sample rate.\n\tpub audio_sample_rate: i32,\n\t/// ffmpeg-style channel layout mask.\n\tpub audio_channel_layout: u64,\n\t/// `SampleFormat`.\n\tpub audio_sample_format: SampleFormat,\n\t/// Audio bit rate.\n\tpub audio_bit_rate: i64,\n\n\t/// Subtitles track enabled (1/0).\n\tpub subtitles_enabled: i32,\n\t/// `ExportCodec::Codec`.\n\tpub subtitles_codec: i32,\n\t/// Subtitles written as a sidecar file (1/0).\n\tpub subtitles_are_sidecar: i32,\n\t/// Sidecar format (`ExportFormat::Format`).\n\tpub subtitles_sidecar_format: i32,\n\n\t/// Output OCIO colorspace name; empty = reference space (no transform).\n\tpub color_transform_output: [u8; 256],\n\n\t/// Export length in seconds (rational).\n\tpub export_length_num: i32,\n\t/// Export length in seconds (rational).\n\tpub export_length_den: i32,\n\n\t/// Custom export range enabled (1/0).\n\tpub has_custom_range: i32,\n\t/// Custom range in, rational seconds.\n\tpub custom_range_in_num: i64,\n\t/// Custom range in denominator.\n\tpub custom_range_in_den: i64,\n\t/// Custom range out, rational seconds.\n\tpub custom_range_out_num: i64,\n\t/// Custom range out denominator.\n\tpub custom_range_out_den: i64,\n}\n\nimpl Default for EncodingParams {\n\tfn default() -\u003e Self {\n\t\tSelf {\n\t\t\tfilename: [0; 1024],\n\t\t\tformat: -1,\n\n\t\t\tvideo_enabled: 0,\n\t\t\tvideo_codec: -1,\n\t\t\tvideo_width: 0,\n\t\t\tvideo_height: 0,\n\t\t\tvideo_time_base_num: 1,\n\t\t\tvideo_time_base_den: 1,\n\t\t\tvideo_pixel_format: PixelFormat::Invalid,\n\t\t\tvideo_interlacing: 0,\n\t\t\tvideo_pixel_aspect_num: 0,\n\t\t\tvideo_pixel_aspect_den: 0,\n\t\t\tvideo_bit_rate: 0,\n\t\t\tvideo_min_bit_rate: 0,\n\t\t\tvideo_max_bit_rate: 0,\n\t\t\tvideo_buffer_size: 0,\n\t\t\tvideo_threads: 0,\n\t\t\tvideo_pix_fmt: [0; 64],\n\t\t\tvideo_is_image_sequence: 0,\n\t\t\tvideo_scaling_method: VideoScalingMethod::Stretch,\n\n\t\t\taudio_enabled: 0,\n\t\t\taudio_codec: -1,\n\t\t\taudio_sample_rate: 0,\n\t\t\taudio_channel_layout: 0,\n\t\t\taudio_sample_format: SampleFormat::Invalid,\n\t\t\taudio_bit_rate: 0,\n\n\t\t\tsubtitles_enabled: 0,\n\t\t\tsubtitles_codec: -1,\n\t\t\tsubtitles_are_sidecar: 0,\n\t\t\tsubtitles_sidecar_format: -1,\n\n\t\t\tcolor_transform_output: [0; 256],\n\n\t\t\texport_length_num: 0,\n\t\t\texport_length_den: 0,\n\n\t\t\thas_custom_range: 0,\n\t\t\tcustom_range_in_num: 0,\n\t\t\tcustom_range_in_den: 0,\n\t\t\tcustom_range_out_num: 0,\n\t\t\tcustom_range_out_den: 0,\n\t\t}\n\t}\n}\n\nimpl EncodingParams {\n\t/// Scaling matrix for a scaling method; row-major 4x4 into `out[16]`.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/encoder.cpp` `EncodingParams::generate_matrix` —\n\t/// returns a row-major `std::array\u003cfloat,16\u003e` (formerly QMatrix4x4).\n\tpub fn generate_matrix(\n\t\tmethod: VideoScalingMethod,\n\t\tsrc_width: i32,\n\t\tsrc_height: i32,\n\t\tdst_width: i32,\n\t\tdst_height: i32,\n\t\tout: \u0026mut [f64; 16],\n\t) {\n\t\t// Identity (former default-constructed QMatrix4x4), row-major.\n\t\t*out = [\n\t\t\t1.0, 0.0, 0.0, 0.0, //\n\t\t\t0.0, 1.0, 0.0, 0.0, //\n\t\t\t0.0, 0.0, 1.0, 0.0, //\n\t\t\t0.0, 0.0, 0.0, 1.0,\n\t\t];\n\n\t\tif method == VideoScalingMethod::Stretch {\n\t\t\treturn;\n\t\t}\n\n\t\t// Guard degenerate sizes: the C++ would produce inf/NaN here; an\n\t\t// identity is the only safe output for the rendering pipeline.\n\t\tif src_width \u003c= 0 || src_height \u003c= 0 || dst_width \u003c= 0 || dst_height \u003c= 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet export_ar = dst_width as f64 / dst_height as f64;\n\t\tlet source_ar = src_width as f64 / src_height as f64;\n\n\t\t// qFuzzyCompare(export_ar, source_ar): within one part in 100000.\n\t\tif (export_ar - source_ar).abs() * 100000.0\n\t\t\t\u003c= export_ar.abs().min(source_ar.abs())\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tif (export_ar \u003e source_ar) == (method == VideoScalingMethod::Fit) {\n\t\t\t// scale(source_ar / export_ar, 1)\n\t\t\tout[0] = source_ar / export_ar;\n\t\t} else {\n\t\t\t// scale(1, export_ar / source_ar)\n\t\t\tout[5] = export_ar / source_ar;\n\t\t}\n\t}\n\n\t/// File extension for this format (e.g. \"mp4\").\n\t///\n\t/// # CPP-PARITY\n\t/// `ExportFormat::get_extension` (`src/codec/src/exportformat.cpp`) keyed\n\t/// on the serialized `Format` discriminant. Unknown / invalid formats\n\t/// return the empty string, matching the C++ default case.\n\tpub fn extension(\u0026self) -\u003e \u0026str {\n\t\tmatch self.format {\n\t\t\t0 =\u003e \"mxf\", // DNxHD\n\t\t\t1 =\u003e \"mkv\", // Matroska\n\t\t\t2 =\u003e \"mp4\", // MPEG-4 video\n\t\t\t3 =\u003e \"exr\", // OpenEXR\n\t\t\t4 =\u003e \"mov\", // QuickTime\n\t\t\t5 =\u003e \"png\", // PNG\n\t\t\t6 =\u003e \"tiff\", // TIFF\n\t\t\t7 =\u003e \"wav\", // WAV\n\t\t\t8 =\u003e \"aiff\", // AIFF\n\t\t\t9 =\u003e \"mp3\", // MP3\n\t\t\t10 =\u003e \"flac\", // FLAC\n\t\t\t11 =\u003e \"ogg\", // Ogg\n\t\t\t12 =\u003e \"webm\", // WebM\n\t\t\t13 =\u003e \"srt\", // SRT\n\t\t\t14 =\u003e \"m4a\", // MPEG-4 audio\n\t\t\t_ =\u003e \"\",\n\t\t}\n\t}\n\n\t/// Load from a compact XML preset string (oakcommon C++ XmlStreamReader).\n\t///\n\t/// # CPP-PARITY\n\t/// `EncodingParams::load` — uses oakcommon's C++ `XmlStreamReader`\n\t/// (`src/common/src/xmlutils.h`), a C++-to-C++ coupling the bridge\n\t/// cannot cover (NOTES.md §7). Preserves the load_v1 bug of not\n\t/// assigning `custom_range`.\n\tpub fn load(\u0026mut self, data: \u0026str) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet root = parse_preset(data)\n\t\t\t.map_err(|e| crate::error::Error::Failed(format!(\"invalid export preset: {e}\")))?;\n\t\tif root.name != \"export\" {\n\t\t\treturn Err(crate::error::Error::Failed(\n\t\t\t\t\"not an export preset document\".to_string(),\n\t\t\t));\n\t\t}\n\n\t\t// `range` / `customrangein` / `customrangeout` are parsed for shape\n\t\t// parity but never assigned — mirroring the C++ `load_v1` bug that\n\t\t// reads them into locals and forgets to store `custom_range`.\n\t\tif let Some(e) = child(\u0026root, \"range\") {\n\t\t\tlet _ = parse_bool(\u0026text(e));\n\t\t}\n\t\tif let Some(e) = child(\u0026root, \"customrangein\") {\n\t\t\tlet _ = \u0026text(e);\n\t\t}\n\t\tif let Some(e) = child(\u0026root, \"customrangeout\") {\n\t\t\tlet _ = \u0026text(e);\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"filename\") {\n\t\t\tset_cstr(\u0026mut self.filename, \u0026text(e));\n\t\t}\n\t\tif let Some(e) = child(\u0026root, \"format\") {\n\t\t\tself.format = parse_i32(\u0026text(e));\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"video\") {\n\t\t\tif let Some(v) = attr(e, \"enabled\") {\n\t\t\t\tself.video_enabled = parse_bool(v) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"codec\") {\n\t\t\t\tself.video_codec = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"width\") {\n\t\t\t\tself.video_width = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"height\") {\n\t\t\t\tself.video_height = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"format\") {\n\t\t\t\tself.video_pixel_format = match parse_i32(\u0026text(e)) {\n\t\t\t\t\t-1 =\u003e PixelFormat::Invalid,\n\t\t\t\t\t0 =\u003e PixelFormat::U8,\n\t\t\t\t\t1 =\u003e PixelFormat::U10,\n\t\t\t\t\t2 =\u003e PixelFormat::U16,\n\t\t\t\t\t3 =\u003e PixelFormat::F16,\n\t\t\t\t\t4 =\u003e PixelFormat::F32,\n\t\t\t\t\t_ =\u003e PixelFormat::Invalid,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"timebase\") {\n\t\t\t\tlet tb = text(e);\n\t\t\t\tif let Some((n, d)) = tb.split_once('/') {\n\t\t\t\t\tself.video_time_base_num = parse_i32(n);\n\t\t\t\t\tself.video_time_base_den = parse_i32(d);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"divider\") {\n\t\t\t\tself.video_interlacing = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"pixelaspect\") {\n\t\t\t\tlet par = text(e);\n\t\t\t\tif let Some((n, d)) = par.split_once('/') {\n\t\t\t\t\tself.video_pixel_aspect_num = parse_i32(n);\n\t\t\t\t\tself.video_pixel_aspect_den = parse_i32(d);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"bitrate\") {\n\t\t\t\tself.video_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"minbitrate\") {\n\t\t\t\tself.video_min_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"maxbitrate\") {\n\t\t\t\tself.video_max_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"bufsize\") {\n\t\t\t\tself.video_buffer_size = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"threads\") {\n\t\t\t\tself.video_threads = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"pixfmt\") {\n\t\t\t\tset_cstr(\u0026mut self.video_pix_fmt, \u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"imgseq\") {\n\t\t\t\tself.video_is_image_sequence = parse_bool(\u0026text(e)) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"vscale\") {\n\t\t\t\tself.video_scaling_method = scaling_from_i32(parse_i32(\u0026text(e)));\n\t\t\t}\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"audio\") {\n\t\t\tif let Some(v) = attr(e, \"enabled\") {\n\t\t\t\tself.audio_enabled = parse_bool(v) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"codec\") {\n\t\t\t\tself.audio_codec = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"samplerate\") {\n\t\t\t\tself.audio_sample_rate = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"channellayout\") {\n\t\t\t\tself.audio_channel_layout = parse_i64(\u0026text(e)) as u64;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"format\") {\n\t\t\t\tself.audio_sample_format = sample_format_from_i32(parse_i32(\u0026text(e)));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"bitrate\") {\n\t\t\t\tself.audio_bit_rate = parse_i64(\u0026text(e));\n\t\t\t}\n\t\t}\n\n\t\tif let Some(e) = child(\u0026root, \"subtitles\") {\n\t\t\tif let Some(v) = attr(e, \"enabled\") {\n\t\t\t\tself.subtitles_enabled = parse_bool(v) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"sidecar\") {\n\t\t\t\tself.subtitles_are_sidecar = parse_bool(\u0026text(e)) as i32;\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"sidecarformat\") {\n\t\t\t\tself.subtitles_sidecar_format = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t\tif let Some(e) = child(e, \"codec\") {\n\t\t\t\tself.subtitles_codec = parse_i32(\u0026text(e));\n\t\t\t}\n\t\t}\n\n\t\tOk(())\n\t}\n\n\t/// Serialize to a compact XML string (no declaration, no indentation;\n\t/// element/attribute names and order preserved).\n\t///\n\t/// # CPP-PARITY\n\t/// The C++ writer also emits per-codec `video_opts_`; those moved out\n\t/// of the byte-exact `EncodingParams` POD (see `encoding_params_c_abi_layout`),\n\t/// so the options are not serialized here.\n\tpub fn save_to_string(\u0026self) -\u003e String {\n\t\tlet mut s = String::new();\n\t\ts.push_str(\"\u003cexport version=\\\"1\\\"\u003e\");\n\t\ts.push_str(\u0026format!(\"\u003cfilename\u003e{}\u003c/filename\u003e\", escape_xml(cstr(\u0026self.filename))));\n\t\ts.push_str(\u0026format!(\"\u003cformat\u003e{}\u003c/format\u003e\", self.format));\n\t\ts.push_str(\u0026format!(\"\u003crange\u003e{}\u003c/range\u003e\", self.has_custom_range));\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003ccustomrangein\u003e{}/{}\u003c/customrangein\u003e\",\n\t\t\tself.custom_range_in_num, self.custom_range_in_den\n\t\t));\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003ccustomrangeout\u003e{}/{}\u003c/customrangeout\u003e\",\n\t\t\tself.custom_range_out_num, self.custom_range_out_den\n\t\t));\n\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003cvideo enabled=\\\"{}\\\"\u003e\",\n\t\t\tself.video_enabled\n\t\t));\n\t\tif self.video_enabled != 0 {\n\t\t\ts.push_str(\u0026format!(\"\u003ccodec\u003e{}\u003c/codec\u003e\", self.video_codec));\n\t\t\ts.push_str(\u0026format!(\"\u003cwidth\u003e{}\u003c/width\u003e\", self.video_width));\n\t\t\ts.push_str(\u0026format!(\"\u003cheight\u003e{}\u003c/height\u003e\", self.video_height));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cformat\u003e{}\u003c/format\u003e\",\n\t\t\t\tself.video_pixel_format as i32\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003ctimebase\u003e{}/{}\u003c/timebase\u003e\",\n\t\t\t\tself.video_time_base_num, self.video_time_base_den\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cdivider\u003e{}\u003c/divider\u003e\",\n\t\t\t\tself.video_interlacing\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cpixelaspect\u003e{}/{}\u003c/pixelaspect\u003e\",\n\t\t\t\tself.video_pixel_aspect_num, self.video_pixel_aspect_den\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\"\u003cbitrate\u003e{}\u003c/bitrate\u003e\", self.video_bit_rate));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cminbitrate\u003e{}\u003c/minbitrate\u003e\",\n\t\t\t\tself.video_min_bit_rate\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cmaxbitrate\u003e{}\u003c/maxbitrate\u003e\",\n\t\t\t\tself.video_max_bit_rate\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cbufsize\u003e{}\u003c/bufsize\u003e\",\n\t\t\t\tself.video_buffer_size\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\"\u003cthreads\u003e{}\u003c/threads\u003e\", self.video_threads));\n\t\t\ts.push_str(\u0026format!(\"\u003cpixfmt\u003e{}\u003c/pixfmt\u003e\", escape_xml(cstr(\u0026self.video_pix_fmt))));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cimgseq\u003e{}\u003c/imgseq\u003e\",\n\t\t\t\tself.video_is_image_sequence\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cvscale\u003e{}\u003c/vscale\u003e\",\n\t\t\t\tself.video_scaling_method as i32\n\t\t\t));\n\t\t}\n\t\ts.push_str(\"\u003c/video\u003e\");\n\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003caudio enabled=\\\"{}\\\"\u003e\",\n\t\t\tself.audio_enabled\n\t\t));\n\t\tif self.audio_enabled != 0 {\n\t\t\ts.push_str(\u0026format!(\"\u003ccodec\u003e{}\u003c/codec\u003e\", self.audio_codec));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003csamplerate\u003e{}\u003c/samplerate\u003e\",\n\t\t\t\tself.audio_sample_rate\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cchannellayout\u003e{}\u003c/channellayout\u003e\",\n\t\t\t\tself.audio_channel_layout\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003cformat\u003e{}\u003c/format\u003e\",\n\t\t\t\tself.audio_sample_format as i32\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\"\u003cbitrate\u003e{}\u003c/bitrate\u003e\", self.audio_bit_rate));\n\t\t}\n\t\ts.push_str(\"\u003c/audio\u003e\");\n\n\t\ts.push_str(\u0026format!(\n\t\t\t\"\u003csubtitles enabled=\\\"{}\\\"\u003e\",\n\t\t\tself.subtitles_enabled\n\t\t));\n\t\tif self.subtitles_enabled != 0 {\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003csidecar\u003e{}\u003c/sidecar\u003e\",\n\t\t\t\tself.subtitles_are_sidecar\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003csidecarformat\u003e{}\u003c/sidecarformat\u003e\",\n\t\t\t\tself.subtitles_sidecar_format\n\t\t\t));\n\t\t\ts.push_str(\u0026format!(\n\t\t\t\t\"\u003ccodec\u003e{}\u003c/codec\u003e\",\n\t\t\t\tself.subtitles_codec\n\t\t\t));\n\t\t}\n\t\ts.push_str(\"\u003c/subtitles\u003e\");\n\n\t\ts.push_str(\"\u003c/export\u003e\");\n\t\ts\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Minimal XML helpers for the round-trip `load`/`save_to_string`.\n//\n// CPP-PARITY: the C++ `load`/`save_to_string` go through oakcommon's\n// `XmlStreamReader`/`XmlStreamWriter` (a C++-to-C++ coupling the Rust bridge\n// cannot cover, NOTES.md §7). Rather than returning `Err`, this port keeps a\n// minimal but faithful round-trip for the fields representable without the\n// videoparams/colortransform/audio bridge. Deviations vs the C++ writer:\n// * the `\u003cvideo\u003e` width/height/format are taken from the Rust fields\n// directly (the C++ reads them out of a live `OakVideoParams` handle);\n// * `\u003ctimebase\u003e`, `\u003cdivider\u003e`, `\u003cpixelaspect\u003e` mirror the corresponding\n// flattened fields;\n// * `customrangein`/`customrangeout` are written but, on `load`, parsed and\n// discarded — preserving the C++ `load_v1` bug that never assigns\n// `custom_range`.\n// ---------------------------------------------------------------------------\n\n/// Minimal element of the round-trip XML tree.\nstruct El {\n\tname: String,\n\tattrs: Vec\u003c(String, String)\u003e,\n\ttext: Option\u003cString\u003e,\n\tchildren: Vec\u003cEl\u003e,\n}\n\n/// Positional cursor over the XML text.\nstruct Cursor\u003c'a\u003e {\n\ts: \u0026'a str,\n\ti: usize,\n}\n\nimpl\u003c'a\u003e Cursor\u003c'a\u003e {\n\tfn peek(\u0026self) -\u003e Option\u003cchar\u003e {\n\t\tself.s[self.i..].chars().next()\n\t}\n\n\tfn skip_ws(\u0026mut self) {\n\t\twhile self.peek().map_or(false, char::is_whitespace) {\n\t\t\tself.i += 1;\n\t\t}\n\t}\n\n\tfn starts_with(\u0026self, p: \u0026str) -\u003e bool {\n\t\tself.s[self.i..].starts_with(p)\n\t}\n\n\tfn read_name(\u0026mut self) -\u003e Result\u003cString, String\u003e {\n\t\tlet start = self.i;\n\t\twhile let Some(ch) = self.peek() {\n\t\t\tif ch.is_alphanumeric() || ch == '_' || ch == '-' {\n\t\t\t\tself.i += ch.len_utf8();\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif self.i == start {\n\t\t\treturn Err(\"expected element/attribute name\".to_string());\n\t\t}\n\t\tOk(self.s[start..self.i].to_string())\n\t}\n\n\tfn read_attr_value(\u0026mut self) -\u003e Result\u003cString, String\u003e {\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\\\"\") {\n\t\t\treturn Err(\"expected quoted attribute value\".to_string());\n\t\t}\n\t\tself.i += 1;\n\t\tlet start = self.i;\n\t\twhile self.i \u003c self.s.len() \u0026\u0026 !self.starts_with(\"\\\"\") {\n\t\t\tself.i += 1;\n\t\t}\n\t\tif self.i \u003e= self.s.len() {\n\t\t\treturn Err(\"unterminated attribute value\".to_string());\n\t\t}\n\t\tlet v = self.s[start..self.i].to_string();\n\t\tself.i += 1;\n\t\tOk(v)\n\t}\n\n\tfn expect_end(\u0026mut self, name: \u0026str) -\u003e Result\u003c(), String\u003e {\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\u003c/\") {\n\t\t\treturn Err(format!(\"expected \u003c/{}\u003e\", name));\n\t\t}\n\t\tself.i += 2;\n\t\tlet n = self.read_name()?;\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\u003e\") {\n\t\t\treturn Err(\"expected '\u003e' closing end tag\".to_string());\n\t\t}\n\t\tself.i += 1;\n\t\tif n != name {\n\t\t\treturn Err(format!(\"mismatched end tag {} vs {}\", n, name));\n\t\t}\n\t\tOk(())\n\t}\n\n\tfn parse_element(\u0026mut self) -\u003e Result\u003cEl, String\u003e {\n\t\tself.skip_ws();\n\t\tif !self.starts_with(\"\u003c\") {\n\t\t\treturn Err(\"expected element\".to_string());\n\t\t}\n\t\tself.i += 1;\n\t\tlet name = self.read_name()?;\n\t\tself.skip_ws();\n\t\tlet mut attrs = Vec::new();\n\t\tloop {\n\t\t\tself.skip_ws();\n\t\t\tif self.starts_with(\"/\u003e\") {\n\t\t\t\tself.i += 2;\n\t\t\t\treturn Ok(El { name, attrs, text: None, children: Vec::new() });\n\t\t\t}\n\t\t\tif self.starts_with(\"\u003e\") {\n\t\t\t\tself.i += 1;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet an = self.read_name()?;\n\t\t\tself.skip_ws();\n\t\t\tif self.starts_with(\"=\") {\n\t\t\t\tself.i += 1;\n\t\t\t}\n\t\t\tlet v = self.read_attr_value()?;\n\t\t\tattrs.push((an, v));\n\t\t}\n\t\t// Content: either nested elements or a text node.\n\t\tself.skip_ws();\n\t\tif self.i \u003c self.s.len() \u0026\u0026 self.starts_with(\"\u003c\") {\n\t\t\tlet mut children = Vec::new();\n\t\t\tloop {\n\t\t\t\tself.skip_ws();\n\t\t\t\tif self.i \u003e= self.s.len() {\n\t\t\t\t\treturn Err(\"unexpected EOF in element\".to_string());\n\t\t\t\t}\n\t\t\t\tif self.starts_with(\"\u003c/\") {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif self.starts_with(\"\u003c\") {\n\t\t\t\t\tchildren.push(self.parse_element()?);\n\t\t\t\t} else {\n\t\t\t\t\treturn Err(\"unexpected text at element level\".to_string());\n\t\t\t\t}\n\t\t\t}\n\t\t\tself.expect_end(\u0026name)?;\n\t\t\tOk(El { name, attrs, text: None, children })\n\t\t} else {\n\t\t\tlet start = self.i;\n\t\t\twhile self.i \u003c self.s.len() \u0026\u0026 !self.starts_with(\"\u003c\") {\n\t\t\t\tself.i += 1;\n\t\t\t}\n\t\t\tlet text = self.s[start..self.i].to_string();\n\t\t\tself.expect_end(\u0026name)?;\n\t\t\tOk(El { name, attrs, text: Some(text), children: Vec::new() })\n\t\t}\n\t}\n}\n\n/// Parse the single root element of a preset document.\nfn parse_preset(data: \u0026str) -\u003e Result\u003cEl, String\u003e {\n\tlet mut c = Cursor { s: data, i: 0 };\n\tc.parse_element()\n}\n\nfn child\u003c'a\u003e(el: \u0026'a El, name: \u0026str) -\u003e Option\u003c\u0026'a El\u003e {\n\tel.children.iter().find(|c| c.name == name)\n}\n\nfn text(el: \u0026El) -\u003e String {\n\tunescape_xml(el.text.as_deref().unwrap_or(\"\"))\n}\n\nfn attr\u003c'a\u003e(el: \u0026'a El, name: \u0026str) -\u003e Option\u003c\u0026'a str\u003e {\n\tel.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())\n}\n\nfn parse_i32(s: \u0026str) -\u003e i32 {\n\ts.trim().parse().unwrap_or(0)\n}\n\nfn parse_i64(s: \u0026str) -\u003e i64 {\n\ts.trim().parse().unwrap_or(0)\n}\n\nfn parse_bool(s: \u0026str) -\u003e bool {\n\tparse_i32(s) != 0\n}\n\n/// Convert an `i32` code to a [`VideoScalingMethod`]; unknown -\u003e `Stretch`\n/// (the C++ default). `pub(crate)` so the ffi encoder layer can map the\n/// `oakcodec_encoding_params.video_scaling_method` field.\npub(crate) fn scaling_from_i32(v: i32) -\u003e VideoScalingMethod {\n\tmatch v {\n\t\t0 =\u003e VideoScalingMethod::Fit,\n\t\t1 =\u003e VideoScalingMethod::Stretch,\n\t\t2 =\u003e VideoScalingMethod::Crop,\n\t\t_ =\u003e VideoScalingMethod::Stretch,\n\t}\n}\n\n/// Convert an `i32` code to a [`SampleFormat`]; unknown -\u003e `Invalid`.\n/// `pub(crate)` so the ffi encoder layer can map the\n/// `oakcodec_encoding_params.audio_sample_format` field.\npub(crate) fn sample_format_from_i32(v: i32) -\u003e SampleFormat {\n\tmatch v {\n\t\t-1 =\u003e SampleFormat::Invalid,\n\t\t0 =\u003e SampleFormat::U8Planar,\n\t\t1 =\u003e SampleFormat::S16Planar,\n\t\t2 =\u003e SampleFormat::S32Planar,\n\t\t3 =\u003e SampleFormat::S64Planar,\n\t\t4 =\u003e SampleFormat::F32Planar,\n\t\t5 =\u003e SampleFormat::F64Planar,\n\t\t6 =\u003e SampleFormat::U8,\n\t\t7 =\u003e SampleFormat::S16,\n\t\t8 =\u003e SampleFormat::S32,\n\t\t9 =\u003e SampleFormat::S64,\n\t\t10 =\u003e SampleFormat::F32,\n\t\t11 =\u003e SampleFormat::F64,\n\t\t_ =\u003e SampleFormat::Invalid,\n\t}\n}\n\n/// NUL-terminated byte array -\u003e `\u0026str` (empty when the first byte is NUL).\nfn cstr(arr: \u0026[u8]) -\u003e \u0026str {\n\tlet end = arr.iter().position(|\u0026b| b == 0).unwrap_or(arr.len());\n\tstd::str::from_utf8(\u0026arr[..end]).unwrap_or(\"\")\n}\n\n/// Write a string into a NUL-padded byte array (truncating if too long).\nfn set_cstr(dst: \u0026mut [u8], s: \u0026str) {\n\tdst.fill(0);\n\tlet n = s.len().min(dst.len());\n\tdst[..n].copy_from_slice(\u0026s.as_bytes()[..n]);\n}\n\n/// Minimal XML escaping for text content.\nfn escape_xml(s: \u0026str) -\u003e String {\n\ts.replace('\u0026', \"\u0026amp;\")\n\t\t.replace('\u003c', \"\u0026lt;\")\n\t\t.replace('\u003e', \"\u0026gt;\")\n}\n\n/// Minimal XML unescaping for text content.\nfn unescape_xml(s: \u0026str) -\u003e String {\n\ts.replace(\"\u0026lt;\", \"\u003c\")\n\t\t.replace(\"\u0026gt;\", \"\u003e\")\n\t\t.replace(\"\u0026amp;\", \"\u0026\")\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tconst EPS: f64 = 1e-9;\n\n\tfn assert_close(a: f64, b: f64) {\n\t\tassert!(\n\t\t\t(a - b).abs() \u003c EPS,\n\t\t\t\"expected {a} close to {b} (eps {EPS})\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn default_has_expected_values() {\n\t\tlet p = EncodingParams::default();\n\t\tassert_eq!(p.filename, [0; 1024]);\n\t\tassert_eq!(p.format, -1);\n\t\tassert_eq!(p.video_enabled, 0);\n\t\tassert_eq!(p.video_codec, -1);\n\t\tassert_eq!(p.video_width, 0);\n\t\tassert_eq!(p.video_height, 0);\n\t\tassert_eq!(p.video_time_base_num, 1);\n\t\tassert_eq!(p.video_time_base_den, 1);\n\t\tassert_eq!(p.video_pixel_format, PixelFormat::Invalid);\n\t\tassert_eq!(p.video_interlacing, 0);\n\t\tassert_eq!(p.video_pixel_aspect_num, 0);\n\t\tassert_eq!(p.video_pixel_aspect_den, 0);\n\t\tassert_eq!(p.video_bit_rate, 0);\n\t\tassert_eq!(p.video_min_bit_rate, 0);\n\t\tassert_eq!(p.video_max_bit_rate, 0);\n\t\tassert_eq!(p.video_buffer_size, 0);\n\t\tassert_eq!(p.video_threads, 0);\n\t\tassert_eq!(p.video_pix_fmt, [0; 64]);\n\t\tassert_eq!(p.video_is_image_sequence, 0);\n\t\tassert_eq!(p.video_scaling_method, VideoScalingMethod::Stretch);\n\t\tassert_eq!(p.audio_enabled, 0);\n\t\tassert_eq!(p.audio_codec, -1);\n\t\tassert_eq!(p.audio_sample_rate, 0);\n\t\tassert_eq!(p.audio_channel_layout, 0);\n\t\tassert_eq!(p.audio_sample_format, SampleFormat::Invalid);\n\t\tassert_eq!(p.audio_bit_rate, 0);\n\t\tassert_eq!(p.subtitles_enabled, 0);\n\t\tassert_eq!(p.subtitles_codec, -1);\n\t\tassert_eq!(p.subtitles_are_sidecar, 0);\n\t\tassert_eq!(p.subtitles_sidecar_format, -1);\n\t\tassert_eq!(p.color_transform_output, [0; 256]);\n\t\tassert_eq!(p.export_length_num, 0);\n\t\tassert_eq!(p.export_length_den, 0);\n\t\tassert_eq!(p.has_custom_range, 0);\n\t\tassert_eq!(p.custom_range_in_num, 0);\n\t\tassert_eq!(p.custom_range_in_den, 0);\n\t\tassert_eq!(p.custom_range_out_num, 0);\n\t\tassert_eq!(p.custom_range_out_den, 0);\n\t}\n\n\t#[test]\n\tfn extension_mapping() {\n\t\tlet mut p = EncodingParams::default();\n\t\tlet cases: [(i32, \u0026str); 15] = [\n\t\t\t(0, \"mxf\"),\n\t\t\t(1, \"mkv\"),\n\t\t\t(2, \"mp4\"),\n\t\t\t(3, \"exr\"),\n\t\t\t(4, \"mov\"),\n\t\t\t(5, \"png\"),\n\t\t\t(6, \"tiff\"),\n\t\t\t(7, \"wav\"),\n\t\t\t(8, \"aiff\"),\n\t\t\t(9, \"mp3\"),\n\t\t\t(10, \"flac\"),\n\t\t\t(11, \"ogg\"),\n\t\t\t(12, \"webm\"),\n\t\t\t(13, \"srt\"),\n\t\t\t(14, \"m4a\"),\n\t\t];\n\t\tfor (fmt, ext) in cases {\n\t\t\tp.format = fmt;\n\t\t\tassert_eq!(p.extension(), ext, \"format {fmt}\");\n\t\t}\n\t\t// Unknown / invalid formats map to the empty string.\n\t\tp.format = 99;\n\t\tassert_eq!(p.extension(), \"\");\n\t\tp.format = -3;\n\t\tassert_eq!(p.extension(), \"\");\n\t}\n\n\t#[test]\n\tfn generate_matrix_stretch_is_identity() {\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Stretch,\n\t\t\t1920,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\t}\n\n\t#[test]\n\tfn generate_matrix_equal_aspect_is_identity() {\n\t\t// Same 16:9 aspect: export_ar == source_ar -\u003e fuzzy-equal -\u003e identity.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Fit,\n\t\t\t1920,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\t}\n\n\t#[test]\n\tfn generate_matrix_fit_scales_x() {\n\t\t// src square (ar 1.0) -\u003e dst 2:1 (ar 2.0). Fit: source wider/narrower\n\t\t// relative to export -\u003e the x axis is squeezed to source_ar/export_ar.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Fit,\n\t\t\t1000,\n\t\t\t1000,\n\t\t\t2000,\n\t\t\t1000,\n\t\t\t\u0026mut out,\n\t\t);\n\t\t// export_ar(2.0) \u003e source_ar(1.0); Fit =\u003e scale(source_ar/export_ar, 1).\n\t\tassert_close(out[0], 0.5);\n\t\tassert_close(out[5], 1.0);\n\t}\n\n\t#[test]\n\tfn generate_matrix_crop_scales_y() {\n\t\t// src square -\u003e dst 2:1. Crop: fit inside, so we zoom the y axis by\n\t\t// export_ar/source_ar and keep x unscaled.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Crop,\n\t\t\t1000,\n\t\t\t1000,\n\t\t\t2000,\n\t\t\t1000,\n\t\t\t\u0026mut out,\n\t\t);\n\t\t// export_ar(2.0) \u003e source_ar(1.0); not Fit =\u003e scale(1, export_ar/source_ar).\n\t\tassert_close(out[0], 1.0);\n\t\tassert_close(out[5], 2.0);\n\t}\n\n\t#[test]\n\tfn generate_matrix_crop_scales_x() {\n\t\t// src square -\u003e dst 0.5:1 (ar 0.5). Crop: export narrower than source,\n\t\t// zoom the x axis by source_ar/export_ar.\n\t\tlet mut out = [0.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Crop,\n\t\t\t1000,\n\t\t\t1000,\n\t\t\t500,\n\t\t\t1000,\n\t\t\t\u0026mut out,\n\t\t);\n\t\t// export_ar(0.5) \u003c source_ar(1.0); (ar\u003esource) == false, (method==Fit)\n\t\t// false =\u003e false == false true =\u003e scale(source_ar/export_ar, 1) = 2.0.\n\t\tassert_close(out[0], 2.0);\n\t\tassert_close(out[5], 1.0);\n\t}\n\n\t#[test]\n\tfn generate_matrix_degenerate_is_identity() {\n\t\t// Zero / negative source sizes must not produce inf/NaN.\n\t\tlet mut out = [9.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Fit,\n\t\t\t0,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\n\t\tlet mut out = [9.0; 16];\n\t\tEncodingParams::generate_matrix(\n\t\t\tVideoScalingMethod::Crop,\n\t\t\t-1,\n\t\t\t1080,\n\t\t\t1280,\n\t\t\t720,\n\t\t\t\u0026mut out,\n\t\t);\n\t\tassert_eq!(out, identity());\n\t}\n\n\t#[test]\n\tfn save_load_roundtrip() {\n\t\tlet mut p = EncodingParams::default();\n\t\tp.filename = [0; 1024];\n\t\tlet name = b\"out/video.mp4\";\n\t\tp.filename[..name.len()].copy_from_slice(name);\n\t\tp.format = 2;\n\t\tp.video_enabled = 1;\n\t\tp.video_codec = 3;\n\t\tp.video_width = 1280;\n\t\tp.video_height = 720;\n\t\tp.video_time_base_num = 1;\n\t\tp.video_time_base_den = 30;\n\t\tp.video_pixel_format = PixelFormat::U8;\n\t\tp.video_interlacing = 1;\n\t\tp.video_pixel_aspect_num = 1;\n\t\tp.video_pixel_aspect_den = 1;\n\t\tp.video_bit_rate = 8_000_000;\n\t\tp.video_min_bit_rate = 4_000_000;\n\t\tp.video_max_bit_rate = 16_000_000;\n\t\tp.video_buffer_size = 4_000_000;\n\t\tp.video_threads = 4;\n\t\tlet pix = b\"yuv420p\";\n\t\tp.video_pix_fmt[..pix.len()].copy_from_slice(pix);\n\t\tp.video_is_image_sequence = 1;\n\t\tp.video_scaling_method = VideoScalingMethod::Crop;\n\n\t\tp.audio_enabled = 1;\n\t\tp.audio_codec = 1;\n\t\tp.audio_sample_rate = 48000;\n\t\tp.audio_channel_layout = 3; // stereo mask\n\t\tp.audio_sample_format = SampleFormat::F32Planar;\n\t\tp.audio_bit_rate = 320_000;\n\n\t\tp.subtitles_enabled = 1;\n\t\tp.subtitles_codec = 1;\n\t\tp.subtitles_are_sidecar = 1;\n\t\tp.subtitles_sidecar_format = 13; // srt\n\n\t\tlet xml = p.save_to_string();\n\t\tassert!(xml.starts_with(\"\u003cexport version=\\\"1\\\"\u003e\"), \"xml: {xml}\");\n\n\t\tlet mut q = EncodingParams::default();\n\t\tq.load(\u0026xml).expect(\"load round-trip\");\n\n\t\tassert_eq!(q.filename[..name.len()], name[..]);\n\t\tassert_eq!(q.format, 2);\n\t\tassert_eq!(q.video_enabled, 1);\n\t\tassert_eq!(q.video_codec, 3);\n\t\tassert_eq!(q.video_width, 1280);\n\t\tassert_eq!(q.video_height, 720);\n\t\tassert_eq!(q.video_time_base_num, 1);\n\t\tassert_eq!(q.video_time_base_den, 30);\n\t\tassert_eq!(q.video_pixel_format, PixelFormat::U8);\n\t\tassert_eq!(q.video_interlacing, 1);\n\t\tassert_eq!(q.video_pixel_aspect_num, 1);\n\t\tassert_eq!(q.video_pixel_aspect_den, 1);\n\t\tassert_eq!(q.video_bit_rate, 8_000_000);\n\t\tassert_eq!(q.video_min_bit_rate, 4_000_000);\n\t\tassert_eq!(q.video_max_bit_rate, 16_000_000);\n\t\tassert_eq!(q.video_buffer_size, 4_000_000);\n\t\tassert_eq!(q.video_threads, 4);\n\t\tassert_eq!(q.video_pix_fmt[..pix.len()], pix[..]);\n\t\tassert_eq!(q.video_is_image_sequence, 1);\n\t\tassert_eq!(q.video_scaling_method, VideoScalingMethod::Crop);\n\n\t\tassert_eq!(q.audio_enabled, 1);\n\t\tassert_eq!(q.audio_codec, 1);\n\t\tassert_eq!(q.audio_sample_rate, 48000);\n\t\tassert_eq!(q.audio_channel_layout, 3);\n\t\tassert_eq!(q.audio_sample_format, SampleFormat::F32Planar);\n\t\tassert_eq!(q.audio_bit_rate, 320_000);\n\n\t\tassert_eq!(q.subtitles_enabled, 1);\n\t\tassert_eq!(q.subtitles_codec, 1);\n\t\tassert_eq!(q.subtitles_are_sidecar, 1);\n\t\tassert_eq!(q.subtitles_sidecar_format, 13);\n\t}\n\n\t#[test]\n\tfn load_malformed_returns_err() {\n\t\tlet mut p = EncodingParams::default();\n\t\tassert!(p.load(\"not xml at all\").is_err());\n\t\tassert!(p.load(\"\u003cunclosed\").is_err());\n\t\tassert!(p.load(\"\u003cexport\u003e\u003cvideo enabled=\\\"1\\\"\u003e\u003c/export\u003e\").is_err());\n\t}\n\n\t#[test]\n\tfn load_non_export_returns_err() {\n\t\tlet mut p = EncodingParams::default();\n\t\tassert!(p.load(\"\u003cfoo/\u003e\").is_err());\n\t}\n\n\t#[test]\n\tfn custom_range_not_assigned_on_load() {\n\t\t// The C++ load_v1 bug: custom range is parsed but never assigned.\n\t\tlet mut p = EncodingParams::default();\n\t\tp.has_custom_range = 1;\n\t\tp.custom_range_in_num = 5;\n\t\tp.custom_range_in_den = 1;\n\t\tp.custom_range_out_num = 10;\n\t\tp.custom_range_out_den = 1;\n\t\tlet xml = p.save_to_string();\n\n\t\tlet mut q = EncodingParams::default();\n\t\tq.load(\u0026xml).expect(\"load\");\n\t\t// Fields stay at their defaults despite being serialized.\n\t\tassert_eq!(q.has_custom_range, 0);\n\t\tassert_eq!(q.custom_range_in_num, 0);\n\t\tassert_eq!(q.custom_range_in_den, 0);\n\t\tassert_eq!(q.custom_range_out_num, 0);\n\t\tassert_eq!(q.custom_range_out_den, 0);\n\t}\n\n\t#[test]\n\tfn cstr_set_cstr_roundtrip_and_truncate() {\n\t\tlet mut arr = [0u8; 8];\n\t\tset_cstr(\u0026mut arr, \"hello\");\n\t\tassert_eq!(cstr(\u0026arr), \"hello\");\n\t\tassert_eq!(arr[5], 0);\n\n\t\t// Longer than the buffer -\u003e truncated to the first 8 bytes (no\n\t\t// terminator required when the buffer is exactly full).\n\t\tset_cstr(\u0026mut arr, \"this string is far too long\");\n\t\tassert_eq!(cstr(\u0026arr), \"this str\");\n\t\tassert_eq!(arr.len(), 8);\n\t\tassert_eq!(\u0026arr[..], b\"this str\");\n\n\t\t// Empty -\u003e all NUL.\n\t\tset_cstr(\u0026mut arr, \"\");\n\t\tassert_eq!(arr, [0u8; 8]);\n\t\tassert_eq!(cstr(\u0026arr), \"\");\n\t}\n\n\t#[test]\n\tfn xml_escaping_roundtrips() {\n\t\tlet s = \"a\u0026b\u003cc\u003ed\";\n\t\tassert_eq!(unescape_xml(\u0026escape_xml(s)), s);\n\t\tassert_eq!(escape_xml(\"plain\"), \"plain\");\n\t}\n\n\tfn identity() -\u003e [f64; 16] {\n\t\t[\n\t\t\t1.0, 0.0, 0.0, 0.0, //\n\t\t\t0.0, 1.0, 0.0, 0.0, //\n\t\t\t0.0, 0.0, 1.0, 0.0, //\n\t\t\t0.0, 0.0, 0.0, 1.0,\n\t\t]\n\t}\n\n\t/// `oakcodec_encoding_params` byte-level layout lock, verified against\n\t/// the real header with a C++ `offsetof` probe (see the crate notes):\n\t/// every field offset and the total size must match `include/codec/\n\t/// encoder.h` exactly so a C caller's POD is read in place.\n\t#[test]\n\tfn encoding_params_c_abi_layout() {\n\t\tuse std::mem::{offset_of, size_of};\n\n\t\tassert_eq!(size_of::\u003cEncodingParams\u003e(), 1536);\n\t\tassert_eq!(offset_of!(EncodingParams, filename), 0);\n\t\tassert_eq!(offset_of!(EncodingParams, format), 1024);\n\t\tassert_eq!(offset_of!(EncodingParams, video_enabled), 1028);\n\t\tassert_eq!(offset_of!(EncodingParams, video_pixel_aspect_den), 1064);\n\t\tassert_eq!(offset_of!(EncodingParams, video_bit_rate), 1072);\n\t\tassert_eq!(offset_of!(EncodingParams, video_pix_fmt), 1108);\n\t\tassert_eq!(offset_of!(EncodingParams, video_is_image_sequence), 1172);\n\t\tassert_eq!(offset_of!(EncodingParams, audio_channel_layout), 1192);\n\t\tassert_eq!(offset_of!(EncodingParams, audio_bit_rate), 1208);\n\t\tassert_eq!(offset_of!(EncodingParams, color_transform_output), 1232);\n\t\tassert_eq!(offset_of!(EncodingParams, has_custom_range), 1496);\n\t\tassert_eq!(offset_of!(EncodingParams, custom_range_in_num), 1504);\n\t\tassert_eq!(offset_of!(EncodingParams, custom_range_out_den), 1528);\n\t}\n}\n","traces":[{"line":127,"address":[],"length":0,"stats":{"Line":24}},{"line":129,"address":[],"length":0,"stats":{"Line":48}},{"line":130,"address":[],"length":0,"stats":{"Line":24}},{"line":133,"address":[],"length":0,"stats":{"Line":24}},{"line":147,"address":[],"length":0,"stats":{"Line":48}},{"line":152,"address":[],"length":0,"stats":{"Line":24}},{"line":159,"address":[],"length":0,"stats":{"Line":24}},{"line":161,"address":[],"length":0,"stats":{"Line":24}},{"line":163,"address":[],"length":0,"stats":{"Line":24}},{"line":183,"address":[],"length":0,"stats":{"Line":9}},{"line":192,"address":[],"length":0,"stats":{"Line":9}},{"line":193,"address":[],"length":0,"stats":{"Line":9}},{"line":194,"address":[],"length":0,"stats":{"Line":9}},{"line":195,"address":[],"length":0,"stats":{"Line":9}},{"line":196,"address":[],"length":0,"stats":{"Line":9}},{"line":199,"address":[],"length":0,"stats":{"Line":9}},{"line":200,"address":[],"length":0,"stats":{"Line":2}},{"line":205,"address":[],"length":0,"stats":{"Line":22}},{"line":206,"address":[],"length":0,"stats":{"Line":2}},{"line":209,"address":[],"length":0,"stats":{"Line":10}},{"line":210,"address":[],"length":0,"stats":{"Line":10}},{"line":213,"address":[],"length":0,"stats":{"Line":10}},{"line":214,"address":[],"length":0,"stats":{"Line":20}},{"line":216,"address":[],"length":0,"stats":{"Line":1}},{"line":219,"address":[],"length":0,"stats":{"Line":11}},{"line":221,"address":[],"length":0,"stats":{"Line":6}},{"line":224,"address":[],"length":0,"stats":{"Line":2}},{"line":234,"address":[],"length":0,"stats":{"Line":17}},{"line":235,"address":[],"length":0,"stats":{"Line":17}},{"line":236,"address":[],"length":0,"stats":{"Line":1}},{"line":237,"address":[],"length":0,"stats":{"Line":1}},{"line":238,"address":[],"length":0,"stats":{"Line":1}},{"line":239,"address":[],"length":0,"stats":{"Line":1}},{"line":240,"address":[],"length":0,"stats":{"Line":1}},{"line":241,"address":[],"length":0,"stats":{"Line":1}},{"line":242,"address":[],"length":0,"stats":{"Line":1}},{"line":243,"address":[],"length":0,"stats":{"Line":1}},{"line":244,"address":[],"length":0,"stats":{"Line":1}},{"line":245,"address":[],"length":0,"stats":{"Line":1}},{"line":246,"address":[],"length":0,"stats":{"Line":1}},{"line":247,"address":[],"length":0,"stats":{"Line":1}},{"line":248,"address":[],"length":0,"stats":{"Line":1}},{"line":249,"address":[],"length":0,"stats":{"Line":1}},{"line":250,"address":[],"length":0,"stats":{"Line":1}},{"line":251,"address":[],"length":0,"stats":{"Line":2}},{"line":262,"address":[],"length":0,"stats":{"Line":6}},{"line":263,"address":[],"length":0,"stats":{"Line":15}},{"line":264,"address":[],"length":0,"stats":{"Line":12}},{"line":265,"address":[],"length":0,"stats":{"Line":3}},{"line":266,"address":[],"length":0,"stats":{"Line":1}},{"line":267,"address":[],"length":0,"stats":{"Line":1}},{"line":274,"address":[],"length":0,"stats":{"Line":8}},{"line":275,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":8}},{"line":278,"address":[],"length":0,"stats":{"Line":2}},{"line":280,"address":[],"length":0,"stats":{"Line":8}},{"line":281,"address":[],"length":0,"stats":{"Line":2}},{"line":284,"address":[],"length":0,"stats":{"Line":8}},{"line":285,"address":[],"length":0,"stats":{"Line":6}},{"line":287,"address":[],"length":0,"stats":{"Line":8}},{"line":288,"address":[],"length":0,"stats":{"Line":4}},{"line":291,"address":[],"length":0,"stats":{"Line":6}},{"line":292,"address":[],"length":0,"stats":{"Line":8}},{"line":293,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":6}},{"line":296,"address":[],"length":0,"stats":{"Line":2}},{"line":298,"address":[],"length":0,"stats":{"Line":6}},{"line":299,"address":[],"length":0,"stats":{"Line":2}},{"line":301,"address":[],"length":0,"stats":{"Line":6}},{"line":302,"address":[],"length":0,"stats":{"Line":2}},{"line":304,"address":[],"length":0,"stats":{"Line":5}},{"line":305,"address":[],"length":0,"stats":{"Line":2}},{"line":306,"address":[],"length":0,"stats":{"Line":0}},{"line":307,"address":[],"length":0,"stats":{"Line":1}},{"line":308,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":0}},{"line":310,"address":[],"length":0,"stats":{"Line":0}},{"line":311,"address":[],"length":0,"stats":{"Line":0}},{"line":312,"address":[],"length":0,"stats":{"Line":0}},{"line":315,"address":[],"length":0,"stats":{"Line":5}},{"line":316,"address":[],"length":0,"stats":{"Line":3}},{"line":317,"address":[],"length":0,"stats":{"Line":4}},{"line":318,"address":[],"length":0,"stats":{"Line":2}},{"line":319,"address":[],"length":0,"stats":{"Line":1}},{"line":322,"address":[],"length":0,"stats":{"Line":6}},{"line":323,"address":[],"length":0,"stats":{"Line":2}},{"line":325,"address":[],"length":0,"stats":{"Line":5}},{"line":326,"address":[],"length":0,"stats":{"Line":3}},{"line":327,"address":[],"length":0,"stats":{"Line":4}},{"line":328,"address":[],"length":0,"stats":{"Line":2}},{"line":329,"address":[],"length":0,"stats":{"Line":1}},{"line":332,"address":[],"length":0,"stats":{"Line":6}},{"line":333,"address":[],"length":0,"stats":{"Line":2}},{"line":335,"address":[],"length":0,"stats":{"Line":6}},{"line":336,"address":[],"length":0,"stats":{"Line":2}},{"line":338,"address":[],"length":0,"stats":{"Line":6}},{"line":339,"address":[],"length":0,"stats":{"Line":2}},{"line":341,"address":[],"length":0,"stats":{"Line":6}},{"line":342,"address":[],"length":0,"stats":{"Line":2}},{"line":344,"address":[],"length":0,"stats":{"Line":6}},{"line":345,"address":[],"length":0,"stats":{"Line":2}},{"line":347,"address":[],"length":0,"stats":{"Line":6}},{"line":348,"address":[],"length":0,"stats":{"Line":3}},{"line":350,"address":[],"length":0,"stats":{"Line":6}},{"line":351,"address":[],"length":0,"stats":{"Line":2}},{"line":353,"address":[],"length":0,"stats":{"Line":6}},{"line":354,"address":[],"length":0,"stats":{"Line":3}},{"line":358,"address":[],"length":0,"stats":{"Line":6}},{"line":359,"address":[],"length":0,"stats":{"Line":8}},{"line":360,"address":[],"length":0,"stats":{"Line":2}},{"line":362,"address":[],"length":0,"stats":{"Line":6}},{"line":363,"address":[],"length":0,"stats":{"Line":2}},{"line":365,"address":[],"length":0,"stats":{"Line":6}},{"line":366,"address":[],"length":0,"stats":{"Line":2}},{"line":368,"address":[],"length":0,"stats":{"Line":6}},{"line":369,"address":[],"length":0,"stats":{"Line":2}},{"line":371,"address":[],"length":0,"stats":{"Line":6}},{"line":372,"address":[],"length":0,"stats":{"Line":3}},{"line":374,"address":[],"length":0,"stats":{"Line":6}},{"line":375,"address":[],"length":0,"stats":{"Line":2}},{"line":379,"address":[],"length":0,"stats":{"Line":6}},{"line":380,"address":[],"length":0,"stats":{"Line":8}},{"line":381,"address":[],"length":0,"stats":{"Line":2}},{"line":383,"address":[],"length":0,"stats":{"Line":6}},{"line":384,"address":[],"length":0,"stats":{"Line":2}},{"line":386,"address":[],"length":0,"stats":{"Line":6}},{"line":387,"address":[],"length":0,"stats":{"Line":2}},{"line":389,"address":[],"length":0,"stats":{"Line":6}},{"line":390,"address":[],"length":0,"stats":{"Line":2}},{"line":394,"address":[],"length":0,"stats":{"Line":2}},{"line":404,"address":[],"length":0,"stats":{"Line":2}},{"line":405,"address":[],"length":0,"stats":{"Line":4}},{"line":406,"address":[],"length":0,"stats":{"Line":6}},{"line":407,"address":[],"length":0,"stats":{"Line":10}},{"line":408,"address":[],"length":0,"stats":{"Line":6}},{"line":409,"address":[],"length":0,"stats":{"Line":6}},{"line":410,"address":[],"length":0,"stats":{"Line":6}},{"line":411,"address":[],"length":0,"stats":{"Line":2}},{"line":412,"address":[],"length":0,"stats":{"Line":2}},{"line":414,"address":[],"length":0,"stats":{"Line":6}},{"line":415,"address":[],"length":0,"stats":{"Line":2}},{"line":416,"address":[],"length":0,"stats":{"Line":2}},{"line":419,"address":[],"length":0,"stats":{"Line":6}},{"line":420,"address":[],"length":0,"stats":{"Line":2}},{"line":421,"address":[],"length":0,"stats":{"Line":2}},{"line":423,"address":[],"length":0,"stats":{"Line":3}},{"line":424,"address":[],"length":0,"stats":{"Line":4}},{"line":425,"address":[],"length":0,"stats":{"Line":4}},{"line":426,"address":[],"length":0,"stats":{"Line":4}},{"line":427,"address":[],"length":0,"stats":{"Line":4}},{"line":428,"address":[],"length":0,"stats":{"Line":2}},{"line":429,"address":[],"length":0,"stats":{"Line":2}},{"line":431,"address":[],"length":0,"stats":{"Line":4}},{"line":432,"address":[],"length":0,"stats":{"Line":2}},{"line":433,"address":[],"length":0,"stats":{"Line":2}},{"line":435,"address":[],"length":0,"stats":{"Line":4}},{"line":436,"address":[],"length":0,"stats":{"Line":2}},{"line":437,"address":[],"length":0,"stats":{"Line":2}},{"line":439,"address":[],"length":0,"stats":{"Line":4}},{"line":440,"address":[],"length":0,"stats":{"Line":2}},{"line":441,"address":[],"length":0,"stats":{"Line":2}},{"line":443,"address":[],"length":0,"stats":{"Line":4}},{"line":444,"address":[],"length":0,"stats":{"Line":4}},{"line":445,"address":[],"length":0,"stats":{"Line":2}},{"line":446,"address":[],"length":0,"stats":{"Line":2}},{"line":448,"address":[],"length":0,"stats":{"Line":4}},{"line":449,"address":[],"length":0,"stats":{"Line":2}},{"line":450,"address":[],"length":0,"stats":{"Line":2}},{"line":452,"address":[],"length":0,"stats":{"Line":4}},{"line":453,"address":[],"length":0,"stats":{"Line":2}},{"line":454,"address":[],"length":0,"stats":{"Line":2}},{"line":456,"address":[],"length":0,"stats":{"Line":4}},{"line":457,"address":[],"length":0,"stats":{"Line":6}},{"line":458,"address":[],"length":0,"stats":{"Line":4}},{"line":459,"address":[],"length":0,"stats":{"Line":2}},{"line":460,"address":[],"length":0,"stats":{"Line":2}},{"line":462,"address":[],"length":0,"stats":{"Line":3}},{"line":463,"address":[],"length":0,"stats":{"Line":1}},{"line":464,"address":[],"length":0,"stats":{"Line":1}},{"line":467,"address":[],"length":0,"stats":{"Line":6}},{"line":469,"address":[],"length":0,"stats":{"Line":6}},{"line":470,"address":[],"length":0,"stats":{"Line":2}},{"line":471,"address":[],"length":0,"stats":{"Line":2}},{"line":473,"address":[],"length":0,"stats":{"Line":3}},{"line":474,"address":[],"length":0,"stats":{"Line":4}},{"line":475,"address":[],"length":0,"stats":{"Line":4}},{"line":476,"address":[],"length":0,"stats":{"Line":2}},{"line":477,"address":[],"length":0,"stats":{"Line":2}},{"line":479,"address":[],"length":0,"stats":{"Line":4}},{"line":480,"address":[],"length":0,"stats":{"Line":2}},{"line":481,"address":[],"length":0,"stats":{"Line":2}},{"line":483,"address":[],"length":0,"stats":{"Line":4}},{"line":484,"address":[],"length":0,"stats":{"Line":2}},{"line":485,"address":[],"length":0,"stats":{"Line":2}},{"line":487,"address":[],"length":0,"stats":{"Line":2}},{"line":489,"address":[],"length":0,"stats":{"Line":6}},{"line":491,"address":[],"length":0,"stats":{"Line":6}},{"line":492,"address":[],"length":0,"stats":{"Line":2}},{"line":493,"address":[],"length":0,"stats":{"Line":2}},{"line":495,"address":[],"length":0,"stats":{"Line":3}},{"line":496,"address":[],"length":0,"stats":{"Line":4}},{"line":497,"address":[],"length":0,"stats":{"Line":2}},{"line":498,"address":[],"length":0,"stats":{"Line":2}},{"line":500,"address":[],"length":0,"stats":{"Line":4}},{"line":501,"address":[],"length":0,"stats":{"Line":2}},{"line":502,"address":[],"length":0,"stats":{"Line":2}},{"line":504,"address":[],"length":0,"stats":{"Line":2}},{"line":505,"address":[],"length":0,"stats":{"Line":1}},{"line":506,"address":[],"length":0,"stats":{"Line":1}},{"line":509,"address":[],"length":0,"stats":{"Line":6}},{"line":511,"address":[],"length":0,"stats":{"Line":6}},{"line":512,"address":[],"length":0,"stats":{"Line":2}},{"line":548,"address":[],"length":0,"stats":{"Line":1167}},{"line":549,"address":[],"length":0,"stats":{"Line":2334}},{"line":552,"address":[],"length":0,"stats":{"Line":340}},{"line":553,"address":[],"length":0,"stats":{"Line":1056}},{"line":554,"address":[],"length":0,"stats":{"Line":9}},{"line":558,"address":[],"length":0,"stats":{"Line":543}},{"line":559,"address":[],"length":0,"stats":{"Line":1629}},{"line":562,"address":[],"length":0,"stats":{"Line":97}},{"line":563,"address":[],"length":0,"stats":{"Line":194}},{"line":564,"address":[],"length":0,"stats":{"Line":1634}},{"line":565,"address":[],"length":0,"stats":{"Line":2543}},{"line":566,"address":[],"length":0,"stats":{"Line":721}},{"line":568,"address":[],"length":0,"stats":{"Line":95}},{"line":571,"address":[],"length":0,"stats":{"Line":97}},{"line":572,"address":[],"length":0,"stats":{"Line":1}},{"line":574,"address":[],"length":0,"stats":{"Line":192}},{"line":577,"address":[],"length":0,"stats":{"Line":9}},{"line":578,"address":[],"length":0,"stats":{"Line":18}},{"line":579,"address":[],"length":0,"stats":{"Line":18}},{"line":580,"address":[],"length":0,"stats":{"Line":0}},{"line":582,"address":[],"length":0,"stats":{"Line":9}},{"line":583,"address":[],"length":0,"stats":{"Line":18}},{"line":584,"address":[],"length":0,"stats":{"Line":81}},{"line":585,"address":[],"length":0,"stats":{"Line":9}},{"line":587,"address":[],"length":0,"stats":{"Line":18}},{"line":588,"address":[],"length":0,"stats":{"Line":0}},{"line":590,"address":[],"length":0,"stats":{"Line":36}},{"line":591,"address":[],"length":0,"stats":{"Line":9}},{"line":592,"address":[],"length":0,"stats":{"Line":9}},{"line":595,"address":[],"length":0,"stats":{"Line":42}},{"line":596,"address":[],"length":0,"stats":{"Line":84}},{"line":597,"address":[],"length":0,"stats":{"Line":84}},{"line":598,"address":[],"length":0,"stats":{"Line":0}},{"line":600,"address":[],"length":0,"stats":{"Line":42}},{"line":601,"address":[],"length":0,"stats":{"Line":126}},{"line":602,"address":[],"length":0,"stats":{"Line":84}},{"line":603,"address":[],"length":0,"stats":{"Line":84}},{"line":604,"address":[],"length":0,"stats":{"Line":0}},{"line":606,"address":[],"length":0,"stats":{"Line":42}},{"line":607,"address":[],"length":0,"stats":{"Line":42}},{"line":608,"address":[],"length":0,"stats":{"Line":1}},{"line":610,"address":[],"length":0,"stats":{"Line":41}},{"line":613,"address":[],"length":0,"stats":{"Line":46}},{"line":614,"address":[],"length":0,"stats":{"Line":92}},{"line":615,"address":[],"length":0,"stats":{"Line":92}},{"line":616,"address":[],"length":0,"stats":{"Line":1}},{"line":618,"address":[],"length":0,"stats":{"Line":45}},{"line":619,"address":[],"length":0,"stats":{"Line":135}},{"line":620,"address":[],"length":0,"stats":{"Line":90}},{"line":621,"address":[],"length":0,"stats":{"Line":90}},{"line":623,"address":[],"length":0,"stats":{"Line":108}},{"line":624,"address":[],"length":0,"stats":{"Line":162}},{"line":625,"address":[],"length":0,"stats":{"Line":1}},{"line":626,"address":[],"length":0,"stats":{"Line":3}},{"line":628,"address":[],"length":0,"stats":{"Line":159}},{"line":629,"address":[],"length":0,"stats":{"Line":43}},{"line":630,"address":[],"length":0,"stats":{"Line":43}},{"line":632,"address":[],"length":0,"stats":{"Line":30}},{"line":633,"address":[],"length":0,"stats":{"Line":18}},{"line":634,"address":[],"length":0,"stats":{"Line":36}},{"line":635,"address":[],"length":0,"stats":{"Line":9}},{"line":637,"address":[],"length":0,"stats":{"Line":27}},{"line":638,"address":[],"length":0,"stats":{"Line":27}},{"line":641,"address":[],"length":0,"stats":{"Line":86}},{"line":642,"address":[],"length":0,"stats":{"Line":215}},{"line":643,"address":[],"length":0,"stats":{"Line":22}},{"line":645,"address":[],"length":0,"stats":{"Line":100}},{"line":646,"address":[],"length":0,"stats":{"Line":100}},{"line":647,"address":[],"length":0,"stats":{"Line":0}},{"line":649,"address":[],"length":0,"stats":{"Line":150}},{"line":650,"address":[],"length":0,"stats":{"Line":10}},{"line":652,"address":[],"length":0,"stats":{"Line":120}},{"line":653,"address":[],"length":0,"stats":{"Line":160}},{"line":655,"address":[],"length":0,"stats":{"Line":0}},{"line":658,"address":[],"length":0,"stats":{"Line":31}},{"line":659,"address":[],"length":0,"stats":{"Line":27}},{"line":661,"address":[],"length":0,"stats":{"Line":64}},{"line":662,"address":[],"length":0,"stats":{"Line":653}},{"line":663,"address":[],"length":0,"stats":{"Line":105}},{"line":665,"address":[],"length":0,"stats":{"Line":128}},{"line":666,"address":[],"length":0,"stats":{"Line":96}},{"line":667,"address":[],"length":0,"stats":{"Line":96}},{"line":673,"address":[],"length":0,"stats":{"Line":6}},{"line":674,"address":[],"length":0,"stats":{"Line":12}},{"line":675,"address":[],"length":0,"stats":{"Line":12}},{"line":678,"address":[],"length":0,"stats":{"Line":62}},{"line":679,"address":[],"length":0,"stats":{"Line":550}},{"line":682,"address":[],"length":0,"stats":{"Line":33}},{"line":683,"address":[],"length":0,"stats":{"Line":132}},{"line":686,"address":[],"length":0,"stats":{"Line":6}},{"line":687,"address":[],"length":0,"stats":{"Line":42}},{"line":690,"address":[],"length":0,"stats":{"Line":28}},{"line":691,"address":[],"length":0,"stats":{"Line":84}},{"line":694,"address":[],"length":0,"stats":{"Line":6}},{"line":695,"address":[],"length":0,"stats":{"Line":18}},{"line":698,"address":[],"length":0,"stats":{"Line":10}},{"line":699,"address":[],"length":0,"stats":{"Line":10}},{"line":705,"address":[],"length":0,"stats":{"Line":10}},{"line":706,"address":[],"length":0,"stats":{"Line":10}},{"line":707,"address":[],"length":0,"stats":{"Line":8}},{"line":708,"address":[],"length":0,"stats":{"Line":1}},{"line":709,"address":[],"length":0,"stats":{"Line":1}},{"line":710,"address":[],"length":0,"stats":{"Line":0}},{"line":717,"address":[],"length":0,"stats":{"Line":9}},{"line":718,"address":[],"length":0,"stats":{"Line":9}},{"line":719,"address":[],"length":0,"stats":{"Line":0}},{"line":720,"address":[],"length":0,"stats":{"Line":1}},{"line":721,"address":[],"length":0,"stats":{"Line":0}},{"line":722,"address":[],"length":0,"stats":{"Line":0}},{"line":723,"address":[],"length":0,"stats":{"Line":0}},{"line":724,"address":[],"length":0,"stats":{"Line":2}},{"line":725,"address":[],"length":0,"stats":{"Line":0}},{"line":726,"address":[],"length":0,"stats":{"Line":0}},{"line":727,"address":[],"length":0,"stats":{"Line":0}},{"line":728,"address":[],"length":0,"stats":{"Line":0}},{"line":729,"address":[],"length":0,"stats":{"Line":0}},{"line":730,"address":[],"length":0,"stats":{"Line":6}},{"line":731,"address":[],"length":0,"stats":{"Line":0}},{"line":732,"address":[],"length":0,"stats":{"Line":0}},{"line":737,"address":[],"length":0,"stats":{"Line":6}},{"line":738,"address":[],"length":0,"stats":{"Line":74}},{"line":739,"address":[],"length":0,"stats":{"Line":18}},{"line":743,"address":[],"length":0,"stats":{"Line":6}},{"line":744,"address":[],"length":0,"stats":{"Line":12}},{"line":745,"address":[],"length":0,"stats":{"Line":36}},{"line":746,"address":[],"length":0,"stats":{"Line":18}},{"line":750,"address":[],"length":0,"stats":{"Line":5}},{"line":751,"address":[],"length":0,"stats":{"Line":20}},{"line":757,"address":[],"length":0,"stats":{"Line":34}},{"line":758,"address":[],"length":0,"stats":{"Line":136}}],"covered":318,"coverable":342},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","error.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Error codes, mirroring `include/codec/error.h` verbatim; project-wide\n//! -MMCCCC scheme (module registry in include/common/error.h), pass-through untranslated.\n\n/// Success.\npub const OAKCODEC_OK: i32 = 0;\n/// Null handle or invalid argument.\npub const OAKCODEC_E_INVALID: i32 = -50001;\n/// Call not valid in the current state.\npub const OAKCODEC_E_STATE: i32 = -50002;\n/// The underlying operation failed.\npub const OAKCODEC_E_FAILED: i32 = -50003;\n/// Index out of range / entry not found.\npub const OAKCODEC_E_NOT_FOUND: i32 = -50004;\n/// Allocation failed.\npub const OAKCODEC_E_NOMEM: i32 = -50005;\n/// The operation was cancelled.\npub const OAKCODEC_E_CANCELLED: i32 = -50006;\n\n/// Current ABI version stamped into every oakcodec handle.\npub const OAKCODEC_ABI_VERSION: u32 = 1;\n\n/// Crate-internal result type; the FFI layer maps it to the codes.\npub type Result\u003cT\u003e = std::result::Result\u003cT, Error\u003e;\n\n/// Crate-internal error.\n#[derive(Debug)]\npub enum Error {\n\t/// Null handle or invalid argument.\n\tInvalid,\n\t/// Wrong state.\n\tState,\n\t/// Operation failed (context string is log-only).\n\tFailed(String),\n\t/// Not found.\n\tNotFound,\n\t/// Out of memory.\n\tNoMem,\n\t/// The operation was cancelled.\n\tCancelled,\n}\n\nimpl Error {\n\t/// Map to the public error code.\n\tpub fn code(\u0026self) -\u003e i32 {\n\t\tmatch self {\n\t\t\tError::Invalid =\u003e OAKCODEC_E_INVALID,\n\t\t\tError::State =\u003e OAKCODEC_E_STATE,\n\t\t\tError::Failed(_) =\u003e OAKCODEC_E_FAILED,\n\t\t\tError::NotFound =\u003e OAKCODEC_E_NOT_FOUND,\n\t\t\tError::NoMem =\u003e OAKCODEC_E_NOMEM,\n\t\t\tError::Cancelled =\u003e OAKCODEC_E_CANCELLED,\n\t\t}\n\t}\n}\n","traces":[{"line":60,"address":[],"length":0,"stats":{"Line":36}},{"line":61,"address":[],"length":0,"stats":{"Line":36}},{"line":62,"address":[],"length":0,"stats":{"Line":21}},{"line":63,"address":[],"length":0,"stats":{"Line":10}},{"line":64,"address":[],"length":0,"stats":{"Line":2}},{"line":65,"address":[],"length":0,"stats":{"Line":2}},{"line":66,"address":[],"length":0,"stats":{"Line":0}},{"line":67,"address":[],"length":0,"stats":{"Line":1}}],"covered":7,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","exportcodec.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ExportCodec` — the export codec enum and name table.\n//!\n//! Mirrors `src/codec/src/exportcodec.h`. The enum is the raw int contract\n//! the C ABI documents (oakengine/encoding.h); values must NOT be\n//! reordered (they are used in serialized files).\n\n/// `olive::ExportCodec::Codec`. Only append (never insert/reorder); the\n/// integer values are part of the file format.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Codec {\n\t/// Apple ProRes.\n\tDNxHD = 0,\n\t/// H.264.\n\tH264 = 1,\n\t/// H.264 with RGB color.\n\tH264RGB = 2,\n\t/// H.265/HEVC.\n\tH265 = 3,\n\t/// OpenEXR.\n\tOpenEXR = 4,\n\t/// PNG.\n\tPNG = 5,\n\t/// Apple ProRes.\n\tProRes = 6,\n\t/// CineForm.\n\tCineForm = 7,\n\t/// TIFF.\n\tTIFF = 8,\n\t/// VP9.\n\tVP9 = 9,\n\t/// MPEG-2.\n\tMP2 = 10,\n\t/// MPEG-3.\n\tMP3 = 11,\n\t/// AAC.\n\tAAC = 12,\n\t/// Uncompressed PCM.\n\tPCM = 13,\n\t/// Opus.\n\tOpus = 14,\n\t/// Vorbis.\n\tVorbis = 15,\n\t/// FLAC.\n\tFLAC = 16,\n\t/// SubRip subtitles.\n\tSRT = 17,\n\t/// AV1.\n\tAV1 = 18,\n\t/// Sentinel count (not a real codec).\n\tCount = 19,\n}\n\nimpl Codec {\n\t/// Human-readable name for a codec.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `get_codec_name`; unknown /\n\t/// `Count` returns \"Unknown\".\n\tpub fn get_codec_name(c: Codec) -\u003e String {\n\t\tmatch c {\n\t\t\tCodec::DNxHD =\u003e \"DNxHD\".to_string(),\n\t\t\tCodec::H264 =\u003e \"H.264\".to_string(),\n\t\t\tCodec::H264RGB =\u003e \"H.264 RGB\".to_string(),\n\t\t\tCodec::H265 =\u003e \"H.265\".to_string(),\n\t\t\tCodec::OpenEXR =\u003e \"OpenEXR\".to_string(),\n\t\t\tCodec::PNG =\u003e \"PNG\".to_string(),\n\t\t\tCodec::ProRes =\u003e \"ProRes\".to_string(),\n\t\t\tCodec::CineForm =\u003e \"Cineform\".to_string(),\n\t\t\tCodec::TIFF =\u003e \"TIFF\".to_string(),\n\t\t\tCodec::VP9 =\u003e \"VP9\".to_string(),\n\t\t\tCodec::MP2 =\u003e \"MP2\".to_string(),\n\t\t\tCodec::MP3 =\u003e \"MP3\".to_string(),\n\t\t\tCodec::AAC =\u003e \"AAC\".to_string(),\n\t\t\tCodec::PCM =\u003e \"PCM (Uncompressed)\".to_string(),\n\t\t\tCodec::Opus =\u003e \"Opus\".to_string(),\n\t\t\tCodec::Vorbis =\u003e \"Vorbis\".to_string(),\n\t\t\tCodec::FLAC =\u003e \"FLAC\".to_string(),\n\t\t\tCodec::SRT =\u003e \"SubRip SRT\".to_string(),\n\t\t\tCodec::AV1 =\u003e \"AV1\".to_string(),\n\t\t\tCodec::Count =\u003e \"Unknown\".to_string(),\n\t\t}\n\t}\n\n\t/// Whether the codec produces a still image.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `is_codec_a_still_image`;\n\t/// OpenEXR / PNG / TIFF are still images, everything else is not.\n\tpub fn is_codec_a_still_image(c: Codec) -\u003e bool {\n\t\tmatch c {\n\t\t\tCodec::OpenEXR | Codec::PNG | Codec::TIFF =\u003e true,\n\t\t\t_ =\u003e false,\n\t\t}\n\t}\n\n\t/// Whether the codec is lossless.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `is_codec_lossless`;\n\t/// only PCM and FLAC are lossless.\n\tpub fn is_codec_lossless(c: Codec) -\u003e bool {\n\t\tmatch c {\n\t\t\tCodec::PCM | Codec::FLAC =\u003e true,\n\t\t\t_ =\u003e false,\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn codec_name_mapping() {\n\t\tassert_eq!(Codec::get_codec_name(Codec::DNxHD), \"DNxHD\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::H264), \"H.264\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::H264RGB), \"H.264 RGB\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::H265), \"H.265\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::OpenEXR), \"OpenEXR\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::PNG), \"PNG\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::ProRes), \"ProRes\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::CineForm), \"Cineform\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::TIFF), \"TIFF\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::VP9), \"VP9\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::MP2), \"MP2\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::MP3), \"MP3\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::AAC), \"AAC\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::PCM), \"PCM (Uncompressed)\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::Opus), \"Opus\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::Vorbis), \"Vorbis\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::FLAC), \"FLAC\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::SRT), \"SubRip SRT\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::AV1), \"AV1\");\n\t\tassert_eq!(Codec::get_codec_name(Codec::Count), \"Unknown\");\n\t}\n\n\t#[test]\n\tfn still_image_and_lossless_flags() {\n\t\tassert!(Codec::is_codec_a_still_image(Codec::OpenEXR));\n\t\tassert!(Codec::is_codec_a_still_image(Codec::PNG));\n\t\tassert!(Codec::is_codec_a_still_image(Codec::TIFF));\n\t\tassert!(!Codec::is_codec_a_still_image(Codec::H264));\n\t\tassert!(!Codec::is_codec_a_still_image(Codec::PCM));\n\n\t\tassert!(Codec::is_codec_lossless(Codec::PCM));\n\t\tassert!(Codec::is_codec_lossless(Codec::FLAC));\n\t\tassert!(!Codec::is_codec_lossless(Codec::MP3));\n\t\tassert!(!Codec::is_codec_lossless(Codec::H264));\n\t}\n}\n","traces":[{"line":75,"address":[],"length":0,"stats":{"Line":20}},{"line":76,"address":[],"length":0,"stats":{"Line":20}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":2}},{"line":80,"address":[],"length":0,"stats":{"Line":2}},{"line":81,"address":[],"length":0,"stats":{"Line":2}},{"line":82,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":2}},{"line":84,"address":[],"length":0,"stats":{"Line":2}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":89,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":2}},{"line":92,"address":[],"length":0,"stats":{"Line":2}},{"line":93,"address":[],"length":0,"stats":{"Line":2}},{"line":94,"address":[],"length":0,"stats":{"Line":2}},{"line":95,"address":[],"length":0,"stats":{"Line":2}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":104,"address":[],"length":0,"stats":{"Line":5}},{"line":105,"address":[],"length":0,"stats":{"Line":5}},{"line":106,"address":[],"length":0,"stats":{"Line":3}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":115,"address":[],"length":0,"stats":{"Line":4}},{"line":116,"address":[],"length":0,"stats":{"Line":4}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":2}}],"covered":30,"coverable":30},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","exportformat.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ExportFormat` — the export container-format enum and tables.\n//!\n//! Mirrors `src/codec/src/exportformat.h`. As with `ExportCodec`, the enum\n//! values are the serialized-file contract and must never be reordered.\n\nuse oakcore_rs::SampleFormat;\n\nuse crate::exportcodec::Codec;\n\n/// `olive::ExportFormat::Format`. Only append (never insert/reorder).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Format {\n\t/// QuickTime DNxHD.\n\tDNxHD = 0,\n\t/// Matroska.\n\tMatroska = 1,\n\t/// MPEG-4 video.\n\tMPEG4Video = 2,\n\t/// OpenEXR.\n\tOpenEXR = 3,\n\t/// QuickTime.\n\tQuickTime = 4,\n\t/// PNG.\n\tPNG = 5,\n\t/// TIFF.\n\tTIFF = 6,\n\t/// WAV.\n\tWAV = 7,\n\t/// AIFF.\n\tAIFF = 8,\n\t/// MP3.\n\tMP3 = 9,\n\t/// FLAC.\n\tFLAC = 10,\n\t/// Ogg.\n\tOgg = 11,\n\t/// WebM.\n\tWebM = 12,\n\t/// SubRip.\n\tSRT = 13,\n\t/// MPEG-4 audio.\n\tMPEG4Audio = 14,\n\t/// Sentinel count (not a real format).\n\tCount = 15,\n}\n\nimpl Format {\n\t/// Convert an `i32` code to a [`Format`]; `None` outside the valid\n\t/// `0..=14` range (incl. `Count`).\n\tpub fn from_i32(v: i32) -\u003e Option\u003cFormat\u003e {\n\t\tmatch v {\n\t\t\t0 =\u003e Some(Format::DNxHD),\n\t\t\t1 =\u003e Some(Format::Matroska),\n\t\t\t2 =\u003e Some(Format::MPEG4Video),\n\t\t\t3 =\u003e Some(Format::OpenEXR),\n\t\t\t4 =\u003e Some(Format::QuickTime),\n\t\t\t5 =\u003e Some(Format::PNG),\n\t\t\t6 =\u003e Some(Format::TIFF),\n\t\t\t7 =\u003e Some(Format::WAV),\n\t\t\t8 =\u003e Some(Format::AIFF),\n\t\t\t9 =\u003e Some(Format::MP3),\n\t\t\t10 =\u003e Some(Format::FLAC),\n\t\t\t11 =\u003e Some(Format::Ogg),\n\t\t\t12 =\u003e Some(Format::WebM),\n\t\t\t13 =\u003e Some(Format::SRT),\n\t\t\t14 =\u003e Some(Format::MPEG4Audio),\n\t\t\t_ =\u003e None,\n\t\t}\n\t}\n\n\t/// Human-readable name for a format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_name`; unknown /\n\t/// `Count` returns \"Unknown\".\n\tpub fn get_name(f: Format) -\u003e String {\n\t\tmatch f {\n\t\t\tFormat::DNxHD =\u003e \"DNxHD\".to_string(),\n\t\t\tFormat::Matroska =\u003e \"Matroska Video\".to_string(),\n\t\t\tFormat::MPEG4Video =\u003e \"MPEG-4 Video\".to_string(),\n\t\t\tFormat::MPEG4Audio =\u003e \"MPEG-4 Audio\".to_string(),\n\t\t\tFormat::OpenEXR =\u003e \"OpenEXR\".to_string(),\n\t\t\tFormat::PNG =\u003e \"PNG\".to_string(),\n\t\t\tFormat::TIFF =\u003e \"TIFF\".to_string(),\n\t\t\tFormat::QuickTime =\u003e \"QuickTime\".to_string(),\n\t\t\tFormat::WAV =\u003e \"Wave Audio\".to_string(),\n\t\t\tFormat::AIFF =\u003e \"AIFF\".to_string(),\n\t\t\tFormat::MP3 =\u003e \"MP3\".to_string(),\n\t\t\tFormat::FLAC =\u003e \"FLAC\".to_string(),\n\t\t\tFormat::Ogg =\u003e \"Ogg\".to_string(),\n\t\t\tFormat::WebM =\u003e \"WebM\".to_string(),\n\t\t\tFormat::SRT =\u003e \"SubRip SRT\".to_string(),\n\t\t\tFormat::Count =\u003e \"Unknown\".to_string(),\n\t\t}\n\t}\n\n\t/// File extension for a format (e.g. \"mp4\").\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_extension`;\n\t/// `Count` returns an empty string.\n\tpub fn get_extension(f: Format) -\u003e String {\n\t\tmatch f {\n\t\t\tFormat::DNxHD =\u003e \"mxf\".to_string(),\n\t\t\tFormat::Matroska =\u003e \"mkv\".to_string(),\n\t\t\tFormat::MPEG4Video =\u003e \"mp4\".to_string(),\n\t\t\tFormat::MPEG4Audio =\u003e \"m4a\".to_string(),\n\t\t\tFormat::OpenEXR =\u003e \"exr\".to_string(),\n\t\t\tFormat::PNG =\u003e \"png\".to_string(),\n\t\t\tFormat::TIFF =\u003e \"tiff\".to_string(),\n\t\t\tFormat::QuickTime =\u003e \"mov\".to_string(),\n\t\t\tFormat::WAV =\u003e \"wav\".to_string(),\n\t\t\tFormat::AIFF =\u003e \"aiff\".to_string(),\n\t\t\tFormat::MP3 =\u003e \"mp3\".to_string(),\n\t\t\tFormat::FLAC =\u003e \"flac\".to_string(),\n\t\t\tFormat::Ogg =\u003e \"ogg\".to_string(),\n\t\t\tFormat::WebM =\u003e \"webm\".to_string(),\n\t\t\tFormat::SRT =\u003e \"srt\".to_string(),\n\t\t\tFormat::Count =\u003e String::new(),\n\t\t}\n\t}\n\n\t/// Codecs that can carry video in this format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_video_codecs`.\n\tpub fn get_video_codecs(f: Format) -\u003e Vec\u003cCodec\u003e {\n\t\tmatch f {\n\t\t\tFormat::DNxHD =\u003e vec![Codec::DNxHD],\n\t\t\tFormat::Matroska =\u003e vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::VP9],\n\t\t\tFormat::MPEG4Video =\u003e vec![Codec::H264, Codec::H264RGB, Codec::H265],\n\t\t\tFormat::OpenEXR =\u003e vec![Codec::OpenEXR],\n\t\t\tFormat::PNG =\u003e vec![Codec::PNG],\n\t\t\tFormat::TIFF =\u003e vec![Codec::TIFF],\n\t\t\tFormat::QuickTime =\u003e vec![\n\t\t\t\tCodec::H264,\n\t\t\t\tCodec::H264RGB,\n\t\t\t\tCodec::H265,\n\t\t\t\tCodec::ProRes,\n\t\t\t\tCodec::CineForm,\n\t\t\t],\n\t\t\tFormat::WebM =\u003e vec![Codec::AV1, Codec::VP9],\n\t\t\t_ =\u003e Vec::new(),\n\t\t}\n\t}\n\n\t/// Codecs that can carry audio in this format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_audio_codecs`.\n\tpub fn get_audio_codecs(f: Format) -\u003e Vec\u003cCodec\u003e {\n\t\tmatch f {\n\t\t\t// Video/audio formats.\n\t\t\tFormat::DNxHD =\u003e vec![Codec::PCM],\n\t\t\tFormat::Matroska =\u003e vec![\n\t\t\t\tCodec::AAC,\n\t\t\t\tCodec::MP2,\n\t\t\t\tCodec::MP3,\n\t\t\t\tCodec::PCM,\n\t\t\t\tCodec::Vorbis,\n\t\t\t\tCodec::Opus,\n\t\t\t\tCodec::FLAC,\n\t\t\t],\n\t\t\tFormat::MPEG4Video | Format::MPEG4Audio =\u003e {\n\t\t\t\tvec![Codec::AAC, Codec::MP2, Codec::MP3]\n\t\t\t}\n\t\t\tFormat::QuickTime =\u003e vec![Codec::AAC, Codec::MP2, Codec::MP3, Codec::PCM],\n\t\t\tFormat::WebM =\u003e vec![\n\t\t\t\tCodec::Opus,\n\t\t\t\tCodec::AAC,\n\t\t\t\tCodec::MP2,\n\t\t\t\tCodec::MP3,\n\t\t\t\tCodec::PCM,\n\t\t\t\tCodec::Vorbis,\n\t\t\t],\n\t\t\t// Audio-only formats.\n\t\t\tFormat::WAV =\u003e vec![Codec::PCM],\n\t\t\tFormat::AIFF =\u003e vec![Codec::PCM],\n\t\t\tFormat::MP3 =\u003e vec![Codec::MP3],\n\t\t\tFormat::FLAC =\u003e vec![Codec::FLAC],\n\t\t\tFormat::Ogg =\u003e vec![Codec::Opus, Codec::Vorbis, Codec::PCM],\n\t\t\t_ =\u003e Vec::new(),\n\t\t}\n\t}\n\n\t/// Codecs that can carry subtitles in this format.\n\t///\n\t/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_subtitle_codecs`;\n\t/// only Matroska and SRT support the SRT subtitle codec.\n\tpub fn get_subtitle_codecs(f: Format) -\u003e Vec\u003cCodec\u003e {\n\t\tmatch f {\n\t\t\tFormat::Matroska | Format::SRT =\u003e vec![Codec::SRT],\n\t\t\t_ =\u003e Vec::new(),\n\t\t}\n\t}\n\n\t/// Pixel formats the given codec supports in this format.\n\t///\n\t/// The C++ side computes this by instantiating the format's encoder and\n\t/// asking the FFmpeg bridge (`fb_encoder_codec_get_pixel_formats`); the\n\t/// base `Encoder::get_pixel_formats_for_codec` returns an empty list.\n\t/// There is no pure-Rust table, so this returns an empty list (matching\n\t/// the C++ base default). CPP-PARITY: `exportformat.cpp:222`.\n\tpub fn get_pixel_formats_for_codec(_f: Format, _c: Codec) -\u003e Vec\u003ci32\u003e {\n\t\tVec::new()\n\t}\n\n\t/// Sample formats the given codec supports in this format.\n\t///\n\t/// CPP-PARITY: `ffmpegencoder.cpp:175`. PCM is pure (signed-16 first so\n\t/// the export dialog's default matches FFmpeg's); all other codecs query\n\t/// the FFmpeg bridge and return empty here (see\n\t/// `get_pixel_formats_for_codec`).\n\tpub fn get_sample_formats_for_codec(_f: Format, c: Codec) -\u003e Vec\u003cSampleFormat\u003e {\n\t\tif c == Codec::PCM {\n\t\t\tvec![\n\t\t\t\tSampleFormat::S16,\n\t\t\t\tSampleFormat::U8,\n\t\t\t\tSampleFormat::S32,\n\t\t\t\tSampleFormat::S64,\n\t\t\t\tSampleFormat::F32,\n\t\t\t\tSampleFormat::F64,\n\t\t\t]\n\t\t} else {\n\t\t\tVec::new()\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn format_name_mapping() {\n\t\tassert_eq!(Format::get_name(Format::DNxHD), \"DNxHD\");\n\t\tassert_eq!(Format::get_name(Format::Matroska), \"Matroska Video\");\n\t\tassert_eq!(Format::get_name(Format::MPEG4Video), \"MPEG-4 Video\");\n\t\tassert_eq!(Format::get_name(Format::MPEG4Audio), \"MPEG-4 Audio\");\n\t\tassert_eq!(Format::get_name(Format::OpenEXR), \"OpenEXR\");\n\t\tassert_eq!(Format::get_name(Format::PNG), \"PNG\");\n\t\tassert_eq!(Format::get_name(Format::TIFF), \"TIFF\");\n\t\tassert_eq!(Format::get_name(Format::QuickTime), \"QuickTime\");\n\t\tassert_eq!(Format::get_name(Format::WAV), \"Wave Audio\");\n\t\tassert_eq!(Format::get_name(Format::AIFF), \"AIFF\");\n\t\tassert_eq!(Format::get_name(Format::MP3), \"MP3\");\n\t\tassert_eq!(Format::get_name(Format::FLAC), \"FLAC\");\n\t\tassert_eq!(Format::get_name(Format::Ogg), \"Ogg\");\n\t\tassert_eq!(Format::get_name(Format::WebM), \"WebM\");\n\t\tassert_eq!(Format::get_name(Format::SRT), \"SubRip SRT\");\n\t\tassert_eq!(Format::get_name(Format::Count), \"Unknown\");\n\t}\n\n\t#[test]\n\tfn format_extension_mapping() {\n\t\tlet cases = [\n\t\t\t(Format::DNxHD, \"mxf\"),\n\t\t\t(Format::Matroska, \"mkv\"),\n\t\t\t(Format::MPEG4Video, \"mp4\"),\n\t\t\t(Format::MPEG4Audio, \"m4a\"),\n\t\t\t(Format::OpenEXR, \"exr\"),\n\t\t\t(Format::PNG, \"png\"),\n\t\t\t(Format::TIFF, \"tiff\"),\n\t\t\t(Format::QuickTime, \"mov\"),\n\t\t\t(Format::WAV, \"wav\"),\n\t\t\t(Format::AIFF, \"aiff\"),\n\t\t\t(Format::MP3, \"mp3\"),\n\t\t\t(Format::FLAC, \"flac\"),\n\t\t\t(Format::Ogg, \"ogg\"),\n\t\t\t(Format::WebM, \"webm\"),\n\t\t\t(Format::SRT, \"srt\"),\n\t\t];\n\t\tfor (f, ext) in cases {\n\t\t\tassert_eq!(Format::get_extension(f), ext, \"{:?}\", f);\n\t\t}\n\t\tassert_eq!(Format::get_extension(Format::Count), \"\");\n\t}\n\n\t#[test]\n\tfn from_i32_maps_valid_and_rejects_others() {\n\t\tfor v in 0..=14 {\n\t\t\tassert!(Format::from_i32(v).is_some(), \"format {v}\");\n\t\t}\n\t\tassert!(Format::from_i32(15).is_none()); // Count\n\t\tassert!(Format::from_i32(-1).is_none());\n\t\tassert!(Format::from_i32(99).is_none());\n\t}\n\n\t#[test]\n\tfn video_codec_tables() {\n\t\tassert_eq!(Format::get_video_codecs(Format::DNxHD), vec![Codec::DNxHD]);\n\t\tassert_eq!(\n\t\t\tFormat::get_video_codecs(Format::Matroska),\n\t\t\tvec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::VP9]\n\t\t);\n\t\tassert_eq!(\n\t\t\tFormat::get_video_codecs(Format::MPEG4Video),\n\t\t\tvec![Codec::H264, Codec::H264RGB, Codec::H265]\n\t\t);\n\t\tassert_eq!(Format::get_video_codecs(Format::OpenEXR), vec![Codec::OpenEXR]);\n\t\tassert_eq!(Format::get_video_codecs(Format::PNG), vec![Codec::PNG]);\n\t\tassert_eq!(Format::get_video_codecs(Format::TIFF), vec![Codec::TIFF]);\n\t\tassert_eq!(\n\t\t\tFormat::get_video_codecs(Format::QuickTime),\n\t\t\tvec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::ProRes, Codec::CineForm]\n\t\t);\n\t\tassert_eq!(Format::get_video_codecs(Format::WebM), vec![Codec::AV1, Codec::VP9]);\n\t\t// Formats without video codecs.\n\t\tassert!(Format::get_video_codecs(Format::WAV).is_empty());\n\t\tassert!(Format::get_video_codecs(Format::MP3).is_empty());\n\t\tassert!(Format::get_video_codecs(Format::SRT).is_empty());\n\t}\n\n\t#[test]\n\tfn audio_codec_tables() {\n\t\tassert_eq!(Format::get_audio_codecs(Format::WAV), vec![Codec::PCM]);\n\t\tassert_eq!(Format::get_audio_codecs(Format::AIFF), vec![Codec::PCM]);\n\t\tassert_eq!(Format::get_audio_codecs(Format::MP3), vec![Codec::MP3]);\n\t\tassert_eq!(Format::get_audio_codecs(Format::FLAC), vec![Codec::FLAC]);\n\t\tassert_eq!(\n\t\t\tFormat::get_audio_codecs(Format::Ogg),\n\t\t\tvec![Codec::Opus, Codec::Vorbis, Codec::PCM]\n\t\t);\n\t\tassert_eq!(Format::get_audio_codecs(Format::DNxHD), vec![Codec::PCM]);\n\t\tassert!(Format::get_audio_codecs(Format::PNG).is_empty());\n\t\tassert!(Format::get_audio_codecs(Format::SRT).is_empty());\n\t}\n\n\t#[test]\n\tfn subtitle_and_codec_capability_tables() {\n\t\tassert_eq!(Format::get_subtitle_codecs(Format::Matroska), vec![Codec::SRT]);\n\t\tassert_eq!(Format::get_subtitle_codecs(Format::SRT), vec![Codec::SRT]);\n\t\tassert!(Format::get_subtitle_codecs(Format::MPEG4Video).is_empty());\n\n\t\t// Pixel-format query is a bridge-dependent default (empty list).\n\t\tassert!(Format::get_pixel_formats_for_codec(Format::MPEG4Video, Codec::H264).is_empty());\n\n\t\t// PCM exposes its native sample formats; other codecs query the\n\t\t// FFmpeg bridge and get an empty list here.\n\t\tassert_eq!(\n\t\t\tFormat::get_sample_formats_for_codec(Format::WAV, Codec::PCM),\n\t\t\tvec![\n\t\t\t\tSampleFormat::S16,\n\t\t\t\tSampleFormat::U8,\n\t\t\t\tSampleFormat::S32,\n\t\t\t\tSampleFormat::S64,\n\t\t\t\tSampleFormat::F32,\n\t\t\t\tSampleFormat::F64,\n\t\t\t]\n\t\t);\n\t\tassert!(Format::get_sample_formats_for_codec(Format::MPEG4Video, Codec::AAC).is_empty());\n\t}\n}\n","traces":[{"line":67,"address":[],"length":0,"stats":{"Line":21}},{"line":68,"address":[],"length":0,"stats":{"Line":21}},{"line":69,"address":[],"length":0,"stats":{"Line":1}},{"line":70,"address":[],"length":0,"stats":{"Line":1}},{"line":71,"address":[],"length":0,"stats":{"Line":3}},{"line":72,"address":[],"length":0,"stats":{"Line":1}},{"line":73,"address":[],"length":0,"stats":{"Line":1}},{"line":74,"address":[],"length":0,"stats":{"Line":1}},{"line":75,"address":[],"length":0,"stats":{"Line":1}},{"line":76,"address":[],"length":0,"stats":{"Line":1}},{"line":77,"address":[],"length":0,"stats":{"Line":1}},{"line":78,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":16}},{"line":93,"address":[],"length":0,"stats":{"Line":16}},{"line":94,"address":[],"length":0,"stats":{"Line":2}},{"line":95,"address":[],"length":0,"stats":{"Line":2}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":97,"address":[],"length":0,"stats":{"Line":2}},{"line":98,"address":[],"length":0,"stats":{"Line":2}},{"line":99,"address":[],"length":0,"stats":{"Line":2}},{"line":100,"address":[],"length":0,"stats":{"Line":2}},{"line":101,"address":[],"length":0,"stats":{"Line":2}},{"line":102,"address":[],"length":0,"stats":{"Line":2}},{"line":103,"address":[],"length":0,"stats":{"Line":2}},{"line":104,"address":[],"length":0,"stats":{"Line":2}},{"line":105,"address":[],"length":0,"stats":{"Line":2}},{"line":106,"address":[],"length":0,"stats":{"Line":2}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":2}},{"line":109,"address":[],"length":0,"stats":{"Line":2}},{"line":117,"address":[],"length":0,"stats":{"Line":18}},{"line":118,"address":[],"length":0,"stats":{"Line":18}},{"line":119,"address":[],"length":0,"stats":{"Line":2}},{"line":120,"address":[],"length":0,"stats":{"Line":2}},{"line":121,"address":[],"length":0,"stats":{"Line":6}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":2}},{"line":124,"address":[],"length":0,"stats":{"Line":2}},{"line":125,"address":[],"length":0,"stats":{"Line":2}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":127,"address":[],"length":0,"stats":{"Line":2}},{"line":128,"address":[],"length":0,"stats":{"Line":2}},{"line":129,"address":[],"length":0,"stats":{"Line":2}},{"line":130,"address":[],"length":0,"stats":{"Line":2}},{"line":131,"address":[],"length":0,"stats":{"Line":2}},{"line":132,"address":[],"length":0,"stats":{"Line":2}},{"line":133,"address":[],"length":0,"stats":{"Line":2}},{"line":134,"address":[],"length":0,"stats":{"Line":1}},{"line":141,"address":[],"length":0,"stats":{"Line":11}},{"line":142,"address":[],"length":0,"stats":{"Line":11}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":144,"address":[],"length":0,"stats":{"Line":5}},{"line":145,"address":[],"length":0,"stats":{"Line":4}},{"line":146,"address":[],"length":0,"stats":{"Line":2}},{"line":147,"address":[],"length":0,"stats":{"Line":2}},{"line":148,"address":[],"length":0,"stats":{"Line":2}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":150,"address":[],"length":0,"stats":{"Line":1}},{"line":151,"address":[],"length":0,"stats":{"Line":1}},{"line":152,"address":[],"length":0,"stats":{"Line":1}},{"line":153,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":1}},{"line":156,"address":[],"length":0,"stats":{"Line":3}},{"line":157,"address":[],"length":0,"stats":{"Line":3}},{"line":164,"address":[],"length":0,"stats":{"Line":8}},{"line":165,"address":[],"length":0,"stats":{"Line":8}},{"line":167,"address":[],"length":0,"stats":{"Line":2}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":181,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":183,"address":[],"length":0,"stats":{"Line":0}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":190,"address":[],"length":0,"stats":{"Line":2}},{"line":191,"address":[],"length":0,"stats":{"Line":2}},{"line":192,"address":[],"length":0,"stats":{"Line":2}},{"line":193,"address":[],"length":0,"stats":{"Line":2}},{"line":194,"address":[],"length":0,"stats":{"Line":4}},{"line":195,"address":[],"length":0,"stats":{"Line":2}},{"line":203,"address":[],"length":0,"stats":{"Line":3}},{"line":204,"address":[],"length":0,"stats":{"Line":3}},{"line":205,"address":[],"length":0,"stats":{"Line":4}},{"line":206,"address":[],"length":0,"stats":{"Line":1}},{"line":217,"address":[],"length":0,"stats":{"Line":1}},{"line":218,"address":[],"length":0,"stats":{"Line":1}},{"line":227,"address":[],"length":0,"stats":{"Line":2}},{"line":228,"address":[],"length":0,"stats":{"Line":2}},{"line":229,"address":[],"length":0,"stats":{"Line":1}},{"line":230,"address":[],"length":0,"stats":{"Line":1}},{"line":231,"address":[],"length":0,"stats":{"Line":1}},{"line":232,"address":[],"length":0,"stats":{"Line":1}},{"line":233,"address":[],"length":0,"stats":{"Line":1}},{"line":234,"address":[],"length":0,"stats":{"Line":1}},{"line":235,"address":[],"length":0,"stats":{"Line":1}},{"line":238,"address":[],"length":0,"stats":{"Line":1}}],"covered":95,"coverable":112},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","conform.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/conform.h` exports.\n//!\n//! Complete inventory: create_instance / destroy_instance / get_state /\n//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the\n//! states.\n//!\n//! # CPP-PARITY\n//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the\n//! singleton is absent (`!ConformManager::instance()`); the Rust\n//! [`crate::conformmanager::ConformManager::instance`] is a lazy\n//! `'static` singleton that can never be absent, so that branch cannot\n//! trigger.\n\nuse std::ffi::{c_char, c_int};\n\nuse crate::conformmanager::{ConformManager, ConformState};\nuse crate::handle;\n\n/// `OAKCODEC_CONFORM_EXISTS`.\npub const OAKCODEC_CONFORM_EXISTS: c_int = 0;\n/// `OAKCODEC_CONFORM_GENERATING`.\npub const OAKCODEC_CONFORM_GENERATING: c_int = 1;\n/// `OAKCODEC_CONFORM_UNAVAILABLE`.\npub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2;\n\n/// `oakcodec_conform_create_instance`: create the singleton (always\n/// present here, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_create_instance() -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet _ = ConformManager::instance();\n\t\tcrate::error::OAKCODEC_OK\n\t})\n}\n\n/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust\n/// manager is stateless, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_destroy_instance() -\u003e c_int {\n\thandle::guard_raw(|| crate::error::OAKCODEC_OK)\n}\n\n/// `oakcodec_conform_get_state`: query the conform state of one audio\n/// stream.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_get_state(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\twait: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) if !c.is_empty() =\u003e c,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) if !s.is_empty() =\u003e s,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet m = ConformManager::instance();\n\t\tmatch m.get_conform_state(\n\t\t\t\u0026cache,\n\t\t\t\u0026source,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t\twait != 0,\n\t\t) {\n\t\t\tOk(ConformState::Exists) =\u003e OAKCODEC_CONFORM_EXISTS,\n\t\t\tOk(ConformState::Generating) =\u003e OAKCODEC_CONFORM_GENERATING,\n\t\t\tOk(ConformState::Unavailable) | Err(_) =\u003e OAKCODEC_CONFORM_UNAVAILABLE,\n\t\t}\n\t})\n}\n\n/// `oakcodec_conform_filename_count`: number of conform files for the\n/// given stream/params; 0 on invalid arguments.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_filename_count(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) if !c.is_empty() =\u003e c,\n\t\t\t_ =\u003e return 0,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) if !s.is_empty() =\u003e s,\n\t\t\t_ =\u003e return 0,\n\t\t};\n\t\tlet m = ConformManager::instance();\n\t\tm.get_conform_filename_count(\n\t\t\t\u0026cache,\n\t\t\t\u0026source,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t) as c_int\n\t})\n}\n\n/// `oakcodec_conform_filename_at`: the `index`-th conform filename\n/// (two-stage string).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_conform_filename_at(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\tindex: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) if !c.is_empty() =\u003e c,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) if !s.is_empty() =\u003e s,\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet m = ConformManager::instance();\n\t\tmatch m.get_conform_filename(\n\t\t\t\u0026cache,\n\t\t\t\u0026source,\n\t\t\tstream_index,\n\t\t\tsample_rate,\n\t\t\tchannel_layout,\n\t\t\tsample_format,\n\t\t\tindex as usize,\n\t\t) {\n\t\t\tOk(f) =\u003e super::string_out(\u0026f, buf, buf_size),\n\t\t\tErr(crate::error::Error::NotFound) =\u003e crate::error::OAKCODEC_E_NOT_FOUND,\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::conformmanager::test_util::REG_LOCK;\n\tuse crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn temp_cache(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_ffi_conform_{}_{}\", name, std::process::id()));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\t#[test]\n\tfn create_destroy_instance_ok() {\n\t\tassert_eq!(unsafe { oakcodec_conform_create_instance() }, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_conform_destroy_instance() }, crate::error::OAKCODEC_OK);\n\t}\n\n\t#[test]\n\tfn get_state_maps_states() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\t// No registrar and no files -\u003e UNAVAILABLE.\n\t\tlet cache = cstr(\u0026temp_cache(\"state\"));\n\t\tlet src = cstr(\"media.mp4\");\n\t\tlet rc = unsafe { oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE);\n\n\t\t// Invalid arguments -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet empty = cstr(\"\");\n\t\tlet rc = unsafe { oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Write the conform files -\u003e EXISTS.\n\t\tlet m = ConformManager::instance();\n\t\tfor i in 0..2 {\n\t\t\tlet f = m\n\t\t\t\t.get_conform_filename(\u0026temp_cache(\"state\"), \"media.mp4\", 0, 48000, 0x3, 0, i)\n\t\t\t\t.unwrap();\n\t\t\tstd::fs::write(\u0026f, b\"pcm\").unwrap();\n\t\t}\n\t\tlet rc = unsafe { oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };\n\t\tassert_eq!(rc, OAKCODEC_CONFORM_EXISTS);\n\t}\n\n\t#[test]\n\tfn filename_count_and_at() {\n\t\tlet cache = cstr(\u0026temp_cache(\"names\"));\n\t\tlet src = cstr(\"media.mp4\");\n\n\t\t// Stereo -\u003e 2 files.\n\t\tlet rc = unsafe { oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0) };\n\t\tassert_eq!(rc, 2);\n\n\t\t// Invalid args -\u003e 0 (not an error).\n\t\tlet rc = unsafe { oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0) };\n\t\tassert_eq!(rc, 0);\n\n\t\t// filename_at round-trips the deterministic name.\n\t\tlet mut buf = [0i8; 512];\n\t\tlet rc = unsafe { oakcodec_conform_filename_at(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0, buf.as_mut_ptr(), 512) };\n\t\tassert!(rc \u003e 0);\n\t\tlet name = crate::ffi::c_str(buf.as_ptr()).unwrap();\n\t\tassert!(name.ends_with(\".0.pcm\"));\n\n\t\t// Out-of-range index -\u003e E_NOT_FOUND.\n\t\tlet rc = unsafe { oakcodec_conform_filename_at(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 5, buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_NOT_FOUND);\n\n\t\t// Invalid args -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_conform_filename_at(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0, buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n}\n","traces":[{"line":45,"address":[],"length":0,"stats":{"Line":1}},{"line":46,"address":[],"length":0,"stats":{"Line":2}},{"line":47,"address":[],"length":0,"stats":{"Line":1}},{"line":48,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":1}},{"line":56,"address":[],"length":0,"stats":{"Line":1}},{"line":62,"address":[],"length":0,"stats":{"Line":4}},{"line":71,"address":[],"length":0,"stats":{"Line":8}},{"line":72,"address":[],"length":0,"stats":{"Line":10}},{"line":73,"address":[],"length":0,"stats":{"Line":12}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":10}},{"line":78,"address":[],"length":0,"stats":{"Line":0}},{"line":80,"address":[],"length":0,"stats":{"Line":4}},{"line":81,"address":[],"length":0,"stats":{"Line":4}},{"line":82,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":2}},{"line":84,"address":[],"length":0,"stats":{"Line":2}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":1}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":1}},{"line":100,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":4}},{"line":109,"address":[],"length":0,"stats":{"Line":5}},{"line":110,"address":[],"length":0,"stats":{"Line":5}},{"line":111,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":3}},{"line":114,"address":[],"length":0,"stats":{"Line":5}},{"line":115,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":3}},{"line":119,"address":[],"length":0,"stats":{"Line":2}},{"line":120,"address":[],"length":0,"stats":{"Line":2}},{"line":121,"address":[],"length":0,"stats":{"Line":2}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":132,"address":[],"length":0,"stats":{"Line":3}},{"line":143,"address":[],"length":0,"stats":{"Line":6}},{"line":144,"address":[],"length":0,"stats":{"Line":8}},{"line":145,"address":[],"length":0,"stats":{"Line":10}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":6}},{"line":149,"address":[],"length":0,"stats":{"Line":10}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":152,"address":[],"length":0,"stats":{"Line":4}},{"line":153,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":2}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":158,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":2}},{"line":160,"address":[],"length":0,"stats":{"Line":2}},{"line":162,"address":[],"length":0,"stats":{"Line":5}},{"line":163,"address":[],"length":0,"stats":{"Line":1}},{"line":164,"address":[],"length":0,"stats":{"Line":0}}],"covered":58,"coverable":63},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","decoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/decoder.h` exports.\n//!\n//! Complete inventory: probe / probe_last_error / probe_decoder_name /\n//! probe_video_stream_count / probe_audio_stream_count /\n//! probe_subtitle_stream_count / probe_get_video_stream /\n//! probe_get_audio_stream / init / free / open / close / is_open /\n//! decode_video / decode_audio / conform_audio /\n//! get_image_sequence_digit_count / get_image_sequence_index /\n//! transform_image_sequence_file_name / last_error.\n//!\n//! Two handle box types share the `OakDecoder` handle, mirroring the C++\n//! `ProbeBox` / `DecoderBox` split in `c_api/decoder.cpp`: probe exports\n//! read a plain [`ProbeBox`], session exports read a `Mutex\u003cDecoderBox\u003e`.\n\nuse std::ffi::{c_char, c_int};\n#[cfg(test)]\nuse std::ffi::c_void;\nuse std::path::Path;\nuse std::sync::{Arc, Mutex};\n\nuse oakcore_rs::{Rational, TimeRange};\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_get_channel_count, oakcommon_videoparams_get_color_primaries,\n\toakcommon_videoparams_get_color_transfer, oakcommon_videoparams_get_duration,\n\toakcommon_videoparams_get_format, oakcommon_videoparams_get_frame_rate,\n\toakcommon_videoparams_get_height, oakcommon_videoparams_get_interlacing,\n\toakcommon_videoparams_get_stream_index, oakcommon_videoparams_get_time_base,\n\toakcommon_videoparams_get_width, oakcore_audioparams_channel_count,\n\toakcore_audioparams_channel_layout, oakcore_audioparams_duration,\n\toakcore_audioparams_sample_rate, oakcore_audioparams_stream_index,\n\toakcore_audioparams_time_base, oakcore_rational_denominator,\n\toakcore_rational_free, oakcore_rational_numerator, OakAudioParams, OakVideoParams,\n};\nuse crate::bridge::render::{oakrender_cancelatom_heard_cancel, OakCancelAtom};\nuse crate::decoder::{\n\tCodecStream, Decoder, K_COLOR_RANGE_DEFAULT, OakCodecAudioStreamInfo,\n\tOakCodecVideoStreamInfo, RenderMode, RetrieveAudioStatus, RetrieveVideoParams,\n};\nuse crate::footagedescription::FootageDescription;\n#[cfg(test)]\nuse crate::frame::Frame;\nuse crate::handle::{self, CHandle};\n\n/// Box behind a probe handle: the recognized decoder name plus the\n/// probed stream inventory (`ProbeBox` in `c_api/decoder.cpp`).\nstruct ProbeBox {\n\tdecoder_name: String,\n\tdesc: FootageDescription,\n}\n\n/// Box behind a decode-session handle (`DecoderBox` in `c_api/decoder.cpp`).\nstruct DecoderBox {\n\tdecoder: Option\u003cArc\u003cdyn Decoder\u003e\u003e,\n\tlast_error: String,\n\topen_filename: String,\n\topen_stream: i32,\n\topen: bool,\n}\n\n/// Last probe failure reason. The C++ uses a `thread_local`; the crate\n/// serializes through a global mutex instead (tests are single-threaded\n/// per binary, so behavior is equivalent).\nstatic PROBE_ERROR: Mutex\u003cString\u003e = Mutex::new(String::new());\n\n/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).\nconst OAKCOMMON_VIDEO_INTERLACE_NONE: c_int = 0;\n\n/// Probe with every available decoder, returning the first valid\n/// description (mirrors `probe_with_any_decoder` in `c_api/decoder.cpp`).\n///\n/// `is_valid` follows the C++ `FootageDescription::is_valid`: a non-empty\n/// decoder name and at least one stream.\nfn probe_with_any_decoder(filename: \u0026str) -\u003e Option\u003c(String, FootageDescription)\u003e {\n\tfor d in crate::decoder::receive_list_of_all_decoders() {\n\t\tlet desc = match d.probe(filename, None) {\n\t\t\tSome(desc) =\u003e desc,\n\t\t\tNone =\u003e continue,\n\t\t};\n\t\tif !desc.decoder().is_empty()\n\t\t\t\u0026\u0026 (desc.video_stream_count() \u003e 0\n\t\t\t\t|| desc.audio_stream_count() \u003e 0\n\t\t\t\t|| desc.subtitle_stream_count() \u003e 0)\n\t\t{\n\t\t\treturn Some((desc.decoder().to_string(), desc));\n\t\t}\n\t}\n\tNone\n}\n\n/// Fill an `oakcodec_video_stream_info` from a probed video params handle\n/// (mirrors `fill_video_info` in `c_api/decoder.cpp`).\nfn fill_video_info(vp: \u0026OakVideoParams, out: \u0026mut OakCodecVideoStreamInfo) {\n\t*out = OakCodecVideoStreamInfo {\n\t\tstream_index: 0,\n\t\twidth: 0,\n\t\theight: 0,\n\t\tframe_rate_num: 0,\n\t\tframe_rate_den: 0,\n\t\tduration_ts: 0,\n\t\ttime_base_num: 0,\n\t\ttime_base_den: 0,\n\t\tformat: 0,\n\t\tchannel_count: 0,\n\t\tcolor_primaries: 0,\n\t\tcolor_trc: 0,\n\t\tinterlaced: 0,\n\t};\n\tout.stream_index = unsafe { oakcommon_videoparams_get_stream_index(vp.clone()) };\n\tout.width = unsafe { oakcommon_videoparams_get_width(vp.clone()) };\n\tout.height = unsafe { oakcommon_videoparams_get_height(vp.clone()) };\n\n\tlet mut fr_num: c_int = 0;\n\tlet mut fr_den: c_int = 0;\n\tunsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), \u0026mut fr_num, \u0026mut fr_den) };\n\tout.frame_rate_num = fr_num;\n\tout.frame_rate_den = fr_den;\n\n\tlet mut tb_num: i64 = 0;\n\tlet mut tb_den: i64 = 0;\n\tunsafe { oakcommon_videoparams_get_time_base(vp.clone(), \u0026mut tb_num, \u0026mut tb_den) };\n\tout.time_base_num = tb_num as c_int;\n\tout.time_base_den = tb_den as c_int;\n\n\tout.duration_ts = unsafe { oakcommon_videoparams_get_duration(vp.clone()) };\n\tout.format = unsafe { oakcommon_videoparams_get_format(vp.clone()) };\n\tout.channel_count = unsafe { oakcommon_videoparams_get_channel_count(vp.clone()) };\n\tout.color_primaries = unsafe { oakcommon_videoparams_get_color_primaries(vp.clone()) };\n\tout.color_trc = unsafe { oakcommon_videoparams_get_color_transfer(vp.clone()) };\n\n\tlet interlacing = unsafe { oakcommon_videoparams_get_interlacing(vp.clone()) };\n\tout.interlaced = (interlacing != OAKCOMMON_VIDEO_INTERLACE_NONE) as c_int;\n}\n\n/// Fill an `oakcodec_audio_stream_info` from a probed audio params handle\n/// (mirrors `fill_audio_info` in `c_api/decoder.cpp`).\nfn fill_audio_info(ap: \u0026OakAudioParams, out: \u0026mut OakCodecAudioStreamInfo) {\n\t*out = OakCodecAudioStreamInfo {\n\t\tstream_index: 0,\n\t\tsample_rate: 0,\n\t\tchannel_layout: 0,\n\t\tchannel_count: 0,\n\t\tduration_ts: 0,\n\t\ttime_base_num: 0,\n\t\ttime_base_den: 0,\n\t};\n\tlet ctx = ap.ctx as *const OakAudioParams;\n\tout.stream_index = unsafe { oakcore_audioparams_stream_index(ctx) };\n\tout.sample_rate = unsafe { oakcore_audioparams_sample_rate(ctx) };\n\tout.channel_layout = unsafe { oakcore_audioparams_channel_layout(ctx) };\n\tout.channel_count = unsafe { oakcore_audioparams_channel_count(ctx) };\n\tout.duration_ts = unsafe { oakcore_audioparams_duration(ctx) };\n\n\t// `time_base` returns a newly allocated rational the caller owns.\n\tlet tb = unsafe { oakcore_audioparams_time_base(ctx) };\n\tif !tb.is_null() {\n\t\tout.time_base_num = unsafe { oakcore_rational_numerator(tb) };\n\t\tout.time_base_den = unsafe { oakcore_rational_denominator(tb) };\n\t\tunsafe { oakcore_rational_free(tb) };\n\t}\n}\n\n/// Whether a media file exists on disk (mirrors the C++ `file_exists`).\nfn file_exists(filename: \u0026str) -\u003e bool {\n\tPath::new(filename).exists()\n}\n\n/* ---- Probe ---------------------------------------------------------------- */\n\n/// `oakcodec_decoder_probe`: probe a media file with every available\n/// decoder; empty handle plus `oakcodec_probe_last_error` on failure.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe(filename: *const c_char) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tlet filename = match crate::ffi::c_str(filename) {\n\t\t\tSome(f) if !f.is_empty() =\u003e f,\n\t\t\t_ =\u003e {\n\t\t\t\t*PROBE_ERROR.lock().unwrap() = \"no filename given\".to_string();\n\t\t\t\treturn Ok(CHandle::null());\n\t\t\t}\n\t\t};\n\t\tif !file_exists(\u0026filename) {\n\t\t\t*PROBE_ERROR.lock().unwrap() = format!(\"file not found: {}\", filename);\n\t\t\treturn Ok(CHandle::null());\n\t\t}\n\t\tmatch probe_with_any_decoder(\u0026filename) {\n\t\t\tSome((decoder_name, desc)) =\u003e {\n\t\t\t\tPROBE_ERROR.lock().unwrap().clear();\n\t\t\t\tOk(handle::make_owned(ProbeBox { decoder_name, desc }))\n\t\t\t}\n\t\t\tNone =\u003e {\n\t\t\t\t*PROBE_ERROR.lock().unwrap() =\n\t\t\t\t\tformat!(\"no decoder recognizes this file: {}\", filename);\n\t\t\t\tOk(CHandle::null())\n\t\t\t}\n\t\t}\n\t})\n}\n\n/// `oakcodec_probe_last_error`: last probe failure reason (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_probe_last_error(buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet s = PROBE_ERROR.lock().unwrap().clone();\n\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t})\n}\n\n/// `oakcodec_decoder_probe_decoder_name`: recognized decoder id\n/// (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_decoder_name(\n\tprobe: CHandle,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tmatch super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\t\tSome(b) =\u003e super::string_out(\u0026b.decoder_name, buf, buf_size),\n\t\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID,\n\t\t}\n\t})\n}\n\n/// `oakcodec_decoder_probe_video_stream_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_video_stream_count(probe: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\tSome(b) =\u003e b.desc.video_stream_count() as c_int,\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_probe_audio_stream_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_audio_stream_count(probe: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\tSome(b) =\u003e b.desc.audio_stream_count() as c_int,\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_probe_subtitle_stream_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_subtitle_stream_count(probe: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cProbeBox\u003e(\u0026probe) {\n\t\tSome(b) =\u003e b.desc.subtitle_stream_count() as c_int,\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_probe_get_video_stream`: fill `out` with the video\n/// stream at `index` (video ordinal).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_get_video_stream(\n\tprobe: CHandle,\n\tindex: c_int,\n\tout: *mut OakCodecVideoStreamInfo,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet b = super::get_box::\u003cProbeBox\u003e(\u0026probe).ok_or(crate::error::Error::Invalid)?;\n\t\tlet vp = b\n\t\t\t.desc\n\t\t\t.get_video_stream(index as usize)\n\t\t\t.ok_or(crate::error::Error::NotFound)?;\n\t\tfill_video_info(vp, unsafe { \u0026mut *out });\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_decoder_probe_get_audio_stream`: fill `out` with the audio\n/// stream at `index` (audio ordinal).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_probe_get_audio_stream(\n\tprobe: CHandle,\n\tindex: c_int,\n\tout: *mut OakCodecAudioStreamInfo,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet b = super::get_box::\u003cProbeBox\u003e(\u0026probe).ok_or(crate::error::Error::Invalid)?;\n\t\tlet ap = b\n\t\t\t.desc\n\t\t\t.get_audio_stream(index as usize)\n\t\t\t.ok_or(crate::error::Error::NotFound)?;\n\t\tfill_audio_info(ap, unsafe { \u0026mut *out });\n\t\tOk(())\n\t})\n}\n\n/* ---- Decode session -------------------------------------------------------- */\n\n/// `oakcodec_decoder_init`: new closed decoder handle, count 1.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_init() -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tOk(handle::make_owned(Mutex::new(DecoderBox {\n\t\t\tdecoder: None,\n\t\t\tlast_error: String::new(),\n\t\t\topen_filename: String::new(),\n\t\t\topen_stream: -1,\n\t\t\topen: false,\n\t\t})))\n\t})\n}\n\n/// `oakcodec_decoder_free`: NULL/empty no-op; nulls `ctx` afterwards.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_free(decoder: *mut CHandle) {\n\thandle::guard_void(|| super::free_handle(decoder));\n}\n\n/// `oakcodec_decoder_open`: open `filename`'s stream `stream_index` for\n/// decoding; opening the same stream again is a successful no-op.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_open(\n\tdecoder: CHandle,\n\tfilename: *const c_char,\n\tstream_index: c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet filename = match crate::ffi::c_str(filename) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tif stream_index \u003c 0 {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet mut b = b.lock().unwrap();\n\n\t\tif b.open \u0026\u0026 b.decoder.is_some() {\n\t\t\tif b.open_filename == filename \u0026\u0026 b.open_stream == stream_index {\n\t\t\t\treturn Ok(()); // already open on this stream\n\t\t\t}\n\t\t\tlet _ = b.decoder.as_ref().unwrap().close();\n\t\t\tb.open = false;\n\t\t}\n\n\t\tif !file_exists(\u0026filename) {\n\t\t\tb.last_error = format!(\"file not found: {}\", filename);\n\t\t\treturn Err(crate::error::Error::NotFound);\n\t\t}\n\n\t\tlet (decoder_name, _desc) = match probe_with_any_decoder(\u0026filename) {\n\t\t\tSome(x) =\u003e x,\n\t\t\tNone =\u003e {\n\t\t\t\tb.last_error = format!(\"no decoder recognizes this file: {}\", filename);\n\t\t\t\treturn Err(crate::error::Error::Failed(\"no decoder recognizes this file\".to_string()));\n\t\t\t}\n\t\t};\n\n\t\tlet decoder = match crate::decoder::create_from_id(\u0026decoder_name) {\n\t\t\tSome(d) =\u003e d,\n\t\t\tNone =\u003e {\n\t\t\t\tb.last_error = format!(\"failed to create decoder: {}\", decoder_name);\n\t\t\t\treturn Err(crate::error::Error::Failed(\"failed to create decoder\".to_string()));\n\t\t\t}\n\t\t};\n\n\t\tlet stream = CodecStream::with_block(filename.clone(), stream_index, None);\n\t\tif decoder.open(\u0026stream).is_err() {\n\t\t\tb.last_error = \"failed to open stream\".to_string();\n\t\t\treturn Err(crate::error::Error::Failed(\"failed to open stream\".to_string()));\n\t\t}\n\n\t\tb.last_error.clear();\n\t\tb.decoder = Some(decoder);\n\t\tb.open_filename = filename;\n\t\tb.open_stream = stream_index;\n\t\tb.open = true;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_decoder_close`: close the current stream (safe when closed).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_close(decoder: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet mut b = b.lock().unwrap();\n\t\tif b.open {\n\t\t\tif let Some(d) = \u0026b.decoder {\n\t\t\t\tlet _ = d.close();\n\t\t\t}\n\t\t}\n\t\tb.open = false;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_decoder_is_open`: 1 when a stream is open.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_is_open(decoder: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder) {\n\t\tSome(b) =\u003e {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif b.open {\n\t\t\t\t1\n\t\t\t} else {\n\t\t\t\t0\n\t\t\t}\n\t\t}\n\t\tNone =\u003e 0,\n\t})\n}\n\n/// `oakcodec_decoder_decode_video`: decode the frame at\n/// `numerator/denominator` seconds into a new frame handle (count 1).\n///\n/// # CPP-PARITY\n/// The C++ boxes an `olive::FramePtr` and happily aliases a frame the\n/// decoder still holds. The Rust interim boxes `Mutex\u003cFrame\u003e` (see\n/// `ffi::frame`), so a decode that returns a still-shared `Arc\u003cFrame\u003e`\n/// cannot be aliased and reports an empty handle plus `last_error`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_decode_video(\n\tdecoder: CHandle,\n\tnumerator: c_int,\n\tdenominator: c_int,\n) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet d = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.decoder.is_none() {\n\t\t\t\t// C++ returns an empty frame without touching last_error.\n\t\t\t\treturn Ok(CHandle::null());\n\t\t\t}\n\t\t\tb.decoder.as_ref().unwrap().clone()\n\t\t};\n\t\tlet p = RetrieveVideoParams {\n\t\t\t// C++ default-constructs the params and only sets `time`.\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(numerator as i64, denominator as i64),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t};\n\t\tmatch d.retrieve_video_frame(\u0026p) {\n\t\t\tOk(arc) =\u003e match Arc::try_unwrap(arc) {\n\t\t\t\tOk(frame) =\u003e Ok(handle::make_owned(Mutex::new(frame))),\n\t\t\t\tErr(_) =\u003e {\n\t\t\t\t\tb.lock().unwrap().last_error = \"failed to decode video frame\".to_string();\n\t\t\t\t\tOk(CHandle::null())\n\t\t\t\t}\n\t\t\t},\n\t\t\tErr(_) =\u003e {\n\t\t\t\tb.lock().unwrap().last_error = \"failed to decode video frame\".to_string();\n\t\t\t\tOk(CHandle::null())\n\t\t\t}\n\t\t}\n\t})\n}\n\n/// `oakcodec_decoder_decode_audio`: decode interleaved float audio\n/// covering [in, out) into `buf` (at least `buf_frames` frames).\n///\n/// Returns the number of frames written, or a negative `OAKCODEC_E_*`.\n/// # CPP-PARITY\n/// The C++ retrieves into an allocated `SampleBuffer` and copies\n/// `min(available, buf_frames)` frames. The Rust trait fills a caller-size\n/// destination, so `Success` is treated as \"`buf_frames` were written\".\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_decode_audio(\n\tdecoder: CHandle,\n\tin_num: c_int,\n\tin_den: c_int,\n\tout_num: c_int,\n\tout_den: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tbuf: *mut f32,\n\tbuf_frames: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tunsafe {\n\t\t\tdecode_audio_inner(\n\t\t\t\tdecoder,\n\t\t\t\tin_num,\n\t\t\t\tin_den,\n\t\t\t\tout_num,\n\t\t\t\tout_den,\n\t\t\t\tsample_rate,\n\t\t\t\tchannel_layout,\n\t\t\t\tbuf,\n\t\t\t\tbuf_frames,\n\t\t\t)\n\t\t}\n\t})\n}\n\nunsafe fn decode_audio_inner(\n\tdecoder: CHandle,\n\tin_num: c_int,\n\tin_den: c_int,\n\tout_num: c_int,\n\tout_den: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tbuf: *mut f32,\n\tbuf_frames: c_int,\n) -\u003e c_int {\n\tlet b = match super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder) {\n\t\tSome(b) =\u003e b,\n\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t};\n\tif (buf.is_null() \u0026\u0026 buf_frames \u003e 0) || buf_frames \u003c 0 {\n\t\treturn crate::error::OAKCODEC_E_INVALID;\n\t}\n\n\tlet d = {\n\t\tlet b = b.lock().unwrap();\n\t\tif !b.open || b.decoder.is_none() {\n\t\t\treturn crate::error::OAKCODEC_E_STATE;\n\t\t}\n\t\tb.decoder.as_ref().unwrap().clone()\n\t};\n\n\tlet range = TimeRange::new(\n\t\tRational::new(in_num as i64, in_den as i64),\n\t\tRational::new(out_num as i64, out_den as i64),\n\t);\n\tlet channels = channel_layout.count_ones() as usize;\n\tlet mut dest = vec![0f32; buf_frames as usize * channels];\n\n\tlet status = match d.retrieve_audio(\u0026mut dest, \u0026range, sample_rate, channel_layout) {\n\t\tOk(s) =\u003e s,\n\t\tErr(_) =\u003e {\n\t\t\tb.lock().unwrap().last_error = \"failed to decode audio\".to_string();\n\t\t\treturn crate::error::OAKCODEC_E_FAILED;\n\t\t}\n\t};\n\n\tmatch status {\n\t\tRetrieveAudioStatus::Success =\u003e {\n\t\t\t// SAFETY: the caller guarantees `buf` holds at least `buf_frames`\n\t\t\t// interleaved floats; `dest` is exactly that size.\n\t\t\tunsafe { std::ptr::copy_nonoverlapping(dest.as_ptr(), buf, dest.len()) };\n\t\t\tbuf_frames\n\t\t}\n\t\tRetrieveAudioStatus::ConformNeeded =\u003e {\n\t\t\tb.lock().unwrap().last_error =\n\t\t\t\t\"audio requires a conform, but no task submit callback is registered (see oakcodec_set_task_submit_cb)\"\n\t\t\t\t\t.to_string();\n\t\t\tcrate::error::OAKCODEC_E_STATE\n\t\t}\n\t\t_ =\u003e {\n\t\t\tb.lock().unwrap().last_error = \"failed to decode audio\".to_string();\n\t\t\tcrate::error::OAKCODEC_E_FAILED\n\t\t}\n\t}\n}\n\n/// `oakcodec_decoder_conform_audio`: conform the open stream's audio into\n/// the given per-channel pcm files.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_conform_audio(\n\tdecoder: CHandle,\n\toutput_filenames: *const *const c_char,\n\tfilename_count: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\tcancelled: OakCancelAtom,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tunsafe {\n\t\t\tconform_audio_inner(\n\t\t\t\tdecoder,\n\t\t\t\toutput_filenames,\n\t\t\t\tfilename_count,\n\t\t\t\tsample_rate,\n\t\t\t\tchannel_layout,\n\t\t\t\tsample_format,\n\t\t\t\tcancelled,\n\t\t\t)\n\t\t}\n\t})\n}\n\nunsafe fn conform_audio_inner(\n\tdecoder: CHandle,\n\toutput_filenames: *const *const c_char,\n\tfilename_count: c_int,\n\tsample_rate: c_int,\n\tchannel_layout: u64,\n\tsample_format: c_int,\n\tcancelled: OakCancelAtom,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tlet b = super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder).ok_or(crate::error::Error::Invalid)?;\n\tif (output_filenames.is_null() \u0026\u0026 filename_count \u003e 0) || filename_count \u003c 0 {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\n\tlet d = {\n\t\tlet b = b.lock().unwrap();\n\t\tif !b.open || b.decoder.is_none() {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tb.decoder.as_ref().unwrap().clone()\n\t};\n\n\tlet mut filenames: Vec\u003cString\u003e = Vec::with_capacity(filename_count as usize);\n\tfor i in 0..filename_count {\n\t\tlet p = unsafe { *output_filenames.add(i as usize) };\n\t\tlet s = crate::ffi::c_str(p).ok_or(crate::error::Error::Invalid)?;\n\t\tfilenames.push(s);\n\t}\n\n\t// The trait takes the target audio params flattened; no handle\n\t// construction is needed.\n\tlet cancelled_ref = if cancelled.ctx.is_null() {\n\t\tNone\n\t} else {\n\t\tSome(\u0026cancelled)\n\t};\n\tlet ret = d.conform_audio(\n\t\t\u0026filenames,\n\t\tsample_rate,\n\t\tchannel_layout,\n\t\tsample_format,\n\t\tcancelled_ref,\n\t);\n\n\tmatch ret {\n\t\tOk(()) =\u003e Ok(()),\n\t\tErr(_) =\u003e {\n\t\t\tlet heard = if cancelled.ctx.is_null() {\n\t\t\t\t0\n\t\t\t} else {\n\t\t\t\tunsafe { oakrender_cancelatom_heard_cancel(cancelled.clone()) }\n\t\t\t};\n\t\t\tif heard != 0 {\n\t\t\t\tErr(crate::error::Error::Cancelled)\n\t\t\t} else {\n\t\t\t\tErr(crate::error::Error::Failed(\"conform failed\".to_string()))\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// `oakcodec_decoder_get_image_sequence_digit_count`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_get_image_sequence_digit_count(\n\tfilename: *const c_char,\n) -\u003e c_int {\n\thandle::guard_raw(|| match crate::ffi::c_str(filename) {\n\t\tSome(f) =\u003e crate::decoder::get_image_sequence_digit_count(\u0026f),\n\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID,\n\t})\n}\n\n/// `oakcodec_decoder_get_image_sequence_index`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_get_image_sequence_index(\n\tfilename: *const c_char,\n) -\u003e i64 {\n\thandle::guard_i64(|| match crate::ffi::c_str(filename) {\n\t\tSome(f) =\u003e crate::decoder::get_image_sequence_index(\u0026f),\n\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID as i64,\n\t})\n}\n\n/// `oakcodec_decoder_transform_image_sequence_file_name` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_transform_image_sequence_file_name(\n\tfilename: *const c_char,\n\tnumber: i64,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| match crate::ffi::c_str(filename) {\n\t\tSome(f) =\u003e {\n\t\t\tlet s = crate::decoder::transform_image_sequence_file_name(\u0026f, number);\n\t\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t\t}\n\t\tNone =\u003e crate::error::OAKCODEC_E_INVALID,\n\t})\n}\n\n/// `oakcodec_decoder_last_error` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_decoder_last_error(\n\tdecoder: CHandle,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cMutex\u003cDecoderBox\u003e\u003e(\u0026decoder) {\n\t\tSome(b) =\u003e {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tsuper::string_out(\u0026b.last_error, buf, buf_size)\n\t\t}\n\t\tNone =\u003e super::string_out(\"\", buf, buf_size),\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::{\n\t\toakcommon_videoparams_init_basic, oakcommon_videoparams_init_with_time_base,\n\t\toakcommon_videoparams_set_stream_index, oakcore_audioparams_create,\n\t};\n\tuse crate::bridge::render::{oakrender_cancelatom_cancel, oakrender_cancelatom_init};\n\tuse crate::decoder::set_test_decoders;\n\tuse crate::footagedescription::StreamEntry;\n\tuse crate::error::{OAKCODEC_E_CANCELLED, OAKCODEC_E_INVALID, OAKCODEC_E_STATE};\n\n\t/// The crate-wide ffi test lock (`crate::ffi::lock_tests`) serializes\n\t/// every test in this module (they share the global probe error, the\n\t/// injected decoder registry and the fake's state).\n\n\t/// Fake decoder driving the ffi export tests.\n\tstruct FakeDecoder {\n\t\tretain_frame: bool,\n\t\tretained: Mutex\u003cOption\u003cArc\u003cFrame\u003e\u003e\u003e,\n\t\tconform_fails: Mutex\u003cbool\u003e,\n\t\taudio_conform_needed: Mutex\u003cbool\u003e,\n\t}\n\n\timpl FakeDecoder {\n\t\tfn new(retain_frame: bool) -\u003e Arc\u003cdyn Decoder\u003e {\n\t\t\tArc::new(FakeDecoder {\n\t\t\t\tretain_frame,\n\t\t\t\tretained: Mutex::new(None),\n\t\t\t\tconform_fails: Mutex::new(false),\n\t\t\t\taudio_conform_needed: Mutex::new(false),\n\t\t\t})\n\t\t}\n\n\t\tfn with_audio_conform_needed() -\u003e Arc\u003cFakeDecoder\u003e {\n\t\t\tArc::new(FakeDecoder {\n\t\t\t\tretain_frame: false,\n\t\t\t\tretained: Mutex::new(None),\n\t\t\t\tconform_fails: Mutex::new(false),\n\t\t\t\taudio_conform_needed: Mutex::new(true),\n\t\t\t})\n\t\t}\n\n\t\tfn with_conform_failing() -\u003e Arc\u003cFakeDecoder\u003e {\n\t\t\tArc::new(FakeDecoder {\n\t\t\t\tretain_frame: false,\n\t\t\t\tretained: Mutex::new(None),\n\t\t\t\tconform_fails: Mutex::new(false),\n\t\t\t\taudio_conform_needed: Mutex::new(false),\n\t\t\t})\n\t\t}\n\t}\n\n\timpl Decoder for FakeDecoder {\n\t\tfn id(\u0026self) -\u003e String {\n\t\t\t\"fake\".to_string()\n\t\t}\n\n\t\tfn supports_video(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\n\t\tfn supports_audio(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\n\t\tfn probe(\n\t\t\t\u0026self,\n\t\t\tfilename: \u0026str,\n\t\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t\t) -\u003e Option\u003cFootageDescription\u003e {\n\t\t\tif filename.ends_with(\"test_video.mp4\") {\n\t\t\t\tlet mut desc = FootageDescription::new(\"fake\");\n\t\t\t\tlet vp = unsafe {\n\t\t\t\t\toakcommon_videoparams_init_with_time_base(1920, 1080, 1001, 30000)\n\t\t\t\t};\n\t\t\t\tunsafe { oakcommon_videoparams_set_stream_index(vp.clone(), 0) };\n\t\t\t\tdesc.push_stream(StreamEntry::Video(vp));\n\t\t\t\tSome(desc)\n\t\t\t} else if filename.ends_with(\"test_audio.mp4\") {\n\t\t\t\tlet mut desc = FootageDescription::new(\"fake\");\n\t\t\t\tlet ap = unsafe { oakcore_audioparams_create(48000, 0x3, 10) };\n\t\t\t\tdesc.push_stream(StreamEntry::Audio(OakAudioParams {\n\t\t\t\t\tctx: ap as *mut c_void,\n\t\t\t\t\taddref: None,\n\t\t\t\t\trelease: None,\n\t\t\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t\t\t}));\n\t\t\t\tSome(desc)\n\t\t\t} else {\n\t\t\t\tNone\n\t\t\t}\n\t\t}\n\n\t\tfn open(\u0026self, _stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\n\t\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\n\t\tfn stream(\u0026self) -\u003e CodecStream {\n\t\t\tCodecStream::new()\n\t\t}\n\n\t\tfn retrieve_video_frame(\n\t\t\t\u0026self,\n\t\t\t_p: \u0026RetrieveVideoParams,\n\t\t) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e {\n\t\t\tlet params = unsafe { oakcommon_videoparams_init_basic(100, 50) };\n\t\t\tlet frame = Arc::new(Frame::with_params(params));\n\t\t\tif self.retain_frame {\n\t\t\t\t// Keep an alias alive so the ffi decode sees a shared Arc.\n\t\t\t\t*self.retained.lock().unwrap() = Some(frame.clone());\n\t\t\t}\n\t\t\tOk(frame)\n\t\t}\n\n\t\tfn retrieve_video(\n\t\t\t\u0026self,\n\t\t\t_p: \u0026RetrieveVideoParams,\n\t\t) -\u003e crate::error::Result\u003ccrate::bridge::render::OakRenderTexture\u003e {\n\t\t\tErr(crate::error::Error::Failed(\"no texture\".to_string()))\n\t\t}\n\n\t\tfn retrieve_audio(\n\t\t\t\u0026self,\n\t\t\tdest: \u0026mut [f32],\n\t\t\t_range: \u0026TimeRange,\n\t\t\t_sample_rate: i32,\n\t\t\t_channel_layout: u64,\n\t\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\t\tif *self.audio_conform_needed.lock().unwrap() {\n\t\t\t\treturn Ok(RetrieveAudioStatus::ConformNeeded);\n\t\t\t}\n\t\t\tfor s in dest.iter_mut() {\n\t\t\t\t*s = 1.0;\n\t\t\t}\n\t\t\tOk(RetrieveAudioStatus::Success)\n\t\t}\n\n\t\tfn conform_audio(\n\t\t\t\u0026self,\n\t\t\t_output_filenames: \u0026[String],\n\t\t\t_sample_rate: i32,\n\t\t\t_channel_layout: u64,\n\t\t\t_sample_format: i32,\n\t\t\t_cancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tif *self.conform_fails.lock().unwrap() {\n\t\t\t\tErr(crate::error::Error::Failed(\"conform failed\".to_string()))\n\t\t\t} else {\n\t\t\t\tOk(())\n\t\t\t}\n\t\t}\n\t}\n\n\tfn inject(fake: Arc\u003cdyn Decoder\u003e) {\n\t\tset_test_decoders(vec![fake]);\n\t}\n\n\tfn restore() {\n\t\tset_test_decoders(Vec::new());\n\t}\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn empty_cancel() -\u003e OakCancelAtom {\n\t\tOakCancelAtom {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: 0,\n\t\t}\n\t}\n\n\tfn media_file(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_ffi_dec_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tlet path = dir.join(name);\n\t\tlet _ = std::fs::write(\u0026path, b\"media\");\n\t\tpath.to_string_lossy().into_owned()\n\t}\n\n\t#[test]\n\tfn probe_golden_video() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\n\t\tlet mut name = [0i8; 64];\n\t\tlet rc = unsafe { oakcodec_decoder_probe_decoder_name(h, name.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 5); // \"fake\" + NUL\n\t\tassert_eq!(crate::ffi::c_str(name.as_ptr()).as_deref(), Some(\"fake\"));\n\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 1);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_subtitle_stream_count(h) }, 0);\n\n\t\tlet mut info: OakCodecVideoStreamInfo = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, \u0026mut info) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(info.stream_index, 0);\n\t\tassert_eq!(info.width, 1920);\n\t\tassert_eq!(info.height, 1080);\n\t\tassert_eq!(info.time_base_num, 1001);\n\t\tassert_eq!(info.time_base_den, 30000);\n\t\tassert_eq!(info.channel_count, 4);\n\t\tassert_eq!(info.interlaced, 0);\n\t\t// frame_rate/duration/colors default to 0 in the stub.\n\t\tassert_eq!(info.frame_rate_num, 0);\n\t\tassert_eq!(info.duration_ts, 0);\n\n\t\t// index out of range / null out.\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 3, \u0026mut info) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND);\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_video_stream(h, 0, std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// probe_last_error is cleared on success.\n\t\tlet mut err = [0i8; 256];\n\t\tlet rc = unsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert_eq!(rc, 1); // just the NUL\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"\"));\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\tassert!(h.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn probe_golden_audio() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_audio_stream_count(h) }, 1);\n\n\t\tlet mut info: OakCodecAudioStreamInfo = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_decoder_probe_get_audio_stream(h, 0, \u0026mut info) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(info.stream_index, 0);\n\t\tassert_eq!(info.sample_rate, 48000);\n\t\tassert_eq!(info.channel_layout, 0x3);\n\t\tassert_eq!(info.channel_count, 2);\n\t\tassert_eq!(info.time_base_num, 1);\n\t\tassert_eq!(info.time_base_den, 48000);\n\t\tassert_eq!(info.duration_ts, 0);\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn probe_failures() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\t// NULL filename.\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(std::ptr::null()) };\n\t\tassert!(h.is_null());\n\t\tlet mut err = [0i8; 256];\n\t\tunsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"no filename given\"));\n\n\t\t// Empty filename.\n\t\tlet f = cstr(\"\");\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(h.is_null());\n\n\t\t// Nonexistent file.\n\t\tlet f = cstr(\"/definitely/not/here.mp4\");\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(h.is_null());\n\t\tunsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert_eq!(\n\t\t\tcrate::ffi::c_str(err.as_ptr()).as_deref(),\n\t\t\tSome(\"file not found: /definitely/not/here.mp4\")\n\t\t);\n\n\t\t// A file no decoder recognizes (a real file: the test harness).\n\t\tlet me = std::env::current_exe().unwrap();\n\t\tlet me = me.to_string_lossy().into_owned();\n\t\tlet f = cstr(\u0026me);\n\t\tlet mut h = unsafe { oakcodec_decoder_probe(f.as_ptr()) };\n\t\tassert!(h.is_null());\n\t\tunsafe { oakcodec_probe_last_error(err.as_mut_ptr(), 256) };\n\t\tassert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains(\"no decoder recognizes\"));\n\n\t\t// Empty handle on probe exports.\n\t\tlet empty = CHandle::null();\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_decoder_name(empty, err.as_mut_ptr(), 256) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_decoder_probe_video_stream_count(empty) }, 0);\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn open_decode_video_golden() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 0);\n\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 1);\n\n\t\t// Reopening the same stream is a successful no-op.\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\tlet mut frame = unsafe { oakcodec_decoder_decode_video(h, 1, 30) };\n\t\tassert!(!frame.is_null());\n\t\tassert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_width(frame) }, 100);\n\t\tassert_eq!(unsafe { crate::ffi::frame::oakcodec_frame_height(frame) }, 50);\n\n\t\tunsafe { crate::ffi::frame::oakcodec_frame_free(\u0026mut frame) };\n\t\tlet rc = unsafe { oakcodec_decoder_close(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(h) }, 0);\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn decode_video_shared_frame_reports_error() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(true)); // decoder retains the frame\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tunsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tlet mut frame = unsafe { oakcodec_decoder_decode_video(h, 0, 1) };\n\t\tassert!(frame.is_null());\n\t\tlet mut err = [0i8; 128];\n\t\tunsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 128) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"failed to decode video frame\"));\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn decode_audio_golden_and_errors() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\tlet mut buf = [0f32; 64];\n\t\tlet frames = unsafe {\n\t\t\toakcodec_decoder_decode_audio(\n\t\t\t\th, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16,\n\t\t\t)\n\t\t};\n\t\tassert_eq!(frames, 16);\n\t\t// Interleaved stereo filled by the fake.\n\t\tassert_eq!(buf[0], 1.0);\n\t\tassert_eq!(buf[31], 1.0);\n\n\t\t// Invalid args.\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, std::ptr::null_mut(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), -1) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Not open -\u003e E_STATE.\n\t\tlet mut h2 = unsafe { oakcodec_decoder_init() };\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h2, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h2) };\n\n\t\t// Empty handle -\u003e E_INVALID.\n\t\tlet empty = CHandle::null();\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(empty, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn decode_audio_conform_needed_is_state() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_decoders(vec![FakeDecoder::with_audio_conform_needed()]);\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tunsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tlet mut buf = [0f32; 64];\n\t\tlet rc = unsafe { oakcodec_decoder_decode_audio(h, 0, 1, 1, 1, 48000, 0x3, buf.as_mut_ptr(), 16) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet mut err = [0i8; 256];\n\t\tunsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };\n\t\tassert!(crate::ffi::c_str(err.as_ptr()).as_deref().unwrap().contains(\"conform\"));\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn conform_audio_golden_cancelled_and_errors() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet fake = FakeDecoder::with_conform_failing();\n\t\tset_test_decoders(vec![fake.clone()]);\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\t\tlet v = media_file(\"test_audio.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tunsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\n\t\tlet a = cstr(\"a.pcm\");\n\t\tlet b = cstr(\"b.pcm\");\n\t\tlet files = [a.as_ptr(), b.as_ptr()];\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// Not open -\u003e E_STATE.\n\t\tlet mut h2 = unsafe { oakcodec_decoder_init() };\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h2, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\t// Invalid args.\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, std::ptr::null(), 1, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), -1, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Failure without cancellation -\u003e E_FAILED.\n\t\t*fake.conform_fails.lock().unwrap() = true;\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, empty_cancel())\n\t\t};\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_FAILED);\n\n\t\t// Failure with a cancelled atom -\u003e E_CANCELLED.\n\t\tlet atom = unsafe { oakrender_cancelatom_init() };\n\t\tunsafe { oakrender_cancelatom_cancel(atom.clone()) };\n\t\tlet rc = unsafe {\n\t\t\toakcodec_decoder_conform_audio(h, files.as_ptr(), 2, 48000, 0x3, 10, atom)\n\t\t};\n\t\tassert_eq!(rc, OAKCODEC_E_CANCELLED);\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h2) };\n\t\trestore();\n\t}\n\n\t#[test]\n\tfn image_sequence_exports() {\n\t\tlet _g = crate::ffi::lock_tests();\n\n\t\tlet f = cstr(\"frame_0001.png\");\n\t\tassert_eq!(\n\t\t\tunsafe { oakcodec_decoder_get_image_sequence_digit_count(f.as_ptr()) },\n\t\t\t4\n\t\t);\n\t\tassert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(f.as_ptr()) }, 1);\n\n\t\tlet mut buf = [0i8; 128];\n\t\tlet rc = unsafe { oakcodec_decoder_transform_image_sequence_file_name(f.as_ptr(), 7, buf.as_mut_ptr(), 128) };\n\t\tassert!(rc \u003e 0);\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"frame_0007.png\"));\n\n\t\t// NULL filename -\u003e E_INVALID.\n\t\tassert_eq!(unsafe { oakcodec_decoder_get_image_sequence_digit_count(std::ptr::null()) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_decoder_get_image_sequence_index(std::ptr::null()) }, OAKCODEC_E_INVALID as i64);\n\t\tassert_eq!(unsafe { oakcodec_decoder_transform_image_sequence_file_name(std::ptr::null(), 1, buf.as_mut_ptr(), 128) }, OAKCODEC_E_INVALID);\n\t}\n\n\t#[test]\n\tfn open_errors_and_last_error_empty_handle() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tinject(FakeDecoder::new(false));\n\n\t\tlet mut h = unsafe { oakcodec_decoder_init() };\n\n\t\t// NULL filename -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, std::ptr::null(), 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Negative stream index -\u003e E_INVALID.\n\t\tlet v = media_file(\"test_video.mp4\");\n\t\tlet f = cstr(\u0026v);\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), -1) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Missing file -\u003e E_NOT_FOUND.\n\t\tlet f = cstr(\"/missing/file.mp4\");\n\t\tlet rc = unsafe { oakcodec_decoder_open(h, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_NOT_FOUND);\n\t\tlet mut err = [0i8; 256];\n\t\tunsafe { oakcodec_decoder_last_error(h, err.as_mut_ptr(), 256) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"file not found: /missing/file.mp4\"));\n\n\t\t// Empty handle -\u003e E_INVALID and empty last_error.\n\t\tlet empty = CHandle::null();\n\t\tlet rc = unsafe { oakcodec_decoder_open(empty, f.as_ptr(), 0) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_decoder_close(empty) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_decoder_is_open(empty) }, 0);\n\t\tlet rc = unsafe { oakcodec_decoder_last_error(empty, err.as_mut_ptr(), 256) };\n\t\tassert_eq!(rc, 1);\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"\"));\n\n\t\tunsafe { oakcodec_decoder_free(\u0026mut h) };\n\t\trestore();\n\t}\n}\n","traces":[{"line":90,"address":[],"length":0,"stats":{"Line":8}},{"line":91,"address":[],"length":0,"stats":{"Line":16}},{"line":92,"address":[],"length":0,"stats":{"Line":31}},{"line":93,"address":[],"length":0,"stats":{"Line":14}},{"line":94,"address":[],"length":0,"stats":{"Line":1}},{"line":96,"address":[],"length":0,"stats":{"Line":7}},{"line":97,"address":[],"length":0,"stats":{"Line":7}},{"line":98,"address":[],"length":0,"stats":{"Line":4}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":14}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":111,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":119,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":1}},{"line":121,"address":[],"length":0,"stats":{"Line":1}},{"line":122,"address":[],"length":0,"stats":{"Line":1}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":2}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":127,"address":[],"length":0,"stats":{"Line":2}},{"line":129,"address":[],"length":0,"stats":{"Line":3}},{"line":130,"address":[],"length":0,"stats":{"Line":3}},{"line":131,"address":[],"length":0,"stats":{"Line":5}},{"line":132,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":1}},{"line":135,"address":[],"length":0,"stats":{"Line":3}},{"line":136,"address":[],"length":0,"stats":{"Line":3}},{"line":137,"address":[],"length":0,"stats":{"Line":5}},{"line":138,"address":[],"length":0,"stats":{"Line":1}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":141,"address":[],"length":0,"stats":{"Line":2}},{"line":142,"address":[],"length":0,"stats":{"Line":2}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":144,"address":[],"length":0,"stats":{"Line":2}},{"line":145,"address":[],"length":0,"stats":{"Line":2}},{"line":147,"address":[],"length":0,"stats":{"Line":4}},{"line":148,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":1}},{"line":155,"address":[],"length":0,"stats":{"Line":1}},{"line":156,"address":[],"length":0,"stats":{"Line":1}},{"line":157,"address":[],"length":0,"stats":{"Line":1}},{"line":158,"address":[],"length":0,"stats":{"Line":1}},{"line":159,"address":[],"length":0,"stats":{"Line":1}},{"line":160,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":1}},{"line":162,"address":[],"length":0,"stats":{"Line":1}},{"line":164,"address":[],"length":0,"stats":{"Line":2}},{"line":165,"address":[],"length":0,"stats":{"Line":1}},{"line":166,"address":[],"length":0,"stats":{"Line":1}},{"line":167,"address":[],"length":0,"stats":{"Line":1}},{"line":168,"address":[],"length":0,"stats":{"Line":1}},{"line":171,"address":[],"length":0,"stats":{"Line":3}},{"line":172,"address":[],"length":0,"stats":{"Line":2}},{"line":173,"address":[],"length":0,"stats":{"Line":2}},{"line":174,"address":[],"length":0,"stats":{"Line":2}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":10}},{"line":181,"address":[],"length":0,"stats":{"Line":20}},{"line":189,"address":[],"length":0,"stats":{"Line":6}},{"line":190,"address":[],"length":0,"stats":{"Line":12}},{"line":191,"address":[],"length":0,"stats":{"Line":16}},{"line":192,"address":[],"length":0,"stats":{"Line":22}},{"line":194,"address":[],"length":0,"stats":{"Line":8}},{"line":195,"address":[],"length":0,"stats":{"Line":2}},{"line":198,"address":[],"length":0,"stats":{"Line":4}},{"line":199,"address":[],"length":0,"stats":{"Line":3}},{"line":200,"address":[],"length":0,"stats":{"Line":1}},{"line":202,"address":[],"length":0,"stats":{"Line":3}},{"line":203,"address":[],"length":0,"stats":{"Line":4}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":4}},{"line":208,"address":[],"length":0,"stats":{"Line":2}},{"line":209,"address":[],"length":0,"stats":{"Line":1}},{"line":210,"address":[],"length":0,"stats":{"Line":1}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":219,"address":[],"length":0,"stats":{"Line":8}},{"line":220,"address":[],"length":0,"stats":{"Line":12}},{"line":221,"address":[],"length":0,"stats":{"Line":16}},{"line":228,"address":[],"length":0,"stats":{"Line":2}},{"line":233,"address":[],"length":0,"stats":{"Line":4}},{"line":234,"address":[],"length":0,"stats":{"Line":2}},{"line":235,"address":[],"length":0,"stats":{"Line":5}},{"line":236,"address":[],"length":0,"stats":{"Line":1}},{"line":243,"address":[],"length":0,"stats":{"Line":3}},{"line":244,"address":[],"length":0,"stats":{"Line":6}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":1}},{"line":252,"address":[],"length":0,"stats":{"Line":2}},{"line":253,"address":[],"length":0,"stats":{"Line":4}},{"line":254,"address":[],"length":0,"stats":{"Line":4}},{"line":255,"address":[],"length":0,"stats":{"Line":0}},{"line":261,"address":[],"length":0,"stats":{"Line":1}},{"line":262,"address":[],"length":0,"stats":{"Line":2}},{"line":263,"address":[],"length":0,"stats":{"Line":2}},{"line":264,"address":[],"length":0,"stats":{"Line":0}},{"line":271,"address":[],"length":0,"stats":{"Line":3}},{"line":276,"address":[],"length":0,"stats":{"Line":6}},{"line":277,"address":[],"length":0,"stats":{"Line":6}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":280,"address":[],"length":0,"stats":{"Line":10}},{"line":281,"address":[],"length":0,"stats":{"Line":3}},{"line":282,"address":[],"length":0,"stats":{"Line":2}},{"line":283,"address":[],"length":0,"stats":{"Line":4}},{"line":284,"address":[],"length":0,"stats":{"Line":5}},{"line":285,"address":[],"length":0,"stats":{"Line":3}},{"line":286,"address":[],"length":0,"stats":{"Line":1}},{"line":293,"address":[],"length":0,"stats":{"Line":1}},{"line":298,"address":[],"length":0,"stats":{"Line":2}},{"line":299,"address":[],"length":0,"stats":{"Line":2}},{"line":300,"address":[],"length":0,"stats":{"Line":0}},{"line":302,"address":[],"length":0,"stats":{"Line":5}},{"line":303,"address":[],"length":0,"stats":{"Line":2}},{"line":304,"address":[],"length":0,"stats":{"Line":1}},{"line":305,"address":[],"length":0,"stats":{"Line":2}},{"line":306,"address":[],"length":0,"stats":{"Line":2}},{"line":307,"address":[],"length":0,"stats":{"Line":3}},{"line":308,"address":[],"length":0,"stats":{"Line":1}},{"line":316,"address":[],"length":0,"stats":{"Line":8}},{"line":317,"address":[],"length":0,"stats":{"Line":16}},{"line":318,"address":[],"length":0,"stats":{"Line":24}},{"line":319,"address":[],"length":0,"stats":{"Line":16}},{"line":320,"address":[],"length":0,"stats":{"Line":8}},{"line":321,"address":[],"length":0,"stats":{"Line":8}},{"line":322,"address":[],"length":0,"stats":{"Line":8}},{"line":323,"address":[],"length":0,"stats":{"Line":8}},{"line":330,"address":[],"length":0,"stats":{"Line":10}},{"line":331,"address":[],"length":0,"stats":{"Line":30}},{"line":337,"address":[],"length":0,"stats":{"Line":10}},{"line":342,"address":[],"length":0,"stats":{"Line":20}},{"line":343,"address":[],"length":0,"stats":{"Line":50}},{"line":344,"address":[],"length":0,"stats":{"Line":17}},{"line":345,"address":[],"length":0,"stats":{"Line":16}},{"line":346,"address":[],"length":0,"stats":{"Line":1}},{"line":348,"address":[],"length":0,"stats":{"Line":8}},{"line":349,"address":[],"length":0,"stats":{"Line":1}},{"line":351,"address":[],"length":0,"stats":{"Line":28}},{"line":353,"address":[],"length":0,"stats":{"Line":9}},{"line":354,"address":[],"length":0,"stats":{"Line":2}},{"line":355,"address":[],"length":0,"stats":{"Line":1}},{"line":357,"address":[],"length":0,"stats":{"Line":0}},{"line":358,"address":[],"length":0,"stats":{"Line":0}},{"line":361,"address":[],"length":0,"stats":{"Line":6}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":363,"address":[],"length":0,"stats":{"Line":1}},{"line":366,"address":[],"length":0,"stats":{"Line":15}},{"line":367,"address":[],"length":0,"stats":{"Line":10}},{"line":369,"address":[],"length":0,"stats":{"Line":0}},{"line":370,"address":[],"length":0,"stats":{"Line":0}},{"line":374,"address":[],"length":0,"stats":{"Line":10}},{"line":375,"address":[],"length":0,"stats":{"Line":10}},{"line":377,"address":[],"length":0,"stats":{"Line":0}},{"line":378,"address":[],"length":0,"stats":{"Line":0}},{"line":382,"address":[],"length":0,"stats":{"Line":30}},{"line":383,"address":[],"length":0,"stats":{"Line":15}},{"line":384,"address":[],"length":0,"stats":{"Line":0}},{"line":385,"address":[],"length":0,"stats":{"Line":0}},{"line":388,"address":[],"length":0,"stats":{"Line":10}},{"line":389,"address":[],"length":0,"stats":{"Line":10}},{"line":390,"address":[],"length":0,"stats":{"Line":10}},{"line":391,"address":[],"length":0,"stats":{"Line":5}},{"line":392,"address":[],"length":0,"stats":{"Line":5}},{"line":393,"address":[],"length":0,"stats":{"Line":5}},{"line":399,"address":[],"length":0,"stats":{"Line":2}},{"line":400,"address":[],"length":0,"stats":{"Line":4}},{"line":401,"address":[],"length":0,"stats":{"Line":10}},{"line":402,"address":[],"length":0,"stats":{"Line":4}},{"line":403,"address":[],"length":0,"stats":{"Line":1}},{"line":404,"address":[],"length":0,"stats":{"Line":3}},{"line":405,"address":[],"length":0,"stats":{"Line":1}},{"line":408,"address":[],"length":0,"stats":{"Line":1}},{"line":409,"address":[],"length":0,"stats":{"Line":1}},{"line":415,"address":[],"length":0,"stats":{"Line":4}},{"line":416,"address":[],"length":0,"stats":{"Line":8}},{"line":417,"address":[],"length":0,"stats":{"Line":3}},{"line":418,"address":[],"length":0,"stats":{"Line":12}},{"line":419,"address":[],"length":0,"stats":{"Line":3}},{"line":420,"address":[],"length":0,"stats":{"Line":1}},{"line":422,"address":[],"length":0,"stats":{"Line":2}},{"line":425,"address":[],"length":0,"stats":{"Line":1}},{"line":438,"address":[],"length":0,"stats":{"Line":2}},{"line":443,"address":[],"length":0,"stats":{"Line":4}},{"line":444,"address":[],"length":0,"stats":{"Line":10}},{"line":445,"address":[],"length":0,"stats":{"Line":2}},{"line":446,"address":[],"length":0,"stats":{"Line":8}},{"line":447,"address":[],"length":0,"stats":{"Line":6}},{"line":449,"address":[],"length":0,"stats":{"Line":0}},{"line":451,"address":[],"length":0,"stats":{"Line":4}},{"line":453,"address":[],"length":0,"stats":{"Line":4}},{"line":455,"address":[],"length":0,"stats":{"Line":4}},{"line":456,"address":[],"length":0,"stats":{"Line":8}},{"line":457,"address":[],"length":0,"stats":{"Line":2}},{"line":458,"address":[],"length":0,"stats":{"Line":2}},{"line":459,"address":[],"length":0,"stats":{"Line":2}},{"line":460,"address":[],"length":0,"stats":{"Line":2}},{"line":461,"address":[],"length":0,"stats":{"Line":2}},{"line":462,"address":[],"length":0,"stats":{"Line":2}},{"line":463,"address":[],"length":0,"stats":{"Line":2}},{"line":465,"address":[],"length":0,"stats":{"Line":4}},{"line":466,"address":[],"length":0,"stats":{"Line":4}},{"line":467,"address":[],"length":0,"stats":{"Line":3}},{"line":469,"address":[],"length":0,"stats":{"Line":4}},{"line":470,"address":[],"length":0,"stats":{"Line":1}},{"line":474,"address":[],"length":0,"stats":{"Line":0}},{"line":475,"address":[],"length":0,"stats":{"Line":0}},{"line":490,"address":[],"length":0,"stats":{"Line":6}},{"line":501,"address":[],"length":0,"stats":{"Line":12}},{"line":503,"address":[],"length":0,"stats":{"Line":6}},{"line":504,"address":[],"length":0,"stats":{"Line":6}},{"line":505,"address":[],"length":0,"stats":{"Line":6}},{"line":506,"address":[],"length":0,"stats":{"Line":6}},{"line":507,"address":[],"length":0,"stats":{"Line":6}},{"line":508,"address":[],"length":0,"stats":{"Line":6}},{"line":509,"address":[],"length":0,"stats":{"Line":6}},{"line":510,"address":[],"length":0,"stats":{"Line":6}},{"line":511,"address":[],"length":0,"stats":{"Line":6}},{"line":512,"address":[],"length":0,"stats":{"Line":6}},{"line":518,"address":[],"length":0,"stats":{"Line":6}},{"line":529,"address":[],"length":0,"stats":{"Line":11}},{"line":530,"address":[],"length":0,"stats":{"Line":10}},{"line":531,"address":[],"length":0,"stats":{"Line":1}},{"line":533,"address":[],"length":0,"stats":{"Line":15}},{"line":534,"address":[],"length":0,"stats":{"Line":2}},{"line":537,"address":[],"length":0,"stats":{"Line":2}},{"line":538,"address":[],"length":0,"stats":{"Line":12}},{"line":539,"address":[],"length":0,"stats":{"Line":7}},{"line":540,"address":[],"length":0,"stats":{"Line":1}},{"line":542,"address":[],"length":0,"stats":{"Line":4}},{"line":546,"address":[],"length":0,"stats":{"Line":6}},{"line":547,"address":[],"length":0,"stats":{"Line":6}},{"line":549,"address":[],"length":0,"stats":{"Line":4}},{"line":550,"address":[],"length":0,"stats":{"Line":6}},{"line":552,"address":[],"length":0,"stats":{"Line":12}},{"line":553,"address":[],"length":0,"stats":{"Line":4}},{"line":555,"address":[],"length":0,"stats":{"Line":0}},{"line":556,"address":[],"length":0,"stats":{"Line":0}},{"line":560,"address":[],"length":0,"stats":{"Line":2}},{"line":564,"address":[],"length":0,"stats":{"Line":6}},{"line":565,"address":[],"length":0,"stats":{"Line":1}},{"line":568,"address":[],"length":0,"stats":{"Line":2}},{"line":569,"address":[],"length":0,"stats":{"Line":1}},{"line":570,"address":[],"length":0,"stats":{"Line":1}},{"line":571,"address":[],"length":0,"stats":{"Line":1}},{"line":574,"address":[],"length":0,"stats":{"Line":0}},{"line":575,"address":[],"length":0,"stats":{"Line":0}},{"line":583,"address":[],"length":0,"stats":{"Line":6}},{"line":592,"address":[],"length":0,"stats":{"Line":12}},{"line":594,"address":[],"length":0,"stats":{"Line":6}},{"line":595,"address":[],"length":0,"stats":{"Line":6}},{"line":596,"address":[],"length":0,"stats":{"Line":6}},{"line":597,"address":[],"length":0,"stats":{"Line":6}},{"line":598,"address":[],"length":0,"stats":{"Line":6}},{"line":599,"address":[],"length":0,"stats":{"Line":6}},{"line":600,"address":[],"length":0,"stats":{"Line":6}},{"line":601,"address":[],"length":0,"stats":{"Line":6}},{"line":607,"address":[],"length":0,"stats":{"Line":6}},{"line":616,"address":[],"length":0,"stats":{"Line":30}},{"line":617,"address":[],"length":0,"stats":{"Line":18}},{"line":618,"address":[],"length":0,"stats":{"Line":2}},{"line":621,"address":[],"length":0,"stats":{"Line":3}},{"line":622,"address":[],"length":0,"stats":{"Line":16}},{"line":623,"address":[],"length":0,"stats":{"Line":10}},{"line":624,"address":[],"length":0,"stats":{"Line":1}},{"line":626,"address":[],"length":0,"stats":{"Line":6}},{"line":629,"address":[],"length":0,"stats":{"Line":12}},{"line":630,"address":[],"length":0,"stats":{"Line":9}},{"line":631,"address":[],"length":0,"stats":{"Line":18}},{"line":632,"address":[],"length":0,"stats":{"Line":30}},{"line":633,"address":[],"length":0,"stats":{"Line":18}},{"line":638,"address":[],"length":0,"stats":{"Line":9}},{"line":639,"address":[],"length":0,"stats":{"Line":2}},{"line":641,"address":[],"length":0,"stats":{"Line":1}},{"line":643,"address":[],"length":0,"stats":{"Line":12}},{"line":644,"address":[],"length":0,"stats":{"Line":6}},{"line":645,"address":[],"length":0,"stats":{"Line":6}},{"line":646,"address":[],"length":0,"stats":{"Line":6}},{"line":647,"address":[],"length":0,"stats":{"Line":3}},{"line":648,"address":[],"length":0,"stats":{"Line":3}},{"line":651,"address":[],"length":0,"stats":{"Line":3}},{"line":652,"address":[],"length":0,"stats":{"Line":1}},{"line":654,"address":[],"length":0,"stats":{"Line":6}},{"line":655,"address":[],"length":0,"stats":{"Line":1}},{"line":657,"address":[],"length":0,"stats":{"Line":3}},{"line":659,"address":[],"length":0,"stats":{"Line":2}},{"line":660,"address":[],"length":0,"stats":{"Line":1}},{"line":662,"address":[],"length":0,"stats":{"Line":1}},{"line":670,"address":[],"length":0,"stats":{"Line":2}},{"line":673,"address":[],"length":0,"stats":{"Line":4}},{"line":674,"address":[],"length":0,"stats":{"Line":3}},{"line":675,"address":[],"length":0,"stats":{"Line":1}},{"line":681,"address":[],"length":0,"stats":{"Line":2}},{"line":684,"address":[],"length":0,"stats":{"Line":4}},{"line":685,"address":[],"length":0,"stats":{"Line":3}},{"line":686,"address":[],"length":0,"stats":{"Line":1}},{"line":692,"address":[],"length":0,"stats":{"Line":2}},{"line":698,"address":[],"length":0,"stats":{"Line":4}},{"line":699,"address":[],"length":0,"stats":{"Line":1}},{"line":700,"address":[],"length":0,"stats":{"Line":4}},{"line":701,"address":[],"length":0,"stats":{"Line":4}},{"line":703,"address":[],"length":0,"stats":{"Line":1}},{"line":709,"address":[],"length":0,"stats":{"Line":4}},{"line":714,"address":[],"length":0,"stats":{"Line":8}},{"line":715,"address":[],"length":0,"stats":{"Line":3}},{"line":716,"address":[],"length":0,"stats":{"Line":12}},{"line":717,"address":[],"length":0,"stats":{"Line":12}},{"line":719,"address":[],"length":0,"stats":{"Line":4}}],"covered":295,"coverable":314},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","encoder.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/encoder.h` exports.\n//!\n//! Complete inventory: init / free / set_video_option / open / write_video\n//! / write_audio / write_subtitle / flush / last_error /\n//! get_desired_pixel_format / export_format_get_extension /\n//! encoding_generate_matrix.\n//!\n//! # CPP-PARITY\n//! The C++ `c_api/encoder.cpp` box holds an `olive::Encoder` plus the\n//! flattened `EncodingParams`; the Rust equivalent boxes `Mutex\u003cEncoderBox\u003e`\n//! and carries an extra `last_error` field because the Rust `Encoder` trait\n//! has no `get_error()` (the C++ reads the message off the encoder). The\n//! `oakcodec_encoding_params` POD below mirrors the header verbatim; note\n//! that the crate's `EncodingParams::video_pixel_format` / `audio_sample_format`\n//! are typed enums, so `to_native` converts the int fields.\n\nuse std::ffi::{c_char, c_int};\nuse std::sync::{Arc, Mutex};\n\nuse oakcore_rs::PixelFormat;\n\nuse crate::encodingparams::EncodingParams;\nuse crate::encoder::Encoder;\nuse crate::handle::{self, CHandle};\n\n/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/\n/// encoder.h` (all fields; a zeroed struct describes an all-tracks-disabled\n/// configuration). Field names and order mirror the header verbatim.\n#[allow(missing_docs)]\n#[repr(C)]\npub struct oakcodec_encoding_params {\n\tpub filename: [u8; 1024],\n\tpub format: c_int,\n\tpub video_enabled: c_int,\n\tpub video_codec: c_int,\n\tpub video_width: c_int,\n\tpub video_height: c_int,\n\tpub video_time_base_num: c_int,\n\tpub video_time_base_den: c_int,\n\tpub video_pixel_format: c_int,\n\tpub video_interlacing: c_int,\n\tpub video_pixel_aspect_num: c_int,\n\tpub video_pixel_aspect_den: c_int,\n\tpub video_bit_rate: i64,\n\tpub video_min_bit_rate: i64,\n\tpub video_max_bit_rate: i64,\n\tpub video_buffer_size: i64,\n\tpub video_threads: c_int,\n\tpub video_pix_fmt: [u8; 64],\n\tpub video_is_image_sequence: c_int,\n\tpub video_scaling_method: c_int,\n\tpub audio_enabled: c_int,\n\tpub audio_codec: c_int,\n\tpub audio_sample_rate: c_int,\n\tpub audio_channel_layout: u64,\n\tpub audio_sample_format: c_int,\n\tpub audio_bit_rate: i64,\n\tpub subtitles_enabled: c_int,\n\tpub subtitles_codec: c_int,\n\tpub subtitles_are_sidecar: c_int,\n\tpub subtitles_sidecar_format: c_int,\n\tpub color_transform_output: [u8; 256],\n\tpub export_length_num: c_int,\n\tpub export_length_den: c_int,\n\tpub has_custom_range: c_int,\n\tpub custom_range_in_num: i64,\n\tpub custom_range_in_den: i64,\n\tpub custom_range_out_num: i64,\n\tpub custom_range_out_den: i64,\n}\n\n/// Box behind an encoder handle (`EncoderBox` in `c_api/encoder.cpp`).\nstruct EncoderBox {\n\tencoder: Option\u003cArc\u003cdyn Encoder\u003e\u003e,\n\tparams: EncodingParams,\n\t/// Per-codec video options set via `oakcodec_encoder_set_video_option`\n\t/// between init and open. Kept here (not in [`EncodingParams`], which\n\t/// is a byte-exact mirror of the C POD).\n\tvideo_opts: Vec\u003c(String, String)\u003e,\n\topen: bool,\n\tflushed: bool,\n\t/// Last error detail (the C++ reads it off the encoder's `get_error`).\n\tlast_error: String,\n}\n\n/// Convert an `OakPixelFormat` int code to a [`PixelFormat`].\nfn pixel_format_from_i32(v: c_int) -\u003e PixelFormat {\n\tmatch v {\n\t\t0 =\u003e PixelFormat::U8,\n\t\t1 =\u003e PixelFormat::U10,\n\t\t2 =\u003e PixelFormat::U16,\n\t\t3 =\u003e PixelFormat::F16,\n\t\t4 =\u003e PixelFormat::F32,\n\t\t_ =\u003e PixelFormat::Invalid,\n\t}\n}\n\n/// Flatten the C POD into the crate's [`EncodingParams`]\n/// (`to_native` in `c_api/encoder.cpp`).\nfn to_native(p: \u0026oakcodec_encoding_params) -\u003e EncodingParams {\n\tlet mut n = EncodingParams::default();\n\tn.filename = p.filename;\n\tn.format = p.format;\n\n\tn.video_enabled = p.video_enabled;\n\tn.video_codec = p.video_codec;\n\tn.video_width = p.video_width;\n\tn.video_height = p.video_height;\n\tn.video_time_base_num = p.video_time_base_num;\n\tn.video_time_base_den = p.video_time_base_den;\n\tn.video_pixel_format = pixel_format_from_i32(p.video_pixel_format);\n\tn.video_interlacing = p.video_interlacing;\n\tn.video_pixel_aspect_num = p.video_pixel_aspect_num;\n\tn.video_pixel_aspect_den = p.video_pixel_aspect_den;\n\tn.video_bit_rate = p.video_bit_rate;\n\tn.video_min_bit_rate = p.video_min_bit_rate;\n\tn.video_max_bit_rate = p.video_max_bit_rate;\n\tn.video_buffer_size = p.video_buffer_size;\n\tn.video_threads = p.video_threads;\n\tn.video_pix_fmt = p.video_pix_fmt;\n\tn.video_is_image_sequence = p.video_is_image_sequence;\n\tn.video_scaling_method = crate::encodingparams::scaling_from_i32(p.video_scaling_method);\n\n\tn.audio_enabled = p.audio_enabled;\n\tn.audio_codec = p.audio_codec;\n\tn.audio_sample_rate = p.audio_sample_rate;\n\tn.audio_channel_layout = p.audio_channel_layout;\n\tn.audio_sample_format = crate::encodingparams::sample_format_from_i32(p.audio_sample_format);\n\tn.audio_bit_rate = p.audio_bit_rate;\n\n\tn.subtitles_enabled = p.subtitles_enabled;\n\tn.subtitles_codec = p.subtitles_codec;\n\tn.subtitles_are_sidecar = p.subtitles_are_sidecar;\n\tn.subtitles_sidecar_format = p.subtitles_sidecar_format;\n\n\tn.color_transform_output = p.color_transform_output;\n\tn.export_length_num = p.export_length_num;\n\tn.export_length_den = p.export_length_den;\n\tn.has_custom_range = p.has_custom_range;\n\tn.custom_range_in_num = p.custom_range_in_num;\n\tn.custom_range_in_den = p.custom_range_in_den;\n\tn.custom_range_out_num = p.custom_range_out_num;\n\tn.custom_range_out_den = p.custom_range_out_den;\n\tn\n}\n\n/// `EncodingParams::is_valid` — the C++ `src/codec/src/encoder.h` checks\n/// only that at least one track is enabled.\nfn is_valid(p: \u0026EncodingParams) -\u003e bool {\n\tp.video_enabled != 0 || p.audio_enabled != 0 || p.subtitles_enabled != 0\n}\n\n/// `oakcodec_encoder_init`: create an encoder for `params` (count 1);\n/// empty handle when the configuration is invalid.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_init(params: *const oakcodec_encoding_params) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tif params.is_null() {\n\t\t\treturn Ok(CHandle::null());\n\t\t}\n\t\tlet native = to_native(unsafe { \u0026*params });\n\t\tif !is_valid(\u0026native) {\n\t\t\treturn Ok(CHandle::null());\n\t\t}\n\t\tOk(handle::make_owned(Mutex::new(EncoderBox {\n\t\t\tencoder: None,\n\t\t\tparams: native,\n\t\t\tvideo_opts: Vec::new(),\n\t\t\topen: false,\n\t\t\tflushed: false,\n\t\t\tlast_error: String::new(),\n\t\t})))\n\t})\n}\n\n/// `oakcodec_encoder_free`: NULL/empty no-op; nulls `ctx` afterwards.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_free(encoder: *mut CHandle) {\n\thandle::guard_void(|| super::free_handle(encoder));\n}\n\n/// `oakcodec_encoder_set_video_option`: set a per-codec video option\n/// (e.g. \"crf\"); only valid between init and open.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_set_video_option(\n\tencoder: CHandle,\n\tkey: *const c_char,\n\tvalue: *const c_char,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet key = match crate::ffi::c_str(key) {\n\t\t\tSome(k) =\u003e k,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet mut b = b.lock().unwrap();\n\t\tif b.open {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tlet value = crate::ffi::c_str(value).unwrap_or_default();\n\t\t// `EncodingParams::set_video_option` replaces an existing key.\n\t\tb.video_opts.retain(|(k, _)| k != \u0026key);\n\t\tb.video_opts.push((key, value));\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_encoder_open`: create the encoder for the configured params,\n/// apply the video options and open the output.\n///\n/// # CPP-PARITY\n/// The C++ `open()` calls `create_from_params` (which applies the options\n/// internally) then `open()`. The Rust trait separates `configure`, so it\n/// is invoked between the two; the box's `video_opts` (set between init\n/// and open) are stored for future wiring but not yet passed to\n/// `configure` (no trait channel carries them in the interim).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_open(encoder: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet mut b = b.lock().unwrap();\n\t\tif b.open {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tlet e = match crate::encoder::create_from_params(\u0026b.params) {\n\t\t\tSome(e) =\u003e e,\n\t\t\tNone =\u003e {\n\t\t\t\tb.last_error = \"failed to create encoder\".to_string();\n\t\t\t\treturn Err(crate::error::Error::Failed(\"failed to create encoder\".to_string()));\n\t\t\t}\n\t\t};\n\t\tif e.configure(\u0026b.params).is_err() {\n\t\t\tb.last_error = \"failed to configure encoder\".to_string();\n\t\t\treturn Err(crate::error::Error::Failed(\"failed to configure encoder\".to_string()));\n\t\t}\n\t\tif e.open().is_err() {\n\t\t\tb.last_error = \"failed to open stream\".to_string();\n\t\t\treturn Err(crate::error::Error::Failed(\"failed to open stream\".to_string()));\n\t\t}\n\t\tb.encoder = Some(e);\n\t\tb.open = true;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_encoder_write_video`: encode one video frame.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_write_video(encoder: CHandle, frame: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet f = super::get_box::\u003cMutex\u003ccrate::frame::Frame\u003e\u003e(\u0026frame)\n\t\t\t.ok_or(crate::error::Error::Invalid)?;\n\t\tlet e = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.flushed || b.encoder.is_none() {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tb.encoder.as_ref().unwrap().clone()\n\t\t};\n\t\tlet f = f.lock().unwrap();\n\t\te.write_video(\u0026f)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"write_video failed\".to_string()))\n\t})\n}\n\n/// `oakcodec_encoder_write_audio`: encode interleaved float audio samples.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_write_audio(\n\tencoder: CHandle,\n\tsamples: *const f32,\n\tframe_count: c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tif (samples.is_null() \u0026\u0026 frame_count \u003e 0) || frame_count \u003c 0 {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet (slice, enc) = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.flushed || b.encoder.is_none() {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tlet channels = b.params.audio_channel_layout.count_ones();\n\t\t\tif channels == 0 {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tlet sample_count = (frame_count as usize).wrapping_mul(channels as usize);\n\t\t\tlet slice: \u0026[f32] = if samples.is_null() {\n\t\t\t\t\u0026[]\n\t\t\t} else {\n\t\t\t\t// SAFETY: the caller guarantees `samples` holds\n\t\t\t\t// `frame_count * channels` floats.\n\t\t\t\tunsafe { std::slice::from_raw_parts(samples, sample_count) }\n\t\t\t};\n\t\t\tlet e = b.encoder.as_ref().unwrap().clone();\n\t\t\t(slice, e)\n\t\t};\n\t\tenc.write_audio(slice, frame_count)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"write_audio failed\".to_string()))\n\t})\n}\n\n/// `oakcodec_encoder_write_subtitle`: encode one subtitle entry.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_write_subtitle(\n\tencoder: CHandle,\n\ttext: *const c_char,\n\tin_seconds: f64,\n\tout_seconds: f64,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet text = match crate::ffi::c_str(text) {\n\t\t\tSome(t) =\u003e t,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet e = {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tif !b.open || b.flushed || b.encoder.is_none() {\n\t\t\t\treturn Err(crate::error::Error::State);\n\t\t\t}\n\t\t\tb.encoder.as_ref().unwrap().clone()\n\t\t};\n\t\te.write_subtitle(\u0026text, in_seconds, out_seconds)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"write_subtitle failed\".to_string()))\n\t})\n}\n\n/// `oakcodec_encoder_flush`: flush the encoders, write the trailer and\n/// close the file. Idempotent.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_flush(encoder: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet b = super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder).ok_or(crate::error::Error::Invalid)?;\n\t\tlet mut b = b.lock().unwrap();\n\t\tif !b.open {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tif b.flushed {\n\t\t\treturn Ok(());\n\t\t}\n\t\tlet e = b.encoder.as_ref().unwrap().clone();\n\t\t// The C++ ignores the close() result; the Rust interim surfaces it.\n\t\te.close()\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"close failed\".to_string()))?;\n\t\tb.flushed = true;\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_encoder_last_error` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_last_error(\n\tencoder: CHandle,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| match super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder) {\n\t\tSome(b) =\u003e {\n\t\t\tlet b = b.lock().unwrap();\n\t\t\tsuper::string_out(\u0026b.last_error, buf, buf_size)\n\t\t}\n\t\tNone =\u003e super::string_out(\"\", buf, buf_size),\n\t})\n}\n\n/// `oakcodec_encoder_get_desired_pixel_format`: the pixel format the\n/// encoder wants frames in, or -1 when unknown; `OAKCODEC_E_INVALID` for\n/// an empty/invalid encoder.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoder_get_desired_pixel_format(encoder: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet b = match super::get_box::\u003cMutex\u003cEncoderBox\u003e\u003e(\u0026encoder) {\n\t\t\tSome(b) =\u003e b,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet e = match \u0026b.lock().unwrap().encoder {\n\t\t\tSome(e) =\u003e e.clone(),\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tmatch e.desired_pixel_format() {\n\t\t\tSome(p) =\u003e p as c_int,\n\t\t\tNone =\u003e -1,\n\t\t}\n\t})\n}\n\n/// `oakcodec_export_format_get_extension` (two-stage); unknown formats\n/// yield the empty string.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_export_format_get_extension(\n\tformat: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet ext = match crate::exportformat::Format::from_i32(format) {\n\t\t\tSome(f) =\u003e crate::exportformat::Format::get_extension(f),\n\t\t\tNone =\u003e String::new(),\n\t\t};\n\t\tsuper::string_out(\u0026ext, buf, buf_size)\n\t})\n}\n\n/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling\n/// method, row-major 4x4 `double` into `out_matrix[16]`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_encoding_generate_matrix(\n\tmethod: c_int,\n\tsrc_width: c_int,\n\tsrc_height: c_int,\n\tdst_width: c_int,\n\tdst_height: c_int,\n\tout_matrix: *mut f64,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out_matrix.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet mut m = [0.0f64; 16];\n\t\tcrate::encodingparams::EncodingParams::generate_matrix(\n\t\t\tcrate::encodingparams::scaling_from_i32(method),\n\t\t\tsrc_width,\n\t\t\tsrc_height,\n\t\t\tdst_width,\n\t\t\tdst_height,\n\t\t\t\u0026mut m,\n\t\t);\n\t\t// SAFETY: the caller guarantees `out_matrix` holds 16 doubles.\n\t\tunsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) };\n\t\tOk(())\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\tuse crate::encoder::set_test_encoders;\n\tuse crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};\n\tuse crate::ffi::frame::{oakcodec_frame_allocate, oakcodec_frame_free, oakcodec_frame_init_with_params};\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn zeroed_params() -\u003e oakcodec_encoding_params {\n\t\tunsafe { std::mem::zeroed() }\n\t}\n\n\tfn valid_params() -\u003e oakcodec_encoding_params {\n\t\tlet mut p = zeroed_params();\n\t\tp.filename = {\n\t\t\tlet mut f = [0u8; 1024];\n\t\t\tlet name = b\"out.mp4\";\n\t\t\tf[..name.len()].copy_from_slice(name);\n\t\t\tf\n\t\t};\n\t\tp.format = 2; // MPEG-4\n\t\tp.video_enabled = 1;\n\t\tp.video_codec = 3;\n\t\tp.video_width = 1920;\n\t\tp.video_height = 1080;\n\t\tp.video_time_base_num = 1;\n\t\tp.video_time_base_den = 30;\n\t\tp.video_pixel_format = 0; // U8\n\t\tp.audio_enabled = 1;\n\t\tp.audio_codec = 4;\n\t\tp.audio_sample_rate = 48000;\n\t\tp.audio_channel_layout = 0x3;\n\t\tp.audio_sample_format = 10; // f32 packed\n\t\tp\n\t}\n\n\t/// Fake encoder that accepts every operation.\n\tstruct FakeEncoder {\n\t\tid: \u0026'static str,\n\t}\n\n\timpl Encoder for FakeEncoder {\n\t\tfn id(\u0026self) -\u003e String {\n\t\t\tself.id.to_string()\n\t\t}\n\t\tfn supports_video(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\t\tfn supports_audio(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\t\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\t\ttrue\n\t\t}\n\t\tfn configure(\u0026self, _p: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_video(\u0026self, _frame: \u0026crate::frame::Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_audio(\u0026self, _samples: \u0026[f32], _frame_count: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn write_subtitle(\n\t\t\t\u0026self,\n\t\t\t_text: \u0026str,\n\t\t\t_in_seconds: f64,\n\t\t\t_out_seconds: f64,\n\t\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t\tOk(())\n\t\t}\n\t\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e {\n\t\t\tSome(PixelFormat::U8)\n\t\t}\n\t\tfn desired_sample_format(\u0026self) -\u003e Option\u003coakcore_rs::SampleFormat\u003e {\n\t\t\tNone\n\t\t}\n\t\tfn filename(\u0026self) -\u003e String {\n\t\t\t\"out.mp4\".to_string()\n\t\t}\n\t}\n\n\t#[test]\n\tfn init_open_write_flush_golden() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\tlet p = valid_params();\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\n\t\t// set_video_option between init and open.\n\t\tlet key = cstr(\"crf\");\n\t\tlet val = cstr(\"18\");\n\t\tlet rc = unsafe { oakcodec_encoder_set_video_option(h, key.as_ptr(), val.as_ptr()) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\tlet rc = unsafe { oakcodec_encoder_open(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// write_video with a real frame handle.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(16, 16) };\n\t\tlet mut fh = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(fh) }, crate::error::OAKCODEC_OK);\n\t\tlet rc = unsafe { oakcodec_encoder_write_video(h, fh) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// write_audio: stereo interleaved floats.\n\t\tlet mut samples = [0f32; 64];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// write_subtitle.\n\t\tlet text = cstr(\"hello\");\n\t\tlet rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 2.5) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// desired pixel format from the fake.\n\t\tassert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(h) }, 0); // U8\n\n\t\t// flush is idempotent.\n\t\tlet rc = unsafe { oakcodec_encoder_flush(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tlet rc = unsafe { oakcodec_encoder_flush(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\n\t\t// Writes after flush -\u003e E_STATE.\n\t\tlet rc = unsafe { oakcodec_encoder_write_video(h, fh) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 1.0) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut fh) };\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tassert!(h.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn init_invalid_config_and_null_params() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(Vec::new());\n\n\t\t// NULL params -\u003e empty handle.\n\t\tlet mut h = unsafe { oakcodec_encoder_init(std::ptr::null()) };\n\t\tassert!(h.is_null());\n\n\t\t// All tracks disabled -\u003e empty handle (is_valid).\n\t\tlet p = zeroed_params();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(h.is_null());\n\n\t\t// Audio-only config is valid.\n\t\tlet mut p = zeroed_params();\n\t\tp.format = 7; // WAV\n\t\tp.audio_enabled = 1;\n\t\tp.audio_sample_rate = 44100;\n\t\tp.audio_channel_layout = 0x4;\n\t\tp.audio_sample_format = 10;\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\t#[test]\n\tfn open_errors_and_state_machine() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\t// Production path: an unknown export format cannot create an encoder.\n\t\tset_test_encoders(Vec::new());\n\n\t\tlet mut p = valid_params();\n\t\tp.format = 99; // unknown format -\u003e create_from_params returns None\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\n\t\tlet rc = unsafe { oakcodec_encoder_open(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_E_FAILED);\n\t\tlet mut err = [0i8; 128];\n\t\tunsafe { oakcodec_encoder_last_error(h, err.as_mut_ptr(), 128) };\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"failed to create encoder\"));\n\n\t\t// Empty handle -\u003e E_INVALID; last_error empty.\n\t\tlet empty = CHandle::null();\n\t\tassert_eq!(unsafe { oakcodec_encoder_open(empty) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_encoder_set_video_option(empty, cstr(\"crf\").as_ptr(), cstr(\"18\").as_ptr()) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(empty) }, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_encoder_last_error(empty, err.as_mut_ptr(), 128) };\n\t\tassert_eq!(rc, 1);\n\t\tassert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(\"\"));\n\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t}\n\n\t#[test]\n\tfn state_and_argument_errors_with_fake() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\tlet p = valid_params();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\n\t\t// set_video_option with a NULL key -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_encoder_set_video_option(h, std::ptr::null(), std::ptr::null()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Writes before open -\u003e E_STATE.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(4, 4) };\n\t\tlet mut fh = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tlet rc = unsafe { oakcodec_encoder_write_video(h, fh) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\tlet rc = unsafe { oakcodec_encoder_flush(h) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\t\t// Null samples with a positive count is an argument error (checked\n\t\t// before the state, matching the C++ validation order).\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\t// Valid args before open -\u003e E_STATE.\n\t\tlet mut pre = [0f32; 8];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, pre.as_ptr(), 4) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut fh) };\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn write_audio_argument_validation() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\tlet p = valid_params();\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);\n\n\t\t// NULL samples with a positive frame count -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// Negative frame count -\u003e E_INVALID.\n\t\tlet samples = [0f32; 8];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), -1) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn write_audio_zero_channels_is_state() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tset_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: \"fake\" })]);\n\n\t\t// Audio enabled but empty channel layout -\u003e E_STATE at write time.\n\t\tlet mut p = zeroed_params();\n\t\tp.format = 7;\n\t\tp.audio_enabled = 1;\n\t\tp.audio_sample_rate = 44100;\n\t\tp.audio_channel_layout = 0;\n\t\tp.audio_sample_format = 10;\n\t\tlet mut h = unsafe { oakcodec_encoder_init(\u0026p) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);\n\t\tlet mut samples = [0f32; 8];\n\t\tlet rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 4) };\n\t\tassert_eq!(rc, OAKCODEC_E_STATE);\n\n\t\tunsafe { oakcodec_encoder_free(\u0026mut h) };\n\t\tset_test_encoders(Vec::new());\n\t}\n\n\t#[test]\n\tfn export_format_extension_and_generate_matrix() {\n\t\tlet mut buf = [0i8; 64];\n\t\tlet rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 4); // \"mp4\" + NUL\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"mp4\"));\n\n\t\t// Unknown format -\u003e empty string (size 1 for the NUL).\n\t\tlet rc = unsafe { oakcodec_export_format_get_extension(99, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 1);\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"\"));\n\n\t\t// Truncation rule: small buffer writes buf_size-1 chars + NUL.\n\t\tlet rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 3) };\n\t\tassert_eq!(rc, 4); // required size unchanged\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"mp\"));\n\n\t\t// generate_matrix: Stretch (1) is the identity.\n\t\tlet mut m = [9.0f64; 16];\n\t\tlet rc = unsafe { oakcodec_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(m[0], 1.0);\n\t\tassert_eq!(m[5], 1.0);\n\t\tassert_eq!(m[10], 1.0);\n\t\tassert_eq!(m[15], 1.0);\n\n\t\t// Fit (0) with a square source into a 2:1 destination scales x.\n\t\tlet mut m = [0.0f64; 16];\n\t\tlet rc = unsafe { oakcodec_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert!((m[0] - 0.5).abs() \u003c 1e-9);\n\t\tassert_eq!(m[5], 1.0);\n\n\t\t// NULL out_matrix -\u003e E_INVALID.\n\t\tlet rc = unsafe { oakcodec_encoding_generate_matrix(0, 1, 1, 2, 2, std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n}\n","traces":[{"line":103,"address":[],"length":0,"stats":{"Line":7}},{"line":104,"address":[],"length":0,"stats":{"Line":7}},{"line":105,"address":[],"length":0,"stats":{"Line":7}},{"line":106,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":0}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":110,"address":[],"length":0,"stats":{"Line":0}},{"line":116,"address":[],"length":0,"stats":{"Line":7}},{"line":117,"address":[],"length":0,"stats":{"Line":14}},{"line":118,"address":[],"length":0,"stats":{"Line":7}},{"line":119,"address":[],"length":0,"stats":{"Line":7}},{"line":121,"address":[],"length":0,"stats":{"Line":7}},{"line":122,"address":[],"length":0,"stats":{"Line":7}},{"line":123,"address":[],"length":0,"stats":{"Line":7}},{"line":124,"address":[],"length":0,"stats":{"Line":7}},{"line":125,"address":[],"length":0,"stats":{"Line":7}},{"line":126,"address":[],"length":0,"stats":{"Line":7}},{"line":127,"address":[],"length":0,"stats":{"Line":7}},{"line":128,"address":[],"length":0,"stats":{"Line":7}},{"line":129,"address":[],"length":0,"stats":{"Line":7}},{"line":130,"address":[],"length":0,"stats":{"Line":7}},{"line":131,"address":[],"length":0,"stats":{"Line":7}},{"line":132,"address":[],"length":0,"stats":{"Line":7}},{"line":133,"address":[],"length":0,"stats":{"Line":7}},{"line":134,"address":[],"length":0,"stats":{"Line":7}},{"line":135,"address":[],"length":0,"stats":{"Line":7}},{"line":136,"address":[],"length":0,"stats":{"Line":7}},{"line":137,"address":[],"length":0,"stats":{"Line":7}},{"line":138,"address":[],"length":0,"stats":{"Line":7}},{"line":140,"address":[],"length":0,"stats":{"Line":7}},{"line":141,"address":[],"length":0,"stats":{"Line":7}},{"line":142,"address":[],"length":0,"stats":{"Line":7}},{"line":143,"address":[],"length":0,"stats":{"Line":7}},{"line":144,"address":[],"length":0,"stats":{"Line":7}},{"line":145,"address":[],"length":0,"stats":{"Line":7}},{"line":147,"address":[],"length":0,"stats":{"Line":7}},{"line":148,"address":[],"length":0,"stats":{"Line":7}},{"line":149,"address":[],"length":0,"stats":{"Line":7}},{"line":150,"address":[],"length":0,"stats":{"Line":7}},{"line":152,"address":[],"length":0,"stats":{"Line":7}},{"line":153,"address":[],"length":0,"stats":{"Line":7}},{"line":154,"address":[],"length":0,"stats":{"Line":7}},{"line":155,"address":[],"length":0,"stats":{"Line":7}},{"line":156,"address":[],"length":0,"stats":{"Line":7}},{"line":157,"address":[],"length":0,"stats":{"Line":7}},{"line":158,"address":[],"length":0,"stats":{"Line":7}},{"line":159,"address":[],"length":0,"stats":{"Line":7}},{"line":160,"address":[],"length":0,"stats":{"Line":7}},{"line":165,"address":[],"length":0,"stats":{"Line":7}},{"line":166,"address":[],"length":0,"stats":{"Line":11}},{"line":172,"address":[],"length":0,"stats":{"Line":8}},{"line":173,"address":[],"length":0,"stats":{"Line":16}},{"line":174,"address":[],"length":0,"stats":{"Line":16}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":177,"address":[],"length":0,"stats":{"Line":21}},{"line":178,"address":[],"length":0,"stats":{"Line":7}},{"line":179,"address":[],"length":0,"stats":{"Line":1}},{"line":181,"address":[],"length":0,"stats":{"Line":18}},{"line":182,"address":[],"length":0,"stats":{"Line":12}},{"line":183,"address":[],"length":0,"stats":{"Line":12}},{"line":184,"address":[],"length":0,"stats":{"Line":6}},{"line":185,"address":[],"length":0,"stats":{"Line":6}},{"line":186,"address":[],"length":0,"stats":{"Line":6}},{"line":187,"address":[],"length":0,"stats":{"Line":6}},{"line":194,"address":[],"length":0,"stats":{"Line":6}},{"line":195,"address":[],"length":0,"stats":{"Line":18}},{"line":201,"address":[],"length":0,"stats":{"Line":3}},{"line":206,"address":[],"length":0,"stats":{"Line":6}},{"line":207,"address":[],"length":0,"stats":{"Line":15}},{"line":208,"address":[],"length":0,"stats":{"Line":3}},{"line":209,"address":[],"length":0,"stats":{"Line":2}},{"line":210,"address":[],"length":0,"stats":{"Line":1}},{"line":212,"address":[],"length":0,"stats":{"Line":4}},{"line":213,"address":[],"length":0,"stats":{"Line":1}},{"line":214,"address":[],"length":0,"stats":{"Line":0}},{"line":216,"address":[],"length":0,"stats":{"Line":4}},{"line":218,"address":[],"length":0,"stats":{"Line":2}},{"line":219,"address":[],"length":0,"stats":{"Line":3}},{"line":220,"address":[],"length":0,"stats":{"Line":1}},{"line":234,"address":[],"length":0,"stats":{"Line":5}},{"line":235,"address":[],"length":0,"stats":{"Line":10}},{"line":236,"address":[],"length":0,"stats":{"Line":25}},{"line":237,"address":[],"length":0,"stats":{"Line":16}},{"line":238,"address":[],"length":0,"stats":{"Line":4}},{"line":239,"address":[],"length":0,"stats":{"Line":0}},{"line":241,"address":[],"length":0,"stats":{"Line":7}},{"line":242,"address":[],"length":0,"stats":{"Line":6}},{"line":244,"address":[],"length":0,"stats":{"Line":3}},{"line":245,"address":[],"length":0,"stats":{"Line":1}},{"line":248,"address":[],"length":0,"stats":{"Line":9}},{"line":249,"address":[],"length":0,"stats":{"Line":0}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":6}},{"line":253,"address":[],"length":0,"stats":{"Line":0}},{"line":254,"address":[],"length":0,"stats":{"Line":0}},{"line":256,"address":[],"length":0,"stats":{"Line":6}},{"line":257,"address":[],"length":0,"stats":{"Line":3}},{"line":258,"address":[],"length":0,"stats":{"Line":3}},{"line":264,"address":[],"length":0,"stats":{"Line":3}},{"line":265,"address":[],"length":0,"stats":{"Line":6}},{"line":266,"address":[],"length":0,"stats":{"Line":15}},{"line":267,"address":[],"length":0,"stats":{"Line":9}},{"line":268,"address":[],"length":0,"stats":{"Line":6}},{"line":269,"address":[],"length":0,"stats":{"Line":1}},{"line":270,"address":[],"length":0,"stats":{"Line":12}},{"line":271,"address":[],"length":0,"stats":{"Line":7}},{"line":272,"address":[],"length":0,"stats":{"Line":2}},{"line":274,"address":[],"length":0,"stats":{"Line":2}},{"line":276,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":2}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":284,"address":[],"length":0,"stats":{"Line":7}},{"line":289,"address":[],"length":0,"stats":{"Line":14}},{"line":290,"address":[],"length":0,"stats":{"Line":35}},{"line":291,"address":[],"length":0,"stats":{"Line":21}},{"line":292,"address":[],"length":0,"stats":{"Line":3}},{"line":294,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":16}},{"line":296,"address":[],"length":0,"stats":{"Line":11}},{"line":297,"address":[],"length":0,"stats":{"Line":2}},{"line":299,"address":[],"length":0,"stats":{"Line":6}},{"line":300,"address":[],"length":0,"stats":{"Line":2}},{"line":301,"address":[],"length":0,"stats":{"Line":1}},{"line":303,"address":[],"length":0,"stats":{"Line":4}},{"line":304,"address":[],"length":0,"stats":{"Line":4}},{"line":305,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":2}},{"line":311,"address":[],"length":0,"stats":{"Line":4}},{"line":312,"address":[],"length":0,"stats":{"Line":1}},{"line":314,"address":[],"length":0,"stats":{"Line":3}},{"line":315,"address":[],"length":0,"stats":{"Line":1}},{"line":321,"address":[],"length":0,"stats":{"Line":2}},{"line":327,"address":[],"length":0,"stats":{"Line":4}},{"line":328,"address":[],"length":0,"stats":{"Line":10}},{"line":329,"address":[],"length":0,"stats":{"Line":4}},{"line":330,"address":[],"length":0,"stats":{"Line":4}},{"line":331,"address":[],"length":0,"stats":{"Line":0}},{"line":333,"address":[],"length":0,"stats":{"Line":1}},{"line":334,"address":[],"length":0,"stats":{"Line":8}},{"line":335,"address":[],"length":0,"stats":{"Line":6}},{"line":336,"address":[],"length":0,"stats":{"Line":1}},{"line":338,"address":[],"length":0,"stats":{"Line":2}},{"line":340,"address":[],"length":0,"stats":{"Line":4}},{"line":341,"address":[],"length":0,"stats":{"Line":1}},{"line":348,"address":[],"length":0,"stats":{"Line":3}},{"line":349,"address":[],"length":0,"stats":{"Line":6}},{"line":350,"address":[],"length":0,"stats":{"Line":15}},{"line":351,"address":[],"length":0,"stats":{"Line":12}},{"line":352,"address":[],"length":0,"stats":{"Line":3}},{"line":353,"address":[],"length":0,"stats":{"Line":1}},{"line":355,"address":[],"length":0,"stats":{"Line":2}},{"line":356,"address":[],"length":0,"stats":{"Line":1}},{"line":358,"address":[],"length":0,"stats":{"Line":4}},{"line":360,"address":[],"length":0,"stats":{"Line":1}},{"line":361,"address":[],"length":0,"stats":{"Line":1}},{"line":362,"address":[],"length":0,"stats":{"Line":1}},{"line":363,"address":[],"length":0,"stats":{"Line":1}},{"line":369,"address":[],"length":0,"stats":{"Line":2}},{"line":374,"address":[],"length":0,"stats":{"Line":4}},{"line":375,"address":[],"length":0,"stats":{"Line":1}},{"line":376,"address":[],"length":0,"stats":{"Line":4}},{"line":377,"address":[],"length":0,"stats":{"Line":4}},{"line":379,"address":[],"length":0,"stats":{"Line":4}},{"line":387,"address":[],"length":0,"stats":{"Line":2}},{"line":388,"address":[],"length":0,"stats":{"Line":4}},{"line":389,"address":[],"length":0,"stats":{"Line":3}},{"line":390,"address":[],"length":0,"stats":{"Line":2}},{"line":391,"address":[],"length":0,"stats":{"Line":1}},{"line":393,"address":[],"length":0,"stats":{"Line":3}},{"line":394,"address":[],"length":0,"stats":{"Line":3}},{"line":395,"address":[],"length":0,"stats":{"Line":0}},{"line":397,"address":[],"length":0,"stats":{"Line":1}},{"line":398,"address":[],"length":0,"stats":{"Line":2}},{"line":399,"address":[],"length":0,"stats":{"Line":0}},{"line":407,"address":[],"length":0,"stats":{"Line":3}},{"line":412,"address":[],"length":0,"stats":{"Line":6}},{"line":413,"address":[],"length":0,"stats":{"Line":6}},{"line":414,"address":[],"length":0,"stats":{"Line":6}},{"line":415,"address":[],"length":0,"stats":{"Line":1}},{"line":417,"address":[],"length":0,"stats":{"Line":12}},{"line":424,"address":[],"length":0,"stats":{"Line":3}},{"line":432,"address":[],"length":0,"stats":{"Line":6}},{"line":433,"address":[],"length":0,"stats":{"Line":6}},{"line":434,"address":[],"length":0,"stats":{"Line":1}},{"line":436,"address":[],"length":0,"stats":{"Line":4}},{"line":437,"address":[],"length":0,"stats":{"Line":2}},{"line":438,"address":[],"length":0,"stats":{"Line":4}},{"line":439,"address":[],"length":0,"stats":{"Line":2}},{"line":440,"address":[],"length":0,"stats":{"Line":2}},{"line":441,"address":[],"length":0,"stats":{"Line":2}},{"line":442,"address":[],"length":0,"stats":{"Line":2}},{"line":443,"address":[],"length":0,"stats":{"Line":2}},{"line":446,"address":[],"length":0,"stats":{"Line":8}},{"line":447,"address":[],"length":0,"stats":{"Line":2}}],"covered":180,"coverable":195},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","frame.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/frame.h` exports.\n//!\n//! Complete inventory: frame_init / init_with_params / free / get_params /\n//! set_params / allocate / is_allocated / data / const_data /\n//! allocated_size / linesize_bytes / linesize_pixels / width / height /\n//! format / channel_count / get_timestamp / set_timestamp /\n//! debug_alive_count.\n//!\n//! # CPP-PARITY\n//! The C++ `c_api/frame.cpp` boxes every `OakFrame` handle with an\n//! `olive::FramePtr` (a shared pointer), so decoder-produced frames may\n//! alias the decoder's internal cache. The Rust equivalent boxes\n//! `Mutex\u003cFrame\u003e`; a decode that hands out a still-shared `Arc\u003cFrame\u003e`\n//! therefore cannot be aliased here and reports an empty handle instead\n//! (see `ffi::decoder`).\n\nuse std::ffi::{c_int, c_void};\nuse std::panic::{catch_unwind, AssertUnwindSafe};\nuse std::sync::Mutex;\n\nuse oakcore_rs::Rational;\n\nuse crate::bridge::common::OakVideoParams;\nuse crate::frame::Frame;\nuse crate::handle::{self, CHandle};\n\n/// `OAKCOMMON_PIXEL_FORMAT_INVALID` (oakcommon `common/videoparams.h`).\nconst OAKCOMMON_PIXEL_FORMAT_INVALID: c_int = -1;\n\n/// `oakcodec_frame_init`: new frame with default (invalid) params,\n/// refcount 1.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_init() -\u003e CHandle {\n\thandle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::new()))))\n}\n\n/// `oakcodec_frame_init_with_params`: new frame holding a copy of `params`\n/// (the handle is addref'd internally); buffer unallocated.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_init_with_params(params: OakVideoParams) -\u003e CHandle {\n\thandle::guard_handle(|| {\n\t\tOk(handle::make_owned(Mutex::new(Frame::with_params(params))))\n\t})\n}\n\n/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_free(frame: *mut CHandle) {\n\thandle::guard_void(|| super::free_handle(frame));\n}\n\n/// `oakcodec_frame_get_params`: copy of the frame's parameter set.\n///\n/// The copy is addref'd: the caller must release it with\n/// `oakcommon_videoparams_free` (see the header contract). Test-stub\n/// handles carry no `addref`, so the caller must not free them.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_get_params(\n\tframe: CHandle,\n\tout: *mut OakVideoParams,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tlet f = f.lock().unwrap();\n\t\tlet p = f.params().cloned().ok_or(crate::error::Error::Invalid)?;\n\t\tcrate::frame::params_addref(\u0026p);\n\t\t// SAFETY: the caller guarantees `out` points to a writable\n\t\t// `OakVideoParams`.\n\t\tunsafe { *out = p };\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_frame_set_params`: replace the parameter set (addref'd\n/// internally); recomputes line sizes, does not reallocate.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_set_params(\n\tframe: CHandle,\n\tparams: OakVideoParams,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tf.lock().unwrap().set_params(params);\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_frame_allocate`: allocate the pixel buffer from the current\n/// params; `OAKCODEC_E_STATE` when the params are invalid.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_allocate(frame: CHandle) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tf.lock().unwrap().allocate()\n\t})\n}\n\n/// `oakcodec_frame_is_allocated`: 1 when the buffer is allocated.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_is_allocated(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tlet f = f.lock().unwrap();\n\t\tif f.is_allocated() {\n\t\t\t1\n\t\t} else {\n\t\t\t0\n\t\t}\n\t})\n}\n\n/// `oakcodec_frame_data`: writable pixel buffer, NULL when\n/// unallocated/empty.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_data(frame: CHandle) -\u003e *mut c_void {\n\tmatch catch_unwind(AssertUnwindSafe(|| unsafe { frame_data_inner(\u0026frame) })) {\n\t\tOk(p) =\u003e p,\n\t\tErr(_) =\u003e std::ptr::null_mut(),\n\t}\n}\n\n/// `oakcodec_frame_const_data`: const variant of `oakcodec_frame_data`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_const_data(frame: CHandle) -\u003e *const c_void {\n\tmatch catch_unwind(AssertUnwindSafe(|| unsafe { frame_const_data_inner(\u0026frame) })) {\n\t\tOk(p) =\u003e p,\n\t\tErr(_) =\u003e std::ptr::null_mut(),\n\t}\n}\n\nunsafe fn frame_data_inner(frame: \u0026CHandle) -\u003e *mut c_void {\n\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(frame) {\n\t\tSome(f) =\u003e f,\n\t\tNone =\u003e return std::ptr::null_mut(),\n\t};\n\tmatch f.lock().unwrap().data_mut() {\n\t\tSome(d) =\u003e d.as_mut_ptr() as *mut c_void,\n\t\tNone =\u003e std::ptr::null_mut(),\n\t}\n}\n\nunsafe fn frame_const_data_inner(frame: \u0026CHandle) -\u003e *const c_void {\n\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(frame) {\n\t\tSome(f) =\u003e f,\n\t\tNone =\u003e return std::ptr::null(),\n\t};\n\tmatch f.lock().unwrap().data() {\n\t\tSome(d) =\u003e d.as_ptr() as *const c_void,\n\t\tNone =\u003e std::ptr::null(),\n\t}\n}\n\n/// `oakcodec_frame_allocated_size`: size of the pixel buffer in bytes\n/// (0 when unallocated).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_allocated_size(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().allocated_size() as c_int\n\t})\n}\n\n/// `oakcodec_frame_linesize_bytes`: distance between two rows in bytes\n/// (0 when params are unset).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_linesize_bytes(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().linesize_bytes()\n\t})\n}\n\n/// `oakcodec_frame_linesize_pixels`: distance between two rows in pixels.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_linesize_pixels(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().linesize_pixels()\n\t})\n}\n\n/// `oakcodec_frame_width`: frame width (0 when params are empty).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_width(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().width()\n\t})\n}\n\n/// `oakcodec_frame_height`: frame height (0 when params are empty).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_height(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().height()\n\t})\n}\n\n/// `oakcodec_frame_format`: pixel format as an `OakPixelFormat` value;\n/// `OAKCOMMON_PIXEL_FORMAT_INVALID` on an empty handle.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_format(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return OAKCOMMON_PIXEL_FORMAT_INVALID,\n\t\t};\n\t\tf.lock().unwrap().format() as c_int\n\t})\n}\n\n/// `oakcodec_frame_channel_count`: plane channel count of the params\n/// format (0 on an empty handle).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_channel_count(frame: CHandle) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame) {\n\t\t\tSome(f) =\u003e f,\n\t\t\tNone =\u003e return 0,\n\t\t};\n\t\tf.lock().unwrap().channel_count()\n\t})\n}\n\n/// `oakcodec_frame_get_timestamp`: frame timestamp as a rational number\n/// of seconds, written through `numerator`/`denominator`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_get_timestamp(\n\tframe: CHandle,\n\tnumerator: *mut c_int,\n\tdenominator: *mut c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif numerator.is_null() || denominator.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tlet f = f.lock().unwrap();\n\t\tlet ts = f.timestamp();\n\t\t// SAFETY: both pointers were range-checked above.\n\t\tunsafe {\n\t\t\t*numerator = ts.numerator() as c_int;\n\t\t\t*denominator = ts.denominator() as c_int;\n\t\t}\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_frame_set_timestamp`: replace the frame timestamp.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_frame_set_timestamp(\n\tframe: CHandle,\n\tnumerator: c_int,\n\tdenominator: c_int,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tlet f = super::get_box::\u003cMutex\u003cFrame\u003e\u003e(\u0026frame).ok_or(crate::error::Error::Invalid)?;\n\t\tf.lock()\n\t\t\t.unwrap()\n\t\t\t.set_timestamp(Rational::new(numerator as i64, denominator as i64));\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_debug_alive_count`: number of live boxed handle objects\n/// across all families (see `crate::handle::alive_count`).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_debug_alive_count() -\u003e c_int {\n\thandle::guard_raw(handle::alive_count)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::{\n\t\toakcommon_videoparams_get_height, oakcommon_videoparams_get_width,\n\t\toakcommon_videoparams_init_basic,\n\t};\n\tuse crate::error::OAKCODEC_E_INVALID;\n\n\t#[test]\n\tfn frame_lifecycle_golden() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(100, 50) };\n\t\tlet before = handle::alive_count();\n\t\tlet mut h = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tassert!(!h.is_null());\n\t\t// init -\u003e exactly one more live box.\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\n\t\t// get_params round-trips width/height through the stub.\n\t\tlet mut out = empty_params();\n\t\tlet rc = unsafe { oakcodec_frame_get_params(h, \u0026mut out) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\t// NOTE: stub handles carry no addref and `oakcommon_videoparams_free`\n\t\t// would drop the shared box; the test keeps the copy alive for the\n\t\t// frame's lifetime and does not free it.\n\t\tassert_eq!(unsafe { oakcommon_videoparams_get_width(out.clone()) }, 100);\n\t\tassert_eq!(unsafe { oakcommon_videoparams_get_height(out.clone()) }, 50);\n\n\t\tassert_eq!(unsafe { oakcodec_frame_width(h) }, 100);\n\t\tassert_eq!(unsafe { oakcodec_frame_height(h) }, 50);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());\n\n\t\tlet rc = unsafe { oakcodec_frame_allocate(h) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 1);\n\t\tassert!(!unsafe { oakcodec_frame_data(h) }.is_null());\n\t\t// U8 RGBA: 100px -\u003e 4*128 bytes linesize.\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);\n\t\tassert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);\n\n\t\t// set_timestamp round-trip.\n\t\tassert_eq!(unsafe { oakcodec_frame_set_timestamp(h, 1, 30) }, crate::error::OAKCODEC_OK);\n\t\tlet (mut num, mut den) = (0, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_get_timestamp(h, \u0026mut num, \u0026mut den) }, crate::error::OAKCODEC_OK);\n\t\tassert_eq!((num, den), (1, 30));\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut h) };\n\t\tassert!(h.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\t#[test]\n\tfn frame_errors_and_empty_handles() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet empty = CHandle::null();\n\t\tassert_eq!(unsafe { oakcodec_frame_width(empty) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_format(empty) }, OAKCOMMON_PIXEL_FORMAT_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(empty) }, OAKCODEC_E_INVALID);\n\t\tassert_eq!(unsafe { oakcodec_frame_get_params(empty, std::ptr::null_mut()) }, OAKCODEC_E_INVALID);\n\n\t\t// init_basic(0, 0) is not valid -\u003e allocate rejects with E_STATE.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(0, 0) };\n\t\tlet mut h = unsafe { oakcodec_frame_init_with_params(params) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_E_STATE);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);\n\t\tunsafe { oakcodec_frame_free(\u0026mut h) };\n\t}\n\n\t#[test]\n\tfn free_null_and_empty_are_noops() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = handle::alive_count();\n\t\tunsafe { oakcodec_frame_free(std::ptr::null_mut()) };\n\t\tlet mut empty = CHandle::null();\n\t\tunsafe { oakcodec_frame_free(\u0026mut empty) };\n\t\tassert!(empty.is_null());\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\t#[test]\n\tfn init_set_params_and_query_helpers() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = handle::alive_count();\n\n\t\t// Bare init (no params): invalid params, not allocated. The stub's\n\t\t// default MockParams carries format 0 (U8); the empty-handle -1\n\t\t// case is covered in `frame_errors_and_empty_handles`.\n\t\tlet mut h = unsafe { oakcodec_frame_init() };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(handle::alive_count(), before + 1);\n\t\tassert_eq!(unsafe { oakcodec_frame_width(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_height(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);\n\t\tassert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 0);\n\t\tassert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());\n\t\tassert_eq!(unsafe { oakcodec_frame_const_data(h) }, std::ptr::null());\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_E_STATE);\n\n\t\t// set_params replaces the parameter set and recomputes line sizes.\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(100, 50) };\n\t\tlet rc = unsafe { oakcodec_frame_set_params(h, params) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_frame_width(h) }, 100);\n\t\tassert_eq!(unsafe { oakcodec_frame_height(h) }, 50);\n\t\tassert_eq!(unsafe { oakcodec_frame_format(h) }, 0); // U8\n\t\tassert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);\n\t\tassert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 128);\n\n\t\t// allocate -\u003e data and const_data point at the buffer.\n\t\tassert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_OK);\n\t\tassert!(!unsafe { oakcodec_frame_data(h) }.is_null());\n\t\tassert!(!unsafe { oakcodec_frame_const_data(h) }.is_null());\n\t\tassert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);\n\n\t\t// get_timestamp rejects NULL out pointers.\n\t\tassert_eq!(\n\t\t\tunsafe { oakcodec_frame_get_timestamp(h, std::ptr::null_mut(), std::ptr::null_mut()) },\n\t\t\tOAKCODEC_E_INVALID\n\t\t);\n\n\t\t// debug_alive_count reports the live boxes (\u003e= our own).\n\t\tassert!(unsafe { oakcodec_debug_alive_count() } \u003e= before + 1);\n\n\t\tunsafe { oakcodec_frame_free(\u0026mut h) };\n\t\tassert_eq!(handle::alive_count(), before);\n\t}\n\n\tfn empty_params() -\u003e OakVideoParams {\n\t\tOakVideoParams {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: 0,\n\t\t}\n\t}\n}\n","traces":[{"line":49,"address":[],"length":0,"stats":{"Line":1}},{"line":50,"address":[],"length":0,"stats":{"Line":4}},{"line":56,"address":[],"length":0,"stats":{"Line":4}},{"line":57,"address":[],"length":0,"stats":{"Line":8}},{"line":58,"address":[],"length":0,"stats":{"Line":12}},{"line":64,"address":[],"length":0,"stats":{"Line":8}},{"line":65,"address":[],"length":0,"stats":{"Line":24}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":4}},{"line":79,"address":[],"length":0,"stats":{"Line":4}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":82,"address":[],"length":0,"stats":{"Line":5}},{"line":83,"address":[],"length":0,"stats":{"Line":4}},{"line":84,"address":[],"length":0,"stats":{"Line":5}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":1}},{"line":89,"address":[],"length":0,"stats":{"Line":1}},{"line":96,"address":[],"length":0,"stats":{"Line":1}},{"line":100,"address":[],"length":0,"stats":{"Line":2}},{"line":101,"address":[],"length":0,"stats":{"Line":5}},{"line":102,"address":[],"length":0,"stats":{"Line":3}},{"line":103,"address":[],"length":0,"stats":{"Line":1}},{"line":110,"address":[],"length":0,"stats":{"Line":6}},{"line":111,"address":[],"length":0,"stats":{"Line":12}},{"line":112,"address":[],"length":0,"stats":{"Line":30}},{"line":113,"address":[],"length":0,"stats":{"Line":10}},{"line":119,"address":[],"length":0,"stats":{"Line":4}},{"line":120,"address":[],"length":0,"stats":{"Line":8}},{"line":121,"address":[],"length":0,"stats":{"Line":8}},{"line":122,"address":[],"length":0,"stats":{"Line":8}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":16}},{"line":126,"address":[],"length":0,"stats":{"Line":4}},{"line":127,"address":[],"length":0,"stats":{"Line":1}},{"line":129,"address":[],"length":0,"stats":{"Line":3}},{"line":137,"address":[],"length":0,"stats":{"Line":4}},{"line":138,"address":[],"length":0,"stats":{"Line":16}},{"line":139,"address":[],"length":0,"stats":{"Line":8}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":2}},{"line":147,"address":[],"length":0,"stats":{"Line":8}},{"line":148,"address":[],"length":0,"stats":{"Line":4}},{"line":149,"address":[],"length":0,"stats":{"Line":0}},{"line":153,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":8}},{"line":155,"address":[],"length":0,"stats":{"Line":8}},{"line":156,"address":[],"length":0,"stats":{"Line":0}},{"line":158,"address":[],"length":0,"stats":{"Line":8}},{"line":159,"address":[],"length":0,"stats":{"Line":4}},{"line":160,"address":[],"length":0,"stats":{"Line":2}},{"line":164,"address":[],"length":0,"stats":{"Line":2}},{"line":165,"address":[],"length":0,"stats":{"Line":4}},{"line":166,"address":[],"length":0,"stats":{"Line":4}},{"line":167,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":4}},{"line":170,"address":[],"length":0,"stats":{"Line":2}},{"line":171,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":3}},{"line":179,"address":[],"length":0,"stats":{"Line":6}},{"line":180,"address":[],"length":0,"stats":{"Line":6}},{"line":181,"address":[],"length":0,"stats":{"Line":6}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":184,"address":[],"length":0,"stats":{"Line":6}},{"line":191,"address":[],"length":0,"stats":{"Line":3}},{"line":192,"address":[],"length":0,"stats":{"Line":6}},{"line":193,"address":[],"length":0,"stats":{"Line":6}},{"line":194,"address":[],"length":0,"stats":{"Line":6}},{"line":195,"address":[],"length":0,"stats":{"Line":0}},{"line":197,"address":[],"length":0,"stats":{"Line":6}},{"line":203,"address":[],"length":0,"stats":{"Line":2}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":4}},{"line":206,"address":[],"length":0,"stats":{"Line":4}},{"line":207,"address":[],"length":0,"stats":{"Line":0}},{"line":209,"address":[],"length":0,"stats":{"Line":4}},{"line":215,"address":[],"length":0,"stats":{"Line":5}},{"line":216,"address":[],"length":0,"stats":{"Line":10}},{"line":217,"address":[],"length":0,"stats":{"Line":9}},{"line":218,"address":[],"length":0,"stats":{"Line":8}},{"line":219,"address":[],"length":0,"stats":{"Line":1}},{"line":221,"address":[],"length":0,"stats":{"Line":8}},{"line":227,"address":[],"length":0,"stats":{"Line":4}},{"line":228,"address":[],"length":0,"stats":{"Line":8}},{"line":229,"address":[],"length":0,"stats":{"Line":8}},{"line":230,"address":[],"length":0,"stats":{"Line":8}},{"line":231,"address":[],"length":0,"stats":{"Line":0}},{"line":233,"address":[],"length":0,"stats":{"Line":8}},{"line":240,"address":[],"length":0,"stats":{"Line":2}},{"line":241,"address":[],"length":0,"stats":{"Line":4}},{"line":242,"address":[],"length":0,"stats":{"Line":3}},{"line":243,"address":[],"length":0,"stats":{"Line":2}},{"line":244,"address":[],"length":0,"stats":{"Line":1}},{"line":246,"address":[],"length":0,"stats":{"Line":2}},{"line":253,"address":[],"length":0,"stats":{"Line":2}},{"line":254,"address":[],"length":0,"stats":{"Line":4}},{"line":255,"address":[],"length":0,"stats":{"Line":4}},{"line":256,"address":[],"length":0,"stats":{"Line":4}},{"line":257,"address":[],"length":0,"stats":{"Line":0}},{"line":259,"address":[],"length":0,"stats":{"Line":4}},{"line":266,"address":[],"length":0,"stats":{"Line":2}},{"line":271,"address":[],"length":0,"stats":{"Line":4}},{"line":272,"address":[],"length":0,"stats":{"Line":6}},{"line":273,"address":[],"length":0,"stats":{"Line":1}},{"line":275,"address":[],"length":0,"stats":{"Line":5}},{"line":276,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":2}},{"line":279,"address":[],"length":0,"stats":{"Line":1}},{"line":280,"address":[],"length":0,"stats":{"Line":2}},{"line":281,"address":[],"length":0,"stats":{"Line":1}},{"line":283,"address":[],"length":0,"stats":{"Line":1}},{"line":289,"address":[],"length":0,"stats":{"Line":1}},{"line":294,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":5}},{"line":296,"address":[],"length":0,"stats":{"Line":3}},{"line":297,"address":[],"length":0,"stats":{"Line":2}},{"line":298,"address":[],"length":0,"stats":{"Line":3}},{"line":299,"address":[],"length":0,"stats":{"Line":1}},{"line":306,"address":[],"length":0,"stats":{"Line":1}},{"line":307,"address":[],"length":0,"stats":{"Line":1}}],"covered":109,"coverable":119},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","mod.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! C ABI export layer: implements `include/codec/*.h` verbatim.\n//!\n//! Organization: one submodule per public header (frame / decoder /\n//! encoder / conform / proxy / task). The authoritative function list is\n//! the header itself; each export only unwraps handles, calls the safe\n//! Rust domains, and maps results through [`crate::handle::guard*`].\n//! `include/codec/error.h` exports macros only, so it is folded into the\n//! preamble below instead of getting its own submodule.\n//!\n//! Shared helpers live here: the two-stage string convention\n//! ([`string_out`]), C-string decoding ([`c_str`]) and in-place handle\n//! release ([`free_handle`]) — all mirroring the `c_api/*.cpp` helpers.\n\n/// `include/codec/error.h` — macros only, no exported functions.\n///\n/// `OAKCODEC_OK` and the `OAKCODEC_E_*` codes are mirrored as\n/// [`crate::error`] constants; `OAKCODEC_ABI_VERSION` lives in\n/// [`crate::handle`].\n\npub mod conform;\npub mod decoder;\npub mod encoder;\npub mod frame;\npub mod proxy;\npub mod task;\n\nuse std::ffi::{c_char, c_int};\n#[cfg(test)]\nuse std::sync::Mutex;\n\nuse crate::handle::CHandle;\n\n/// Serializes every ffi unit test: they share the global handle ALIVE\n/// counter, the injected decoder/encoder registries and the probe error,\n/// so exact `alive_count` assertions and registry injection require\n/// serial execution. Held poison-tolerant (`into_inner`) so one failing\n/// test cannot cascade-fail the rest.\n#[cfg(test)]\npub(crate) static TEST_LOCK: Mutex\u003c()\u003e = Mutex::new(());\n\n/// Poison-tolerant lock helper for the ffi tests.\n#[cfg(test)]\npub(crate) fn lock_tests() -\u003e std::sync::MutexGuard\u003c'static, ()\u003e {\n\tTEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())\n}\n\n/// Two-stage string copy helper (`string_out` in every `c_api/*.cpp`).\n///\n/// Returns the required buffer size including the trailing NUL; when\n/// `buf` is non-NULL and `buf_size \u003e 0` the string is copied truncated to\n/// `buf_size - 1` bytes and NUL-terminated.\npub(crate) fn string_out(s: \u0026str, buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet need = s.len() as c_int + 1;\n\tif !buf.is_null() \u0026\u0026 buf_size \u003e 0 {\n\t\tlet n = (s.len() as c_int).min(buf_size - 1);\n\t\t// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.\n\t\tunsafe {\n\t\t\tstd::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize);\n\t\t\t*buf.add(n as usize) = 0;\n\t\t}\n\t}\n\tneed\n}\n\n/// Read a NUL-terminated C string; `None` on NULL pointers.\npub(crate) fn c_str(ptr: *const c_char) -\u003e Option\u003cString\u003e {\n\tif ptr.is_null() {\n\t\treturn None;\n\t}\n\t// SAFETY: `ptr` must be a valid NUL-terminated C string by contract.\n\tlet s = unsafe { std::ffi::CStr::from_ptr(ptr) };\n\tSome(s.to_string_lossy().into_owned())\n}\n\n/// Release a handle in place and null its `ctx` (`free_handle` in\n/// `c_api/refcounted.h`); NULL pointer and empty handle are no-ops.\npub(crate) fn free_handle(h: *mut CHandle) {\n\tif h.is_null() {\n\t\treturn;\n\t}\n\tlet handle = unsafe { \u0026mut *h };\n\tif handle.ctx.is_null() {\n\t\treturn;\n\t}\n\tif let Some(release) = handle.release {\n\t\t// SAFETY: `release` targets the box behind `ctx`.\n\t\tunsafe { release(handle.ctx) };\n\t}\n\thandle.ctx = std::ptr::null_mut();\n}\n\n/// Safe view into a handle's boxed value; `None` for empty handles.\n///\n/// Thin wrapper over [`crate::handle::get`] so the export bodies can call\n/// it without `unsafe` blocks everywhere.\n///\n/// # Safety\n/// `T` must be the boxed type; each export asserts it via the handle\n/// contract (the same typed box is used by its `make_owned` call).\npub(crate) fn get_box\u003cT: 'static\u003e(h: \u0026CHandle) -\u003e Option\u003c\u0026T\u003e {\n\tunsafe { crate::handle::get::\u003cT\u003e(h) }\n}\n","traces":[{"line":59,"address":[],"length":0,"stats":{"Line":26}},{"line":60,"address":[],"length":0,"stats":{"Line":78}},{"line":68,"address":[],"length":0,"stats":{"Line":22}},{"line":69,"address":[],"length":0,"stats":{"Line":44}},{"line":70,"address":[],"length":0,"stats":{"Line":44}},{"line":71,"address":[],"length":0,"stats":{"Line":88}},{"line":74,"address":[],"length":0,"stats":{"Line":110}},{"line":75,"address":[],"length":0,"stats":{"Line":44}},{"line":78,"address":[],"length":0,"stats":{"Line":22}},{"line":82,"address":[],"length":0,"stats":{"Line":85}},{"line":83,"address":[],"length":0,"stats":{"Line":170}},{"line":84,"address":[],"length":0,"stats":{"Line":15}},{"line":87,"address":[],"length":0,"stats":{"Line":210}},{"line":88,"address":[],"length":0,"stats":{"Line":140}},{"line":93,"address":[],"length":0,"stats":{"Line":24}},{"line":94,"address":[],"length":0,"stats":{"Line":48}},{"line":95,"address":[],"length":0,"stats":{"Line":1}},{"line":97,"address":[],"length":0,"stats":{"Line":46}},{"line":98,"address":[],"length":0,"stats":{"Line":46}},{"line":99,"address":[],"length":0,"stats":{"Line":1}},{"line":101,"address":[],"length":0,"stats":{"Line":66}},{"line":103,"address":[],"length":0,"stats":{"Line":22}},{"line":105,"address":[],"length":0,"stats":{"Line":22}},{"line":116,"address":[],"length":0,"stats":{"Line":116}},{"line":117,"address":[],"length":0,"stats":{"Line":232}}],"covered":25,"coverable":25},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","proxy.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/proxy.h` exports.\n//!\n//! Complete inventory: create_instance / destroy_instance / params_default\n//! / get_state / state_to_string / get_proxy_directory / get_proxy_filename\n//! / get_working_filename / get_or_start / find_ffmpeg.\n//!\n//! # CPP-PARITY\n//! The C++ `oakcodec_proxy_params.include_audio` is an `int`, mirrored\n//! byte-for-byte by [`crate::proxymanager::ProxyParams`]; the POD structs\n//! are still defined here (rather than reused) so the ffi layer never\n//! depends on the crate-internal type's layout. A NULL `params` maps to\n//! the crate's compiled-in defaults (`ProxyParams::default`), matching\n//! `to_native` in `c_api/proxy.cpp`.\n\nuse std::ffi::{c_char, c_int};\n\nuse crate::handle;\nuse crate::proxymanager::{ProxyManager, ProxyParams, ProxyState};\n\n/// `OAKCODEC_PROXY_STATE_MISSING`.\npub const OAKCODEC_PROXY_STATE_MISSING: c_int = 0;\n/// `OAKCODEC_PROXY_STATE_GENERATING`.\npub const OAKCODEC_PROXY_STATE_GENERATING: c_int = 1;\n/// `OAKCODEC_PROXY_STATE_READY`.\npub const OAKCODEC_PROXY_STATE_READY: c_int = 2;\n/// `OAKCODEC_PROXY_STATE_FAILED`.\npub const OAKCODEC_PROXY_STATE_FAILED: c_int = 3;\n\n/// `oakcodec_proxy_params` — POD mirror of `include/codec/proxy.h`.\n#[allow(missing_docs)]\n#[repr(C)]\npub struct oakcodec_proxy_params {\n\tpub width: c_int,\n\tpub height: c_int,\n\tpub divider: c_int,\n\tpub version: c_int,\n\tpub crf: c_int,\n\tpub include_audio: c_int,\n\tpub extension: [u8; 32],\n\tpub preset: [u8; 32],\n}\n\n/// `oakcodec_proxy_result` — POD result of `oakcodec_proxy_get_or_start`.\n#[allow(missing_docs)]\n#[repr(C)]\npub struct oakcodec_proxy_result {\n\tpub state: c_int,\n\tpub filename: [u8; 1024],\n}\n\n/// Convert the C POD to the crate's [`ProxyParams`] (`to_native` in\n/// `c_api/proxy.cpp`); NULL maps to the compiled-in defaults.\nfn to_native(p: *const oakcodec_proxy_params) -\u003e ProxyParams {\n\tif p.is_null() {\n\t\treturn ProxyParams::default();\n\t}\n\tlet p = unsafe { \u0026*p };\n\tProxyParams {\n\t\twidth: p.width,\n\t\theight: p.height,\n\t\tdivider: p.divider,\n\t\tversion: p.version,\n\t\tcrf: p.crf,\n\t\tinclude_audio: p.include_audio,\n\t\textension: p.extension,\n\t\tpreset: p.preset,\n\t}\n}\n\n/// Copy a NUL-terminated byte array into a C buffer (truncated).\nfn copy_cstr(src: \u0026[u8], dst: \u0026mut [u8]) {\n\tdst.fill(0);\n\tlet n = src.len().min(dst.len().saturating_sub(1));\n\tdst[..n].copy_from_slice(\u0026src[..n]);\n}\n\n/// `oakcodec_proxy_create_instance`: create the singleton (always present\n/// here, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_create_instance() -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet _ = ProxyManager::instance();\n\t\tcrate::error::OAKCODEC_OK\n\t})\n}\n\n/// `oakcodec_proxy_destroy_instance`: destroy the singleton (the Rust\n/// manager is stateless, so a no-op).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_destroy_instance() -\u003e c_int {\n\thandle::guard_raw(|| crate::error::OAKCODEC_OK)\n}\n\n/// `oakcodec_proxy_params_default`: fill `out` with the compiled-in\n/// default proxy parameters.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_params_default(out: *mut oakcodec_proxy_params) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet n = ProxyManager::proxy_params_from_config();\n\t\tunsafe {\n\t\t\t(*out).width = n.width;\n\t\t\t(*out).height = n.height;\n\t\t\t(*out).divider = n.divider;\n\t\t\t(*out).version = n.version;\n\t\t\t(*out).crf = n.crf;\n\t\t\t(*out).include_audio = n.include_audio;\n\t\t\tcopy_cstr(\u0026n.extension, \u0026mut (*out).extension);\n\t\t\tcopy_cstr(\u0026n.preset, \u0026mut (*out).preset);\n\t\t}\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_proxy_get_state`: state of a proxy file on disk\n/// (`OAKCODEC_PROXY_STATE_MISSING` for NULL/empty/absent).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_state(proxy_filename: *const c_char) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet f = match crate::ffi::c_str(proxy_filename) {\n\t\t\tSome(f) if !f.is_empty() =\u003e f,\n\t\t\t_ =\u003e return OAKCODEC_PROXY_STATE_MISSING,\n\t\t};\n\t\tProxyManager::get_proxy_state(\u0026f) as c_int\n\t})\n}\n\n/// `oakcodec_proxy_state_to_string` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_state_to_string(\n\tstate: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet s = match state {\n\t\t\t0 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Missing),\n\t\t\t1 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Generating),\n\t\t\t2 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Ready),\n\t\t\t3 =\u003e ProxyManager::proxy_state_to_string(ProxyState::Failed),\n\t\t\t_ =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t})\n}\n\n/// `oakcodec_proxy_get_proxy_directory` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_proxy_directory(\n\tcache_path: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) =\u003e c,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tmatch ProxyManager::get_proxy_directory(\u0026cache) {\n\t\t\tOk(s) =\u003e super::string_out(\u0026s, buf, buf_size),\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n/// `oakcodec_proxy_get_proxy_filename` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_proxy_filename(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tparams: *const oakcodec_proxy_params,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) =\u003e c,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) =\u003e s,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tlet native = to_native(params);\n\t\tmatch ProxyManager::get_proxy_filename(\u0026cache, \u0026source, stream_index, \u0026native) {\n\t\t\tOk(s) =\u003e super::string_out(\u0026s, buf, buf_size),\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n/// `oakcodec_proxy_get_working_filename` (two-stage).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_working_filename(\n\tproxy_filename: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet proxy = match crate::ffi::c_str(proxy_filename) {\n\t\t\tSome(p) =\u003e p,\n\t\t\tNone =\u003e return crate::error::OAKCODEC_E_INVALID,\n\t\t};\n\t\tmatch ProxyManager::get_working_filename(\u0026proxy) {\n\t\t\tOk(s) =\u003e super::string_out(\u0026s, buf, buf_size),\n\t\t\tErr(_) =\u003e crate::error::OAKCODEC_E_FAILED,\n\t\t}\n\t})\n}\n\n/// `oakcodec_proxy_get_or_start`: get or start generating a proxy for\n/// `source_filename`.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_get_or_start(\n\tcache_path: *const c_char,\n\tsource_filename: *const c_char,\n\tstream_index: c_int,\n\tparams: *const oakcodec_proxy_params,\n\tout: *mut oakcodec_proxy_result,\n) -\u003e c_int {\n\thandle::guard(|| {\n\t\tif out.is_null() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet cache = match crate::ffi::c_str(cache_path) {\n\t\t\tSome(c) =\u003e c,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet source = match crate::ffi::c_str(source_filename) {\n\t\t\tSome(s) =\u003e s,\n\t\t\tNone =\u003e return Err(crate::error::Error::Invalid),\n\t\t};\n\t\tlet native = to_native(params);\n\t\tlet (state, filename) = ProxyManager::instance()\n\t\t\t.get_or_start(\u0026cache, \u0026source, stream_index, \u0026native)\n\t\t\t.map_err(|_| crate::error::Error::Failed(\"get_or_start failed\".to_string()))?;\n\t\tunsafe {\n\t\t\tlet out_ref = \u0026mut *out;\n\t\t\tout_ref.state = state as c_int;\n\t\t\t// Truncate to 1023 chars + NUL, matching `snprintf` in\n\t\t\t// `c_api/proxy.cpp`.\n\t\t\tlet n = filename.len().min(1023);\n\t\t\tout_ref.filename[..n].copy_from_slice(\u0026filename.as_bytes()[..n]);\n\t\t\tout_ref.filename[n] = 0;\n\t\t}\n\t\tOk(())\n\t})\n}\n\n/// `oakcodec_proxy_find_ffmpeg` (two-stage; empty string when none found).\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_proxy_find_ffmpeg(\n\tconfigured_path: *const c_char,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tlet configured = crate::ffi::c_str(configured_path).unwrap_or_default();\n\t\tlet s = ProxyManager::find_ffmpeg(\u0026configured);\n\t\tsuper::string_out(\u0026s, buf, buf_size)\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::conformmanager::test_util::{accept_cb, REG_LOCK};\n\tuse crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};\n\tuse crate::task::set_task_submit_cb_extern;\n\n\tfn cstr(s: \u0026str) -\u003e std::ffi::CString {\n\t\tstd::ffi::CString::new(s).unwrap()\n\t}\n\n\tfn temp_cache(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_ffi_proxy_{}_{}\", name, std::process::id()));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\tfn defaults() -\u003e oakcodec_proxy_params {\n\t\tlet mut p: oakcodec_proxy_params = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_proxy_params_default(\u0026mut p) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tp\n\t}\n\n\t#[test]\n\tfn create_destroy_and_params_default() {\n\t\tassert_eq!(unsafe { oakcodec_proxy_create_instance() }, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(unsafe { oakcodec_proxy_destroy_instance() }, crate::error::OAKCODEC_OK);\n\n\t\tlet p = defaults();\n\t\tassert_eq!(p.width, 1280);\n\t\tassert_eq!(p.height, 720);\n\t\tassert_eq!(p.divider, 1);\n\t\tassert_eq!(p.version, 1);\n\t\tassert_eq!(p.crf, 23);\n\t\tassert_eq!(p.include_audio, 1);\n\t\tassert_eq!(\u0026p.extension[..3], b\"mp4\");\n\t\tassert_eq!(\u0026p.preset[..8], b\"veryfast\");\n\n\t\tlet rc = unsafe { oakcodec_proxy_params_default(std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n\n\t#[test]\n\tfn get_state_and_state_to_string() {\n\t\tlet cache = temp_cache(\"state\");\n\t\tlet p = defaults();\n\t\tlet src = cstr(\"media.mp4\");\n\t\tlet cache_c = cstr(\u0026cache);\n\n\t\t// Resolve the proxy filename, then query its state.\n\t\tlet mut name = [0i8; 1024];\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_filename(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, name.as_mut_ptr(), 1024) };\n\t\tassert!(rc \u003e 0);\n\t\tlet proxy = crate::ffi::c_str(name.as_ptr()).unwrap();\n\t\tassert!(proxy.contains(\"1280x720\"));\n\n\t\t// Missing by default.\n\t\tlet pc = cstr(\u0026proxy);\n\t\tassert_eq!(unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, OAKCODEC_PROXY_STATE_MISSING);\n\t\tassert_eq!(unsafe { oakcodec_proxy_get_state(std::ptr::null()) }, OAKCODEC_PROXY_STATE_MISSING);\n\n\t\t// Ready once the file exists.\n\t\tstd::fs::create_dir_all(std::path::Path::new(\u0026proxy).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026proxy, b\"x\").unwrap();\n\t\tassert_eq!(unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, OAKCODEC_PROXY_STATE_READY);\n\n\t\t// state_to_string mapping + invalid range.\n\t\tlet mut buf = [0i8; 64];\n\t\tlet rc = unsafe { oakcodec_proxy_state_to_string(2, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, 6); // \"ready\" + NUL\n\t\tassert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(\"ready\"));\n\t\tlet rc = unsafe { oakcodec_proxy_state_to_string(7, buf.as_mut_ptr(), 64) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t}\n\n\t#[test]\n\tfn directory_working_and_get_or_start() {\n\t\tlet cache = temp_cache(\"getorstart\");\n\t\tlet cache_c = cstr(\u0026cache);\n\t\tlet src = cstr(\"media.mp4\");\n\t\tlet p = defaults();\n\n\t\t// get_proxy_directory.\n\t\tlet mut buf = [0i8; 512];\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_directory(cache_c.as_ptr(), buf.as_mut_ptr(), 512) };\n\t\tassert!(rc \u003e 0);\n\t\tassert_eq!(\n\t\t\tcrate::ffi::c_str(buf.as_ptr()).as_deref(),\n\t\t\tSome(format!(\"{}/proxy\", cache).as_str())\n\t\t);\n\n\t\t// get_working_filename appends \".working.mp4\".\n\t\tlet proxy = format!(\"{}/proxy/{}-0.1280x720.v1.a1.mp4\", cache, 12345);\n\t\tlet pc = cstr(\u0026proxy);\n\t\tlet rc = unsafe { oakcodec_proxy_get_working_filename(pc.as_ptr(), buf.as_mut_ptr(), 512) };\n\t\tassert!(rc \u003e 0);\n\t\tassert_eq!(\n\t\t\tcrate::ffi::c_str(buf.as_ptr()).as_deref(),\n\t\t\tSome(format!(\"{}.working.mp4\", proxy).as_str())\n\t\t);\n\n\t\t// get_or_start without a registrar -\u003e Missing.\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tlet mut out: oakcodec_proxy_result = unsafe { std::mem::zeroed() };\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, \u0026mut out) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(out.state, OAKCODEC_PROXY_STATE_MISSING);\n\n\t\t// With a registrar and no files -\u003e Generating.\n\t\tset_task_submit_cb_extern(Some(accept_cb), std::ptr::null_mut());\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, \u0026mut out) };\n\t\tassert_eq!(rc, crate::error::OAKCODEC_OK);\n\t\tassert_eq!(out.state, OAKCODEC_PROXY_STATE_GENERATING);\n\n\t\t// Invalid args.\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(std::ptr::null(), src.as_ptr(), 0, \u0026p, \u0026mut out) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, \u0026p, std::ptr::null_mut()) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\t// get_proxy_directory / get_proxy_filename / get_working_filename\n\t\t// argument validation.\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_directory(std::ptr::null(), buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_proxy_get_proxy_filename(std::ptr::null(), src.as_ptr(), 0, \u0026p, buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\t\tlet rc = unsafe { oakcodec_proxy_get_working_filename(std::ptr::null(), buf.as_mut_ptr(), 512) };\n\t\tassert_eq!(rc, OAKCODEC_E_INVALID);\n\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n\n\t#[test]\n\tfn find_ffmpeg_uses_configured_path() {\n\t\t// The current test binary is a real executable: the configured\n\t\t// path resolves to an absolute path.\n\t\tlet me = std::env::current_exe().unwrap();\n\t\tlet mc = cstr(me.to_str().unwrap());\n\t\tlet mut buf = [0i8; 1024];\n\t\tlet rc = unsafe { oakcodec_proxy_find_ffmpeg(mc.as_ptr(), buf.as_mut_ptr(), 1024) };\n\t\tassert!(rc \u003e 0);\n\t\tlet found = crate::ffi::c_str(buf.as_ptr()).unwrap();\n\t\tassert!(found.starts_with('/'));\n\n\t\t// NULL configured path falls back to the search (empty or absolute).\n\t\tlet rc = unsafe { oakcodec_proxy_find_ffmpeg(std::ptr::null(), buf.as_mut_ptr(), 1024) };\n\t\tassert!(rc \u003e 0);\n\t\tlet found = crate::ffi::c_str(buf.as_ptr()).unwrap();\n\t\tassert!(found.is_empty() || found.starts_with('/'));\n\t}\n}\n","traces":[{"line":69,"address":[],"length":0,"stats":{"Line":3}},{"line":70,"address":[],"length":0,"stats":{"Line":6}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":6}},{"line":75,"address":[],"length":0,"stats":{"Line":6}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":6}},{"line":78,"address":[],"length":0,"stats":{"Line":6}},{"line":79,"address":[],"length":0,"stats":{"Line":6}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":3}},{"line":82,"address":[],"length":0,"stats":{"Line":3}},{"line":87,"address":[],"length":0,"stats":{"Line":6}},{"line":88,"address":[],"length":0,"stats":{"Line":12}},{"line":89,"address":[],"length":0,"stats":{"Line":42}},{"line":90,"address":[],"length":0,"stats":{"Line":18}},{"line":96,"address":[],"length":0,"stats":{"Line":1}},{"line":97,"address":[],"length":0,"stats":{"Line":2}},{"line":98,"address":[],"length":0,"stats":{"Line":1}},{"line":99,"address":[],"length":0,"stats":{"Line":1}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":107,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":4}},{"line":114,"address":[],"length":0,"stats":{"Line":8}},{"line":115,"address":[],"length":0,"stats":{"Line":8}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":118,"address":[],"length":0,"stats":{"Line":6}},{"line":119,"address":[],"length":0,"stats":{"Line":3}},{"line":120,"address":[],"length":0,"stats":{"Line":6}},{"line":121,"address":[],"length":0,"stats":{"Line":6}},{"line":122,"address":[],"length":0,"stats":{"Line":6}},{"line":123,"address":[],"length":0,"stats":{"Line":6}},{"line":124,"address":[],"length":0,"stats":{"Line":6}},{"line":125,"address":[],"length":0,"stats":{"Line":6}},{"line":126,"address":[],"length":0,"stats":{"Line":12}},{"line":127,"address":[],"length":0,"stats":{"Line":6}},{"line":129,"address":[],"length":0,"stats":{"Line":3}},{"line":136,"address":[],"length":0,"stats":{"Line":3}},{"line":137,"address":[],"length":0,"stats":{"Line":6}},{"line":138,"address":[],"length":0,"stats":{"Line":8}},{"line":139,"address":[],"length":0,"stats":{"Line":10}},{"line":140,"address":[],"length":0,"stats":{"Line":1}},{"line":142,"address":[],"length":0,"stats":{"Line":2}},{"line":148,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":3}},{"line":155,"address":[],"length":0,"stats":{"Line":0}},{"line":156,"address":[],"length":0,"stats":{"Line":0}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":158,"address":[],"length":0,"stats":{"Line":0}},{"line":159,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":4}},{"line":167,"address":[],"length":0,"stats":{"Line":2}},{"line":172,"address":[],"length":0,"stats":{"Line":4}},{"line":173,"address":[],"length":0,"stats":{"Line":3}},{"line":174,"address":[],"length":0,"stats":{"Line":2}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":177,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":5}},{"line":179,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":2}},{"line":194,"address":[],"length":0,"stats":{"Line":4}},{"line":195,"address":[],"length":0,"stats":{"Line":3}},{"line":196,"address":[],"length":0,"stats":{"Line":2}},{"line":197,"address":[],"length":0,"stats":{"Line":1}},{"line":199,"address":[],"length":0,"stats":{"Line":2}},{"line":200,"address":[],"length":0,"stats":{"Line":2}},{"line":201,"address":[],"length":0,"stats":{"Line":0}},{"line":203,"address":[],"length":0,"stats":{"Line":3}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":5}},{"line":206,"address":[],"length":0,"stats":{"Line":0}},{"line":213,"address":[],"length":0,"stats":{"Line":2}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":219,"address":[],"length":0,"stats":{"Line":3}},{"line":220,"address":[],"length":0,"stats":{"Line":2}},{"line":221,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":1}},{"line":224,"address":[],"length":0,"stats":{"Line":5}},{"line":225,"address":[],"length":0,"stats":{"Line":0}},{"line":233,"address":[],"length":0,"stats":{"Line":4}},{"line":240,"address":[],"length":0,"stats":{"Line":8}},{"line":241,"address":[],"length":0,"stats":{"Line":8}},{"line":242,"address":[],"length":0,"stats":{"Line":1}},{"line":244,"address":[],"length":0,"stats":{"Line":5}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":1}},{"line":248,"address":[],"length":0,"stats":{"Line":4}},{"line":249,"address":[],"length":0,"stats":{"Line":4}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":6}},{"line":253,"address":[],"length":0,"stats":{"Line":6}},{"line":254,"address":[],"length":0,"stats":{"Line":10}},{"line":255,"address":[],"length":0,"stats":{"Line":2}},{"line":256,"address":[],"length":0,"stats":{"Line":2}},{"line":257,"address":[],"length":0,"stats":{"Line":6}},{"line":258,"address":[],"length":0,"stats":{"Line":4}},{"line":261,"address":[],"length":0,"stats":{"Line":10}},{"line":262,"address":[],"length":0,"stats":{"Line":6}},{"line":263,"address":[],"length":0,"stats":{"Line":2}},{"line":265,"address":[],"length":0,"stats":{"Line":2}},{"line":271,"address":[],"length":0,"stats":{"Line":2}},{"line":276,"address":[],"length":0,"stats":{"Line":4}},{"line":277,"address":[],"length":0,"stats":{"Line":8}},{"line":278,"address":[],"length":0,"stats":{"Line":6}},{"line":279,"address":[],"length":0,"stats":{"Line":8}}],"covered":97,"coverable":106},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffi","task.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `include/codec/task.h` exports.\n//!\n//! Complete inventory: set_task_submit_cb / task_submit_is_registered.\n//! The callback typedef and request struct are mirrored in\n//! [`crate::task`].\n\nuse std::ffi::c_int;\n\nuse crate::handle;\n\n/// `oakcodec_set_task_submit_cb`: register (or replace, or clear with\n/// NULL) the global task submit callback.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_set_task_submit_cb(\n\tcb: Option\u003ccrate::task::OakCodecTaskSubmitFn\u003e,\n\tuserdata: *mut std::ffi::c_void,\n) {\n\thandle::guard_void(|| {\n\t\tcrate::task::set_task_submit_cb_extern(cb, userdata);\n\t})\n}\n\n/// `oakcodec_task_submit_is_registered`: 1 when a callback is set.\n#[no_mangle]\npub unsafe extern \"C\" fn oakcodec_task_submit_is_registered() -\u003e c_int {\n\thandle::guard_raw(|| {\n\t\tif crate::task::task_submit_is_registered() {\n\t\t\t1\n\t\t} else {\n\t\t\t0\n\t\t}\n\t})\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::conformmanager::test_util::{accept_cb, REG_LOCK};\n\tuse crate::task::set_task_submit_cb_extern;\n\n\t#[test]\n\tfn register_query_clear() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\n\t\tassert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);\n\n\t\tunsafe { oakcodec_set_task_submit_cb(Some(accept_cb), std::ptr::null_mut()) };\n\t\tassert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 1);\n\n\t\tunsafe { oakcodec_set_task_submit_cb(None, std::ptr::null_mut()) };\n\t\tassert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);\n\n\t\t// Restore a clean slate for the other modules.\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n}\n","traces":[{"line":30,"address":[],"length":0,"stats":{"Line":2}},{"line":34,"address":[],"length":0,"stats":{"Line":4}},{"line":35,"address":[],"length":0,"stats":{"Line":6}},{"line":41,"address":[],"length":0,"stats":{"Line":3}},{"line":42,"address":[],"length":0,"stats":{"Line":6}},{"line":43,"address":[],"length":0,"stats":{"Line":3}},{"line":44,"address":[],"length":0,"stats":{"Line":1}},{"line":46,"address":[],"length":0,"stats":{"Line":2}}],"covered":8,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","ffmpeg.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `FFmpegDecoder` / `FFmpegEncoder` — real FFmpeg-backed implementations.\n//!\n//! Mirrors `src/codec/src/ffmpeg/{ffmpegdecoder,ffmpegencoder}.{h,cpp}`.\n//! Unlike the C++ side — which routes everything through the `ffmpeg_bridge`\n//! C library — this module calls the [`ffmpeg_next`] crate directly. The\n//! bridge existed only to absorb FFmpeg API churn (the `liboakffmpeg`\n//! dylib) and is deliberately not replicated. `// CPP-PARITY:` comments\n//! point at the oracle lines each behavior mirrors.\n//!\n//! ## Delivery formats\n//!\n//! Video decode delivers **F32 RGBA** frames ([`PixelFormat::F32`], the\n//! primary oakcore pipeline format): the decoded frame is run through\n//! swscale into a float-RGBA target and copied into the crate's [`Frame`]\n//! (4 channels, linesize rounded to 32 bytes — see [`Frame`]). Audio is\n//! delivered as interleaved `f32`, matching the C ABI.\n//!\n//! ## Deviations from the C++ oracle (all trait-shaped)\n//!\n//! * [`Decoder::retrieve_audio`] decodes directly (the C++ path goes\n//! through the ConformManager cache; the Rust trait carries no cache\n//! path or loop mode).\n//! * [`RetrieveVideoParams`] drops `renderer`, `divider` and\n//! `maximum_format` (the Rust trait surface), so [`Decoder::retrieve_video`]\n//! can only produce textures through an empty renderer handle and there\n//! is no preview-divider scaling.\n//! * Probing uses stream parameters (no second decode pass), so `is_still`\n//! is always false and interlacing always progressive.\n//! * Subtitle streams are counted but not added as subtitle entries.\n\nuse std::collections::VecDeque;\nuse std::path::PathBuf;\nuse std::sync::{Arc, Mutex, OnceLock};\n\nuse ffmpeg_next as ffmpeg;\nuse ffmpeg::ffi as sys;\nuse ffmpeg::format::sample::Type as SampleType;\nuse ffmpeg::format::{Pixel, Sample};\nuse ffmpeg::media::Type as MediaType;\nuse ffmpeg::software::{resampling, scaling};\nuse ffmpeg::{ChannelLayout, Dictionary, Error as FfmpegError, Rational as FfRational};\n\nuse oakcore_rs::{PixelFormat, Rational, SampleFormat, TimeRange};\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_init_basic, oakcommon_videoparams_set_channel_count,\n\toakcommon_videoparams_set_duration, oakcommon_videoparams_set_format,\n\toakcommon_videoparams_set_frame_rate, oakcommon_videoparams_set_height,\n\toakcommon_videoparams_set_interlacing, oakcommon_videoparams_set_pixel_aspect_ratio,\n\toakcommon_videoparams_set_premultiplied_alpha, oakcommon_videoparams_set_start_time,\n\toakcommon_videoparams_set_stream_index, oakcommon_videoparams_set_time_base,\n\toakcommon_videoparams_set_video_type, oakcommon_videoparams_set_width,\n\toakcore_audioparams_create, oakcore_audioparams_set_duration, oakcore_audioparams_set_stream_index,\n\toakcore_audioparams_set_time_base, OakAudioParams,\n};\nuse crate::bridge::render::{oakrender_cancelatom_is_cancelled, OakCancelAtom, OakRenderTexture};\nuse crate::decoder::{CodecStream, Decoder, RetrieveAudioStatus, RetrieveVideoParams};\nuse crate::encoder::Encoder;\nuse crate::encodingparams::EncodingParams;\nuse crate::footagedescription::{FootageDescription, StreamEntry};\nuse crate::frame::Frame;\n\n/// `OAKCOMMON_VIDEO_INTERLACE_NONE` (oakcommon `common/videoparams.h`).\nconst OAKCOMMON_VIDEO_INTERLACE_NONE: i32 = 0;\n/// `OAKCOMMON_COLOR_RANGE_FULL`.\nconst OAKCOMMON_COLOR_RANGE_FULL: i32 = 1;\n/// `OAKCOMMON_VIDEO_TYPE_VIDEO`.\nconst OAKCOMMON_VIDEO_TYPE_VIDEO: i32 = 0;\n/// The format-level time base (microseconds), `FB_TIME_BASE` in the bridge.\nconst FB_TIME_BASE: i64 = 1_000_000;\n/// `AV_NOPTS_VALUE`.\nconst AV_NOPTS_VALUE: i64 = i64::MIN;\n/// Channel count of the internal RGBA pipeline layout.\nconst VIDEO_CHANNELS: i32 = 4;\n/// Number of f32 samples in one RGBA pixel (R, G, B, A).\nconst PIXEL_F32_BYTES: usize = 16;\n\n/// Lazily initialize the FFmpeg libraries (idempotent, at most once).\nfn ffmpeg_init() -\u003e crate::error::Result\u003c()\u003e {\n\tstatic INIT: OnceLock\u003cResult\u003c(), String\u003e\u003e = OnceLock::new();\n\tif let Err(e) = INIT.get_or_init(|| {\n\t\tffmpeg::init().map_err(|e| format!(\"ffmpeg initialization failed: {e}\"))\n\t}) {\n\t\treturn Err(crate::error::Error::Failed(e.clone()));\n\t}\n\tOk(())\n}\n\n/// Wrap an FFmpeg error in the crate error type.\nfn ffmpeg_err(e: FfmpegError) -\u003e crate::error::Error {\n\tcrate::error::Error::Failed(format!(\"ffmpeg error: {e}\"))\n}\n\n/// Build a generic failure.\nfn fail(msg: impl Into\u003cString\u003e) -\u003e crate::error::Error {\n\tcrate::error::Error::Failed(msg.into())\n}\n\n/// `cancel_atom_is_cancelled` — NULL/empty-handle-safe check of an\n/// oakrender cancel atom (borrowed pointer).\n///\n/// # CPP-PARITY\n/// `src/codec/src/ffmpeg/ffmpegdecoder.cpp` (anonymous namespace helper).\nfn cancel_atom_is_cancelled(cancelled: Option\u003c\u0026OakCancelAtom\u003e) -\u003e bool {\n\tmatch cancelled {\n\t\tSome(atom) if !atom.ctx.is_null() =\u003e unsafe {\n\t\t\toakrender_cancelatom_is_cancelled(atom.clone()) != 0\n\t\t},\n\t\t_ =\u003e false,\n\t}\n}\n\n/// Whether an FFmpeg error means \"end of stream\" or \"try again\".\nfn is_eof_or_eagain(e: \u0026FfmpegError) -\u003e bool {\n\tmatches!(e, FfmpegError::Eof)\n\t\t|| matches!(e, FfmpegError::Other { errno } if *errno == ffmpeg::error::EAGAIN)\n}\n\n/// Map a \"JPEG\" (full-range) pixel format to its regular counterpart,\n/// mirroring the bridge `convert_jpeg_space_to_regular_space`.\nfn convert_jpeg_space_to_regular_space(p: Pixel) -\u003e Pixel {\n\tmatch p {\n\t\tPixel::YUVJ420P =\u003e Pixel::YUV420P,\n\t\tPixel::YUVJ422P =\u003e Pixel::YUV422P,\n\t\tPixel::YUVJ444P =\u003e Pixel::YUV444P,\n\t\tPixel::YUVJ440P =\u003e Pixel::YUV440P,\n\t\tPixel::YUVJ411P =\u003e Pixel::YUV411P,\n\t\tother =\u003e other,\n\t}\n}\n\n/// Native `OakPixelFormat` for a decoded pixel format (probe reporting).\n/// Mirrors `FFmpegDecoder::get_native_pixel_format`: 8-bit sources map to\n/// [`PixelFormat::U8`], 16-bit to [`PixelFormat::U16`], float to\n/// [`PixelFormat::F32`].\nfn native_pixel_format(p: Pixel) -\u003e PixelFormat {\n\tmatch p {\n\t\tPixel::RGBF32BE\n\t\t| Pixel::RGBF32LE\n\t\t| Pixel::RGBAF32BE\n\t\t| Pixel::RGBAF32LE\n\t\t| Pixel::GBRPF32BE\n\t\t| Pixel::GBRPF32LE =\u003e PixelFormat::F32,\n\t\tPixel::RGB48BE\n\t\t| Pixel::RGB48LE\n\t\t| Pixel::RGBA64BE\n\t\t| Pixel::RGBA64LE\n\t\t| Pixel::GBRP9BE\n\t\t| Pixel::GBRP9LE\n\t\t| Pixel::GBRP10BE\n\t\t| Pixel::GBRP10LE\n\t\t| Pixel::GBRP12BE\n\t\t| Pixel::GBRP12LE\n\t\t| Pixel::GBRP14BE\n\t\t| Pixel::GBRP14LE\n\t\t| Pixel::GBRP16BE\n\t\t| Pixel::GBRP16LE =\u003e PixelFormat::U16,\n\t\t_ =\u003e PixelFormat::U8,\n\t}\n}\n\n/// Build an ffmpeg channel layout from a legacy ffmpeg channel mask.\n///\n/// Dipped into `ffmpeg-sys-next`: `ffmpeg-next` 9 exposes `ChannelLayout`\n/// as a wrapper around `AVChannelLayout` (no mask constructor).\nfn channel_layout_from_mask(mask: u64) -\u003e ChannelLayout {\n\tif mask == 0 {\n\t\treturn ChannelLayout::default(2);\n\t}\n\tlet channels = mask.count_ones() as i32;\n\tChannelLayout(sys::AVChannelLayout {\n\t\torder: sys::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE,\n\t\tnb_channels: channels,\n\t\tu: sys::AVChannelLayout__bindgen_ty_1 { mask },\n\t\topaque: std::ptr::null_mut(),\n\t})\n}\n\n/// Convert a raw `AVSampleFormat` discriminant (as stored in\n/// `AVCodecParameters.format`) into an ffmpeg [`Sample`].\n///\n/// Dipped into `ffmpeg-sys-next`: bindgen emits the C enum as a `#[repr(i32)]`\n/// Rust enum, which cannot be built from an `i32` with an `as` cast.\nfn sample_from_raw(v: i32) -\u003e Sample {\n\t// SAFETY: `v` is always a discriminant FFmpeg itself produced; the\n\t// `repr(i32)` C enum has the same size and layout as `i32`.\n\tSample::from(unsafe { std::mem::transmute::\u003ci32, sys::AVSampleFormat\u003e(v) })\n}\n\n/// Convert a raw `AVPixelFormat` discriminant (as stored in\n/// `AVCodecParameters.format`) into an ffmpeg [`Pixel`].\n///\n/// Dipped into `ffmpeg-sys-next` (see [`sample_from_raw`]).\nfn pixel_from_raw(v: i32) -\u003e Pixel {\n\t// SAFETY: see `sample_from_raw`.\n\tPixel::from(unsafe { std::mem::transmute::\u003ci32, sys::AVPixelFormat\u003e(v) })\n}\n\n/// The oakcore timebase equivalent of an ffmpeg rational.\nfn oak_rational(r: FfRational) -\u003e Rational {\n\tRational::new(r.0 as i64, r.1 as i64)\n}\n\n// ---------------------------------------------------------------------------\n// FFmpegDecoder\n// ---------------------------------------------------------------------------\n\n/// `olive::FFmpegDecoder` — FFmpeg-backed media decoder.\npub struct FFmpegDecoder {\n\t/// Opened stream (locked).\n\tstream: std::sync::Mutex\u003cOption\u003cCodecStream\u003e\u003e,\n\t/// The open FFmpeg decode session, if any.\n\tstate: Mutex\u003cOption\u003cDecoderState\u003e\u003e,\n}\n\nimpl FFmpegDecoder {\n\t/// New, closed decoder.\n\tpub fn new() -\u003e Self {\n\t\tFFmpegDecoder {\n\t\t\tstream: std::sync::Mutex::new(None),\n\t\t\tstate: Mutex::new(None),\n\t\t}\n\t}\n}\n\nimpl Default for FFmpegDecoder {\n\tfn default() -\u003e Self {\n\t\tSelf::new()\n\t}\n}\n\nimpl Decoder for FFmpegDecoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"ffmpeg\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn probe(\u0026self, filename: \u0026str, cancelled: Option\u003c\u0026OakCancelAtom\u003e) -\u003e Option\u003cFootageDescription\u003e {\n\t\tffmpeg_init().ok()?;\n\t\tprobe_file(filename, cancelled)\n\t}\n\n\tfn open(\u0026self, stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\tffmpeg_init()?;\n\n\t\t// # CPP-PARITY\n\t\t// `src/codec/src/decoder.cpp` `Decoder::open`: already-open decoders\n\t\t// accept the same stream again, reject a different one; on failure the\n\t\t// stream is unset again.\n\t\tlet mut stream_guard = self.stream.lock().unwrap_or_else(|e| e.into_inner());\n\t\tif let Some(cur) = stream_guard.as_ref() {\n\t\t\tif cur == stream {\n\t\t\t\treturn Ok(());\n\t\t\t}\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tif !stream.is_valid() {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tif !stream.exists() {\n\t\t\treturn Err(crate::error::Error::NotFound);\n\t\t}\n\n\t\tlet opened = DecoderState::open(stream)?;\n\t\t*self.state.lock().unwrap_or_else(|e| e.into_inner()) = Some(opened);\n\t\t*stream_guard = Some(stream.clone());\n\t\tOk(())\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// C++ `Decoder::close` is void and safe when closed.\n\t\t*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = None;\n\t\t*self.state.lock().unwrap_or_else(|e| e.into_inner()) = None;\n\t\tOk(())\n\t}\n\n\tfn stream(\u0026self) -\u003e CodecStream {\n\t\tself.stream\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.clone()\n\t\t\t.unwrap_or_else(CodecStream::new)\n\t}\n\n\tfn retrieve_video_frame(\u0026self, p: \u0026RetrieveVideoParams) -\u003e crate::error::Result\u003cArc\u003cFrame\u003e\u003e {\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Video(_)) {\n\t\t\treturn Err(fail(\"decoder is not open on a video stream\"));\n\t\t}\n\n\t\t// # CPP-PARITY\n\t\t// `retrieve_video_frame_internal` (ffmpegdecoder.cpp): the decoded\n\t\t// frame is color-range-forced, scaled and copied into a `Frame` at\n\t\t// the requested timestamp.\n\t\t// Note: the Rust `RetrieveVideoParams` carries no cancellation atom\n\t\t// (dropped from the C++ struct), so decoding is not cancellable here.\n\t\tlet f = state\n\t\t\t.retrieve_frame(\u0026p.time, p.time == crate::decoder::k_any_timecode(), None)?\n\t\t\t.ok_or_else(|| fail(\"no video frame available at the requested time\"))?;\n\n\t\tlet (w, h, bytes) = state.scale_video_to_f32(f, p.force_range)?;\n\t\tlet frame = copy_rgba_f32_to_frame(w, h, \u0026bytes, p.time)?;\n\t\tOk(Arc::new(frame))\n\t}\n\n\tfn retrieve_video(\u0026self, p: \u0026RetrieveVideoParams) -\u003e crate::error::Result\u003cOakRenderTexture\u003e {\n\t\t// The Rust `RetrieveVideoParams` carries no `OakRenderRenderer`, so\n\t\t// texture creation cannot be performed — the C++ failure path returns\n\t\t// an empty texture (ffmpegdecoder.cpp `process_frame_into_texture`).\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Video(_)) {\n\t\t\treturn Err(fail(\"decoder is not open on a video stream\"));\n\t\t}\n\t\tlet _ = state.retrieve_frame(\u0026p.time, p.time == crate::decoder::k_any_timecode(), None)?;\n\t\tOk(OakRenderTexture {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t})\n\t}\n\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\tdest: \u0026mut [f32],\n\t\trange: \u0026TimeRange,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Audio(_)) {\n\t\t\t// The stream does not support audio.\n\t\t\treturn Ok(RetrieveAudioStatus::Unsupported);\n\t\t}\n\t\tif sample_rate \u003c= 0 || channel_layout == 0 {\n\t\t\treturn Ok(RetrieveAudioStatus::Unsupported);\n\t\t}\n\t\tstate.retrieve_audio_to(dest, range, sample_rate, channel_layout)\n\t}\n\n\tfn conform_audio(\n\t\t\u0026self,\n\t\toutput_filenames: \u0026[String],\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tffmpeg_init()?;\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tlet state = state.as_mut().ok_or(crate::error::Error::State)?;\n\t\tif !matches!(state.inner, DecoderInner::Audio(_)) {\n\t\t\treturn Err(fail(\"decoder is not open on an audio stream\"));\n\t\t}\n\t\tstate.conform_audio_to(output_filenames, sample_rate, channel_layout, sample_format, cancelled)\n\t}\n\n\tfn get_audio_start_offset(\u0026self) -\u003e Rational {\n\t\t// # CPP-PARITY\n\t\t// `FFmpegDecoder::get_audio_start_offset`: format start minus stream\n\t\t// start, in seconds.\n\t\tlet state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tmatch state.as_ref() {\n\t\t\tSome(s) if s.format_start_time != AV_NOPTS_VALUE =\u003e {\n\t\t\t\tlet fmt_start = Rational::new(s.format_start_time, FB_TIME_BASE);\n\t\t\t\tlet str_start = oak_rational(s.stream_time_base).timestamp_to_time(s.stream_start_time);\n\t\t\t\tfmt_start - str_start\n\t\t\t}\n\t\t\t_ =\u003e Rational::new(0, 1),\n\t\t}\n\t}\n}\n\n/// A decoded frame plus its media type.\nenum DecodedFrame {\n\tVideo(ffmpeg::frame::Video),\n\tAudio(ffmpeg::frame::Audio),\n}\n\n/// Outcome of a single decoder receive attempt.\nenum Pull {\n\tFrame(DecodedFrame),\n\tEof,\n\tEagain,\n}\n\n/// Whether feeding one packet advanced the decode.\nenum PacketFeed {\n\tSent,\n\tInputEof,\n}\n\n/// One opened (filename, stream) decode session.\nstruct DecoderState {\n\tinput: ffmpeg::format::context::Input,\n\tstream_index: usize,\n\tinner: DecoderInner,\n\tstream_time_base: FfRational,\n\tstream_start_time: i64,\n\t/// Format start time in microseconds (`AV_TIME_BASE`).\n\tformat_start_time: i64,\n\tinput_sample_format: Sample,\n\tinput_sample_rate: u32,\n\tinput_channel_layout_mask: u64,\n\t/// True once the decoder has been drained.\n\teof: bool,\n\tvideo: Option\u003cVideoDecodeState\u003e,\n\taudio: Option\u003cAudioDecodeState\u003e,\n}\n\nenum DecoderInner {\n\tVideo(ffmpeg::codec::decoder::Video),\n\tAudio(ffmpeg::codec::decoder::Audio),\n}\n\n// SAFETY: `DecoderState` owns raw FFmpeg contexts that are not thread-safe\n// themselves, but every access goes through the `FFmpegDecoder::state`\n// `Mutex` (the C++ decoder uses the same lock-discipline around its bridge\n// instance), so concurrent access never happens.\nunsafe impl Send for DecoderState {}\nunsafe impl Sync for DecoderState {}\n\n/// Video decode state: the frame cache plus a cached swscale context.\nstruct VideoDecodeState {\n\tscaler: Option\u003cScalingCache\u003e,\n\tcache: VecDeque\u003cffmpeg::frame::Video\u003e,\n\tcache_at_zero: bool,\n\tcache_at_eof: bool,\n\t/// One second in the stream's time base.\n\tsecond_ts: i64,\n}\n\n/// Cached swscale context (recreated when any dimension/format changes).\nstruct ScalingCache {\n\tctx: scaling::Context,\n\tsrc_format: Pixel,\n\tsrc_width: u32,\n\tsrc_height: u32,\n\tdst_format: Pixel,\n\tdst_width: u32,\n\tdst_height: u32,\n}\n\n/// Audio decode state: a resampler keyed by (rate, layout).\nstruct AudioDecodeState {\n\tresampler: Option\u003c(u32, u64, AudioResampler)\u003e,\n}\n\n/// A swresample conversion context.\nstruct AudioResampler {\n\tctx: resampling::Context,\n\tdst_format: Sample,\n\tdst_layout: ChannelLayout,\n\tdst_rate: u32,\n\tdst_channels: usize,\n}\n\nimpl DecoderState {\n\t/// Open `(filename, stream_index)` for decoding.\n\tfn open(stream: \u0026CodecStream) -\u003e crate::error::Result\u003cDecoderState\u003e {\n\t\tlet mut dict = Dictionary::new();\n\t\tdict.set(\"analyzeduration\", \"5000000\");\n\t\tdict.set(\"probesize\", \"20000000\");\n\t\tlet input = ffmpeg::format::input_with_dictionary(\u0026stream.filename(), dict)\n\t\t\t.map_err(ffmpeg_err)?;\n\n\t\tlet stream_index = stream.stream() as usize;\n\t\tlet fstream = input\n\t\t\t.stream(stream_index)\n\t\t\t.ok_or_else(|| fail(format!(\"no stream {}\", stream.stream())))?;\n\t\tlet params = fstream.parameters();\n\t\tlet medium = params.medium();\n\t\tlet codec_id = params.id();\n\t\tlet codec = ffmpeg::decoder::find(codec_id)\n\t\t\t.ok_or_else(|| fail(format!(\"no decoder for codec {codec_id:?}\")))?;\n\n\t\t// Per-medium stream parameters (mirroring the bridge stream info);\n\t\t// read before `params` is moved into the codec context below.\n\t\tlet mut input_sample_format = Sample::None;\n\t\tlet mut input_sample_rate = 0;\n\t\tlet mut input_channel_layout_mask = 0u64;\n\t\tif matches!(medium, MediaType::Audio) {\n\t\t\tlet raw = unsafe { params.as_ptr() };\n\t\t\tinput_sample_format = sample_from_raw(unsafe { (*raw).format });\n\t\t\tinput_sample_rate = unsafe { (*raw).sample_rate }.max(0) as u32;\n\t\t\tinput_channel_layout_mask =\n\t\t\t\tunsafe { ChannelLayout::from((*raw).ch_layout) }.bits();\n\t\t}\n\n\t\tlet mut open_opts = Dictionary::new();\n\t\topen_opts.set(\"threads\", \"auto\");\n\t\tlet opened = ffmpeg::codec::Context::from_parameters(params)\n\t\t\t.map_err(ffmpeg_err)?\n\t\t\t.decoder()\n\t\t\t.open_as_with(codec, open_opts)\n\t\t\t.map_err(ffmpeg_err)?;\n\n\t\tlet inner = match medium {\n\t\t\tMediaType::Video =\u003e DecoderInner::Video(ffmpeg::codec::decoder::Video(opened)),\n\t\t\tMediaType::Audio =\u003e DecoderInner::Audio(ffmpeg::codec::decoder::Audio(opened)),\n\t\t\tother =\u003e {\n\t\t\t\treturn Err(fail(format!(\n\t\t\t\t\t\"stream {} is {:?}, expected video or audio\",\n\t\t\t\t\tstream.stream(),\n\t\t\t\t\tother\n\t\t\t\t)))\n\t\t\t}\n\t\t};\n\n\t\tlet stream_time_base = fstream.time_base();\n\t\tlet stream_start_time = fstream.start_time();\n\t\tlet format_start_time = unsafe { (*input.as_ptr()).start_time };\n\n\t\t// One second in the stream's time base (round of den/num).\n\t\tlet second_ts = (stream_time_base.1 as f64 / stream_time_base.0 as f64).round() as i64;\n\t\tlet video = if matches!(medium, MediaType::Video) {\n\t\t\tSome(VideoDecodeState {\n\t\t\t\tscaler: None,\n\t\t\t\tcache: VecDeque::new(),\n\t\t\t\tcache_at_zero: false,\n\t\t\t\tcache_at_eof: false,\n\t\t\t\tsecond_ts,\n\t\t\t})\n\t\t} else {\n\t\t\tNone\n\t\t};\n\t\tlet audio = if matches!(medium, MediaType::Audio) {\n\t\t\tSome(AudioDecodeState { resampler: None })\n\t\t} else {\n\t\t\tNone\n\t\t};\n\n\t\tOk(DecoderState {\n\t\t\tinput,\n\t\t\tstream_index,\n\t\t\tinner,\n\t\t\tstream_time_base,\n\t\t\tstream_start_time,\n\t\t\tformat_start_time,\n\t\t\tinput_sample_format,\n\t\t\tinput_sample_rate,\n\t\t\tinput_channel_layout_mask,\n\t\t\teof: false,\n\t\t\tvideo,\n\t\t\taudio,\n\t\t})\n\t}\n\n\tfn send_packet(\u0026mut self, packet: \u0026ffmpeg::packet::Packet) -\u003e crate::error::Result\u003c()\u003e {\n\t\tmatch \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e d.send_packet(packet).map_err(ffmpeg_err),\n\t\t\tDecoderInner::Audio(d) =\u003e d.send_packet(packet).map_err(ffmpeg_err),\n\t\t}\n\t}\n\n\tfn send_eof(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tmatch \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e d.send_eof().map_err(ffmpeg_err),\n\t\t\tDecoderInner::Audio(d) =\u003e d.send_eof().map_err(ffmpeg_err),\n\t\t}\n\t}\n\n\t/// One receive attempt on the decoder.\n\tfn pull(\u0026mut self) -\u003e crate::error::Result\u003cPull\u003e {\n\t\tlet result = match \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e {\n\t\t\t\tlet mut scratch = ffmpeg::frame::Video::empty();\n\t\t\t\td.receive_frame(\u0026mut scratch).map(|_| {\n\t\t\t\t\tPull::Frame(DecodedFrame::Video(std::mem::replace(\n\t\t\t\t\t\t\u0026mut scratch,\n\t\t\t\t\t\tffmpeg::frame::Video::empty(),\n\t\t\t\t\t)))\n\t\t\t\t})\n\t\t\t}\n\t\t\tDecoderInner::Audio(d) =\u003e {\n\t\t\t\tlet mut scratch = ffmpeg::frame::Audio::empty();\n\t\t\t\td.receive_frame(\u0026mut scratch).map(|_| {\n\t\t\t\t\tPull::Frame(DecodedFrame::Audio(std::mem::replace(\n\t\t\t\t\t\t\u0026mut scratch,\n\t\t\t\t\t\tffmpeg::frame::Audio::empty(),\n\t\t\t\t\t)))\n\t\t\t\t})\n\t\t\t}\n\t\t};\n\t\tmatch result {\n\t\t\tOk(p) =\u003e Ok(p),\n\t\t\tErr(FfmpegError::Eof) =\u003e Ok(Pull::Eof),\n\t\t\tErr(FfmpegError::Other { errno }) if errno == ffmpeg::error::EAGAIN =\u003e Ok(Pull::Eagain),\n\t\t\tErr(e) =\u003e Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\n\t/// Feed the next packet of the opened stream into the decoder.\n\tfn feed_packet(\u0026mut self) -\u003e crate::error::Result\u003cPacketFeed\u003e {\n\t\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\t\tmatch pkt.read(\u0026mut self.input) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tif pkt.stream() == self.stream_index {\n\t\t\t\t\tself.send_packet(\u0026pkt)?;\n\t\t\t\t}\n\t\t\t\tOk(PacketFeed::Sent)\n\t\t\t}\n\t\t\tErr(FfmpegError::Eof) =\u003e {\n\t\t\t\tself.send_eof()?;\n\t\t\t\tOk(PacketFeed::InputEof)\n\t\t\t}\n\t\t\tErr(e) =\u003e Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\n\t/// Pull the next decoded frame, feeding packets as needed. Returns\n\t/// `Ok(None)` once the decoder is drained.\n\tfn next_frame(\u0026mut self) -\u003e crate::error::Result\u003cOption\u003cDecodedFrame\u003e\u003e {\n\t\tif self.eof {\n\t\t\treturn Ok(None);\n\t\t}\n\t\tloop {\n\t\t\tmatch self.pull()? {\n\t\t\t\tPull::Frame(f) =\u003e return Ok(Some(f)),\n\t\t\t\tPull::Eof =\u003e {\n\t\t\t\t\tself.eof = true;\n\t\t\t\t\treturn Ok(None);\n\t\t\t\t}\n\t\t\t\tPull::Eagain =\u003e {\n\t\t\t\t\tself.feed_packet()?;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Seek to `timestamp` (in the stream's time base) and flush the decoder.\n\t///\n\t/// # CPP-PARITY\n\t/// `fb_decoder_seek` semantics (backward seek on the stream, decoder\n\t/// flushed afterwards).\n\tfn seek(\u0026mut self, timestamp: i64) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet stream_tb = self\n\t\t\t.input\n\t\t\t.stream(self.stream_index)\n\t\t\t.map(|s| s.time_base())\n\t\t\t.unwrap_or(FfRational(1, 1));\n\t\tlet target = unsafe {\n\t\t\tsys::av_rescale_q(timestamp, self.stream_time_base.into(), stream_tb.into())\n\t\t};\n\t\tlet ret = unsafe {\n\t\t\tsys::av_seek_frame(\n\t\t\t\tself.input.as_mut_ptr(),\n\t\t\t\tself.stream_index as i32,\n\t\t\t\ttarget,\n\t\t\t\tsys::AVSEEK_FLAG_BACKWARD,\n\t\t\t)\n\t\t};\n\t\tif ret \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(ret)));\n\t\t}\n\t\tmatch \u0026mut self.inner {\n\t\t\tDecoderInner::Video(d) =\u003e d.flush(),\n\t\t\tDecoderInner::Audio(d) =\u003e d.flush(),\n\t\t}\n\t\tself.eof = false;\n\t\tOk(())\n\t}\n\n\t/// Retrieve the video frame at (or before) `time`, mirroring the C++\n\t/// `retrieve_frame` seek/cache logic.\n\tfn retrieve_frame(\n\t\t\u0026mut self,\n\t\ttime: \u0026Rational,\n\t\tany_timecode: bool,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003cOption\u003cffmpeg::frame::Video\u003e\u003e {\n\t\t// Move the video state out so `self.seek` / `self.pull` (which touch\n\t\t// other fields) can be called without conflicting borrows.\n\t\tlet mut video = self\n\t\t\t.video\n\t\t\t.take()\n\t\t\t.expect(\"retrieve_frame requires a video session\");\n\n\t\tlet mut target_ts = oak_rational(self.stream_time_base).time_to_timestamp(*time);\n\t\tif self.format_start_time != AV_NOPTS_VALUE {\n\t\t\ttarget_ts += unsafe {\n\t\t\t\tsys::av_rescale_q(\n\t\t\t\t\tself.format_start_time,\n\t\t\t\t\tFfRational(1, FB_TIME_BASE as i32).into(),\n\t\t\t\t\tself.stream_time_base.into(),\n\t\t\t\t)\n\t\t\t};\n\t\t}\n\n\t\tconst MIN_SEEK: i64 = 0;\n\t\tlet mut seek_ts = (target_ts - maximum_queue_size() as i64).max(MIN_SEEK);\n\t\tlet mut still_seeking = false;\n\n\t\tif !any_timecode {\n\t\t\t// If the frame wasn't in the frame cache, see if this cache is\n\t\t\t// too old to use (CPP-PARITY ffmpegdecoder.cpp:975).\n\t\t\tif video.cache.is_empty()\n\t\t\t\t|| target_ts \u003c pts_of(video.cache.front()).unwrap_or(AV_NOPTS_VALUE)\n\t\t\t\t|| target_ts\n\t\t\t\t\t\u003e pts_of(video.cache.back()).unwrap_or(AV_NOPTS_VALUE) + 2 * video.second_ts\n\t\t\t{\n\t\t\t\tvideo.cache.clear();\n\t\t\t\tvideo.cache_at_eof = false;\n\n\t\t\t\tself.seek(seek_ts)?;\n\t\t\t\tif seek_ts == MIN_SEEK {\n\t\t\t\t\tvideo.cache_at_zero = true;\n\t\t\t\t}\n\n\t\t\t\tstill_seeking = true;\n\t\t\t} else if let Some(cached) = get_frame_from_cache(\u0026video, target_ts) {\n\t\t\t\tself.video = Some(video);\n\t\t\t\treturn Ok(Some(cached));\n\t\t\t}\n\t\t}\n\n\t\tlet mut retried_after_eof = false;\n\t\tlet mut return_frame: Option\u003cffmpeg::frame::Video\u003e = None;\n\n\t\tloop {\n\t\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet frame = match self.pull()? {\n\t\t\t\tPull::Frame(DecodedFrame::Video(f)) =\u003e f,\n\t\t\t\tPull::Frame(_) =\u003e unreachable!(\"video session yields only video frames\"),\n\t\t\t\tPull::Eof =\u003e {\n\t\t\t\t\t// Handle an \"expected\" EOF by using the last cached frame\n\t\t\t\t\t// (CPP-PARITY ffmpegdecoder.cpp:1043).\n\t\t\t\t\tvideo.cache_at_eof = true;\n\t\t\t\t\tif video.cache.is_empty() {\n\t\t\t\t\t\tif !retried_after_eof {\n\t\t\t\t\t\t\tretried_after_eof = true;\n\t\t\t\t\t\t\tvideo.cache.clear();\n\t\t\t\t\t\t\tself.seek(MIN_SEEK)?;\n\t\t\t\t\t\t\tvideo.cache_at_zero = true;\n\t\t\t\t\t\t\tstill_seeking = true;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tself.video = Some(video);\n\t\t\t\t\t\treturn Err(fail(\"unexpected codec EOF - unable to retrieve frame\"));\n\t\t\t\t\t}\n\t\t\t\t\treturn_frame = video.cache.back().cloned();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tPull::Eagain =\u003e {\n\t\t\t\t\tself.feed_packet()?;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet frame_pts = frame.pts().unwrap_or(AV_NOPTS_VALUE);\n\n\t\t\tif still_seeking {\n\t\t\t\t// Handle a failed seek: step back by one second and retry\n\t\t\t\t// (CPP-PARITY ffmpegdecoder.cpp:1028).\n\t\t\t\tif !video.cache_at_zero \u0026\u0026 frame_pts \u003e target_ts {\n\t\t\t\t\tseek_ts = (seek_ts - video.second_ts).max(MIN_SEEK);\n\t\t\t\t\tself.seek(seek_ts)?;\n\t\t\t\t\tif seek_ts == MIN_SEEK {\n\t\t\t\t\t\tvideo.cache_at_zero = true;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tstill_seeking = false;\n\t\t\t}\n\n\t\t\t// Cut the cache down to `maximum_queue_size` before pushing\n\t\t\t// (CPP-PARITY ffmpegdecoder.cpp:1065).\n\t\t\tif video.cache.len() \u003e maximum_queue_size() {\n\t\t\t\tvideo.cache.pop_front();\n\t\t\t\tvideo.cache_at_zero = false;\n\t\t\t}\n\n\t\t\tlet previous = video.cache.back().cloned();\n\t\t\tvideo.cache.push_back(frame);\n\n\t\t\tif frame_pts == target_ts || any_timecode {\n\t\t\t\treturn_frame = video.cache.back().cloned();\n\t\t\t\tbreak;\n\t\t\t} else if frame_pts \u003e target_ts {\n\t\t\t\tif previous.is_none() \u0026\u0026 video.cache_at_zero {\n\t\t\t\t\treturn_frame = video.cache.back().cloned();\n\t\t\t\t} else {\n\t\t\t\t\treturn_frame = previous;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tself.video = Some(video);\n\t\tOk(return_frame)\n\t}\n\n\t/// Scale a decoded frame to float RGBA (F32, 4 channels), returning the\n\t/// raw pixel bytes plus dimensions. Mirrors `pre_process_frame` +\n\t/// `retrieve_video_frame_internal` scaling with the color-range forcing.\n\tfn scale_video_to_f32(\n\t\t\u0026mut self,\n\t\tf: ffmpeg::frame::Video,\n\t\tforce_range: i32,\n\t) -\u003e crate::error::Result\u003c(u32, u32, Vec\u003cu8\u003e)\u003e {\n\t\tlet video = self\n\t\t\t.video\n\t\t\t.as_mut()\n\t\t\t.expect(\"scale_video_to_f32 requires a video session\");\n\n\t\t// # CPP-PARITY ffmpegdecoder.cpp:376: disregard \"JPEG\" pixel formats\n\t\t// and force the color range to whatever the caller requested.\n\t\tlet src_format = convert_jpeg_space_to_regular_space(f.format());\n\t\tlet mut f = f;\n\t\tf.set_format(src_format);\n\t\tf.set_color_range(if force_range == OAKCOMMON_COLOR_RANGE_FULL {\n\t\t\tffmpeg::color::Range::JPEG\n\t\t} else {\n\t\t\tffmpeg::color::Range::MPEG\n\t\t});\n\n\t\tlet (w, h) = (f.width(), f.height());\n\n\t\t// swscale cannot reliably output float RGBA on every build; prefer\n\t\t// RGBAF32LE and fall back to RGBA64 (converted to f32 below).\n\t\tlet bytes = match get_or_create_scaler(\u0026mut video.scaler, src_format, w, h, Pixel::RGBAF32LE, w, h) {\n\t\t\tOk(ctx) =\u003e {\n\t\t\t\tlet mut out = ffmpeg::frame::Video::empty();\n\t\t\t\tctx.run(\u0026f, \u0026mut out).map_err(ffmpeg_err)?;\n\t\t\t\tlet stride = out.stride(0);\n\t\t\t\tconvert_rgba_f32_le(\u0026out.data(0), w, h, stride)\n\t\t\t}\n\t\t\tErr(_) =\u003e {\n\t\t\t\tlet ctx = get_or_create_scaler(\u0026mut video.scaler, src_format, w, h, Pixel::RGBA64LE, w, h)?;\n\t\t\t\tlet mut out = ffmpeg::frame::Video::empty();\n\t\t\t\tctx.run(\u0026f, \u0026mut out).map_err(ffmpeg_err)?;\n\t\t\t\tlet stride = out.stride(0);\n\t\t\t\tconvert_rgba64_to_f32(\u0026out.data(0), w, h, stride)\n\t\t\t}\n\t\t};\n\t\tOk((w, h, bytes))\n\t}\n\n\t/// Fill `dest` (interleaved f32) with the decoded audio covering\n\t/// `range`, resampled to `sample_rate` / `channel_layout`. Samples\n\t/// outside the decoded media are left as silence.\n\tfn retrieve_audio_to(\n\t\t\u0026mut self,\n\t\tdest: \u0026mut [f32],\n\t\trange: \u0026TimeRange,\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t) -\u003e crate::error::Result\u003cRetrieveAudioStatus\u003e {\n\t\tlet dst_layout = channel_layout_from_mask(channel_layout);\n\t\tlet dst_channels = dst_layout.channels().max(0) as usize;\n\t\tif dst_channels == 0 {\n\t\t\treturn Ok(RetrieveAudioStatus::Unsupported);\n\t\t}\n\n\t\tlet start_sec = range.in_().to_f64();\n\t\tlet end_sec = range.out().to_f64();\n\t\tlet start_sample = (start_sec * sample_rate as f64).round() as i64;\n\t\tlet end_sample = (end_sec * sample_rate as f64).round() as i64;\n\t\tif end_sample \u003c= start_sample {\n\t\t\treturn Ok(RetrieveAudioStatus::Success);\n\t\t}\n\n\t\tdest.fill(0.0);\n\n\t\t// Seek to just before the range start.\n\t\tlet start_ts = oak_rational(self.stream_time_base)\n\t\t\t.time_to_timestamp(Rational::from_double(start_sec));\n\t\tself.seek(start_ts)?;\n\n\t\t// Take the cached resampler out (or create one) so the decode loop\n\t\t// below can borrow `self` freely; it is put back before returning.\n\t\tlet src_layout = channel_layout_from_mask(self.input_channel_layout_mask);\n\t\tlet mut resampler = match self\n\t\t\t.audio\n\t\t\t.as_mut()\n\t\t\t.expect(\"audio session\")\n\t\t\t.resampler\n\t\t\t.take()\n\t\t{\n\t\t\tSome((rate, layout, rs)) if rate == sample_rate as u32 \u0026\u0026 layout == channel_layout =\u003e rs,\n\t\t\t_ =\u003e AudioResampler::get(\n\t\t\t\tself.input_sample_format,\n\t\t\t\tsrc_layout,\n\t\t\t\tself.input_sample_rate,\n\t\t\t\tSample::F32(SampleType::Packed),\n\t\t\t\tdst_layout,\n\t\t\t\tsample_rate as u32,\n\t\t\t)?,\n\t\t};\n\t\tlet stream_time_base = self.stream_time_base;\n\n\t\tlet mut next_sample: Option\u003ci64\u003e = None;\n\t\tlet dest_frames = (dest.len() / dst_channels) as i64;\n\n\t\twhile let Some(frame) = self.next_frame()? {\n\t\t\tlet DecodedFrame::Audio(audio) = frame else {\n\t\t\t\tcontinue;\n\t\t\t};\n\t\t\tlet converted = resample_to_interleaved_f32(\u0026mut resampler, \u0026audio)?;\n\t\t\tif converted.is_empty() {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet chunk_samples = (converted.len() / dst_channels) as i64;\n\t\t\tlet frame_start = match audio.pts() {\n\t\t\t\tSome(pts) =\u003e {\n\t\t\t\t\tlet secs = oak_rational(stream_time_base).timestamp_to_time(pts).to_f64();\n\t\t\t\t\t(secs * sample_rate as f64).round() as i64\n\t\t\t\t}\n\t\t\t\tNone =\u003e next_sample.unwrap_or(start_sample),\n\t\t\t};\n\t\t\tlet offset = frame_start - start_sample;\n\t\t\tif offset \u003c dest_frames \u0026\u0026 offset + chunk_samples \u003e 0 {\n\t\t\t\tlet copy_start = offset.max(0) as usize;\n\t\t\t\tlet copy_end = (offset + chunk_samples).min(dest_frames).max(0) as usize;\n\t\t\t\tif copy_end \u003e copy_start {\n\t\t\t\t\tlet src_off = (copy_start as i64 - offset) as usize * dst_channels;\n\t\t\t\t\tlet n = (copy_end - copy_start) * dst_channels;\n\t\t\t\t\tlet dst_off = copy_start * dst_channels;\n\t\t\t\t\tif dst_off + n \u003c= dest.len() {\n\t\t\t\t\t\tdest[dst_off..dst_off + n]\n\t\t\t\t\t\t\t.copy_from_slice(\u0026converted[src_off..src_off + n]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tnext_sample = Some(frame_start + chunk_samples);\n\t\t\tif offset + chunk_samples \u003e= dest_frames {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Put the resampler back into the cache for the next call.\n\t\tself.audio\n\t\t\t.as_mut()\n\t\t\t.expect(\"audio session\")\n\t\t\t.resampler = Some((sample_rate as u32, channel_layout, resampler));\n\n\t\t// Flush any samples still buffered in the resampler (rate conversion\n\t\t// tail), appending after the last decoded sample.\n\t\tlet resampler = self\n\t\t\t.audio\n\t\t\t.as_mut()\n\t\t\t.expect(\"audio session\")\n\t\t\t.resampler\n\t\t\t.as_mut()\n\t\t\t.map(|r| \u0026mut r.2)\n\t\t\t.unwrap();\n\t\tif let Some(flush) = flush_resampler_interleaved_f32(resampler)? {\n\t\t\tif !flush.is_empty() {\n\t\t\t\tlet chunk_samples = (flush.len() / dst_channels) as i64;\n\t\t\t\tlet frame_start = next_sample.unwrap_or(start_sample);\n\t\t\t\tlet offset = frame_start - start_sample;\n\t\t\t\tif offset \u003c dest_frames \u0026\u0026 offset + chunk_samples \u003e 0 {\n\t\t\t\t\tlet copy_start = offset.max(0) as usize;\n\t\t\t\t\tlet copy_end = (offset + chunk_samples).min(dest_frames).max(0) as usize;\n\t\t\t\t\tif copy_end \u003e copy_start {\n\t\t\t\t\t\tlet src_off = (copy_start as i64 - offset) as usize * dst_channels;\n\t\t\t\t\t\tlet n = (copy_end - copy_start) * dst_channels;\n\t\t\t\t\t\tlet dst_off = copy_start * dst_channels;\n\t\t\t\t\t\tif dst_off + n \u003c= dest.len() {\n\t\t\t\t\t\t\tdest[dst_off..dst_off + n]\n\t\t\t\t\t\t\t\t.copy_from_slice(\u0026flush[src_off..src_off + n]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tOk(RetrieveAudioStatus::Success)\n\t}\n\n\t/// Conform the open stream's audio into per-channel PCM files, mirroring\n\t/// `FFmpegDecoder::conform_audio_internal`.\n\tfn conform_audio_to(\n\t\t\u0026mut self,\n\t\toutput_filenames: \u0026[String],\n\t\tsample_rate: i32,\n\t\tchannel_layout: u64,\n\t\tsample_format: i32,\n\t\tcancelled: Option\u003c\u0026OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif self.input_channel_layout_mask == 0 {\n\t\t\treturn Err(fail(\"could not determine the channel layout of the audio file\"));\n\t\t}\n\n\t\tlet target_fmt = crate::encodingparams::sample_format_from_i32(sample_format);\n\t\tlet dst_sample = sample_format_to_ffmpeg(target_fmt)\n\t\t\t.ok_or_else(|| fail(format!(\"unsupported conform sample format {sample_format}\")))?;\n\t\tlet dst_layout = channel_layout_from_mask(channel_layout);\n\t\tlet planar = dst_sample.is_planar();\n\t\tlet planes = if planar {\n\t\t\tdst_layout.channels().max(0) as usize\n\t\t} else {\n\t\t\t1\n\t\t};\n\t\tif planes == 0 {\n\t\t\treturn Err(fail(\"invalid conform channel layout\"));\n\t\t}\n\n\t\t// Seek to the start (CPP-PARITY ffmpegdecoder.cpp:694).\n\t\tself.seek(0)?;\n\n\t\tlet mut resampler = AudioResampler::get(\n\t\t\tself.input_sample_format,\n\t\t\tchannel_layout_from_mask(self.input_channel_layout_mask),\n\t\t\tself.input_sample_rate,\n\t\t\tdst_sample,\n\t\t\tdst_layout,\n\t\t\tsample_rate as u32,\n\t\t)?;\n\n\t\tlet filenames: Vec\u003cPathBuf\u003e = output_filenames.iter().map(PathBuf::from).collect();\n\t\tlet mut wave_out = crate::planarfiledevice::PlanarFileDevice::new();\n\t\tif !wave_out.open(\u0026filenames, crate::planarfiledevice::OpenMode::WriteOnly) {\n\t\t\treturn Err(fail(\"failed to open conform output files\"));\n\t\t}\n\n\t\tlet bytes_per_sample = dst_sample.bytes();\n\t\twhile let Some(frame) = self.next_frame()? {\n\t\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet DecodedFrame::Audio(audio) = frame else {\n\t\t\t\tcontinue;\n\t\t\t};\n\t\t\tlet planes_buf = resampler.convert_to_planes(\u0026audio)?;\n\t\t\tlet written = planes_buf.first().map_or(0, Vec::len) / bytes_per_sample;\n\t\t\tif written \u003e 0 {\n\t\t\t\tlet refs: Vec\u003c\u0026[u8]\u003e = planes_buf.iter().map(|p| p.as_slice()).collect();\n\t\t\t\twave_out.write(\u0026refs, written as i64 * bytes_per_sample as i64, 0);\n\t\t\t}\n\t\t}\n\t\t// Flush the resampler tail.\n\t\tfor planes_buf in resampler.flush_planes()? {\n\t\t\tlet written = planes_buf.first().map_or(0, Vec::len) / bytes_per_sample;\n\t\t\tif written \u003e 0 {\n\t\t\t\tlet refs: Vec\u003c\u0026[u8]\u003e = planes_buf.iter().map(|p| p.as_slice()).collect();\n\t\t\t\twave_out.write(\u0026refs, written as i64 * bytes_per_sample as i64, 0);\n\t\t\t}\n\t\t}\n\t\twave_out.close();\n\n\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\treturn Err(crate::error::Error::Cancelled);\n\t\t}\n\t\tOk(())\n\t}\n\n}\n\nimpl AudioResampler {\n\t/// Create a resampler converting from the source definition to the\n\t/// destination definition.\n\tfn get(\n\t\tsrc_format: Sample,\n\t\tsrc_layout: ChannelLayout,\n\t\tsrc_rate: u32,\n\t\tdst_format: Sample,\n\t\tdst_layout: ChannelLayout,\n\t\tdst_rate: u32,\n\t) -\u003e crate::error::Result\u003cAudioResampler\u003e {\n\t\tlet ctx = resampling::Context::get(\n\t\t\tsrc_format, src_layout, src_rate, dst_format, dst_layout, dst_rate,\n\t\t)\n\t\t.map_err(ffmpeg_err)?;\n\t\tOk(AudioResampler {\n\t\t\tctx,\n\t\t\tdst_format,\n\t\t\tdst_layout,\n\t\t\tdst_rate,\n\t\t\tdst_channels: dst_layout.channels().max(0) as usize,\n\t\t})\n\t}\n\n\t/// Convert one input frame, returning one byte buffer per output plane\n\t/// (plane 0 for packed destinations).\n\tfn convert_to_planes(\u0026mut self, input: \u0026ffmpeg::frame::Audio) -\u003e crate::error::Result\u003cVec\u003cVec\u003cu8\u003e\u003e\u003e {\n\t\tlet (written, bufs) = self.swr_convert_buffers(input)?;\n\t\tlet _ = written;\n\t\tOk(bufs)\n\t}\n\n\t/// Convert one input frame into a freshly allocated output frame in the\n\t/// destination format, layout and rate.\n\tfn convert_to_frame(\u0026mut self, input: \u0026ffmpeg::frame::Audio) -\u003e crate::error::Result\u003cffmpeg::frame::Audio\u003e {\n\t\tlet (written, bufs) = self.swr_convert_buffers(input)?;\n\t\tlet mut out = ffmpeg::frame::Audio::new(self.dst_format, written, self.dst_layout);\n\t\tout.set_rate(self.dst_rate);\n\t\tfor (plane, buf) in bufs.iter().enumerate() {\n\t\t\tlet len = buf.len().min(out.data_mut(plane).len());\n\t\t\tout.data_mut(plane)[..len].copy_from_slice(\u0026buf[..len]);\n\t\t}\n\t\tOk(out)\n\t}\n\n\t/// Flush any samples still buffered inside the resampler, as planes.\n\tfn flush_planes(\u0026mut self) -\u003e crate::error::Result\u003cVec\u003cVec\u003cVec\u003cu8\u003e\u003e\u003e\u003e {\n\t\tlet mut all: Vec\u003cVec\u003cVec\u003cu8\u003e\u003e\u003e = Vec::new();\n\t\tfor _ in 0..32 {\n\t\t\tlet out_samples = unsafe { sys::swr_get_out_samples(self.ctx.as_mut_ptr(), 0) };\n\t\t\tif out_samples \u003c= 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet (bufs, written) = self.swr_convert(out_samples as usize, None, 0)?;\n\t\t\tif written == 0 {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tall.push(bufs);\n\t\t}\n\t\tOk(all)\n\t}\n\n\t/// Run the swr conversion for `input`, returning the number of written\n\t/// samples and one byte buffer per output plane.\n\tfn swr_convert_buffers(\n\t\t\u0026mut self,\n\t\tinput: \u0026ffmpeg::frame::Audio,\n\t) -\u003e crate::error::Result\u003c(usize, Vec\u003cVec\u003cu8\u003e\u003e)\u003e {\n\t\tlet in_samples = input.samples() as i32;\n\t\tlet out_samples = unsafe { sys::swr_get_out_samples(self.ctx.as_mut_ptr(), in_samples) };\n\t\tif out_samples \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(out_samples)));\n\t\t}\n\t\tif out_samples == 0 {\n\t\t\treturn Ok((0, Vec::new()));\n\t\t}\n\t\tlet in_ptrs: Vec\u003c*const u8\u003e = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();\n\t\tlet (bufs, written) = self.swr_convert(out_samples as usize, Some(\u0026in_ptrs), in_samples)?;\n\t\tOk((written, bufs))\n\t}\n\n\t/// Allocate output buffers for `out_samples` and run `swr_convert`\n\t/// (with the given input pointers, or a drain when `None`).\n\tfn swr_convert(\n\t\t\u0026mut self,\n\t\tout_samples: usize,\n\t\tin_ptrs: Option\u003c\u0026[*const u8]\u003e,\n\t\tin_samples: i32,\n\t) -\u003e crate::error::Result\u003c(Vec\u003cVec\u003cu8\u003e\u003e, usize)\u003e {\n\t\tlet planar = self.dst_format.is_planar();\n\t\tlet planes = if planar { self.dst_channels.max(1) } else { 1 };\n\t\tlet plane_len =\n\t\t\tout_samples * self.dst_format.bytes() * if planar { 1 } else { self.dst_channels };\n\t\tlet mut bufs: Vec\u003cVec\u003cu8\u003e\u003e = (0..planes).map(|_| vec![0u8; plane_len]).collect();\n\t\tlet mut out_ptrs: Vec\u003c*mut u8\u003e = bufs.iter_mut().map(|b| b.as_mut_ptr()).collect();\n\t\tlet written = unsafe {\n\t\t\tsys::swr_convert(\n\t\t\t\tself.ctx.as_mut_ptr(),\n\t\t\t\tout_ptrs.as_mut_ptr(),\n\t\t\t\tout_samples as i32,\n\t\t\t\tin_ptrs.map_or(std::ptr::null(), |p| p.as_ptr()),\n\t\t\t\tin_samples,\n\t\t\t)\n\t\t};\n\t\tif written \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(written)));\n\t\t}\n\t\tfor buf in bufs.iter_mut() {\n\t\t\tlet keep = written as usize * self.dst_format.bytes()\n\t\t\t\t* if planar { 1 } else { self.dst_channels };\n\t\t\tbuf.truncate(keep);\n\t\t}\n\t\tOk((bufs, written as usize))\n\t}\n}\n\n/// Convert a decoded audio frame to interleaved f32 at the resampler's\n/// destination definition.\nfn resample_to_interleaved_f32(\n\tresampler: \u0026mut AudioResampler,\n\tinput: \u0026ffmpeg::frame::Audio,\n) -\u003e crate::error::Result\u003cVec\u003cf32\u003e\u003e {\n\tlet in_samples = input.samples() as i32;\n\tlet out_samples = unsafe { sys::swr_get_out_samples(resampler.ctx.as_mut_ptr(), in_samples) };\n\tif out_samples \u003c 0 {\n\t\treturn Err(ffmpeg_err(FfmpegError::from(out_samples)));\n\t}\n\tif out_samples == 0 {\n\t\treturn Ok(Vec::new());\n\t}\n\tlet out_samples = out_samples as usize;\n\tlet channels = resampler.dst_channels;\n\tlet mut buf = vec![0f32; out_samples * channels];\n\tlet out_ptrs = [buf.as_mut_ptr() as *mut u8];\n\tlet in_ptrs: Vec\u003c*const u8\u003e = (0..input.planes()).map(|i| input.data(i).as_ptr()).collect();\n\tlet written = unsafe {\n\t\tsys::swr_convert(\n\t\t\tresampler.ctx.as_mut_ptr(),\n\t\t\tout_ptrs.as_ptr(),\n\t\t\tout_samples as i32,\n\t\t\tin_ptrs.as_ptr(),\n\t\t\tin_samples,\n\t\t)\n\t};\n\tif written \u003c 0 {\n\t\treturn Err(ffmpeg_err(FfmpegError::from(written)));\n\t}\n\tbuf.truncate(written as usize * channels);\n\tOk(buf)\n}\n\n/// Flush any samples buffered in the resampler as interleaved f32.\nfn flush_resampler_interleaved_f32(\n\tresampler: \u0026mut AudioResampler,\n) -\u003e crate::error::Result\u003cOption\u003cVec\u003cf32\u003e\u003e\u003e {\n\tlet channels = resampler.dst_channels;\n\tlet mut out = Vec::new();\n\tfor _ in 0..32 {\n\t\tlet out_samples = unsafe { sys::swr_get_out_samples(resampler.ctx.as_mut_ptr(), 0) };\n\t\tif out_samples \u003c= 0 {\n\t\t\tbreak;\n\t\t}\n\t\tlet out_samples = out_samples as usize;\n\t\tlet mut buf = vec![0f32; out_samples * channels];\n\t\tlet out_ptrs = [buf.as_mut_ptr() as *mut u8];\n\t\tlet written = unsafe {\n\t\t\tsys::swr_convert(\n\t\t\t\tresampler.ctx.as_mut_ptr(),\n\t\t\t\tout_ptrs.as_ptr(),\n\t\t\t\tout_samples as i32,\n\t\t\t\tstd::ptr::null(),\n\t\t\t\t0,\n\t\t\t)\n\t\t};\n\t\tif written \u003c 0 {\n\t\t\treturn Err(ffmpeg_err(FfmpegError::from(written)));\n\t\t}\n\t\tif written == 0 {\n\t\t\tbreak;\n\t\t}\n\t\tbuf.truncate(written as usize * channels);\n\t\tout.extend_from_slice(\u0026buf);\n\t}\n\tif out.is_empty() {\n\t\tOk(None)\n\t} else {\n\t\tOk(Some(out))\n\t}\n}\n\n/// Get the cached scaler for a (src → dst) pair, recreating it when the\n/// parameters change.\nfn get_or_create_scaler(\n\tcache: \u0026mut Option\u003cScalingCache\u003e,\n\tsrc_format: Pixel,\n\tsrc_width: u32,\n\tsrc_height: u32,\n\tdst_format: Pixel,\n\tdst_width: u32,\n\tdst_height: u32,\n) -\u003e crate::error::Result\u003c\u0026mut scaling::Context\u003e {\n\tlet recreate = match cache.as_ref() {\n\t\tSome(s) =\u003e {\n\t\t\ts.src_format != src_format\n\t\t\t\t|| s.src_width != src_width\n\t\t\t\t|| s.src_height != src_height\n\t\t\t\t|| s.dst_format != dst_format\n\t\t\t\t|| s.dst_width != dst_width\n\t\t\t\t|| s.dst_height != dst_height\n\t\t}\n\t\tNone =\u003e true,\n\t};\n\tif recreate {\n\t\tlet ctx = scaling::Context::get(\n\t\t\tsrc_format,\n\t\t\tsrc_width,\n\t\t\tsrc_height,\n\t\t\tdst_format,\n\t\t\tdst_width,\n\t\t\tdst_height,\n\t\t\tscaling::Flags::POINT,\n\t\t)\n\t\t.map_err(ffmpeg_err)?;\n\t\t*cache = Some(ScalingCache {\n\t\t\tctx,\n\t\t\tsrc_format,\n\t\t\tsrc_width,\n\t\t\tsrc_height,\n\t\t\tdst_format,\n\t\t\tdst_width,\n\t\t\tdst_height,\n\t\t});\n\t}\n\tOk(\u0026mut cache.as_mut().expect(\"set above\").ctx)\n}\n\n/// The frame's presentation timestamp (NOPTS when unset).\nfn pts_of(f: Option\u003c\u0026ffmpeg::frame::Video\u003e) -\u003e Option\u003ci64\u003e {\n\tf.and_then(|f| f.pts())\n}\n\n/// Search the frame cache for the frame at (or closest to) `t`.\n///\n/// # CPP-PARITY\n/// `FFmpegDecoder::get_frame_from_cache`.\nfn get_frame_from_cache(video: \u0026VideoDecodeState, t: i64) -\u003e Option\u003cffmpeg::frame::Video\u003e {\n\tlet front = pts_of(video.cache.front()).unwrap_or(AV_NOPTS_VALUE);\n\tlet back = pts_of(video.cache.back()).unwrap_or(AV_NOPTS_VALUE);\n\tif t \u003c front {\n\t\tif video.cache_at_zero {\n\t\t\treturn video.cache.front().cloned();\n\t\t}\n\t} else if t \u003e back {\n\t\tif video.cache_at_eof {\n\t\t\treturn video.cache.back().cloned();\n\t\t}\n\t} else {\n\t\tfor (i, frame) in video.cache.iter().enumerate() {\n\t\t\tlet this_pts = frame.pts().unwrap_or(AV_NOPTS_VALUE);\n\t\t\tlet next_pts = video\n\t\t\t\t.cache\n\t\t\t\t.get(i + 1)\n\t\t\t\t.and_then(|f| f.pts())\n\t\t\t\t.unwrap_or(AV_NOPTS_VALUE);\n\t\t\tif this_pts == t || next_pts \u003e t {\n\t\t\t\treturn Some(frame.clone());\n\t\t\t}\n\t\t}\n\t}\n\tNone\n}\n\n/// Decoder frame-cache size (C++ `maximum_queue_size`).\nfn maximum_queue_size() -\u003e usize {\n\t// # CPP-PARITY ffmpegdecoder.cpp:1184 — \"Fairly arbitrary size... This\n\t// value may be tweaked over time.\"\n\t2\n}\n\n/// Copy a packed F32-RGBA byte buffer (as produced by swscale RGBAF32LE)\n/// into an owned buffer, forcing alpha opaque (1.0) per pixel\n/// (CPP-PARITY ffmpegdecoder.cpp:428).\nfn convert_rgba_f32_le(data: \u0026[u8], w: u32, h: u32, stride: usize) -\u003e Vec\u003cu8\u003e {\n\tlet mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];\n\tfor y in 0..h as usize {\n\t\tlet row = \u0026data[y * stride..y * stride + (w as usize) * PIXEL_F32_BYTES];\n\t\tlet dst = \u0026mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];\n\t\tdst.copy_from_slice(row);\n\t\tfor px in dst.chunks_exact_mut(PIXEL_F32_BYTES) {\n\t\t\tpx[12..16].copy_from_slice(\u00261.0f32.to_le_bytes());\n\t\t}\n\t}\n\tout\n}\n\n/// Copy a packed u16-RGBA buffer (RGBA64LE) into F32 RGBA bytes.\nfn convert_rgba64_to_f32(data: \u0026[u8], w: u32, h: u32, stride: usize) -\u003e Vec\u003cu8\u003e {\n\tlet mut out = vec![0u8; (w as usize) * (h as usize) * PIXEL_F32_BYTES];\n\tfor y in 0..h as usize {\n\t\tlet row = \u0026data[y * stride..y * stride + (w as usize) * 8];\n\t\tlet dst = \u0026mut out[y * (w as usize) * PIXEL_F32_BYTES..(y + 1) * (w as usize) * PIXEL_F32_BYTES];\n\t\tfor (px, src_px) in dst.chunks_exact_mut(PIXEL_F32_BYTES).zip(row.chunks_exact(8)) {\n\t\t\tfor c in 0..3 {\n\t\t\t\tlet v = u16::from_le_bytes([src_px[c * 2], src_px[c * 2 + 1]]);\n\t\t\t\tpx[c * 4..c * 4 + 4].copy_from_slice(\u0026(v as f32 / 65535.0).to_le_bytes());\n\t\t\t}\n\t\t\tpx[12..16].copy_from_slice(\u00261.0f32.to_le_bytes());\n\t\t}\n\t}\n\tout\n}\n\n/// Build an allocated [`Frame`] (F32, RGBA) from raw pixel bytes.\n///\n/// # CPP-PARITY\n/// `copy_packed_av_frame_to_frame` (ffmpegdecoder.cpp:116): params are built\n/// from the decoded dimensions, the buffer is allocated and each row is\n/// copied with the destination linesize.\nfn copy_rgba_f32_to_frame(\n\twidth: u32,\n\theight: u32,\n\tbytes: \u0026[u8],\n\ttimestamp: Rational,\n) -\u003e crate::error::Result\u003cFrame\u003e {\n\tlet params = unsafe { oakcommon_videoparams_init_basic(width as i32, height as i32) };\n\tunsafe {\n\t\toakcommon_videoparams_set_format(params.clone(), PixelFormat::F32 as i32);\n\t\toakcommon_videoparams_set_channel_count(params.clone(), VIDEO_CHANNELS);\n\t}\n\tlet mut frame = Frame::with_params(params);\n\tframe.set_timestamp(timestamp);\n\tframe.allocate()?;\n\n\tlet row_bytes = (width as usize) * PIXEL_F32_BYTES;\n\tlet linesize = frame.linesize_bytes() as usize;\n\tlet out = frame.data_mut().ok_or(crate::error::Error::State)?;\n\tif out.len() \u003c linesize * (height as usize) {\n\t\treturn Err(crate::error::Error::State);\n\t}\n\tfor y in 0..(height as usize) {\n\t\tlet src = \u0026bytes[y * row_bytes..y * row_bytes + row_bytes];\n\t\tlet dst = \u0026mut out[y * linesize..y * linesize + row_bytes];\n\t\tdst.copy_from_slice(src);\n\t}\n\tOk(frame)\n}\n\n// ---------------------------------------------------------------------------\n// Probe\n// ---------------------------------------------------------------------------\n\n/// Probe `filename`, building a [`FootageDescription`].\n///\n/// # CPP-PARITY\n/// `FFmpegDecoder::probe` (ffmpegdecoder.cpp:484). Differences: video\n/// stream details are taken from stream parameters (no second decode pass),\n/// so `is_still` is always false and interlacing always progressive;\n/// subtitle streams are counted but not added.\nfn probe_file(filename: \u0026str, cancelled: Option\u003c\u0026OakCancelAtom\u003e) -\u003e Option\u003cFootageDescription\u003e {\n\tlet mut dict = Dictionary::new();\n\tdict.set(\"analyzeduration\", \"5000000\");\n\tdict.set(\"probesize\", \"20000000\");\n\tlet input = ffmpeg::format::input_with_dictionary(filename, dict).ok()?;\n\n\tlet mut desc = FootageDescription::new(\"ffmpeg\");\n\tlet footage_duration = input.duration();\n\n\tlet file_meta: Vec\u003c(String, String)\u003e = input\n\t\t.metadata()\n\t\t.iter()\n\t\t.map(|(k, v)| (k.to_string(), v.to_string()))\n\t\t.collect();\n\tlet mut source_start_time =\n\t\textract_source_start_time(\u0026file_meta, FfRational(1, FB_TIME_BASE as i32), 0);\n\n\tlet stream_count = input.nb_streams();\n\tfor i in 0..stream_count {\n\t\tif cancel_atom_is_cancelled(cancelled) {\n\t\t\treturn None;\n\t\t}\n\t\tlet Some(stream) = input.stream(i as usize) else { continue };\n\t\tlet params = stream.parameters();\n\t\tlet medium = params.medium();\n\n\t\tif !source_start_time.valid {\n\t\t\tlet stream_meta: Vec\u003c(String, String)\u003e = stream\n\t\t\t\t.metadata()\n\t\t\t\t.iter()\n\t\t\t\t.map(|(k, v)| (k.to_string(), v.to_string()))\n\t\t\t\t.collect();\n\t\t\tlet raw = unsafe { params.as_ptr() };\n\t\t\tsource_start_time =\n\t\t\t\textract_source_start_time(\u0026stream_meta, stream.time_base(), unsafe { (*raw).sample_rate });\n\t\t}\n\n\t\t// Only proceed if a decoder exists for this stream\n\t\t// (CPP-PARITY ffmpegdecoder.cpp:530).\n\t\tif ffmpeg::decoder::find(params.id()).is_none() {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet raw = unsafe { params.as_ptr() };\n\t\tmatch medium {\n\t\t\tMediaType::Video =\u003e {\n\t\t\t\tlet pixel = pixel_from_raw(unsafe { (*raw).format });\n\t\t\t\tlet native = native_pixel_format(pixel);\n\t\t\t\tlet frame_rate = stream.avg_frame_rate();\n\t\t\t\tlet tb = stream.time_base();\n\n\t\t\t\tlet vp = unsafe { oakcommon_videoparams_init_basic(1, 1) };\n\t\t\t\tunsafe {\n\t\t\t\t\toakcommon_videoparams_set_stream_index(vp.clone(), i as i32);\n\t\t\t\t\toakcommon_videoparams_set_width(vp.clone(), (*raw).width);\n\t\t\t\t\toakcommon_videoparams_set_height(vp.clone(), (*raw).height);\n\t\t\t\t\toakcommon_videoparams_set_video_type(vp.clone(), OAKCOMMON_VIDEO_TYPE_VIDEO);\n\t\t\t\t\toakcommon_videoparams_set_format(vp.clone(), native as i32);\n\t\t\t\t\toakcommon_videoparams_set_channel_count(vp.clone(), VIDEO_CHANNELS);\n\t\t\t\t\toakcommon_videoparams_set_interlacing(vp.clone(), OAKCOMMON_VIDEO_INTERLACE_NONE);\n\t\t\t\t\toakcommon_videoparams_set_pixel_aspect_ratio(vp.clone(), 1, 1);\n\t\t\t\t\toakcommon_videoparams_set_frame_rate(vp.clone(), frame_rate.0 as i64, frame_rate.1 as i64);\n\t\t\t\t\toakcommon_videoparams_set_start_time(vp.clone(), stream.start_time());\n\t\t\t\t\toakcommon_videoparams_set_time_base(vp.clone(), tb.0 as i64, tb.1 as i64);\n\t\t\t\t\toakcommon_videoparams_set_duration(vp.clone(), stream.duration());\n\t\t\t\t\toakcommon_videoparams_set_premultiplied_alpha(vp.clone(), 0);\n\t\t\t\t}\n\t\t\t\tdesc.push_stream(StreamEntry::Video(vp));\n\t\t\t}\n\t\t\tMediaType::Audio =\u003e {\n\t\t\t\tlet mut stream_duration = stream.duration();\n\t\t\t\tif stream_duration == AV_NOPTS_VALUE \u0026\u0026 footage_duration != AV_NOPTS_VALUE {\n\t\t\t\t\t// Fall back to the container duration rescaled into the\n\t\t\t\t\t// stream time base (CPP-PARITY ffmpegdecoder.cpp:621).\n\t\t\t\t\tstream_duration = unsafe {\n\t\t\t\t\t\tsys::av_rescale_q(\n\t\t\t\t\t\t\tfootage_duration,\n\t\t\t\t\t\t\tFfRational(1, FB_TIME_BASE as i32).into(),\n\t\t\t\t\t\t\tstream.time_base().into(),\n\t\t\t\t\t\t)\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tlet sample_rate = unsafe { (*raw).sample_rate };\n\t\t\t\tlet layout_mask = unsafe { ChannelLayout::from((*raw).ch_layout) }.bits();\n\t\t\t\tlet ap = unsafe { oakcore_audioparams_create(sample_rate, layout_mask, 0) };\n\t\t\t\tif !ap.is_null() {\n\t\t\t\t\tlet tb = stream.time_base();\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\toakcore_audioparams_set_stream_index(ap, i as i32);\n\t\t\t\t\t\toakcore_audioparams_set_duration(ap, stream_duration);\n\t\t\t\t\t\toakcore_audioparams_set_time_base(ap, tb.0 as i32, tb.1 as i32);\n\t\t\t\t\t}\n\t\t\t\t\tdesc.push_stream(StreamEntry::Audio(OakAudioParams {\n\t\t\t\t\t\tctx: ap as *mut std::ffi::c_void,\n\t\t\t\t\t\taddref: None,\n\t\t\t\t\t\trelease: None,\n\t\t\t\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t\t\t\t}));\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ =\u003e {}\n\t\t}\n\t}\n\n\tdesc.set_stream_count(stream_count as usize);\n\tif source_start_time.valid {\n\t\tdesc.set_source_start_time(source_start_time.time, 0);\n\t}\n\tSome(desc)\n}\n\n/// Parsed source start time.\nstruct SourceTime {\n\tvalid: bool,\n\ttime: Rational,\n}\n\n/// Extract a source start time from `timecode` / `time_reference` metadata,\n/// mirroring `extract_source_start_time` (ffmpegdecoder.cpp:176).\nfn extract_source_start_time(\n\tmetadata: \u0026[(String, String)],\n\ttimebase: FfRational,\n\tsample_rate: i32,\n) -\u003e SourceTime {\n\tlet mut out = SourceTime { valid: false, time: Rational::new(0, 1) };\n\tfor (key, value) in metadata {\n\t\tif key == \"timecode\" {\n\t\t\tlet parsed = crate::timecodemetadata::SourceTime::from_timecode_string(\n\t\t\t\tvalue,\n\t\t\t\t\u0026oak_rational(timebase),\n\t\t\t);\n\t\t\tif parsed.valid {\n\t\t\t\tout.valid = true;\n\t\t\t\tout.time = parsed.time;\n\t\t\t\treturn out;\n\t\t\t}\n\t\t} else if key == \"time_reference\" {\n\t\t\tlet parsed = crate::timecodemetadata::SourceTime::from_bwf_time_reference(\n\t\t\t\tvalue,\n\t\t\t\tsample_rate,\n\t\t\t);\n\t\t\tif parsed.valid {\n\t\t\t\tout.valid = true;\n\t\t\t\tout.time = parsed.time;\n\t\t\t\treturn out;\n\t\t\t}\n\t\t}\n\t}\n\tout\n}\n\n// ---------------------------------------------------------------------------\n// FFmpegEncoder\n// ---------------------------------------------------------------------------\n\n/// `olive::FFmpegEncoder` — FFmpeg-backed media encoder.\npub struct FFmpegEncoder {\n\t/// The encoding parameters this encoder was configured with.\n\tpub params: EncodingParams,\n\t/// Encoder session state.\n\tstate: Mutex\u003cEncoderState\u003e,\n}\n\n/// Encoder session state.\nstruct EncoderState {\n\t/// Parameters from [`Encoder::configure`] (overrides `params`).\n\tconfigured: Option\u003cEncodingParams\u003e,\n\t/// The open output, if any.\n\toutput: Option\u003cOutputState\u003e,\n\t/// Last error detail.\n\tlast_error: String,\n}\n\n/// One opened output file.\nstruct OutputState {\n\toutput: ffmpeg::format::context::Output,\n\tvideo: Option\u003cVideoEncoderState\u003e,\n\taudio: Option\u003cAudioEncoderState\u003e,\n\t/// Audio presentation timestamp in 1/sample_rate time base units.\n\taudio_pts: i64,\n\tflushed: bool,\n}\n\n// SAFETY: `EncoderState` owns raw FFmpeg contexts (output + encoders) that\n// are not thread-safe themselves, but every access goes through the\n// `FFmpegEncoder::state` `Mutex`, so concurrent access never happens.\nunsafe impl Send for EncoderState {}\nunsafe impl Sync for EncoderState {}\n\n/// Opened video encoder + its conversion scaler.\nstruct VideoEncoderState {\n\tencoder: ffmpeg::codec::encoder::video::Encoder,\n\tstream_index: usize,\n\t/// swscale from the incoming F32-RGBA bytes to the encoder pixel format.\n\tscaler: scaling::Context,\n\twidth: u32,\n\theight: u32,\n}\n\n/// Opened audio encoder + its conversion resampler.\nstruct AudioEncoderState {\n\tencoder: ffmpeg::codec::encoder::audio::Encoder,\n\tstream_index: usize,\n\t/// swr from interleaved f32 to the encoder sample format.\n\tresampler: AudioResampler,\n}\n\nimpl FFmpegEncoder {\n\t/// New encoder configured with `params`.\n\tpub(crate) fn with_params(params: EncodingParams) -\u003e Self {\n\t\tFFmpegEncoder {\n\t\t\tparams,\n\t\t\tstate: Mutex::new(EncoderState {\n\t\t\t\tconfigured: None,\n\t\t\t\toutput: None,\n\t\t\t\tlast_error: String::new(),\n\t\t\t}),\n\t\t}\n\t}\n\n\t/// The effective encoding parameters (configured overrides construction).\n\tfn effective_params(\u0026self) -\u003e EncodingParams {\n\t\tlet state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.configured.clone().unwrap_or_else(|| self.params.clone())\n\t}\n}\n\nimpl Encoder for FFmpegEncoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"ffmpeg\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn supports_image_sequences(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn is_configurable(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn configure(\u0026self, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif !is_ffmpeg_format(params.format) {\n\t\t\treturn Err(fail(format!(\"unknown export format {}\", params.format)));\n\t\t}\n\t\t// `configure` cannot write the public `params` field through `\u0026self`;\n\t\t// the configured copy is stored in the session state and used by\n\t\t// `open` (CPP-PARITY encoder.cpp `configure` stores the params).\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.configured = Some(params.clone());\n\t\tstate.last_error.clear();\n\t\tOk(())\n\t}\n\n\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = self.effective_params();\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.open(\u0026params)\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// `Encoder::close` is documented idempotent; flush if needed.\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.close()\n\t}\n\n\tfn write_video(\u0026self, frame: \u0026Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = self.effective_params();\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.write_video(frame, \u0026params)\n\t}\n\n\tfn write_audio(\u0026self, samples: \u0026[f32], frame_count: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = self.effective_params();\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.write_audio(samples, frame_count, \u0026params)\n\t}\n\n\tfn write_subtitle(\n\t\t\u0026self,\n\t\t_text: \u0026str,\n\t\t_in_seconds: f64,\n\t\t_out_seconds: f64,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// Subtitle muxing is not exposed by the crate's encoder trait flow\n\t\t// (the C++ writes through the bridge's SRT encoder); report the same\n\t\t// unsupported state the stub did.\n\t\tErr(fail(\"subtitle encoding is not supported by the ffmpeg encoder\"))\n\t}\n\n\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());\n\t\tstate.close()\n\t}\n\n\tfn desired_pixel_format(\u0026self) -\u003e Option\u003cPixelFormat\u003e {\n\t\t// The encoder accepts (and converts) F32 RGBA frames.\n\t\tSome(PixelFormat::F32)\n\t}\n\n\tfn desired_sample_format(\u0026self) -\u003e Option\u003cSampleFormat\u003e {\n\t\t// The encoder accepts interleaved f32 audio.\n\t\tSome(SampleFormat::F32)\n\t}\n\n\tfn filename(\u0026self) -\u003e String {\n\t\tc_string_1024(\u0026self.params.filename)\n\t}\n\n\tfn get_error(\u0026self) -\u003e String {\n\t\tself.state\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.last_error\n\t\t\t.clone()\n\t}\n}\n\nimpl EncoderState {\n\t/// Open the output file, create the streams and encoders and write the\n\t/// header.\n\tfn open(\u0026mut self, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif self.output.is_some() {\n\t\t\treturn Ok(());\n\t\t}\n\t\tlet filename = c_string_1024(\u0026params.filename);\n\t\tif filename.is_empty() {\n\t\t\treturn Err(fail(\"no output filename\"));\n\t\t}\n\t\tif params.video_enabled == 0 \u0026\u0026 params.audio_enabled == 0 {\n\t\t\treturn Err(fail(\"no output tracks enabled\"));\n\t\t}\n\n\t\tlet mut output = ffmpeg::format::output(\u0026filename)\n\t\t\t.map_err(|e| fail(format!(\"failed to create output '{filename}': {e}\")))?;\n\n\t\tlet mut video = None;\n\t\tif params.video_enabled != 0 {\n\t\t\tlet codec_id = export_codec_to_id(params.video_codec)\n\t\t\t\t.ok_or_else(|| fail(format!(\"unknown video codec {}\", params.video_codec)))?;\n\t\t\tlet codec = ffmpeg::encoder::find(codec_id)\n\t\t\t\t.ok_or_else(|| fail(format!(\"no encoder for codec {:?}\", codec_id)))?;\n\n\t\t\tlet width = params.video_width.max(1) as u32;\n\t\t\tlet height = params.video_height.max(1) as u32;\n\t\t\tlet time_base = FfRational(params.video_time_base_num, params.video_time_base_den);\n\t\t\tlet frame_rate = FfRational(params.video_time_base_den, params.video_time_base_num.max(1));\n\n\t\t\tlet mut stream = output.add_stream(codec).map_err(ffmpeg_err)?;\n\t\t\tlet stream_index = stream.index();\n\n\t\t\tlet mut encoder = ffmpeg::codec::Context::new_with_codec(codec)\n\t\t\t\t.encoder()\n\t\t\t\t.video()\n\t\t\t\t.map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026encoder);\n\t\t\tstream.set_time_base(time_base);\n\n\t\t\tencoder.set_width(width);\n\t\t\tencoder.set_height(height);\n\t\t\tencoder.set_aspect_ratio(FfRational(\n\t\t\t\tparams.video_pixel_aspect_num.max(1),\n\t\t\t\tparams.video_pixel_aspect_den.max(1),\n\t\t\t));\n\t\t\tencoder.set_frame_rate(Some(frame_rate));\n\t\t\tencoder.set_time_base(time_base);\n\t\t\tif params.video_bit_rate \u003e 0 {\n\t\t\t\tencoder.set_bit_rate(params.video_bit_rate as usize);\n\t\t\t}\n\t\t\tif params.video_max_bit_rate \u003e 0 {\n\t\t\t\tencoder.set_max_bit_rate(params.video_max_bit_rate as usize);\n\t\t\t}\n\n\t\t\t// Choose the target pixel format: explicit `video_pix_fmt`, else\n\t\t\t// a codec-appropriate default.\n\t\t\tlet pix_fmt = pixel_format_from_name(\u0026params.video_pix_fmt)\n\t\t\t\t.unwrap_or_else(|| default_pixel_format_for_codec(codec_id));\n\t\t\tencoder.set_format(pix_fmt);\n\n\t\t\tlet opened = encoder.open().map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026opened);\n\n\t\t\tlet scaler = scaling::Context::get(\n\t\t\t\tPixel::RGBA,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tpix_fmt,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t\tscaling::Flags::BILINEAR,\n\t\t\t)\n\t\t\t.map_err(ffmpeg_err)?;\n\n\t\t\tvideo = Some(VideoEncoderState {\n\t\t\t\tencoder: opened,\n\t\t\t\tstream_index,\n\t\t\t\tscaler,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t});\n\t\t}\n\n\t\tlet mut audio = None;\n\t\tif params.audio_enabled != 0 {\n\t\t\tlet codec_id = export_codec_to_id(params.audio_codec)\n\t\t\t\t.ok_or_else(|| fail(format!(\"unknown audio codec {}\", params.audio_codec)))?;\n\t\t\tlet codec = ffmpeg::encoder::find(codec_id)\n\t\t\t\t.ok_or_else(|| fail(format!(\"no encoder for codec {:?}\", codec_id)))?;\n\n\t\t\tlet sample_rate = params.audio_sample_rate.max(1) as u32;\n\t\t\tlet layout = channel_layout_from_mask(params.audio_channel_layout);\n\n\t\t\tlet mut stream = output.add_stream(codec).map_err(ffmpeg_err)?;\n\t\t\tlet stream_index = stream.index();\n\t\t\tstream.set_time_base(FfRational(1, sample_rate as i32));\n\n\t\t\tlet mut encoder = ffmpeg::codec::Context::new_with_codec(codec)\n\t\t\t\t.encoder()\n\t\t\t\t.audio()\n\t\t\t\t.map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026encoder);\n\n\t\t\tencoder.set_rate(sample_rate as i32);\n\t\t\tencoder.set_channel_layout(layout);\n\t\t\tif params.audio_bit_rate \u003e 0 {\n\t\t\t\tencoder.set_bit_rate(params.audio_bit_rate as usize);\n\t\t\t}\n\t\t\tlet sample_fmt = sample_format_to_ffmpeg(params.audio_sample_format)\n\t\t\t\t.unwrap_or_else(|| default_sample_format_for_codec(codec_id));\n\t\t\tencoder.set_format(sample_fmt);\n\n\t\t\tlet opened = encoder.open().map_err(ffmpeg_err)?;\n\t\t\tstream.set_parameters(\u0026opened);\n\n\t\t\tlet resampler = AudioResampler::get(\n\t\t\t\tSample::F32(SampleType::Packed),\n\t\t\t\tlayout,\n\t\t\t\tsample_rate,\n\t\t\t\tsample_fmt,\n\t\t\t\tlayout,\n\t\t\t\tsample_rate,\n\t\t\t)?;\n\n\t\t\taudio = Some(AudioEncoderState {\n\t\t\t\tencoder: opened,\n\t\t\t\tstream_index,\n\t\t\t\tresampler,\n\t\t\t});\n\t\t}\n\n\t\toutput.write_header().map_err(ffmpeg_err)?;\n\n\t\tself.output = Some(OutputState {\n\t\t\toutput,\n\t\t\tvideo,\n\t\t\taudio,\n\t\t\taudio_pts: 0,\n\t\t\tflushed: false,\n\t\t});\n\t\tOk(())\n\t}\n\n\t/// Encode one frame (F32 RGBA) into the open output.\n\tfn write_video(\u0026mut self, frame: \u0026Frame, params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet output = self\n\t\t\t.output\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder is not open\"))?;\n\t\tif output.flushed {\n\t\t\treturn Err(fail(\"encoder is already flushed\"));\n\t\t}\n\t\tlet video = output\n\t\t\t.video\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder has no video track\"))?;\n\n\t\tlet data = frame.data().ok_or(crate::error::Error::State)?;\n\t\tlet (w, h) = (frame.width() as u32, frame.height() as u32);\n\t\tif w != video.width || h != video.height {\n\t\t\treturn Err(fail(format!(\n\t\t\t\t\"frame size {w}x{h} does not match the encoder size {}x{}\",\n\t\t\t\tvideo.width, video.height\n\t\t\t)));\n\t\t}\n\n\t\t// Convert the F32 RGBA buffer to 8-bit RGBA (swscale cannot take\n\t\t// float input on every build) and scale into the encoder format.\n\t\tlet mut rgba = vec![0u8; (w as usize) * (h as usize) * 4];\n\t\tlet linesize = frame.linesize_bytes() as usize;\n\t\tfor y in 0..(h as usize) {\n\t\t\tlet row = \u0026data[y * linesize..y * linesize + (w as usize) * PIXEL_F32_BYTES];\n\t\t\tlet dst = \u0026mut rgba[y * (w as usize) * 4..(y + 1) * (w as usize) * 4];\n\t\t\tfor (out_px, in_px) in dst.chunks_exact_mut(4).zip(row.chunks_exact(PIXEL_F32_BYTES)) {\n\t\t\t\tfor c in 0..4 {\n\t\t\t\t\tlet v = f32::from_le_bytes([\n\t\t\t\t\t\tin_px[c * 4],\n\t\t\t\t\t\tin_px[c * 4 + 1],\n\t\t\t\t\t\tin_px[c * 4 + 2],\n\t\t\t\t\t\tin_px[c * 4 + 3],\n\t\t\t\t\t]);\n\t\t\t\t\tout_px[c] = (v * 255.0).clamp(0.0, 1.0) as u8;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet mut src = ffmpeg::frame::Video::new(Pixel::RGBA, w, h);\n\t\tsrc.data_mut(0).copy_from_slice(\u0026rgba);\n\n\t\tlet mut scaled = ffmpeg::frame::Video::empty();\n\t\tvideo.scaler.run(\u0026src, \u0026mut scaled).map_err(ffmpeg_err)?;\n\n\t\t// # CPP-PARITY\n\t\t// `FFmpegEncoder::write_frame` passes the frame time in seconds; the\n\t\t// Rust `Frame` carries the timestamp as a rational.\n\t\tlet secs = frame.timestamp().to_f64();\n\t\tlet tb = FfRational(params.video_time_base_num.max(1), params.video_time_base_den.max(1));\n\t\tlet pts = (secs * tb.1 as f64 / tb.0 as f64).round() as i64;\n\t\tscaled.set_pts(Some(pts));\n\n\t\tvideo.encoder.send_frame(\u0026scaled).map_err(ffmpeg_err)?;\n\t\tdrain_video_packets(\u0026mut output.output, video)\n\t}\n\n\t/// Encode interleaved f32 audio into the open output.\n\tfn write_audio(\n\t\t\u0026mut self,\n\t\tsamples: \u0026[f32],\n\t\t_frame_count: i32,\n\t\tparams: \u0026EncodingParams,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet output = self\n\t\t\t.output\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder is not open\"))?;\n\t\tif output.flushed {\n\t\t\treturn Err(fail(\"encoder is already flushed\"));\n\t\t}\n\t\tlet audio = output\n\t\t\t.audio\n\t\t\t.as_mut()\n\t\t\t.ok_or_else(|| fail(\"encoder has no audio track\"))?;\n\n\t\tif samples.is_empty() {\n\t\t\treturn Ok(());\n\t\t}\n\t\tlet channels = audio.resampler.dst_channels.max(1);\n\t\tlet in_frames = samples.len() / channels;\n\n\t\t// Presentation timestamp in the output stream time base (1/sample_rate).\n\t\tlet pts = output.audio_pts;\n\n\t\tlet layout = channel_layout_from_mask(params.audio_channel_layout);\n\t\tlet mut input = ffmpeg::frame::Audio::new(Sample::F32(SampleType::Packed), in_frames, layout);\n\t\tlet bytes = unsafe {\n\t\t\tstd::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 4)\n\t\t};\n\t\tinput.data_mut(0)[..bytes.len()].copy_from_slice(bytes);\n\n\t\tlet mut converted = audio.resampler.convert_to_frame(\u0026input)?;\n\t\tconverted.set_pts(Some(pts));\n\t\toutput.audio_pts += converted.samples() as i64;\n\n\t\taudio.encoder.send_frame(\u0026converted).map_err(ffmpeg_err)?;\n\t\tdrain_audio_packets(\u0026mut output.output, audio)\n\t}\n\n\t/// Flush encoders, write the trailer and close the output (idempotent).\n\tfn close(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet Some(output) = self.output.as_mut() else {\n\t\t\treturn Ok(());\n\t\t};\n\t\tif !output.flushed {\n\t\t\tif let Some(v) = output.video.as_mut() {\n\t\t\t\tdrain_video_encoder(\u0026mut output.output, v)?;\n\t\t\t}\n\t\t\tif let Some(a) = output.audio.as_mut() {\n\t\t\t\tdrain_audio_encoder(\u0026mut output.output, a)?;\n\t\t\t}\n\t\t\toutput.output.write_trailer().map_err(ffmpeg_err)?;\n\t\t\toutput.flushed = true;\n\t\t}\n\t\t// Drop the output (closes the file).\n\t\tself.output = None;\n\t\tOk(())\n\t}\n}\n\n/// Drain a video encoder after EOF, writing any packets.\nfn drain_video_encoder(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\tvideo: \u0026mut VideoEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tvideo.encoder.send_eof().map_err(ffmpeg_err)?;\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch video.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(video.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Drain an audio encoder after EOF, writing any packets.\nfn drain_audio_encoder(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\taudio: \u0026mut AudioEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\taudio.encoder.send_eof().map_err(ffmpeg_err)?;\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch audio.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(audio.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Drain ready packets from a video encoder into the muxer.\nfn drain_video_packets(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\tvideo: \u0026mut VideoEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch video.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(video.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Drain ready packets from an audio encoder into the muxer.\nfn drain_audio_packets(\n\toutput: \u0026mut ffmpeg::format::context::Output,\n\taudio: \u0026mut AudioEncoderState,\n) -\u003e crate::error::Result\u003c()\u003e {\n\tlet mut pkt = ffmpeg::packet::Packet::empty();\n\tloop {\n\t\tmatch audio.encoder.receive_packet(\u0026mut pkt) {\n\t\t\tOk(()) =\u003e {\n\t\t\t\tpkt.set_stream(audio.stream_index);\n\t\t\t\tpkt.write_interleaved(output).map_err(ffmpeg_err)?;\n\t\t\t}\n\t\t\tErr(e) if is_eof_or_eagain(\u0026e) =\u003e break,\n\t\t\tErr(e) =\u003e return Err(ffmpeg_err(e)),\n\t\t}\n\t}\n\tOk(())\n}\n\n/// Map an `ExportCodec::Codec` raw value to an FFmpeg codec id\n/// (CPP-PARITY `FFmpegEncoder::export_codec_to_bridge`). Values are the\n/// documented `ExportCodec::Codec` discriminants (see `exportcodec.rs`).\nfn export_codec_to_id(codec: i32) -\u003e Option\u003cffmpeg::codec::Id\u003e {\n\tmatch codec {\n\t\t0 =\u003e Some(ffmpeg::codec::Id::DNXHD), // DNxHD\n\t\t1 | 2 =\u003e Some(ffmpeg::codec::Id::H264), // H264 / H264 RGB\n\t\t3 =\u003e Some(ffmpeg::codec::Id::HEVC), // H265\n\t\t6 =\u003e Some(ffmpeg::codec::Id::PRORES), // ProRes\n\t\t7 =\u003e Some(ffmpeg::codec::Id::CFHD), // CineForm\n\t\t10 =\u003e Some(ffmpeg::codec::Id::MPEG2VIDEO), // MP2\n\t\t11 =\u003e Some(ffmpeg::codec::Id::MP3), // MP3\n\t\t12 =\u003e Some(ffmpeg::codec::Id::AAC), // AAC\n\t\t13 =\u003e Some(ffmpeg::codec::Id::PCM_S16LE), // PCM\n\t\t14 =\u003e Some(ffmpeg::codec::Id::OPUS), // Opus\n\t\t15 =\u003e Some(ffmpeg::codec::Id::VORBIS), // Vorbis\n\t\t16 =\u003e Some(ffmpeg::codec::Id::FLAC), // FLAC\n\t\t17 =\u003e Some(ffmpeg::codec::Id::SUBRIP), // SRT\n\t\t18 =\u003e Some(ffmpeg::codec::Id::AV1), // AV1\n\t\t_ =\u003e None,\n\t}\n}\n\n/// Whether `format` is a container the FFmpeg encoder handles\n/// (CPP-PARITY `Encoder::create_from_params` format mapping).\nfn is_ffmpeg_format(format: i32) -\u003e bool {\n\tmatches!(format, 0 | 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14)\n}\n\n/// Default encoder pixel format for a codec.\nfn default_pixel_format_for_codec(codec: ffmpeg::codec::Id) -\u003e Pixel {\n\tmatch codec {\n\t\tffmpeg::codec::Id::PRORES | ffmpeg::codec::Id::DNXHD =\u003e Pixel::YUV422P10LE,\n\t\t_ =\u003e Pixel::YUV420P,\n\t}\n}\n\n/// Default encoder sample format for a codec.\nfn default_sample_format_for_codec(codec: ffmpeg::codec::Id) -\u003e Sample {\n\tmatch codec {\n\t\tffmpeg::codec::Id::PCM_S16LE =\u003e Sample::I16(SampleType::Packed),\n\t\tffmpeg::codec::Id::FLAC =\u003e Sample::I16(SampleType::Planar),\n\t\tffmpeg::codec::Id::MP3 =\u003e Sample::F32(SampleType::Packed),\n\t\t_ =\u003e Sample::F32(SampleType::Planar),\n\t}\n}\n\n/// Parse a NUL-terminated pixel-format name (e.g. \"yuv420p\").\nfn pixel_format_from_name(buf: \u0026[u8; 64]) -\u003e Option\u003cPixel\u003e {\n\tlet end = buf.iter().position(|\u0026b| b == 0).unwrap_or(buf.len());\n\tlet name = std::str::from_utf8(\u0026buf[..end]).ok()?;\n\tif name.is_empty() {\n\t\treturn None;\n\t}\n\tname.parse::\u003cPixel\u003e().ok()\n}\n\n/// Map an oakcore sample format to an FFmpeg sample format.\nfn sample_format_to_ffmpeg(f: SampleFormat) -\u003e Option\u003cSample\u003e {\n\tmatch f {\n\t\tSampleFormat::U8Planar =\u003e Some(Sample::U8(SampleType::Planar)),\n\t\tSampleFormat::S16Planar =\u003e Some(Sample::I16(SampleType::Planar)),\n\t\tSampleFormat::S32Planar =\u003e Some(Sample::I32(SampleType::Planar)),\n\t\tSampleFormat::S64Planar =\u003e Some(Sample::I64(SampleType::Planar)),\n\t\tSampleFormat::F32Planar =\u003e Some(Sample::F32(SampleType::Planar)),\n\t\tSampleFormat::F64Planar =\u003e Some(Sample::F64(SampleType::Planar)),\n\t\tSampleFormat::U8 =\u003e Some(Sample::U8(SampleType::Packed)),\n\t\tSampleFormat::S16 =\u003e Some(Sample::I16(SampleType::Packed)),\n\t\tSampleFormat::S32 =\u003e Some(Sample::I32(SampleType::Packed)),\n\t\tSampleFormat::S64 =\u003e Some(Sample::I64(SampleType::Packed)),\n\t\tSampleFormat::F32 =\u003e Some(Sample::F32(SampleType::Packed)),\n\t\tSampleFormat::F64 =\u003e Some(Sample::F64(SampleType::Packed)),\n\t\tSampleFormat::Invalid =\u003e None,\n\t}\n}\n\n/// Read a NUL-terminated C string out of the `EncodingParams.filename` byte\n/// buffer, stopping at the first NUL (empty string when unset).\nfn c_string_1024(buf: \u0026[u8; 1024]) -\u003e String {\n\tlet end = buf.iter().position(|\u0026b| b == 0).unwrap_or(buf.len());\n\tString::from_utf8_lossy(\u0026buf[..end]).into_owned()\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::decoder::RetrieveVideoParams;\n\tuse oakcore_rs::Rational;\n\n\tfn video_params() -\u003e RetrieveVideoParams {\n\t\tRetrieveVideoParams {\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(1, 30),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: crate::decoder::K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: crate::decoder::RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t}\n\t}\n\n\t#[test]\n\tfn ffmpeg_decoder_identity_and_capabilities() {\n\t\tlet d = FFmpegDecoder::new();\n\t\tassert_eq!(d.id(), \"ffmpeg\");\n\t\tassert!(d.supports_video());\n\t\tassert!(d.supports_audio());\n\t\tassert!(d.probe(\"any.mp4\", None).is_none());\n\t}\n\n\t#[test]\n\tfn ffmpeg_decoder_closed_state_errors() {\n\t\tlet d = FFmpegDecoder::new();\n\t\t// Open on a missing file fails and leaves the decoder closed\n\t\t// (C++ parity: the stream is unset on failure).\n\t\tlet s = CodecStream::with_block(\"in.mp4\".to_string(), 0, None);\n\t\tassert!(d.open(\u0026s).is_err());\n\t\tassert_eq!(d.stream().filename(), \"\");\n\t\tassert!(d.close().is_ok());\n\n\t\t// All media operations on a closed decoder fail.\n\t\tassert!(d.retrieve_video_frame(\u0026video_params()).is_err());\n\t\tassert!(d.retrieve_video(\u0026video_params()).is_err());\n\t\tlet mut dest = [0f32; 8];\n\t\tassert!(d\n\t\t\t.retrieve_audio(\n\t\t\t\t\u0026mut dest,\n\t\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t\t48000,\n\t\t\t\t0x3\n\t\t\t)\n\t\t\t.is_err());\n\t\tassert!(d\n\t\t\t.conform_audio(\u0026[\"a.pcm\".to_string()], 48000, 0x3, 10, None)\n\t\t\t.is_err());\n\t\tassert_eq!(d.get_audio_start_offset(), Rational::new(0, 1));\n\t}\n\n\t#[test]\n\tfn ffmpeg_encoder_identity_and_config_validation() {\n\t\tlet mut params = EncodingParams::default();\n\t\tlet name = b\"out/ffmpeg.mp4\";\n\t\tparams.filename[..name.len()].copy_from_slice(name);\n\t\tparams.format = 2; // mp4\n\t\tparams.video_enabled = 1;\n\t\tparams.video_codec = 1; // H264\n\t\tparams.video_width = 64;\n\t\tparams.video_height = 64;\n\t\tparams.video_time_base_num = 1;\n\t\tparams.video_time_base_den = 10;\n\t\tlet e = FFmpegEncoder::with_params(params);\n\n\t\tassert_eq!(e.id(), \"ffmpeg\");\n\t\tassert!(e.supports_video());\n\t\tassert!(e.supports_audio());\n\t\tassert!(e.supports_subtitles());\n\t\tassert!(!e.supports_image_sequences());\n\t\tassert!(e.is_configurable());\n\t\tassert_eq!(e.filename(), \"out/ffmpeg.mp4\");\n\t\tassert_eq!(e.desired_pixel_format(), Some(PixelFormat::F32));\n\t\tassert_eq!(e.desired_sample_format(), Some(SampleFormat::F32));\n\t\tassert_eq!(e.get_error(), \"\");\n\n\t\t// configure rejects unknown export formats.\n\t\tlet mut bad = EncodingParams::default();\n\t\tbad.format = 99;\n\t\tassert!(e.configure(\u0026bad).is_err());\n\n\t\t// A valid configures succeeds but nothing is open yet.\n\t\tlet mut good = EncodingParams::default();\n\t\tgood.format = 2;\n\t\tgood.video_enabled = 1;\n\t\tgood.video_codec = 1;\n\t\tgood.video_width = 64;\n\t\tgood.video_height = 64;\n\t\tgood.video_time_base_num = 1;\n\t\tgood.video_time_base_den = 10;\n\t\tassert!(e.configure(\u0026good).is_ok());\n\n\t\t// Operations before open fail cleanly; close/flush are safe.\n\t\tlet frame = Frame::new();\n\t\tassert!(e.write_video(\u0026frame).is_err());\n\t\tassert!(e.write_audio(\u0026[0f32; 4], 1).is_err());\n\t\tassert!(e.write_subtitle(\"hi\", 0.0, 1.0).is_err());\n\t\tassert!(e.close().is_ok());\n\t\tassert!(e.flush().is_ok());\n\t}\n\n\t#[test]\n\tfn ffmpeg_encoder_open_rejects_invalid_config() {\n\t\t// No filename.\n\t\tlet e = FFmpegEncoder::with_params(EncodingParams::default());\n\t\tassert!(e.open().is_err());\n\n\t\t// No enabled tracks.\n\t\tlet mut p = EncodingParams::default();\n\t\tlet name = b\"out/x.mp4\";\n\t\tp.filename[..name.len()].copy_from_slice(name);\n\t\tp.format = 2;\n\t\tlet e = FFmpegEncoder::with_params(p);\n\t\tassert!(e.open().is_err());\n\t}\n}\n","traces":[{"line":95,"address":[],"length":0,"stats":{"Line":24}},{"line":97,"address":[],"length":0,"stats":{"Line":49}},{"line":98,"address":[],"length":0,"stats":{"Line":2}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":102,"address":[],"length":0,"stats":{"Line":24}},{"line":106,"address":[],"length":0,"stats":{"Line":3}},{"line":107,"address":[],"length":0,"stats":{"Line":3}},{"line":111,"address":[],"length":0,"stats":{"Line":6}},{"line":112,"address":[],"length":0,"stats":{"Line":6}},{"line":120,"address":[],"length":0,"stats":{"Line":1026}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":1026}},{"line":130,"address":[],"length":0,"stats":{"Line":11}},{"line":131,"address":[],"length":0,"stats":{"Line":21}},{"line":132,"address":[],"length":0,"stats":{"Line":50}},{"line":137,"address":[],"length":0,"stats":{"Line":3}},{"line":138,"address":[],"length":0,"stats":{"Line":3}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":141,"address":[],"length":0,"stats":{"Line":0}},{"line":142,"address":[],"length":0,"stats":{"Line":0}},{"line":143,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":6}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":2}},{"line":182,"address":[],"length":0,"stats":{"Line":4}},{"line":183,"address":[],"length":0,"stats":{"Line":4}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":186,"address":[],"length":0,"stats":{"Line":8}},{"line":187,"address":[],"length":0,"stats":{"Line":4}},{"line":188,"address":[],"length":0,"stats":{"Line":8}},{"line":189,"address":[],"length":0,"stats":{"Line":8}},{"line":190,"address":[],"length":0,"stats":{"Line":4}},{"line":191,"address":[],"length":0,"stats":{"Line":4}},{"line":200,"address":[],"length":0,"stats":{"Line":2}},{"line":203,"address":[],"length":0,"stats":{"Line":6}},{"line":210,"address":[],"length":0,"stats":{"Line":2}},{"line":212,"address":[],"length":0,"stats":{"Line":6}},{"line":216,"address":[],"length":0,"stats":{"Line":52}},{"line":217,"address":[],"length":0,"stats":{"Line":156}},{"line":234,"address":[],"length":0,"stats":{"Line":17}},{"line":236,"address":[],"length":0,"stats":{"Line":51}},{"line":237,"address":[],"length":0,"stats":{"Line":17}},{"line":243,"address":[],"length":0,"stats":{"Line":0}},{"line":244,"address":[],"length":0,"stats":{"Line":0}},{"line":249,"address":[],"length":0,"stats":{"Line":8}},{"line":250,"address":[],"length":0,"stats":{"Line":16}},{"line":253,"address":[],"length":0,"stats":{"Line":2}},{"line":254,"address":[],"length":0,"stats":{"Line":2}},{"line":257,"address":[],"length":0,"stats":{"Line":2}},{"line":258,"address":[],"length":0,"stats":{"Line":2}},{"line":261,"address":[],"length":0,"stats":{"Line":4}},{"line":262,"address":[],"length":0,"stats":{"Line":8}},{"line":263,"address":[],"length":0,"stats":{"Line":12}},{"line":266,"address":[],"length":0,"stats":{"Line":7}},{"line":267,"address":[],"length":0,"stats":{"Line":7}},{"line":273,"address":[],"length":0,"stats":{"Line":28}},{"line":274,"address":[],"length":0,"stats":{"Line":7}},{"line":275,"address":[],"length":0,"stats":{"Line":0}},{"line":276,"address":[],"length":0,"stats":{"Line":0}},{"line":278,"address":[],"length":0,"stats":{"Line":0}},{"line":280,"address":[],"length":0,"stats":{"Line":7}},{"line":281,"address":[],"length":0,"stats":{"Line":0}},{"line":283,"address":[],"length":0,"stats":{"Line":7}},{"line":284,"address":[],"length":0,"stats":{"Line":2}},{"line":287,"address":[],"length":0,"stats":{"Line":15}},{"line":288,"address":[],"length":0,"stats":{"Line":20}},{"line":289,"address":[],"length":0,"stats":{"Line":10}},{"line":290,"address":[],"length":0,"stats":{"Line":5}},{"line":293,"address":[],"length":0,"stats":{"Line":2}},{"line":295,"address":[],"length":0,"stats":{"Line":8}},{"line":296,"address":[],"length":0,"stats":{"Line":8}},{"line":297,"address":[],"length":0,"stats":{"Line":2}},{"line":300,"address":[],"length":0,"stats":{"Line":2}},{"line":301,"address":[],"length":0,"stats":{"Line":2}},{"line":303,"address":[],"length":0,"stats":{"Line":2}},{"line":305,"address":[],"length":0,"stats":{"Line":2}},{"line":308,"address":[],"length":0,"stats":{"Line":5}},{"line":309,"address":[],"length":0,"stats":{"Line":5}},{"line":310,"address":[],"length":0,"stats":{"Line":20}},{"line":311,"address":[],"length":0,"stats":{"Line":20}},{"line":312,"address":[],"length":0,"stats":{"Line":3}},{"line":313,"address":[],"length":0,"stats":{"Line":0}},{"line":322,"address":[],"length":0,"stats":{"Line":6}},{"line":323,"address":[],"length":0,"stats":{"Line":12}},{"line":324,"address":[],"length":0,"stats":{"Line":3}},{"line":326,"address":[],"length":0,"stats":{"Line":21}},{"line":327,"address":[],"length":0,"stats":{"Line":18}},{"line":328,"address":[],"length":0,"stats":{"Line":3}},{"line":331,"address":[],"length":0,"stats":{"Line":2}},{"line":335,"address":[],"length":0,"stats":{"Line":2}},{"line":336,"address":[],"length":0,"stats":{"Line":8}},{"line":337,"address":[],"length":0,"stats":{"Line":8}},{"line":338,"address":[],"length":0,"stats":{"Line":0}},{"line":339,"address":[],"length":0,"stats":{"Line":0}},{"line":341,"address":[],"length":0,"stats":{"Line":0}},{"line":342,"address":[],"length":0,"stats":{"Line":0}},{"line":343,"address":[],"length":0,"stats":{"Line":0}},{"line":344,"address":[],"length":0,"stats":{"Line":0}},{"line":345,"address":[],"length":0,"stats":{"Line":0}},{"line":346,"address":[],"length":0,"stats":{"Line":0}},{"line":350,"address":[],"length":0,"stats":{"Line":3}},{"line":357,"address":[],"length":0,"stats":{"Line":3}},{"line":358,"address":[],"length":0,"stats":{"Line":12}},{"line":359,"address":[],"length":0,"stats":{"Line":12}},{"line":360,"address":[],"length":0,"stats":{"Line":1}},{"line":362,"address":[],"length":0,"stats":{"Line":0}},{"line":364,"address":[],"length":0,"stats":{"Line":2}},{"line":365,"address":[],"length":0,"stats":{"Line":0}},{"line":367,"address":[],"length":0,"stats":{"Line":6}},{"line":370,"address":[],"length":0,"stats":{"Line":3}},{"line":378,"address":[],"length":0,"stats":{"Line":3}},{"line":379,"address":[],"length":0,"stats":{"Line":12}},{"line":380,"address":[],"length":0,"stats":{"Line":12}},{"line":381,"address":[],"length":0,"stats":{"Line":1}},{"line":382,"address":[],"length":0,"stats":{"Line":0}},{"line":384,"address":[],"length":0,"stats":{"Line":7}},{"line":387,"address":[],"length":0,"stats":{"Line":2}},{"line":391,"address":[],"length":0,"stats":{"Line":8}},{"line":392,"address":[],"length":0,"stats":{"Line":4}},{"line":393,"address":[],"length":0,"stats":{"Line":0}},{"line":394,"address":[],"length":0,"stats":{"Line":0}},{"line":395,"address":[],"length":0,"stats":{"Line":0}},{"line":396,"address":[],"length":0,"stats":{"Line":0}},{"line":398,"address":[],"length":0,"stats":{"Line":2}},{"line":489,"address":[],"length":0,"stats":{"Line":5}},{"line":490,"address":[],"length":0,"stats":{"Line":10}},{"line":491,"address":[],"length":0,"stats":{"Line":15}},{"line":492,"address":[],"length":0,"stats":{"Line":15}},{"line":493,"address":[],"length":0,"stats":{"Line":20}},{"line":494,"address":[],"length":0,"stats":{"Line":5}},{"line":496,"address":[],"length":0,"stats":{"Line":10}},{"line":497,"address":[],"length":0,"stats":{"Line":15}},{"line":498,"address":[],"length":0,"stats":{"Line":5}},{"line":499,"address":[],"length":0,"stats":{"Line":5}},{"line":500,"address":[],"length":0,"stats":{"Line":15}},{"line":501,"address":[],"length":0,"stats":{"Line":15}},{"line":502,"address":[],"length":0,"stats":{"Line":15}},{"line":503,"address":[],"length":0,"stats":{"Line":15}},{"line":504,"address":[],"length":0,"stats":{"Line":5}},{"line":508,"address":[],"length":0,"stats":{"Line":10}},{"line":509,"address":[],"length":0,"stats":{"Line":10}},{"line":510,"address":[],"length":0,"stats":{"Line":10}},{"line":511,"address":[],"length":0,"stats":{"Line":10}},{"line":512,"address":[],"length":0,"stats":{"Line":8}},{"line":513,"address":[],"length":0,"stats":{"Line":4}},{"line":514,"address":[],"length":0,"stats":{"Line":4}},{"line":515,"address":[],"length":0,"stats":{"Line":2}},{"line":516,"address":[],"length":0,"stats":{"Line":2}},{"line":519,"address":[],"length":0,"stats":{"Line":10}},{"line":520,"address":[],"length":0,"stats":{"Line":15}},{"line":521,"address":[],"length":0,"stats":{"Line":15}},{"line":522,"address":[],"length":0,"stats":{"Line":5}},{"line":524,"address":[],"length":0,"stats":{"Line":15}},{"line":525,"address":[],"length":0,"stats":{"Line":5}},{"line":527,"address":[],"length":0,"stats":{"Line":10}},{"line":528,"address":[],"length":0,"stats":{"Line":3}},{"line":529,"address":[],"length":0,"stats":{"Line":2}},{"line":530,"address":[],"length":0,"stats":{"Line":0}},{"line":531,"address":[],"length":0,"stats":{"Line":0}},{"line":532,"address":[],"length":0,"stats":{"Line":0}},{"line":533,"address":[],"length":0,"stats":{"Line":0}},{"line":534,"address":[],"length":0,"stats":{"Line":0}},{"line":539,"address":[],"length":0,"stats":{"Line":15}},{"line":540,"address":[],"length":0,"stats":{"Line":15}},{"line":541,"address":[],"length":0,"stats":{"Line":10}},{"line":544,"address":[],"length":0,"stats":{"Line":10}},{"line":545,"address":[],"length":0,"stats":{"Line":12}},{"line":546,"address":[],"length":0,"stats":{"Line":3}},{"line":547,"address":[],"length":0,"stats":{"Line":6}},{"line":548,"address":[],"length":0,"stats":{"Line":3}},{"line":549,"address":[],"length":0,"stats":{"Line":3}},{"line":550,"address":[],"length":0,"stats":{"Line":3}},{"line":551,"address":[],"length":0,"stats":{"Line":3}},{"line":554,"address":[],"length":0,"stats":{"Line":2}},{"line":556,"address":[],"length":0,"stats":{"Line":13}},{"line":557,"address":[],"length":0,"stats":{"Line":2}},{"line":559,"address":[],"length":0,"stats":{"Line":3}},{"line":562,"address":[],"length":0,"stats":{"Line":5}},{"line":563,"address":[],"length":0,"stats":{"Line":10}},{"line":564,"address":[],"length":0,"stats":{"Line":10}},{"line":565,"address":[],"length":0,"stats":{"Line":10}},{"line":566,"address":[],"length":0,"stats":{"Line":10}},{"line":567,"address":[],"length":0,"stats":{"Line":10}},{"line":568,"address":[],"length":0,"stats":{"Line":10}},{"line":569,"address":[],"length":0,"stats":{"Line":10}},{"line":570,"address":[],"length":0,"stats":{"Line":10}},{"line":571,"address":[],"length":0,"stats":{"Line":10}},{"line":572,"address":[],"length":0,"stats":{"Line":5}},{"line":573,"address":[],"length":0,"stats":{"Line":5}},{"line":574,"address":[],"length":0,"stats":{"Line":5}},{"line":578,"address":[],"length":0,"stats":{"Line":903}},{"line":579,"address":[],"length":0,"stats":{"Line":903}},{"line":580,"address":[],"length":0,"stats":{"Line":236}},{"line":581,"address":[],"length":0,"stats":{"Line":3376}},{"line":585,"address":[],"length":0,"stats":{"Line":2}},{"line":586,"address":[],"length":0,"stats":{"Line":2}},{"line":587,"address":[],"length":0,"stats":{"Line":3}},{"line":588,"address":[],"length":0,"stats":{"Line":3}},{"line":593,"address":[],"length":0,"stats":{"Line":2330}},{"line":594,"address":[],"length":0,"stats":{"Line":4660}},{"line":595,"address":[],"length":0,"stats":{"Line":196}},{"line":596,"address":[],"length":0,"stats":{"Line":392}},{"line":597,"address":[],"length":0,"stats":{"Line":616}},{"line":598,"address":[],"length":0,"stats":{"Line":56}},{"line":599,"address":[],"length":0,"stats":{"Line":28}},{"line":600,"address":[],"length":0,"stats":{"Line":28}},{"line":604,"address":[],"length":0,"stats":{"Line":2134}},{"line":605,"address":[],"length":0,"stats":{"Line":4268}},{"line":606,"address":[],"length":0,"stats":{"Line":7246}},{"line":607,"address":[],"length":0,"stats":{"Line":1688}},{"line":608,"address":[],"length":0,"stats":{"Line":844}},{"line":609,"address":[],"length":0,"stats":{"Line":844}},{"line":614,"address":[],"length":0,"stats":{"Line":1457}},{"line":615,"address":[],"length":0,"stats":{"Line":1744}},{"line":616,"address":[],"length":0,"stats":{"Line":1}},{"line":617,"address":[],"length":0,"stats":{"Line":5828}},{"line":618,"address":[],"length":0,"stats":{"Line":0}},{"line":623,"address":[],"length":0,"stats":{"Line":1457}},{"line":624,"address":[],"length":0,"stats":{"Line":2914}},{"line":625,"address":[],"length":0,"stats":{"Line":4371}},{"line":627,"address":[],"length":0,"stats":{"Line":2910}},{"line":628,"address":[],"length":0,"stats":{"Line":2709}},{"line":630,"address":[],"length":0,"stats":{"Line":1455}},{"line":633,"address":[],"length":0,"stats":{"Line":4}},{"line":634,"address":[],"length":0,"stats":{"Line":2}},{"line":636,"address":[],"length":0,"stats":{"Line":0}},{"line":642,"address":[],"length":0,"stats":{"Line":845}},{"line":643,"address":[],"length":0,"stats":{"Line":845}},{"line":644,"address":[],"length":0,"stats":{"Line":0}},{"line":647,"address":[],"length":0,"stats":{"Line":4268}},{"line":648,"address":[],"length":0,"stats":{"Line":1688}},{"line":650,"address":[],"length":0,"stats":{"Line":1}},{"line":651,"address":[],"length":0,"stats":{"Line":1}},{"line":654,"address":[],"length":0,"stats":{"Line":2578}},{"line":665,"address":[],"length":0,"stats":{"Line":5}},{"line":666,"address":[],"length":0,"stats":{"Line":15}},{"line":667,"address":[],"length":0,"stats":{"Line":10}},{"line":668,"address":[],"length":0,"stats":{"Line":5}},{"line":669,"address":[],"length":0,"stats":{"Line":15}},{"line":670,"address":[],"length":0,"stats":{"Line":10}},{"line":672,"address":[],"length":0,"stats":{"Line":30}},{"line":676,"address":[],"length":0,"stats":{"Line":10}},{"line":677,"address":[],"length":0,"stats":{"Line":5}},{"line":678,"address":[],"length":0,"stats":{"Line":5}},{"line":682,"address":[],"length":0,"stats":{"Line":5}},{"line":683,"address":[],"length":0,"stats":{"Line":0}},{"line":685,"address":[],"length":0,"stats":{"Line":5}},{"line":686,"address":[],"length":0,"stats":{"Line":6}},{"line":687,"address":[],"length":0,"stats":{"Line":4}},{"line":689,"address":[],"length":0,"stats":{"Line":5}},{"line":690,"address":[],"length":0,"stats":{"Line":5}},{"line":695,"address":[],"length":0,"stats":{"Line":3}},{"line":703,"address":[],"length":0,"stats":{"Line":6}},{"line":704,"address":[],"length":0,"stats":{"Line":3}},{"line":708,"address":[],"length":0,"stats":{"Line":15}},{"line":709,"address":[],"length":0,"stats":{"Line":6}},{"line":710,"address":[],"length":0,"stats":{"Line":3}},{"line":711,"address":[],"length":0,"stats":{"Line":6}},{"line":712,"address":[],"length":0,"stats":{"Line":6}},{"line":713,"address":[],"length":0,"stats":{"Line":9}},{"line":714,"address":[],"length":0,"stats":{"Line":3}},{"line":720,"address":[],"length":0,"stats":{"Line":9}},{"line":721,"address":[],"length":0,"stats":{"Line":6}},{"line":723,"address":[],"length":0,"stats":{"Line":3}},{"line":726,"address":[],"length":0,"stats":{"Line":6}},{"line":727,"address":[],"length":0,"stats":{"Line":0}},{"line":728,"address":[],"length":0,"stats":{"Line":0}},{"line":729,"address":[],"length":0,"stats":{"Line":0}},{"line":731,"address":[],"length":0,"stats":{"Line":6}},{"line":732,"address":[],"length":0,"stats":{"Line":3}},{"line":734,"address":[],"length":0,"stats":{"Line":9}},{"line":735,"address":[],"length":0,"stats":{"Line":5}},{"line":736,"address":[],"length":0,"stats":{"Line":2}},{"line":739,"address":[],"length":0,"stats":{"Line":3}},{"line":740,"address":[],"length":0,"stats":{"Line":0}},{"line":741,"address":[],"length":0,"stats":{"Line":0}},{"line":742,"address":[],"length":0,"stats":{"Line":0}},{"line":746,"address":[],"length":0,"stats":{"Line":6}},{"line":747,"address":[],"length":0,"stats":{"Line":9}},{"line":750,"address":[],"length":0,"stats":{"Line":392}},{"line":751,"address":[],"length":0,"stats":{"Line":0}},{"line":754,"address":[],"length":0,"stats":{"Line":420}},{"line":755,"address":[],"length":0,"stats":{"Line":56}},{"line":760,"address":[],"length":0,"stats":{"Line":0}},{"line":761,"address":[],"length":0,"stats":{"Line":0}},{"line":762,"address":[],"length":0,"stats":{"Line":0}},{"line":763,"address":[],"length":0,"stats":{"Line":0}},{"line":764,"address":[],"length":0,"stats":{"Line":0}},{"line":765,"address":[],"length":0,"stats":{"Line":0}},{"line":766,"address":[],"length":0,"stats":{"Line":0}},{"line":767,"address":[],"length":0,"stats":{"Line":0}},{"line":768,"address":[],"length":0,"stats":{"Line":0}},{"line":770,"address":[],"length":0,"stats":{"Line":0}},{"line":771,"address":[],"length":0,"stats":{"Line":0}},{"line":773,"address":[],"length":0,"stats":{"Line":0}},{"line":774,"address":[],"length":0,"stats":{"Line":0}},{"line":777,"address":[],"length":0,"stats":{"Line":336}},{"line":778,"address":[],"length":0,"stats":{"Line":168}},{"line":782,"address":[],"length":0,"stats":{"Line":56}},{"line":783,"address":[],"length":0,"stats":{"Line":0}},{"line":786,"address":[],"length":0,"stats":{"Line":84}},{"line":788,"address":[],"length":0,"stats":{"Line":28}},{"line":791,"address":[],"length":0,"stats":{"Line":4}},{"line":792,"address":[],"length":0,"stats":{"Line":0}},{"line":793,"address":[],"length":0,"stats":{"Line":0}},{"line":794,"address":[],"length":0,"stats":{"Line":0}},{"line":795,"address":[],"length":0,"stats":{"Line":0}},{"line":797,"address":[],"length":0,"stats":{"Line":0}},{"line":799,"address":[],"length":0,"stats":{"Line":3}},{"line":804,"address":[],"length":0,"stats":{"Line":79}},{"line":805,"address":[],"length":0,"stats":{"Line":46}},{"line":806,"address":[],"length":0,"stats":{"Line":23}},{"line":809,"address":[],"length":0,"stats":{"Line":112}},{"line":810,"address":[],"length":0,"stats":{"Line":84}},{"line":812,"address":[],"length":0,"stats":{"Line":53}},{"line":813,"address":[],"length":0,"stats":{"Line":12}},{"line":814,"address":[],"length":0,"stats":{"Line":3}},{"line":815,"address":[],"length":0,"stats":{"Line":25}},{"line":816,"address":[],"length":0,"stats":{"Line":0}},{"line":817,"address":[],"length":0,"stats":{"Line":0}},{"line":819,"address":[],"length":0,"stats":{"Line":0}},{"line":821,"address":[],"length":0,"stats":{"Line":0}},{"line":825,"address":[],"length":0,"stats":{"Line":6}},{"line":826,"address":[],"length":0,"stats":{"Line":3}},{"line":832,"address":[],"length":0,"stats":{"Line":3}},{"line":837,"address":[],"length":0,"stats":{"Line":6}},{"line":838,"address":[],"length":0,"stats":{"Line":3}},{"line":844,"address":[],"length":0,"stats":{"Line":12}},{"line":845,"address":[],"length":0,"stats":{"Line":6}},{"line":846,"address":[],"length":0,"stats":{"Line":9}},{"line":847,"address":[],"length":0,"stats":{"Line":9}},{"line":848,"address":[],"length":0,"stats":{"Line":0}},{"line":850,"address":[],"length":0,"stats":{"Line":3}},{"line":853,"address":[],"length":0,"stats":{"Line":15}},{"line":857,"address":[],"length":0,"stats":{"Line":24}},{"line":858,"address":[],"length":0,"stats":{"Line":0}},{"line":859,"address":[],"length":0,"stats":{"Line":0}},{"line":860,"address":[],"length":0,"stats":{"Line":0}},{"line":861,"address":[],"length":0,"stats":{"Line":0}},{"line":862,"address":[],"length":0,"stats":{"Line":0}},{"line":865,"address":[],"length":0,"stats":{"Line":27}},{"line":866,"address":[],"length":0,"stats":{"Line":6}},{"line":867,"address":[],"length":0,"stats":{"Line":15}},{"line":868,"address":[],"length":0,"stats":{"Line":9}},{"line":869,"address":[],"length":0,"stats":{"Line":15}},{"line":872,"address":[],"length":0,"stats":{"Line":6}},{"line":878,"address":[],"length":0,"stats":{"Line":1}},{"line":885,"address":[],"length":0,"stats":{"Line":3}},{"line":886,"address":[],"length":0,"stats":{"Line":3}},{"line":887,"address":[],"length":0,"stats":{"Line":1}},{"line":888,"address":[],"length":0,"stats":{"Line":0}},{"line":891,"address":[],"length":0,"stats":{"Line":4}},{"line":892,"address":[],"length":0,"stats":{"Line":4}},{"line":893,"address":[],"length":0,"stats":{"Line":2}},{"line":894,"address":[],"length":0,"stats":{"Line":2}},{"line":895,"address":[],"length":0,"stats":{"Line":1}},{"line":896,"address":[],"length":0,"stats":{"Line":0}},{"line":899,"address":[],"length":0,"stats":{"Line":2}},{"line":902,"address":[],"length":0,"stats":{"Line":3}},{"line":903,"address":[],"length":0,"stats":{"Line":3}},{"line":904,"address":[],"length":0,"stats":{"Line":3}},{"line":908,"address":[],"length":0,"stats":{"Line":3}},{"line":909,"address":[],"length":0,"stats":{"Line":3}},{"line":910,"address":[],"length":0,"stats":{"Line":2}},{"line":911,"address":[],"length":0,"stats":{"Line":2}},{"line":912,"address":[],"length":0,"stats":{"Line":1}},{"line":913,"address":[],"length":0,"stats":{"Line":1}},{"line":914,"address":[],"length":0,"stats":{"Line":1}},{"line":916,"address":[],"length":0,"stats":{"Line":0}},{"line":918,"address":[],"length":0,"stats":{"Line":1}},{"line":919,"address":[],"length":0,"stats":{"Line":1}},{"line":920,"address":[],"length":0,"stats":{"Line":1}},{"line":921,"address":[],"length":0,"stats":{"Line":1}},{"line":922,"address":[],"length":0,"stats":{"Line":1}},{"line":923,"address":[],"length":0,"stats":{"Line":1}},{"line":926,"address":[],"length":0,"stats":{"Line":2}},{"line":928,"address":[],"length":0,"stats":{"Line":3}},{"line":929,"address":[],"length":0,"stats":{"Line":3}},{"line":931,"address":[],"length":0,"stats":{"Line":141}},{"line":932,"address":[],"length":0,"stats":{"Line":94}},{"line":933,"address":[],"length":0,"stats":{"Line":0}},{"line":935,"address":[],"length":0,"stats":{"Line":188}},{"line":936,"address":[],"length":0,"stats":{"Line":94}},{"line":937,"address":[],"length":0,"stats":{"Line":0}},{"line":939,"address":[],"length":0,"stats":{"Line":141}},{"line":940,"address":[],"length":0,"stats":{"Line":94}},{"line":941,"address":[],"length":0,"stats":{"Line":47}},{"line":942,"address":[],"length":0,"stats":{"Line":282}},{"line":943,"address":[],"length":0,"stats":{"Line":47}},{"line":945,"address":[],"length":0,"stats":{"Line":0}},{"line":947,"address":[],"length":0,"stats":{"Line":94}},{"line":948,"address":[],"length":0,"stats":{"Line":94}},{"line":949,"address":[],"length":0,"stats":{"Line":94}},{"line":950,"address":[],"length":0,"stats":{"Line":188}},{"line":951,"address":[],"length":0,"stats":{"Line":47}},{"line":952,"address":[],"length":0,"stats":{"Line":94}},{"line":953,"address":[],"length":0,"stats":{"Line":94}},{"line":954,"address":[],"length":0,"stats":{"Line":94}},{"line":955,"address":[],"length":0,"stats":{"Line":141}},{"line":956,"address":[],"length":0,"stats":{"Line":188}},{"line":957,"address":[],"length":0,"stats":{"Line":188}},{"line":961,"address":[],"length":0,"stats":{"Line":47}},{"line":962,"address":[],"length":0,"stats":{"Line":47}},{"line":963,"address":[],"length":0,"stats":{"Line":1}},{"line":968,"address":[],"length":0,"stats":{"Line":2}},{"line":969,"address":[],"length":0,"stats":{"Line":2}},{"line":970,"address":[],"length":0,"stats":{"Line":1}},{"line":971,"address":[],"length":0,"stats":{"Line":3}},{"line":975,"address":[],"length":0,"stats":{"Line":3}},{"line":976,"address":[],"length":0,"stats":{"Line":2}},{"line":977,"address":[],"length":0,"stats":{"Line":2}},{"line":978,"address":[],"length":0,"stats":{"Line":1}},{"line":979,"address":[],"length":0,"stats":{"Line":1}},{"line":981,"address":[],"length":0,"stats":{"Line":1}},{"line":983,"address":[],"length":0,"stats":{"Line":2}},{"line":984,"address":[],"length":0,"stats":{"Line":0}},{"line":985,"address":[],"length":0,"stats":{"Line":0}},{"line":986,"address":[],"length":0,"stats":{"Line":0}},{"line":987,"address":[],"length":0,"stats":{"Line":0}},{"line":988,"address":[],"length":0,"stats":{"Line":0}},{"line":989,"address":[],"length":0,"stats":{"Line":0}},{"line":990,"address":[],"length":0,"stats":{"Line":0}},{"line":991,"address":[],"length":0,"stats":{"Line":0}},{"line":992,"address":[],"length":0,"stats":{"Line":0}},{"line":993,"address":[],"length":0,"stats":{"Line":0}},{"line":994,"address":[],"length":0,"stats":{"Line":0}},{"line":995,"address":[],"length":0,"stats":{"Line":0}},{"line":996,"address":[],"length":0,"stats":{"Line":0}},{"line":997,"address":[],"length":0,"stats":{"Line":0}},{"line":1004,"address":[],"length":0,"stats":{"Line":1}},{"line":1009,"address":[],"length":0,"stats":{"Line":1}},{"line":1017,"address":[],"length":0,"stats":{"Line":1}},{"line":1018,"address":[],"length":0,"stats":{"Line":0}},{"line":1021,"address":[],"length":0,"stats":{"Line":3}},{"line":1022,"address":[],"length":0,"stats":{"Line":3}},{"line":1023,"address":[],"length":0,"stats":{"Line":1}},{"line":1024,"address":[],"length":0,"stats":{"Line":3}},{"line":1025,"address":[],"length":0,"stats":{"Line":3}},{"line":1026,"address":[],"length":0,"stats":{"Line":2}},{"line":1027,"address":[],"length":0,"stats":{"Line":2}},{"line":1029,"address":[],"length":0,"stats":{"Line":0}},{"line":1031,"address":[],"length":0,"stats":{"Line":1}},{"line":1032,"address":[],"length":0,"stats":{"Line":0}},{"line":1036,"address":[],"length":0,"stats":{"Line":2}},{"line":1039,"address":[],"length":0,"stats":{"Line":1}},{"line":1040,"address":[],"length":0,"stats":{"Line":2}},{"line":1041,"address":[],"length":0,"stats":{"Line":1}},{"line":1042,"address":[],"length":0,"stats":{"Line":1}},{"line":1043,"address":[],"length":0,"stats":{"Line":1}},{"line":1044,"address":[],"length":0,"stats":{"Line":1}},{"line":1047,"address":[],"length":0,"stats":{"Line":6}},{"line":1048,"address":[],"length":0,"stats":{"Line":2}},{"line":1049,"address":[],"length":0,"stats":{"Line":3}},{"line":1050,"address":[],"length":0,"stats":{"Line":0}},{"line":1053,"address":[],"length":0,"stats":{"Line":3}},{"line":1054,"address":[],"length":0,"stats":{"Line":2393}},{"line":1055,"address":[],"length":0,"stats":{"Line":1594}},{"line":1056,"address":[],"length":0,"stats":{"Line":0}},{"line":1058,"address":[],"length":0,"stats":{"Line":1594}},{"line":1059,"address":[],"length":0,"stats":{"Line":0}},{"line":1061,"address":[],"length":0,"stats":{"Line":3188}},{"line":1062,"address":[],"length":0,"stats":{"Line":2391}},{"line":1063,"address":[],"length":0,"stats":{"Line":797}},{"line":1064,"address":[],"length":0,"stats":{"Line":7173}},{"line":1065,"address":[],"length":0,"stats":{"Line":3188}},{"line":1069,"address":[],"length":0,"stats":{"Line":2}},{"line":1070,"address":[],"length":0,"stats":{"Line":0}},{"line":1071,"address":[],"length":0,"stats":{"Line":0}},{"line":1072,"address":[],"length":0,"stats":{"Line":0}},{"line":1073,"address":[],"length":0,"stats":{"Line":0}},{"line":1076,"address":[],"length":0,"stats":{"Line":2}},{"line":1078,"address":[],"length":0,"stats":{"Line":2}},{"line":1079,"address":[],"length":0,"stats":{"Line":0}},{"line":1081,"address":[],"length":0,"stats":{"Line":1}},{"line":1089,"address":[],"length":0,"stats":{"Line":2}},{"line":1098,"address":[],"length":0,"stats":{"Line":12}},{"line":1100,"address":[],"length":0,"stats":{"Line":2}},{"line":1101,"address":[],"length":0,"stats":{"Line":2}},{"line":1102,"address":[],"length":0,"stats":{"Line":4}},{"line":1103,"address":[],"length":0,"stats":{"Line":4}},{"line":1104,"address":[],"length":0,"stats":{"Line":4}},{"line":1105,"address":[],"length":0,"stats":{"Line":4}},{"line":1106,"address":[],"length":0,"stats":{"Line":4}},{"line":1112,"address":[],"length":0,"stats":{"Line":797}},{"line":1113,"address":[],"length":0,"stats":{"Line":3985}},{"line":1114,"address":[],"length":0,"stats":{"Line":797}},{"line":1115,"address":[],"length":0,"stats":{"Line":797}},{"line":1120,"address":[],"length":0,"stats":{"Line":0}},{"line":1121,"address":[],"length":0,"stats":{"Line":0}},{"line":1122,"address":[],"length":0,"stats":{"Line":0}},{"line":1123,"address":[],"length":0,"stats":{"Line":0}},{"line":1124,"address":[],"length":0,"stats":{"Line":0}},{"line":1125,"address":[],"length":0,"stats":{"Line":0}},{"line":1126,"address":[],"length":0,"stats":{"Line":0}},{"line":1128,"address":[],"length":0,"stats":{"Line":0}},{"line":1132,"address":[],"length":0,"stats":{"Line":1}},{"line":1133,"address":[],"length":0,"stats":{"Line":3}},{"line":1134,"address":[],"length":0,"stats":{"Line":1}},{"line":1135,"address":[],"length":0,"stats":{"Line":4}},{"line":1136,"address":[],"length":0,"stats":{"Line":1}},{"line":1137,"address":[],"length":0,"stats":{"Line":1}},{"line":1139,"address":[],"length":0,"stats":{"Line":0}},{"line":1140,"address":[],"length":0,"stats":{"Line":0}},{"line":1141,"address":[],"length":0,"stats":{"Line":0}},{"line":1143,"address":[],"length":0,"stats":{"Line":0}},{"line":1145,"address":[],"length":0,"stats":{"Line":1}},{"line":1150,"address":[],"length":0,"stats":{"Line":797}},{"line":1154,"address":[],"length":0,"stats":{"Line":1594}},{"line":1155,"address":[],"length":0,"stats":{"Line":3985}},{"line":1156,"address":[],"length":0,"stats":{"Line":797}},{"line":1157,"address":[],"length":0,"stats":{"Line":0}},{"line":1159,"address":[],"length":0,"stats":{"Line":797}},{"line":1160,"address":[],"length":0,"stats":{"Line":0}},{"line":1162,"address":[],"length":0,"stats":{"Line":8767}},{"line":1163,"address":[],"length":0,"stats":{"Line":5579}},{"line":1164,"address":[],"length":0,"stats":{"Line":797}},{"line":1169,"address":[],"length":0,"stats":{"Line":797}},{"line":1175,"address":[],"length":0,"stats":{"Line":2391}},{"line":1176,"address":[],"length":0,"stats":{"Line":3188}},{"line":1177,"address":[],"length":0,"stats":{"Line":797}},{"line":1178,"address":[],"length":0,"stats":{"Line":3188}},{"line":1179,"address":[],"length":0,"stats":{"Line":7173}},{"line":1180,"address":[],"length":0,"stats":{"Line":7173}},{"line":1183,"address":[],"length":0,"stats":{"Line":1594}},{"line":1184,"address":[],"length":0,"stats":{"Line":797}},{"line":1185,"address":[],"length":0,"stats":{"Line":797}},{"line":1186,"address":[],"length":0,"stats":{"Line":3985}},{"line":1187,"address":[],"length":0,"stats":{"Line":797}},{"line":1190,"address":[],"length":0,"stats":{"Line":797}},{"line":1191,"address":[],"length":0,"stats":{"Line":0}},{"line":1193,"address":[],"length":0,"stats":{"Line":2391}},{"line":1194,"address":[],"length":0,"stats":{"Line":4782}},{"line":1195,"address":[],"length":0,"stats":{"Line":3188}},{"line":1196,"address":[],"length":0,"stats":{"Line":4782}},{"line":1198,"address":[],"length":0,"stats":{"Line":797}},{"line":1204,"address":[],"length":0,"stats":{"Line":47}},{"line":1208,"address":[],"length":0,"stats":{"Line":94}},{"line":1209,"address":[],"length":0,"stats":{"Line":235}},{"line":1210,"address":[],"length":0,"stats":{"Line":47}},{"line":1211,"address":[],"length":0,"stats":{"Line":0}},{"line":1213,"address":[],"length":0,"stats":{"Line":47}},{"line":1214,"address":[],"length":0,"stats":{"Line":0}},{"line":1216,"address":[],"length":0,"stats":{"Line":94}},{"line":1217,"address":[],"length":0,"stats":{"Line":94}},{"line":1218,"address":[],"length":0,"stats":{"Line":141}},{"line":1219,"address":[],"length":0,"stats":{"Line":94}},{"line":1220,"address":[],"length":0,"stats":{"Line":517}},{"line":1223,"address":[],"length":0,"stats":{"Line":94}},{"line":1224,"address":[],"length":0,"stats":{"Line":94}},{"line":1225,"address":[],"length":0,"stats":{"Line":47}},{"line":1226,"address":[],"length":0,"stats":{"Line":94}},{"line":1227,"address":[],"length":0,"stats":{"Line":47}},{"line":1230,"address":[],"length":0,"stats":{"Line":47}},{"line":1231,"address":[],"length":0,"stats":{"Line":0}},{"line":1233,"address":[],"length":0,"stats":{"Line":141}},{"line":1234,"address":[],"length":0,"stats":{"Line":47}},{"line":1238,"address":[],"length":0,"stats":{"Line":1}},{"line":1241,"address":[],"length":0,"stats":{"Line":2}},{"line":1242,"address":[],"length":0,"stats":{"Line":2}},{"line":1243,"address":[],"length":0,"stats":{"Line":1}},{"line":1244,"address":[],"length":0,"stats":{"Line":4}},{"line":1245,"address":[],"length":0,"stats":{"Line":1}},{"line":1246,"address":[],"length":0,"stats":{"Line":1}},{"line":1248,"address":[],"length":0,"stats":{"Line":0}},{"line":1249,"address":[],"length":0,"stats":{"Line":0}},{"line":1250,"address":[],"length":0,"stats":{"Line":0}},{"line":1253,"address":[],"length":0,"stats":{"Line":0}},{"line":1254,"address":[],"length":0,"stats":{"Line":0}},{"line":1255,"address":[],"length":0,"stats":{"Line":0}},{"line":1256,"address":[],"length":0,"stats":{"Line":0}},{"line":1260,"address":[],"length":0,"stats":{"Line":0}},{"line":1261,"address":[],"length":0,"stats":{"Line":0}},{"line":1263,"address":[],"length":0,"stats":{"Line":0}},{"line":1264,"address":[],"length":0,"stats":{"Line":0}},{"line":1266,"address":[],"length":0,"stats":{"Line":0}},{"line":1267,"address":[],"length":0,"stats":{"Line":0}},{"line":1269,"address":[],"length":0,"stats":{"Line":2}},{"line":1270,"address":[],"length":0,"stats":{"Line":1}},{"line":1272,"address":[],"length":0,"stats":{"Line":0}},{"line":1278,"address":[],"length":0,"stats":{"Line":6}},{"line":1287,"address":[],"length":0,"stats":{"Line":12}},{"line":1288,"address":[],"length":0,"stats":{"Line":0}},{"line":1289,"address":[],"length":0,"stats":{"Line":0}},{"line":1290,"address":[],"length":0,"stats":{"Line":0}},{"line":1291,"address":[],"length":0,"stats":{"Line":0}},{"line":1292,"address":[],"length":0,"stats":{"Line":0}},{"line":1293,"address":[],"length":0,"stats":{"Line":0}},{"line":1294,"address":[],"length":0,"stats":{"Line":0}},{"line":1296,"address":[],"length":0,"stats":{"Line":6}},{"line":1298,"address":[],"length":0,"stats":{"Line":6}},{"line":1300,"address":[],"length":0,"stats":{"Line":6}},{"line":1301,"address":[],"length":0,"stats":{"Line":6}},{"line":1302,"address":[],"length":0,"stats":{"Line":6}},{"line":1303,"address":[],"length":0,"stats":{"Line":6}},{"line":1304,"address":[],"length":0,"stats":{"Line":6}},{"line":1305,"address":[],"length":0,"stats":{"Line":6}},{"line":1308,"address":[],"length":0,"stats":{"Line":9}},{"line":1309,"address":[],"length":0,"stats":{"Line":6}},{"line":1310,"address":[],"length":0,"stats":{"Line":6}},{"line":1311,"address":[],"length":0,"stats":{"Line":6}},{"line":1312,"address":[],"length":0,"stats":{"Line":6}},{"line":1313,"address":[],"length":0,"stats":{"Line":6}},{"line":1314,"address":[],"length":0,"stats":{"Line":6}},{"line":1315,"address":[],"length":0,"stats":{"Line":3}},{"line":1316,"address":[],"length":0,"stats":{"Line":3}},{"line":1319,"address":[],"length":0,"stats":{"Line":9}},{"line":1323,"address":[],"length":0,"stats":{"Line":0}},{"line":1324,"address":[],"length":0,"stats":{"Line":0}},{"line":1331,"address":[],"length":0,"stats":{"Line":0}},{"line":1332,"address":[],"length":0,"stats":{"Line":0}},{"line":1333,"address":[],"length":0,"stats":{"Line":0}},{"line":1334,"address":[],"length":0,"stats":{"Line":0}},{"line":1335,"address":[],"length":0,"stats":{"Line":0}},{"line":1336,"address":[],"length":0,"stats":{"Line":0}},{"line":1338,"address":[],"length":0,"stats":{"Line":0}},{"line":1339,"address":[],"length":0,"stats":{"Line":0}},{"line":1340,"address":[],"length":0,"stats":{"Line":0}},{"line":1343,"address":[],"length":0,"stats":{"Line":0}},{"line":1344,"address":[],"length":0,"stats":{"Line":0}},{"line":1345,"address":[],"length":0,"stats":{"Line":0}},{"line":1346,"address":[],"length":0,"stats":{"Line":0}},{"line":1347,"address":[],"length":0,"stats":{"Line":0}},{"line":1348,"address":[],"length":0,"stats":{"Line":0}},{"line":1349,"address":[],"length":0,"stats":{"Line":0}},{"line":1350,"address":[],"length":0,"stats":{"Line":0}},{"line":1351,"address":[],"length":0,"stats":{"Line":0}},{"line":1355,"address":[],"length":0,"stats":{"Line":0}},{"line":1359,"address":[],"length":0,"stats":{"Line":31}},{"line":1362,"address":[],"length":0,"stats":{"Line":31}},{"line":1368,"address":[],"length":0,"stats":{"Line":0}},{"line":1369,"address":[],"length":0,"stats":{"Line":0}},{"line":1370,"address":[],"length":0,"stats":{"Line":0}},{"line":1371,"address":[],"length":0,"stats":{"Line":0}},{"line":1372,"address":[],"length":0,"stats":{"Line":0}},{"line":1373,"address":[],"length":0,"stats":{"Line":0}},{"line":1374,"address":[],"length":0,"stats":{"Line":0}},{"line":1375,"address":[],"length":0,"stats":{"Line":0}},{"line":1378,"address":[],"length":0,"stats":{"Line":0}},{"line":1382,"address":[],"length":0,"stats":{"Line":3}},{"line":1383,"address":[],"length":0,"stats":{"Line":9}},{"line":1384,"address":[],"length":0,"stats":{"Line":2227}},{"line":1385,"address":[],"length":0,"stats":{"Line":8896}},{"line":1386,"address":[],"length":0,"stats":{"Line":8896}},{"line":1387,"address":[],"length":0,"stats":{"Line":8313712}},{"line":1388,"address":[],"length":0,"stats":{"Line":29059072}},{"line":1389,"address":[],"length":0,"stats":{"Line":49815552}},{"line":1390,"address":[],"length":0,"stats":{"Line":49815552}},{"line":1392,"address":[],"length":0,"stats":{"Line":12453888}},{"line":1395,"address":[],"length":0,"stats":{"Line":3}},{"line":1404,"address":[],"length":0,"stats":{"Line":3}},{"line":1410,"address":[],"length":0,"stats":{"Line":12}},{"line":1412,"address":[],"length":0,"stats":{"Line":15}},{"line":1413,"address":[],"length":0,"stats":{"Line":6}},{"line":1415,"address":[],"length":0,"stats":{"Line":9}},{"line":1416,"address":[],"length":0,"stats":{"Line":9}},{"line":1417,"address":[],"length":0,"stats":{"Line":6}},{"line":1419,"address":[],"length":0,"stats":{"Line":6}},{"line":1420,"address":[],"length":0,"stats":{"Line":6}},{"line":1421,"address":[],"length":0,"stats":{"Line":15}},{"line":1422,"address":[],"length":0,"stats":{"Line":9}},{"line":1423,"address":[],"length":0,"stats":{"Line":0}},{"line":1425,"address":[],"length":0,"stats":{"Line":4451}},{"line":1426,"address":[],"length":0,"stats":{"Line":11120}},{"line":1427,"address":[],"length":0,"stats":{"Line":11120}},{"line":1428,"address":[],"length":0,"stats":{"Line":4448}},{"line":1430,"address":[],"length":0,"stats":{"Line":3}},{"line":1444,"address":[],"length":0,"stats":{"Line":4}},{"line":1445,"address":[],"length":0,"stats":{"Line":8}},{"line":1446,"address":[],"length":0,"stats":{"Line":12}},{"line":1447,"address":[],"length":0,"stats":{"Line":12}},{"line":1448,"address":[],"length":0,"stats":{"Line":20}},{"line":1450,"address":[],"length":0,"stats":{"Line":6}},{"line":1451,"address":[],"length":0,"stats":{"Line":4}},{"line":1453,"address":[],"length":0,"stats":{"Line":6}},{"line":1456,"address":[],"length":0,"stats":{"Line":38}},{"line":1458,"address":[],"length":0,"stats":{"Line":2}},{"line":1459,"address":[],"length":0,"stats":{"Line":6}},{"line":1461,"address":[],"length":0,"stats":{"Line":4}},{"line":1462,"address":[],"length":0,"stats":{"Line":6}},{"line":1463,"address":[],"length":0,"stats":{"Line":8}},{"line":1464,"address":[],"length":0,"stats":{"Line":0}},{"line":1466,"address":[],"length":0,"stats":{"Line":12}},{"line":1467,"address":[],"length":0,"stats":{"Line":12}},{"line":1468,"address":[],"length":0,"stats":{"Line":12}},{"line":1470,"address":[],"length":0,"stats":{"Line":4}},{"line":1471,"address":[],"length":0,"stats":{"Line":6}},{"line":1474,"address":[],"length":0,"stats":{"Line":26}},{"line":1476,"address":[],"length":0,"stats":{"Line":6}},{"line":1477,"address":[],"length":0,"stats":{"Line":2}},{"line":1478,"address":[],"length":0,"stats":{"Line":8}},{"line":1483,"address":[],"length":0,"stats":{"Line":12}},{"line":1484,"address":[],"length":0,"stats":{"Line":1}},{"line":1487,"address":[],"length":0,"stats":{"Line":9}},{"line":1488,"address":[],"length":0,"stats":{"Line":3}},{"line":1489,"address":[],"length":0,"stats":{"Line":2}},{"line":1490,"address":[],"length":0,"stats":{"Line":8}},{"line":1491,"address":[],"length":0,"stats":{"Line":8}},{"line":1492,"address":[],"length":0,"stats":{"Line":8}},{"line":1493,"address":[],"length":0,"stats":{"Line":8}},{"line":1495,"address":[],"length":0,"stats":{"Line":6}},{"line":1497,"address":[],"length":0,"stats":{"Line":12}},{"line":1498,"address":[],"length":0,"stats":{"Line":12}},{"line":1499,"address":[],"length":0,"stats":{"Line":12}},{"line":1500,"address":[],"length":0,"stats":{"Line":10}},{"line":1501,"address":[],"length":0,"stats":{"Line":12}},{"line":1502,"address":[],"length":0,"stats":{"Line":10}},{"line":1503,"address":[],"length":0,"stats":{"Line":10}},{"line":1504,"address":[],"length":0,"stats":{"Line":10}},{"line":1505,"address":[],"length":0,"stats":{"Line":14}},{"line":1506,"address":[],"length":0,"stats":{"Line":14}},{"line":1507,"address":[],"length":0,"stats":{"Line":14}},{"line":1508,"address":[],"length":0,"stats":{"Line":14}},{"line":1509,"address":[],"length":0,"stats":{"Line":6}},{"line":1511,"address":[],"length":0,"stats":{"Line":4}},{"line":1514,"address":[],"length":0,"stats":{"Line":3}},{"line":1515,"address":[],"length":0,"stats":{"Line":1}},{"line":1518,"address":[],"length":0,"stats":{"Line":0}},{"line":1519,"address":[],"length":0,"stats":{"Line":0}},{"line":1520,"address":[],"length":0,"stats":{"Line":0}},{"line":1521,"address":[],"length":0,"stats":{"Line":0}},{"line":1522,"address":[],"length":0,"stats":{"Line":0}},{"line":1526,"address":[],"length":0,"stats":{"Line":2}},{"line":1527,"address":[],"length":0,"stats":{"Line":3}},{"line":1528,"address":[],"length":0,"stats":{"Line":4}},{"line":1529,"address":[],"length":0,"stats":{"Line":2}},{"line":1530,"address":[],"length":0,"stats":{"Line":4}},{"line":1532,"address":[],"length":0,"stats":{"Line":5}},{"line":1533,"address":[],"length":0,"stats":{"Line":5}},{"line":1534,"address":[],"length":0,"stats":{"Line":4}},{"line":1536,"address":[],"length":0,"stats":{"Line":3}},{"line":1537,"address":[],"length":0,"stats":{"Line":2}},{"line":1538,"address":[],"length":0,"stats":{"Line":1}},{"line":1539,"address":[],"length":0,"stats":{"Line":1}},{"line":1540,"address":[],"length":0,"stats":{"Line":1}},{"line":1544,"address":[],"length":0,"stats":{"Line":0}},{"line":1548,"address":[],"length":0,"stats":{"Line":6}},{"line":1549,"address":[],"length":0,"stats":{"Line":3}},{"line":1550,"address":[],"length":0,"stats":{"Line":2}},{"line":1552,"address":[],"length":0,"stats":{"Line":2}},{"line":1563,"address":[],"length":0,"stats":{"Line":4}},{"line":1568,"address":[],"length":0,"stats":{"Line":8}},{"line":1569,"address":[],"length":0,"stats":{"Line":34}},{"line":1570,"address":[],"length":0,"stats":{"Line":15}},{"line":1572,"address":[],"length":0,"stats":{"Line":1}},{"line":1573,"address":[],"length":0,"stats":{"Line":1}},{"line":1575,"address":[],"length":0,"stats":{"Line":1}},{"line":1576,"address":[],"length":0,"stats":{"Line":1}},{"line":1577,"address":[],"length":0,"stats":{"Line":1}},{"line":1578,"address":[],"length":0,"stats":{"Line":1}},{"line":1580,"address":[],"length":0,"stats":{"Line":14}},{"line":1582,"address":[],"length":0,"stats":{"Line":0}},{"line":1583,"address":[],"length":0,"stats":{"Line":0}},{"line":1585,"address":[],"length":0,"stats":{"Line":0}},{"line":1586,"address":[],"length":0,"stats":{"Line":0}},{"line":1587,"address":[],"length":0,"stats":{"Line":0}},{"line":1588,"address":[],"length":0,"stats":{"Line":0}},{"line":1592,"address":[],"length":0,"stats":{"Line":3}},{"line":1653,"address":[],"length":0,"stats":{"Line":16}},{"line":1656,"address":[],"length":0,"stats":{"Line":32}},{"line":1665,"address":[],"length":0,"stats":{"Line":15}},{"line":1666,"address":[],"length":0,"stats":{"Line":60}},{"line":1667,"address":[],"length":0,"stats":{"Line":49}},{"line":1672,"address":[],"length":0,"stats":{"Line":14}},{"line":1673,"address":[],"length":0,"stats":{"Line":28}},{"line":1676,"address":[],"length":0,"stats":{"Line":1}},{"line":1677,"address":[],"length":0,"stats":{"Line":1}},{"line":1680,"address":[],"length":0,"stats":{"Line":1}},{"line":1681,"address":[],"length":0,"stats":{"Line":1}},{"line":1684,"address":[],"length":0,"stats":{"Line":1}},{"line":1685,"address":[],"length":0,"stats":{"Line":1}},{"line":1688,"address":[],"length":0,"stats":{"Line":1}},{"line":1689,"address":[],"length":0,"stats":{"Line":1}},{"line":1692,"address":[],"length":0,"stats":{"Line":1}},{"line":1693,"address":[],"length":0,"stats":{"Line":1}},{"line":1696,"address":[],"length":0,"stats":{"Line":3}},{"line":1697,"address":[],"length":0,"stats":{"Line":3}},{"line":1698,"address":[],"length":0,"stats":{"Line":1}},{"line":1703,"address":[],"length":0,"stats":{"Line":8}},{"line":1704,"address":[],"length":0,"stats":{"Line":4}},{"line":1705,"address":[],"length":0,"stats":{"Line":4}},{"line":1706,"address":[],"length":0,"stats":{"Line":2}},{"line":1709,"address":[],"length":0,"stats":{"Line":3}},{"line":1710,"address":[],"length":0,"stats":{"Line":9}},{"line":1711,"address":[],"length":0,"stats":{"Line":12}},{"line":1712,"address":[],"length":0,"stats":{"Line":6}},{"line":1715,"address":[],"length":0,"stats":{"Line":1}},{"line":1717,"address":[],"length":0,"stats":{"Line":4}},{"line":1718,"address":[],"length":0,"stats":{"Line":1}},{"line":1721,"address":[],"length":0,"stats":{"Line":11}},{"line":1722,"address":[],"length":0,"stats":{"Line":33}},{"line":1723,"address":[],"length":0,"stats":{"Line":44}},{"line":1724,"address":[],"length":0,"stats":{"Line":33}},{"line":1727,"address":[],"length":0,"stats":{"Line":1}},{"line":1728,"address":[],"length":0,"stats":{"Line":3}},{"line":1729,"address":[],"length":0,"stats":{"Line":4}},{"line":1730,"address":[],"length":0,"stats":{"Line":4}},{"line":1733,"address":[],"length":0,"stats":{"Line":1}},{"line":1742,"address":[],"length":0,"stats":{"Line":1}},{"line":1745,"address":[],"length":0,"stats":{"Line":2}},{"line":1746,"address":[],"length":0,"stats":{"Line":8}},{"line":1747,"address":[],"length":0,"stats":{"Line":2}},{"line":1750,"address":[],"length":0,"stats":{"Line":1}},{"line":1752,"address":[],"length":0,"stats":{"Line":1}},{"line":1755,"address":[],"length":0,"stats":{"Line":1}},{"line":1757,"address":[],"length":0,"stats":{"Line":1}},{"line":1760,"address":[],"length":0,"stats":{"Line":1}},{"line":1761,"address":[],"length":0,"stats":{"Line":2}},{"line":1764,"address":[],"length":0,"stats":{"Line":1}},{"line":1765,"address":[],"length":0,"stats":{"Line":1}},{"line":1766,"address":[],"length":0,"stats":{"Line":1}},{"line":1767,"address":[],"length":0,"stats":{"Line":1}},{"line":1776,"address":[],"length":0,"stats":{"Line":3}},{"line":1777,"address":[],"length":0,"stats":{"Line":6}},{"line":1778,"address":[],"length":0,"stats":{"Line":0}},{"line":1780,"address":[],"length":0,"stats":{"Line":9}},{"line":1781,"address":[],"length":0,"stats":{"Line":6}},{"line":1782,"address":[],"length":0,"stats":{"Line":1}},{"line":1784,"address":[],"length":0,"stats":{"Line":3}},{"line":1785,"address":[],"length":0,"stats":{"Line":1}},{"line":1788,"address":[],"length":0,"stats":{"Line":3}},{"line":1789,"address":[],"length":0,"stats":{"Line":1}},{"line":1791,"address":[],"length":0,"stats":{"Line":2}},{"line":1792,"address":[],"length":0,"stats":{"Line":1}},{"line":1793,"address":[],"length":0,"stats":{"Line":3}},{"line":1794,"address":[],"length":0,"stats":{"Line":1}},{"line":1795,"address":[],"length":0,"stats":{"Line":3}},{"line":1796,"address":[],"length":0,"stats":{"Line":1}},{"line":1798,"address":[],"length":0,"stats":{"Line":2}},{"line":1799,"address":[],"length":0,"stats":{"Line":2}},{"line":1800,"address":[],"length":0,"stats":{"Line":2}},{"line":1801,"address":[],"length":0,"stats":{"Line":3}},{"line":1803,"address":[],"length":0,"stats":{"Line":5}},{"line":1804,"address":[],"length":0,"stats":{"Line":2}},{"line":1806,"address":[],"length":0,"stats":{"Line":3}},{"line":1809,"address":[],"length":0,"stats":{"Line":1}},{"line":1810,"address":[],"length":0,"stats":{"Line":3}},{"line":1811,"address":[],"length":0,"stats":{"Line":3}},{"line":1813,"address":[],"length":0,"stats":{"Line":3}},{"line":1814,"address":[],"length":0,"stats":{"Line":3}},{"line":1815,"address":[],"length":0,"stats":{"Line":3}},{"line":1816,"address":[],"length":0,"stats":{"Line":3}},{"line":1817,"address":[],"length":0,"stats":{"Line":1}},{"line":1819,"address":[],"length":0,"stats":{"Line":2}},{"line":1820,"address":[],"length":0,"stats":{"Line":2}},{"line":1821,"address":[],"length":0,"stats":{"Line":1}},{"line":1822,"address":[],"length":0,"stats":{"Line":0}},{"line":1824,"address":[],"length":0,"stats":{"Line":1}},{"line":1825,"address":[],"length":0,"stats":{"Line":0}},{"line":1830,"address":[],"length":0,"stats":{"Line":3}},{"line":1831,"address":[],"length":0,"stats":{"Line":3}},{"line":1832,"address":[],"length":0,"stats":{"Line":3}},{"line":1834,"address":[],"length":0,"stats":{"Line":4}},{"line":1835,"address":[],"length":0,"stats":{"Line":3}},{"line":1838,"address":[],"length":0,"stats":{"Line":1}},{"line":1839,"address":[],"length":0,"stats":{"Line":1}},{"line":1840,"address":[],"length":0,"stats":{"Line":1}},{"line":1841,"address":[],"length":0,"stats":{"Line":1}},{"line":1842,"address":[],"length":0,"stats":{"Line":1}},{"line":1843,"address":[],"length":0,"stats":{"Line":1}},{"line":1846,"address":[],"length":0,"stats":{"Line":1}},{"line":1848,"address":[],"length":0,"stats":{"Line":2}},{"line":1849,"address":[],"length":0,"stats":{"Line":2}},{"line":1850,"address":[],"length":0,"stats":{"Line":2}},{"line":1851,"address":[],"length":0,"stats":{"Line":2}},{"line":1852,"address":[],"length":0,"stats":{"Line":1}},{"line":1853,"address":[],"length":0,"stats":{"Line":1}},{"line":1857,"address":[],"length":0,"stats":{"Line":2}},{"line":1858,"address":[],"length":0,"stats":{"Line":1}},{"line":1859,"address":[],"length":0,"stats":{"Line":0}},{"line":1860,"address":[],"length":0,"stats":{"Line":0}},{"line":1861,"address":[],"length":0,"stats":{"Line":0}},{"line":1862,"address":[],"length":0,"stats":{"Line":0}},{"line":1864,"address":[],"length":0,"stats":{"Line":0}},{"line":1865,"address":[],"length":0,"stats":{"Line":0}},{"line":1867,"address":[],"length":0,"stats":{"Line":0}},{"line":1868,"address":[],"length":0,"stats":{"Line":0}},{"line":1869,"address":[],"length":0,"stats":{"Line":0}},{"line":1871,"address":[],"length":0,"stats":{"Line":0}},{"line":1874,"address":[],"length":0,"stats":{"Line":0}},{"line":1875,"address":[],"length":0,"stats":{"Line":0}},{"line":1877,"address":[],"length":0,"stats":{"Line":0}},{"line":1878,"address":[],"length":0,"stats":{"Line":0}},{"line":1879,"address":[],"length":0,"stats":{"Line":0}},{"line":1880,"address":[],"length":0,"stats":{"Line":0}},{"line":1882,"address":[],"length":0,"stats":{"Line":0}},{"line":1883,"address":[],"length":0,"stats":{"Line":0}},{"line":1884,"address":[],"length":0,"stats":{"Line":0}},{"line":1886,"address":[],"length":0,"stats":{"Line":0}},{"line":1887,"address":[],"length":0,"stats":{"Line":0}},{"line":1890,"address":[],"length":0,"stats":{"Line":0}},{"line":1891,"address":[],"length":0,"stats":{"Line":0}},{"line":1892,"address":[],"length":0,"stats":{"Line":0}},{"line":1893,"address":[],"length":0,"stats":{"Line":0}},{"line":1894,"address":[],"length":0,"stats":{"Line":0}},{"line":1895,"address":[],"length":0,"stats":{"Line":0}},{"line":1898,"address":[],"length":0,"stats":{"Line":0}},{"line":1899,"address":[],"length":0,"stats":{"Line":0}},{"line":1900,"address":[],"length":0,"stats":{"Line":0}},{"line":1901,"address":[],"length":0,"stats":{"Line":0}},{"line":1905,"address":[],"length":0,"stats":{"Line":3}},{"line":1907,"address":[],"length":0,"stats":{"Line":2}},{"line":1908,"address":[],"length":0,"stats":{"Line":2}},{"line":1909,"address":[],"length":0,"stats":{"Line":1}},{"line":1910,"address":[],"length":0,"stats":{"Line":1}},{"line":1911,"address":[],"length":0,"stats":{"Line":1}},{"line":1912,"address":[],"length":0,"stats":{"Line":1}},{"line":1914,"address":[],"length":0,"stats":{"Line":1}},{"line":1918,"address":[],"length":0,"stats":{"Line":11}},{"line":1919,"address":[],"length":0,"stats":{"Line":21}},{"line":1920,"address":[],"length":0,"stats":{"Line":11}},{"line":1922,"address":[],"length":0,"stats":{"Line":13}},{"line":1923,"address":[],"length":0,"stats":{"Line":10}},{"line":1924,"address":[],"length":0,"stats":{"Line":0}},{"line":1926,"address":[],"length":0,"stats":{"Line":20}},{"line":1927,"address":[],"length":0,"stats":{"Line":10}},{"line":1929,"address":[],"length":0,"stats":{"Line":10}},{"line":1931,"address":[],"length":0,"stats":{"Line":50}},{"line":1932,"address":[],"length":0,"stats":{"Line":40}},{"line":1933,"address":[],"length":0,"stats":{"Line":20}},{"line":1934,"address":[],"length":0,"stats":{"Line":0}},{"line":1935,"address":[],"length":0,"stats":{"Line":0}},{"line":1936,"address":[],"length":0,"stats":{"Line":0}},{"line":1942,"address":[],"length":0,"stats":{"Line":30}},{"line":1943,"address":[],"length":0,"stats":{"Line":20}},{"line":1944,"address":[],"length":0,"stats":{"Line":650}},{"line":1945,"address":[],"length":0,"stats":{"Line":2560}},{"line":1946,"address":[],"length":0,"stats":{"Line":2560}},{"line":1947,"address":[],"length":0,"stats":{"Line":85120}},{"line":1948,"address":[],"length":0,"stats":{"Line":368640}},{"line":1949,"address":[],"length":0,"stats":{"Line":655360}},{"line":1950,"address":[],"length":0,"stats":{"Line":491520}},{"line":1951,"address":[],"length":0,"stats":{"Line":491520}},{"line":1952,"address":[],"length":0,"stats":{"Line":327680}},{"line":1953,"address":[],"length":0,"stats":{"Line":327680}},{"line":1955,"address":[],"length":0,"stats":{"Line":327680}},{"line":1960,"address":[],"length":0,"stats":{"Line":50}},{"line":1961,"address":[],"length":0,"stats":{"Line":30}},{"line":1963,"address":[],"length":0,"stats":{"Line":20}},{"line":1964,"address":[],"length":0,"stats":{"Line":50}},{"line":1969,"address":[],"length":0,"stats":{"Line":40}},{"line":1970,"address":[],"length":0,"stats":{"Line":40}},{"line":1971,"address":[],"length":0,"stats":{"Line":20}},{"line":1972,"address":[],"length":0,"stats":{"Line":20}},{"line":1974,"address":[],"length":0,"stats":{"Line":30}},{"line":1975,"address":[],"length":0,"stats":{"Line":30}},{"line":1979,"address":[],"length":0,"stats":{"Line":1}},{"line":1985,"address":[],"length":0,"stats":{"Line":1}},{"line":1986,"address":[],"length":0,"stats":{"Line":1}},{"line":1988,"address":[],"length":0,"stats":{"Line":3}},{"line":1989,"address":[],"length":0,"stats":{"Line":0}},{"line":1990,"address":[],"length":0,"stats":{"Line":0}},{"line":1992,"address":[],"length":0,"stats":{"Line":0}},{"line":1993,"address":[],"length":0,"stats":{"Line":0}},{"line":1995,"address":[],"length":0,"stats":{"Line":0}},{"line":1997,"address":[],"length":0,"stats":{"Line":0}},{"line":1998,"address":[],"length":0,"stats":{"Line":0}},{"line":2000,"address":[],"length":0,"stats":{"Line":0}},{"line":2001,"address":[],"length":0,"stats":{"Line":0}},{"line":2004,"address":[],"length":0,"stats":{"Line":0}},{"line":2006,"address":[],"length":0,"stats":{"Line":0}},{"line":2007,"address":[],"length":0,"stats":{"Line":0}},{"line":2009,"address":[],"length":0,"stats":{"Line":0}},{"line":2011,"address":[],"length":0,"stats":{"Line":0}},{"line":2013,"address":[],"length":0,"stats":{"Line":0}},{"line":2014,"address":[],"length":0,"stats":{"Line":0}},{"line":2015,"address":[],"length":0,"stats":{"Line":0}},{"line":2017,"address":[],"length":0,"stats":{"Line":0}},{"line":2018,"address":[],"length":0,"stats":{"Line":0}},{"line":2022,"address":[],"length":0,"stats":{"Line":3}},{"line":2023,"address":[],"length":0,"stats":{"Line":4}},{"line":2024,"address":[],"length":0,"stats":{"Line":2}},{"line":2026,"address":[],"length":0,"stats":{"Line":1}},{"line":2027,"address":[],"length":0,"stats":{"Line":2}},{"line":2028,"address":[],"length":0,"stats":{"Line":3}},{"line":2030,"address":[],"length":0,"stats":{"Line":1}},{"line":2031,"address":[],"length":0,"stats":{"Line":0}},{"line":2033,"address":[],"length":0,"stats":{"Line":3}},{"line":2034,"address":[],"length":0,"stats":{"Line":1}},{"line":2037,"address":[],"length":0,"stats":{"Line":2}},{"line":2038,"address":[],"length":0,"stats":{"Line":1}},{"line":2043,"address":[],"length":0,"stats":{"Line":1}},{"line":2047,"address":[],"length":0,"stats":{"Line":2}},{"line":2048,"address":[],"length":0,"stats":{"Line":2}},{"line":2050,"address":[],"length":0,"stats":{"Line":33}},{"line":2052,"address":[],"length":0,"stats":{"Line":30}},{"line":2053,"address":[],"length":0,"stats":{"Line":40}},{"line":2055,"address":[],"length":0,"stats":{"Line":5}},{"line":2056,"address":[],"length":0,"stats":{"Line":0}},{"line":2059,"address":[],"length":0,"stats":{"Line":1}},{"line":2063,"address":[],"length":0,"stats":{"Line":0}},{"line":2067,"address":[],"length":0,"stats":{"Line":0}},{"line":2068,"address":[],"length":0,"stats":{"Line":0}},{"line":2070,"address":[],"length":0,"stats":{"Line":0}},{"line":2072,"address":[],"length":0,"stats":{"Line":0}},{"line":2073,"address":[],"length":0,"stats":{"Line":0}},{"line":2075,"address":[],"length":0,"stats":{"Line":0}},{"line":2076,"address":[],"length":0,"stats":{"Line":0}},{"line":2079,"address":[],"length":0,"stats":{"Line":0}},{"line":2083,"address":[],"length":0,"stats":{"Line":10}},{"line":2087,"address":[],"length":0,"stats":{"Line":20}},{"line":2089,"address":[],"length":0,"stats":{"Line":30}},{"line":2091,"address":[],"length":0,"stats":{"Line":0}},{"line":2092,"address":[],"length":0,"stats":{"Line":0}},{"line":2094,"address":[],"length":0,"stats":{"Line":50}},{"line":2095,"address":[],"length":0,"stats":{"Line":0}},{"line":2098,"address":[],"length":0,"stats":{"Line":10}},{"line":2102,"address":[],"length":0,"stats":{"Line":0}},{"line":2106,"address":[],"length":0,"stats":{"Line":0}},{"line":2108,"address":[],"length":0,"stats":{"Line":0}},{"line":2110,"address":[],"length":0,"stats":{"Line":0}},{"line":2111,"address":[],"length":0,"stats":{"Line":0}},{"line":2113,"address":[],"length":0,"stats":{"Line":0}},{"line":2114,"address":[],"length":0,"stats":{"Line":0}},{"line":2117,"address":[],"length":0,"stats":{"Line":0}},{"line":2123,"address":[],"length":0,"stats":{"Line":1}},{"line":2124,"address":[],"length":0,"stats":{"Line":1}},{"line":2125,"address":[],"length":0,"stats":{"Line":0}},{"line":2126,"address":[],"length":0,"stats":{"Line":1}},{"line":2127,"address":[],"length":0,"stats":{"Line":0}},{"line":2128,"address":[],"length":0,"stats":{"Line":0}},{"line":2129,"address":[],"length":0,"stats":{"Line":0}},{"line":2130,"address":[],"length":0,"stats":{"Line":0}},{"line":2131,"address":[],"length":0,"stats":{"Line":0}},{"line":2132,"address":[],"length":0,"stats":{"Line":0}},{"line":2133,"address":[],"length":0,"stats":{"Line":0}},{"line":2134,"address":[],"length":0,"stats":{"Line":0}},{"line":2135,"address":[],"length":0,"stats":{"Line":0}},{"line":2136,"address":[],"length":0,"stats":{"Line":0}},{"line":2137,"address":[],"length":0,"stats":{"Line":0}},{"line":2138,"address":[],"length":0,"stats":{"Line":0}},{"line":2139,"address":[],"length":0,"stats":{"Line":0}},{"line":2145,"address":[],"length":0,"stats":{"Line":3}},{"line":2146,"address":[],"length":0,"stats":{"Line":4}},{"line":2150,"address":[],"length":0,"stats":{"Line":1}},{"line":2151,"address":[],"length":0,"stats":{"Line":1}},{"line":2152,"address":[],"length":0,"stats":{"Line":0}},{"line":2153,"address":[],"length":0,"stats":{"Line":1}},{"line":2158,"address":[],"length":0,"stats":{"Line":0}},{"line":2159,"address":[],"length":0,"stats":{"Line":0}},{"line":2160,"address":[],"length":0,"stats":{"Line":0}},{"line":2161,"address":[],"length":0,"stats":{"Line":0}},{"line":2162,"address":[],"length":0,"stats":{"Line":0}},{"line":2163,"address":[],"length":0,"stats":{"Line":0}},{"line":2168,"address":[],"length":0,"stats":{"Line":1}},{"line":2169,"address":[],"length":0,"stats":{"Line":7}},{"line":2170,"address":[],"length":0,"stats":{"Line":4}},{"line":2171,"address":[],"length":0,"stats":{"Line":2}},{"line":2172,"address":[],"length":0,"stats":{"Line":1}},{"line":2174,"address":[],"length":0,"stats":{"Line":0}},{"line":2178,"address":[],"length":0,"stats":{"Line":1}},{"line":2179,"address":[],"length":0,"stats":{"Line":1}},{"line":2180,"address":[],"length":0,"stats":{"Line":0}},{"line":2181,"address":[],"length":0,"stats":{"Line":0}},{"line":2182,"address":[],"length":0,"stats":{"Line":0}},{"line":2183,"address":[],"length":0,"stats":{"Line":0}},{"line":2184,"address":[],"length":0,"stats":{"Line":1}},{"line":2185,"address":[],"length":0,"stats":{"Line":0}},{"line":2186,"address":[],"length":0,"stats":{"Line":0}},{"line":2187,"address":[],"length":0,"stats":{"Line":0}},{"line":2188,"address":[],"length":0,"stats":{"Line":0}},{"line":2189,"address":[],"length":0,"stats":{"Line":0}},{"line":2190,"address":[],"length":0,"stats":{"Line":0}},{"line":2191,"address":[],"length":0,"stats":{"Line":0}},{"line":2192,"address":[],"length":0,"stats":{"Line":0}},{"line":2198,"address":[],"length":0,"stats":{"Line":4}},{"line":2199,"address":[],"length":0,"stats":{"Line":128}},{"line":2200,"address":[],"length":0,"stats":{"Line":12}}],"covered":767,"coverable":1072},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","footagedescription.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::FootageDescription` — codec-internal stream inventory.\n//!\n//! Mirrors `src/codec/src/footagedescription.h`. A value type describing\n//! the streams a `Decoder::probe()` found in a file. Video and subtitle\n//! streams are stored as oakcommon by-value handles; audio streams as\n//! `oakcore_rs::TimeRangeList`/raw audio params. The original's\n//! `Track::Type` mapping and XML load/save are intentionally not reproduced\n//! (NOTES.md §4) — use [`FootageDescription::stream_is_video`] etc.\n\nuse oakcore_rs::{Rational, TimeRange};\n\nuse crate::bridge::common::{OakAudioParams, OakSubtitleParams, OakVideoParams};\n\n/// One stream entry in a footage description.\n#[derive(Clone, Debug)]\npub enum StreamEntry {\n\t/// A video stream (params handle, addref'd).\n\tVideo(OakVideoParams),\n\t/// An audio stream.\n\tAudio(OakAudioParams),\n\t/// A subtitle stream (params handle, addref'd).\n\tSubtitle(OakSubtitleParams),\n}\n\n/// `olive::FootageDescription` — the decoder name plus stream inventory.\n#[derive(Clone, Debug, Default)]\npub struct FootageDescription {\n\t/// The decoder id that produced this description.\n\tdecoder: String,\n\t/// Total number of streams (video + audio + subtitle).\n\ttotal_stream_count: usize,\n\t/// The streams, in probe order.\n\tstreams: Vec\u003cStreamEntry\u003e,\n\t/// The media has a source start time.\n\thas_source_start_time: bool,\n\t/// Source start time (when present).\n\tsource_start_time: Rational,\n\t/// Total duration across streams (may be empty).\n\tduration: Option\u003cTimeRange\u003e,\n}\n\nimpl FootageDescription {\n\t/// New, empty description with the given decoder id.\n\tpub fn new(decoder: \u0026str) -\u003e Self {\n\t\tSelf {\n\t\t\tdecoder: decoder.to_string(),\n\t\t\ttotal_stream_count: 0,\n\t\t\tstreams: Vec::new(),\n\t\t\thas_source_start_time: false,\n\t\t\t// C++ default-constructs the Rational member, i.e. the 0/0\n\t\t\t// null sentinel. It is not meaningful until a source start time\n\t\t\t// is set.\n\t\t\tsource_start_time: Rational::default(),\n\t\t\tduration: None,\n\t\t}\n\t}\n\n\t/// The decoder id.\n\tpub fn decoder(\u0026self) -\u003e \u0026str {\n\t\t\u0026self.decoder\n\t}\n\n\t/// Total stream count.\n\tpub fn total_stream_count(\u0026self) -\u003e usize {\n\t\tself.total_stream_count\n\t}\n\n\t/// Number of video streams.\n\tpub fn video_stream_count(\u0026self) -\u003e usize {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter(|s| matches!(s, StreamEntry::Video(_)))\n\t\t\t.count()\n\t}\n\n\t/// Number of audio streams.\n\tpub fn audio_stream_count(\u0026self) -\u003e usize {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter(|s| matches!(s, StreamEntry::Audio(_)))\n\t\t\t.count()\n\t}\n\n\t/// Number of subtitle streams.\n\tpub fn subtitle_stream_count(\u0026self) -\u003e usize {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter(|s| matches!(s, StreamEntry::Subtitle(_)))\n\t\t\t.count()\n\t}\n\n\t/// Whether the `index`-th stream (in probe order) is a video stream.\n\tpub fn stream_is_video(\u0026self, index: usize) -\u003e bool {\n\t\tself.streams\n\t\t\t.get(index)\n\t\t\t.is_some_and(|s| matches!(s, StreamEntry::Video(_)))\n\t}\n\n\t/// Whether the `index`-th stream (in probe order) is an audio stream.\n\tpub fn stream_is_audio(\u0026self, index: usize) -\u003e bool {\n\t\tself.streams\n\t\t\t.get(index)\n\t\t\t.is_some_and(|s| matches!(s, StreamEntry::Audio(_)))\n\t}\n\n\t/// Whether the `index`-th stream (in probe order) is a subtitle stream.\n\tpub fn stream_is_subtitle(\u0026self, index: usize) -\u003e bool {\n\t\tself.streams\n\t\t\t.get(index)\n\t\t\t.is_some_and(|s| matches!(s, StreamEntry::Subtitle(_)))\n\t}\n\n\t/// The `index`-th video stream's params (by video-stream ordinal).\n\tpub fn get_video_stream(\u0026self, index: usize) -\u003e Option\u003c\u0026OakVideoParams\u003e {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter_map(|s| match s {\n\t\t\t\tStreamEntry::Video(p) =\u003e Some(p),\n\t\t\t\t_ =\u003e None,\n\t\t\t})\n\t\t\t.nth(index)\n\t}\n\n\t/// The `index`-th audio stream's params (by audio-stream ordinal).\n\tpub fn get_audio_stream(\u0026self, index: usize) -\u003e Option\u003c\u0026OakAudioParams\u003e {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter_map(|s| match s {\n\t\t\t\tStreamEntry::Audio(p) =\u003e Some(p),\n\t\t\t\t_ =\u003e None,\n\t\t\t})\n\t\t\t.nth(index)\n\t}\n\n\t/// The `index`-th subtitle stream's params (by subtitle-stream ordinal).\n\tpub fn get_subtitle_stream(\u0026self, index: usize) -\u003e Option\u003c\u0026OakSubtitleParams\u003e {\n\t\tself.streams\n\t\t\t.iter()\n\t\t\t.filter_map(|s| match s {\n\t\t\t\tStreamEntry::Subtitle(p) =\u003e Some(p),\n\t\t\t\t_ =\u003e None,\n\t\t\t})\n\t\t\t.nth(index)\n\t}\n\n\t/// Whether the media has a source start time.\n\tpub fn has_source_start_time(\u0026self) -\u003e bool {\n\t\tself.has_source_start_time\n\t}\n\n\t/// Source start time.\n\tpub fn source_start_time(\u0026self) -\u003e Rational {\n\t\tself.source_start_time\n\t}\n\n\t/// Total duration across streams.\n\tpub fn duration(\u0026self) -\u003e Option\u003cTimeRange\u003e {\n\t\tself.duration\n\t}\n\n\t/// Append one stream entry, mirroring the C++ `add_*_stream` family.\n\t///\n\t/// Test/extension support: the ffi probe tests build\n\t/// `FootageDescription`s through a fake decoder and need a way to\n\t/// populate them. Hidden from docs; never called by production code.\n\t#[doc(hidden)]\n\tpub fn push_stream(\u0026mut self, entry: StreamEntry) {\n\t\tself.total_stream_count += 1;\n\t\tself.streams.push(entry);\n\t}\n\n\t/// Set the total stream count, mirroring the C++ `set_stream_count`\n\t/// (which records every stream in the container, including ones the\n\t/// probe could not describe and therefore did not push).\n\t///\n\t/// Hidden from docs; probe/extension support only.\n\t#[doc(hidden)]\n\tpub fn set_stream_count(\u0026mut self, count: usize) {\n\t\tself.total_stream_count = count;\n\t}\n\n\t/// Set the source start time, mirroring the C++ `set_source_start_time`.\n\t/// `source` is the raw metadata source kind (unused by the Rust port).\n\t///\n\t/// Hidden from docs; probe/extension support only.\n\t#[doc(hidden)]\n\tpub fn set_source_start_time(\u0026mut self, time: Rational, _source: i32) {\n\t\tself.has_source_start_time = true;\n\t\tself.source_start_time = time;\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn video_params(index: i32) -\u003e OakVideoParams {\n\t\tOakVideoParams {\n\t\t\tctx: index as usize as *mut std::ffi::c_void,\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\tfn audio_params() -\u003e OakAudioParams {\n\t\tOakAudioParams {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\tfn subtitle_params() -\u003e OakSubtitleParams {\n\t\tOakSubtitleParams {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: crate::handle::OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\t#[test]\n\tfn new_description_is_empty() {\n\t\tlet d = FootageDescription::new(\"ffmpeg\");\n\t\tassert_eq!(d.decoder(), \"ffmpeg\");\n\t\tassert_eq!(d.total_stream_count(), 0);\n\t\tassert_eq!(d.video_stream_count(), 0);\n\t\tassert_eq!(d.audio_stream_count(), 0);\n\t\tassert_eq!(d.subtitle_stream_count(), 0);\n\t\tassert!(!d.has_source_start_time());\n\t\tassert!(d.duration().is_none());\n\t}\n\n\t#[test]\n\tfn push_stream_updates_counts_and_queries() {\n\t\tlet mut d = FootageDescription::new(\"mock\");\n\t\td.push_stream(StreamEntry::Video(video_params(0)));\n\t\td.push_stream(StreamEntry::Audio(audio_params()));\n\t\td.push_stream(StreamEntry::Subtitle(subtitle_params()));\n\t\td.push_stream(StreamEntry::Video(video_params(1)));\n\n\t\tassert_eq!(d.total_stream_count(), 4);\n\t\tassert_eq!(d.video_stream_count(), 2);\n\t\tassert_eq!(d.audio_stream_count(), 1);\n\t\tassert_eq!(d.subtitle_stream_count(), 1);\n\n\t\t// stream_is_* by probe order.\n\t\tassert!(d.stream_is_video(0));\n\t\tassert!(d.stream_is_audio(1));\n\t\tassert!(d.stream_is_subtitle(2));\n\t\tassert!(d.stream_is_video(3));\n\n\t\t// Ordinal getters.\n\t\tassert!(d.get_video_stream(0).is_some());\n\t\tassert!(d.get_video_stream(1).is_some());\n\t\tassert!(d.get_video_stream(2).is_none());\n\t\tassert!(d.get_audio_stream(0).is_some());\n\t\tassert!(d.get_audio_stream(1).is_none());\n\t\tassert!(d.get_subtitle_stream(0).is_some());\n\t\tassert!(d.get_subtitle_stream(1).is_none());\n\t}\n}\n","traces":[{"line":60,"address":[],"length":0,"stats":{"Line":11}},{"line":62,"address":[],"length":0,"stats":{"Line":33}},{"line":64,"address":[],"length":0,"stats":{"Line":22}},{"line":69,"address":[],"length":0,"stats":{"Line":11}},{"line":75,"address":[],"length":0,"stats":{"Line":16}},{"line":76,"address":[],"length":0,"stats":{"Line":16}},{"line":80,"address":[],"length":0,"stats":{"Line":3}},{"line":81,"address":[],"length":0,"stats":{"Line":3}},{"line":85,"address":[],"length":0,"stats":{"Line":13}},{"line":86,"address":[],"length":0,"stats":{"Line":13}},{"line":88,"address":[],"length":0,"stats":{"Line":29}},{"line":93,"address":[],"length":0,"stats":{"Line":9}},{"line":94,"address":[],"length":0,"stats":{"Line":9}},{"line":96,"address":[],"length":0,"stats":{"Line":21}},{"line":101,"address":[],"length":0,"stats":{"Line":3}},{"line":102,"address":[],"length":0,"stats":{"Line":3}},{"line":104,"address":[],"length":0,"stats":{"Line":8}},{"line":109,"address":[],"length":0,"stats":{"Line":2}},{"line":110,"address":[],"length":0,"stats":{"Line":4}},{"line":111,"address":[],"length":0,"stats":{"Line":2}},{"line":112,"address":[],"length":0,"stats":{"Line":4}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":119,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":2}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":130,"address":[],"length":0,"stats":{"Line":7}},{"line":131,"address":[],"length":0,"stats":{"Line":7}},{"line":133,"address":[],"length":0,"stats":{"Line":20}},{"line":134,"address":[],"length":0,"stats":{"Line":18}},{"line":135,"address":[],"length":0,"stats":{"Line":4}},{"line":137,"address":[],"length":0,"stats":{"Line":14}},{"line":141,"address":[],"length":0,"stats":{"Line":3}},{"line":142,"address":[],"length":0,"stats":{"Line":3}},{"line":144,"address":[],"length":0,"stats":{"Line":10}},{"line":145,"address":[],"length":0,"stats":{"Line":6}},{"line":146,"address":[],"length":0,"stats":{"Line":4}},{"line":148,"address":[],"length":0,"stats":{"Line":6}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":9}},{"line":156,"address":[],"length":0,"stats":{"Line":4}},{"line":157,"address":[],"length":0,"stats":{"Line":5}},{"line":159,"address":[],"length":0,"stats":{"Line":4}},{"line":163,"address":[],"length":0,"stats":{"Line":1}},{"line":164,"address":[],"length":0,"stats":{"Line":1}},{"line":168,"address":[],"length":0,"stats":{"Line":0}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":173,"address":[],"length":0,"stats":{"Line":1}},{"line":174,"address":[],"length":0,"stats":{"Line":1}},{"line":183,"address":[],"length":0,"stats":{"Line":14}},{"line":184,"address":[],"length":0,"stats":{"Line":14}},{"line":185,"address":[],"length":0,"stats":{"Line":42}},{"line":194,"address":[],"length":0,"stats":{"Line":2}},{"line":195,"address":[],"length":0,"stats":{"Line":2}},{"line":203,"address":[],"length":0,"stats":{"Line":1}},{"line":204,"address":[],"length":0,"stats":{"Line":1}},{"line":205,"address":[],"length":0,"stats":{"Line":1}}],"covered":59,"coverable":61},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","frame.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::Frame` — a CPU pixel buffer plus an `OakVideoParams` handle.\n//!\n//! Mirrors `src/codec/src/frame.h`. The params are held as an oakcommon\n//! by-value handle (`bridge::common::OakVideoParams`, refcounted) so the\n//! byte-level ABI of `oakcodec_frame_get_params`/`_set_params` is\n//! unchanged; the pixel data itself is a plain `Vec\u003cu8\u003e`. Line-size and\n//! pixel-format math lives here.\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_free, oakcommon_videoparams_get_format,\n\toakcommon_videoparams_get_height, oakcommon_videoparams_get_is_valid,\n\toakcommon_videoparams_get_width, oakcommon_videoparams_init, OakVideoParams,\n};\nuse oakcore_rs::{PixelFormat, Rational};\n\n/// Number of channels in the internal RGBA pipeline layout\n/// (`VideoParams::k_internal_channel_count == k_rgba_channel_count == 4`).\n/// The frame math (linesize, per-pixel offsets) always assumes this layout,\n/// matching the C++ decoder path which produces/consumes RGBA frames.\nconst VIDEO_CHANNELS: i32 = 4;\n\n/// Interlacing of a frame's parameter set (VideoParams::Interlacing).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Interlacing {\n\t/// Progressive.\n\tNone = 0,\n\t/// Upper field first.\n\tTopFieldFirst = 1,\n\t/// Lower field first.\n\tBottomFieldFirst = 2,\n}\n\n/// `olive::Frame`: reference-counted CPU pixel buffer + params handle.\n#[derive(Debug)]\npub struct Frame {\n\t/// Video parameter set (oakcommon handle, refcounted).\n\tpub params: Option\u003cOakVideoParams\u003e,\n\t/// Pixel buffer (unallocated until `allocate`).\n\tdata: Vec\u003cu8\u003e,\n\t/// Distance between rows in bytes (0 until params are set).\n\tlinesize_bytes: i32,\n\t/// Timestamp, rational seconds.\n\ttimestamp: Rational,\n\t/// Allocated pixel format (may differ from params while converting).\n\tallocated_format: PixelFormat,\n}\n\n/// Map an `OakPixelFormat` int code back to a `PixelFormat` (unknown codes\n/// become [`PixelFormat::Invalid`]).\nfn pixel_format_from_i32(v: i32) -\u003e PixelFormat {\n\tmatch v {\n\t\t0 =\u003e PixelFormat::U8,\n\t\t1 =\u003e PixelFormat::U10,\n\t\t2 =\u003e PixelFormat::U16,\n\t\t3 =\u003e PixelFormat::F16,\n\t\t4 =\u003e PixelFormat::F32,\n\t\t_ =\u003e PixelFormat::Invalid,\n\t}\n}\n\n/// Bytes per pixel for `format` at `channels`, matching\n/// `VideoParams::get_bytes_per_pixel`:\n/// - U10 is a packed RGBA10A2 pixel: 4 bytes for the RGBA layout,\n/// regardless of channel count; anything else is rejected (0).\n/// - All other formats are `bytes_per_channel * channels`.\nfn bytes_per_pixel(format: PixelFormat, channels: i32) -\u003e i32 {\n\tif format == PixelFormat::U10 {\n\t\treturn if channels == VIDEO_CHANNELS { 4 } else { 0 };\n\t}\n\t(format.bytes_per_channel() as i32) * channels\n}\n\n/// Increment the refcount of a params handle (a no-op for test-stub handles\n/// whose `addref` is `None`). `pub(crate)` so the ffi layer can hand out\n/// addref'd copies (`oakcodec_frame_get_params`).\npub(crate) fn params_addref(p: \u0026OakVideoParams) {\n\tif let Some(addref) = p.addref {\n\t\t// SAFETY: `addref` is a valid C function pointer targeting `ctx`.\n\t\tunsafe { addref(p.ctx) };\n\t}\n}\n\n/// Release a params handle (prefers the `release` function pointer; the\n/// test stubs use `oakcommon_videoparams_free` instead). Nulls `ctx` so the\n/// handle cannot be released twice.\npub(crate) fn params_release(p: \u0026mut OakVideoParams) {\n\tif p.ctx.is_null() {\n\t\treturn;\n\t}\n\tif let Some(release) = p.release {\n\t\t// SAFETY: `release` is a valid C function pointer targeting `ctx`.\n\t\tunsafe { release(p.ctx) };\n\t} else {\n\t\t// SAFETY: `p` points at a live handle; `oakcommon_videoparams_free`\n\t\t// is a no-op for the null ctx we leave behind.\n\t\tunsafe { oakcommon_videoparams_free(p) };\n\t}\n\tp.ctx = std::ptr::null_mut();\n}\n\nimpl Frame {\n\t/// New frame with default (invalid) params; buffer unallocated.\n\tpub fn new() -\u003e Self {\n\t\tlet params = unsafe { oakcommon_videoparams_init() };\n\t\tFrame {\n\t\t\tparams: Some(params),\n\t\t\tdata: Vec::new(),\n\t\t\tlinesize_bytes: 0,\n\t\t\ttimestamp: Rational::new(0, 1),\n\t\t\tallocated_format: PixelFormat::Invalid,\n\t\t}\n\t}\n\n\t/// New frame with a copy of `params` (handle addref'd internally).\n\tpub fn with_params(params: OakVideoParams) -\u003e Self {\n\t\tparams_addref(\u0026params);\n\t\tlet mut frame = Frame {\n\t\t\tparams: Some(params),\n\t\t\tdata: Vec::new(),\n\t\t\tlinesize_bytes: 0,\n\t\t\ttimestamp: Rational::new(0, 1),\n\t\t\tallocated_format: PixelFormat::Invalid,\n\t\t};\n\t\tframe.recompute_linesize();\n\t\tframe\n\t}\n\n\t/// The video parameter set, or `None` when empty.\n\tpub fn params(\u0026self) -\u003e Option\u003c\u0026OakVideoParams\u003e {\n\t\tself.params.as_ref()\n\t}\n\n\t/// Replace the parameter set (handle addref'd), recompute line sizes,\n\t/// do NOT reallocate the buffer.\n\tpub fn set_params(\u0026mut self, params: OakVideoParams) {\n\t\tif let Some(mut old) = self.params.take() {\n\t\t\tparams_release(\u0026mut old);\n\t\t}\n\t\tparams_addref(\u0026params);\n\t\tself.params = Some(params);\n\t\tself.recompute_linesize();\n\t\t// Deliberately do not touch `data`: an existing buffer keeps its\n\t\t// layout; `allocated_format` stays at the old format until the next\n\t\t// `allocate()`.\n\t}\n\n\t/// Recompute `linesize_bytes` from the current params (0 when unset).\n\tfn recompute_linesize(\u0026mut self) {\n\t\tself.linesize_bytes = match \u0026self.params {\n\t\t\tSome(p) =\u003e {\n\t\t\t\tlet w = unsafe { oakcommon_videoparams_get_width(p.clone()) };\n\t\t\t\tlet fmt = pixel_format_from_i32(unsafe {\n\t\t\t\t\toakcommon_videoparams_get_format(p.clone())\n\t\t\t\t});\n\t\t\t\tSelf::generate_linesize_bytes(fmt, w)\n\t\t\t}\n\t\t\tNone =\u003e 0,\n\t\t};\n\t}\n\n\t/// Allocate the pixel buffer from the current params.\n\tpub fn allocate(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tlet params = match \u0026self.params {\n\t\t\tSome(p) =\u003e p.clone(),\n\t\t\tNone =\u003e return Err(crate::error::Error::State),\n\t\t};\n\n\t\tlet is_valid = unsafe { oakcommon_videoparams_get_is_valid(params.clone()) };\n\t\tif is_valid == 0 {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\n\t\tif self.is_allocated() {\n\t\t\t// Already allocated; leave the buffer alone.\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet width = unsafe { oakcommon_videoparams_get_width(params.clone()) };\n\t\tlet height = unsafe { oakcommon_videoparams_get_height(params.clone()) };\n\t\tlet format = pixel_format_from_i32(unsafe {\n\t\t\toakcommon_videoparams_get_format(params)\n\t\t});\n\n\t\tlet linesize = Self::generate_linesize_bytes(format, width);\n\t\tlet size = (linesize as usize).wrapping_mul(height as usize);\n\n\t\tself.data.resize(size, 0);\n\t\tself.linesize_bytes = linesize;\n\t\tself.allocated_format = format;\n\n\t\tOk(())\n\t}\n\n\t/// 1 when the pixel buffer is allocated.\n\tpub fn is_allocated(\u0026self) -\u003e bool {\n\t\t!self.data.is_empty()\n\t}\n\n\t/// Writable pixel buffer slice, or `None` when unallocated.\n\tpub fn data(\u0026self) -\u003e Option\u003c\u0026[u8]\u003e {\n\t\tif self.is_allocated() {\n\t\t\tSome(\u0026self.data)\n\t\t} else {\n\t\t\tNone\n\t\t}\n\t}\n\n\t/// Mutable pixel buffer slice, or `None` when unallocated.\n\tpub fn data_mut(\u0026mut self) -\u003e Option\u003c\u0026mut [u8]\u003e {\n\t\tif self.is_allocated() {\n\t\t\tSome(\u0026mut self.data)\n\t\t} else {\n\t\t\tNone\n\t\t}\n\t}\n\n\t/// Size of the pixel buffer in bytes (0 when unallocated).\n\tpub fn allocated_size(\u0026self) -\u003e usize {\n\t\tself.data.len()\n\t}\n\n\t/// Distance between two rows in bytes (0 when params are unset).\n\tpub fn linesize_bytes(\u0026self) -\u003e i32 {\n\t\tself.linesize_bytes\n\t}\n\n\t/// Distance between two rows in pixels.\n\tpub fn linesize_pixels(\u0026self) -\u003e i32 {\n\t\tlet bpp = self.bytes_per_pixel();\n\t\tif bpp \u003e 0 {\n\t\t\tself.linesize_bytes / bpp\n\t\t} else {\n\t\t\t0\n\t\t}\n\t}\n\n\t/// Bytes per pixel for the current params format (RGBA layout).\n\tfn bytes_per_pixel(\u0026self) -\u003e i32 {\n\t\tbytes_per_pixel(self.format(), VIDEO_CHANNELS)\n\t}\n\n\t/// Frame width in pixels (0 when params are empty).\n\tpub fn width(\u0026self) -\u003e i32 {\n\t\tmatch \u0026self.params {\n\t\t\tSome(p) =\u003e unsafe { oakcommon_videoparams_get_width(p.clone()) },\n\t\t\tNone =\u003e 0,\n\t\t}\n\t}\n\n\t/// Frame height in pixels (0 when params are empty).\n\tpub fn height(\u0026self) -\u003e i32 {\n\t\tmatch \u0026self.params {\n\t\t\tSome(p) =\u003e unsafe { oakcommon_videoparams_get_height(p.clone()) },\n\t\t\tNone =\u003e 0,\n\t\t}\n\t}\n\n\t/// Pixel format (`OakPixelFormat` value).\n\tpub fn format(\u0026self) -\u003e PixelFormat {\n\t\tmatch \u0026self.params {\n\t\t\tSome(p) =\u003e {\n\t\t\t\tpixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) })\n\t\t\t}\n\t\t\tNone =\u003e PixelFormat::Invalid,\n\t\t}\n\t}\n\n\t/// Plane channel count of the params format.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/frame.h` reads this from the params handle via\n\t/// `oakcommon_videoparams_get_channel_count`, which is not exposed in the\n\t/// Rust bridge. Decoder frames are always produced in the internal RGBA\n\t/// layout, so this returns [`VIDEO_CHANNELS`] (4).\n\tpub fn channel_count(\u0026self) -\u003e i32 {\n\t\tVIDEO_CHANNELS\n\t}\n\n\t/// Timestamp as a rational number of seconds.\n\tpub fn timestamp(\u0026self) -\u003e Rational {\n\t\tself.timestamp\n\t}\n\n\t/// Set the timestamp.\n\tpub fn set_timestamp(\u0026mut self, ts: Rational) {\n\t\tself.timestamp = ts;\n\t}\n\n\t/// Distance between rows for a (format, width) pair, in bytes.\n\t///\n\t/// Matches `Frame::generate_linesize_bytes(width, format, channel_count)`\n\t/// in `src/codec/src/frame.cpp` with `channel_count` fixed at\n\t/// [`VIDEO_CHANNELS`]: bytes per pixel times the width rounded up to a\n\t/// 32-byte boundary. Uses wrapping arithmetic so extreme (or negative)\n\t/// widths behave like the C++ `int` math rather than panicking.\n\tpub fn generate_linesize_bytes(format: PixelFormat, width: i32) -\u003e i32 {\n\t\tlet bpp = bytes_per_pixel(format, VIDEO_CHANNELS);\n\t\tlet aligned = width.wrapping_add(31) \u0026 !31;\n\t\tbpp.wrapping_mul(aligned)\n\t}\n\n\t/// Convert the buffer to another pixel format (`convert_to_olive_format`).\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/frame.cpp` — the destination params are carried by\n\t/// the C++ callers via `oakcommon_videoparams_*`; Rust keeps the\n\t/// equivalent state in `self.params`.\n\t///\n\t/// When the current params format already matches the format the buffer\n\t/// was allocated in, this is a no-op (`Ok`). A genuine pixel-format\n\t/// conversion requires the OIIO bridge (`convert_to_olive_format`), which\n\t/// is not yet ported to the pure-Rust crate; until then a mismatched\n\t/// conversion is rejected with [`crate::error::Error::State`].\n\tpub fn convert(\u0026mut self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tif !self.is_allocated() {\n\t\t\treturn Err(crate::error::Error::State);\n\t\t}\n\t\tlet fmt = self.format();\n\t\tif self.allocated_format == fmt {\n\t\t\tself.recompute_linesize();\n\t\t\treturn Ok(());\n\t\t}\n\t\tErr(crate::error::Error::State)\n\t}\n\n\t/// True when `(x, y)` lies inside the allocated buffer.\n\tfn contains_pixel(\u0026self, x: i32, y: i32) -\u003e bool {\n\t\tself.is_allocated() \u0026\u0026 x \u003e= 0 \u0026\u0026 x \u003c self.width() \u0026\u0026 y \u003e= 0 \u0026\u0026 y \u003c self.height()\n\t}\n\n\t/// Read a pixel sample at (x, y).\n\t///\n\t/// Returns the first byte of the pixel at `(x, y)` (the R channel for\n\t/// RGBA). Out-of-bounds reads return 0, matching the C++ default\n\t/// (transparent black) color.\n\tpub fn get_pixel(\u0026self, x: i32, y: i32) -\u003e u8 {\n\t\tif !self.contains_pixel(x, y) {\n\t\t\treturn 0;\n\t\t}\n\t\tlet offset = (y as usize).wrapping_mul(self.linesize_bytes as usize)\n\t\t\t+ (x as usize).wrapping_mul(self.bytes_per_pixel() as usize);\n\t\t*self.data.get(offset).unwrap_or(\u00260)\n\t}\n\n\t/// Write a pixel sample at (x, y).\n\t///\n\t/// Writes `value` to the first byte of the pixel at `(x, y)`. Out-of-bounds\n\t/// writes are ignored, matching the C++ `set_pixel`.\n\tpub fn set_pixel(\u0026mut self, x: i32, y: i32, value: u8) {\n\t\tif !self.contains_pixel(x, y) {\n\t\t\treturn;\n\t\t}\n\t\tlet offset = (y as usize).wrapping_mul(self.linesize_bytes as usize)\n\t\t\t+ (x as usize).wrapping_mul(self.bytes_per_pixel() as usize);\n\t\tif let Some(byte) = self.data.get_mut(offset) {\n\t\t\t*byte = value;\n\t\t}\n\t}\n}\n\nimpl Drop for Frame {\n\t/// Release the owned params handle when the last reference dies,\n\t/// mirroring the C++ `Frame::~Frame`.\n\tfn drop(\u0026mut self) {\n\t\tif let Some(mut p) = self.params.take() {\n\t\t\tparams_release(\u0026mut p);\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\n\tfn frame(w: i32, h: i32) -\u003e Frame {\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(w, h) };\n\t\tFrame::with_params(params)\n\t}\n\n\t#[test]\n\tfn linesize_is_32_byte_aligned_for_u8() {\n\t\t// U8 RGBA: 4 bytes/pixel, width rounded up to a 32-byte boundary.\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 9), 4 * 32);\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 100), 4 * 128);\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 0), 0);\n\t}\n\n\t#[test]\n\tfn linesize_respects_16bit_and_u10() {\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U16, 16), 8 * 32);\n\t\t// U10 is a packed 4-byte RGBA pixel regardless of channel count.\n\t\tassert_eq!(Frame::generate_linesize_bytes(PixelFormat::U10, 16), 4 * 32);\n\t}\n\n\t#[test]\n\tfn allocate_fills_buffer_and_reports_size() {\n\t\tlet mut f = frame(100, 50);\n\t\tassert!(!f.is_allocated());\n\t\tassert_eq!(f.allocated_size(), 0);\n\t\tassert!(f.data().is_none());\n\n\t\tf.allocate().unwrap();\n\t\tassert!(f.is_allocated());\n\t\tassert_eq!(f.allocated_size(), (4 * 128) * 50);\n\t\tassert_eq!(f.data().map(|d| d.len()), Some((4 * 128) * 50));\n\t\tassert_eq!(f.linesize_bytes(), 4 * 128);\n\t}\n\n\t#[test]\n\tfn allocate_invalid_params_is_error() {\n\t\t// init_basic(0, 0) is not valid -\u003e allocate must reject.\n\t\tlet mut f = frame(0, 0);\n\t\tassert!(f.allocate().is_err());\n\t}\n\n\t#[test]\n\tfn get_set_pixel_round_trip() {\n\t\tlet mut f = frame(100, 50);\n\t\tf.allocate().unwrap();\n\n\t\tf.set_pixel(3, 4, 0xAB);\n\t\tassert_eq!(f.get_pixel(3, 4), 0xAB);\n\n\t\t// pixel (0,0) is the first byte; pixel (1,0) is bpp bytes later.\n\t\tf.set_pixel(0, 0, 0x11);\n\t\tf.set_pixel(1, 0, 0x22);\n\t\tassert_eq!(f.get_pixel(0, 0), 0x11);\n\t\tassert_eq!(f.get_pixel(1, 0), 0x22);\n\t}\n\n\t#[test]\n\tfn out_of_bounds_reads_zero_and_writes_ignored() {\n\t\tlet mut f = frame(10, 10);\n\t\tf.allocate().unwrap();\n\t\tassert_eq!(f.get_pixel(50, 50), 0);\n\t\tassert_eq!(f.get_pixel(-1, 0), 0);\n\t\tf.set_pixel(50, 50, 0xFF);\n\t\t// untouched\n\t\tassert_eq!(f.data().unwrap()[0], 0);\n\t}\n\n\t#[test]\n\tfn set_params_recomputes_linesize_without_realloc() {\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(10, 10) };\n\t\tlet mut f = Frame::with_params(params);\n\t\tf.allocate().unwrap();\n\t\tlet before = f.allocated_size();\n\n\t\tlet wider = unsafe { oakcommon_videoparams_init_basic(100, 10) };\n\t\tf.set_params(wider);\n\t\t// linesize reflects the new width, but the buffer is untouched.\n\t\tassert_eq!(f.linesize_bytes(), 4 * 128);\n\t\tassert_eq!(f.allocated_size(), before);\n\t}\n}\n\n#[cfg(test)]\nmod tests_extra {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\n\tfn frame(w: i32, h: i32) -\u003e Frame {\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(w, h) };\n\t\tFrame::with_params(params)\n\t}\n\n\t#[test]\n\tfn linesize_pixels_derives_from_bytes() {\n\t\t// U8 RGBA: bpp 4 -\u003e linesize_pixels = linesize_bytes / 4.\n\t\tlet mut f = frame(32, 16);\n\t\tf.allocate().unwrap();\n\t\tassert_eq!(f.linesize_bytes(), 4 * 32);\n\t\tassert_eq!(f.linesize_pixels(), 32);\n\t\t// Unallocated / unset params -\u003e 0.\n\t\tlet g = Frame::new();\n\t\tassert_eq!(g.linesize_pixels(), 0);\n\t}\n\n\t#[test]\n\tfn channel_count_is_internal_rgba_layout() {\n\t\tlet f = frame(4, 4);\n\t\tassert_eq!(f.channel_count(), VIDEO_CHANNELS);\n\t}\n\n\t#[test]\n\tfn convert_is_noop_when_format_matches() {\n\t\tlet mut f = frame(16, 16);\n\t\tf.allocate().unwrap();\n\t\t// allocated U8 == params U8 -\u003e no-op Ok.\n\t\tassert!(f.convert().is_ok());\n\t\t// Unallocated -\u003e Err(State).\n\t\tlet mut g = Frame::new();\n\t\tassert!(g.convert().is_err());\n\t}\n\n\t#[test]\n\tfn pixel_format_from_unknown_code_is_invalid() {\n\t\tlet p = unsafe { oakcommon_videoparams_init_basic(1, 1) };\n\t\tunsafe { crate::bridge::common::oakcommon_videoparams_set_format(p.clone(), 99) };\n\t\tlet f = Frame::with_params(p);\n\t\tassert_eq!(f.format(), PixelFormat::Invalid);\n\t}\n\n\t#[test]\n\tfn default_timestamp_is_zero() {\n\t\tlet f = Frame::new();\n\t\tlet ts = f.timestamp();\n\t\tassert_eq!(ts.numerator(), 0);\n\t\tassert_eq!(ts.denominator(), 1);\n\t}\n}\n","traces":[{"line":67,"address":[],"length":0,"stats":{"Line":85}},{"line":68,"address":[],"length":0,"stats":{"Line":85}},{"line":69,"address":[],"length":0,"stats":{"Line":55}},{"line":70,"address":[],"length":0,"stats":{"Line":0}},{"line":71,"address":[],"length":0,"stats":{"Line":0}},{"line":72,"address":[],"length":0,"stats":{"Line":0}},{"line":73,"address":[],"length":0,"stats":{"Line":28}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":81}},{"line":84,"address":[],"length":0,"stats":{"Line":81}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":80}},{"line":93,"address":[],"length":0,"stats":{"Line":40}},{"line":94,"address":[],"length":0,"stats":{"Line":40}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":103,"address":[],"length":0,"stats":{"Line":45}},{"line":104,"address":[],"length":0,"stats":{"Line":90}},{"line":105,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":45}},{"line":109,"address":[],"length":0,"stats":{"Line":0}},{"line":113,"address":[],"length":0,"stats":{"Line":45}},{"line":115,"address":[],"length":0,"stats":{"Line":45}},{"line":120,"address":[],"length":0,"stats":{"Line":6}},{"line":121,"address":[],"length":0,"stats":{"Line":12}},{"line":123,"address":[],"length":0,"stats":{"Line":12}},{"line":124,"address":[],"length":0,"stats":{"Line":12}},{"line":126,"address":[],"length":0,"stats":{"Line":6}},{"line":132,"address":[],"length":0,"stats":{"Line":37}},{"line":133,"address":[],"length":0,"stats":{"Line":74}},{"line":135,"address":[],"length":0,"stats":{"Line":74}},{"line":136,"address":[],"length":0,"stats":{"Line":74}},{"line":138,"address":[],"length":0,"stats":{"Line":37}},{"line":141,"address":[],"length":0,"stats":{"Line":74}},{"line":142,"address":[],"length":0,"stats":{"Line":37}},{"line":146,"address":[],"length":0,"stats":{"Line":5}},{"line":147,"address":[],"length":0,"stats":{"Line":10}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":153,"address":[],"length":0,"stats":{"Line":6}},{"line":154,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":4}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":158,"address":[],"length":0,"stats":{"Line":4}},{"line":165,"address":[],"length":0,"stats":{"Line":40}},{"line":166,"address":[],"length":0,"stats":{"Line":40}},{"line":167,"address":[],"length":0,"stats":{"Line":40}},{"line":168,"address":[],"length":0,"stats":{"Line":160}},{"line":169,"address":[],"length":0,"stats":{"Line":80}},{"line":170,"address":[],"length":0,"stats":{"Line":120}},{"line":172,"address":[],"length":0,"stats":{"Line":120}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"address":[],"length":0,"stats":{"Line":29}},{"line":180,"address":[],"length":0,"stats":{"Line":58}},{"line":181,"address":[],"length":0,"stats":{"Line":87}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":116}},{"line":186,"address":[],"length":0,"stats":{"Line":29}},{"line":187,"address":[],"length":0,"stats":{"Line":3}},{"line":190,"address":[],"length":0,"stats":{"Line":52}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":195,"address":[],"length":0,"stats":{"Line":104}},{"line":196,"address":[],"length":0,"stats":{"Line":104}},{"line":197,"address":[],"length":0,"stats":{"Line":52}},{"line":198,"address":[],"length":0,"stats":{"Line":52}},{"line":201,"address":[],"length":0,"stats":{"Line":104}},{"line":202,"address":[],"length":0,"stats":{"Line":104}},{"line":204,"address":[],"length":0,"stats":{"Line":78}},{"line":205,"address":[],"length":0,"stats":{"Line":26}},{"line":206,"address":[],"length":0,"stats":{"Line":26}},{"line":208,"address":[],"length":0,"stats":{"Line":26}},{"line":212,"address":[],"length":0,"stats":{"Line":87}},{"line":213,"address":[],"length":0,"stats":{"Line":87}},{"line":217,"address":[],"length":0,"stats":{"Line":23}},{"line":218,"address":[],"length":0,"stats":{"Line":46}},{"line":219,"address":[],"length":0,"stats":{"Line":21}},{"line":221,"address":[],"length":0,"stats":{"Line":2}},{"line":226,"address":[],"length":0,"stats":{"Line":20}},{"line":227,"address":[],"length":0,"stats":{"Line":40}},{"line":228,"address":[],"length":0,"stats":{"Line":18}},{"line":230,"address":[],"length":0,"stats":{"Line":2}},{"line":235,"address":[],"length":0,"stats":{"Line":12}},{"line":236,"address":[],"length":0,"stats":{"Line":24}},{"line":240,"address":[],"length":0,"stats":{"Line":32}},{"line":241,"address":[],"length":0,"stats":{"Line":32}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":12}},{"line":247,"address":[],"length":0,"stats":{"Line":4}},{"line":248,"address":[],"length":0,"stats":{"Line":4}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":255,"address":[],"length":0,"stats":{"Line":10}},{"line":256,"address":[],"length":0,"stats":{"Line":30}},{"line":260,"address":[],"length":0,"stats":{"Line":29}},{"line":261,"address":[],"length":0,"stats":{"Line":29}},{"line":262,"address":[],"length":0,"stats":{"Line":116}},{"line":263,"address":[],"length":0,"stats":{"Line":0}},{"line":268,"address":[],"length":0,"stats":{"Line":27}},{"line":269,"address":[],"length":0,"stats":{"Line":27}},{"line":270,"address":[],"length":0,"stats":{"Line":108}},{"line":271,"address":[],"length":0,"stats":{"Line":0}},{"line":276,"address":[],"length":0,"stats":{"Line":19}},{"line":277,"address":[],"length":0,"stats":{"Line":19}},{"line":278,"address":[],"length":0,"stats":{"Line":19}},{"line":279,"address":[],"length":0,"stats":{"Line":76}},{"line":281,"address":[],"length":0,"stats":{"Line":0}},{"line":292,"address":[],"length":0,"stats":{"Line":3}},{"line":293,"address":[],"length":0,"stats":{"Line":3}},{"line":297,"address":[],"length":0,"stats":{"Line":17}},{"line":298,"address":[],"length":0,"stats":{"Line":17}},{"line":302,"address":[],"length":0,"stats":{"Line":18}},{"line":303,"address":[],"length":0,"stats":{"Line":18}},{"line":313,"address":[],"length":0,"stats":{"Line":71}},{"line":314,"address":[],"length":0,"stats":{"Line":213}},{"line":315,"address":[],"length":0,"stats":{"Line":213}},{"line":316,"address":[],"length":0,"stats":{"Line":213}},{"line":331,"address":[],"length":0,"stats":{"Line":2}},{"line":332,"address":[],"length":0,"stats":{"Line":2}},{"line":333,"address":[],"length":0,"stats":{"Line":1}},{"line":335,"address":[],"length":0,"stats":{"Line":3}},{"line":336,"address":[],"length":0,"stats":{"Line":1}},{"line":337,"address":[],"length":0,"stats":{"Line":2}},{"line":338,"address":[],"length":0,"stats":{"Line":1}},{"line":340,"address":[],"length":0,"stats":{"Line":0}},{"line":344,"address":[],"length":0,"stats":{"Line":9}},{"line":345,"address":[],"length":0,"stats":{"Line":61}},{"line":353,"address":[],"length":0,"stats":{"Line":5}},{"line":354,"address":[],"length":0,"stats":{"Line":15}},{"line":355,"address":[],"length":0,"stats":{"Line":2}},{"line":357,"address":[],"length":0,"stats":{"Line":15}},{"line":358,"address":[],"length":0,"stats":{"Line":9}},{"line":359,"address":[],"length":0,"stats":{"Line":9}},{"line":366,"address":[],"length":0,"stats":{"Line":4}},{"line":367,"address":[],"length":0,"stats":{"Line":12}},{"line":368,"address":[],"length":0,"stats":{"Line":1}},{"line":370,"address":[],"length":0,"stats":{"Line":15}},{"line":371,"address":[],"length":0,"stats":{"Line":9}},{"line":372,"address":[],"length":0,"stats":{"Line":12}},{"line":373,"address":[],"length":0,"stats":{"Line":3}},{"line":381,"address":[],"length":0,"stats":{"Line":43}},{"line":382,"address":[],"length":0,"stats":{"Line":129}},{"line":383,"address":[],"length":0,"stats":{"Line":43}}],"covered":125,"coverable":139},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","framemanager.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::FrameManager` — a pool of reusable [`crate::frame::Frame`]\n//! buffers plus a background garbage-collection thread.\n//!\n//! Mirrors `src/codec/src/framemanager.h`. The C++ manager kept a pool of\n//! `std::list\u003cFramePtr\u003e` and a QThread that periodically dropped frames\n//! whose last reference died. Rust keeps the same contract behind a\n//! `Mutex`; the background thread is replaced by a dedicated GC thread\n//! that drains the pool of freeable frames.\n\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::thread;\nuse std::time::Duration;\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_equals, OakVideoParams,\n};\nuse crate::frame::Frame;\n\n/// `olive::FrameManager`: singleton frame pool with background GC.\npub struct FrameManager {\n\t/// Pooled frames waiting for reuse (most-recently-freed first).\n\tpool: Mutex\u003cVec\u003cFrame\u003e\u003e,\n\t/// Peak number of live frames observed (diagnostics).\n\tpeak_count: Mutex\u003cusize\u003e,\n\t/// Current number of frames outstanding (not yet returned).\n\toutstanding: Mutex\u003cusize\u003e,\n}\n\nimpl FrameManager {\n\t/// The process-wide FrameManager singleton.\n\t///\n\t/// Constructs the manager on first use and spawns the background\n\t/// garbage-collection thread exactly once.\n\tpub fn instance() -\u003e \u0026'static FrameManager {\n\t\tstatic INSTANCE: OnceLock\u003cFrameManager\u003e = OnceLock::new();\n\t\tlet mgr = INSTANCE.get_or_init(FrameManager::new);\n\t\t// Spawn the GC thread on first construction only. We use a `static`\n\t\t// flag guarded by the same lock-free path: the first caller to build\n\t\t// the manager also starts the thread. Subsequent calls skip it.\n\t\tspawn_gc_thread_once(mgr);\n\t\tmgr\n\t}\n\n\t/// Create the empty manager.\n\tfn new() -\u003e Self {\n\t\tFrameManager {\n\t\t\tpool: Mutex::new(Vec::new()),\n\t\t\tpeak_count: Mutex::new(0),\n\t\t\toutstanding: Mutex::new(0),\n\t\t}\n\t}\n\n\t/// Clear the pool (dropping all cached frames).\n\tpub fn clear(\u0026self) {\n\t\tself.pool.lock().unwrap().clear();\n\t}\n\n\t/// Create a frame with the given params (borrowed from the pool when a\n\t/// compatible free frame exists, else freshly allocated).\n\tpub fn create_frame(\u0026self, params: OakVideoParams) -\u003e Arc\u003cFrame\u003e {\n\t\tlet frame = {\n\t\t\tlet mut pool = self.pool.lock().unwrap();\n\t\t\tmatch pool\n\t\t\t\t.iter()\n\t\t\t\t.position(|f| frame_matches(f, \u0026params))\n\t\t\t{\n\t\t\t\tSome(idx) =\u003e pool.swap_remove(idx),\n\t\t\t\tNone =\u003e Frame::with_params(params),\n\t\t\t}\n\t\t};\n\n\t\tlet mut outstanding = self.outstanding.lock().unwrap();\n\t\t*outstanding += 1;\n\t\tlet mut peak = self.peak_count.lock().unwrap();\n\t\tif *outstanding \u003e *peak {\n\t\t\t*peak = *outstanding;\n\t\t}\n\n\t\tArc::new(frame)\n\t}\n\n\t/// Return a frame to the pool for reuse.\n\tpub fn return_frame(\u0026self, frame: Frame) {\n\t\tlet mut outstanding = self.outstanding.lock().unwrap();\n\t\t*outstanding = outstanding.saturating_sub(1);\n\t\tself.pool.lock().unwrap().push(frame);\n\t}\n\n\t/// Number of frames currently outstanding (not in the pool).\n\tpub fn live_count(\u0026self) -\u003e usize {\n\t\t*self.outstanding.lock().unwrap()\n\t}\n\n\t/// Peak number of live frames observed.\n\tpub fn peak_count(\u0026self) -\u003e usize {\n\t\t*self.peak_count.lock().unwrap()\n\t}\n\n\t/// Background GC loop; runs on the manager's dedicated thread.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/framemanager.cpp` `run()` collected frames whose last\n\t/// reference had died, based on per-frame timestamps. The Rust skeleton\n\t/// keeps a pool of reusable buffers but no per-frame age, so the GC\n\t/// simply drains the whole pool. This bounds memory: frames are reused\n\t/// between GC passes and released once every GC period, which matches\n\t/// the C++ manager's intent of keeping pool memory from growing\n\t/// unbounded.\n\tfn gc_loop(\u0026self) {\n\t\tself.clear();\n\t}\n}\n\n/// Spawn the GC thread once for the process.\nfn spawn_gc_thread_once(mgr: \u0026'static FrameManager) {\n\tstatic STARTED: OnceLock\u003c()\u003e = OnceLock::new();\n\tSTARTED.get_or_init(|| {\n\t\tthread::spawn(move || {\n\t\t\t// `mgr` is `'static`; the thread may outlive every other\n\t\t\t// reference. Keep polling until the process exits.\n\t\t\tloop {\n\t\t\t\tthread::sleep(Duration::from_millis(5000));\n\t\t\t\tmgr.gc_loop();\n\t\t\t}\n\t\t});\n\t});\n}\n\n/// True when `frame` carries params equal to `params`.\nfn frame_matches(frame: \u0026Frame, params: \u0026OakVideoParams) -\u003e bool {\n\tlet Some(frame_params) = frame.params() else {\n\t\treturn false;\n\t};\n\tlet eq = unsafe {\n\t\toakcommon_videoparams_equals(frame_params.clone(), params.clone())\n\t};\n\teq != 0\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::bridge::common::oakcommon_videoparams_init_basic;\n\n\t#[test]\n\tfn create_and_return_tracks_counts() {\n\t\tlet mgr = FrameManager::new();\n\t\tassert_eq!(mgr.live_count(), 0);\n\t\tassert_eq!(mgr.peak_count(), 0);\n\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet frame = mgr.create_frame(params);\n\t\tassert_eq!(mgr.live_count(), 1);\n\t\tassert_eq!(mgr.peak_count(), 1);\n\n\t\t// Return by unwrapping the single strong reference.\n\t\tlet frame = Arc::try_unwrap(frame).unwrap();\n\t\tmgr.return_frame(frame);\n\t\tassert_eq!(mgr.live_count(), 0);\n\t\tassert_eq!(mgr.peak_count(), 1);\n\t}\n\n\t#[test]\n\tfn pool_reuses_compatible_frames() {\n\t\tlet mgr = FrameManager::new();\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet f1 = mgr.create_frame(params);\n\t\tmgr.return_frame(Arc::try_unwrap(f1).unwrap());\n\t\tassert_eq!(mgr.live_count(), 0);\n\n\t\t// A compatible request reuses the pooled buffer rather than\n\t\t// allocating a new one.\n\t\tlet f2 = mgr.create_frame(unsafe {\n\t\t\toakcommon_videoparams_init_basic(64, 64)\n\t\t});\n\t\tassert_eq!(mgr.live_count(), 1);\n\t\tassert_eq!(mgr.peak_count(), 1);\n\t\tArc::try_unwrap(f2).unwrap();\n\t}\n\n\t#[test]\n\tfn peak_count_tracks_maximum() {\n\t\tlet mgr = FrameManager::new();\n\t\tlet p1 = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet p2 = unsafe { oakcommon_videoparams_init_basic(128, 128) };\n\t\tlet a = mgr.create_frame(p1);\n\t\tlet b = mgr.create_frame(p2);\n\t\tassert_eq!(mgr.live_count(), 2);\n\t\tassert_eq!(mgr.peak_count(), 2);\n\t\tmgr.return_frame(Arc::try_unwrap(a).unwrap());\n\t\tassert_eq!(mgr.live_count(), 1);\n\t\tassert_eq!(mgr.peak_count(), 2);\n\t\tArc::try_unwrap(b).unwrap();\n\t}\n\n\t#[test]\n\tfn clear_drops_pooled_frames() {\n\t\tlet mgr = FrameManager::new();\n\t\tlet params = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\t\tlet f = mgr.create_frame(params);\n\t\tmgr.return_frame(Arc::try_unwrap(f).unwrap());\n\t\tassert_eq!(mgr.pool.lock().unwrap().len(), 1);\n\n\t\tmgr.clear();\n\t\tassert_eq!(mgr.pool.lock().unwrap().len(), 0);\n\t}\n}\n","traces":[{"line":50,"address":[],"length":0,"stats":{"Line":0}},{"line":52,"address":[],"length":0,"stats":{"Line":0}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":61,"address":[],"length":0,"stats":{"Line":4}},{"line":63,"address":[],"length":0,"stats":{"Line":12}},{"line":64,"address":[],"length":0,"stats":{"Line":4}},{"line":65,"address":[],"length":0,"stats":{"Line":4}},{"line":70,"address":[],"length":0,"stats":{"Line":1}},{"line":71,"address":[],"length":0,"stats":{"Line":2}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":6}},{"line":78,"address":[],"length":0,"stats":{"Line":24}},{"line":79,"address":[],"length":0,"stats":{"Line":6}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":9}},{"line":83,"address":[],"length":0,"stats":{"Line":3}},{"line":84,"address":[],"length":0,"stats":{"Line":10}},{"line":88,"address":[],"length":0,"stats":{"Line":24}},{"line":89,"address":[],"length":0,"stats":{"Line":6}},{"line":90,"address":[],"length":0,"stats":{"Line":24}},{"line":91,"address":[],"length":0,"stats":{"Line":11}},{"line":92,"address":[],"length":0,"stats":{"Line":5}},{"line":95,"address":[],"length":0,"stats":{"Line":12}},{"line":99,"address":[],"length":0,"stats":{"Line":4}},{"line":100,"address":[],"length":0,"stats":{"Line":16}},{"line":101,"address":[],"length":0,"stats":{"Line":8}},{"line":102,"address":[],"length":0,"stats":{"Line":12}},{"line":106,"address":[],"length":0,"stats":{"Line":7}},{"line":107,"address":[],"length":0,"stats":{"Line":14}},{"line":111,"address":[],"length":0,"stats":{"Line":6}},{"line":112,"address":[],"length":0,"stats":{"Line":12}},{"line":125,"address":[],"length":0,"stats":{"Line":0}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":133,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":2}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":5}},{"line":153,"address":[],"length":0,"stats":{"Line":1}}],"covered":32,"coverable":45},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","handle.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Refcounted-handle scaffolding. Same pattern as the oaknode/oakplugin\n//! crates (`src/node/rust/src/handle.rs`); intentionally duplicated rather\n//! than shared — each module DLL must run its own addref/release code\n//! (the function pointers in a handle always point into the DLL that\n//! created the object).\n\nuse std::panic::{catch_unwind, AssertUnwindSafe};\nuse std::ptr;\nuse std::sync::atomic::{AtomicI32, AtomicU32, Ordering};\n\nuse crate::error::{self, OAKCODEC_E_FAILED};\n\n/// Number of boxed handle objects currently alive (leak/debug checking).\n///\n/// Mirrors `oakcodec::g_alive_count` in `src/codec/c_api/frame.cpp`: every\n/// `make_owned` box increments it and `box_release` decrements it when the\n/// last reference drops. `oakcodec_debug_alive_count` reports it.\nstatic ALIVE: AtomicI32 = AtomicI32::new(0);\n\n/// ABI version stamped into every handle.\npub const OAKCODEC_ABI_VERSION: u32 = 1;\n\n/// Heap box behind a handle's `ctx`.\npub struct RefBox\u003cT: ?Sized\u003e {\n\t/// Atomic reference count.\n\tpub refs: AtomicU32,\n\t/// Boxed value.\n\tpub value: T,\n}\n\n/// `#[repr(C)]` mirror of the public handle structs\n/// (`{ctx, addref, release, abi_version}`).\n///\n/// Handles are `Copy`: passing one by value copies the struct, not the\n/// reference count — exactly the by-value convention the C ABI documents.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(C)]\npub struct CHandle {\n\t/// Opaque box pointer.\n\tpub ctx: *mut std::ffi::c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut std::ffi::c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut std::ffi::c_void)\u003e,\n\t/// ABI version.\n\tpub abi_version: u32,\n}\n\nimpl CHandle {\n\t/// The empty handle.\n\tpub fn null() -\u003e Self {\n\t\tCHandle {\n\t\t\tctx: ptr::null_mut(),\n\t\t\taddref: None,\n\t\t\trelease: None,\n\t\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t\t}\n\t}\n\n\t/// Whether this is an empty (null) handle.\n\tpub fn is_null(\u0026self) -\u003e bool {\n\t\tself.ctx.is_null()\n\t}\n}\n\n/// Increment the reference count of a boxed `RefBox\u003cT\u003e`.\n///\n/// # Safety\n/// `ptr` must point to a live `RefBox\u003cT\u003e` previously created by this module.\nunsafe extern \"C\" fn box_addref\u003cT: Send + 'static\u003e(ptr: *mut std::ffi::c_void) {\n\tif ptr.is_null() {\n\t\treturn;\n\t}\n\tlet boxed = unsafe { \u0026*(ptr as *const RefBox\u003cT\u003e) };\n\tboxed.refs.fetch_add(1, Ordering::SeqCst);\n}\n\n/// Decrement the reference count; destroys the box at zero.\n///\n/// # Safety\n/// `ptr` must point to a live `RefBox\u003cT\u003e` previously created by this module.\nunsafe extern \"C\" fn box_release\u003cT: Send + 'static\u003e(ptr: *mut std::ffi::c_void) {\n\tif ptr.is_null() {\n\t\treturn;\n\t}\n\tlet boxed = unsafe { \u0026*(ptr as *const RefBox\u003cT\u003e) };\n\tif boxed.refs.fetch_sub(1, Ordering::SeqCst) == 1 {\n\t\t// The last reference: the box is destroyed and the alive count\n\t\t// drops with it (mirrors `alive_dec` in c_api/frame.cpp).\n\t\tALIVE.fetch_sub(1, Ordering::SeqCst);\n\t\tunsafe { drop(Box::from_raw(ptr as *mut RefBox\u003cT\u003e)) };\n\t}\n}\n\n/// Owned handle with count 1; empty on allocation failure.\npub fn make_owned\u003cT: Send + 'static\u003e(value: T) -\u003e CHandle {\n\tlet boxed = Box::new(RefBox {\n\t\trefs: AtomicU32::new(1),\n\t\tvalue,\n\t});\n\tlet ctx = Box::into_raw(boxed) as *mut std::ffi::c_void;\n\t// Every boxed handle counts toward `oakcodec_debug_alive_count`\n\t// (mirrors `alive_inc` in c_api/frame.cpp).\n\tALIVE.fetch_add(1, Ordering::SeqCst);\n\tCHandle {\n\t\tctx,\n\t\taddref: Some(box_addref::\u003cT\u003e),\n\t\trelease: Some(box_release::\u003cT\u003e),\n\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t}\n}\n\n/// Borrowed handle for an object owned elsewhere.\n///\n/// Takes ownership of the boxed `T` already allocated at `ptr` (e.g. one\n/// passed in from C++). The resulting handle's release drops that box.\n///\n/// # Safety\n/// Caller guarantees `ptr` was allocated with `Box::new` and is not used\n/// after this call.\npub unsafe fn make_borrowed\u003cT: Send + 'static\u003e(ptr: *mut T) -\u003e CHandle {\n\tif ptr.is_null() {\n\t\treturn CHandle::null();\n\t}\n\t// Move ownership into a RefBox so addref/release and get() behave\n\t// uniformly with owned handles.\n\tlet value = unsafe { *Box::from_raw(ptr) };\n\tmake_owned(value)\n}\n\n/// Typed view into a handle; `None` for empty handles.\n///\n/// # Safety\n/// `T` must be the boxed type.\npub unsafe fn get\u003cT: 'static\u003e(h: \u0026CHandle) -\u003e Option\u003c\u0026T\u003e {\n\tif h.is_null() {\n\t\treturn None;\n\t}\n\tlet boxed = unsafe { \u0026*(h.ctx as *const RefBox\u003cT\u003e) };\n\tSome(\u0026boxed.value)\n}\n\n/// Panic-catching FFI wrapper for i32-returning exports.\npub fn guard\u003cF: FnOnce() -\u003e error::Result\u003c()\u003e\u003e(f: F) -\u003e i32 {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(Ok(())) =\u003e error::OAKCODEC_OK,\n\t\tOk(Err(e)) =\u003e e.code(),\n\t\tErr(_) =\u003e OAKCODEC_E_FAILED,\n\t}\n}\n\n/// Panic-catching FFI wrapper for handle-returning exports.\npub fn guard_handle\u003cF: FnOnce() -\u003e error::Result\u003cCHandle\u003e\u003e(f: F) -\u003e CHandle {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(Ok(h)) =\u003e h,\n\t\tOk(Err(_)) | Err(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// Panic-catching FFI wrapper for void exports.\npub fn guard_void\u003cF: FnOnce()\u003e(f: F) {\n\tlet _ = catch_unwind(AssertUnwindSafe(f));\n}\n\n/// Panic-catching FFI wrapper for exports that return a raw `i32` code\n/// directly (neither `Result` nor a handle). On panic, `OAKCODEC_E_FAILED`.\npub fn guard_raw\u003cF: FnOnce() -\u003e i32\u003e(f: F) -\u003e i32 {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(code) =\u003e code,\n\t\tErr(_) =\u003e OAKCODEC_E_FAILED,\n\t}\n}\n\n/// Panic-catching FFI wrapper for exports that return a raw `i64` directly\n/// (e.g. `oakcodec_decoder_get_image_sequence_index`). On panic,\n/// `OAKCODEC_E_FAILED`.\npub fn guard_i64\u003cF: FnOnce() -\u003e i64\u003e(f: F) -\u003e i64 {\n\tmatch catch_unwind(AssertUnwindSafe(f)) {\n\t\tOk(v) =\u003e v,\n\t\tErr(_) =\u003e OAKCODEC_E_FAILED as i64,\n\t}\n}\n\n/// Number of live boxed handle objects (see [`ALIVE`]).\npub fn alive_count() -\u003e i32 {\n\tALIVE.load(Ordering::SeqCst)\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::error::OAKCODEC_E_INVALID;\n\n\t#[test]\n\tfn make_owned_lifecycle_tracks_alive_count() {\n\t\t// The shared ffi test lock serializes the crate's `alive_count`\n\t\t// assertions against every other test that creates handles.\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = alive_count();\n\t\tlet h = make_owned(42u32);\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(alive_count(), before + 1);\n\n\t\t// addref/release cycle keeps the box alive.\n\t\tlet addref = h.addref.unwrap();\n\t\tlet release = h.release.unwrap();\n\t\t// SAFETY: `h.ctx` is a live RefBox\u003cu32\u003e.\n\t\tunsafe { addref(h.ctx) };\n\t\t// SAFETY: second reference released; box stays (refs 2 -\u003e 1).\n\t\tunsafe { release(h.ctx) };\n\t\tassert_eq!(alive_count(), before + 1);\n\n\t\t// Release the owned reference: box destroyed.\n\t\t// SAFETY: last reference.\n\t\tunsafe { release(h.ctx) };\n\t\tassert_eq!(alive_count(), before);\n\t}\n\n\t#[test]\n\tfn make_borrowed_null_is_null_handle() {\n\t\tlet h = unsafe { make_borrowed::\u003cu32\u003e(std::ptr::null_mut()) };\n\t\tassert!(h.is_null());\n\t}\n\n\t#[test]\n\tfn make_borrowed_takes_ownership() {\n\t\tlet _g = crate::ffi::lock_tests();\n\t\tlet before = alive_count();\n\t\tlet raw = Box::into_raw(Box::new(7u32));\n\t\tlet h = unsafe { make_borrowed(raw) };\n\t\tassert!(!h.is_null());\n\t\tassert_eq!(alive_count(), before + 1);\n\t\tassert_eq!(unsafe { *get::\u003cu32\u003e(\u0026h).unwrap() }, 7);\n\t\tunsafe { h.release.unwrap()(h.ctx) };\n\t\tassert_eq!(alive_count(), before);\n\t}\n\n\t#[test]\n\tfn addref_on_null_ctx_is_noop() {\n\t\t// A handle with function pointers but a null ctx: both thunks no-op.\n\t\tlet h = CHandle {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: Some(box_addref::\u003cu32\u003e),\n\t\t\trelease: Some(box_release::\u003cu32\u003e),\n\t\t\tabi_version: OAKCODEC_ABI_VERSION,\n\t\t};\n\t\t// SAFETY: ctx is null; the thunks guard on it.\n\t\tunsafe { h.addref.unwrap()(h.ctx) };\n\t\t// SAFETY: ctx is null; the thunks guard on it.\n\t\tunsafe { h.release.unwrap()(h.ctx) };\n\t}\n\n\t#[test]\n\tfn guard_maps_results_and_panics() {\n\t\tassert_eq!(guard(|| Ok(())), crate::error::OAKCODEC_OK);\n\t\tassert_eq!(guard(|| Err(crate::error::Error::Invalid)), OAKCODEC_E_INVALID);\n\t\tassert_eq!(guard(|| panic!(\"boom\")), crate::error::OAKCODEC_E_FAILED);\n\n\t\tlet ok = guard_handle(|| Ok(make_owned(1u32)));\n\t\tassert!(!ok.is_null());\n\t\tassert!(guard_handle(|| Err::\u003cCHandle, _\u003e(crate::error::Error::Invalid)).is_null());\n\t\tassert!(guard_handle(|| panic!(\"boom\")).is_null());\n\n\t\tassert_eq!(guard_raw(|| 5), 5);\n\t\tassert_eq!(guard_raw(|| panic!(\"boom\")), crate::error::OAKCODEC_E_FAILED);\n\n\t\tassert_eq!(guard_i64(|| 5), 5);\n\t\tassert_eq!(guard_i64(|| panic!(\"boom\")), crate::error::OAKCODEC_E_FAILED as i64);\n\n\t\tlet mut called = false;\n\t\tguard_void(|| called = true);\n\t\tassert!(called);\n\t\tguard_void(|| panic!(\"boom\"));\n\t}\n\n\t#[test]\n\tfn null_handle_helpers() {\n\t\tlet h = CHandle::null();\n\t\tassert!(h.is_null());\n\t\tassert_eq!(h.abi_version, OAKCODEC_ABI_VERSION);\n\t}\n}\n","traces":[{"line":67,"address":[],"length":0,"stats":{"Line":17}},{"line":69,"address":[],"length":0,"stats":{"Line":34}},{"line":77,"address":[],"length":0,"stats":{"Line":147}},{"line":78,"address":[],"length":0,"stats":{"Line":294}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":4}},{"line":88,"address":[],"length":0,"stats":{"Line":1}},{"line":90,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":3}},{"line":98,"address":[],"length":0,"stats":{"Line":26}},{"line":99,"address":[],"length":0,"stats":{"Line":52}},{"line":100,"address":[],"length":0,"stats":{"Line":1}},{"line":102,"address":[],"length":0,"stats":{"Line":50}},{"line":103,"address":[],"length":0,"stats":{"Line":74}},{"line":106,"address":[],"length":0,"stats":{"Line":96}},{"line":107,"address":[],"length":0,"stats":{"Line":48}},{"line":112,"address":[],"length":0,"stats":{"Line":25}},{"line":113,"address":[],"length":0,"stats":{"Line":75}},{"line":114,"address":[],"length":0,"stats":{"Line":25}},{"line":115,"address":[],"length":0,"stats":{"Line":25}},{"line":117,"address":[],"length":0,"stats":{"Line":50}},{"line":120,"address":[],"length":0,"stats":{"Line":75}},{"line":123,"address":[],"length":0,"stats":{"Line":25}},{"line":124,"address":[],"length":0,"stats":{"Line":25}},{"line":137,"address":[],"length":0,"stats":{"Line":2}},{"line":138,"address":[],"length":0,"stats":{"Line":4}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":144,"address":[],"length":0,"stats":{"Line":2}},{"line":151,"address":[],"length":0,"stats":{"Line":117}},{"line":152,"address":[],"length":0,"stats":{"Line":234}},{"line":153,"address":[],"length":0,"stats":{"Line":14}},{"line":155,"address":[],"length":0,"stats":{"Line":206}},{"line":156,"address":[],"length":0,"stats":{"Line":103}},{"line":160,"address":[],"length":0,"stats":{"Line":71}},{"line":161,"address":[],"length":0,"stats":{"Line":142}},{"line":162,"address":[],"length":0,"stats":{"Line":34}},{"line":163,"address":[],"length":0,"stats":{"Line":108}},{"line":164,"address":[],"length":0,"stats":{"Line":1}},{"line":169,"address":[],"length":0,"stats":{"Line":32}},{"line":170,"address":[],"length":0,"stats":{"Line":64}},{"line":171,"address":[],"length":0,"stats":{"Line":60}},{"line":172,"address":[],"length":0,"stats":{"Line":2}},{"line":177,"address":[],"length":0,"stats":{"Line":28}},{"line":178,"address":[],"length":0,"stats":{"Line":28}},{"line":183,"address":[],"length":0,"stats":{"Line":94}},{"line":184,"address":[],"length":0,"stats":{"Line":94}},{"line":185,"address":[],"length":0,"stats":{"Line":186}},{"line":186,"address":[],"length":0,"stats":{"Line":1}},{"line":193,"address":[],"length":0,"stats":{"Line":4}},{"line":194,"address":[],"length":0,"stats":{"Line":4}},{"line":195,"address":[],"length":0,"stats":{"Line":6}},{"line":196,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":25}},{"line":202,"address":[],"length":0,"stats":{"Line":75}}],"covered":55,"coverable":55},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","lib.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! # oakcodec — the media codec module (Rust)\n//!\n//! Reimplements the C++ oakcodec module behind its frozen C ABI\n//! (`include/codec/*.h`). See README.md for the architectural mapping\n//! (inheritance → traits, shared_ptr → refcounted handles, etc.).\n//!\n//! ## FFI discipline\n//!\n//! Identical to the oaknode/oakplugin crates: every export goes through\n//! [`handle::guard*`], handles are opaque refcounted boxes, shared\n//! state behind `Mutex`.\n\n#![deny(unsafe_op_in_unsafe_fn)]\n#![warn(missing_docs)]\n\npub mod bridge;\npub mod conformmanager;\npub mod decoder;\npub mod encoder;\npub mod encodingparams;\npub mod error;\npub mod exportcodec;\npub mod exportformat;\npub mod ffmpeg;\npub mod ffi;\npub mod footagedescription;\npub mod frame;\npub mod framemanager;\npub mod handle;\npub mod oiio;\npub mod oiioframebridge;\npub mod planarfiledevice;\npub mod proxymanager;\npub mod task;\npub mod timecodemetadata;\n\n#[cfg(test)]\nmod realmedia_tests;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","oiio.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `OIIODecoder` / `OIIOEncoder` — the OpenImageIO-backed implementations.\n//!\n//! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO\n//! frame conversion goes through the local\n//! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping\n//! functions (`oakcommon_oiioutils_*` via `bridge/common.rs`).\n//!\n//! The OIIO dylib (`liboakoiio`) is not linked into this build, so every\n//! operation that would touch the media engine is a documented stub returning\n//! [`crate::error::Error::Failed`]; only bookkeeping that keeps the\n//! decoder/encoder safe to use when closed succeeds. The stream is still\n//! recorded on [`Decoder::open`] so [`Decoder::stream`] reflects the target,\n//! and [`Decoder::close`] / [`Encoder::close`] are no-ops.\n\nuse crate::decoder::{CodecStream, Decoder, RetrieveVideoParams};\nuse crate::encoder::Encoder;\nuse crate::encodingparams::EncodingParams;\n\n/// `olive::OIIODecoder` — OpenImageIO-backed media decoder (still images).\npub struct OIIODecoder {\n\t/// Opened stream (locked).\n\tstream: std::sync::Mutex\u003cOption\u003cCodecStream\u003e\u003e,\n}\n\nimpl OIIODecoder {\n\t/// Error returned for operations that need the missing `liboakoiio`.\n\tconst NOT_AVAILABLE: \u0026'static str =\n\t\t\"OIIO decoding is not available in this build (needs liboakoiio dylib)\";\n}\n\nimpl Decoder for OIIODecoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"oiio\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn probe(\n\t\t\u0026self,\n\t\t_filename: \u0026str,\n\t\t_cancelled: Option\u003c\u0026crate::bridge::render::OakCancelAtom\u003e,\n\t) -\u003e Option\u003ccrate::footagedescription::FootageDescription\u003e {\n\t\t// Probing is a dylib operation; without it we cannot report anything.\n\t\tNone\n\t}\n\n\tfn open(\u0026self, stream: \u0026CodecStream) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// Record the requested stream so `stream()` reflects the open target,\n\t\t// but actual decoding is unavailable without the dylib.\n\t\t*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = Some(stream.clone());\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// Trait contract: \"safe when closed\". Clear the opened stream and\n\t\t// no-op; nothing was ever decoded.\n\t\t*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = None;\n\t\tOk(())\n\t}\n\n\tfn stream(\u0026self) -\u003e CodecStream {\n\t\tself.stream\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.clone()\n\t\t\t.unwrap_or_else(CodecStream::new)\n\t}\n\n\tfn retrieve_video_frame(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003cstd::sync::Arc\u003ccrate::frame::Frame\u003e\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn retrieve_video(\n\t\t\u0026self,\n\t\t_p: \u0026RetrieveVideoParams,\n\t) -\u003e crate::error::Result\u003ccrate::bridge::render::OakRenderTexture\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn retrieve_audio(\n\t\t\u0026self,\n\t\t_dest: \u0026mut [f32],\n\t\t_range: \u0026oakcore_rs::TimeRange,\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t) -\u003e crate::error::Result\u003ccrate::decoder::RetrieveAudioStatus\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n\n\tfn conform_audio(\n\t\t\u0026self,\n\t\t_output_filenames: \u0026[String],\n\t\t_sample_rate: i32,\n\t\t_channel_layout: u64,\n\t\t_sample_format: i32,\n\t\t_cancelled: Option\u003c\u0026crate::bridge::render::OakCancelAtom\u003e,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))\n\t}\n}\n\n/// `olive::OIIOEncoder` — OpenImageIO-backed media encoder (still images).\npub struct OIIOEncoder {\n\t/// The encoding parameters this encoder was configured with.\n\tpub params: EncodingParams,\n}\n\nimpl Encoder for OIIOEncoder {\n\tfn id(\u0026self) -\u003e String {\n\t\t\"oiio\".to_string()\n\t}\n\n\tfn supports_video(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_audio(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_subtitles(\u0026self) -\u003e bool {\n\t\tfalse\n\t}\n\n\tfn supports_image_sequences(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn is_configurable(\u0026self) -\u003e bool {\n\t\ttrue\n\t}\n\n\tfn configure(\u0026self, _params: \u0026EncodingParams) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// `configure` writes `self.params`, which an `\u0026self` receiver cannot\n\t\t// do, and real encoding needs the dylib anyway.\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn open(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn close(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\t// `Encoder::close` is documented idempotent; with no encoder opened\n\t\t// there is nothing to release.\n\t\tOk(())\n\t}\n\n\tfn write_video(\u0026self, _frame: \u0026crate::frame::Frame) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn write_audio(\u0026self, _samples: \u0026[f32], _frame_count: i32) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn write_subtitle(\n\t\t\u0026self,\n\t\t_text: \u0026str,\n\t\t_in_seconds: f64,\n\t\t_out_seconds: f64,\n\t) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn flush(\u0026self) -\u003e crate::error::Result\u003c()\u003e {\n\t\tErr(crate::error::Error::Failed(\n\t\t\t\"OIIO encoding is not available in this build (needs liboakoiio dylib)\".to_string(),\n\t\t))\n\t}\n\n\tfn desired_pixel_format(\u0026self) -\u003e Option\u003coakcore_rs::PixelFormat\u003e {\n\t\tNone\n\t}\n\n\tfn desired_sample_format(\u0026self) -\u003e Option\u003coakcore_rs::SampleFormat\u003e {\n\t\tNone\n\t}\n\n\tfn filename(\u0026self) -\u003e String {\n\t\tc_string_1024(\u0026self.params.filename)\n\t}\n}\n\n/// Read a NUL-terminated C string out of the `EncodingParams.filename` byte\n/// buffer, stopping at the first NUL (empty string when unset).\nfn c_string_1024(buf: \u0026[u8; 1024]) -\u003e String {\n\tlet end = buf.iter().position(|\u0026b| b == 0).unwrap_or(buf.len());\n\tString::from_utf8_lossy(\u0026buf[..end]).into_owned()\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\tuse crate::decoder::RetrieveVideoParams;\n\tuse crate::encodingparams::EncodingParams;\n\tuse oakcore_rs::{Rational, TimeRange};\n\n\tfn video_params() -\u003e RetrieveVideoParams {\n\t\tRetrieveVideoParams {\n\t\t\tstream: CodecStream::new(),\n\t\t\ttime: Rational::new(1, 30),\n\t\t\tlength: TimeRange::default(),\n\t\t\tforce_range: crate::decoder::K_COLOR_RANGE_DEFAULT,\n\t\t\tis_image_sequence: false,\n\t\t\timage_sequence_digits: 0,\n\t\t\timage_sequence_number: 0,\n\t\t\tmode: crate::decoder::RenderMode::Offline,\n\t\t\talpha_is_premultiplied: false,\n\t\t}\n\t}\n\n\t#[test]\n\tfn oiio_decoder_identity_and_stub_operations() {\n\t\tlet d = OIIODecoder {\n\t\t\tstream: std::sync::Mutex::new(None),\n\t\t};\n\t\tassert_eq!(d.id(), \"oiio\");\n\t\t// Still images only: no audio support.\n\t\tassert!(!d.supports_video());\n\t\tassert!(!d.supports_audio());\n\t\tassert!(d.probe(\"any.png\", None).is_none());\n\n\t\tlet s = CodecStream::with_block(\"in.exr\".to_string(), 0, None);\n\t\tassert!(d.open(\u0026s).is_err());\n\t\tassert_eq!(d.stream().filename(), \"in.exr\");\n\t\tassert!(d.close().is_ok());\n\t\tassert_eq!(d.stream().filename(), \"\");\n\n\t\tassert!(d.retrieve_video_frame(\u0026video_params()).is_err());\n\t\tassert!(d.retrieve_video(\u0026video_params()).is_err());\n\t\tlet mut dest = [0f32; 8];\n\t\tassert!(d\n\t\t\t.retrieve_audio(\n\t\t\t\t\u0026mut dest,\n\t\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t\t48000,\n\t\t\t\t0x3\n\t\t\t)\n\t\t\t.is_err());\n\t\tassert!(d\n\t\t\t.conform_audio(\u0026[\"a.pcm\".to_string()], 48000, 0x3, 10, None)\n\t\t\t.is_err());\n\t}\n\n\t#[test]\n\tfn oiio_encoder_stub_behaviors() {\n\t\tlet mut params = EncodingParams::default();\n\t\tlet name = b\"out/img.exr\";\n\t\tparams.filename[..name.len()].copy_from_slice(name);\n\t\tlet e = OIIOEncoder { params };\n\n\t\tassert_eq!(e.id(), \"oiio\");\n\t\tassert!(!e.supports_video());\n\t\tassert!(!e.supports_audio());\n\t\tassert!(!e.supports_subtitles());\n\t\tassert!(e.supports_image_sequences());\n\t\tassert!(e.is_configurable());\n\t\tassert_eq!(e.filename(), \"out/img.exr\");\n\t\tassert_eq!(e.desired_pixel_format(), None);\n\t\tassert_eq!(e.get_error(), \"\");\n\n\t\tassert!(e.configure(\u0026EncodingParams::default()).is_err());\n\t\tassert!(e.open().is_err());\n\t\tassert!(e.close().is_ok());\n\t\tlet frame = crate::frame::Frame::new();\n\t\tassert!(e.write_video(\u0026frame).is_err());\n\t\tassert!(e.write_audio(\u0026[0f32; 4], 1).is_err());\n\t\tassert!(e.write_subtitle(\"hi\", 0.0, 1.0).is_err());\n\t\tassert!(e.flush().is_err());\n\t}\n}\n","traces":[{"line":48,"address":[],"length":0,"stats":{"Line":1}},{"line":49,"address":[],"length":0,"stats":{"Line":2}},{"line":52,"address":[],"length":0,"stats":{"Line":1}},{"line":53,"address":[],"length":0,"stats":{"Line":1}},{"line":56,"address":[],"length":0,"stats":{"Line":1}},{"line":57,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":1}},{"line":66,"address":[],"length":0,"stats":{"Line":1}},{"line":69,"address":[],"length":0,"stats":{"Line":1}},{"line":72,"address":[],"length":0,"stats":{"Line":4}},{"line":73,"address":[],"length":0,"stats":{"Line":1}},{"line":76,"address":[],"length":0,"stats":{"Line":1}},{"line":79,"address":[],"length":0,"stats":{"Line":4}},{"line":80,"address":[],"length":0,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":2}},{"line":84,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":1}},{"line":95,"address":[],"length":0,"stats":{"Line":1}},{"line":98,"address":[],"length":0,"stats":{"Line":1}},{"line":102,"address":[],"length":0,"stats":{"Line":1}},{"line":105,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":134,"address":[],"length":0,"stats":{"Line":4}},{"line":135,"address":[],"length":0,"stats":{"Line":8}},{"line":138,"address":[],"length":0,"stats":{"Line":1}},{"line":139,"address":[],"length":0,"stats":{"Line":1}},{"line":142,"address":[],"length":0,"stats":{"Line":1}},{"line":143,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":150,"address":[],"length":0,"stats":{"Line":1}},{"line":151,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":1}},{"line":155,"address":[],"length":0,"stats":{"Line":1}},{"line":158,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":1}},{"line":162,"address":[],"length":0,"stats":{"Line":1}},{"line":166,"address":[],"length":0,"stats":{"Line":1}},{"line":167,"address":[],"length":0,"stats":{"Line":1}},{"line":168,"address":[],"length":0,"stats":{"Line":1}},{"line":172,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":178,"address":[],"length":0,"stats":{"Line":1}},{"line":179,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":1}},{"line":185,"address":[],"length":0,"stats":{"Line":1}},{"line":186,"address":[],"length":0,"stats":{"Line":1}},{"line":190,"address":[],"length":0,"stats":{"Line":1}},{"line":196,"address":[],"length":0,"stats":{"Line":1}},{"line":197,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":202,"address":[],"length":0,"stats":{"Line":1}},{"line":203,"address":[],"length":0,"stats":{"Line":1}},{"line":207,"address":[],"length":0,"stats":{"Line":1}},{"line":208,"address":[],"length":0,"stats":{"Line":1}},{"line":211,"address":[],"length":0,"stats":{"Line":0}},{"line":212,"address":[],"length":0,"stats":{"Line":0}},{"line":215,"address":[],"length":0,"stats":{"Line":1}},{"line":216,"address":[],"length":0,"stats":{"Line":2}},{"line":222,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":18}},{"line":224,"address":[],"length":0,"stats":{"Line":3}}],"covered":65,"coverable":67},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","oiioframebridge.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `oiio_frame_to_buffer` / `oiio_buffer_to_frame` — codec-internal OIIO\n//! frame \u003c-\u003e pixel-buffer conversion.\n//!\n//! Mirrors `src/codec/src/oiioframebridge.{h,cpp}`. These are internal C++\n//! functions that moved into codec from oakcommon (NOTES.md §oakcommon侧修复);\n//! oakcommon keeps its OIIO mapping functions; the frame conversion itself\n//! lives here.\n//!\n//! The C++ bridge copies pixels through the live OpenImageIO `ImageBuf`\n//! (`oiio_frame_to_buffer`/`oiio_buffer_to_frame`). OIIO is not linked into\n//! this build, so the port serializes the frame into a self-describing byte\n//! buffer instead. The layout is stable (documented in\n//! [`OiioBufferHeader`]); it carries the frame's geometry, pixel format,\n//! timestamp and time base alongside the raw pixel rows, so a buffer can be\n//! turned back into an equivalent [`Frame`] without any external state.\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base,\n\toakcommon_videoparams_set_format,\n};\nuse crate::frame::Frame;\nuse oakcore_rs::Rational;\n\n/// Fixed header size, in bytes, of an OIIO frame buffer.\n///\n/// The layout is a 64-byte little-endian header followed by the raw pixel\n/// data. `OiioBufferHeader::to_bytes` / `from_bytes` are the single writer /\n/// reader of this header, so the exact offsets only ever exist in one place.\nconst HEADER_LEN: usize = 64;\n\n/// Magic bytes identifying an OIIO frame buffer (`\"OFMB\"`).\nconst MAGIC: \u0026[u8; 4] = b\"OFMB\";\n\n/// Current serialization version.\nconst VERSION: u32 = 1;\n\n/// Decoded OIIO frame-buffer header.\n///\n/// # Byte layout (little-endian, `HEADER_LEN` = 64 bytes)\n///\n/// | Offset | Size | Field |\n/// |--------|------|-------|\n/// | 0 | 4 | Magic bytes `\"OFMB\"` |\n/// | 4 | 4 | Serialization `version` (`u32`, currently 1) |\n/// | 8 | 4 | `width` (`i32`) |\n/// | 12 | 4 | `height` (`i32`) |\n/// | 16 | 4 | `format` (`i32`, an `OakPixelFormat` value) |\n/// | 20 | 4 | `linesize_bytes` (`i32`, distance between pixel rows) |\n/// | 24 | 8 | `timestamp_num` (`i64`) |\n/// | 32 | 8 | `timestamp_den` (`i64`) |\n/// | 40 | 8 | `time_base_num` (`i64`) |\n/// | 48 | 8 | `time_base_den` (`i64`) |\n/// | 56 | 8 | `pixel_len` (`u64`, pixel-data length in bytes) |\n/// | 64 | … | raw pixel data (`pixel_len` bytes) |\n///\n/// `pixel_len` must equal `linesize_bytes * height`; the frame geometry in the\n/// header must match the params the frame is reconstructed with.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\nstruct OiioBufferHeader {\n\tversion: u32,\n\twidth: i32,\n\theight: i32,\n\tformat: i32,\n\tlinesize_bytes: i32,\n\ttimestamp_num: i64,\n\ttimestamp_den: i64,\n\ttime_base_num: i64,\n\ttime_base_den: i64,\n\tpixel_len: u64,\n}\n\nimpl OiioBufferHeader {\n\t/// Serialize the header into exactly `HEADER_LEN` bytes (little-endian).\n\tfn to_bytes(\u0026self) -\u003e [u8; HEADER_LEN] {\n\t\tlet mut b = [0u8; HEADER_LEN];\n\t\tb[0..4].copy_from_slice(MAGIC);\n\t\tb[4..8].copy_from_slice(\u0026self.version.to_le_bytes());\n\t\tb[8..12].copy_from_slice(\u0026self.width.to_le_bytes());\n\t\tb[12..16].copy_from_slice(\u0026self.height.to_le_bytes());\n\t\tb[16..20].copy_from_slice(\u0026self.format.to_le_bytes());\n\t\tb[20..24].copy_from_slice(\u0026self.linesize_bytes.to_le_bytes());\n\t\tb[24..32].copy_from_slice(\u0026self.timestamp_num.to_le_bytes());\n\t\tb[32..40].copy_from_slice(\u0026self.timestamp_den.to_le_bytes());\n\t\tb[40..48].copy_from_slice(\u0026self.time_base_num.to_le_bytes());\n\t\tb[48..56].copy_from_slice(\u0026self.time_base_den.to_le_bytes());\n\t\tb[56..64].copy_from_slice(\u0026self.pixel_len.to_le_bytes());\n\t\tb\n\t}\n\n\t/// Parse a header from the start of `bytes`, validating the magic and\n\t/// version and requiring at least `HEADER_LEN` bytes. Returns the decoded\n\t/// header and the number of bytes consumed (`HEADER_LEN`).\n\tfn from_bytes(bytes: \u0026[u8]) -\u003e crate::error::Result\u003c(Self, usize)\u003e {\n\t\tif bytes.len() \u003c HEADER_LEN {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tif \u0026bytes[0..4] != MAGIC {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());\n\t\tif version != VERSION {\n\t\t\treturn Err(crate::error::Error::Invalid);\n\t\t}\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion,\n\t\t\twidth: i32::from_le_bytes(bytes[8..12].try_into().unwrap()),\n\t\t\theight: i32::from_le_bytes(bytes[12..16].try_into().unwrap()),\n\t\t\tformat: i32::from_le_bytes(bytes[16..20].try_into().unwrap()),\n\t\t\tlinesize_bytes: i32::from_le_bytes(bytes[20..24].try_into().unwrap()),\n\t\t\ttimestamp_num: i64::from_le_bytes(bytes[24..32].try_into().unwrap()),\n\t\t\ttimestamp_den: i64::from_le_bytes(bytes[32..40].try_into().unwrap()),\n\t\t\ttime_base_num: i64::from_le_bytes(bytes[40..48].try_into().unwrap()),\n\t\t\ttime_base_den: i64::from_le_bytes(bytes[48..56].try_into().unwrap()),\n\t\t\tpixel_len: u64::from_le_bytes(bytes[56..64].try_into().unwrap()),\n\t\t};\n\t\tOk((header, HEADER_LEN))\n\t}\n}\n\n/// Convert an OIIO-backed `Frame` into a raw pixel buffer.\n///\n/// # CPP-PARITY\n/// `src/codec/src/oiioframebridge.cpp` `oiio_frame_to_buffer` —\n/// allocates the destination and copies the OIIO pixel data out.\n///\n/// The buffer is a 64-byte [`OiioBufferHeader`] followed by the frame's pixel\n/// rows (see the header docs for the exact layout). The frame must already be\n/// allocated ([`Frame::allocate`]); otherwise this returns\n/// [`crate::error::Error::State`].\npub fn oiio_frame_to_buffer(frame: \u0026Frame) -\u003e crate::error::Result\u003cVec\u003cu8\u003e\u003e {\n\tlet data = frame.data().ok_or(crate::error::Error::State)?;\n\tlet width = frame.width();\n\tlet height = frame.height();\n\tlet format = frame.format() as i32;\n\tlet linesize_bytes = frame.linesize_bytes();\n\tlet timestamp = frame.timestamp();\n\n\tlet (time_base_num, time_base_den) = match frame.params() {\n\t\tSome(p) =\u003e {\n\t\t\tlet mut num = 0i64;\n\t\t\tlet mut den = 0i64;\n\t\t\t// SAFETY: `num`/`den` are live mutable i64s and `p` is a valid\n\t\t\t// handle; the C function only writes through the two out pointers.\n\t\t\tunsafe {\n\t\t\t\toakcommon_videoparams_get_time_base(p.clone(), \u0026mut num, \u0026mut den);\n\t\t\t}\n\t\t\t(num, den)\n\t\t}\n\t\tNone =\u003e (0, 0),\n\t};\n\n\tlet header = OiioBufferHeader {\n\t\tversion: VERSION,\n\t\twidth,\n\t\theight,\n\t\tformat,\n\t\tlinesize_bytes,\n\t\ttimestamp_num: timestamp.numerator(),\n\t\ttimestamp_den: timestamp.denominator(),\n\t\ttime_base_num,\n\t\ttime_base_den,\n\t\tpixel_len: data.len() as u64,\n\t};\n\n\tlet mut out = Vec::with_capacity(HEADER_LEN + data.len());\n\tout.extend_from_slice(\u0026header.to_bytes());\n\tout.extend_from_slice(data);\n\tOk(out)\n}\n\n/// Convert a raw pixel buffer back into a `Frame`.\n///\n/// # CPP-PARITY\n/// `src/codec/src/oiioframebridge.cpp` `oiio_buffer_to_frame` —\n/// wraps the buffer bytes in a `Frame` for encoder consumption.\n///\n/// Parses a [`OiioBufferHeader`] from the front of `buffer`, reconstructs the\n/// frame's params (geometry, time base, pixel format) and timestamp, then\n/// copies the pixel rows into an allocated frame. The buffer is rejected with\n/// [`crate::error::Error::Invalid`] when the magic/version is wrong, the\n/// header is truncated, or `pixel_len` is inconsistent with the declared\n/// `linesize_bytes * height` / the bytes actually present.\npub fn oiio_buffer_to_frame(buffer: \u0026[u8]) -\u003e crate::error::Result\u003cFrame\u003e {\n\tlet (header, consumed) = OiioBufferHeader::from_bytes(buffer)?;\n\tlet pixels = \u0026buffer[consumed..];\n\n\t// `pixel_len` must be consistent with the header geometry and the bytes\n\t// actually present (checked, so a corrupt header can't panic later).\n\tif header.linesize_bytes \u003c 0 || header.height \u003c 0 {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\tlet expected =\n\t\t(header.linesize_bytes as u64).checked_mul(header.height as u64);\n\tif expected != Some(header.pixel_len) {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\tif pixels.len() != header.pixel_len as usize {\n\t\treturn Err(crate::error::Error::Invalid);\n\t}\n\n\t// Build the params from the header, then hand ownership to the frame.\n\t// SAFETY: the init returns a live handle; the clone for `set_format` is\n\t// only read, and the original is moved into `Frame::with_params` (which\n\t// takes ownership), so there is no double release.\n\tlet params = unsafe {\n\t\toakcommon_videoparams_init_with_time_base(\n\t\t\theader.width,\n\t\t\theader.height,\n\t\t\theader.time_base_num,\n\t\t\theader.time_base_den,\n\t\t)\n\t};\n\tunsafe {\n\t\toakcommon_videoparams_set_format(params.clone(), header.format);\n\t}\n\tlet mut frame = Frame::with_params(params);\n\tframe.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));\n\tframe.allocate()?;\n\n\tmatch frame.data_mut() {\n\t\tSome(dst) if dst.len() == pixels.len() =\u003e {\n\t\t\tdst.copy_from_slice(pixels);\n\t\t\tOk(frame)\n\t\t}\n\t\t// Reconstructed params should produce exactly `pixel_len` bytes; if\n\t\t// the geometry in the header disagreed with the format's line size,\n\t\t// refuse rather than copy a mismatched slice.\n\t\t_ =\u003e Err(crate::error::Error::Invalid),\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\t#[test]\n\tfn header_round_trip() {\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 100,\n\t\t\theight: 50,\n\t\t\tformat: 0, // U8\n\t\t\tlinesize_bytes: 512,\n\t\t\ttimestamp_num: 5,\n\t\t\ttimestamp_den: 2,\n\t\t\ttime_base_num: 1,\n\t\t\ttime_base_den: 30,\n\t\t\tpixel_len: 25600,\n\t\t};\n\t\tlet bytes = header.to_bytes();\n\t\tassert_eq!(bytes.len(), HEADER_LEN);\n\t\tlet (decoded, consumed) = OiioBufferHeader::from_bytes(\u0026bytes).unwrap();\n\t\tassert_eq!(consumed, HEADER_LEN);\n\t\tassert_eq!(decoded, header);\n\t}\n\n\t#[test]\n\tfn header_is_little_endian_magic_and_version() {\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t};\n\t\tlet bytes = header.to_bytes();\n\t\tassert_eq!(\u0026bytes[0..4], b\"OFMB\");\n\t\tassert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), 1);\n\t\t// width = 1 -\u003e little-endian 01 00 00 00.\n\t\tassert_eq!(\u0026bytes[8..12], \u0026[1, 0, 0, 0]);\n\t}\n\n\t#[test]\n\tfn from_bytes_rejects_truncated() {\n\t\tlet header = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t};\n\t\tlet bytes = header.to_bytes();\n\t\tassert!(OiioBufferHeader::from_bytes(\u0026bytes[..HEADER_LEN - 1]).is_err());\n\t}\n\n\t#[test]\n\tfn from_bytes_rejects_bad_magic_and_version() {\n\t\tlet mut bytes = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t}\n\t\t.to_bytes();\n\t\tbytes[0] = b'X';\n\t\tassert!(OiioBufferHeader::from_bytes(\u0026bytes).is_err());\n\n\t\tlet mut bytes = OiioBufferHeader {\n\t\t\tversion: VERSION,\n\t\t\twidth: 1,\n\t\t\theight: 1,\n\t\t\tformat: 0,\n\t\t\tlinesize_bytes: 4,\n\t\t\ttimestamp_num: 0,\n\t\t\ttimestamp_den: 1,\n\t\t\ttime_base_num: 0,\n\t\t\ttime_base_den: 0,\n\t\t\tpixel_len: 4,\n\t\t}\n\t\t.to_bytes();\n\t\tbytes[4..8].copy_from_slice(\u002699u32.to_le_bytes());\n\t\tassert!(OiioBufferHeader::from_bytes(\u0026bytes).is_err());\n\t}\n\n\t/// A small helper to build a fully allocated, filled frame using the same\n\t/// test-stub params pattern as `frame.rs`.\n\tfn make_frame() -\u003e Frame {\n\t\t// SAFETY: test-stub videoparams; ownership moves into `with_params`.\n\t\tlet params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30) };\n\t\tunsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8\n\t\tlet mut frame = Frame::with_params(params);\n\t\tframe.set_timestamp(Rational::new(5, 2));\n\t\tframe.allocate().unwrap();\n\t\tframe\n\t}\n\n\t#[test]\n\tfn frame_to_buffer_round_trip() {\n\t\tlet mut frame = make_frame();\n\t\tif let Some(d) = frame.data_mut() {\n\t\t\td.fill(0xAB);\n\t\t}\n\n\t\tlet buffer = oiio_frame_to_buffer(\u0026frame).unwrap();\n\t\tassert_eq!(buffer.len(), HEADER_LEN + frame.allocated_size());\n\n\t\tlet (header, consumed) = OiioBufferHeader::from_bytes(\u0026buffer).unwrap();\n\t\tassert_eq!(consumed, HEADER_LEN);\n\t\tassert_eq!(header.width, 100);\n\t\tassert_eq!(header.height, 50);\n\t\tassert_eq!(header.format, 0);\n\t\tassert_eq!(header.linesize_bytes, 512);\n\t\tassert_eq!(header.timestamp_num, 5);\n\t\tassert_eq!(header.timestamp_den, 2);\n\t\tassert_eq!(header.time_base_num, 1);\n\t\tassert_eq!(header.time_base_den, 30);\n\t\tassert_eq!(header.pixel_len, frame.allocated_size() as u64);\n\t}\n\n\t#[test]\n\tfn buffer_to_frame_round_trip() {\n\t\tlet mut frame = make_frame();\n\t\tlet expected_len = frame.allocated_size();\n\t\tif let Some(d) = frame.data_mut() {\n\t\t\tfor (i, b) in d.iter_mut().enumerate() {\n\t\t\t\t*b = (i % 256) as u8;\n\t\t\t}\n\t\t}\n\t\tlet buffer = oiio_frame_to_buffer(\u0026frame).unwrap();\n\n\t\tlet out = oiio_buffer_to_frame(\u0026buffer).unwrap();\n\t\tassert_eq!(out.width(), 100);\n\t\tassert_eq!(out.height(), 50);\n\t\tassert_eq!(out.format() as i32, 0);\n\t\tassert_eq!(out.allocated_size(), expected_len);\n\t\tassert_eq!(out.timestamp().numerator(), 5);\n\t\tassert_eq!(out.timestamp().denominator(), 2);\n\t\tassert_eq!(out.data().unwrap(), frame.data().unwrap());\n\t}\n\n\t#[test]\n\tfn buffer_to_frame_rejects_corrupt() {\n\t\tlet frame = make_frame();\n\t\tlet buffer = oiio_frame_to_buffer(\u0026frame).unwrap();\n\n\t\t// Truncated pixel data.\n\t\tassert!(oiio_buffer_to_frame(\u0026buffer[..HEADER_LEN + 1]).is_err());\n\t\t// Bad magic.\n\t\tlet mut bad = buffer.clone();\n\t\tbad[0] = 0;\n\t\tassert!(oiio_buffer_to_frame(\u0026bad).is_err());\n\t\t// pixel_len inconsistent with linesize * height.\n\t\tlet mut bad = buffer.clone();\n\t\tbad[56..64].copy_from_slice(\u0026(999u64).to_le_bytes());\n\t\tassert!(oiio_buffer_to_frame(\u0026bad).is_err());\n\t}\n}\n","traces":[{"line":90,"address":[],"length":0,"stats":{"Line":8}},{"line":91,"address":[],"length":0,"stats":{"Line":16}},{"line":92,"address":[],"length":0,"stats":{"Line":24}},{"line":93,"address":[],"length":0,"stats":{"Line":24}},{"line":94,"address":[],"length":0,"stats":{"Line":24}},{"line":95,"address":[],"length":0,"stats":{"Line":24}},{"line":96,"address":[],"length":0,"stats":{"Line":24}},{"line":97,"address":[],"length":0,"stats":{"Line":24}},{"line":98,"address":[],"length":0,"stats":{"Line":24}},{"line":99,"address":[],"length":0,"stats":{"Line":24}},{"line":100,"address":[],"length":0,"stats":{"Line":24}},{"line":101,"address":[],"length":0,"stats":{"Line":24}},{"line":102,"address":[],"length":0,"stats":{"Line":24}},{"line":103,"address":[],"length":0,"stats":{"Line":8}},{"line":109,"address":[],"length":0,"stats":{"Line":9}},{"line":110,"address":[],"length":0,"stats":{"Line":9}},{"line":111,"address":[],"length":0,"stats":{"Line":1}},{"line":113,"address":[],"length":0,"stats":{"Line":8}},{"line":114,"address":[],"length":0,"stats":{"Line":2}},{"line":116,"address":[],"length":0,"stats":{"Line":30}},{"line":117,"address":[],"length":0,"stats":{"Line":6}},{"line":118,"address":[],"length":0,"stats":{"Line":1}},{"line":122,"address":[],"length":0,"stats":{"Line":25}},{"line":123,"address":[],"length":0,"stats":{"Line":25}},{"line":124,"address":[],"length":0,"stats":{"Line":25}},{"line":125,"address":[],"length":0,"stats":{"Line":25}},{"line":126,"address":[],"length":0,"stats":{"Line":25}},{"line":127,"address":[],"length":0,"stats":{"Line":25}},{"line":128,"address":[],"length":0,"stats":{"Line":25}},{"line":129,"address":[],"length":0,"stats":{"Line":25}},{"line":130,"address":[],"length":0,"stats":{"Line":15}},{"line":132,"address":[],"length":0,"stats":{"Line":5}},{"line":146,"address":[],"length":0,"stats":{"Line":3}},{"line":147,"address":[],"length":0,"stats":{"Line":15}},{"line":148,"address":[],"length":0,"stats":{"Line":9}},{"line":149,"address":[],"length":0,"stats":{"Line":9}},{"line":150,"address":[],"length":0,"stats":{"Line":6}},{"line":151,"address":[],"length":0,"stats":{"Line":9}},{"line":152,"address":[],"length":0,"stats":{"Line":9}},{"line":154,"address":[],"length":0,"stats":{"Line":9}},{"line":155,"address":[],"length":0,"stats":{"Line":3}},{"line":156,"address":[],"length":0,"stats":{"Line":6}},{"line":157,"address":[],"length":0,"stats":{"Line":6}},{"line":161,"address":[],"length":0,"stats":{"Line":12}},{"line":163,"address":[],"length":0,"stats":{"Line":3}},{"line":165,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":9}},{"line":175,"address":[],"length":0,"stats":{"Line":9}},{"line":178,"address":[],"length":0,"stats":{"Line":3}},{"line":181,"address":[],"length":0,"stats":{"Line":9}},{"line":182,"address":[],"length":0,"stats":{"Line":9}},{"line":183,"address":[],"length":0,"stats":{"Line":9}},{"line":184,"address":[],"length":0,"stats":{"Line":3}},{"line":199,"address":[],"length":0,"stats":{"Line":4}},{"line":200,"address":[],"length":0,"stats":{"Line":15}},{"line":201,"address":[],"length":0,"stats":{"Line":6}},{"line":205,"address":[],"length":0,"stats":{"Line":6}},{"line":206,"address":[],"length":0,"stats":{"Line":0}},{"line":208,"address":[],"length":0,"stats":{"Line":3}},{"line":209,"address":[],"length":0,"stats":{"Line":9}},{"line":210,"address":[],"length":0,"stats":{"Line":3}},{"line":211,"address":[],"length":0,"stats":{"Line":1}},{"line":213,"address":[],"length":0,"stats":{"Line":4}},{"line":214,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":1}},{"line":224,"address":[],"length":0,"stats":{"Line":1}},{"line":225,"address":[],"length":0,"stats":{"Line":1}},{"line":226,"address":[],"length":0,"stats":{"Line":1}},{"line":230,"address":[],"length":0,"stats":{"Line":3}},{"line":232,"address":[],"length":0,"stats":{"Line":3}},{"line":233,"address":[],"length":0,"stats":{"Line":5}},{"line":234,"address":[],"length":0,"stats":{"Line":2}},{"line":236,"address":[],"length":0,"stats":{"Line":2}},{"line":237,"address":[],"length":0,"stats":{"Line":6}},{"line":238,"address":[],"length":0,"stats":{"Line":3}},{"line":239,"address":[],"length":0,"stats":{"Line":1}},{"line":244,"address":[],"length":0,"stats":{"Line":0}}],"covered":74,"coverable":77},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","planarfiledevice.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::PlanarFileDevice` — planar (per-channel) file read/write.\n//!\n//! Mirrors `src/codec/src/planarfiledevice.h`. Reads/writes one FILE* per\n//! channel so multi-channel audio is stored as one planar file per channel.\n//! `FILE*` implementation (the QIODevice-based original was replaced).\n\nuse std::fs::{File, OpenOptions};\nuse std::io::{Read, Seek, SeekFrom, Write};\nuse std::path::PathBuf;\n\n/// Open mode (replaces QIODevice::OpenMode).\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum OpenMode {\n\t/// Read-only.\n\tReadOnly = 0,\n\t/// Write-only.\n\tWriteOnly = 1,\n}\n\n/// `olive::PlanarFileDevice` — one file per channel.\npub struct PlanarFileDevice {\n\t/// Open file handles, one per channel.\n\tfiles: Vec\u003cFile\u003e,\n\t/// Open mode.\n\tmode: OpenMode,\n}\n\nimpl PlanarFileDevice {\n\t/// New, closed device.\n\tpub fn new() -\u003e Self {\n\t\tPlanarFileDevice {\n\t\t\tfiles: Vec::new(),\n\t\t\tmode: OpenMode::ReadOnly,\n\t\t}\n\t}\n\n\t/// Whether the device is open.\n\tpub fn is_open(\u0026self) -\u003e bool {\n\t\t!self.files.is_empty()\n\t}\n\n\t/// Open `filenames` (one per channel) in `mode`. Returns false if already\n\t/// open or any file could not be opened (closing any opened so far).\n\tpub fn open(\u0026mut self, filenames: \u0026[PathBuf], mode: OpenMode) -\u003e bool {\n\t\tif self.is_open() {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut opened = Vec::with_capacity(filenames.len());\n\t\tfor name in filenames {\n\t\t\tlet mut opt = OpenOptions::new();\n\t\t\tlet f = match mode {\n\t\t\t\tOpenMode::ReadOnly =\u003e opt.read(true).open(name),\n\t\t\t\tOpenMode::WriteOnly =\u003e opt.create(true).write(true).truncate(true).open(name),\n\t\t\t};\n\t\t\tmatch f {\n\t\t\t\tOk(f) =\u003e opened.push(f),\n\t\t\t\tErr(_) =\u003e {\n\t\t\t\t\t// Roll back: close anything opened so far and report failure.\n\t\t\t\t\tself.files = opened;\n\t\t\t\t\tself.close();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tself.files = opened;\n\t\tself.mode = mode;\n\t\ttrue\n\t}\n\n\t/// Read `bytes_per_channel` bytes from each channel (at the current file\n\t/// position) into `data[i][offset..]`. Returns bytes read per channel, or\n\t/// -1 if closed or a buffer is too small.\n\tpub fn read(\n\t\t\u0026mut self,\n\t\tdata: \u0026mut [\u0026mut [u8]],\n\t\tbytes_per_channel: i64,\n\t\toffset: i64,\n\t) -\u003e i64 {\n\t\tif !self.is_open() {\n\t\t\treturn -1;\n\t\t}\n\t\tlet bytes = bytes_per_channel as usize;\n\t\tlet off = offset as usize;\n\t\tlet mut ret = -1i64;\n\t\tfor (i, f) in self.files.iter_mut().enumerate() {\n\t\t\tlet buf = match data.get_mut(i) {\n\t\t\t\tSome(b) if b.len() \u003e= off + bytes =\u003e \u0026mut b[off..off + bytes],\n\t\t\t\t_ =\u003e return -1,\n\t\t\t};\n\t\t\tret = f.read(buf).unwrap_or(0) as i64;\n\t\t}\n\t\tret\n\t}\n\n\t/// Write `bytes_per_channel` bytes to each channel from `data[i][offset..]`.\n\t/// Returns bytes written per channel, or -1.\n\tpub fn write(\n\t\t\u0026mut self,\n\t\tdata: \u0026[\u0026[u8]],\n\t\tbytes_per_channel: i64,\n\t\toffset: i64,\n\t) -\u003e i64 {\n\t\tif !self.is_open() {\n\t\t\treturn -1;\n\t\t}\n\t\tlet bytes = bytes_per_channel as usize;\n\t\tlet off = offset as usize;\n\t\tlet mut ret = -1i64;\n\t\tfor (i, f) in self.files.iter_mut().enumerate() {\n\t\t\tlet buf = match data.get(i) {\n\t\t\t\tSome(b) if b.len() \u003e= off + bytes =\u003e \u0026b[off..off + bytes],\n\t\t\t\t_ =\u003e return -1,\n\t\t\t};\n\t\t\tret = f.write(buf).unwrap_or(0) as i64;\n\t\t}\n\t\tret\n\t}\n\n\t/// Total size in bytes of one channel (from the first open file).\n\tpub fn size(\u0026self) -\u003e i64 {\n\t\tif self.is_open() {\n\t\t\tif let Ok(meta) = self.files[0].metadata() {\n\t\t\t\treturn meta.len() as i64;\n\t\t\t}\n\t\t}\n\t\t0\n\t}\n\n\t/// Seek all channels to `pos`.\n\tpub fn seek(\u0026mut self, pos: i64) -\u003e bool {\n\t\tlet mut ok = true;\n\t\tfor f in self.files.iter_mut() {\n\t\t\tok = f.seek(SeekFrom::Start(pos as u64)).is_ok() \u0026\u0026 ok;\n\t\t}\n\t\tok\n\t}\n\n\t/// Close all channels.\n\tpub fn close(\u0026mut self) {\n\t\tself.files.clear();\n\t}\n}\n\nimpl Default for PlanarFileDevice {\n\tfn default() -\u003e Self {\n\t\tSelf::new()\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn temp_dir(name: \u0026str) -\u003e PathBuf {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_planar_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir\n\t}\n\n\t#[test]\n\tfn new_device_is_closed() {\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(!d.is_open());\n\t\tassert_eq!(d.size(), 0);\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut [0u8; 4]], 4, 0), -1);\n\t\tassert_eq!(d.write(\u0026[\u0026[0u8; 4]], 4, 0), -1);\n\t}\n\n\t#[test]\n\tfn write_then_read_roundtrip() {\n\t\tlet dir = temp_dir(\"rw\");\n\t\tlet names: Vec\u003cPathBuf\u003e = (0..2).map(|i| dir.join(format!(\"ch{}.pcm\", i))).collect();\n\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::WriteOnly));\n\t\tassert!(d.is_open());\n\t\tassert_eq!(d.mode, OpenMode::WriteOnly);\n\n\t\t// Two channels, 4 bytes each.\n\t\tlet ch0 = [1u8, 2, 3, 4];\n\t\tlet ch1 = [9u8, 8, 7, 6];\n\t\tassert_eq!(d.write(\u0026[\u0026ch0, \u0026ch1], 4, 0), 4);\n\t\tassert_eq!(d.size(), 4);\n\t\td.close();\n\t\tassert!(!d.is_open());\n\n\t\t// A \"wb\" handle cannot be read back (matching the C++ fopen mode);\n\t\t// reopen read-only to verify the written bytes.\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::ReadOnly));\n\t\tassert_eq!(d.size(), 4);\n\t\tlet mut out0 = [0u8; 8];\n\t\tlet mut out1 = [0u8; 8];\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut out0[..], \u0026mut out1[..]], 4, 0), 4);\n\t\tassert_eq!(\u0026out0[..4], \u0026ch0);\n\t\tassert_eq!(\u0026out1[..4], \u0026ch1);\n\t\td.close();\n\t\tassert!(!d.is_open());\n\t}\n\n\t#[test]\n\tfn offset_writes_and_reads() {\n\t\tlet dir = temp_dir(\"off\");\n\t\tlet names = vec![dir.join(\"ch.pcm\")];\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::WriteOnly));\n\n\t\t// The offset is a *buffer* offset (C++ `data[i] + offset`); file\n\t\t// offsets go through `seek`. Seek to 4 then write 4 bytes, leaving\n\t\t// a 4-byte gap.\n\t\tlet data = [7u8, 7, 7, 7];\n\t\tassert!(d.seek(4));\n\t\tassert_eq!(d.write(\u0026[\u0026data], 4, 0), 4);\n\t\tassert_eq!(d.size(), 8);\n\n\t\td.close();\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::ReadOnly));\n\t\tlet mut buf = [0u8; 8];\n\t\tassert!(d.seek(0));\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut buf[..]], 8, 0), 8);\n\t\tassert_eq!(\u0026buf[..4], \u0026[0, 0, 0, 0]);\n\t\tassert_eq!(\u0026buf[4..], \u0026data);\n\t}\n\n\t#[test]\n\tfn open_rollback_on_missing_file_and_double_open() {\n\t\tlet dir = temp_dir(\"roll\");\n\t\tlet ok = dir.join(\"ok.pcm\");\n\t\tlet missing = dir.join(\"missing.pcm\");\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(!d.open(\u0026[ok.clone(), missing], OpenMode::ReadOnly));\n\t\tassert!(!d.is_open());\n\n\t\t// Already open -\u003e refuse.\n\t\tassert!(d.open(\u0026[ok.clone()], OpenMode::WriteOnly));\n\t\tassert!(!d.open(\u0026[ok], OpenMode::WriteOnly));\n\t}\n\n\t#[test]\n\tfn read_write_reject_small_buffers() {\n\t\tlet dir = temp_dir(\"small\");\n\t\tlet names = vec![dir.join(\"ch.pcm\")];\n\t\tlet mut d = PlanarFileDevice::new();\n\t\tassert!(d.open(\u0026names, OpenMode::WriteOnly));\n\t\tlet data = [1u8; 8];\n\t\tassert_eq!(d.write(\u0026[\u0026data], 8, 0), 8);\n\n\t\t// Buffer too small for the requested bytes.\n\t\tlet mut small = [0u8; 4];\n\t\tassert_eq!(d.write(\u0026[\u0026small], 8, 0), -1);\n\t\tassert!(d.seek(0));\n\t\tassert_eq!(d.read(\u0026mut [\u0026mut small[..]], 8, 0), -1);\n\t}\n}\n","traces":[{"line":47,"address":[],"length":0,"stats":{"Line":8}},{"line":49,"address":[],"length":0,"stats":{"Line":8}},{"line":55,"address":[],"length":0,"stats":{"Line":824}},{"line":56,"address":[],"length":0,"stats":{"Line":824}},{"line":61,"address":[],"length":0,"stats":{"Line":9}},{"line":62,"address":[],"length":0,"stats":{"Line":18}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":66,"address":[],"length":0,"stats":{"Line":32}},{"line":67,"address":[],"length":0,"stats":{"Line":19}},{"line":68,"address":[],"length":0,"stats":{"Line":22}},{"line":69,"address":[],"length":0,"stats":{"Line":22}},{"line":70,"address":[],"length":0,"stats":{"Line":12}},{"line":71,"address":[],"length":0,"stats":{"Line":21}},{"line":73,"address":[],"length":0,"stats":{"Line":11}},{"line":74,"address":[],"length":0,"stats":{"Line":40}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":1}},{"line":84,"address":[],"length":0,"stats":{"Line":14}},{"line":85,"address":[],"length":0,"stats":{"Line":7}},{"line":86,"address":[],"length":0,"stats":{"Line":7}},{"line":92,"address":[],"length":0,"stats":{"Line":4}},{"line":98,"address":[],"length":0,"stats":{"Line":4}},{"line":99,"address":[],"length":0,"stats":{"Line":1}},{"line":101,"address":[],"length":0,"stats":{"Line":6}},{"line":102,"address":[],"length":0,"stats":{"Line":6}},{"line":103,"address":[],"length":0,"stats":{"Line":6}},{"line":104,"address":[],"length":0,"stats":{"Line":14}},{"line":105,"address":[],"length":0,"stats":{"Line":15}},{"line":106,"address":[],"length":0,"stats":{"Line":27}},{"line":107,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":9}},{"line":111,"address":[],"length":0,"stats":{"Line":2}},{"line":116,"address":[],"length":0,"stats":{"Line":802}},{"line":122,"address":[],"length":0,"stats":{"Line":802}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":125,"address":[],"length":0,"stats":{"Line":1602}},{"line":126,"address":[],"length":0,"stats":{"Line":1602}},{"line":127,"address":[],"length":0,"stats":{"Line":1602}},{"line":128,"address":[],"length":0,"stats":{"Line":4800}},{"line":129,"address":[],"length":0,"stats":{"Line":6395}},{"line":130,"address":[],"length":0,"stats":{"Line":12787}},{"line":131,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":4794}},{"line":135,"address":[],"length":0,"stats":{"Line":800}},{"line":139,"address":[],"length":0,"stats":{"Line":4}},{"line":140,"address":[],"length":0,"stats":{"Line":8}},{"line":141,"address":[],"length":0,"stats":{"Line":6}},{"line":142,"address":[],"length":0,"stats":{"Line":3}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":149,"address":[],"length":0,"stats":{"Line":3}},{"line":150,"address":[],"length":0,"stats":{"Line":6}},{"line":151,"address":[],"length":0,"stats":{"Line":6}},{"line":152,"address":[],"length":0,"stats":{"Line":12}},{"line":154,"address":[],"length":0,"stats":{"Line":3}},{"line":158,"address":[],"length":0,"stats":{"Line":5}},{"line":159,"address":[],"length":0,"stats":{"Line":10}},{"line":164,"address":[],"length":0,"stats":{"Line":0}},{"line":165,"address":[],"length":0,"stats":{"Line":0}}],"covered":57,"coverable":59},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","proxymanager.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::ProxyManager` — proxy (low-res transcode) generation.\n//!\n//! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual\n//! transcodes are delegated to the global task submit callback\n//! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy\n//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI\n//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23\n//! / \"mp4\" / \"veryfast\" / audio included).\n\nuse std::ffi::{c_char, CString};\nuse std::path::Path;\n\n/// Proxy state of a proxy file on disk.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum ProxyState {\n\t/// Missing (or NULL/empty/absent).\n\tMissing = 0,\n\t/// Generating.\n\tGenerating = 1,\n\t/// Ready on disk.\n\tReady = 2,\n\t/// Generation failed.\n\tFailed = 3,\n}\n\n/// `olive::ProxyManager::ProxyParams` — mirror of `oakcodec_proxy_params`.\n#[derive(Clone, Debug)]\n#[repr(C)]\npub struct ProxyParams {\n\t/// Absolute target width (0 when divider-based).\n\tpub width: i32,\n\t/// Absolute target height (0 when divider-based).\n\tpub height: i32,\n\t/// Source resolution divider (1 = absolute width/height, 2/4/8).\n\tpub divider: i32,\n\t/// Proxy format version.\n\tpub version: i32,\n\t/// x264 crf.\n\tpub crf: i32,\n\t/// Include the audio track (1/0; C `int`).\n\tpub include_audio: i32,\n\t/// ffmpeg output container (e.g. \"mp4\").\n\tpub extension: [u8; 32],\n\t/// ffmpeg encoder preset (e.g. \"veryfast\").\n\tpub preset: [u8; 32],\n}\n\nimpl Default for ProxyParams {\n\tfn default() -\u003e Self {\n\t\tProxyParams {\n\t\t\twidth: 1280,\n\t\t\theight: 720,\n\t\t\tdivider: 1,\n\t\t\tversion: 1,\n\t\t\tcrf: 23,\n\t\t\tinclude_audio: 1,\n\t\t\textension: bytes32(b\"mp4\"),\n\t\t\tpreset: bytes32(b\"veryfast\"),\n\t\t}\n\t}\n}\n\nimpl ProxyParams {\n\tfn extension_str(\u0026self) -\u003e \u0026str {\n\t\tcstr_slice(\u0026self.extension)\n\t}\n\n\tfn preset_str(\u0026self) -\u003e \u0026str {\n\t\tcstr_slice(\u0026self.preset)\n\t}\n}\n\n/// `oakcodec_proxy_result` — POD result of [`ProxyManager::get_or_start`];\n/// see `include/codec/proxy.h`.\n#[repr(C)]\npub struct OakCodecProxyResult {\n\t/// `ProxyState` value.\n\tpub state: i32,\n\t/// Resulting proxy filename (may be empty).\n\tpub filename: [u8; 1024],\n}\n\n/// `olive::ProxyManager` — stateless proxy query/generate manager.\npub struct ProxyManager;\n\nimpl ProxyManager {\n\t/// The process-wide ProxyManager singleton.\n\tpub fn instance() -\u003e \u0026'static ProxyManager {\n\t\tstatic INSTANCE: ProxyManager = ProxyManager;\n\t\t\u0026INSTANCE\n\t}\n\n\t/// Compiled-in default proxy parameters (the `Default` values).\n\tpub fn proxy_params_default() -\u003e ProxyParams {\n\t\tProxyParams::default()\n\t}\n\n\t/// Proxy parameters read from the oakcommon config, with the compiled-in\n\t/// defaults as fallback.\n\t///\n\t/// # CPP-PARITY\n\t/// `src/codec/src/proxymanager.h` `proxy_params_from_config` — reads\n\t/// ProxyWidth/ProxyHeight/ProxyDivider/ProxyCRF/ProxyPreset/\n\t/// ProxyIncludeAudio via `oakcommon_config_*`.\n\tpub fn proxy_params_from_config() -\u003e ProxyParams {\n\t\tlet mut p = ProxyParams::default();\n\t\tp.width = config_get_int(\"ProxyWidth\", p.width);\n\t\tp.height = config_get_int(\"ProxyHeight\", p.height);\n\t\tp.divider = config_get_int(\"ProxyDivider\", p.divider);\n\t\tp.crf = config_get_int(\"ProxyCRF\", p.crf);\n\t\tp.include_audio = if config_get_bool(\"ProxyIncludeAudio\", p.include_audio) != 0 {\n\t\t\t1\n\t\t} else {\n\t\t\t0\n\t\t};\n\t\tif let Some(preset) = config_get_str(\"ProxyPreset\") {\n\t\t\tif !preset.is_empty() {\n\t\t\t\tp.preset = bytes32(preset.as_bytes());\n\t\t\t}\n\t\t}\n\t\tp\n\t}\n\n\t/// State of a proxy file on disk.\n\tpub fn get_proxy_state(proxy_filename: \u0026str) -\u003e ProxyState {\n\t\tif proxy_filename.is_empty() {\n\t\t\treturn ProxyState::Missing;\n\t\t}\n\t\tif Path::new(proxy_filename).exists() {\n\t\t\treturn ProxyState::Ready;\n\t\t}\n\t\tlet working = Self::get_working_filename(proxy_filename);\n\t\tif let Ok(w) = working {\n\t\t\tif Path::new(\u0026w).exists() {\n\t\t\t\treturn ProxyState::Generating;\n\t\t\t}\n\t\t}\n\t\tProxyState::Missing\n\t}\n\n\t/// Human-readable string for a proxy state.\n\tpub fn proxy_state_to_string(state: ProxyState) -\u003e String {\n\t\tmatch state {\n\t\t\tProxyState::Missing =\u003e \"missing\".to_string(),\n\t\t\tProxyState::Generating =\u003e \"generating\".to_string(),\n\t\t\tProxyState::Ready =\u003e \"ready\".to_string(),\n\t\t\tProxyState::Failed =\u003e \"failed\".to_string(),\n\t\t}\n\t}\n\n\t/// Proxy directory for a project cache path.\n\tpub fn get_proxy_directory(cache_path: \u0026str) -\u003e crate::error::Result\u003cString\u003e {\n\t\tOk(Path::new(cache_path)\n\t\t\t.join(\"proxy\")\n\t\t\t.to_string_lossy()\n\t\t\t.into_owned())\n\t}\n\n\t/// Deterministic proxy filename for a source stream.\n\tpub fn get_proxy_filename(\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tparams: \u0026ProxyParams,\n\t) -\u003e crate::error::Result\u003cString\u003e {\n\t\tlet proxy_dir = Self::get_proxy_directory(cache_path)?;\n\t\tlet extension = if params.extension_str().is_empty() {\n\t\t\t\"mp4\"\n\t\t} else {\n\t\t\tparams.extension_str()\n\t\t};\n\n\t\t// Divider mode scales relative to the source, so the tag names the\n\t\t// divider rather than an absolute target size.\n\t\tlet size_tag = if params.divider \u003e 1 {\n\t\t\tformat!(\"div{}\", params.divider)\n\t\t} else {\n\t\t\tformat!(\"{}x{}\", params.width, params.height)\n\t\t};\n\n\t\tlet filename = format!(\n\t\t\t\"{}-{}.{}.v{}.a{}.{}\",\n\t\t\tunique_file_identifier(source_filename),\n\t\t\tstream_index,\n\t\t\tsize_tag,\n\t\t\tparams.version,\n\t\t\tparams.include_audio,\n\t\t\textension,\n\t\t);\n\n\t\tOk(Path::new(\u0026proxy_dir)\n\t\t\t.join(filename)\n\t\t\t.to_string_lossy()\n\t\t\t.into_owned())\n\t}\n\n\t/// Working (in-progress) filename of a proxy.\n\tpub fn get_working_filename(proxy_filename: \u0026str) -\u003e crate::error::Result\u003cString\u003e {\n\t\t// Append a recognizable suffix while keeping a standard container\n\t\t// extension so ffmpeg can infer the output format.\n\t\tOk(format!(\"{}.working.mp4\", proxy_filename))\n\t}\n\n\t/// Get or start generating a proxy for `source_filename`. With no task\n\t/// registrar the state stays `Missing`.\n\tpub fn get_or_start(\n\t\t\u0026self,\n\t\tcache_path: \u0026str,\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tparams: \u0026ProxyParams,\n\t) -\u003e crate::error::Result\u003c(ProxyState, String)\u003e {\n\t\tlet filename = Self::get_proxy_filename(cache_path, source_filename, stream_index, params)?;\n\t\tlet file_state = Self::get_proxy_state(\u0026filename);\n\t\tif file_state == ProxyState::Ready {\n\t\t\treturn Ok((ProxyState::Ready, filename));\n\t\t}\n\n\t\tif !crate::task::task_submit_is_registered() {\n\t\t\t// Interim state (pre-M8): no task system, proxy cannot be generated.\n\t\t\treturn Ok((ProxyState::Missing, filename));\n\t\t}\n\n\t\tif file_state == ProxyState::Generating {\n\t\t\t// Stale working file from an interrupted run.\n\t\t\tif let Ok(working) = Self::get_working_filename(\u0026filename) {\n\t\t\t\tlet _ = std::fs::remove_file(\u0026working);\n\t\t\t}\n\t\t}\n\n\t\t// The task owns the \".working.mp4\" temporary name and the rename to the\n\t\t// final filename on success.\n\t\tlet req = crate::task::TaskRequest {\n\t\t\tkind: crate::task::TaskKind::Proxy,\n\t\t\tinput_filename: source_filename,\n\t\t\toutput_filename: \u0026filename,\n\t\t\tstream_index,\n\t\t\tsample_rate: 0,\n\t\t\tchannel_layout: 0,\n\t\t\tsample_format: 0,\n\t\t\tproxy_width: if params.divider \u003c= 1 { params.width } else { 0 },\n\t\t\tproxy_height: if params.divider \u003c= 1 { params.height } else { 0 },\n\t\t};\n\n\t\t// Interim simplification: submission is synchronous.\n\t\tif crate::task::submit_task(\u0026req).is_err() {\n\t\t\treturn Ok((ProxyState::Failed, filename));\n\t\t}\n\n\t\tif Self::get_proxy_state(\u0026filename) == ProxyState::Ready {\n\t\t\treturn Ok((ProxyState::Ready, filename));\n\t\t}\n\n\t\tOk((ProxyState::Generating, filename))\n\t}\n\n\t/// Locate an ffmpeg executable (empty string when none found).\n\tpub fn find_ffmpeg(configured_path: \u0026str) -\u003e String {\n\t\t// An explicitly configured path takes precedence if it is usable.\n\t\tif !configured_path.is_empty() {\n\t\t\tif is_executable_file(Path::new(configured_path)) {\n\t\t\t\treturn absolute(configured_path);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to searching the system PATH.\n\t\tif let Ok(path_env) = std::env::var(\"PATH\") {\n\t\t\tfor dir in path_env.split(':') {\n\t\t\t\tif dir.is_empty() {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlet candidate = Path::new(dir).join(\"ffmpeg\");\n\t\t\t\tif is_executable_file(\u0026candidate) {\n\t\t\t\t\treturn absolute(\u0026candidate.to_string_lossy());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Finally, try common install locations (PATH on GUI-launched apps,\n\t\t// particularly on macOS, often lacks these).\n\t\tlet mut candidates: Vec\u003cString\u003e = Vec::new();\n\t\tlet app_path = application_path();\n\t\tif !app_path.is_empty() {\n\t\t\tcandidates.push(format!(\"{}/ffmpeg\", app_path));\n\t\t}\n\t\tcandidates.push(\"/opt/homebrew/bin/ffmpeg\".to_string());\n\t\tcandidates.push(\"/usr/local/bin/ffmpeg\".to_string());\n\t\tcandidates.push(\"/usr/bin/ffmpeg\".to_string());\n\t\tcandidates.push(\"/usr/local/bin/ffmpeg\".to_string());\n\n\t\tfor c in candidates {\n\t\t\tif is_executable_file(Path::new(\u0026c)) {\n\t\t\t\treturn absolute(\u0026c);\n\t\t\t}\n\t\t}\n\n\t\tString::new()\n\t}\n}\n\n/// Copy a byte string into a NUL-terminated `[u8; 32]` (truncated to 31\n/// chars so there is always a trailing NUL).\nfn bytes32(s: \u0026[u8]) -\u003e [u8; 32] {\n\tlet mut a = [0u8; 32];\n\tlet n = s.len().min(31);\n\ta[..n].copy_from_slice(\u0026s[..n]);\n\ta\n}\n\n/// View a NUL-terminated `[u8; 32]` as a `\u0026str` (up to the first NUL).\nfn cstr_slice(a: \u0026[u8; 32]) -\u003e \u0026str {\n\tlet end = a.iter().position(|\u0026b| b == 0).unwrap_or(a.len());\n\tstd::str::from_utf8(\u0026a[..end]).unwrap_or(\"\")\n}\n\n/// `oakcommon_config_get_int` wrapper (null group).\nfn config_get_int(key: \u0026str, default: i32) -\u003e i32 {\n\tlet ckey = cstring(key);\n\t// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_config_get_int(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tdefault,\n\t\t)\n\t}\n}\n\n/// `oakcommon_config_get_bool` wrapper (null group).\nfn config_get_bool(key: \u0026str, default: i32) -\u003e i32 {\n\tlet ckey = cstring(key);\n\t// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_config_get_bool(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tdefault,\n\t\t)\n\t}\n}\n\n/// Two-stage `oakcommon_config_get` string read; `None` when the stored\n/// value is empty or absent.\nfn config_get_str(key: \u0026str) -\u003e Option\u003cString\u003e {\n\tlet ckey = cstring(key);\n\t// # Safety: `ckey` is valid; first call asks only for the required size.\n\tlet size = unsafe {\n\t\tcrate::bridge::common::oakcommon_config_get(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tstd::ptr::null_mut(),\n\t\t\t0,\n\t\t)\n\t};\n\tif size \u003c= 1 {\n\t\treturn None;\n\t}\n\tlet mut buf = vec![0u8; size as usize];\n\t// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_config_get(\n\t\t\tstd::ptr::null(),\n\t\t\tckey.as_ptr(),\n\t\t\tbuf.as_mut_ptr() as *mut c_char,\n\t\t\tsize,\n\t\t);\n\t}\n\tlet mut end = buf.len();\n\twhile end \u003e 0 \u0026\u0026 buf[end - 1] == 0 {\n\t\tend -= 1;\n\t}\n\tSome(String::from_utf8_lossy(\u0026buf[..end]).into_owned())\n}\n\n/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge\n/// returns a 64-bit id directly).\nfn unique_file_identifier(filename: \u0026str) -\u003e String {\n\tlet c = match CString::new(filename) {\n\t\tOk(c) =\u003e c,\n\t\tErr(_) =\u003e return String::new(),\n\t};\n\t// # Safety: `c` is a valid NUL-terminated C string alive for the call.\n\tlet id = unsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())\n\t};\n\tformat!(\"{}\", id)\n}\n\n/// Two-stage `oakcommon_filefunctions_get_application_path` read.\nfn application_path() -\u003e String {\n\t// # Safety: first call asks only for the required size.\n\tlet size = unsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_application_path(\n\t\t\tstd::ptr::null_mut(),\n\t\t\t0,\n\t\t)\n\t};\n\tif size \u003c= 1 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0u8; size as usize];\n\t// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.\n\tunsafe {\n\t\tcrate::bridge::common::oakcommon_filefunctions_get_application_path(\n\t\t\tbuf.as_mut_ptr() as *mut c_char,\n\t\t\tsize,\n\t\t);\n\t}\n\tlet mut end = buf.len();\n\twhile end \u003e 0 \u0026\u0026 buf[end - 1] == 0 {\n\t\tend -= 1;\n\t}\n\tString::from_utf8_lossy(\u0026buf[..end]).into_owned()\n}\n\n/// Build a NUL-terminated C string from a Rust string; empty on embedded\n/// NUL (defensive only — callers pass sane keys).\nfn cstring(s: \u0026str) -\u003e CString {\n\tCString::new(s).unwrap_or_else(|_| CString::new(\"\").unwrap())\n}\n\n/// True when `p` is a regular file with at least one execute bit set.\nfn is_executable_file(p: \u0026Path) -\u003e bool {\n\tuse std::os::unix::fs::PermissionsExt;\n\tmatch std::fs::metadata(p) {\n\t\tOk(md) if md.is_file() =\u003e md.permissions().mode() \u0026 0o111 != 0,\n\t\t_ =\u003e false,\n\t}\n}\n\n/// Canonicalize a path, falling back to the raw string on failure.\nfn absolute(p: \u0026str) -\u003e String {\n\tstd::fs::canonicalize(p)\n\t\t.map(|c| c.to_string_lossy().into_owned())\n\t\t.unwrap_or_else(|_| p.to_string())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn temp_subdir(name: \u0026str) -\u003e String {\n\t\tlet dir = std::env::temp_dir().join(format!(\n\t\t\t\"oakcodec_proxy_{}_{}\",\n\t\t\tname,\n\t\t\tstd::process::id()\n\t\t));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tdir.to_string_lossy().into_owned()\n\t}\n\n\tfn fnv1a64(bytes: \u0026[u8]) -\u003e u64 {\n\t\tlet mut h: u64 = 14695981039346656037;\n\t\tfor \u0026b in bytes {\n\t\t\th ^= b as u64;\n\t\t\th = h.wrapping_mul(1099511628211);\n\t\t}\n\t\th\n\t}\n\n\t#[test]\n\tfn proxy_params_default_values() {\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tassert_eq!(p.width, 1280);\n\t\tassert_eq!(p.height, 720);\n\t\tassert_eq!(p.divider, 1);\n\t\tassert_eq!(p.version, 1);\n\t\tassert_eq!(p.crf, 23);\n\t\tassert_eq!(p.include_audio, 1);\n\t\tassert_eq!(p.extension_str(), \"mp4\");\n\t\tassert_eq!(p.preset_str(), \"veryfast\");\n\t}\n\n\t#[test]\n\tfn proxy_params_from_config_uses_defaults_without_store() {\n\t\t// The test stub returns defaults for every int/bool and an empty\n\t\t// ProxyPreset; the empty preset must not clobber the compiled-in one.\n\t\tlet p = ProxyManager::proxy_params_from_config();\n\t\tassert_eq!(p.width, 1280);\n\t\tassert_eq!(p.height, 720);\n\t\tassert_eq!(p.divider, 1);\n\t\tassert_eq!(p.crf, 23);\n\t\tassert_eq!(p.include_audio, 1);\n\t\tassert_eq!(p.preset_str(), \"veryfast\");\n\t\tassert_eq!(p.extension_str(), \"mp4\");\n\t}\n\n\t#[test]\n\tfn proxy_directory_is_cache_slash_proxy() {\n\t\tassert_eq!(\n\t\t\tProxyManager::get_proxy_directory(\"/tmp/cache\").unwrap(),\n\t\t\t\"/tmp/cache/proxy\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn proxy_filename_derivation() {\n\t\tlet cache = temp_subdir(\"fn\");\n\t\tlet id = fnv1a64(b\"media.mp4\") as i64;\n\t\tlet p = ProxyManager::proxy_params_default();\n\n\t\tlet f = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026p).unwrap();\n\t\tassert_eq!(\n\t\t\tf,\n\t\t\tformat!(\"{}/proxy/{}-0.1280x720.v1.a1.mp4\", cache, id)\n\t\t);\n\n\t\t// Divider mode tags the divider instead of an absolute size.\n\t\tlet mut d = p.clone();\n\t\td.divider = 2;\n\t\tlet f2 = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026d).unwrap();\n\t\tassert!(f2.contains(\".div2.\"));\n\n\t\t// No audio.\n\t\tlet mut na = p.clone();\n\t\tna.include_audio = 0;\n\t\tlet f3 = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026na).unwrap();\n\t\tassert!(f3.contains(\".a0.\"));\n\t}\n\n\t#[test]\n\tfn proxy_state_transitions() {\n\t\tlet cache = temp_subdir(\"state\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet f = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026p).unwrap();\n\n\t\t// Missing when neither the file nor the working file exists.\n\t\tassert_eq!(ProxyManager::get_proxy_state(\u0026f), ProxyState::Missing);\n\n\t\t// Generating when only the working file exists.\n\t\tlet working = ProxyManager::get_working_filename(\u0026f).unwrap();\n\t\tstd::fs::create_dir_all(Path::new(\u0026working).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026working, b\"x\").unwrap();\n\t\tassert_eq!(ProxyManager::get_proxy_state(\u0026f), ProxyState::Generating);\n\n\t\t// Ready when the final file exists (takes precedence over working).\n\t\tstd::fs::create_dir_all(Path::new(\u0026f).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026f, b\"x\").unwrap();\n\t\tassert_eq!(ProxyManager::get_proxy_state(\u0026f), ProxyState::Ready);\n\t}\n\n\t#[test]\n\tfn proxy_state_to_string_mapping() {\n\t\tassert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Missing), \"missing\");\n\t\tassert_eq!(\n\t\t\tProxyManager::proxy_state_to_string(ProxyState::Generating),\n\t\t\t\"generating\"\n\t\t);\n\t\tassert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Ready), \"ready\");\n\t\tassert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Failed), \"failed\");\n\t}\n\n\t#[test]\n\tfn get_working_filename_appends_suffix() {\n\t\tassert_eq!(\n\t\t\tProxyManager::get_working_filename(\"/a/b.mp4\").unwrap(),\n\t\t\t\"/a/b.mp4.working.mp4\"\n\t\t);\n\t}\n\n\t#[test]\n\tfn get_or_start_missing_without_registrar() {\n\t\tlet _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tlet cache = temp_subdir(\"nostart\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet (state, _f) =\n\t\t\tProxyManager::instance()\n\t\t\t\t.get_or_start(\u0026cache, \"media.mp4\", 0, \u0026p)\n\t\t\t\t.unwrap();\n\t\tassert_eq!(state, ProxyState::Missing);\n\t}\n\n\t#[test]\n\tfn get_or_start_ready_when_file_exists() {\n\t\tlet cache = temp_subdir(\"ready\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet f = ProxyManager::get_proxy_filename(\u0026cache, \"media.mp4\", 0, \u0026p).unwrap();\n\t\tstd::fs::create_dir_all(Path::new(\u0026f).parent().unwrap()).unwrap();\n\t\tstd::fs::write(\u0026f, b\"x\").unwrap();\n\t\tlet (state, filename) =\n\t\t\tProxyManager::instance()\n\t\t\t\t.get_or_start(\u0026cache, \"media.mp4\", 0, \u0026p)\n\t\t\t\t.unwrap();\n\t\tassert_eq!(state, ProxyState::Ready);\n\t\tassert_eq!(filename, f);\n\t}\n\n\t#[test]\n\tfn get_or_start_generating_when_registered() {\n\t\tlet _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(\n\t\t\tSome(crate::conformmanager::test_util::accept_cb),\n\t\t\tstd::ptr::null_mut(),\n\t\t);\n\t\tlet cache = temp_subdir(\"start\");\n\t\tlet p = ProxyManager::proxy_params_default();\n\t\tlet (state, _f) =\n\t\t\tProxyManager::instance()\n\t\t\t\t.get_or_start(\u0026cache, \"media.mp4\", 0, \u0026p)\n\t\t\t\t.unwrap();\n\t\tcrate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());\n\t\tassert_eq!(state, ProxyState::Generating);\n\t}\n\n\t#[test]\n\tfn find_ffmpeg_configured_path_wins() {\n\t\t// Point at a real executable (the current test binary) so the\n\t\t// configured-path branch resolves to an absolute path.\n\t\tlet me = std::env::current_exe().unwrap();\n\t\tlet found = ProxyManager::find_ffmpeg(me.to_str().unwrap());\n\t\tlet canonical = std::fs::canonicalize(\u0026me).unwrap();\n\t\tassert_eq!(found, canonical.to_string_lossy());\n\t}\n\n\t#[test]\n\tfn find_ffmpeg_missing_returns_empty() {\n\t\tlet found = ProxyManager::find_ffmpeg(\"/definitely/not/a/real/ffmpeg\");\n\t\t// Either an absolute configured/installed match or empty; never a raw\n\t\t// unresolved path.\n\t\tassert!(found.is_empty() || found.starts_with('/'));\n\t}\n\n\t/// `oakcodec_proxy_params` byte-level layout lock against\n\t/// `include/codec/proxy.h` (verified with a C++ `offsetof` probe): the\n\t/// Rust mirror must read a C caller's POD in place.\n\t#[test]\n\tfn proxy_params_c_abi_layout() {\n\t\tuse std::mem::{offset_of, size_of};\n\n\t\tassert_eq!(size_of::\u003cProxyParams\u003e(), 88);\n\t\tassert_eq!(offset_of!(ProxyParams, width), 0);\n\t\tassert_eq!(offset_of!(ProxyParams, height), 4);\n\t\tassert_eq!(offset_of!(ProxyParams, divider), 8);\n\t\tassert_eq!(offset_of!(ProxyParams, version), 12);\n\t\tassert_eq!(offset_of!(ProxyParams, crf), 16);\n\t\tassert_eq!(offset_of!(ProxyParams, include_audio), 20);\n\t\tassert_eq!(offset_of!(ProxyParams, extension), 24);\n\t\tassert_eq!(offset_of!(ProxyParams, preset), 56);\n\t}\n}\n\n#[cfg(test)]\nmod tests_extra {\n\tuse super::*;\n\n\t#[test]\n\tfn find_ffmpeg_searches_path() {\n\t\t// Create a fake executable in a temp dir and prepend it to PATH.\n\t\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_ffmpeg_{}\", std::process::id()));\n\t\tlet _ = std::fs::create_dir_all(\u0026dir);\n\t\tlet fake = dir.join(\"ffmpeg\");\n\t\tuse std::os::unix::fs::PermissionsExt;\n\t\tstd::fs::write(\u0026fake, b\"#!/bin/sh\\n\").unwrap();\n\t\tstd::fs::set_permissions(\u0026fake, std::fs::Permissions::from_mode(0o755)).unwrap();\n\n\t\tlet mut paths = dir.to_string_lossy().into_owned();\n\t\tif let Ok(existing) = std::env::var(\"PATH\") {\n\t\t\tpaths.push(':');\n\t\t\tpaths.push_str(\u0026existing);\n\t\t}\n\t\tlet old = std::env::var_os(\"PATH\");\n\t\tstd::env::set_var(\"PATH\", \u0026paths);\n\t\tlet found = ProxyManager::find_ffmpeg(\"\");\n\t\tif let Some(old) = old {\n\t\t\tstd::env::set_var(\"PATH\", old);\n\t\t} else {\n\t\t\tstd::env::remove_var(\"PATH\");\n\t\t}\n\n\t\t// `std::env::set_var` is not thread-safe, so under parallel tests the\n\t\t// canonicalized form can race; assert the search branch invariants\n\t\t// instead of the exact canonical path.\n\t\tassert!(found.starts_with('/'), \"found: {found}\");\n\t\tassert!(found.ends_with(\"/ffmpeg\"), \"found: {found}\");\n\t\tassert!(std::path::Path::new(\u0026found).exists(), \"found: {found}\");\n\t}\n\n\t#[test]\n\tfn get_proxy_state_empty_and_working() {\n\t\t// Empty filename -\u003e Missing.\n\t\tassert_eq!(ProxyManager::get_proxy_state(\"\"), ProxyState::Missing);\n\t\t// A path that does not exist -\u003e Missing.\n\t\tassert_eq!(ProxyManager::get_proxy_state(\"/nope/nope.mp4\"), ProxyState::Missing);\n\t}\n}\n","traces":[{"line":66,"address":[],"length":0,"stats":{"Line":10}},{"line":74,"address":[],"length":0,"stats":{"Line":30}},{"line":75,"address":[],"length":0,"stats":{"Line":10}},{"line":81,"address":[],"length":0,"stats":{"Line":24}},{"line":82,"address":[],"length":0,"stats":{"Line":24}},{"line":85,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":2}},{"line":105,"address":[],"length":0,"stats":{"Line":6}},{"line":107,"address":[],"length":0,"stats":{"Line":6}},{"line":111,"address":[],"length":0,"stats":{"Line":6}},{"line":112,"address":[],"length":0,"stats":{"Line":6}},{"line":122,"address":[],"length":0,"stats":{"Line":4}},{"line":123,"address":[],"length":0,"stats":{"Line":8}},{"line":124,"address":[],"length":0,"stats":{"Line":8}},{"line":125,"address":[],"length":0,"stats":{"Line":8}},{"line":126,"address":[],"length":0,"stats":{"Line":8}},{"line":127,"address":[],"length":0,"stats":{"Line":8}},{"line":128,"address":[],"length":0,"stats":{"Line":8}},{"line":129,"address":[],"length":0,"stats":{"Line":4}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":133,"address":[],"length":0,"stats":{"Line":4}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":4}},{"line":142,"address":[],"length":0,"stats":{"Line":14}},{"line":143,"address":[],"length":0,"stats":{"Line":28}},{"line":144,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":26}},{"line":147,"address":[],"length":0,"stats":{"Line":3}},{"line":149,"address":[],"length":0,"stats":{"Line":30}},{"line":150,"address":[],"length":0,"stats":{"Line":20}},{"line":151,"address":[],"length":0,"stats":{"Line":20}},{"line":152,"address":[],"length":0,"stats":{"Line":1}},{"line":155,"address":[],"length":0,"stats":{"Line":9}},{"line":159,"address":[],"length":0,"stats":{"Line":5}},{"line":160,"address":[],"length":0,"stats":{"Line":5}},{"line":161,"address":[],"length":0,"stats":{"Line":2}},{"line":162,"address":[],"length":0,"stats":{"Line":2}},{"line":163,"address":[],"length":0,"stats":{"Line":4}},{"line":164,"address":[],"length":0,"stats":{"Line":2}},{"line":169,"address":[],"length":0,"stats":{"Line":13}},{"line":170,"address":[],"length":0,"stats":{"Line":13}},{"line":171,"address":[],"length":0,"stats":{"Line":13}},{"line":172,"address":[],"length":0,"stats":{"Line":13}},{"line":173,"address":[],"length":0,"stats":{"Line":13}},{"line":177,"address":[],"length":0,"stats":{"Line":11}},{"line":183,"address":[],"length":0,"stats":{"Line":33}},{"line":184,"address":[],"length":0,"stats":{"Line":33}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":11}},{"line":192,"address":[],"length":0,"stats":{"Line":22}},{"line":193,"address":[],"length":0,"stats":{"Line":1}},{"line":195,"address":[],"length":0,"stats":{"Line":10}},{"line":198,"address":[],"length":0,"stats":{"Line":22}},{"line":200,"address":[],"length":0,"stats":{"Line":22}},{"line":208,"address":[],"length":0,"stats":{"Line":22}},{"line":209,"address":[],"length":0,"stats":{"Line":11}},{"line":210,"address":[],"length":0,"stats":{"Line":11}},{"line":211,"address":[],"length":0,"stats":{"Line":11}},{"line":215,"address":[],"length":0,"stats":{"Line":13}},{"line":218,"address":[],"length":0,"stats":{"Line":13}},{"line":223,"address":[],"length":0,"stats":{"Line":5}},{"line":230,"address":[],"length":0,"stats":{"Line":30}},{"line":231,"address":[],"length":0,"stats":{"Line":15}},{"line":232,"address":[],"length":0,"stats":{"Line":5}},{"line":233,"address":[],"length":0,"stats":{"Line":1}},{"line":236,"address":[],"length":0,"stats":{"Line":4}},{"line":238,"address":[],"length":0,"stats":{"Line":2}},{"line":241,"address":[],"length":0,"stats":{"Line":2}},{"line":243,"address":[],"length":0,"stats":{"Line":0}},{"line":244,"address":[],"length":0,"stats":{"Line":0}},{"line":253,"address":[],"length":0,"stats":{"Line":2}},{"line":258,"address":[],"length":0,"stats":{"Line":4}},{"line":259,"address":[],"length":0,"stats":{"Line":4}},{"line":263,"address":[],"length":0,"stats":{"Line":4}},{"line":264,"address":[],"length":0,"stats":{"Line":0}},{"line":267,"address":[],"length":0,"stats":{"Line":2}},{"line":268,"address":[],"length":0,"stats":{"Line":0}},{"line":271,"address":[],"length":0,"stats":{"Line":2}},{"line":275,"address":[],"length":0,"stats":{"Line":5}},{"line":277,"address":[],"length":0,"stats":{"Line":5}},{"line":278,"address":[],"length":0,"stats":{"Line":6}},{"line":279,"address":[],"length":0,"stats":{"Line":4}},{"line":284,"address":[],"length":0,"stats":{"Line":6}},{"line":285,"address":[],"length":0,"stats":{"Line":26}},{"line":286,"address":[],"length":0,"stats":{"Line":46}},{"line":287,"address":[],"length":0,"stats":{"Line":0}},{"line":289,"address":[],"length":0,"stats":{"Line":69}},{"line":290,"address":[],"length":0,"stats":{"Line":46}},{"line":291,"address":[],"length":0,"stats":{"Line":6}},{"line":298,"address":[],"length":0,"stats":{"Line":0}},{"line":299,"address":[],"length":0,"stats":{"Line":0}},{"line":300,"address":[],"length":0,"stats":{"Line":0}},{"line":301,"address":[],"length":0,"stats":{"Line":0}},{"line":303,"address":[],"length":0,"stats":{"Line":0}},{"line":304,"address":[],"length":0,"stats":{"Line":0}},{"line":305,"address":[],"length":0,"stats":{"Line":0}},{"line":306,"address":[],"length":0,"stats":{"Line":0}},{"line":308,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":0}},{"line":310,"address":[],"length":0,"stats":{"Line":0}},{"line":314,"address":[],"length":0,"stats":{"Line":0}},{"line":320,"address":[],"length":0,"stats":{"Line":20}},{"line":321,"address":[],"length":0,"stats":{"Line":40}},{"line":322,"address":[],"length":0,"stats":{"Line":80}},{"line":323,"address":[],"length":0,"stats":{"Line":60}},{"line":324,"address":[],"length":0,"stats":{"Line":20}},{"line":328,"address":[],"length":0,"stats":{"Line":26}},{"line":329,"address":[],"length":0,"stats":{"Line":270}},{"line":330,"address":[],"length":0,"stats":{"Line":78}},{"line":334,"address":[],"length":0,"stats":{"Line":16}},{"line":335,"address":[],"length":0,"stats":{"Line":48}},{"line":339,"address":[],"length":0,"stats":{"Line":16}},{"line":340,"address":[],"length":0,"stats":{"Line":16}},{"line":341,"address":[],"length":0,"stats":{"Line":16}},{"line":347,"address":[],"length":0,"stats":{"Line":4}},{"line":348,"address":[],"length":0,"stats":{"Line":12}},{"line":352,"address":[],"length":0,"stats":{"Line":4}},{"line":353,"address":[],"length":0,"stats":{"Line":4}},{"line":354,"address":[],"length":0,"stats":{"Line":4}},{"line":361,"address":[],"length":0,"stats":{"Line":4}},{"line":362,"address":[],"length":0,"stats":{"Line":12}},{"line":366,"address":[],"length":0,"stats":{"Line":4}},{"line":367,"address":[],"length":0,"stats":{"Line":4}},{"line":368,"address":[],"length":0,"stats":{"Line":4}},{"line":372,"address":[],"length":0,"stats":{"Line":4}},{"line":373,"address":[],"length":0,"stats":{"Line":4}},{"line":375,"address":[],"length":0,"stats":{"Line":0}},{"line":379,"address":[],"length":0,"stats":{"Line":0}},{"line":380,"address":[],"length":0,"stats":{"Line":0}},{"line":381,"address":[],"length":0,"stats":{"Line":0}},{"line":382,"address":[],"length":0,"stats":{"Line":0}},{"line":385,"address":[],"length":0,"stats":{"Line":0}},{"line":386,"address":[],"length":0,"stats":{"Line":0}},{"line":387,"address":[],"length":0,"stats":{"Line":0}},{"line":389,"address":[],"length":0,"stats":{"Line":0}},{"line":394,"address":[],"length":0,"stats":{"Line":11}},{"line":395,"address":[],"length":0,"stats":{"Line":22}},{"line":396,"address":[],"length":0,"stats":{"Line":22}},{"line":397,"address":[],"length":0,"stats":{"Line":0}},{"line":401,"address":[],"length":0,"stats":{"Line":22}},{"line":403,"address":[],"length":0,"stats":{"Line":11}},{"line":407,"address":[],"length":0,"stats":{"Line":0}},{"line":411,"address":[],"length":0,"stats":{"Line":0}},{"line":415,"address":[],"length":0,"stats":{"Line":0}},{"line":416,"address":[],"length":0,"stats":{"Line":0}},{"line":418,"address":[],"length":0,"stats":{"Line":0}},{"line":422,"address":[],"length":0,"stats":{"Line":0}},{"line":423,"address":[],"length":0,"stats":{"Line":0}},{"line":426,"address":[],"length":0,"stats":{"Line":0}},{"line":427,"address":[],"length":0,"stats":{"Line":0}},{"line":428,"address":[],"length":0,"stats":{"Line":0}},{"line":430,"address":[],"length":0,"stats":{"Line":0}},{"line":435,"address":[],"length":0,"stats":{"Line":24}},{"line":436,"address":[],"length":0,"stats":{"Line":72}},{"line":440,"address":[],"length":0,"stats":{"Line":26}},{"line":442,"address":[],"length":0,"stats":{"Line":52}},{"line":443,"address":[],"length":0,"stats":{"Line":25}},{"line":444,"address":[],"length":0,"stats":{"Line":21}},{"line":449,"address":[],"length":0,"stats":{"Line":5}},{"line":450,"address":[],"length":0,"stats":{"Line":10}},{"line":451,"address":[],"length":0,"stats":{"Line":15}},{"line":452,"address":[],"length":0,"stats":{"Line":5}}],"covered":121,"coverable":163},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","realmedia_tests.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Real-media tests for the FFmpeg decoder/encoder (`#[cfg(test)]` module).\n//!\n//! These exercise the real `ffmpeg-next` implementation against\n//! `tests/demo.mp4` at the repository root (H.264 1920x1080@25fps + AAC\n//! 48kHz stereo) and a full H.264 encode round-trip through `/tmp`.\n//!\n//! They live inside the crate (not `tests/`) because the crate's\n//! `#[cfg(test)]` in-memory oakcommon/oakrender stubs — which the\n//! `Frame`/`FootageDescription` paths need — are only linked for the lib\n//! test binary (`tests/` is compiled without `#[cfg(test)]` and cannot\n//! resolve those symbols; see `tests/ffi_contract_test.rs`).\n\nuse crate::bridge::common::{\n\toakcommon_videoparams_get_duration, oakcommon_videoparams_get_frame_rate,\n\toakcommon_videoparams_get_height, oakcommon_videoparams_get_width,\n\toakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,\n};\nuse crate::decoder::{\n\tCodecStream, Decoder, K_COLOR_RANGE_DEFAULT, RenderMode, RetrieveAudioStatus,\n\tRetrieveVideoParams,\n};\nuse crate::encoder::create_from_params;\nuse crate::ffmpeg::FFmpegDecoder;\nuse crate::frame::Frame;\nuse oakcore_rs::{PixelFormat, Rational, TimeRange};\n\n/// `tests/demo.mp4` at the repository root.\nfn demo_path() -\u003e std::path::PathBuf {\n\tstd::path::Path::new(env!(\"CARGO_MANIFEST_DIR\")).join(\"../../../tests/demo.mp4\")\n}\n\nfn video_params(stream: CodecStream, time: Rational) -\u003e RetrieveVideoParams {\n\tRetrieveVideoParams {\n\t\tstream,\n\t\ttime,\n\t\tlength: TimeRange::default(),\n\t\tforce_range: K_COLOR_RANGE_DEFAULT,\n\t\tis_image_sequence: false,\n\t\timage_sequence_digits: 0,\n\t\timage_sequence_number: 0,\n\t\tmode: RenderMode::Offline,\n\t\talpha_is_premultiplied: false,\n\t}\n}\n\n/// H.264 encoder parameters: 64x64, 10 fps, `out` as the target file.\nfn h264_params(out: \u0026std::path::Path) -\u003e crate::encodingparams::EncodingParams {\n\tlet mut p = crate::encodingparams::EncodingParams::default();\n\tlet name = out.as_os_str().as_encoded_bytes();\n\tp.filename[..name.len()].copy_from_slice(name);\n\tp.format = 2; // MPEG-4 video\n\tp.video_enabled = 1;\n\tp.video_codec = 1; // H.264\n\tp.video_width = 64;\n\tp.video_height = 64;\n\tp.video_time_base_num = 1;\n\tp.video_time_base_den = 10;\n\tp.video_pixel_format = PixelFormat::F32;\n\tp.video_interlacing = 0;\n\tp.video_pixel_aspect_num = 1;\n\tp.video_pixel_aspect_den = 1;\n\tp\n}\n\n/// Build an allocated F32-RGBA frame with a moving color pattern.\nfn pattern_frame(i: i32) -\u003e Frame {\n\tlet vp = unsafe { oakcommon_videoparams_init_basic(64, 64) };\n\tunsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };\n\tlet mut f = Frame::with_params(vp);\n\tf.set_timestamp(Rational::new(i as i64, 10));\n\tf.allocate().unwrap();\n\n\tlet linesize = f.linesize_bytes() as usize;\n\tlet data = f.data_mut().unwrap();\n\tfor y in 0..64usize {\n\t\tfor x in 0..64usize {\n\t\t\tlet off = y * linesize + x * 16;\n\t\t\tlet r: f32 = if x \u003c 32 { 0.4 + i as f32 * 0.05 } else { 0.1 };\n\t\t\tlet g: f32 = y as f32 / 64.0;\n\t\t\tlet b: f32 = if x \u003e= 32 { 0.7 } else { 0.2 };\n\t\t\tdata[off..off + 4].copy_from_slice(\u0026r.to_le_bytes());\n\t\t\tdata[off + 4..off + 8].copy_from_slice(\u0026g.to_le_bytes());\n\t\t\tdata[off + 8..off + 12].copy_from_slice(\u0026b.to_le_bytes());\n\t\t\tdata[off + 12..off + 16].copy_from_slice(\u00261.0f32.to_le_bytes());\n\t\t}\n\t}\n\tf\n}\n\n#[test]\nfn probe_reports_streams_and_duration() {\n\tlet d = FFmpegDecoder::new();\n\tlet desc = d\n\t\t.probe(demo_path().to_str().unwrap(), None)\n\t\t.expect(\"demo.mp4 should probe\");\n\tassert_eq!(desc.decoder(), \"ffmpeg\");\n\t// video + audio + data (timecode) stream.\n\tassert_eq!(desc.total_stream_count(), 3);\n\tassert_eq!(desc.video_stream_count(), 1);\n\tassert_eq!(desc.audio_stream_count(), 1);\n\n\t// Video stream: 1920x1080, 25fps, 17s at 1/12800 time base.\n\tlet vp = desc.get_video_stream(0).expect(\"video stream\");\n\tassert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 1920);\n\tassert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 1080);\n\tassert_eq!(unsafe { oakcommon_videoparams_get_duration(vp.clone()) }, 17 * 12800);\n\n\tlet mut num: i32 = 0;\n\tlet mut den: i32 = 0;\n\tunsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), \u0026mut num, \u0026mut den) };\n\tassert_eq!((num, den), (25, 1));\n}\n\n#[test]\nfn decode_first_video_frame_has_dimensions_and_content() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None);\n\td.open(\u0026s).expect(\"open video stream\");\n\n\tlet f = d\n\t\t.retrieve_video_frame(\u0026video_params(s, Rational::new(0, 1)))\n\t\t.expect(\"decode first frame\");\n\tassert_eq!(f.width(), 1920);\n\tassert_eq!(f.height(), 1080);\n\tassert_eq!(f.format(), PixelFormat::F32);\n\tassert!(f.is_allocated());\n\t// Expected size: 4 channels x 4 bytes, linesize 32-byte aligned.\n\tassert_eq!(f.allocated_size(), (16 * 1920) * 1080);\n\n\t// The frame must contain non-zero pixels.\n\tlet data = f.data().expect(\"allocated data\");\n\tassert!(data.iter().any(|\u0026b| b != 0), \"decoded frame is all zeros\");\n}\n\n#[test]\nfn decode_video_frame_at_midpoint() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None);\n\td.open(\u0026s).expect(\"open video stream\");\n\n\tlet f = d\n\t\t.retrieve_video_frame(\u0026video_params(s, Rational::new(8, 1)))\n\t\t.expect(\"decode mid frame\");\n\tassert_eq!(f.width(), 1920);\n\tassert_eq!(f.height(), 1080);\n\tassert!(f.data().unwrap().iter().any(|\u0026b| b != 0));\n}\n\n#[test]\nfn audio_decode_is_non_empty() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 1, None);\n\td.open(\u0026s).expect(\"open audio stream\");\n\n\t// One second of stereo at 48 kHz.\n\tlet mut dest = vec![0f32; 48000 * 2];\n\tlet status = d\n\t\t.retrieve_audio(\n\t\t\t\u0026mut dest,\n\t\t\t\u0026TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),\n\t\t\t48000,\n\t\t\t0x3, // stereo mask\n\t\t)\n\t\t.expect(\"retrieve audio\");\n\tassert_eq!(status, RetrieveAudioStatus::Success);\n\n\tlet peak = dest.iter().fold(0.0f32, |a, \u0026s| a.max(s.abs()));\n\tassert!(peak \u003e 0.0, \"decoded audio is all silence\");\n}\n\n#[test]\nfn encode_h264_roundtrip_to_tmp() {\n\tlet out = std::env::temp_dir().join(format!(\"oakcodec_roundtrip_{}.mp4\", std::process::id()));\n\tlet params = h264_params(\u0026out);\n\tlet out_str = out.to_str().expect(\"utf8 temp path\").to_string();\n\n\tlet e = create_from_params(\u0026params).expect(\"create ffmpeg encoder\");\n\tassert_eq!(e.id(), \"ffmpeg\");\n\te.configure(\u0026params).expect(\"configure\");\n\te.open().expect(\"open output\");\n\n\t// Encode 10 frames with a moving pattern.\n\tfor i in 0..10 {\n\t\tlet f = pattern_frame(i);\n\t\te.write_video(\u0026f).expect(\"write video frame\");\n\t}\n\te.flush().expect(\"flush\");\n\n\t// The output exists and has a plausible size.\n\tassert!(out.exists(), \"round-trip file was not created\");\n\tassert!(out.metadata().unwrap().len() \u003e 1000, \"round-trip file is empty\");\n\n\t// Probe the result: one 64x64 video stream.\n\tlet d = FFmpegDecoder::new();\n\tlet desc = d.probe(\u0026out_str, None).expect(\"probe round-trip output\");\n\tassert_eq!(desc.video_stream_count(), 1);\n\tlet vp = desc.get_video_stream(0).expect(\"video stream\");\n\tassert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 64);\n\tassert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 64);\n\n\t// Decode the first frame of the result.\n\tlet s = CodecStream::with_block(out_str.clone(), 0, None);\n\td.open(\u0026s).expect(\"open round-trip video\");\n\tlet f = d\n\t\t.retrieve_video_frame(\u0026video_params(s, Rational::new(0, 1)))\n\t\t.expect(\"decode round-trip first frame\");\n\tassert_eq!(f.width(), 64);\n\tassert_eq!(f.height(), 64);\n\tassert_eq!(f.format(), PixelFormat::F32);\n\tassert!(f.data().unwrap().iter().any(|\u0026b| b != 0));\n\n\tlet _ = std::fs::remove_file(\u0026out);\n}\n\n#[test]\nfn audio_conform_writes_planar_pcm() {\n\tlet d = FFmpegDecoder::new();\n\tlet s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 1, None);\n\td.open(\u0026s).expect(\"open audio stream\");\n\n\tlet dir = std::env::temp_dir().join(format!(\"oakcodec_conform_{}\", std::process::id()));\n\tstd::fs::create_dir_all(\u0026dir).unwrap();\n\tlet ch0 = dir.join(\"0.pcm\").to_string_lossy().into_owned();\n\tlet ch1 = dir.join(\"1.pcm\").to_string_lossy().into_owned();\n\n\td.conform_audio(\u0026[ch0.clone(), ch1.clone()], 48000, 0x3, 4, None)\n\t\t.expect(\"conform to f32 planar\");\n\n\tfor path in [\u0026ch0, \u0026ch1] {\n\t\tlet meta = std::fs::metadata(path).expect(\"conform output exists\");\n\t\tassert!(meta.len() \u003e 0, \"conform file is empty\");\n\t\t// 1 second at 48kHz * 4 bytes = 192 KB minimum.\n\t\tassert!(meta.len() \u003e= 192_000, \"conform file too short: {}\", meta.len());\n\t}\n\n\tlet _ = std::fs::remove_dir_all(\u0026dir);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","task.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! Background task submission hook (`include/codec/task.h`).\n//!\n//! The codec module needs occasional background work (audio conforms,\n//! proxy transcodes). The task system itself splits out at milestone M8;\n//! until then oakcodec exposes a single global submit callback. A host\n//! (M8: oaktask) registers with [`set_task_submit_cb`]; the conform/proxy\n//! managers call it whenever they need a task. With no callback, managers\n//! report work as unavailable — they never crash and never block.\n\nuse std::ffi::{c_void, CString};\nuse std::sync::Mutex;\n\nuse crate::error::{Error, OAKCODEC_OK};\n\n/// Kinds of background tasks oakcodec can request.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum TaskKind {\n\t/// Audio conform to pcm cache files.\n\tConform = 0,\n\t/// Video proxy transcode.\n\tProxy = 1,\n}\n\n/// Description of one background task request.\n///\n/// All strings are borrowed and only valid for the duration of the submit\n/// call; the callback must copy anything it retains.\n#[repr(C)]\npub struct TaskRequest\u003c'a\u003e {\n\t/// `TaskKind`.\n\tpub kind: TaskKind,\n\t/// Source media filename.\n\tpub input_filename: \u0026'a str,\n\t/// Final destination path (see field docs in include/codec/task.h).\n\tpub output_filename: \u0026'a str,\n\t/// Stream inside the source media.\n\tpub stream_index: i32,\n\t/// Conform: target sample rate.\n\tpub sample_rate: i32,\n\t/// Conform: target channel layout mask.\n\tpub channel_layout: u64,\n\t/// Conform: target sample format (enum as int).\n\tpub sample_format: i32,\n\t/// Proxy: target width (0 = unspecified/divider-based).\n\tpub proxy_width: i32,\n\t/// Proxy: target height (0 = unspecified/divider-based).\n\tpub proxy_height: i32,\n}\n\n/// Task submit callback signature.\n///\n/// Returns `Ok(())` if the task was accepted (completed synchronously or\n/// queued), `Err` if the request was rejected.\npub type TaskSubmitFn = dyn Fn(\u0026TaskRequest, *mut std::ffi::c_void) -\u003e crate::error::Result\u003c()\u003e;\n\n/// `OakCodecTaskRequest` — C ABI mirror of [`TaskRequest`] for the submit\n/// callback; see `include/codec/task.h`. Strings are borrowed C pointers,\n/// valid only for the duration of the call.\n#[repr(C)]\npub struct OakCodecTaskRequest {\n\t/// `TaskKind` value.\n\tpub kind: i32,\n\t/// Source media filename.\n\tpub input_filename: *const std::ffi::c_char,\n\t/// Final destination path.\n\tpub output_filename: *const std::ffi::c_char,\n\t/// Stream inside the source media.\n\tpub stream_index: i32,\n\t/// Conform: target sample rate.\n\tpub sample_rate: i32,\n\t/// Conform: target channel-layout mask.\n\tpub channel_layout: u64,\n\t/// Conform: target sample format (enum as int).\n\tpub sample_format: i32,\n\t/// Proxy: target width (0 = unspecified/divider-based).\n\tpub proxy_width: i32,\n\t/// Proxy: target height (0 = unspecified/divider-based).\n\tpub proxy_height: i32,\n}\n\n/// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see\n/// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a\n/// negative `OAKCODEC_E_*` code.\npub type OakCodecTaskSubmitFn = unsafe extern \"C\" fn(\n\treq: *const OakCodecTaskRequest,\n\tuserdata: *mut std::ffi::c_void,\n) -\u003e i32;\n\n/// One registered submit callback (extern-C from the host, or a crate\n/// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair.\nenum SubmitCb {\n\t/// No callback registered.\n\tNone,\n\t/// Extern-C callback registered via `oakcodec_set_task_submit_cb`.\n\tExtern {\n\t\t/// The C function pointer.\n\t\tcb: OakCodecTaskSubmitFn,\n\t\t/// Opaque userdata passed back on each call.\n\t\tuserdata: *mut c_void,\n\t},\n\t/// Crate-internal Rust closure registered via [`set_task_submit_cb`].\n\tRust {\n\t\t/// Raw fat-pointer to the `\u0026'static TaskSubmitFn` (kept `*const` so\n\t\t/// the registry is `Send`).\n\t\tcb: *const TaskSubmitFn,\n\t\t/// Opaque userdata passed back on each call.\n\t\tuserdata: *mut c_void,\n\t},\n}\n\n// # Safety: the stored pointers (extern-C fn pointer, fat pointer to a\n// 'static closure, userdata) are only dereferenced/called while holding the\n// registry mutex; the Rust closure is 'static and the extern-C fn outlives\n// registration by contract. Moving the enum between threads under the lock\n// therefore cannot alias.\nunsafe impl Send for SubmitCb {}\n\n/// The global task submit callback registry. Only one callback is held at\n/// a time; registering replaces it, `None` clears it. Thread-safe.\nstatic TASK_SUBMIT: Mutex\u003cSubmitCb\u003e = Mutex::new(SubmitCb::None);\n\n/// Registers (or replaces) the global task submit callback. Pass `None` to\n/// unregister. Thread-safe. Interim state (pre-M8): nobody registers and all\n/// task-dependent work reports unavailable.\npub fn set_task_submit_cb(cb: Option\u003c\u0026'static TaskSubmitFn\u003e, userdata: *mut std::ffi::c_void) {\n\tlet mut g = TASK_SUBMIT.lock().unwrap();\n\t*g = match cb {\n\t\tSome(cb) =\u003e SubmitCb::Rust {\n\t\t\tcb: cb as *const TaskSubmitFn,\n\t\t\tuserdata,\n\t\t},\n\t\tNone =\u003e SubmitCb::None,\n\t};\n}\n\n/// Register an extern-C submit callback (used by `ffi::task`).\n///\n/// Mirrors `oakcodec_set_task_submit_cb`: a `None` pointer clears it.\npub(crate) fn set_task_submit_cb_extern(\n\tcb: Option\u003cOakCodecTaskSubmitFn\u003e,\n\tuserdata: *mut std::ffi::c_void,\n) {\n\tlet mut g = TASK_SUBMIT.lock().unwrap();\n\t*g = match cb {\n\t\tSome(cb) =\u003e SubmitCb::Extern { cb, userdata },\n\t\tNone =\u003e SubmitCb::None,\n\t};\n}\n\n/// Returns 1 if a submit callback is currently registered, else 0.\n/// Thread-safe.\npub fn task_submit_is_registered() -\u003e bool {\n\tlet g = TASK_SUBMIT.lock().unwrap();\n\t!matches!(\u0026*g, SubmitCb::None)\n}\n\n/// Submit a task through the registered callback, if any.\n///\n/// Returns `Ok(false)` when no callback is registered (nothing submitted),\n/// `Ok(true)` when accepted, or `Err` when the callback rejected it.\npub fn submit_task(\n\treq: \u0026TaskRequest,\n) -\u003e crate::error::Result\u003cbool\u003e {\n\tlet g = TASK_SUBMIT.lock().unwrap();\n\tmatch \u0026*g {\n\t\tSubmitCb::None =\u003e Ok(false),\n\t\tSubmitCb::Extern { cb, userdata } =\u003e {\n\t\t\t// Bind the C strings to locals so the temporaries outlive the\n\t\t\t// callback call (their pointers feed the request struct).\n\t\t\tlet in_c = cstring_or_empty(req.input_filename);\n\t\t\tlet out_c = cstring_or_empty(req.output_filename);\n\t\t\tlet creq = OakCodecTaskRequest {\n\t\t\t\tkind: req.kind as i32,\n\t\t\t\tinput_filename: in_c.as_ptr(),\n\t\t\t\toutput_filename: out_c.as_ptr(),\n\t\t\t\tstream_index: req.stream_index,\n\t\t\t\tsample_rate: req.sample_rate,\n\t\t\t\tchannel_layout: req.channel_layout,\n\t\t\t\tsample_format: req.sample_format,\n\t\t\t\tproxy_width: req.proxy_width,\n\t\t\t\tproxy_height: req.proxy_height,\n\t\t\t};\n\t\t\t// # Safety: the callback is a C function we registered; passing a\n\t\t\t// request whose string pointers are alive for the call duration.\n\t\t\tlet ret = unsafe { cb(\u0026creq, *userdata) };\n\t\t\tif ret == OAKCODEC_OK {\n\t\t\t\tOk(true)\n\t\t\t} else {\n\t\t\t\tErr(Error::Failed(format!(\"task submit rejected (code {})\", ret)))\n\t\t\t}\n\t\t}\n\t\tSubmitCb::Rust { cb, userdata } =\u003e {\n\t\t\t// # Safety: the fat pointer was stored by set_task_submit_cb and\n\t\t\t// points to a 'static closure that outlives this call.\n\t\t\tlet cb = unsafe { \u0026**cb };\n\t\t\tmatch cb(req, *userdata) {\n\t\t\t\tOk(()) =\u003e Ok(true),\n\t\t\t\tErr(e) =\u003e Err(e),\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// Build a NUL-terminated C string from a Rust string; empty on embedded\n/// NUL (callers pass sane filenames, so this is defensive only).\nfn cstring_or_empty(s: \u0026str) -\u003e CString {\n\tCString::new(s).unwrap_or_else(|_| CString::new(\"\").unwrap())\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\t// Same registry lock the conform/proxy/ffi tests use: the submit\n\t// callback is process-global and every test that mutates it must\n\t// serialize on the same mutex.\n\tuse crate::conformmanager::test_util::REG_LOCK;\n\n\tunsafe extern \"C\" fn reject_cb(\n\t\t_req: *const OakCodecTaskRequest,\n\t\t_ud: *mut std::ffi::c_void,\n\t) -\u003e i32 {\n\t\t-1 // rejected\n\t}\n\n\t#[test]\n\tfn submit_via_rust_closure_and_clear() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\n\t\t// A Rust closure that accepts and records the request.\n\t\tlet accepted = std::sync::Arc::new(std::sync::Mutex::new(false));\n\t\tlet recorded = std::sync::Arc::new(std::sync::Mutex::new(None::\u003cString\u003e));\n\t\tlet acc = accepted.clone();\n\t\tlet rec = recorded.clone();\n\t\tlet cb: \u0026'static TaskSubmitFn = Box::leak(Box::new(\n\t\t\tmove |req: \u0026TaskRequest, _ud: *mut std::ffi::c_void| {\n\t\t\t\t*acc.lock().unwrap() = true;\n\t\t\t\t*rec.lock().unwrap() = Some(req.output_filename.to_string());\n\t\t\t\tOk(())\n\t\t\t},\n\t\t));\n\t\tset_task_submit_cb(Some(cb), std::ptr::null_mut());\n\t\tassert!(task_submit_is_registered());\n\n\t\tlet req = TaskRequest {\n\t\t\tkind: TaskKind::Conform,\n\t\t\tinput_filename: \"in.mp4\",\n\t\t\toutput_filename: \"out.pcm\",\n\t\t\tstream_index: 1,\n\t\t\tsample_rate: 48000,\n\t\t\tchannel_layout: 0x3,\n\t\t\tsample_format: 10,\n\t\t\tproxy_width: 0,\n\t\t\tproxy_height: 0,\n\t\t};\n\t\tassert!(submit_task(\u0026req).unwrap());\n\t\tassert!(*accepted.lock().unwrap());\n\t\tassert_eq!(recorded.lock().unwrap().as_deref(), Some(\"out.pcm\"));\n\n\t\t// Clearing the callback: nothing submitted.\n\t\tset_task_submit_cb(None, std::ptr::null_mut());\n\t\tassert!(!task_submit_is_registered());\n\t\tassert!(!submit_task(\u0026req).unwrap());\n\t}\n\n\t#[test]\n\tfn extern_cb_reject_maps_to_err() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\tset_task_submit_cb_extern(Some(reject_cb), std::ptr::null_mut());\n\t\tlet req = TaskRequest {\n\t\t\tkind: TaskKind::Proxy,\n\t\t\tinput_filename: \"in.mp4\",\n\t\t\toutput_filename: \"out.mp4\",\n\t\t\tstream_index: 0,\n\t\t\tsample_rate: 0,\n\t\t\tchannel_layout: 0,\n\t\t\tsample_format: 0,\n\t\t\tproxy_width: 1280,\n\t\t\tproxy_height: 720,\n\t\t};\n\t\tassert!(submit_task(\u0026req).is_err());\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n\n\t#[test]\n\tfn extern_cb_accept_returns_ok() {\n\t\tlet _g = REG_LOCK.lock().unwrap();\n\t\tset_task_submit_cb_extern(Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut());\n\t\tlet req = TaskRequest {\n\t\t\tkind: TaskKind::Conform,\n\t\t\tinput_filename: \"in.mp4\",\n\t\t\toutput_filename: \"out.pcm\",\n\t\t\tstream_index: 0,\n\t\t\tsample_rate: 0,\n\t\t\tchannel_layout: 0,\n\t\t\tsample_format: 0,\n\t\t\tproxy_width: 0,\n\t\t\tproxy_height: 0,\n\t\t};\n\t\tassert!(submit_task(\u0026req).unwrap());\n\t\tset_task_submit_cb_extern(None, std::ptr::null_mut());\n\t}\n\n\t#[test]\n\tfn cstring_or_empty_handles_embedded_nul() {\n\t\t// Embedded NUL -\u003e empty string (defensive).\n\t\tlet c = cstring_or_empty(\"a\\0b\");\n\t\tassert_eq!(c.as_c_str().to_bytes(), b\"\");\n\t\tassert_eq!(cstring_or_empty(\"ok\").as_c_str().to_bytes(), b\"ok\");\n\t}\n\n\t#[test]\n\tfn task_kind_values_match_abi() {\n\t\tassert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32);\n\t\tassert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32);\n\t}\n\n\t// ABI constants mirrored from include/codec/task.h.\n\tconst OAKCODEC_TASK_CONFORM: i32 = 0;\n\tconst OAKCODEC_TASK_PROXY: i32 = 1;\n}\n","traces":[{"line":142,"address":[],"length":0,"stats":{"Line":2}},{"line":143,"address":[],"length":0,"stats":{"Line":8}},{"line":144,"address":[],"length":0,"stats":{"Line":4}},{"line":145,"address":[],"length":0,"stats":{"Line":2}},{"line":146,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":156,"address":[],"length":0,"stats":{"Line":16}},{"line":160,"address":[],"length":0,"stats":{"Line":64}},{"line":161,"address":[],"length":0,"stats":{"Line":32}},{"line":162,"address":[],"length":0,"stats":{"Line":12}},{"line":163,"address":[],"length":0,"stats":{"Line":10}},{"line":169,"address":[],"length":0,"stats":{"Line":12}},{"line":170,"address":[],"length":0,"stats":{"Line":48}},{"line":171,"address":[],"length":0,"stats":{"Line":17}},{"line":178,"address":[],"length":0,"stats":{"Line":7}},{"line":181,"address":[],"length":0,"stats":{"Line":28}},{"line":182,"address":[],"length":0,"stats":{"Line":7}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":10}},{"line":187,"address":[],"length":0,"stats":{"Line":15}},{"line":188,"address":[],"length":0,"stats":{"Line":15}},{"line":190,"address":[],"length":0,"stats":{"Line":10}},{"line":191,"address":[],"length":0,"stats":{"Line":10}},{"line":192,"address":[],"length":0,"stats":{"Line":10}},{"line":193,"address":[],"length":0,"stats":{"Line":10}},{"line":194,"address":[],"length":0,"stats":{"Line":10}},{"line":195,"address":[],"length":0,"stats":{"Line":10}},{"line":196,"address":[],"length":0,"stats":{"Line":10}},{"line":197,"address":[],"length":0,"stats":{"Line":5}},{"line":198,"address":[],"length":0,"stats":{"Line":5}},{"line":202,"address":[],"length":0,"stats":{"Line":15}},{"line":203,"address":[],"length":0,"stats":{"Line":5}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":206,"address":[],"length":0,"stats":{"Line":1}},{"line":209,"address":[],"length":0,"stats":{"Line":2}},{"line":212,"address":[],"length":0,"stats":{"Line":2}},{"line":213,"address":[],"length":0,"stats":{"Line":2}},{"line":214,"address":[],"length":0,"stats":{"Line":1}},{"line":215,"address":[],"length":0,"stats":{"Line":0}},{"line":223,"address":[],"length":0,"stats":{"Line":12}},{"line":224,"address":[],"length":0,"stats":{"Line":38}}],"covered":41,"coverable":42},{"path":["/","Users","sunyu","Projects","oak","src","codec","rust","src","timecodemetadata.rs"],"content":"// Oak Video Editor - Non-Linear Video Editor\n// Copyright (C) 2026 Oak Team\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with this program. If not, see \u003chttp://www.gnu.org/licenses/\u003e.\n\n//! `olive::TimecodeMetadata` — parse source timecode strings.\n//!\n//! Mirrors `src/codec/src/timecodemetadata.h`: parse an SMPTE timecode\n//! string or a BWF `time reference` chunk into a rational media timestamp.\n\nuse oakcore_rs::Rational;\n\n/// `TimecodeMetadata::SourceTime` — the parsed result.\npub struct SourceTime {\n\t/// Media timestamp, rational seconds.\n\tpub time: Rational,\n\t/// Source string (normalized form, or the raw input on failure).\n\tpub source: String,\n\t/// Parse succeeded.\n\tpub valid: bool,\n}\n\n/// Trim the same whitespace set as the C++ `trimmed()` helper\n/// (`\" \\t\\n\\r\\f\\v\"`): space, tab, LF, CR, form feed, vertical tab.\nfn trimmed(s: \u0026str) -\u003e String {\n\ts.trim_matches(|c: char| {\n\t\tc == ' ' || c == '\\t' || c == '\\n' || c == '\\r' || c == '\\u{000c}' || c == '\\u{000b}'\n\t})\n\t.to_string()\n}\n\n/// Port of `str_to_int64_empty_tolerant`: an empty field is a valid `0`,\n/// any other unparseable field is an error (`None`).\nfn str_to_int64_empty_tolerant(s: \u0026str) -\u003e Option\u003ci64\u003e {\n\tif s.is_empty() {\n\t\tSome(0)\n\t} else {\n\t\ts.parse::\u003ci64\u003e().ok()\n\t}\n}\n\n/// `std::llround` (round half away from zero, truncate to `i64`).\nfn llround(x: f64) -\u003e i64 {\n\tx.round() as i64\n}\n\n/// `timebase.flipped().to_double()` — seconds-per-frame to frames-per-second.\nfn frame_rate(timebase: \u0026Rational) -\u003e f64 {\n\ttimebase.denominator() as f64 / timebase.numerator() as f64\n}\n\n/// Port of `olive::core::Timecode::timecode_to_time` for the\n/// `k_timecode_non_drop_frame` / `k_timecode_drop_frame` displays.\n///\n/// `drop_frame` is true when the (already trimmed) string contains a `;`.\n/// Returns `None` on any parse failure.\nfn timecode_to_time(timecode: \u0026str, timebase: \u0026Rational, drop_frame: bool) -\u003e Option\u003cRational\u003e {\n\tlet mut tokens: Vec\u003c\u0026str\u003e = timecode.split(|c| c == ':' || c == ';').collect();\n\tlet element_count = 4;\n\n\t// Keep only the leading `HH:MM:SS:FF` tokens.\n\tif tokens.len() \u003e element_count {\n\t\ttokens.truncate(element_count);\n\t}\n\t// Pad missing leading fields with empty strings (which parse to 0).\n\twhile tokens.len() \u003c element_count {\n\t\ttokens.insert(0, \"\");\n\t}\n\n\tlet negative = timecode.starts_with('-');\n\n\tlet hours = str_to_int64_empty_tolerant(tokens[0])?;\n\tlet mins = str_to_int64_empty_tolerant(tokens[1])?;\n\tlet secs = str_to_int64_empty_tolerant(tokens[2])?;\n\tlet frames = str_to_int64_empty_tolerant(tokens[3])?;\n\n\tlet fr = frame_rate(timebase);\n\tlet rounded_frame_rate = llround(fr);\n\n\tlet sec_count = hours * 3600 + mins * 60 + secs;\n\tlet mut frame_count = sec_count * rounded_frame_rate + frames;\n\n\tif drop_frame \u0026\u0026 timebase.numerator() != 1 {\n\t\t// `timebase_is_drop_frame(timebase)`: numerator != 1.\n\t\t// Number of frames dropped on the minute marks ≈ 6% of the framerate.\n\t\tlet drop_frames = llround(fr * (2.0 / 30.0));\n\n\t\t// `d` and `m` are derived from the real (non-rounded) framerate.\n\t\tlet real_fr_ts = llround(sec_count as f64 * fr) + frames;\n\n\t\tlet frames_per10_minutes = llround(fr * 600.0);\n\t\tlet d = real_fr_ts / frames_per10_minutes;\n\t\tlet m = real_fr_ts % frames_per10_minutes;\n\n\t\tif m \u003e drop_frames {\n\t\t\tframe_count -= drop_frames\n\t\t\t\t* ((m - drop_frames) / (llround(fr) * 60 - drop_frames));\n\t\t}\n\t\tframe_count -= drop_frames * 9 * d;\n\t}\n\n\t// `timestamp_to_time`: `timebase.num * frame_count / timebase.den`, reduced.\n\tlet mut time = timebase.timestamp_to_time(frame_count);\n\n\tif negative {\n\t\ttime = time * Rational::new(-1, 1);\n\t}\n\n\tSome(time)\n}\n\n/// Signed Euclidean GCD on absolute values (mirrors `i64_gcd`).\nfn gcd_u64(mut a: u64, mut b: u64) -\u003e u64 {\n\twhile b != 0 {\n\t\tlet t = a % b;\n\t\ta = b;\n\t\tb = t;\n\t}\n\ta\n}\n\nimpl SourceTime {\n\t/// New invalid (empty) source time.\n\tpub fn invalid() -\u003e Self {\n\t\tSourceTime {\n\t\t\ttime: Rational::NULL,\n\t\t\tsource: String::new(),\n\t\t\tvalid: false,\n\t\t}\n\t}\n\n\t/// Parse an SMPTE timecode string at the given timebase\n\t/// (`from_timecode_string`).\n\t///\n\t/// The string is trimmed; an empty result is invalid. A `;` separator\n\t/// selects drop-frame, otherwise non-drop-frame. On any parse failure\n\t/// the result is invalid and `source` holds the trimmed raw input.\n\tpub fn from_timecode_string(timecode: \u0026str, timebase: \u0026Rational) -\u003e SourceTime {\n\t\tlet trimmed_tc = trimmed(timecode);\n\t\tif trimmed_tc.is_empty() {\n\t\t\treturn SourceTime::invalid();\n\t\t}\n\n\t\tlet drop_frame = trimmed_tc.contains(';');\n\t\tmatch timecode_to_time(\u0026trimmed_tc, timebase, drop_frame) {\n\t\t\tSome(time) =\u003e SourceTime {\n\t\t\t\ttime,\n\t\t\t\tsource: \"timecode\".to_string(),\n\t\t\t\tvalid: true,\n\t\t\t},\n\t\t\tNone =\u003e SourceTime {\n\t\t\t\ttime: Rational::NULL,\n\t\t\t\tsource: trimmed_tc,\n\t\t\t\tvalid: false,\n\t\t\t},\n\t\t}\n\t}\n\n\t/// Parse a BWF `time reference` chunk into a timestamp\n\t/// (`from_bwf_time_reference`).\n\t///\n\t/// A non-positive `sample_rate`, or a string that is not a single\n\t/// base-10 unsigned integer, yields an invalid result. The parsed\n\t/// sample count over `sample_rate` is reduced by their GCD; if the\n\t/// reduced numerator or denominator exceed `i32::MAX` the value falls\n\t/// back to the (capped, reduced) `Rational::new(samples, sample_rate)`\n\t/// approximation, since `oakcore_rs::Rational` exposes no `from_double`.\n\tpub fn from_bwf_time_reference(time_reference: \u0026str, sample_rate: i32) -\u003e SourceTime {\n\t\tif sample_rate \u003c= 0 {\n\t\t\treturn SourceTime::invalid();\n\t\t}\n\n\t\tlet trimmed_ref = trimmed(time_reference);\n\t\t// `std::strtoull` base 10 with a \"whole string consumed\" check:\n\t\t// at least one digit, no leading/trailing junk.\n\t\tlet samples: u64 = match trimmed_ref.parse() {\n\t\t\tOk(v) =\u003e v,\n\t\t\tErr(_) =\u003e return SourceTime::invalid(),\n\t\t};\n\n\t\tlet mut numerator = samples;\n\t\tlet mut denominator = sample_rate as u64;\n\t\tlet divisor = gcd_u64(numerator, denominator);\n\t\tnumerator /= divisor;\n\t\tdenominator /= divisor;\n\n\t\tlet rational_limit = i32::MAX as u64;\n\t\tlet time = if numerator \u003c= rational_limit \u0026\u0026 denominator \u003c= rational_limit {\n\t\t\tRational::new(numerator as i64, denominator as i64)\n\t\t} else {\n\t\t\t// `Rational::from_double` is not part of the oakcore_rs public\n\t\t\t// API; `Rational::new` applies the same INT_MAX-capped reduction\n\t\t\t// (FFmpeg `av_reduce`), which is the intended approximation.\n\t\t\tlet n = i64::try_from(samples).unwrap_or(i64::MAX);\n\t\t\tRational::new(n, sample_rate as i64)\n\t\t};\n\n\t\tSourceTime {\n\t\t\ttime,\n\t\t\tsource: \"bwf_time_reference\".to_string(),\n\t\t\tvalid: true,\n\t\t}\n\t}\n}\n\n#[cfg(test)]\nmod tests {\n\tuse super::*;\n\n\tfn tc(s: \u0026str, timebase: \u0026Rational) -\u003e SourceTime {\n\t\tSourceTime::from_timecode_string(s, timebase)\n\t}\n\n\t#[test]\n\tfn invalid_is_null() {\n\t\tlet inv = SourceTime::invalid();\n\t\tassert!(inv.time.is_null());\n\t\tassert!(inv.source.is_empty());\n\t\tassert!(!inv.valid);\n\t}\n\n\t#[test]\n\tfn empty_or_whitespace_is_invalid() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tassert!(!tc(\"\", \u0026tb).valid);\n\t\tassert!(!tc(\" \\t\\n\", \u0026tb).valid);\n\t}\n\n\t#[test]\n\tfn non_drop_integral_fps() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"00:00:00:10\", \u0026tb);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.source, \"timecode\");\n\t\tassert_eq!(r.time, Rational::new(2, 5)); // 10 frames @ 25fps = 0.4s\n\t}\n\n\t#[test]\n\tfn non_drop_leading_zeros_and_whitespace() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\" 01:02:03:04 \", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// 1*3600+2*60+3 = 3723s * 25 + 4 frames = 93079 frames\n\t\tassert_eq!(r.time, Rational::new(93079, 25));\n\t}\n\n\t#[test]\n\tfn missing_leading_fields_pad_from_front() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"00:01:02\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// C++ pads missing leading fields at the front, so 3 fields become\n\t\t// [\"\",00,01,02] = HH=0, MM=0, SS=1, FF=2 -\u003e 27 frames @25fps.\n\t\tassert_eq!(r.time, Rational::new(27, 25));\n\t}\n\n\t#[test]\n\tfn negative_timecode_is_negated() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"-00:00:00:05\", \u0026tb);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.time, Rational::new(-1, 5));\n\t}\n\n\t#[test]\n\tfn parse_failure_is_invalid_with_raw_source() {\n\t\tlet tb = Rational::new(1, 25);\n\t\tlet r = tc(\"abc:def\", \u0026tb);\n\t\tassert!(!r.valid);\n\t\tassert!(r.time.is_null());\n\t\tassert_eq!(r.source, \"abc:def\");\n\t}\n\n\t#[test]\n\tfn drop_frame_29_97_first_minute_no_correction() {\n\t\t// 29.97fps -\u003e timebase 1001/30000.\n\t\tlet tb = Rational::new(1001, 30000);\n\t\tlet r = tc(\"00:01:00;00\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// No frames dropped in the first minute: `frame_count` stays at\n\t\t// 60s * 30fps = 1800 frames (a minute boundary at 29.97 is 60.06s).\n\t\tassert_eq!(r.time, tb.timestamp_to_time(1800));\n\t}\n\n\t#[test]\n\tfn drop_frame_29_97_later_minute_corrects() {\n\t\tlet tb = Rational::new(1001, 30000);\n\t\t// At 10 minutes of drop-frame timecode the running correction is\n\t\t// 2 frames dropped per minute for 9 of the 10 minutes (18 frames).\n\t\tlet r = tc(\"00:10:00;00\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// NDF would be 10*60*30 = 18000 frames; 18 dropped -\u003e 17982 frames.\n\t\tassert_eq!(r.time, tb.timestamp_to_time(17982));\n\t}\n\n\t#[test]\n\tfn non_drop_uses_colon() {\n\t\tlet tb = Rational::new(1001, 30000);\n\t\tlet r = tc(\"00:10:00:00\", \u0026tb);\n\t\tassert!(r.valid);\n\t\t// Non-drop: 18000 frames, no correction.\n\t\tassert_eq!(r.time, tb.timestamp_to_time(18000));\n\t}\n\n\t#[test]\n\tfn bwf_valid_reduces() {\n\t\tlet r = SourceTime::from_bwf_time_reference(\"48000\", 48000);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.source, \"bwf_time_reference\");\n\t\tassert_eq!(r.time, Rational::new(1, 1));\n\t}\n\n\t#[test]\n\tfn bwf_valid_whitespace_trimmed() {\n\t\tlet r = SourceTime::from_bwf_time_reference(\" 24000 \", 48000);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.time, Rational::new(1, 2));\n\t}\n\n\t#[test]\n\tfn bwf_bad_sample_rate_is_invalid() {\n\t\tfor sr in [0, -1] {\n\t\t\tlet r = SourceTime::from_bwf_time_reference(\"100\", sr);\n\t\t\tassert!(!r.valid);\n\t\t\tassert!(r.time.is_null());\n\t\t}\n\t}\n\n\t#[test]\n\tfn bwf_unparseable_is_invalid() {\n\t\tfor s in [\"\", \" \", \"abc\", \"12x\", \"1.5\", \"-5\", \"1 2\"] {\n\t\t\tlet r = SourceTime::from_bwf_time_reference(s, 48000);\n\t\t\tassert!(!r.valid, \"should reject {:?}\", s);\n\t\t\tassert!(r.time.is_null());\n\t\t}\n\t}\n\n\t#[test]\n\tfn bwf_no_common_divisor() {\n\t\tlet r = SourceTime::from_bwf_time_reference(\"3\", 48000);\n\t\tassert!(r.valid);\n\t\tassert_eq!(r.time, Rational::new(1, 16000));\n\t}\n}\n","traces":[{"line":36,"address":[],"length":0,"stats":{"Line":21}},{"line":37,"address":[],"length":0,"stats":{"Line":91}},{"line":38,"address":[],"length":0,"stats":{"Line":217}},{"line":45,"address":[],"length":0,"stats":{"Line":35}},{"line":46,"address":[],"length":0,"stats":{"Line":70}},{"line":47,"address":[],"length":0,"stats":{"Line":3}},{"line":49,"address":[],"length":0,"stats":{"Line":96}},{"line":54,"address":[],"length":0,"stats":{"Line":15}},{"line":55,"address":[],"length":0,"stats":{"Line":15}},{"line":59,"address":[],"length":0,"stats":{"Line":8}},{"line":60,"address":[],"length":0,"stats":{"Line":16}},{"line":68,"address":[],"length":0,"stats":{"Line":9}},{"line":69,"address":[],"length":0,"stats":{"Line":209}},{"line":70,"address":[],"length":0,"stats":{"Line":18}},{"line":73,"address":[],"length":0,"stats":{"Line":18}},{"line":74,"address":[],"length":0,"stats":{"Line":0}},{"line":77,"address":[],"length":0,"stats":{"Line":27}},{"line":78,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":27}},{"line":83,"address":[],"length":0,"stats":{"Line":27}},{"line":84,"address":[],"length":0,"stats":{"Line":27}},{"line":85,"address":[],"length":0,"stats":{"Line":27}},{"line":86,"address":[],"length":0,"stats":{"Line":24}},{"line":88,"address":[],"length":0,"stats":{"Line":24}},{"line":89,"address":[],"length":0,"stats":{"Line":24}},{"line":91,"address":[],"length":0,"stats":{"Line":16}},{"line":92,"address":[],"length":0,"stats":{"Line":16}},{"line":94,"address":[],"length":0,"stats":{"Line":10}},{"line":97,"address":[],"length":0,"stats":{"Line":6}},{"line":100,"address":[],"length":0,"stats":{"Line":6}},{"line":102,"address":[],"length":0,"stats":{"Line":6}},{"line":103,"address":[],"length":0,"stats":{"Line":4}},{"line":104,"address":[],"length":0,"stats":{"Line":4}},{"line":106,"address":[],"length":0,"stats":{"Line":3}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":2}},{"line":110,"address":[],"length":0,"stats":{"Line":2}},{"line":114,"address":[],"length":0,"stats":{"Line":32}},{"line":116,"address":[],"length":0,"stats":{"Line":9}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":120,"address":[],"length":0,"stats":{"Line":8}},{"line":124,"address":[],"length":0,"stats":{"Line":3}},{"line":125,"address":[],"length":0,"stats":{"Line":13}},{"line":126,"address":[],"length":0,"stats":{"Line":15}},{"line":127,"address":[],"length":0,"stats":{"Line":5}},{"line":128,"address":[],"length":0,"stats":{"Line":5}},{"line":130,"address":[],"length":0,"stats":{"Line":3}},{"line":135,"address":[],"length":0,"stats":{"Line":12}},{"line":138,"address":[],"length":0,"stats":{"Line":12}},{"line":149,"address":[],"length":0,"stats":{"Line":11}},{"line":150,"address":[],"length":0,"stats":{"Line":33}},{"line":151,"address":[],"length":0,"stats":{"Line":22}},{"line":152,"address":[],"length":0,"stats":{"Line":2}},{"line":155,"address":[],"length":0,"stats":{"Line":18}},{"line":156,"address":[],"length":0,"stats":{"Line":27}},{"line":159,"address":[],"length":0,"stats":{"Line":8}},{"line":179,"address":[],"length":0,"stats":{"Line":12}},{"line":180,"address":[],"length":0,"stats":{"Line":12}},{"line":181,"address":[],"length":0,"stats":{"Line":2}},{"line":184,"address":[],"length":0,"stats":{"Line":30}},{"line":187,"address":[],"length":0,"stats":{"Line":16}},{"line":188,"address":[],"length":0,"stats":{"Line":6}},{"line":189,"address":[],"length":0,"stats":{"Line":7}},{"line":192,"address":[],"length":0,"stats":{"Line":6}},{"line":193,"address":[],"length":0,"stats":{"Line":6}},{"line":194,"address":[],"length":0,"stats":{"Line":12}},{"line":195,"address":[],"length":0,"stats":{"Line":3}},{"line":196,"address":[],"length":0,"stats":{"Line":3}},{"line":198,"address":[],"length":0,"stats":{"Line":6}},{"line":199,"address":[],"length":0,"stats":{"Line":9}},{"line":200,"address":[],"length":0,"stats":{"Line":9}},{"line":205,"address":[],"length":0,"stats":{"Line":0}},{"line":206,"address":[],"length":0,"stats":{"Line":0}},{"line":211,"address":[],"length":0,"stats":{"Line":3}}],"covered":71,"coverable":74}]};
</script>
<script crossorigin>/** @license React v16.13.1
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';(function(d,r){"object"===typeof exports&&"undefined"!==typeof module?r(exports):"function"===typeof define&&define.amd?define(["exports"],r):(d=d||self,r(d.React={}))})(this,function(d){function r(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function w(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function da(){}function L(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function ea(a,b,c){var g,e={},fa=null,d=null;if(null!=b)for(g in void 0!==b.ref&&(d=b.ref),void 0!==b.key&&(fa=""+b.key),b)ha.call(b,g)&&!ia.hasOwnProperty(g)&&(e[g]=b[g]);var h=arguments.length-2;if(1===h)e.children=c;else if(1<h){for(var k=Array(h),f=0;f<h;f++)k[f]=arguments[f+2];e.children=k}if(a&&a.defaultProps)for(g in h=a.defaultProps,
h)void 0===e[g]&&(e[g]=h[g]);return{$$typeof:x,type:a,key:fa,ref:d,props:e,_owner:M.current}}function va(a,b){return{$$typeof:x,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function N(a){return"object"===typeof a&&null!==a&&a.$$typeof===x}function wa(a){var b={"=":"=0",":":"=2"};return"$"+(""+a).replace(/[=:]/g,function(a){return b[a]})}function ja(a,b,c,g){if(C.length){var e=C.pop();e.result=a;e.keyPrefix=b;e.func=c;e.context=g;e.count=0;return e}return{result:a,keyPrefix:b,func:c,
context:g,count:0}}function ka(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>C.length&&C.push(a)}function O(a,b,c,g){var e=typeof a;if("undefined"===e||"boolean"===e)a=null;var d=!1;if(null===a)d=!0;else switch(e){case "string":case "number":d=!0;break;case "object":switch(a.$$typeof){case x:case xa:d=!0}}if(d)return c(g,a,""===b?"."+P(a,0):b),1;d=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f<a.length;f++){e=a[f];var h=b+P(e,f);d+=O(e,h,c,g)}else if(null===a||
"object"!==typeof a?h=null:(h=la&&a[la]||a["@@iterator"],h="function"===typeof h?h:null),"function"===typeof h)for(a=h.call(a),f=0;!(e=a.next()).done;)e=e.value,h=b+P(e,f++),d+=O(e,h,c,g);else if("object"===e)throw c=""+a,Error(r(31,"[object Object]"===c?"object with keys {"+Object.keys(a).join(", ")+"}":c,""));return d}function Q(a,b,c){return null==a?0:O(a,"",b,c)}function P(a,b){return"object"===typeof a&&null!==a&&null!=a.key?wa(a.key):b.toString(36)}function ya(a,b,c){a.func.call(a.context,b,
a.count++)}function za(a,b,c){var g=a.result,e=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?R(a,g,c,function(a){return a}):null!=a&&(N(a)&&(a=va(a,e+(!a.key||b&&b.key===a.key?"":(""+a.key).replace(ma,"$&/")+"/")+c)),g.push(a))}function R(a,b,c,g,e){var d="";null!=c&&(d=(""+c).replace(ma,"$&/")+"/");b=ja(b,d,g,e);Q(a,za,b);ka(b)}function t(){var a=na.current;if(null===a)throw Error(r(321));return a}function S(a,b){var c=a.length;a.push(b);a:for(;;){var g=c-1>>>1,e=a[g];if(void 0!==
e&&0<D(e,b))a[g]=b,a[c]=e,c=g;else break a}}function n(a){a=a[0];return void 0===a?null:a}function E(a){var b=a[0];if(void 0!==b){var c=a.pop();if(c!==b){a[0]=c;a:for(var g=0,e=a.length;g<e;){var d=2*(g+1)-1,f=a[d],h=d+1,k=a[h];if(void 0!==f&&0>D(f,c))void 0!==k&&0>D(k,f)?(a[g]=k,a[h]=c,g=h):(a[g]=f,a[d]=c,g=d);else if(void 0!==k&&0>D(k,c))a[g]=k,a[h]=c,g=h;else break a}}return b}return null}function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function F(a){for(var b=n(u);null!==
b;){if(null===b.callback)E(u);else if(b.startTime<=a)E(u),b.sortIndex=b.expirationTime,S(p,b);else break;b=n(u)}}function T(a){y=!1;F(a);if(!v)if(null!==n(p))v=!0,z(U);else{var b=n(u);null!==b&&G(T,b.startTime-a)}}function U(a,b){v=!1;y&&(y=!1,V());H=!0;var c=m;try{F(b);for(l=n(p);null!==l&&(!(l.expirationTime>b)||a&&!W());){var g=l.callback;if(null!==g){l.callback=null;m=l.priorityLevel;var e=g(l.expirationTime<=b);b=q();"function"===typeof e?l.callback=e:l===n(p)&&E(p);F(b)}else E(p);l=n(p)}if(null!==
l)var d=!0;else{var f=n(u);null!==f&&G(T,f.startTime-b);d=!1}return d}finally{l=null,m=c,H=!1}}function oa(a){switch(a){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1E4;default:return 5E3}}var f="function"===typeof Symbol&&Symbol.for,x=f?Symbol.for("react.element"):60103,xa=f?Symbol.for("react.portal"):60106,Aa=f?Symbol.for("react.fragment"):60107,Ba=f?Symbol.for("react.strict_mode"):60108,Ca=f?Symbol.for("react.profiler"):60114,Da=f?Symbol.for("react.provider"):60109,
Ea=f?Symbol.for("react.context"):60110,Fa=f?Symbol.for("react.forward_ref"):60112,Ga=f?Symbol.for("react.suspense"):60113,Ha=f?Symbol.for("react.memo"):60115,Ia=f?Symbol.for("react.lazy"):60116,la="function"===typeof Symbol&&Symbol.iterator,pa=Object.getOwnPropertySymbols,Ja=Object.prototype.hasOwnProperty,Ka=Object.prototype.propertyIsEnumerable,I=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=0;10>a;a++)b["_"+
String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var g,e=1;e<arguments.length;e++){var d=Object(arguments[e]);
for(var f in d)Ja.call(d,f)&&(c[f]=d[f]);if(pa){g=pa(d);for(var h=0;h<g.length;h++)Ka.call(d,g[h])&&(c[g[h]]=d[g[h]])}}return c},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,d){},enqueueSetState:function(a,b,c,d){}},ba={};w.prototype.isReactComponent={};w.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error(r(85));this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=
function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};da.prototype=w.prototype;f=L.prototype=new da;f.constructor=L;I(f,w.prototype);f.isPureReactComponent=!0;var M={current:null},ha=Object.prototype.hasOwnProperty,ia={key:!0,ref:!0,__self:!0,__source:!0},ma=/\/+/g,C=[],na={current:null},X;if("undefined"===typeof window||"function"!==typeof MessageChannel){var A=null,qa=null,ra=function(){if(null!==A)try{var a=q();A(!0,a);A=null}catch(b){throw setTimeout(ra,0),b;}},La=Date.now();var q=
function(){return Date.now()-La};var z=function(a){null!==A?setTimeout(z,0,a):(A=a,setTimeout(ra,0))};var G=function(a,b){qa=setTimeout(a,b)};var V=function(){clearTimeout(qa)};var W=function(){return!1};f=X=function(){}}else{var Y=window.performance,sa=window.Date,Ma=window.setTimeout,Na=window.clearTimeout;"undefined"!==typeof console&&(f=window.cancelAnimationFrame,"function"!==typeof window.requestAnimationFrame&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),
"function"!==typeof f&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));if("object"===typeof Y&&"function"===typeof Y.now)q=function(){return Y.now()};else{var Oa=sa.now();q=function(){return sa.now()-Oa}}var J=!1,K=null,Z=-1,ta=5,ua=0;W=function(){return q()>=ua};f=function(){};X=function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing framerates higher than 125 fps is not unsupported"):
ta=0<a?Math.floor(1E3/a):5};var B=new MessageChannel,aa=B.port2;B.port1.onmessage=function(){if(null!==K){var a=q();ua=a+ta;try{K(!0,a)?aa.postMessage(null):(J=!1,K=null)}catch(b){throw aa.postMessage(null),b;}}else J=!1};z=function(a){K=a;J||(J=!0,aa.postMessage(null))};G=function(a,b){Z=Ma(function(){a(q())},b)};V=function(){Na(Z);Z=-1}}var p=[],u=[],Pa=1,l=null,m=3,H=!1,v=!1,y=!1,Qa=0;B={ReactCurrentDispatcher:na,ReactCurrentOwner:M,IsSomeRendererActing:{current:!1},assign:I};I(B,{Scheduler:{__proto__:null,
unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=m;m=a;try{return b()}finally{m=c}},unstable_next:function(a){switch(m){case 1:case 2:case 3:var b=3;break;default:b=m}var c=m;m=b;try{return a()}finally{m=c}},unstable_scheduleCallback:function(a,b,c){var d=q();if("object"===typeof c&&null!==c){var e=c.delay;
e="number"===typeof e&&0<e?d+e:d;c="number"===typeof c.timeout?c.timeout:oa(a)}else c=oa(a),e=d;c=e+c;a={id:Pa++,callback:b,priorityLevel:a,startTime:e,expirationTime:c,sortIndex:-1};e>d?(a.sortIndex=e,S(u,a),null===n(p)&&a===n(u)&&(y?V():y=!0,G(T,e-d))):(a.sortIndex=c,S(p,a),v||H||(v=!0,z(U)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=m;return function(){var c=m;m=b;try{return a.apply(this,arguments)}finally{m=c}}},unstable_getCurrentPriorityLevel:function(){return m},
unstable_shouldYield:function(){var a=q();F(a);var b=n(p);return b!==l&&null!==l&&null!==b&&null!==b.callback&&b.startTime<=a&&b.expirationTime<l.expirationTime||W()},unstable_requestPaint:f,unstable_continueExecution:function(){v||H||(v=!0,z(U))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return n(p)},get unstable_now(){return q},get unstable_forceFrameRate(){return X},unstable_Profiling:null},SchedulerTracing:{__proto__:null,__interactionsRef:null,__subscriberRef:null,
unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Qa},unstable_trace:function(a,b,c){return c()},unstable_wrap:function(a){return a},unstable_subscribe:function(a){},unstable_unsubscribe:function(a){}}});d.Children={map:function(a,b,c){if(null==a)return a;var d=[];R(a,d,null,b,c);return d},forEach:function(a,b,c){if(null==a)return a;b=ja(null,null,b,c);Q(a,ya,b);ka(b)},count:function(a){return Q(a,function(){return null},null)},
toArray:function(a){var b=[];R(a,b,null,function(a){return a});return b},only:function(a){if(!N(a))throw Error(r(143));return a}};d.Component=w;d.Fragment=Aa;d.Profiler=Ca;d.PureComponent=L;d.StrictMode=Ba;d.Suspense=Ga;d.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=B;d.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error(r(267,a));var d=I({},a.props),e=a.key,f=a.ref,m=a._owner;if(null!=b){void 0!==b.ref&&(f=b.ref,m=M.current);void 0!==b.key&&(e=""+b.key);if(a.type&&a.type.defaultProps)var h=
a.type.defaultProps;for(k in b)ha.call(b,k)&&!ia.hasOwnProperty(k)&&(d[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)d.children=c;else if(1<k){h=Array(k);for(var l=0;l<k;l++)h[l]=arguments[l+2];d.children=h}return{$$typeof:x,type:a.type,key:e,ref:f,props:d,_owner:m}};d.createContext=function(a,b){void 0===b&&(b=null);a={$$typeof:Ea,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Da,_context:a};return a.Consumer=
a};d.createElement=ea;d.createFactory=function(a){var b=ea.bind(null,a);b.type=a;return b};d.createRef=function(){return{current:null}};d.forwardRef=function(a){return{$$typeof:Fa,render:a}};d.isValidElement=N;d.lazy=function(a){return{$$typeof:Ia,_ctor:a,_status:-1,_result:null}};d.memo=function(a,b){return{$$typeof:Ha,type:a,compare:void 0===b?null:b}};d.useCallback=function(a,b){return t().useCallback(a,b)};d.useContext=function(a,b){return t().useContext(a,b)};d.useDebugValue=function(a,b){};
d.useEffect=function(a,b){return t().useEffect(a,b)};d.useImperativeHandle=function(a,b,c){return t().useImperativeHandle(a,b,c)};d.useLayoutEffect=function(a,b){return t().useLayoutEffect(a,b)};d.useMemo=function(a,b){return t().useMemo(a,b)};d.useReducer=function(a,b,c){return t().useReducer(a,b,c)};d.useRef=function(a){return t().useRef(a)};d.useState=function(a){return t().useState(a)};d.version="16.13.1"});
</script>
<script crossorigin>/** @license React v16.13.1
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*
Modernizr 3.0.0pre (Custom Build) | MIT
*/
'use strict';(function(I,ea){"object"===typeof exports&&"undefined"!==typeof module?ea(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],ea):(I=I||self,ea(I.ReactDOM={},I.React))})(this,function(I,ea){function k(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function ji(a,b,c,d,e,f,g,h,m){yb=!1;gc=null;ki.apply(li,arguments)}function mi(a,b,c,d,e,f,g,h,m){ji.apply(this,arguments);if(yb){if(yb){var n=gc;yb=!1;gc=null}else throw Error(k(198));hc||(hc=!0,pd=n)}}function lf(a,b,c){var d=a.type||"unknown-event";a.currentTarget=mf(c);mi(d,b,void 0,a);a.currentTarget=null}function nf(){if(ic)for(var a in cb){var b=cb[a],c=ic.indexOf(a);if(!(-1<c))throw Error(k(96,a));if(!jc[c]){if(!b.extractEvents)throw Error(k(97,a));jc[c]=b;c=b.eventTypes;for(var d in c){var e=
void 0;var f=c[d],g=b,h=d;if(qd.hasOwnProperty(h))throw Error(k(99,h));qd[h]=f;var m=f.phasedRegistrationNames;if(m){for(e in m)m.hasOwnProperty(e)&&of(m[e],g,h);e=!0}else f.registrationName?(of(f.registrationName,g,h),e=!0):e=!1;if(!e)throw Error(k(98,d,a));}}}}function of(a,b,c){if(db[a])throw Error(k(100,a));db[a]=b;rd[a]=b.eventTypes[c].dependencies}function pf(a){var b=!1,c;for(c in a)if(a.hasOwnProperty(c)){var d=a[c];if(!cb.hasOwnProperty(c)||cb[c]!==d){if(cb[c])throw Error(k(102,c));cb[c]=
d;b=!0}}b&&nf()}function qf(a){if(a=rf(a)){if("function"!==typeof sd)throw Error(k(280));var b=a.stateNode;b&&(b=td(b),sd(a.stateNode,a.type,b))}}function sf(a){eb?fb?fb.push(a):fb=[a]:eb=a}function tf(){if(eb){var a=eb,b=fb;fb=eb=null;qf(a);if(b)for(a=0;a<b.length;a++)qf(b[a])}}function ud(){if(null!==eb||null!==fb)vd(),tf()}function uf(a,b,c){if(wd)return a(b,c);wd=!0;try{return vf(a,b,c)}finally{wd=!1,ud()}}function ni(a){if(wf.call(xf,a))return!0;if(wf.call(yf,a))return!1;if(oi.test(a))return xf[a]=
!0;yf[a]=!0;return!1}function pi(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function qi(a,b,c,d){if(null===b||"undefined"===typeof b||pi(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function L(a,
b,c,d,e,f){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f}function xd(a,b,c,d){var e=E.hasOwnProperty(b)?E[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||"o"!==b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1]?!1:!0;f||(qi(b,c,e,d)&&(c=null),d||null===e?ni(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,
d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function zb(a){if(null===a||"object"!==typeof a)return null;a=zf&&a[zf]||a["@@iterator"];return"function"===typeof a?a:null}function ri(a){if(-1===a._status){a._status=0;var b=a._ctor;b=b();a._result=b;b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)})}}function na(a){if(null==a)return null;
if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Ma:return"Fragment";case gb:return"Portal";case kc:return"Profiler";case Af:return"StrictMode";case lc:return"Suspense";case yd:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case Bf:return"Context.Consumer";case Cf:return"Context.Provider";case zd:var b=a.render;b=b.displayName||b.name||"";return a.displayName||(""!==b?"ForwardRef("+b+")":"ForwardRef");case Ad:return na(a.type);
case Df:return na(a.render);case Ef:if(a=1===a._status?a._result:null)return na(a)}return null}function Bd(a){var b="";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c="";break a;default:var d=a._debugOwner,e=a._debugSource,f=na(a.type);c=null;d&&(c=na(d.type));d=f;f="";e?f=" (at "+e.fileName.replace(si,"")+":"+e.lineNumber+")":c&&(f=" (created by "+c+")");c="\n in "+(d||"Unknown")+f}b+=c;a=a.return}while(a);return b}function va(a){switch(typeof a){case "boolean":case "number":case "object":case "string":case "undefined":return a;
default:return""}}function Ff(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===b)}function ti(a){var b=Ff(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,
b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function mc(a){a._valueTracker||(a._valueTracker=ti(a))}function Gf(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=Ff(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Cd(a,b){var c=b.checked;return M({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=
c?c:a._wrapperState.initialChecked})}function Hf(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=va(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function If(a,b){b=b.checked;null!=b&&xd(a,"checked",b,!1)}function Dd(a,b){If(a,b);var c=va(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=c)a.value=""+c}else a.value!==
""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?Ed(a,b.type,c):b.hasOwnProperty("defaultValue")&&Ed(a,b.type,va(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function Jf(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;c||b===a.value||(a.value=
b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function Ed(a,b,c){if("number"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function ui(a){var b="";ea.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Fd(a,b){a=M({children:void 0},b);if(b=ui(b.children))a.children=b;return a}function hb(a,b,c,d){a=a.options;if(b){b={};
for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=""+va(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Gd(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(k(91));return M({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}
function Kf(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(k(92));if(Array.isArray(c)){if(!(1>=c.length))throw Error(k(93));c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:va(c)}}function Lf(a,b){var c=va(b.value),d=va(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function Mf(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==
b&&null!==b&&(a.value=b)}function Nf(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Hd(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Nf(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function nc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function oc(a){if(Id[a])return Id[a];
if(!ib[a])return a;var b=ib[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Of)return Id[a]=b[c];return a}function Jd(a){var b=Pf.get(a);void 0===b&&(b=new Map,Pf.set(a,b));return b}function Na(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.effectTag&1026)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function Qf(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Rf(a){if(Na(a)!==
a)throw Error(k(188));}function vi(a){var b=a.alternate;if(!b){b=Na(a);if(null===b)throw Error(k(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Rf(e),a;if(f===d)return Rf(e),b;f=f.sibling}throw Error(k(188));}if(c.return!==d.return)c=e,d=f;else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=
f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(k(189));}}if(c.alternate!==d)throw Error(k(190));}if(3!==c.tag)throw Error(k(188));return c.stateNode.current===c?a:b}function Sf(a){a=vi(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function jb(a,b){if(null==
b)throw Error(k(30));if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function Kd(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function pc(a){null!==a&&(Ab=jb(Ab,a));a=Ab;Ab=null;if(a){Kd(a,wi);if(Ab)throw Error(k(95));if(hc)throw a=pd,hc=!1,pd=null,a;}}function Ld(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:
a}function Tf(a){if(!wa)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Uf(a){a.topLevelType=null;a.nativeEvent=null;a.targetInst=null;a.ancestors.length=0;10>qc.length&&qc.push(a)}function Vf(a,b,c,d){if(qc.length){var e=qc.pop();e.topLevelType=a;e.eventSystemFlags=d;e.nativeEvent=b;e.targetInst=c;return e}return{topLevelType:a,eventSystemFlags:d,nativeEvent:b,targetInst:c,ancestors:[]}}function Wf(a){var b=
a.targetInst,c=b;do{if(!c){a.ancestors.push(c);break}var d=c;if(3===d.tag)d=d.stateNode.containerInfo;else{for(;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo}if(!d)break;b=c.tag;5!==b&&6!==b||a.ancestors.push(c);c=Bb(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=Ld(a.nativeEvent);d=a.topLevelType;var f=a.nativeEvent,g=a.eventSystemFlags;0===c&&(g|=64);for(var h=null,m=0;m<jc.length;m++){var n=jc[m];n&&(n=n.extractEvents(d,b,f,e,g))&&(h=jb(h,n))}pc(h)}}function Md(a,
b,c){if(!c.has(a)){switch(a){case "scroll":Cb(b,"scroll",!0);break;case "focus":case "blur":Cb(b,"focus",!0);Cb(b,"blur",!0);c.set("blur",null);c.set("focus",null);break;case "cancel":case "close":Tf(a)&&Cb(b,a,!0);break;case "invalid":case "submit":case "reset":break;default:-1===Db.indexOf(a)&&w(a,b)}c.set(a,null)}}function xi(a,b){var c=Jd(b);Nd.forEach(function(a){Md(a,b,c)});yi.forEach(function(a){Md(a,b,c)})}function Od(a,b,c,d,e){return{blockedOn:a,topLevelType:b,eventSystemFlags:c|32,nativeEvent:e,
container:d}}function Xf(a,b){switch(a){case "focus":case "blur":xa=null;break;case "dragenter":case "dragleave":ya=null;break;case "mouseover":case "mouseout":za=null;break;case "pointerover":case "pointerout":Eb.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":Fb.delete(b.pointerId)}}function Gb(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a=Od(b,c,d,e,f),null!==b&&(b=Hb(b),null!==b&&Yf(b)),a;a.eventSystemFlags|=d;return a}function zi(a,b,c,d,e){switch(b){case "focus":return xa=
Gb(xa,a,b,c,d,e),!0;case "dragenter":return ya=Gb(ya,a,b,c,d,e),!0;case "mouseover":return za=Gb(za,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;Eb.set(f,Gb(Eb.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,Fb.set(f,Gb(Fb.get(f)||null,a,b,c,d,e)),!0}return!1}function Ai(a){var b=Bb(a.target);if(null!==b){var c=Na(b);if(null!==c)if(b=c.tag,13===b){if(b=Qf(c),null!==b){a.blockedOn=b;Pd(a.priority,function(){Bi(c)});return}}else if(3===b&&c.stateNode.hydrate){a.blockedOn=
3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function rc(a){if(null!==a.blockedOn)return!1;var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);if(null!==b){var c=Hb(b);null!==c&&Yf(c);a.blockedOn=b;return!1}return!0}function Zf(a,b,c){rc(a)&&c.delete(b)}function Ci(){for(Rd=!1;0<fa.length;){var a=fa[0];if(null!==a.blockedOn){a=Hb(a.blockedOn);null!==a&&Di(a);break}var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);null!==b?a.blockedOn=b:fa.shift()}null!==
xa&&rc(xa)&&(xa=null);null!==ya&&rc(ya)&&(ya=null);null!==za&&rc(za)&&(za=null);Eb.forEach(Zf);Fb.forEach(Zf)}function Ib(a,b){a.blockedOn===b&&(a.blockedOn=null,Rd||(Rd=!0,$f(ag,Ci)))}function bg(a){if(0<fa.length){Ib(fa[0],a);for(var b=1;b<fa.length;b++){var c=fa[b];c.blockedOn===a&&(c.blockedOn=null)}}null!==xa&&Ib(xa,a);null!==ya&&Ib(ya,a);null!==za&&Ib(za,a);b=function(b){return Ib(b,a)};Eb.forEach(b);Fb.forEach(b);for(b=0;b<Jb.length;b++)c=Jb[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Jb.length&&
(b=Jb[0],null===b.blockedOn);)Ai(b),null===b.blockedOn&&Jb.shift()}function Sd(a,b){for(var c=0;c<a.length;c+=2){var d=a[c],e=a[c+1],f="on"+(e[0].toUpperCase()+e.slice(1));f={phasedRegistrationNames:{bubbled:f,captured:f+"Capture"},dependencies:[d],eventPriority:b};Td.set(d,b);cg.set(d,f);dg[e]=f}}function w(a,b){Cb(b,a,!1)}function Cb(a,b,c){var d=Td.get(b);switch(void 0===d?2:d){case 0:d=Ei.bind(null,b,1,a);break;case 1:d=Fi.bind(null,b,1,a);break;default:d=sc.bind(null,b,1,a)}c?a.addEventListener(b,
d,!0):a.addEventListener(b,d,!1)}function Ei(a,b,c,d){Oa||vd();var e=sc,f=Oa;Oa=!0;try{eg(e,a,b,c,d)}finally{(Oa=f)||ud()}}function Fi(a,b,c,d){Gi(Hi,sc.bind(null,a,b,c,d))}function sc(a,b,c,d){if(tc)if(0<fa.length&&-1<Nd.indexOf(a))a=Od(null,a,b,c,d),fa.push(a);else{var e=Qd(a,b,c,d);if(null===e)Xf(a,d);else if(-1<Nd.indexOf(a))a=Od(e,a,b,c,d),fa.push(a);else if(!zi(e,a,b,c,d)){Xf(a,d);a=Vf(a,d,null,b);try{uf(Wf,a)}finally{Uf(a)}}}}function Qd(a,b,c,d){c=Ld(d);c=Bb(c);if(null!==c){var e=Na(c);if(null===
e)c=null;else{var f=e.tag;if(13===f){c=Qf(e);if(null!==c)return c;c=null}else if(3===f){if(e.stateNode.hydrate)return 3===e.tag?e.stateNode.containerInfo:null;c=null}else e!==c&&(c=null)}}a=Vf(a,d,c,b);try{uf(Wf,a)}finally{Uf(a)}return null}function fg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||Kb.hasOwnProperty(a)&&Kb[a]?(""+b).trim():b+"px"}function gg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=fg(c,b[c],d);"float"===
c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function Ud(a,b){if(b){if(Ii[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(k(137,a,""));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(k(60));if(!("object"===typeof b.dangerouslySetInnerHTML&&"__html"in b.dangerouslySetInnerHTML))throw Error(k(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(k(62,""));}}function Vd(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
default:return!0}}function oa(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=Jd(a);b=rd[b];for(var d=0;d<b.length;d++)Md(b[d],a,c)}function uc(){}function Wd(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function hg(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ig(a,b){var c=hg(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,
offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=hg(c)}}function jg(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?jg(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function kg(){for(var a=window,b=Wd();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=Wd(a.document)}return b}
function Xd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function lg(a,b){switch(a){case "button":case "input":case "select":case "textarea":return!!b.autoFocus}return!1}function Yd(a,b){return"textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&
null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function kb(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break}return a}function mg(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if(c===ng||c===Zd||c===$d){if(0===b)return a;b--}else c===og&&b++}a=a.previousSibling}return null}function Bb(a){var b=a[Aa];if(b)return b;for(var c=a.parentNode;c;){if(b=c[Lb]||c[Aa]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=mg(a);null!==
a;){if(c=a[Aa])return c;a=mg(a)}return b}a=c;c=a.parentNode}return null}function Hb(a){a=a[Aa]||a[Lb];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Pa(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(k(33));}function ae(a){return a[vc]||null}function pa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function pg(a,b){var c=a.stateNode;if(!c)return null;var d=td(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=
!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;if(c&&"function"!==typeof c)throw Error(k(231,b,typeof c));return c}function qg(a,b,c){if(b=pg(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a)}function Ji(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=pa(b);for(b=c.length;0<b--;)qg(c[b],
"captured",a);for(b=0;b<c.length;b++)qg(c[b],"bubbled",a)}}function be(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=pg(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a))}function Ki(a){a&&a.dispatchConfig.registrationName&&be(a._targetInst,null,a)}function lb(a){Kd(a,Ji)}function rg(){if(wc)return wc;var a,b=ce,c=b.length,d,e="value"in Ba?Ba.value:Ba.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=
c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return wc=e.slice(a,1<d?1-d:void 0)}function xc(){return!0}function yc(){return!1}function R(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):"target"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?xc:yc;this.isPropagationStopped=yc;return this}function Li(a,b,c,d){if(this.eventPool.length){var e=
this.eventPool.pop();this.call(e,a,b,c,d);return e}return new this(a,b,c,d)}function Mi(a){if(!(a instanceof this))throw Error(k(279));a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function sg(a){a.eventPool=[];a.getPooled=Li;a.release=Mi}function tg(a,b){switch(a){case "keyup":return-1!==Ni.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return!0;default:return!1}}function ug(a){a=a.detail;return"object"===typeof a&&"data"in
a?a.data:null}function Oi(a,b){switch(a){case "compositionend":return ug(b);case "keypress":if(32!==b.which)return null;vg=!0;return wg;case "textInput":return a=b.data,a===wg&&vg?null:a;default:return null}}function Pi(a,b){if(mb)return"compositionend"===a||!de&&tg(a,b)?(a=rg(),wc=ce=Ba=null,mb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
case "compositionend":return xg&&"ko"!==b.locale?null:b.data;default:return null}}function yg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Qi[a.type]:"textarea"===b?!0:!1}function zg(a,b,c){a=R.getPooled(Ag.change,a,b,c);a.type="change";sf(c);lb(a);return a}function Ri(a){pc(a)}function zc(a){var b=Pa(a);if(Gf(b))return a}function Si(a,b){if("change"===a)return b}function Bg(){Mb&&(Mb.detachEvent("onpropertychange",Cg),Nb=Mb=null)}function Cg(a){if("value"===a.propertyName&&
zc(Nb))if(a=zg(Nb,a,Ld(a)),Oa)pc(a);else{Oa=!0;try{ee(Ri,a)}finally{Oa=!1,ud()}}}function Ti(a,b,c){"focus"===a?(Bg(),Mb=b,Nb=c,Mb.attachEvent("onpropertychange",Cg)):"blur"===a&&Bg()}function Ui(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return zc(Nb)}function Vi(a,b){if("click"===a)return zc(b)}function Wi(a,b){if("input"===a||"change"===a)return zc(b)}function Xi(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Yi[a])?!!b[a]:!1}function fe(a){return Xi}
function Zi(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function Ob(a,b){if(Qa(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!$i.call(b,c[d])||!Qa(a[c[d]],b[c[d]]))return!1;return!0}function Dg(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(ge||null==nb||nb!==Wd(c))return null;c=nb;"selectionStart"in c&&Xd(c)?c={start:c.selectionStart,
end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return Pb&&Ob(Pb,c)?null:(Pb=c,a=R.getPooled(Eg.select,he,a,b),a.type="select",a.target=nb,lb(a),a)}function Ac(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function q(a,b){0>ob||(a.current=ie[ob],ie[ob]=null,ob--)}function y(a,b,c){ob++;
ie[ob]=a.current;a.current=b}function pb(a,b){var c=a.type.contextTypes;if(!c)return Ca;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function N(a){a=a.childContextTypes;return null!==a&&void 0!==a}function Fg(a,b,c){if(B.current!==Ca)throw Error(k(168));y(B,b);y(G,c)}
function Gg(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in a))throw Error(k(108,na(b)||"Unknown",e));return M({},c,{},d)}function Bc(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Ca;Ra=B.current;y(B,a);y(G,G.current);return!0}function Hg(a,b,c){var d=a.stateNode;if(!d)throw Error(k(169));c?(a=Gg(a,b,Ra),d.__reactInternalMemoizedMergedChildContext=a,q(G),q(B),y(B,a)):q(G);y(G,c)}function Cc(){switch(aj()){case Dc:return 99;
case Ig:return 98;case Jg:return 97;case Kg:return 96;case Lg:return 95;default:throw Error(k(332));}}function Mg(a){switch(a){case 99:return Dc;case 98:return Ig;case 97:return Jg;case 96:return Kg;case 95:return Lg;default:throw Error(k(332));}}function Da(a,b){a=Mg(a);return bj(a,b)}function Ng(a,b,c){a=Mg(a);return je(a,b,c)}function Og(a){null===qa?(qa=[a],Ec=je(Dc,Pg)):qa.push(a);return Qg}function ha(){if(null!==Ec){var a=Ec;Ec=null;Rg(a)}Pg()}function Pg(){if(!ke&&null!==qa){ke=!0;var a=0;
try{var b=qa;Da(99,function(){for(;a<b.length;a++){var c=b[a];do c=c(!0);while(null!==c)}});qa=null}catch(c){throw null!==qa&&(qa=qa.slice(a+1)),je(Dc,ha),c;}finally{ke=!1}}}function Fc(a,b,c){c/=10;return 1073741821-(((1073741821-a+b/10)/c|0)+1)*c}function aa(a,b){if(a&&a.defaultProps){b=M({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function le(){Gc=qb=Hc=null}function me(a){var b=Ic.current;q(Ic);a.type._context._currentValue=b}function Sg(a,b){for(;null!==a;){var c=
a.alternate;if(a.childExpirationTime<b)a.childExpirationTime=b,null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);else if(null!==c&&c.childExpirationTime<b)c.childExpirationTime=b;else break;a=a.return}}function rb(a,b){Hc=a;Gc=qb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(a.expirationTime>=b&&(ia=!0),a.firstContext=null)}function W(a,b){if(Gc!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)Gc=a,b=1073741823;b={context:a,observedBits:b,next:null};if(null===qb){if(null===
Hc)throw Error(k(308));qb=b;Hc.dependencies={expirationTime:0,firstContext:b,responders:null}}else qb=qb.next=b}return a._currentValue}function ne(a){a.updateQueue={baseState:a.memoizedState,baseQueue:null,shared:{pending:null},effects:null}}function oe(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue={baseState:a.baseState,baseQueue:a.baseQueue,shared:a.shared,effects:a.effects})}function Ea(a,b){a={expirationTime:a,suspenseConfig:b,tag:Tg,payload:null,callback:null,next:null};return a.next=
a}function Fa(a,b){a=a.updateQueue;if(null!==a){a=a.shared;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}}function Ug(a,b){var c=a.alternate;null!==c&&oe(c,a);a=a.updateQueue;c=a.baseQueue;null===c?(a.baseQueue=b.next=b,b.next=b):(b.next=c.next,c.next=b)}function Qb(a,b,c,d){var e=a.updateQueue;Ga=!1;var f=e.baseQueue,g=e.shared.pending;if(null!==g){if(null!==f){var h=f.next;f.next=g.next;g.next=h}f=g;e.shared.pending=null;h=a.alternate;null!==h&&(h=h.updateQueue,null!==h&&
(h.baseQueue=g))}if(null!==f){h=f.next;var m=e.baseState,n=0,k=null,ba=null,l=null;if(null!==h){var p=h;do{g=p.expirationTime;if(g<d){var t={expirationTime:p.expirationTime,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null};null===l?(ba=l=t,k=m):l=l.next=t;g>n&&(n=g)}else{null!==l&&(l=l.next={expirationTime:1073741823,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null});Vg(g,p.suspenseConfig);a:{var q=a,r=p;g=b;t=c;switch(r.tag){case 1:q=
r.payload;if("function"===typeof q){m=q.call(t,m,g);break a}m=q;break a;case 3:q.effectTag=q.effectTag&-4097|64;case Tg:q=r.payload;g="function"===typeof q?q.call(t,m,g):q;if(null===g||void 0===g)break a;m=M({},m,g);break a;case Jc:Ga=!0}}null!==p.callback&&(a.effectTag|=32,g=e.effects,null===g?e.effects=[p]:g.push(p))}p=p.next;if(null===p||p===h)if(g=e.shared.pending,null===g)break;else p=f.next=g.next,g.next=h,e.baseQueue=f=g,e.shared.pending=null}while(1)}null===l?k=m:l.next=ba;e.baseState=k;e.baseQueue=
l;Kc(n);a.expirationTime=n;a.memoizedState=m}}function Wg(a,b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=e;e=c;if("function"!==typeof d)throw Error(k(191,d));d.call(e)}}}function Lc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:M({},b,c);a.memoizedState=c;0===a.expirationTime&&(a.updateQueue.baseState=c)}function Xg(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,
f,g):b.prototype&&b.prototype.isPureReactComponent?!Ob(c,d)||!Ob(e,f):!0}function Yg(a,b,c){var d=!1,e=Ca;var f=b.contextType;"object"===typeof f&&null!==f?f=W(f):(e=N(b)?Ra:B.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?pb(a,e):Ca);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Mc;a.stateNode=b;b._reactInternalFiber=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zg(a,
b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Mc.enqueueReplaceState(b,b.state,null)}function pe(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$g;ne(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=W(f):(f=N(b)?Ra:B.current,e.context=pb(a,f));Qb(a,c,e,d);e.state=a.memoizedState;f=b.getDerivedStateFromProps;
"function"===typeof f&&(Lc(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Mc.enqueueReplaceState(e,e.state,null),Qb(a,c,e,d),e.state=a.memoizedState);"function"===
typeof e.componentDidMount&&(a.effectTag|=4)}function Rb(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(k(309));var d=c.stateNode}if(!d)throw Error(k(147,a));var e=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$g&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}if("string"!==typeof a)throw Error(k(284));if(!c._owner)throw Error(k(290,
a));}return a}function Nc(a,b){if("textarea"!==a.type)throw Error(k(31,"[object Object]"===Object.prototype.toString.call(b)?"object with keys {"+Object.keys(b).join(", ")+"}":b,""));}function ah(a){function b(b,c){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,
b),b=b.sibling;return a}function e(a,b){a=Sa(a,b);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=qe(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function m(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props),d.ref=Rb(a,b,c),d.return=a,d;d=Oc(c.type,
c.key,c.props,null,a.mode,d);d.ref=Rb(a,b,c);d.return=a;return d}function n(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=re(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=Ha(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function ba(a,b,c){if("string"===typeof b||"number"===typeof b)return b=qe(""+b,a.mode,c),b.return=a,b;if("object"===
typeof b&&null!==b){switch(b.$$typeof){case Pc:return c=Oc(b.type,b.key,b.props,null,a.mode,c),c.ref=Rb(a,null,b),c.return=a,c;case gb:return b=re(b,a.mode,c),b.return=a,b}if(Qc(b)||zb(b))return b=Ha(b,a.mode,c,null),b.return=a,b;Nc(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case Pc:return c.key===e?c.type===Ma?l(a,b,c.props.children,d,e):m(a,b,c,
d):null;case gb:return c.key===e?n(a,b,c,d):null}if(Qc(c)||zb(c))return null!==e?null:l(a,b,c,d,null);Nc(a,c)}return null}function t(a,b,c,d,e){if("string"===typeof d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case Pc:return a=a.get(null===d.key?c:d.key)||null,d.type===Ma?l(b,a,d.props.children,e,d.key):m(b,a,d,e);case gb:return a=a.get(null===d.key?c:d.key)||null,n(b,a,d,e)}if(Qc(d)||zb(d))return a=a.get(c)||null,l(b,a,d,e,null);
Nc(b,d)}return null}function q(e,g,h,m){for(var n=null,k=null,l=g,r=g=0,C=null;null!==l&&r<h.length;r++){l.index>r?(C=l,l=null):C=l.sibling;var O=p(e,l,h[r],m);if(null===O){null===l&&(l=C);break}a&&l&&null===O.alternate&&b(e,l);g=f(O,g,r);null===k?n=O:k.sibling=O;k=O;l=C}if(r===h.length)return c(e,l),n;if(null===l){for(;r<h.length;r++)l=ba(e,h[r],m),null!==l&&(g=f(l,g,r),null===k?n=l:k.sibling=l,k=l);return n}for(l=d(e,l);r<h.length;r++)C=t(l,e,r,h[r],m),null!==C&&(a&&null!==C.alternate&&l.delete(null===
C.key?r:C.key),g=f(C,g,r),null===k?n=C:k.sibling=C,k=C);a&&l.forEach(function(a){return b(e,a)});return n}function w(e,g,h,n){var m=zb(h);if("function"!==typeof m)throw Error(k(150));h=m.call(h);if(null==h)throw Error(k(151));for(var l=m=null,r=g,C=g=0,O=null,v=h.next();null!==r&&!v.done;C++,v=h.next()){r.index>C?(O=r,r=null):O=r.sibling;var q=p(e,r,v.value,n);if(null===q){null===r&&(r=O);break}a&&r&&null===q.alternate&&b(e,r);g=f(q,g,C);null===l?m=q:l.sibling=q;l=q;r=O}if(v.done)return c(e,r),m;
if(null===r){for(;!v.done;C++,v=h.next())v=ba(e,v.value,n),null!==v&&(g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);return m}for(r=d(e,r);!v.done;C++,v=h.next())v=t(r,e,C,v.value,n),null!==v&&(a&&null!==v.alternate&&r.delete(null===v.key?C:v.key),g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);a&&r.forEach(function(a){return b(e,a)});return m}return function(a,d,f,h){var m="object"===typeof f&&null!==f&&f.type===Ma&&null===f.key;m&&(f=f.props.children);var n="object"===typeof f&&null!==f;if(n)switch(f.$$typeof){case Pc:a:{n=
f.key;for(m=d;null!==m;){if(m.key===n){switch(m.tag){case 7:if(f.type===Ma){c(a,m.sibling);d=e(m,f.props.children);d.return=a;a=d;break a}break;default:if(m.elementType===f.type){c(a,m.sibling);d=e(m,f.props);d.ref=Rb(a,m,f);d.return=a;a=d;break a}}c(a,m);break}else b(a,m);m=m.sibling}f.type===Ma?(d=Ha(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=Oc(f.type,f.key,f.props,null,a.mode,h),h.ref=Rb(a,d,f),h.return=a,a=h)}return g(a);case gb:a:{for(m=f.key;null!==d;){if(d.key===m)if(4===d.tag&&d.stateNode.containerInfo===
f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=re(f,a.mode,h);d.return=a;a=d}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=qe(f,a.mode,h),d.return=a,a=d),g(a);if(Qc(f))return q(a,d,f,h);if(zb(f))return w(a,d,f,h);n&&Nc(a,f);if("undefined"===typeof f&&!m)switch(a.tag){case 1:case 0:throw a=
a.type,Error(k(152,a.displayName||a.name||"Component"));}return c(a,d)}}function Ta(a){if(a===Sb)throw Error(k(174));return a}function se(a,b){y(Tb,b);y(Ub,a);y(ja,Sb);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Hd(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=Hd(b,a)}q(ja);y(ja,b)}function tb(a){q(ja);q(Ub);q(Tb)}function bh(a){Ta(Tb.current);var b=Ta(ja.current);var c=Hd(b,a.type);b!==c&&(y(Ub,a),y(ja,c))}function te(a){Ub.current===
a&&(q(ja),q(Ub))}function Rc(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||c.data===$d||c.data===Zd))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.effectTag&64))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}return null}function ue(a,b){return{responder:a,props:b}}
function S(){throw Error(k(321));}function ve(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Qa(a[c],b[c]))return!1;return!0}function we(a,b,c,d,e,f){Ia=f;z=b;b.memoizedState=null;b.updateQueue=null;b.expirationTime=0;Sc.current=null===a||null===a.memoizedState?dj:ej;a=c(d,e);if(b.expirationTime===Ia){f=0;do{b.expirationTime=0;if(!(25>f))throw Error(k(301));f+=1;J=K=null;b.updateQueue=null;Sc.current=fj;a=c(d,e)}while(b.expirationTime===Ia)}Sc.current=Tc;b=null!==K&&null!==K.next;
Ia=0;J=K=z=null;Uc=!1;if(b)throw Error(k(300));return a}function ub(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===J?z.memoizedState=J=a:J=J.next=a;return J}function vb(){if(null===K){var a=z.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===J?z.memoizedState:J.next;if(null!==b)J=b,K=a;else{if(null===a)throw Error(k(310));K=a;a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===J?z.memoizedState=
J=a:J=J.next=a}return J}function Ua(a,b){return"function"===typeof b?b(a):b}function Vc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){e=e.next;d=d.baseState;var h=g=f=null,m=e;do{var n=m.expirationTime;if(n<Ia){var l={expirationTime:m.expirationTime,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,
next:null};null===h?(g=h=l,f=d):h=h.next=l;n>z.expirationTime&&(z.expirationTime=n,Kc(n))}else null!==h&&(h=h.next={expirationTime:1073741823,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,next:null}),Vg(n,m.suspenseConfig),d=m.eagerReducer===a?m.eagerState:a(d,m.action);m=m.next}while(null!==m&&m!==e);null===h?f=d:h.next=g;Qa(d,b.memoizedState)||(ia=!0);b.memoizedState=d;b.baseState=f;b.baseQueue=h;c.lastRenderedState=d}return[b.memoizedState,
c.dispatch]}function Wc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);Qa(f,b.memoizedState)||(ia=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function xe(a){var b=ub();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={pending:null,dispatch:null,lastRenderedReducer:Ua,
lastRenderedState:a};a=a.dispatch=ch.bind(null,z,a);return[b.memoizedState,a]}function ye(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};b=z.updateQueue;null===b?(b={lastEffect:null},z.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function dh(a){return vb().memoizedState}function ze(a,b,c,d){var e=ub();z.effectTag|=a;e.memoizedState=ye(1|b,c,void 0,void 0===d?null:d)}function Ae(a,b,c,d){var e=vb();
d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&ve(d,g.deps)){ye(b,c,f,d);return}}z.effectTag|=a;e.memoizedState=ye(1|b,c,f,d)}function eh(a,b){return ze(516,4,a,b)}function Xc(a,b){return Ae(516,4,a,b)}function fh(a,b){return Ae(4,2,a,b)}function gh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function hh(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;
return Ae(4,2,gh.bind(null,b,a),c)}function Be(a,b){}function ih(a,b){ub().memoizedState=[a,void 0===b?null:b];return a}function Yc(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];c.memoizedState=[a,b];return a}function jh(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Ce(a,b,c){var d=Cc();Da(98>d?98:d,function(){a(!0)});Da(97<d?97:d,function(){var d=
X.suspense;X.suspense=void 0===b?null:b;try{a(!1),c()}finally{X.suspense=d}})}function ch(a,b,c){var d=ka(),e=Vb.suspense;d=Va(d,a,e);e={expirationTime:d,suspenseConfig:e,action:c,eagerReducer:null,eagerState:null,next:null};var f=b.pending;null===f?e.next=e:(e.next=f.next,f.next=e);b.pending=e;f=a.alternate;if(a===z||null!==f&&f===z)Uc=!0,e.expirationTime=Ia,z.expirationTime=Ia;else{if(0===a.expirationTime&&(null===f||0===f.expirationTime)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,
h=f(g,c);e.eagerReducer=f;e.eagerState=h;if(Qa(h,g))return}catch(m){}finally{}Ja(a,d)}}function kh(a,b){var c=la(5,null,null,0);c.elementType="DELETED";c.type="DELETED";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function lh(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?
null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function De(a){if(Wa){var b=Ka;if(b){var c=b;if(!lh(a,b)){b=kb(c.nextSibling);if(!b||!lh(a,b)){a.effectTag=a.effectTag&-1025|2;Wa=!1;ra=a;return}kh(ra,c)}ra=a;Ka=kb(b.firstChild)}else a.effectTag=a.effectTag&-1025|2,Wa=!1,ra=a}}function mh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;ra=a}function Zc(a){if(a!==ra)return!1;if(!Wa)return mh(a),Wa=!0,!1;var b=a.type;if(5!==a.tag||"head"!==b&&"body"!==
b&&!Yd(b,a.memoizedProps))for(b=Ka;b;)kh(a,b),b=kb(b.nextSibling);mh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(k(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if(c===og){if(0===b){Ka=kb(a.nextSibling);break a}b--}else c!==ng&&c!==Zd&&c!==$d||b++}a=a.nextSibling}Ka=null}}else Ka=ra?kb(a.stateNode.nextSibling):null;return!0}function Ee(){Ka=ra=null;Wa=!1}function T(a,b,c,d){b.child=null===a?Fe(b,null,c,d):wb(b,a.child,c,d)}function nh(a,
b,c,d,e){c=c.render;var f=b.ref;rb(b,e);d=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,d,e);return b.child}function oh(a,b,c,d,e,f){if(null===a){var g=c.type;if("function"===typeof g&&!Ge(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,ph(a,b,g,d,e,f);a=Oc(c.type,null,d,null,b.mode,f);a.ref=b.ref;a.return=b;return b.child=a}g=a.child;if(e<
f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:Ob,c(e,d)&&a.ref===b.ref))return sa(a,b,f);b.effectTag|=1;a=Sa(g,d);a.ref=b.ref;a.return=b;return b.child=a}function ph(a,b,c,d,e,f){return null!==a&&Ob(a.memoizedProps,d)&&a.ref===b.ref&&(ia=!1,e<f)?(b.expirationTime=a.expirationTime,sa(a,b,f)):He(a,b,c,d,f)}function qh(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function He(a,b,c,d,e){var f=N(c)?Ra:B.current;f=pb(b,f);rb(b,e);c=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=
a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,c,e);return b.child}function rh(a,b,c,d,e){if(N(c)){var f=!0;Bc(b)}else f=!1;rb(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yg(b,c,d),pe(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var m=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n));var l=c.getDerivedStateFromProps,k="function"===
typeof l||"function"===typeof g.getSnapshotBeforeUpdate;k||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n);Ga=!1;var p=b.memoizedState;g.state=p;Qb(b,d,g,e);m=b.memoizedState;h!==d||p!==m||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),m=b.memoizedState),(h=Ga||Xg(b,c,h,d,p,m,n))?(k||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||("function"===typeof g.componentWillMount&&
g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.effectTag|=4)):("function"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=m),g.props=d,g.state=m,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,oe(a,b),h=b.memoizedProps,g.props=b.type===b.elementType?h:aa(b.type,h),m=g.context,n=c.contextType,"object"===typeof n&&null!==
n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n)),l=c.getDerivedStateFromProps,(k="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n),Ga=!1,m=b.memoizedState,g.state=m,Qb(b,d,g,e),p=b.memoizedState,h!==d||m!==p||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),p=b.memoizedState),(l=Ga||Xg(b,c,h,d,m,p,n))?(k||"function"!==typeof g.UNSAFE_componentWillUpdate&&
"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,n),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,n)),"function"===typeof g.componentDidUpdate&&(b.effectTag|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===
a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=n,d=l):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=256),d=!1);return Ie(a,b,c,d,f,e)}function Ie(a,b,c,d,e,f){qh(a,b);var g=0!==(b.effectTag&64);if(!d&&!g)return e&&Hg(b,c,!1),sa(a,b,f);d=b.stateNode;gj.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?
null:d.render();b.effectTag|=1;null!==a&&g?(b.child=wb(b,a.child,null,f),b.child=wb(b,null,h,f)):T(a,b,h,f);b.memoizedState=d.state;e&&Hg(b,c,!0);return b.child}function sh(a){var b=a.stateNode;b.pendingContext?Fg(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Fg(a,b.context,!1);se(a,b.containerInfo)}function th(a,b,c){var d=b.mode,e=b.pendingProps,f=D.current,g=!1,h;(h=0!==(b.effectTag&64))||(h=0!==(f&2)&&(null===a||null!==a.memoizedState));h?(g=!0,b.effectTag&=-65):null!==a&&null===
a.memoizedState||void 0===e.fallback||!0===e.unstable_avoidThisFallback||(f|=1);y(D,f&1);if(null===a){void 0!==e.fallback&&De(b);if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;b.memoizedState=Je;b.child=e;return c}d=e.children;b.memoizedState=null;return b.child=Fe(b,null,d,c)}if(null!==a.memoizedState){a=a.child;d=a.sibling;if(g){e=e.fallback;
c=Sa(a,a.pendingProps);c.return=b;if(0===(b.mode&2)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==a.child))for(c.child=g;null!==g;)g.return=c,g=g.sibling;d=Sa(d,e);d.return=b;c.sibling=d;c.childExpirationTime=0;b.memoizedState=Je;b.child=c;return d}c=wb(b,a.child,e.children,c);b.memoizedState=null;return b.child=c}a=a.child;if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;e.child=a;null!==a&&(a.return=e);if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==
a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;c.effectTag|=2;e.childExpirationTime=0;b.memoizedState=Je;b.child=e;return c}b.memoizedState=null;return b.child=wb(b,a,e.children,c)}function uh(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);Sg(a.return,b)}function Ke(a,b,c,d,e,f){var g=a.memoizedState;null===g?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailExpiration:0,tailMode:e,
lastEffect:f}:(g.isBackwards=b,g.rendering=null,g.renderingStartTime=0,g.last=d,g.tail=c,g.tailExpiration=0,g.tailMode=e,g.lastEffect=f)}function vh(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;T(a,b,d.children,c);d=D.current;if(0!==(d&2))d=d&1|2,b.effectTag|=64;else{if(null!==a&&0!==(a.effectTag&64))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&uh(a,c);else if(19===a.tag)uh(a,c);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===
a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(D,d);if(0===(b.mode&2))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===Rc(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ke(b,!1,e,c,f,b.lastEffect);break;case "backwards":c=null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===Rc(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ke(b,
!0,c,null,f,b.lastEffect);break;case "together":Ke(b,!1,null,null,void 0,b.lastEffect);break;default:b.memoizedState=null}return b.child}function sa(a,b,c){null!==a&&(b.dependencies=a.dependencies);var d=b.expirationTime;0!==d&&Kc(d);if(b.childExpirationTime<c)return null;if(null!==a&&b.child!==a.child)throw Error(k(153));if(null!==b.child){a=b.child;c=Sa(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Sa(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}
function $c(a,b){switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function hj(a,b,c){var d=b.pendingProps;switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return null;case 1:return N(b.type)&&(q(G),q(B)),
null;case 3:return tb(),q(G),q(B),c=b.stateNode,c.pendingContext&&(c.context=c.pendingContext,c.pendingContext=null),null!==a&&null!==a.child||!Zc(b)||(b.effectTag|=4),wh(b),null;case 5:te(b);c=Ta(Tb.current);var e=b.type;if(null!==a&&null!=b.stateNode)ij(a,b,e,d,c),a.ref!==b.ref&&(b.effectTag|=128);else{if(!d){if(null===b.stateNode)throw Error(k(166));return null}a=Ta(ja.current);if(Zc(b)){d=b.stateNode;e=b.type;var f=b.memoizedProps;d[Aa]=b;d[vc]=f;switch(e){case "iframe":case "object":case "embed":w("load",
d);break;case "video":case "audio":for(a=0;a<Db.length;a++)w(Db[a],d);break;case "source":w("error",d);break;case "img":case "image":case "link":w("error",d);w("load",d);break;case "form":w("reset",d);w("submit",d);break;case "details":w("toggle",d);break;case "input":Hf(d,f);w("invalid",d);oa(c,"onChange");break;case "select":d._wrapperState={wasMultiple:!!f.multiple};w("invalid",d);oa(c,"onChange");break;case "textarea":Kf(d,f),w("invalid",d),oa(c,"onChange")}Ud(e,f);a=null;for(var g in f)if(f.hasOwnProperty(g)){var h=
f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(a=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(a=["children",""+h]):db.hasOwnProperty(g)&&null!=h&&oa(c,g)}switch(e){case "input":mc(d);Jf(d,f,!0);break;case "textarea":mc(d);Mf(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=uc)}c=a;b.updateQueue=c;null!==c&&(b.effectTag|=4)}else{g=9===c.nodeType?c:c.ownerDocument;"http://www.w3.org/1999/xhtml"===a&&(a=Nf(e));"http://www.w3.org/1999/xhtml"===
a?"script"===e?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(e,{is:d.is}):(a=g.createElement(e),"select"===e&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,e);a[Aa]=b;a[vc]=d;jj(a,b,!1,!1);b.stateNode=a;g=Vd(e,d);switch(e){case "iframe":case "object":case "embed":w("load",a);h=d;break;case "video":case "audio":for(h=0;h<Db.length;h++)w(Db[h],a);h=d;break;case "source":w("error",a);
h=d;break;case "img":case "image":case "link":w("error",a);w("load",a);h=d;break;case "form":w("reset",a);w("submit",a);h=d;break;case "details":w("toggle",a);h=d;break;case "input":Hf(a,d);h=Cd(a,d);w("invalid",a);oa(c,"onChange");break;case "option":h=Fd(a,d);break;case "select":a._wrapperState={wasMultiple:!!d.multiple};h=M({},d,{value:void 0});w("invalid",a);oa(c,"onChange");break;case "textarea":Kf(a,d);h=Gd(a,d);w("invalid",a);oa(c,"onChange");break;default:h=d}Ud(e,h);var m=h;for(f in m)if(m.hasOwnProperty(f)){var n=
m[f];"style"===f?gg(a,n):"dangerouslySetInnerHTML"===f?(n=n?n.__html:void 0,null!=n&&xh(a,n)):"children"===f?"string"===typeof n?("textarea"!==e||""!==n)&&Wb(a,n):"number"===typeof n&&Wb(a,""+n):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&(db.hasOwnProperty(f)?null!=n&&oa(c,f):null!=n&&xd(a,f,n,g))}switch(e){case "input":mc(a);Jf(a,d,!1);break;case "textarea":mc(a);Mf(a);break;case "option":null!=d.value&&a.setAttribute("value",""+va(d.value));break;case "select":a.multiple=
!!d.multiple;c=d.value;null!=c?hb(a,!!d.multiple,c,!1):null!=d.defaultValue&&hb(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof h.onClick&&(a.onclick=uc)}lg(e,d)&&(b.effectTag|=4)}null!==b.ref&&(b.effectTag|=128)}return null;case 6:if(a&&null!=b.stateNode)kj(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===b.stateNode)throw Error(k(166));c=Ta(Tb.current);Ta(ja.current);Zc(b)?(c=b.stateNode,d=b.memoizedProps,c[Aa]=b,c.nodeValue!==d&&(b.effectTag|=4)):(c=(9===c.nodeType?
c:c.ownerDocument).createTextNode(d),c[Aa]=b,b.stateNode=c)}return null;case 13:q(D);d=b.memoizedState;if(0!==(b.effectTag&64))return b.expirationTime=c,b;c=null!==d;d=!1;null===a?void 0!==b.memoizedProps.fallback&&Zc(b):(e=a.memoizedState,d=null!==e,c||null===e||(e=a.child.sibling,null!==e&&(f=b.firstEffect,null!==f?(b.firstEffect=e,e.nextEffect=f):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8)));if(c&&!d&&0!==(b.mode&2))if(null===a&&!0!==b.memoizedProps.unstable_avoidThisFallback||
0!==(D.current&1))F===Xa&&(F=ad);else{if(F===Xa||F===ad)F=bd;0!==Xb&&null!==U&&(Ya(U,P),yh(U,Xb))}if(c||d)b.effectTag|=4;return null;case 4:return tb(),wh(b),null;case 10:return me(b),null;case 17:return N(b.type)&&(q(G),q(B)),null;case 19:q(D);d=b.memoizedState;if(null===d)return null;e=0!==(b.effectTag&64);f=d.rendering;if(null===f)if(e)$c(d,!1);else{if(F!==Xa||null!==a&&0!==(a.effectTag&64))for(f=b.child;null!==f;){a=Rc(f);if(null!==a){b.effectTag|=64;$c(d,!1);e=a.updateQueue;null!==e&&(b.updateQueue=
e,b.effectTag|=4);null===d.lastEffect&&(b.firstEffect=null);b.lastEffect=d.lastEffect;for(d=b.child;null!==d;)e=d,f=c,e.effectTag&=2,e.nextEffect=null,e.firstEffect=null,e.lastEffect=null,a=e.alternate,null===a?(e.childExpirationTime=0,e.expirationTime=f,e.child=null,e.memoizedProps=null,e.memoizedState=null,e.updateQueue=null,e.dependencies=null):(e.childExpirationTime=a.childExpirationTime,e.expirationTime=a.expirationTime,e.child=a.child,e.memoizedProps=a.memoizedProps,e.memoizedState=a.memoizedState,
e.updateQueue=a.updateQueue,f=a.dependencies,e.dependencies=null===f?null:{expirationTime:f.expirationTime,firstContext:f.firstContext,responders:f.responders}),d=d.sibling;y(D,D.current&1|2);return b.child}f=f.sibling}}else{if(!e)if(a=Rc(f),null!==a){if(b.effectTag|=64,e=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.effectTag|=4),$c(d,!0),null===d.tail&&"hidden"===d.tailMode&&!f.alternate)return b=b.lastEffect=d.lastEffect,null!==b&&(b.nextEffect=null),null}else 2*Y()-d.renderingStartTime>d.tailExpiration&&
1<c&&(b.effectTag|=64,e=!0,$c(d,!1),b.expirationTime=b.childExpirationTime=c-1);d.isBackwards?(f.sibling=b.child,b.child=f):(c=d.last,null!==c?c.sibling=f:b.child=f,d.last=f)}return null!==d.tail?(0===d.tailExpiration&&(d.tailExpiration=Y()+500),c=d.tail,d.rendering=c,d.tail=c.sibling,d.lastEffect=b.lastEffect,d.renderingStartTime=Y(),c.sibling=null,b=D.current,y(D,e?b&1|2:b&1),c):null}throw Error(k(156,b.tag));}function lj(a,b){switch(a.tag){case 1:return N(a.type)&&(q(G),q(B)),b=a.effectTag,b&4096?
(a.effectTag=b&-4097|64,a):null;case 3:tb();q(G);q(B);b=a.effectTag;if(0!==(b&64))throw Error(k(285));a.effectTag=b&-4097|64;return a;case 5:return te(a),null;case 13:return q(D),b=a.effectTag,b&4096?(a.effectTag=b&-4097|64,a):null;case 19:return q(D),null;case 4:return tb(),null;case 10:return me(a),null;default:return null}}function Le(a,b){return{value:a,source:b,stack:Bd(b)}}function Me(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=Bd(c));null!==c&&na(c.type);b=b.value;null!==a&&1===a.tag&&
na(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function mj(a,b){try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(c){Za(a,c)}}function zh(a){var b=a.ref;if(null!==b)if("function"===typeof b)try{b(null)}catch(c){Za(a,c)}else b.current=null}function nj(a,b){switch(b.tag){case 0:case 11:case 15:case 22:return;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===
b.type?c:aa(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}return;case 3:case 5:case 6:case 4:case 17:return}throw Error(k(163));}function Ah(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.destroy;c.destroy=void 0;void 0!==d&&d()}c=c.next}while(c!==b)}}function Bh(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function oj(a,b,c,d){switch(c.tag){case 0:case 11:case 15:case 22:Bh(3,
c);return;case 1:a=c.stateNode;c.effectTag&4&&(null===b?a.componentDidMount():(d=c.elementType===c.type?b.memoizedProps:aa(c.type,b.memoizedProps),a.componentDidUpdate(d,b.memoizedState,a.__reactInternalSnapshotBeforeUpdate)));b=c.updateQueue;null!==b&&Wg(c,b,a);return;case 3:b=c.updateQueue;if(null!==b){a=null;if(null!==c.child)switch(c.child.tag){case 5:a=c.child.stateNode;break;case 1:a=c.child.stateNode}Wg(c,b,a)}return;case 5:a=c.stateNode;null===b&&c.effectTag&4&&lg(c.type,c.memoizedProps)&&
a.focus();return;case 6:return;case 4:return;case 12:return;case 13:null===c.memoizedState&&(c=c.alternate,null!==c&&(c=c.memoizedState,null!==c&&(c=c.dehydrated,null!==c&&bg(c))));return;case 19:case 17:case 20:case 21:return}throw Error(k(163));}function Ch(a,b,c){"function"===typeof Ne&&Ne(b);switch(b.tag){case 0:case 11:case 14:case 15:case 22:a=b.updateQueue;if(null!==a&&(a=a.lastEffect,null!==a)){var d=a.next;Da(97<c?97:c,function(){var a=d;do{var c=a.destroy;if(void 0!==c){var g=b;try{c()}catch(h){Za(g,
h)}}a=a.next}while(a!==d)})}break;case 1:zh(b);c=b.stateNode;"function"===typeof c.componentWillUnmount&&mj(b,c);break;case 5:zh(b);break;case 4:Dh(a,b,c)}}function Eh(a){var b=a.alternate;a.return=null;a.child=null;a.memoizedState=null;a.updateQueue=null;a.dependencies=null;a.alternate=null;a.firstEffect=null;a.lastEffect=null;a.pendingProps=null;a.memoizedProps=null;a.stateNode=null;null!==b&&Eh(b)}function Fh(a){return 5===a.tag||3===a.tag||4===a.tag}function Gh(a){a:{for(var b=a.return;null!==
b;){if(Fh(b)){var c=b;break a}b=b.return}throw Error(k(160));}b=c.stateNode;switch(c.tag){case 5:var d=!1;break;case 3:b=b.containerInfo;d=!0;break;case 4:b=b.containerInfo;d=!0;break;default:throw Error(k(161));}c.effectTag&16&&(Wb(b,""),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fh(c.return)){c=null;break a}c=c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;
else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}d?Oe(a,c,b):Pe(a,c,b)}function Oe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=uc));else if(4!==d&&(a=a.child,null!==a))for(Oe(a,b,c),a=a.sibling;null!==a;)Oe(a,b,c),a=a.sibling}
function Pe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?c.insertBefore(a,b):c.appendChild(a);else if(4!==d&&(a=a.child,null!==a))for(Pe(a,b,c),a=a.sibling;null!==a;)Pe(a,b,c),a=a.sibling}function Dh(a,b,c){for(var d=b,e=!1,f,g;;){if(!e){e=d.return;a:for(;;){if(null===e)throw Error(k(160));f=e.stateNode;switch(e.tag){case 5:g=!1;break a;case 3:f=f.containerInfo;g=!0;break a;case 4:f=f.containerInfo;g=!0;break a}e=e.return}e=!0}if(5===d.tag||6===d.tag){a:for(var h=
a,m=d,n=c,l=m;;)if(Ch(h,l,n),null!==l.child&&4!==l.tag)l.child.return=l,l=l.child;else{if(l===m)break a;for(;null===l.sibling;){if(null===l.return||l.return===m)break a;l=l.return}l.sibling.return=l.return;l=l.sibling}g?(h=f,m=d.stateNode,8===h.nodeType?h.parentNode.removeChild(m):h.removeChild(m)):f.removeChild(d.stateNode)}else if(4===d.tag){if(null!==d.child){f=d.stateNode.containerInfo;g=!0;d.child.return=d;d=d.child;continue}}else if(Ch(a,d,c),null!==d.child){d.child.return=d;d=d.child;continue}if(d===
b)break;for(;null===d.sibling;){if(null===d.return||d.return===b)return;d=d.return;4===d.tag&&(e=!1)}d.sibling.return=d.return;d=d.sibling}}function Qe(a,b){switch(b.tag){case 0:case 11:case 14:case 15:case 22:Ah(3,b);return;case 1:return;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps,e=null!==a?a.memoizedProps:d;a=b.type;var f=b.updateQueue;b.updateQueue=null;if(null!==f){c[vc]=d;"input"===a&&"radio"===d.type&&null!=d.name&&If(c,d);Vd(a,e);b=Vd(a,d);for(e=0;e<f.length;e+=2){var g=f[e],
h=f[e+1];"style"===g?gg(c,h):"dangerouslySetInnerHTML"===g?xh(c,h):"children"===g?Wb(c,h):xd(c,g,h,b)}switch(a){case "input":Dd(c,d);break;case "textarea":Lf(c,d);break;case "select":b=c._wrapperState.wasMultiple,c._wrapperState.wasMultiple=!!d.multiple,a=d.value,null!=a?hb(c,!!d.multiple,a,!1):b!==!!d.multiple&&(null!=d.defaultValue?hb(c,!!d.multiple,d.defaultValue,!0):hb(c,!!d.multiple,d.multiple?[]:"",!1))}}}return;case 6:if(null===b.stateNode)throw Error(k(162));b.stateNode.nodeValue=b.memoizedProps;
return;case 3:b=b.stateNode;b.hydrate&&(b.hydrate=!1,bg(b.containerInfo));return;case 12:return;case 13:c=b;null===b.memoizedState?d=!1:(d=!0,c=b.child,Re=Y());if(null!==c)a:for(a=c;;){if(5===a.tag)f=a.stateNode,d?(f=f.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(f=a.stateNode,e=a.memoizedProps.style,e=void 0!==e&&null!==e&&e.hasOwnProperty("display")?e.display:null,f.style.display=fg("display",e));else if(6===a.tag)a.stateNode.nodeValue=d?
"":a.memoizedProps;else if(13===a.tag&&null!==a.memoizedState&&null===a.memoizedState.dehydrated){f=a.child.sibling;f.return=a;a=f;continue}else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===c)break;for(;null===a.sibling;){if(null===a.return||a.return===c)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}Hh(b);return;case 19:Hh(b);return;case 17:return}throw Error(k(163));}function Hh(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=
new pj);b.forEach(function(b){var d=qj.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Ih(a,b,c){c=Ea(c,null);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){cd||(cd=!0,Se=d);Me(a,b)};return c}function Jh(a,b,c){c=Ea(c,null);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){Me(a,b);return d(e)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){"function"!==typeof d&&
(null===La?La=new Set([this]):La.add(this),Me(a,b));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ka(){return(p&(ca|ma))!==H?1073741821-(Y()/10|0):0!==dd?dd:dd=1073741821-(Y()/10|0)}function Va(a,b,c){b=b.mode;if(0===(b&2))return 1073741823;var d=Cc();if(0===(b&4))return 99===d?1073741823:1073741822;if((p&ca)!==H)return P;if(null!==c)a=Fc(a,c.timeoutMs|0||5E3,250);else switch(d){case 99:a=1073741823;break;case 98:a=Fc(a,150,100);break;case 97:case 96:a=
Fc(a,5E3,250);break;case 95:a=2;break;default:throw Error(k(326));}null!==U&&a===P&&--a;return a}function ed(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}null!==e&&
(U===e&&(Kc(b),F===bd&&Ya(e,P)),yh(e,b));return e}function fd(a){var b=a.lastExpiredTime;if(0!==b)return b;b=a.firstPendingTime;if(!Kh(a,b))return b;var c=a.lastPingedTime;a=a.nextKnownPendingLevel;a=c>a?c:a;return 2>=a&&b!==a?0:a}function V(a){if(0!==a.lastExpiredTime)a.callbackExpirationTime=1073741823,a.callbackPriority=99,a.callbackNode=Og(Te.bind(null,a));else{var b=fd(a),c=a.callbackNode;if(0===b)null!==c&&(a.callbackNode=null,a.callbackExpirationTime=0,a.callbackPriority=90);else{var d=ka();
1073741823===b?d=99:1===b||2===b?d=95:(d=10*(1073741821-b)-10*(1073741821-d),d=0>=d?99:250>=d?98:5250>=d?97:95);if(null!==c){var e=a.callbackPriority;if(a.callbackExpirationTime===b&&e>=d)return;c!==Qg&&Rg(c)}a.callbackExpirationTime=b;a.callbackPriority=d;b=1073741823===b?Og(Te.bind(null,a)):Ng(d,Lh.bind(null,a),{timeout:10*(1073741821-b)-Y()});a.callbackNode=b}}}function Lh(a,b){dd=0;if(b)return b=ka(),Ue(a,b),V(a),null;var c=fd(a);if(0!==c){b=a.callbackNode;if((p&(ca|ma))!==H)throw Error(k(327));
xb();a===U&&c===P||$a(a,c);if(null!==t){var d=p;p|=ca;var e=Mh();do try{rj();break}catch(h){Nh(a,h)}while(1);le();p=d;gd.current=e;if(F===hd)throw b=id,$a(a,c),Ya(a,c),V(a),b;if(null===t)switch(e=a.finishedWork=a.current.alternate,a.finishedExpirationTime=c,d=F,U=null,d){case Xa:case hd:throw Error(k(345));case Oh:Ue(a,2<c?2:c);break;case ad:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(1073741823===ta&&(e=Re+Ph-Y(),10<e)){if(jd){var f=a.lastPingedTime;if(0===f||f>=c){a.lastPingedTime=
c;$a(a,c);break}}f=fd(a);if(0!==f&&f!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}a.timeoutHandle=We(ab.bind(null,a),e);break}ab(a);break;case bd:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(jd&&(e=a.lastPingedTime,0===e||e>=c)){a.lastPingedTime=c;$a(a,c);break}e=fd(a);if(0!==e&&e!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}1073741823!==Yb?d=10*(1073741821-Yb)-Y():1073741823===ta?d=0:(d=10*(1073741821-ta)-5E3,e=Y(),c=10*(1073741821-c)-e,d=e-d,0>d&&(d=0),d=
(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*sj(d/1960))-d,c<d&&(d=c));if(10<d){a.timeoutHandle=We(ab.bind(null,a),d);break}ab(a);break;case Xe:if(1073741823!==ta&&null!==kd){f=ta;var g=kd;d=g.busyMinDurationMs|0;0>=d?d=0:(e=g.busyDelayMs|0,f=Y()-(10*(1073741821-f)-(g.timeoutMs|0||5E3)),d=f<=e?0:e+d-f);if(10<d){Ya(a,c);a.timeoutHandle=We(ab.bind(null,a),d);break}}ab(a);break;default:throw Error(k(329));}V(a);if(a.callbackNode===b)return Lh.bind(null,a)}}return null}function Te(a){var b=
a.lastExpiredTime;b=0!==b?b:1073741823;if((p&(ca|ma))!==H)throw Error(k(327));xb();a===U&&b===P||$a(a,b);if(null!==t){var c=p;p|=ca;var d=Mh();do try{tj();break}catch(e){Nh(a,e)}while(1);le();p=c;gd.current=d;if(F===hd)throw c=id,$a(a,b),Ya(a,b),V(a),c;if(null!==t)throw Error(k(261));a.finishedWork=a.current.alternate;a.finishedExpirationTime=b;U=null;ab(a);V(a)}return null}function uj(){if(null!==bb){var a=bb;bb=null;a.forEach(function(a,c){Ue(c,a);V(c)});ha()}}function Qh(a,b){var c=p;p|=1;try{return a(b)}finally{p=
c,p===H&&ha()}}function Rh(a,b){var c=p;p&=-2;p|=Ye;try{return a(b)}finally{p=c,p===H&&ha()}}function $a(a,b){a.finishedWork=null;a.finishedExpirationTime=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,vj(c));if(null!==t)for(c=t.return;null!==c;){var d=c;switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(q(G),q(B));break;case 3:tb();q(G);q(B);break;case 5:te(d);break;case 4:tb();break;case 13:q(D);break;case 19:q(D);break;case 10:me(d)}c=c.return}U=a;t=Sa(a.current,null);
P=b;F=Xa;id=null;Yb=ta=1073741823;kd=null;Xb=0;jd=!1}function Nh(a,b){do{try{le();Sc.current=Tc;if(Uc)for(var c=z.memoizedState;null!==c;){var d=c.queue;null!==d&&(d.pending=null);c=c.next}Ia=0;J=K=z=null;Uc=!1;if(null===t||null===t.return)return F=hd,id=b,t=null;a:{var e=a,f=t.return,g=t,h=b;b=P;g.effectTag|=2048;g.firstEffect=g.lastEffect=null;if(null!==h&&"object"===typeof h&&"function"===typeof h.then){var m=h;if(0===(g.mode&2)){var n=g.alternate;n?(g.updateQueue=n.updateQueue,g.memoizedState=
n.memoizedState,g.expirationTime=n.expirationTime):(g.updateQueue=null,g.memoizedState=null)}var l=0!==(D.current&1),k=f;do{var p;if(p=13===k.tag){var q=k.memoizedState;if(null!==q)p=null!==q.dehydrated?!0:!1;else{var w=k.memoizedProps;p=void 0===w.fallback?!1:!0!==w.unstable_avoidThisFallback?!0:l?!1:!0}}if(p){var y=k.updateQueue;if(null===y){var r=new Set;r.add(m);k.updateQueue=r}else y.add(m);if(0===(k.mode&2)){k.effectTag|=64;g.effectTag&=-2981;if(1===g.tag)if(null===g.alternate)g.tag=17;else{var O=
Ea(1073741823,null);O.tag=Jc;Fa(g,O)}g.expirationTime=1073741823;break a}h=void 0;g=b;var v=e.pingCache;null===v?(v=e.pingCache=new wj,h=new Set,v.set(m,h)):(h=v.get(m),void 0===h&&(h=new Set,v.set(m,h)));if(!h.has(g)){h.add(g);var x=xj.bind(null,e,m,g);m.then(x,x)}k.effectTag|=4096;k.expirationTime=b;break a}k=k.return}while(null!==k);h=Error((na(g.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."+
Bd(g))}F!==Xe&&(F=Oh);h=Le(h,g);k=f;do{switch(k.tag){case 3:m=h;k.effectTag|=4096;k.expirationTime=b;var A=Ih(k,m,b);Ug(k,A);break a;case 1:m=h;var u=k.type,B=k.stateNode;if(0===(k.effectTag&64)&&("function"===typeof u.getDerivedStateFromError||null!==B&&"function"===typeof B.componentDidCatch&&(null===La||!La.has(B)))){k.effectTag|=4096;k.expirationTime=b;var H=Jh(k,m,b);Ug(k,H);break a}}k=k.return}while(null!==k)}t=Sh(t)}catch(cj){b=cj;continue}break}while(1)}function Mh(a){a=gd.current;gd.current=
Tc;return null===a?Tc:a}function Vg(a,b){a<ta&&2<a&&(ta=a);null!==b&&a<Yb&&2<a&&(Yb=a,kd=b)}function Kc(a){a>Xb&&(Xb=a)}function tj(){for(;null!==t;)t=Th(t)}function rj(){for(;null!==t&&!yj();)t=Th(t)}function Th(a){var b=zj(a.alternate,a,P);a.memoizedProps=a.pendingProps;null===b&&(b=Sh(a));Uh.current=null;return b}function Sh(a){t=a;do{var b=t.alternate;a=t.return;if(0===(t.effectTag&2048)){b=hj(b,t,P);if(1===P||1!==t.childExpirationTime){for(var c=0,d=t.child;null!==d;){var e=d.expirationTime,
f=d.childExpirationTime;e>c&&(c=e);f>c&&(c=f);d=d.sibling}t.childExpirationTime=c}if(null!==b)return b;null!==a&&0===(a.effectTag&2048)&&(null===a.firstEffect&&(a.firstEffect=t.firstEffect),null!==t.lastEffect&&(null!==a.lastEffect&&(a.lastEffect.nextEffect=t.firstEffect),a.lastEffect=t.lastEffect),1<t.effectTag&&(null!==a.lastEffect?a.lastEffect.nextEffect=t:a.firstEffect=t,a.lastEffect=t))}else{b=lj(t);if(null!==b)return b.effectTag&=2047,b;null!==a&&(a.firstEffect=a.lastEffect=null,a.effectTag|=
2048)}b=t.sibling;if(null!==b)return b;t=a}while(null!==t);F===Xa&&(F=Xe);return null}function Ve(a){var b=a.expirationTime;a=a.childExpirationTime;return b>a?b:a}function ab(a){var b=Cc();Da(99,Aj.bind(null,a,b));return null}function Aj(a,b){do xb();while(null!==Zb);if((p&(ca|ma))!==H)throw Error(k(327));var c=a.finishedWork,d=a.finishedExpirationTime;if(null===c)return null;a.finishedWork=null;a.finishedExpirationTime=0;if(c===a.current)throw Error(k(177));a.callbackNode=null;a.callbackExpirationTime=
0;a.callbackPriority=90;a.nextKnownPendingLevel=0;var e=Ve(c);a.firstPendingTime=e;d<=a.lastSuspendedTime?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:d<=a.firstSuspendedTime&&(a.firstSuspendedTime=d-1);d<=a.lastPingedTime&&(a.lastPingedTime=0);d<=a.lastExpiredTime&&(a.lastExpiredTime=0);a===U&&(t=U=null,P=0);1<c.effectTag?null!==c.lastEffect?(c.lastEffect.nextEffect=c,e=c.firstEffect):e=c:e=c.firstEffect;if(null!==e){var f=p;p|=ma;Uh.current=null;Ze=tc;var g=kg();if(Xd(g)){if("selectionStart"in
g)var h={start:g.selectionStart,end:g.selectionEnd};else a:{h=(h=g.ownerDocument)&&h.defaultView||window;var m=h.getSelection&&h.getSelection();if(m&&0!==m.rangeCount){h=m.anchorNode;var n=m.anchorOffset,q=m.focusNode;m=m.focusOffset;try{h.nodeType,q.nodeType}catch(sb){h=null;break a}var ba=0,w=-1,y=-1,B=0,D=0,r=g,z=null;b:for(;;){for(var v;;){r!==h||0!==n&&3!==r.nodeType||(w=ba+n);r!==q||0!==m&&3!==r.nodeType||(y=ba+m);3===r.nodeType&&(ba+=r.nodeValue.length);if(null===(v=r.firstChild))break;z=r;
r=v}for(;;){if(r===g)break b;z===h&&++B===n&&(w=ba);z===q&&++D===m&&(y=ba);if(null!==(v=r.nextSibling))break;r=z;z=r.parentNode}r=v}h=-1===w||-1===y?null:{start:w,end:y}}else h=null}h=h||{start:0,end:0}}else h=null;$e={activeElementDetached:null,focusedElem:g,selectionRange:h};tc=!1;l=e;do try{Bj()}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=e;do try{for(g=a,h=b;null!==l;){var x=l.effectTag;x&16&&Wb(l.stateNode,"");if(x&128){var A=l.alternate;if(null!==A){var u=
A.ref;null!==u&&("function"===typeof u?u(null):u.current=null)}}switch(x&1038){case 2:Gh(l);l.effectTag&=-3;break;case 6:Gh(l);l.effectTag&=-3;Qe(l.alternate,l);break;case 1024:l.effectTag&=-1025;break;case 1028:l.effectTag&=-1025;Qe(l.alternate,l);break;case 4:Qe(l.alternate,l);break;case 8:n=l,Dh(g,n,h),Eh(n)}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);u=$e;A=kg();x=u.focusedElem;h=u.selectionRange;if(A!==x&&x&&x.ownerDocument&&jg(x.ownerDocument.documentElement,
x)){null!==h&&Xd(x)&&(A=h.start,u=h.end,void 0===u&&(u=A),"selectionStart"in x?(x.selectionStart=A,x.selectionEnd=Math.min(u,x.value.length)):(u=(A=x.ownerDocument||document)&&A.defaultView||window,u.getSelection&&(u=u.getSelection(),n=x.textContent.length,g=Math.min(h.start,n),h=void 0===h.end?g:Math.min(h.end,n),!u.extend&&g>h&&(n=h,h=g,g=n),n=ig(x,g),q=ig(x,h),n&&q&&(1!==u.rangeCount||u.anchorNode!==n.node||u.anchorOffset!==n.offset||u.focusNode!==q.node||u.focusOffset!==q.offset)&&(A=A.createRange(),
A.setStart(n.node,n.offset),u.removeAllRanges(),g>h?(u.addRange(A),u.extend(q.node,q.offset)):(A.setEnd(q.node,q.offset),u.addRange(A))))));A=[];for(u=x;u=u.parentNode;)1===u.nodeType&&A.push({element:u,left:u.scrollLeft,top:u.scrollTop});"function"===typeof x.focus&&x.focus();for(x=0;x<A.length;x++)u=A[x],u.element.scrollLeft=u.left,u.element.scrollTop=u.top}tc=!!Ze;$e=Ze=null;a.current=c;l=e;do try{for(x=a;null!==l;){var F=l.effectTag;F&36&&oj(x,l.alternate,l);if(F&128){A=void 0;var E=l.ref;if(null!==
E){var G=l.stateNode;switch(l.tag){case 5:A=G;break;default:A=G}"function"===typeof E?E(A):E.current=A}}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=null;Cj();p=f}else a.current=c;if(ld)ld=!1,Zb=a,$b=b;else for(l=e;null!==l;)b=l.nextEffect,l.nextEffect=null,l=b;b=a.firstPendingTime;0===b&&(La=null);1073741823===b?a===af?ac++:(ac=0,af=a):ac=0;"function"===typeof bf&&bf(c.stateNode,d);V(a);if(cd)throw cd=!1,a=Se,Se=null,a;if((p&Ye)!==H)return null;
ha();return null}function Bj(){for(;null!==l;){var a=l.effectTag;0!==(a&256)&&nj(l.alternate,l);0===(a&512)||ld||(ld=!0,Ng(97,function(){xb();return null}));l=l.nextEffect}}function xb(){if(90!==$b){var a=97<$b?97:$b;$b=90;return Da(a,Dj)}}function Dj(){if(null===Zb)return!1;var a=Zb;Zb=null;if((p&(ca|ma))!==H)throw Error(k(331));var b=p;p|=ma;for(a=a.current.firstEffect;null!==a;){try{var c=a;if(0!==(c.effectTag&512))switch(c.tag){case 0:case 11:case 15:case 22:Ah(5,c),Bh(5,c)}}catch(d){if(null===
a)throw Error(k(330));Za(a,d)}c=a.nextEffect;a.nextEffect=null;a=c}p=b;ha();return!0}function Vh(a,b,c){b=Le(c,b);b=Ih(a,b,1073741823);Fa(a,b);a=ed(a,1073741823);null!==a&&V(a)}function Za(a,b){if(3===a.tag)Vh(a,a,b);else for(var c=a.return;null!==c;){if(3===c.tag){Vh(c,a,b);break}else if(1===c.tag){var d=c.stateNode;if("function"===typeof c.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===La||!La.has(d))){a=Le(b,a);a=Jh(c,a,1073741823);Fa(c,a);c=ed(c,1073741823);null!==
c&&V(c);break}}c=c.return}}function xj(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);U===a&&P===c?F===bd||F===ad&&1073741823===ta&&Y()-Re<Ph?$a(a,P):jd=!0:Kh(a,c)&&(b=a.lastPingedTime,0!==b&&b<c||(a.lastPingedTime=c,V(a)))}function qj(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=0;0===b&&(b=ka(),b=Va(b,a,null));a=ed(a,b);null!==a&&V(a)}function Ej(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=
b.inject(a);bf=function(a,e){try{b.onCommitFiberRoot(c,a,void 0,64===(a.current.effectTag&64))}catch(f){}};Ne=function(a){try{b.onCommitFiberUnmount(c,a)}catch(e){}}}catch(d){}return!0}function Fj(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=
null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Ge(a){a=a.prototype;return!(!a||!a.isReactComponent)}function Gj(a){if("function"===typeof a)return Ge(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===zd)return 11;if(a===Ad)return 14}return 2}function Sa(a,b){var c=a.alternate;null===c?(c=la(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=
null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{expirationTime:b.expirationTime,firstContext:b.firstContext,responders:b.responders};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function Oc(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)Ge(a)&&(g=1);else if("string"===typeof a)g=
5;else a:switch(a){case Ma:return Ha(c.children,e,f,b);case Hj:g=8;e|=7;break;case Af:g=8;e|=1;break;case kc:return a=la(12,c,b,e|8),a.elementType=kc,a.type=kc,a.expirationTime=f,a;case lc:return a=la(13,c,b,e),a.type=lc,a.elementType=lc,a.expirationTime=f,a;case yd:return a=la(19,c,b,e),a.elementType=yd,a.expirationTime=f,a;default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case Cf:g=10;break a;case Bf:g=9;break a;case zd:g=11;break a;case Ad:g=14;break a;case Ef:g=16;d=null;break a;case Df:g=
22;break a}throw Error(k(130,null==a?a:typeof a,""));}b=la(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function Ha(a,b,c,d){a=la(7,a,d,b);a.expirationTime=c;return a}function qe(a,b,c){a=la(6,a,null,b);a.expirationTime=c;return a}function re(a,b,c){b=la(4,null!==a.children?a.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Ij(a,b,c){this.tag=b;this.current=null;this.containerInfo=
a;this.pingCache=this.pendingChildren=null;this.finishedExpirationTime=0;this.finishedWork=null;this.timeoutHandle=-1;this.pendingContext=this.context=null;this.hydrate=c;this.callbackNode=null;this.callbackPriority=90;this.lastExpiredTime=this.lastPingedTime=this.nextKnownPendingLevel=this.lastSuspendedTime=this.firstSuspendedTime=this.firstPendingTime=0}function Kh(a,b){var c=a.firstSuspendedTime;a=a.lastSuspendedTime;return 0!==c&&c>=b&&a<=b}function Ya(a,b){var c=a.firstSuspendedTime,d=a.lastSuspendedTime;
c<b&&(a.firstSuspendedTime=b);if(d>b||0===c)a.lastSuspendedTime=b;b<=a.lastPingedTime&&(a.lastPingedTime=0);b<=a.lastExpiredTime&&(a.lastExpiredTime=0)}function yh(a,b){b>a.firstPendingTime&&(a.firstPendingTime=b);var c=a.firstSuspendedTime;0!==c&&(b>=c?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:b>=a.lastSuspendedTime&&(a.lastSuspendedTime=b+1),b>a.nextKnownPendingLevel&&(a.nextKnownPendingLevel=b))}function Ue(a,b){var c=a.lastExpiredTime;if(0===c||c>b)a.lastExpiredTime=b}
function md(a,b,c,d){var e=b.current,f=ka(),g=Vb.suspense;f=Va(f,e,g);a:if(c){c=c._reactInternalFiber;b:{if(Na(c)!==c||1!==c.tag)throw Error(k(170));var h=c;do{switch(h.tag){case 3:h=h.stateNode.context;break b;case 1:if(N(h.type)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}}h=h.return}while(null!==h);throw Error(k(171));}if(1===c.tag){var m=c.type;if(N(m)){c=Gg(c,m,h);break a}}c=h}else c=Ca;null===b.context?b.context=c:b.pendingContext=c;b=Ea(f,g);b.payload={element:a};d=void 0===
d?null:d;null!==d&&(b.callback=d);Fa(e,b);Ja(e,f);return f}function cf(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Wh(a,b){a=a.memoizedState;null!==a&&null!==a.dehydrated&&a.retryTime<b&&(a.retryTime=b)}function df(a,b){Wh(a,b);(a=a.alternate)&&Wh(a,b)}function ef(a,b,c){c=null!=c&&!0===c.hydrate;var d=new Ij(a,b,c),e=la(3,null,null,2===b?7:1===b?3:0);d.current=e;e.stateNode=d;ne(e);a[Lb]=d.current;c&&0!==b&&
xi(a,9===a.nodeType?a:a.ownerDocument);this._internalRoot=d}function bc(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Jj(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute("data-reactroot")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new ef(a,0,b?{hydrate:!0}:void 0)}function nd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f._internalRoot;
if("function"===typeof e){var h=e;e=function(){var a=cf(g);h.call(a)}}md(b,g,a,e)}else{f=c._reactRootContainer=Jj(c,d);g=f._internalRoot;if("function"===typeof e){var m=e;e=function(){var a=cf(g);m.call(a)}}Rh(function(){md(b,g,a,e)})}return cf(g)}function Kj(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:gb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Xh(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;
if(!bc(b))throw Error(k(200));return Kj(a,b,null,c)}if(!ea)throw Error(k(227));var ki=function(a,b,c,d,e,f,g,h,m){var n=Array.prototype.slice.call(arguments,3);try{b.apply(c,n)}catch(C){this.onError(C)}},yb=!1,gc=null,hc=!1,pd=null,li={onError:function(a){yb=!0;gc=a}},td=null,rf=null,mf=null,ic=null,cb={},jc=[],qd={},db={},rd={},wa=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),M=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign,
sd=null,eb=null,fb=null,ee=function(a,b){return a(b)},eg=function(a,b,c,d,e){return a(b,c,d,e)},vd=function(){},vf=ee,Oa=!1,wd=!1,Z=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Lj=Z.unstable_cancelCallback,ff=Z.unstable_now,$f=Z.unstable_scheduleCallback,Mj=Z.unstable_shouldYield,Yh=Z.unstable_requestPaint,Pd=Z.unstable_runWithPriority,Nj=Z.unstable_getCurrentPriorityLevel,Oj=Z.unstable_ImmediatePriority,Zh=Z.unstable_UserBlockingPriority,ag=Z.unstable_NormalPriority,Pj=Z.unstable_LowPriority,
Qj=Z.unstable_IdlePriority,oi=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,wf=Object.prototype.hasOwnProperty,yf={},xf={},E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){E[a]=
new L(a,0,!1,a,null,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];E[b]=new L(b,1,!1,a[1],null,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){E[a]=new L(a,2,!1,a.toLowerCase(),null,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){E[a]=new L(a,2,!1,a,null,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){E[a]=
new L(a,3,!1,a.toLowerCase(),null,!1)});["checked","multiple","muted","selected"].forEach(function(a){E[a]=new L(a,3,!0,a,null,!1)});["capture","download"].forEach(function(a){E[a]=new L(a,4,!1,a,null,!1)});["cols","rows","size","span"].forEach(function(a){E[a]=new L(a,6,!1,a,null,!1)});["rowSpan","start"].forEach(function(a){E[a]=new L(a,5,!1,a.toLowerCase(),null,!1)});var gf=/[\-:]([a-z])/g,hf=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
a.replace(gf,hf);E[b]=new L(b,1,!1,a,null,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/1999/xlink",!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1)});["tabIndex","crossOrigin"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!1)});E.xlinkHref=new L("xlinkHref",1,
!1,"xlink:href","http://www.w3.org/1999/xlink",!0);["src","href","action","formAction"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!0)});var da=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;da.hasOwnProperty("ReactCurrentDispatcher")||(da.ReactCurrentDispatcher={current:null});da.hasOwnProperty("ReactCurrentBatchConfig")||(da.ReactCurrentBatchConfig={suspense:null});var si=/^(.*)[\\\/]/,Q="function"===typeof Symbol&&Symbol.for,Pc=Q?Symbol.for("react.element"):60103,gb=Q?Symbol.for("react.portal"):
60106,Ma=Q?Symbol.for("react.fragment"):60107,Af=Q?Symbol.for("react.strict_mode"):60108,kc=Q?Symbol.for("react.profiler"):60114,Cf=Q?Symbol.for("react.provider"):60109,Bf=Q?Symbol.for("react.context"):60110,Hj=Q?Symbol.for("react.concurrent_mode"):60111,zd=Q?Symbol.for("react.forward_ref"):60112,lc=Q?Symbol.for("react.suspense"):60113,yd=Q?Symbol.for("react.suspense_list"):60120,Ad=Q?Symbol.for("react.memo"):60115,Ef=Q?Symbol.for("react.lazy"):60116,Df=Q?Symbol.for("react.block"):60121,zf="function"===
typeof Symbol&&Symbol.iterator,od,xh=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{od=od||document.createElement("div");od.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=od.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Wb=function(a,
b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},ib={animationend:nc("Animation","AnimationEnd"),animationiteration:nc("Animation","AnimationIteration"),animationstart:nc("Animation","AnimationStart"),transitionend:nc("Transition","TransitionEnd")},Id={},Of={};wa&&(Of=document.createElement("div").style,"AnimationEvent"in window||(delete ib.animationend.animation,delete ib.animationiteration.animation,delete ib.animationstart.animation),"TransitionEvent"in
window||delete ib.transitionend.transition);var $h=oc("animationend"),ai=oc("animationiteration"),bi=oc("animationstart"),ci=oc("transitionend"),Db="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Pf=new ("function"===typeof WeakMap?WeakMap:Map),Ab=null,wi=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;
if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)lf(a,b[d],c[d]);else b&&lf(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},qc=[],Rd=!1,fa=[],xa=null,ya=null,za=null,Eb=new Map,Fb=new Map,Jb=[],Nd="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput close cancel copy cut paste click change contextmenu reset submit".split(" "),
yi="focus blur dragenter dragleave mouseover mouseout pointerover pointerout gotpointercapture lostpointercapture".split(" "),dg={},cg=new Map,Td=new Map,Rj=["abort","abort",$h,"animationEnd",ai,"animationIteration",bi,"animationStart","canplay","canPlay","canplaythrough","canPlayThrough","durationchange","durationChange","emptied","emptied","encrypted","encrypted","ended","ended","error","error","gotpointercapture","gotPointerCapture","load","load","loadeddata","loadedData","loadedmetadata","loadedMetadata",
"loadstart","loadStart","lostpointercapture","lostPointerCapture","playing","playing","progress","progress","seeking","seeking","stalled","stalled","suspend","suspend","timeupdate","timeUpdate",ci,"transitionEnd","waiting","waiting"];Sd("blur blur cancel cancel click click close close contextmenu contextMenu copy copy cut cut auxclick auxClick dblclick doubleClick dragend dragEnd dragstart dragStart drop drop focus focus input input invalid invalid keydown keyDown keypress keyPress keyup keyUp mousedown mouseDown mouseup mouseUp paste paste pause pause play play pointercancel pointerCancel pointerdown pointerDown pointerup pointerUp ratechange rateChange reset reset seeked seeked submit submit touchcancel touchCancel touchend touchEnd touchstart touchStart volumechange volumeChange".split(" "),
0);Sd("drag drag dragenter dragEnter dragexit dragExit dragleave dragLeave dragover dragOver mousemove mouseMove mouseout mouseOut mouseover mouseOver pointermove pointerMove pointerout pointerOut pointerover pointerOver scroll scroll toggle toggle touchmove touchMove wheel wheel".split(" "),1);Sd(Rj,2);(function(a,b){for(var c=0;c<a.length;c++)Td.set(a[c],b)})("change selectionchange textInput compositionstart compositionend compositionupdate".split(" "),0);var Hi=Zh,Gi=Pd,tc=!0,Kb={animationIterationCount:!0,
borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,
strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Sj=["Webkit","ms","Moz","O"];Object.keys(Kb).forEach(function(a){Sj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);Kb[b]=Kb[a]})});var Ii=M({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ng="$",og="/$",$d="$?",Zd="$!",Ze=null,$e=null,We="function"===typeof setTimeout?setTimeout:void 0,vj="function"===
typeof clearTimeout?clearTimeout:void 0,jf=Math.random().toString(36).slice(2),Aa="__reactInternalInstance$"+jf,vc="__reactEventHandlers$"+jf,Lb="__reactContainere$"+jf,Ba=null,ce=null,wc=null;M(R.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=xc)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():"unknown"!==
typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=xc)},persist:function(){this.isPersistent=xc},isPersistent:yc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=yc;this._dispatchInstances=this._dispatchListeners=null}});R.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||
Date.now()},defaultPrevented:null,isTrusted:null};R.extend=function(a){function b(){return c.apply(this,arguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;M(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=M({},c.Interface,a);b.extend=c.extend;sg(b);return b};sg(R);var Tj=R.extend({data:null}),Uj=R.extend({data:null}),Ni=[9,13,27,32],de=wa&&"CompositionEvent"in window,cc=null;wa&&"documentMode"in document&&(cc=document.documentMode);var Vj=wa&&"TextEvent"in window&&
!cc,xg=wa&&(!de||cc&&8<cc&&11>=cc),wg=String.fromCharCode(32),ua={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},
dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},vg=!1,mb=!1,Wj={eventTypes:ua,extractEvents:function(a,b,c,d,e){var f;if(de)b:{switch(a){case "compositionstart":var g=ua.compositionStart;break b;case "compositionend":g=ua.compositionEnd;break b;case "compositionupdate":g=
ua.compositionUpdate;break b}g=void 0}else mb?tg(a,c)&&(g=ua.compositionEnd):"keydown"===a&&229===c.keyCode&&(g=ua.compositionStart);g?(xg&&"ko"!==c.locale&&(mb||g!==ua.compositionStart?g===ua.compositionEnd&&mb&&(f=rg()):(Ba=d,ce="value"in Ba?Ba.value:Ba.textContent,mb=!0)),e=Tj.getPooled(g,b,c,d),f?e.data=f:(f=ug(c),null!==f&&(e.data=f)),lb(e),f=e):f=null;(a=Vj?Oi(a,c):Pi(a,c))?(b=Uj.getPooled(ua.beforeInput,b,c,d),b.data=a,lb(b)):b=null;return null===f?b:null===b?f:[f,b]}},Qi={color:!0,date:!0,
datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ag={change:{phasedRegistrationNames:{bubbled:"onChange",captured:"onChangeCapture"},dependencies:"blur change click focus input keydown keyup selectionchange".split(" ")}},Mb=null,Nb=null,kf=!1;wa&&(kf=Tf("input")&&(!document.documentMode||9<document.documentMode));var Xj={eventTypes:Ag,_isInputEventSupported:kf,extractEvents:function(a,b,c,d,e){e=b?Pa(b):window;var f=
e.nodeName&&e.nodeName.toLowerCase();if("select"===f||"input"===f&&"file"===e.type)var g=Si;else if(yg(e))if(kf)g=Wi;else{g=Ui;var h=Ti}else(f=e.nodeName)&&"input"===f.toLowerCase()&&("checkbox"===e.type||"radio"===e.type)&&(g=Vi);if(g&&(g=g(a,b)))return zg(g,c,d);h&&h(a,e,b);"blur"===a&&(a=e._wrapperState)&&a.controlled&&"number"===e.type&&Ed(e,"number",e.value)}},dc=R.extend({view:null,detail:null}),Yi={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},di=0,ei=0,fi=!1,gi=!1,ec=dc.extend({screenX:null,
screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:fe,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if("movementX"in a)return a.movementX;var b=di;di=a.screenX;return fi?"mousemove"===a.type?a.screenX-b:0:(fi=!0,0)},movementY:function(a){if("movementY"in a)return a.movementY;var b=ei;ei=a.screenY;return gi?"mousemove"===
a.type?a.screenY-b:0:(gi=!0,0)}}),hi=ec.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),fc={mouseEnter:{registrationName:"onMouseEnter",dependencies:["mouseout","mouseover"]},mouseLeave:{registrationName:"onMouseLeave",dependencies:["mouseout","mouseover"]},pointerEnter:{registrationName:"onPointerEnter",dependencies:["pointerout","pointerover"]},pointerLeave:{registrationName:"onPointerLeave",dependencies:["pointerout",
"pointerover"]}},Yj={eventTypes:fc,extractEvents:function(a,b,c,d,e){var f="mouseover"===a||"pointerover"===a,g="mouseout"===a||"pointerout"===a;if(f&&0===(e&32)&&(c.relatedTarget||c.fromElement)||!g&&!f)return null;f=d.window===d?d:(f=d.ownerDocument)?f.defaultView||f.parentWindow:window;if(g){if(g=b,b=(b=c.relatedTarget||c.toElement)?Bb(b):null,null!==b){var h=Na(b);if(b!==h||5!==b.tag&&6!==b.tag)b=null}}else g=null;if(g===b)return null;if("mouseout"===a||"mouseover"===a){var m=ec;var n=fc.mouseLeave;
var l=fc.mouseEnter;var k="mouse"}else if("pointerout"===a||"pointerover"===a)m=hi,n=fc.pointerLeave,l=fc.pointerEnter,k="pointer";a=null==g?f:Pa(g);f=null==b?f:Pa(b);n=m.getPooled(n,g,c,d);n.type=k+"leave";n.target=a;n.relatedTarget=f;c=m.getPooled(l,b,c,d);c.type=k+"enter";c.target=f;c.relatedTarget=a;d=g;k=b;if(d&&k)a:{m=d;l=k;g=0;for(a=m;a;a=pa(a))g++;a=0;for(b=l;b;b=pa(b))a++;for(;0<g-a;)m=pa(m),g--;for(;0<a-g;)l=pa(l),a--;for(;g--;){if(m===l||m===l.alternate)break a;m=pa(m);l=pa(l)}m=null}else m=
null;l=m;for(m=[];d&&d!==l;){g=d.alternate;if(null!==g&&g===l)break;m.push(d);d=pa(d)}for(d=[];k&&k!==l;){g=k.alternate;if(null!==g&&g===l)break;d.push(k);k=pa(k)}for(k=0;k<m.length;k++)be(m[k],"bubbled",n);for(k=d.length;0<k--;)be(d[k],"captured",c);return 0===(e&64)?[n]:[n,c]}},Qa="function"===typeof Object.is?Object.is:Zi,$i=Object.prototype.hasOwnProperty,Zj=wa&&"documentMode"in document&&11>=document.documentMode,Eg={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},
dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},nb=null,he=null,Pb=null,ge=!1,ak={eventTypes:Eg,extractEvents:function(a,b,c,d,e,f){e=f||(d.window===d?d.document:9===d.nodeType?d:d.ownerDocument);if(!(f=!e)){a:{e=Jd(e);f=rd.onSelect;for(var g=0;g<f.length;g++)if(!e.has(f[g])){e=!1;break a}e=!0}f=!e}if(f)return null;e=b?Pa(b):window;switch(a){case "focus":if(yg(e)||"true"===e.contentEditable)nb=e,he=b,Pb=null;break;case "blur":Pb=he=nb=null;
break;case "mousedown":ge=!0;break;case "contextmenu":case "mouseup":case "dragend":return ge=!1,Dg(c,d);case "selectionchange":if(Zj)break;case "keydown":case "keyup":return Dg(c,d)}return null}},bk=R.extend({animationName:null,elapsedTime:null,pseudoElement:null}),ck=R.extend({clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),dk=dc.extend({relatedTarget:null}),ek={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",
Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},fk={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",
224:"Meta"},gk=dc.extend({key:function(a){if(a.key){var b=ek[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=Ac(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?fk[a.keyCode]||"Unidentified":""},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:fe,charCode:function(a){return"keypress"===a.type?Ac(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===
a.type?Ac(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),hk=ec.extend({dataTransfer:null}),ik=dc.extend({touches:null,targetTouches:null,changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:fe}),jk=R.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),kk=ec.extend({deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?
-a.wheelDelta:0},deltaZ:null,deltaMode:null}),lk={eventTypes:dg,extractEvents:function(a,b,c,d,e){e=cg.get(a);if(!e)return null;switch(a){case "keypress":if(0===Ac(c))return null;case "keydown":case "keyup":a=gk;break;case "blur":case "focus":a=dk;break;case "click":if(2===c.button)return null;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":a=ec;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":a=
hk;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":a=ik;break;case $h:case ai:case bi:a=bk;break;case ci:a=jk;break;case "scroll":a=dc;break;case "wheel":a=kk;break;case "copy":case "cut":case "paste":a=ck;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":a=hi;break;default:a=R}b=a.getPooled(e,b,c,d);lb(b);return b}};(function(a){if(ic)throw Error(k(101));
ic=Array.prototype.slice.call(a);nf()})("ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin".split(" "));(function(a,b,c){td=a;rf=b;mf=c})(ae,Hb,Pa);pf({SimpleEventPlugin:lk,EnterLeaveEventPlugin:Yj,ChangeEventPlugin:Xj,SelectEventPlugin:ak,BeforeInputEventPlugin:Wj});var ie=[],ob=-1,Ca={},B={current:Ca},G={current:!1},Ra=Ca,bj=Pd,je=$f,Rg=Lj,aj=Nj,Dc=Oj,Ig=Zh,Jg=ag,Kg=Pj,Lg=Qj,Qg={},yj=Mj,Cj=void 0!==Yh?Yh:function(){},qa=null,
Ec=null,ke=!1,ii=ff(),Y=1E4>ii?ff:function(){return ff()-ii},Ic={current:null},Hc=null,qb=null,Gc=null,Tg=0,Jc=2,Ga=!1,Vb=da.ReactCurrentBatchConfig,$g=(new ea.Component).refs,Mc={isMounted:function(a){return(a=a._reactInternalFiber)?Na(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;d=Va(d,a,e);e=Ea(d,e);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;
d=Va(d,a,e);e=Ea(d,e);e.tag=1;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka(),d=Vb.suspense;c=Va(c,a,d);d=Ea(c,d);d.tag=Jc;void 0!==b&&null!==b&&(d.callback=b);Fa(a,d);Ja(a,c)}},Qc=Array.isArray,wb=ah(!0),Fe=ah(!1),Sb={},ja={current:Sb},Ub={current:Sb},Tb={current:Sb},D={current:0},Sc=da.ReactCurrentDispatcher,X=da.ReactCurrentBatchConfig,Ia=0,z=null,K=null,J=null,Uc=!1,Tc={readContext:W,useCallback:S,useContext:S,
useEffect:S,useImperativeHandle:S,useLayoutEffect:S,useMemo:S,useReducer:S,useRef:S,useState:S,useDebugValue:S,useResponder:S,useDeferredValue:S,useTransition:S},dj={readContext:W,useCallback:ih,useContext:W,useEffect:eh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return ze(4,2,gh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return ze(4,2,a,b)},useMemo:function(a,b){var c=ub();b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=
ub();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={pending:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=ch.bind(null,z,a);return[d.memoizedState,a]},useRef:function(a){var b=ub();a={current:a};return b.memoizedState=a},useState:xe,useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=xe(a),d=c[0],e=c[1];eh(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=
xe(!1),c=b[0];b=b[1];return[ih(Ce.bind(null,b,a),[b,a]),c]}},ej={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Vc,useRef:dh,useState:function(a){return Vc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Vc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Vc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,
b,a),[b,a]),c]}},fj={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Wc,useRef:dh,useState:function(a){return Wc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Wc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Wc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,b,a),[b,a]),c]}},ra=null,Ka=null,Wa=
!1,gj=da.ReactCurrentOwner,ia=!1,Je={dehydrated:null,retryTime:0};var jj=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};var wh=function(a){};var ij=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ta(ja.current);a=null;switch(c){case "input":f=
Cd(g,f);d=Cd(g,d);a=[];break;case "option":f=Fd(g,f);d=Fd(g,d);a=[];break;case "select":f=M({},f,{value:void 0});d=M({},d,{value:void 0});a=[];break;case "textarea":f=Gd(g,f);d=Gd(g,d);a=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(g.onclick=uc)}Ud(c,d);var h,m;c=null;for(h in f)if(!d.hasOwnProperty(h)&&f.hasOwnProperty(h)&&null!=f[h])if("style"===h)for(m in g=f[h],g)g.hasOwnProperty(m)&&(c||(c={}),c[m]="");else"dangerouslySetInnerHTML"!==h&&"children"!==h&&"suppressContentEditableWarning"!==
h&&"suppressHydrationWarning"!==h&&"autoFocus"!==h&&(db.hasOwnProperty(h)?a||(a=[]):(a=a||[]).push(h,null));for(h in d){var k=d[h];g=null!=f?f[h]:void 0;if(d.hasOwnProperty(h)&&k!==g&&(null!=k||null!=g))if("style"===h)if(g){for(m in g)!g.hasOwnProperty(m)||k&&k.hasOwnProperty(m)||(c||(c={}),c[m]="");for(m in k)k.hasOwnProperty(m)&&g[m]!==k[m]&&(c||(c={}),c[m]=k[m])}else c||(a||(a=[]),a.push(h,c)),c=k;else"dangerouslySetInnerHTML"===h?(k=k?k.__html:void 0,g=g?g.__html:void 0,null!=k&&g!==k&&(a=a||
[]).push(h,k)):"children"===h?g===k||"string"!==typeof k&&"number"!==typeof k||(a=a||[]).push(h,""+k):"suppressContentEditableWarning"!==h&&"suppressHydrationWarning"!==h&&(db.hasOwnProperty(h)?(null!=k&&oa(e,h),a||g===k||(a=[])):(a=a||[]).push(h,k))}c&&(a=a||[]).push("style",c);e=a;if(b.updateQueue=e)b.effectTag|=4}};var kj=function(a,b,c,d){c!==d&&(b.effectTag|=4)};var pj="function"===typeof WeakSet?WeakSet:Set,wj="function"===typeof WeakMap?WeakMap:Map,sj=Math.ceil,gd=da.ReactCurrentDispatcher,
Uh=da.ReactCurrentOwner,H=0,Ye=8,ca=16,ma=32,Xa=0,hd=1,Oh=2,ad=3,bd=4,Xe=5,p=H,U=null,t=null,P=0,F=Xa,id=null,ta=1073741823,Yb=1073741823,kd=null,Xb=0,jd=!1,Re=0,Ph=500,l=null,cd=!1,Se=null,La=null,ld=!1,Zb=null,$b=90,bb=null,ac=0,af=null,dd=0,Ja=function(a,b){if(50<ac)throw ac=0,af=null,Error(k(185));a=ed(a,b);if(null!==a){var c=Cc();1073741823===b?(p&Ye)!==H&&(p&(ca|ma))===H?Te(a):(V(a),p===H&&ha()):V(a);(p&4)===H||98!==c&&99!==c||(null===bb?bb=new Map([[a,b]]):(c=bb.get(a),(void 0===c||c>b)&&bb.set(a,
b)))}};var zj=function(a,b,c){var d=b.expirationTime;if(null!==a){var e=b.pendingProps;if(a.memoizedProps!==e||G.current)ia=!0;else{if(d<c){ia=!1;switch(b.tag){case 3:sh(b);Ee();break;case 5:bh(b);if(b.mode&4&&1!==c&&e.hidden)return b.expirationTime=b.childExpirationTime=1,null;break;case 1:N(b.type)&&Bc(b);break;case 4:se(b,b.stateNode.containerInfo);break;case 10:d=b.memoizedProps.value;e=b.type._context;y(Ic,e._currentValue);e._currentValue=d;break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;
if(0!==d&&d>=c)return th(a,b,c);y(D,D.current&1);b=sa(a,b,c);return null!==b?b.sibling:null}y(D,D.current&1);break;case 19:d=b.childExpirationTime>=c;if(0!==(a.effectTag&64)){if(d)return vh(a,b,c);b.effectTag|=64}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null);y(D,D.current);if(!d)return null}return sa(a,b,c)}ia=!1}}else ia=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.type;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;e=pb(b,B.current);rb(b,c);e=we(null,
b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=1;b.memoizedState=null;b.updateQueue=null;if(N(d)){var f=!0;Bc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;ne(b);var g=d.getDerivedStateFromProps;"function"===typeof g&&Lc(b,d,g,a);e.updater=Mc;b.stateNode=e;e._reactInternalFiber=b;pe(b,d,a,c);b=Ie(null,b,d,!0,f,c)}else b.tag=0,T(null,b,e,c),b=b.child;return b;case 16:a:{e=b.elementType;null!==a&&(a.alternate=
null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;ri(e);if(1!==e._status)throw e._result;e=e._result;b.type=e;f=b.tag=Gj(e);a=aa(e,a);switch(f){case 0:b=He(null,b,e,a,c);break a;case 1:b=rh(null,b,e,a,c);break a;case 11:b=nh(null,b,e,a,c);break a;case 14:b=oh(null,b,e,aa(e.type,a),d,c);break a}throw Error(k(306,e,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),He(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),rh(a,b,d,e,c);
case 3:sh(b);d=b.updateQueue;if(null===a||null===d)throw Error(k(282));d=b.pendingProps;e=b.memoizedState;e=null!==e?e.element:null;oe(a,b);Qb(b,d,null,c);d=b.memoizedState.element;if(d===e)Ee(),b=sa(a,b,c);else{if(e=b.stateNode.hydrate)Ka=kb(b.stateNode.containerInfo.firstChild),ra=b,e=Wa=!0;if(e)for(c=Fe(b,null,d,c),b.child=c;c;)c.effectTag=c.effectTag&-3|1024,c=c.sibling;else T(a,b,d,c),Ee();b=b.child}return b;case 5:return bh(b),null===a&&De(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:
null,g=e.children,Yd(d,e)?g=null:null!==f&&Yd(d,f)&&(b.effectTag|=16),qh(a,b),b.mode&4&&1!==c&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(T(a,b,g,c),b=b.child),b;case 6:return null===a&&De(b),null;case 13:return th(a,b,c);case 4:return se(b,b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=wb(b,null,d,c):T(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),nh(a,b,d,e,c);case 7:return T(a,b,b.pendingProps,c),b.child;case 8:return T(a,
b,b.pendingProps.children,c),b.child;case 12:return T(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;var h=b.type._context;y(Ic,h._currentValue);h._currentValue=f;if(null!==g)if(h=g.value,f=Qa(h,f)?0:("function"===typeof d._calculateChangedBits?d._calculateChangedBits(h,f):1073741823)|0,0===f){if(g.children===e.children&&!G.current){b=sa(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var m=h.dependencies;if(null!==
m){g=h.child;for(var l=m.firstContext;null!==l;){if(l.context===d&&0!==(l.observedBits&f)){1===h.tag&&(l=Ea(c,null),l.tag=Jc,Fa(h,l));h.expirationTime<c&&(h.expirationTime=c);l=h.alternate;null!==l&&l.expirationTime<c&&(l.expirationTime=c);Sg(h.return,c);m.expirationTime<c&&(m.expirationTime=c);break}l=l.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=
g}T(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,d=f.children,rb(b,c),e=W(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,T(a,b,d,c),b.child;case 14:return e=b.type,f=aa(e,b.pendingProps),f=aa(e.type,f),oh(a,b,e,f,d,c);case 15:return ph(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,N(d)?(a=!0,Bc(b)):a=!1,rb(b,c),Yg(b,d,e),pe(b,d,e,c),Ie(null,
b,d,!0,a,c);case 19:return vh(a,b,c)}throw Error(k(156,b.tag));};var bf=null,Ne=null,la=function(a,b,c,d){return new Fj(a,b,c,d)};ef.prototype.render=function(a){md(a,this._internalRoot,null,null)};ef.prototype.unmount=function(){var a=this._internalRoot,b=a.containerInfo;md(null,a,null,function(){b[Lb]=null})};var Di=function(a){if(13===a.tag){var b=Fc(ka(),150,100);Ja(a,b);df(a,b)}};var Yf=function(a){13===a.tag&&(Ja(a,3),df(a,3))};var Bi=function(a){if(13===a.tag){var b=ka();b=Va(b,a,null);Ja(a,
b);df(a,b)}};sd=function(a,b,c){switch(b){case "input":Dd(a,c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=ae(d);if(!e)throw Error(k(90));Gf(d);Dd(d,e)}}}break;case "textarea":Lf(a,c);break;case "select":b=c.value,null!=b&&hb(a,!!c.multiple,b,!1)}};(function(a,b,c,d){ee=a;eg=b;vd=c;vf=d})(Qh,function(a,b,c,d,e){var f=p;p|=4;
try{return Da(98,a.bind(null,b,c,d,e))}finally{p=f,p===H&&ha()}},function(){(p&(1|ca|ma))===H&&(uj(),xb())},function(a,b){var c=p;p|=2;try{return a(b)}finally{p=c,p===H&&ha()}});var mk={Events:[Hb,Pa,ae,pf,qd,lb,function(a){Kd(a,Ki)},sf,tf,sc,pc,xb,{current:!1}]};(function(a){var b=a.findFiberByHostInstance;return Ej(M({},a,{overrideHookState:null,overrideProps:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:da.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=Sf(a);
return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null}))})({findFiberByHostInstance:Bb,bundleType:0,version:"16.13.1",rendererPackageName:"react-dom"});I.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=mk;I.createPortal=Xh;I.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;if(void 0===
b){if("function"===typeof a.render)throw Error(k(188));throw Error(k(268,Object.keys(a)));}a=Sf(b);a=null===a?null:a.stateNode;return a};I.flushSync=function(a,b){if((p&(ca|ma))!==H)throw Error(k(187));var c=p;p|=1;try{return Da(99,a.bind(null,b))}finally{p=c,ha()}};I.hydrate=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!0,c)};I.render=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!1,c)};I.unmountComponentAtNode=function(a){if(!bc(a))throw Error(k(40));return a._reactRootContainer?
(Rh(function(){nd(null,null,a,!1,function(){a._reactRootContainer=null;a[Lb]=null})}),!0):!1};I.unstable_batchedUpdates=Qh;I.unstable_createPortal=function(a,b){return Xh(a,b,2<arguments.length&&void 0!==arguments[2]?arguments[2]:null)};I.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!bc(c))throw Error(k(200));if(null==a||void 0===a._reactInternalFiber)throw Error(k(38));return nd(a,b,c,!1,d)};I.version="16.13.1"});
</script>
<script>const e = React.createElement;
function pathToString(path) {
if (path[0] === '/') {
return '/' + path.slice(1).join('/');
} else {
return path.join('/');
}
}
function findCommonPath(files) {
if (!files || !files.length) {
return [];
}
function isPrefix(arr, prefix) {
if (arr.length < prefix.length) {
return false;
}
for (let i = prefix.length - 1; i >= 0; --i) {
if (arr[i] !== prefix[i]) {
return false;
}
}
return true;
}
let commonPath = files[0].path.slice(0, -1);
while (commonPath.length) {
if (files.every(file => isPrefix(file.path, commonPath))) {
break;
}
commonPath.pop();
}
return commonPath;
}
function findFolders(files) {
if (!files || !files.length) {
return [];
}
let folders = files.filter(file => file.path.length > 1).map(file => file.path[0]);
folders = [...new Set(folders)]; // unique
folders.sort();
folders = folders.map(folder => {
let filesInFolder = files
.filter(file => file.path[0] === folder)
.map(file => ({
...file,
path: file.path.slice(1),
parent: [...file.parent, file.path[0]],
}));
const children = findFolders(filesInFolder); // recursion
return {
is_folder: true,
path: [folder],
parent: files[0].parent,
children,
covered: children.reduce((sum, file) => sum + file.covered, 0),
coverable: children.reduce((sum, file) => sum + file.coverable, 0),
prevRun: {
covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
},
};
});
return [...folders, ...files.filter(file => file.path.length === 1)];
}
class App extends React.Component {
constructor(...args) {
super(...args);
this.state = {
current: [],
};
}
componentDidMount() {
this.updateStateFromLocation();
window.addEventListener('hashchange', () => this.updateStateFromLocation(), false);
}
updateStateFromLocation() {
if (window.location.hash.length > 1) {
const current = window.location.hash.slice(1).split('/').map(decodeURIComponent);
this.setState({current});
} else {
this.setState({current: []});
}
}
getCurrentPath() {
let file = this.props.root;
let path = [file];
for (let p of this.state.current) {
file = file.children.find(file => file.path[0] === p);
if (!file) {
return path;
}
path.push(file);
}
return path;
}
render() {
const path = this.getCurrentPath();
const file = path[path.length - 1];
let w = null;
if (file.is_folder) {
w = e(FilesList, {
folder: file,
onSelectFile: this.selectFile.bind(this),
onBack: path.length > 1 ? this.back.bind(this) : null,
});
} else {
w = e(DisplayFile, {
file,
onBack: this.back.bind(this),
});
}
return e('div', {className: 'app'}, w);
}
selectFile(file) {
this.setState(
({current}) => {
return {current: [...current, file.path[0]]};
},
() => this.updateHash(),
);
}
back(file) {
this.setState(
({current}) => {
return {current: current.slice(0, current.length - 1)};
},
() => this.updateHash(),
);
}
updateHash() {
if (!this.state.current || !this.state.current.length) {
window.location = '#';
} else {
window.location = '#' + this.state.current.join('/');
}
}
}
function FilesList({folder, onSelectFile, onBack}) {
let files = folder.children;
return e(
'div',
{className: 'display-folder'},
e(FileHeader, {file: folder, onBack}),
e(
'table',
{className: 'files-list'},
e('thead', {className: 'files-list__head'}, e('tr', null, e('th', null, 'Path'), e('th', null, 'Coverage'))),
e(
'tbody',
{className: 'files-list__body'},
files.map(file => e(File, {file, onClick: onSelectFile})),
),
),
);
}
function File({file, onClick}) {
const coverage = file.coverable ? (file.covered / file.coverable) * 100 : -1;
const coverageDelta =
file.prevRun && (file.covered / file.coverable) * 100 - (file.prevRun.covered / file.prevRun.coverable) * 100;
return e(
'tr',
{
className:
'files-list__file' +
(coverage >= 0 && coverage < 50 ? ' files-list__file_low' : '') +
(coverage >= 50 && coverage < 80 ? ' files-list__file_medium' : '') +
(coverage >= 80 ? ' files-list__file_high' : '') +
(file.is_folder ? ' files-list__file_folder' : ''),
onClick: () => onClick(file),
},
e('td', null, e('a', null, pathToString(file.path))),
e(
'td',
null,
file.covered + ' / ' + file.coverable + (coverage >= 0 ? ' (' + coverage.toFixed(2) + '%)' : ''),
e(
'span',
{title: 'Change from the previous run'},
coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : '',
),
),
);
}
function DisplayFile({file, onBack}) {
return e('div', {className: 'display-file'}, e(FileHeader, {file, onBack}), e(FileContent, {file}));
}
function FileHeader({file, onBack}) {
const coverage = (file.covered / file.coverable) * 100;
const coverageDelta = file.prevRun && coverage - (file.prevRun.covered / file.prevRun.coverable) * 100;
return e(
'div',
{className: 'file-header'},
onBack ? e('a', {className: 'file-header__back', onClick: onBack}, 'Back') : null,
e('div', {className: 'file-header__name'}, pathToString([...file.parent, ...file.path])),
e(
'div',
{className: 'file-header__stat'},
'Covered: ' + file.covered + ' of ' + file.coverable + (file.coverable ? ' (' + coverage.toFixed(2) + '%)' : ''),
e(
'span',
{title: 'Change from the previous run'},
coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : '',
),
e('input', {id: 'theme-toggle', type: 'checkbox', hidden: true}),
e('label', {for: 'theme-toggle', id: 'theme-toggle-label'}, '🌙'),
),
);
}
function FileContent({file}) {
return e(
'pre',
{className: 'file-content'},
file.content.split(/\r?\n/).map((line, index) => {
const trace = file.traces.find(trace => trace.line === index + 1);
const covered = trace && trace.stats.Line;
const uncovered = trace && !trace.stats.Line;
const nbHit = covered? trace.stats.Line: 0;
return e(
'div',
{ className: 'code-text-container' },
e(
'code',
{
className: 'code-line' + (covered ? ' code-line_covered' : '') + (uncovered ? ' code-line_uncovered' : ''),
},
line
),
e(
'div',
{ className: 'cover-indicator' + (covered? ' check-cover': '') + (uncovered? ' no-cover': '')},
e(
'div',
{ className: (covered? 'stat-line-hit': '')},
covered? nbHit: ""
)
)
);
}),
);
}
(function () {
const commonPath = findCommonPath(data.files);
const prevFilesMap = new Map();
previousData &&
previousData.files.forEach(file => {
const path = file.path.slice(commonPath.length).join('/');
prevFilesMap.set(path, file);
});
const files = data.files.map(file => {
const path = file.path.slice(commonPath.length);
const {covered = 0, coverable = 0} = prevFilesMap.get(path.join('/')) || {};
return {
...file,
path,
parent: commonPath,
prevRun: {covered, coverable},
};
});
const children = findFolders(files);
const root = {
is_folder: true,
children,
path: commonPath,
parent: [],
covered: children.reduce((sum, file) => sum + file.covered, 0),
coverable: children.reduce((sum, file) => sum + file.coverable, 0),
prevRun: {
covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
},
};
ReactDOM.render(e(App, {root, prevFilesMap}), document.getElementById('root'));
const toggle = document.getElementById('theme-toggle');
const label = document.getElementById('theme-toggle-label');
label.textContent = '🌙';
toggle.addEventListener('change', () => {
if (toggle.checked) {
document.documentElement.setAttribute('data-theme', 'dark');
label.textContent = '☀️';
} else {
document.documentElement.removeAttribute('data-theme');
label.textContent = '🌙';
}
});
})();
</script>
</body>
</html>