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)
794 lines
938 KiB
HTML
794 lines
938 KiB
HTML
<!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","task","rust","src","bridge","codec.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 C ABI imports. The task module reaches the codec side through\n//! `include/codec/task.h` (the task submitter), `include/codec/decoder.h`\n//! (conform/proxy work) and `include/codec/encoder.h` (export). Signatures\n//! mirror the headers verbatim.\n\nuse std::ffi::{c_char, c_int, c_uint, c_void};\n\nuse crate::bridge::render::OakCancelAtom;\nuse crate::handle::CHandle;\n\n/// `OakCodecTaskKind` enum values (`include/codec/task.h`).\npub const OAKCODEC_TASK_CONFORM: c_int = 0;\n/// `OakCodecTaskKind::OAKCODEC_TASK_PROXY`.\npub const OAKCODEC_TASK_PROXY: c_int = 1;\n/// `OAKCODEC_E_CANCELLED` (`include/codec/error.h`).\npub const OAKCODEC_E_CANCELLED: c_int = -50006;\n/// `OAKCODEC_E_INVALID` (`include/codec/error.h`).\npub const OAKCODEC_E_INVALID: c_int = -50001;\n/// `OAKCODEC_E_FAILED` (`include/codec/error.h`).\npub const OAKCODEC_E_FAILED: c_int = -50003;\n\n/// Mirror of `OakCodecTaskRequest` (`include/codec/task.h`).\n#[repr(C)]\npub struct OakCodecTaskRequest {\n\t/// `OakCodecTaskKind` as int.\n\tpub kind: c_int,\n\t/// Source media filename.\n\tpub input_filename: *const c_char,\n\t/// Final destination path.\n\tpub output_filename: *const c_char,\n\t/// Stream inside the source media.\n\tpub stream_index: c_int,\n\t/// conform: target sample rate.\n\tpub sample_rate: c_int,\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: c_int,\n\t/// proxy: target width, 0 = unspecified/divider.\n\tpub proxy_width: c_int,\n\t/// proxy: target height, 0 = unspecified/divider.\n\tpub proxy_height: c_int,\n}\n\n/// `oakcodec_task_submit_fn` callback typedef.\npub type OakCodecTaskSubmitFn =\n\tunsafe extern \"C\" fn(req: *const OakCodecTaskRequest, userdata: *mut c_void) -\u003e c_int;\n\n/// Mirror of `OakCodecFrame`/`OakFrame` handle type (`include/codec/frame.h`).\npub type OakFrame = CHandle;\n\n/// Mirror of `OakEncoder` handle type (`include/codec/encoder.h`).\npub type OakEncoder = CHandle;\n\n/// Mirror of `OakDecoder` handle type (`include/codec/decoder.h`).\npub type OakDecoder = CHandle;\n\n/// Mirror of `OakCodecProxyParams` (`include/codec/proxy.h`).\n#[repr(C)]\npub struct OakCodecProxyParams {\n\t/// Target width.\n\tpub width: c_int,\n\t/// Target height.\n\tpub height: c_int,\n\t/// Resolution divider.\n\tpub divider: c_int,\n\t/// Proxy format version.\n\tpub version: c_int,\n\t/// CRF for the proxy encode.\n\tpub crf: c_int,\n\t/// Whether audio is included.\n\tpub include_audio: c_int,\n\t/// Output container extension.\n\tpub extension: [c_char; 32],\n\t/// ffmpeg preset name.\n\tpub preset: [c_char; 32],\n}\n\n/// Mirror of `oakcodec_encoding_params` (`include/codec/encoder.h`) — the\n/// export task fills this to open the encoder. Layout mirrors the header\n/// verbatim (a zeroed struct = all tracks disabled).\n#[repr(C)]\npub struct OakCodecEncodingParams {\n\t/// Output filename.\n\tpub filename: [c_char; 1024],\n\t/// Export format id (`olive::ExportFormat::Format`).\n\tpub format: c_int,\n\t/// Whether video is exported.\n\tpub video_enabled: c_int,\n\t/// Video codec id.\n\tpub video_codec: c_int,\n\t/// Output width.\n\tpub video_width: c_int,\n\t/// Output height.\n\tpub video_height: c_int,\n\t/// Frame duration rational numerator.\n\tpub video_time_base_num: c_int,\n\t/// Frame duration rational denominator.\n\tpub video_time_base_den: c_int,\n\t/// Delivery pixel format (`OakPixelFormat` as int).\n\tpub video_pixel_format: c_int,\n\t/// Interlacing (`OAKCODEC_INTERLACE_*`).\n\tpub video_interlacing: c_int,\n\t/// Pixel aspect ratio numerator.\n\tpub video_pixel_aspect_num: c_int,\n\t/// Pixel aspect ratio denominator.\n\tpub video_pixel_aspect_den: c_int,\n\t/// Video bit rate (bit/s, 0 = default).\n\tpub video_bit_rate: i64,\n\t/// Minimum video bit rate.\n\tpub video_min_bit_rate: i64,\n\t/// Maximum video bit rate.\n\tpub video_max_bit_rate: i64,\n\t/// Video buffer size (bytes).\n\tpub video_buffer_size: i64,\n\t/// Video threads (0 = auto).\n\tpub video_threads: c_int,\n\t/// Encoded pixel format name (\"yuv420p\").\n\tpub video_pix_fmt: [c_char; 64],\n\t/// Whether the output is an image sequence.\n\tpub video_is_image_sequence: c_int,\n\t/// Scaling method (`OAKCODEC_ENCODING_SCALING_*`).\n\tpub video_scaling_method: c_int,\n\t/// Whether audio is exported.\n\tpub audio_enabled: c_int,\n\t/// Audio codec id.\n\tpub audio_codec: c_int,\n\t/// Audio sample rate.\n\tpub audio_sample_rate: c_int,\n\t/// Audio channel layout mask.\n\tpub audio_channel_layout: u64,\n\t/// Audio sample format (`olive::core::SampleFormat::Format` as int).\n\tpub audio_sample_format: c_int,\n\t/// Audio bit rate.\n\tpub audio_bit_rate: i64,\n\t/// Whether subtitles are exported.\n\tpub subtitles_enabled: c_int,\n\t/// Subtitle codec id.\n\tpub subtitles_codec: c_int,\n\t/// Whether subtitles go to a sidecar file.\n\tpub subtitles_are_sidecar: c_int,\n\t/// Sidecar subtitle format.\n\tpub subtitles_sidecar_format: c_int,\n\t/// Output OCIO colorspace name (empty = reference space).\n\tpub color_transform_output: [c_char; 256],\n\t/// Export length in seconds (rational).\n\tpub export_length_num: c_int,\n\t/// Export length denominator.\n\tpub export_length_den: c_int,\n\t/// Whether a custom export range is in effect.\n\tpub has_custom_range: c_int,\n\t/// Custom range in (seconds, rational).\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 numerator.\n\tpub custom_range_out_num: i64,\n\t/// Custom range out denominator.\n\tpub custom_range_out_den: i64,\n}\n\nextern \"C\" {\n\t/// `oakcodec_set_task_submit_cb` — install the task submitter (`None`\n\t/// unregisters, matching the header's `cb == NULL`).\n\tpub fn oakcodec_set_task_submit_cb(\n\t\tcb: Option\u003cOakCodecTaskSubmitFn\u003e,\n\t\tuserdata: *mut c_void,\n\t);\n\t/// `oakcodec_task_submit_is_registered`.\n\tpub fn oakcodec_task_submit_is_registered() -\u003e c_int;\n\n\t/// `oakcodec_decoder_init`.\n\tpub fn oakcodec_decoder_init() -\u003e OakDecoder;\n\t/// `oakcodec_decoder_free`.\n\tpub fn oakcodec_decoder_free(decoder: *mut OakDecoder);\n\t/// `oakcodec_decoder_open`.\n\tpub fn oakcodec_decoder_open(\n\t\tdecoder: OakDecoder,\n\t\tfilename: *const c_char,\n\t\tstream_index: c_int,\n\t) -\u003e c_int;\n\t/// `oakcodec_decoder_close`.\n\tpub fn oakcodec_decoder_close(decoder: OakDecoder) -\u003e c_int;\n\t/// `oakcodec_decoder_is_open`.\n\tpub fn oakcodec_decoder_is_open(decoder: OakDecoder) -\u003e c_int;\n\t/// `oakcodec_decoder_decode_audio`.\n\tpub fn oakcodec_decoder_decode_audio(\n\t\tdecoder: OakDecoder,\n\t\tin_num: c_int,\n\t\tin_den: c_int,\n\t\tout_num: c_int,\n\t\tout_den: c_int,\n\t\tsample_rate: c_int,\n\t\tchannel_layout: c_uint,\n\t\tbuf: *mut f32,\n\t\tbuf_frames: c_int,\n\t) -\u003e c_int;\n\t/// `oakcodec_decoder_conform_audio` — the whole-stream conform pass.\n\tpub fn oakcodec_decoder_conform_audio(\n\t\tdecoder: OakDecoder,\n\t\toutput_filenames: *const *const c_char,\n\t\tfilename_count: c_int,\n\t\tsample_rate: c_int,\n\t\tchannel_layout: u64,\n\t\tsample_format: c_int,\n\t\tcancelled: OakCancelAtom,\n\t) -\u003e c_int;\n\t/// `oakcodec_decoder_last_error`.\n\tpub fn oakcodec_decoder_last_error(decoder: OakDecoder, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\n\t// --- decoder.h: image-sequence heuristics ---\n\t/// `oakcodec_decoder_get_image_sequence_digit_count`.\n\tpub fn oakcodec_decoder_get_image_sequence_digit_count(filename: *const c_char) -\u003e c_int;\n\t/// `oakcodec_decoder_get_image_sequence_index`.\n\tpub fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -\u003e i64;\n\t/// `oakcodec_decoder_transform_image_sequence_file_name` (two-stage).\n\tpub fn oakcodec_decoder_transform_image_sequence_file_name(\n\t\tfilename: *const c_char,\n\t\tnumber: i64,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\n\t// --- encoder.h extras ---\n\t/// `oakcodec_encoder_get_desired_pixel_format`.\n\tpub fn oakcodec_encoder_get_desired_pixel_format(encoder: OakEncoder) -\u003e c_int;\n\t/// `oakcodec_encoding_generate_matrix` (row-major 4x4).\n\tpub fn oakcodec_encoding_generate_matrix(\n\t\tmethod: c_int,\n\t\tsrc_width: c_int,\n\t\tsrc_height: c_int,\n\t\tdst_width: c_int,\n\t\tdst_height: c_int,\n\t\tout_matrix: *mut f64,\n\t) -\u003e c_int;\n\n\t// --- proxy.h ---\n\t/// `oakcodec_proxy_find_ffmpeg` (buf/size getter).\n\tpub fn oakcodec_proxy_find_ffmpeg(configured_path: *const c_char, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oakcodec_proxy_params_default`.\n\tpub fn oakcodec_proxy_params_default(out: *mut OakCodecProxyParams) -\u003e c_int;\n\n\t/// `oakcodec_encoder_init`.\n\tpub fn oakcodec_encoder_init(params: *const OakCodecEncodingParams) -\u003e OakEncoder;\n\t/// `oakcodec_encoder_free`.\n\tpub fn oakcodec_encoder_free(encoder: *mut OakEncoder);\n\t/// `oakcodec_encoder_open`.\n\tpub fn oakcodec_encoder_open(encoder: OakEncoder) -\u003e c_int;\n\t/// `oakcodec_encoder_write_video`.\n\tpub fn oakcodec_encoder_write_video(encoder: OakEncoder, frame: OakFrame) -\u003e c_int;\n\t/// `oakcodec_encoder_write_audio`.\n\tpub fn oakcodec_encoder_write_audio(\n\t\tencoder: OakEncoder,\n\t\tsamples: *const f32,\n\t\tframe_count: c_int,\n\t) -\u003e c_int;\n\t/// `oakcodec_encoder_write_subtitle`.\n\tpub fn oakcodec_encoder_write_subtitle(\n\t\tencoder: OakEncoder,\n\t\ttext: *const c_char,\n\t\tin_seconds: f64,\n\t\tout_seconds: f64,\n\t) -\u003e c_int;\n\t/// `oakcodec_encoder_flush`.\n\tpub fn oakcodec_encoder_flush(encoder: OakEncoder) -\u003e c_int;\n\t/// `oakcodec_encoder_last_error`.\n\tpub fn oakcodec_encoder_last_error(encoder: OakEncoder, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","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. The task module reaches the shared\n//! value types through `include/common/videoparams.h`,\n//! `include/common/colortransform.h` and the oakcore audio-params C ABI.\n//! Signatures mirror the headers verbatim.\n\nuse std::ffi::{c_char, c_int};\n\nuse crate::handle::CHandle;\n\n/// Mirror of `OakVideoParams` (`include/common/videoparams.h`).\n///\n/// A by-value handle (shared_ptr semantics); its `{ctx, addref, release,\n/// abi_version}` layout is identical to [`CHandle`], so it is a type alias —\n/// every oakcommon handle is created and destroyed inside the oakcommon DLL\n/// and only crosses the FFI boundary as this opaque by-value struct.\npub type OakVideoParams = CHandle;\n\n/// Mirror of `OakColorTransform` (`include/common/colortransform.h`); see\n/// [`OakVideoParams`] for the alias rationale.\npub type OakColorTransform = CHandle;\n\n/// Mirror of `OakAudioParams` — oakcore audio parameters. Declared as a\n/// `CHandle`-shaped opaque; the oakcore C ABI header that owns its layout is\n/// the authoritative source once the oakcore C ABI ships.\npub type OakAudioParams = CHandle;\n\n/// `OAKCOMMON_VIDEO_TYPE_STILL` (`include/common/videoparams.h`).\npub const OAKCOMMON_VIDEO_TYPE_STILL: c_int = 1;\n/// `OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE` (`include/common/videoparams.h`).\npub const OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE: c_int = 2;\n\nextern \"C\" {\n\t// --- videoparams.h ---\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(\n\t\twidth: c_int,\n\t\theight: c_int,\n\t\tpixel_format: c_int,\n\t\tnb_channels: c_int,\n\t\tpixel_aspect_num: c_int,\n\t\tpixel_aspect_den: c_int,\n\t\tinterlacing: c_int,\n\t\tdivider: c_int,\n\t) -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_free`.\n\tpub fn oakcommon_videoparams_free(params: *mut OakVideoParams);\n\t/// `oakcommon_videoparams_get_width`.\n\tpub fn oakcommon_videoparams_get_width(params: OakVideoParams, width: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_height`.\n\tpub fn oakcommon_videoparams_get_height(params: OakVideoParams, height: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_format`.\n\tpub fn oakcommon_videoparams_get_format(params: OakVideoParams, format: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_format`.\n\tpub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_time_base`.\n\tpub fn oakcommon_videoparams_get_time_base(\n\t\tparams: OakVideoParams,\n\t\tnumerator: *mut c_int,\n\t\tdenominator: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_time_base`.\n\tpub fn oakcommon_videoparams_set_time_base(\n\t\tparams: OakVideoParams,\n\t\tnumerator: c_int,\n\t\tdenominator: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_frame_rate_as_time_base` — frame rate flipped\n\t/// to a frame duration rational.\n\tpub fn oakcommon_videoparams_frame_rate_as_time_base(\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_set_frame_rate`.\n\tpub fn oakcommon_videoparams_set_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tnumerator: c_int,\n\t\tdenominator: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_video_type`.\n\tpub fn oakcommon_videoparams_get_video_type(params: OakVideoParams, out_type: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_video_type`.\n\tpub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_start_time`.\n\tpub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start: i64) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_duration`.\n\tpub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_is_valid`.\n\tpub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams, out_valid: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_frame_rate`.\n\tpub fn oakcommon_videoparams_get_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tnumerator: *mut c_int,\n\t\tdenominator: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_duration`.\n\tpub fn oakcommon_videoparams_get_duration(params: OakVideoParams, duration: *mut i64) -\u003e c_int;\n\n\t// --- colortransform.h ---\n\t/// `oakcommon_colortransform_init_display`.\n\tpub fn oakcommon_colortransform_init_display(\n\t\tdisplay: *const c_char,\n\t\tview: *const c_char,\n\t\tlook: *const c_char,\n\t) -\u003e OakColorTransform;\n\t/// `oakcommon_colortransform_init_output`.\n\tpub fn oakcommon_colortransform_init_output(\n\t\toutput: *const c_char,\n\t\tdisplay: *const c_char,\n\t\tview: *const c_char,\n\t\tlook: *const c_char,\n\t) -\u003e OakColorTransform;\n\t/// `oakcommon_colortransform_free`.\n\tpub fn oakcommon_colortransform_free(transform: *mut OakColorTransform);\n\n\t// --- config.h ---\n\t/// `oakcommon_config_get` (buf/size getter; returns needed size).\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_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}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","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 other oak modules (signatures mirror the public\n//! headers verbatim; resolved at link time).\n\npub mod codec;\npub mod common;\npub mod node;\npub mod render;\npub mod timeline;\npub mod undo;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","node.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//! oaknode C ABI imports. The task module drives node-graph work through\n//! `include/node/project.h` (project shell), `include/node/folder.h`,\n//! `include/node/footage.h`, `include/node/sequence.h`,\n//! `include/node/colormanager.h` and `include/node/serializer.h`. Signatures\n//! mirror the headers verbatim; node handles are `CHandle` values whose\n//! underlying objects live inside the node library.\n\nuse std::ffi::{c_char, c_int, c_void};\n\nuse crate::bridge::common::OakVideoParams;\nuse crate::bridge::render::OakCancelAtom;\nuse crate::handle::CHandle;\n\n/// `OakNodeProject` (`include/node/project.h`).\npub type OakNodeProject = CHandle;\n/// `OakNodeNode` (`include/node/node.h`).\npub type OakNodeNode = CHandle;\n/// `OakNodeFolder` (`include/node/folder.h`).\npub type OakNodeFolder = CHandle;\n/// `OakNodeFootage` (`include/node/footage.h`).\npub type OakNodeFootage = CHandle;\n/// `OakNodeSequence` (`include/node/sequence.h`).\npub type OakNodeSequence = CHandle;\n/// `OakNodeTrack` (`include/node/track.h`).\npub type OakNodeTrack = CHandle;\n/// `OakNodeTrackList` (`include/node/track.h`).\npub type OakNodeTrackList = CHandle;\n/// `OakNodeBlock` (`include/node/block.h`).\npub type OakNodeBlock = CHandle;\n/// `OakNodeColorManager` (`include/node/colormanager.h`).\npub type OakNodeColorManager = CHandle;\n/// `OakNodeSerializerSaveData` (`include/node/serializer.h`).\npub type OakNodeSerializerSaveData = CHandle;\n/// `OakNodeSerializerLoadData` (`include/node/serializer.h`).\npub type OakNodeSerializerLoadData = CHandle;\n\n/// `OAKNODE_SEQUENCE_TEXTURE_INPUT` (`include/node/sequence.h`).\npub const OAKNODE_SEQUENCE_TEXTURE_INPUT: \u0026str = \"tex_in\";\n/// `OAKNODE_SEQUENCE_SAMPLES_INPUT` (`include/node/sequence.h`).\npub const OAKNODE_SEQUENCE_SAMPLES_INPUT: \u0026str = \"samples_in\";\n/// `OAKNODE_TRACK_TYPE_VIDEO` (`include/node/track.h`).\npub const OAKNODE_TRACK_TYPE_VIDEO: c_int = 0;\n/// `OAKNODE_TRACK_TYPE_AUDIO` (`include/node/track.h`).\npub const OAKNODE_TRACK_TYPE_AUDIO: c_int = 1;\n/// `OAKNODE_TRACK_TYPE_NONE` (`include/node/track.h`).\npub const OAKNODE_TRACK_TYPE_NONE: c_int = -1;\n/// `OAKNODE_BLOCK_OTHER` (`include/node/block.h`).\npub const OAKNODE_BLOCK_OTHER: c_int = 0;\n/// `OAKNODE_BLOCK_CLIP` (`include/node/block.h`).\npub const OAKNODE_BLOCK_CLIP: c_int = 1;\n/// `OAKNODE_BLOCK_GAP` (`include/node/block.h`).\npub const OAKNODE_BLOCK_GAP: c_int = 2;\n/// `OAKNODE_BLOCK_TRANSITION` (`include/node/block.h`).\npub const OAKNODE_BLOCK_TRANSITION: c_int = 3;\n/// `OAKNODE_TRANSITION_CROSS_DISSOLVE` (`include/node/block.h`).\npub const OAKNODE_TRANSITION_CROSS_DISSOLVE: c_int = 0;\n/// `OAKNODE_TRANSITION_OUT_BLOCK_INPUT` (`include/node/block.h`).\npub const OAKNODE_TRANSITION_OUT_BLOCK_INPUT: \u0026str = \"out_block_in\";\n/// `OAKNODE_TRANSITION_IN_BLOCK_INPUT` (`include/node/block.h`).\npub const OAKNODE_TRANSITION_IN_BLOCK_INPUT: \u0026str = \"in_block_in\";\n\n/// Node type ids used by the OTIO tasks\n/// (`src/task/src/project/loadotio/loadotio.cpp`).\n/// `k_sequence_id` — the sequence node type.\npub const OAKNODE_TYPE_SEQUENCE: \u0026str = \"org.olivevideoeditor.Olive.sequence\";\n/// `k_transform_id` — the transform node type.\npub const OAKNODE_TYPE_TRANSFORM: \u0026str = \"org.olivevideoeditor.Olive.transform\";\n/// `k_volume_id` — the volume node type.\npub const OAKNODE_TYPE_VOLUME: \u0026str = \"org.olivevideoeditor.Olive.volume\";\n\n/// C string helper: build a NUL-terminated copy of a Rust string for the\n/// C ABI. The buffer is leaked for the duration of the process; callers\n/// pass short-lived literals.\npub fn cstr(s: \u0026str) -\u003e *const c_char {\n\tlet mut bytes = s.as_bytes().to_vec();\n\tbytes.push(0);\n\tbytes.leak().as_ptr() as *const c_char\n}\n\nextern \"C\" {\n\t// --- node.h ---\n\t/// `oaknode_node_get_label` (two-stage string getter).\n\tpub fn oaknode_node_get_label(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_node_set_label`.\n\tpub fn oaknode_node_set_label(node: OakNodeNode, label: *const c_char) -\u003e c_int;\n\t/// `oaknode_node_get_id` (two-stage string getter).\n\tpub fn oaknode_node_get_id(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_node_get_project` (borrowed project handle out).\n\tpub fn oaknode_node_get_project(node: OakNodeNode, out: *mut OakNodeProject) -\u003e c_int;\n\t/// `oaknode_node_input_get_connected_node` (borrowed handle out).\n\tpub fn oaknode_node_input_get_connected_node(\n\t\tnode: OakNodeNode,\n\t\tinput_id: *const c_char,\n\t\tout_node: *mut OakNodeNode,\n\t) -\u003e c_int;\n\t/// `oaknode_node_set_context_position` (live, non-undoable).\n\tpub fn oaknode_node_set_context_position(\n\t\tnode: OakNodeNode,\n\t\tcontext: OakNodeNode,\n\t\tx: f64,\n\t\ty: f64,\n\t\texpanded: c_int,\n\t) -\u003e c_int;\n\t/// `oaknode_node_find_input_footage` (borrowed handle out).\n\tpub fn oaknode_node_find_input_footage(node: OakNodeNode, out: *mut OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_node_get_input_string` (two-stage string getter).\n\tpub fn oaknode_node_get_input_string(\n\t\tnode: OakNodeNode,\n\t\tinput_id: *const c_char,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oaknode_node_create_copy` — orphaned deep copy of a node.\n\tpub fn oaknode_node_create_copy(node: OakNodeNode) -\u003e OakNodeNode;\n\t/// `oaknode_node_copy_inputs`.\n\tpub fn oaknode_node_copy_inputs(dst: OakNodeNode, src: OakNodeNode, include_connections: c_int) -\u003e c_int;\n\t/// `oaknode_node_connect`.\n\tpub fn oaknode_node_connect(output_node: OakNodeNode, input_node: OakNodeNode, input_id: *const c_char) -\u003e c_int;\n\t/// `oaknode_node_set_value_hint_track`.\n\tpub fn oaknode_node_set_value_hint_track(\n\t\tnode: OakNodeNode,\n\t\tinput_id: *const c_char,\n\t\ttrack_type: c_int,\n\t\ttrack_index: c_int,\n\t) -\u003e c_int;\n\t/// `oaknode_node_get_video_frame_cache` (borrowed cache out).\n\tpub fn oaknode_node_get_video_frame_cache(\n\t\tnode: OakNodeNode,\n\t\tout: *mut crate::bridge::render::OakRenderCache,\n\t) -\u003e c_int;\n\t/// `oaknode_node_free`.\n\tpub fn oaknode_node_free(node: *mut OakNodeNode);\n\t/// `oaknode_command_create_remove_node` — undoable node removal.\n\tpub fn oaknode_command_create_remove_node(node: OakNodeNode) -\u003e crate::bridge::undo::OakUndoCommand;\n\n\t// --- factory.h ---\n\t/// `oaknode_factory_create_from_id`.\n\tpub fn oaknode_factory_create_from_id(type_id: *const c_char) -\u003e OakNodeNode;\n\n\t// --- node.h (viewer) ---\n\t/// `oaknode_viewer_set_video_params`.\n\tpub fn oaknode_viewer_set_video_params(viewer: OakNodeNode, params: *const OakVideoParams) -\u003e c_int;\n\t/// `oaknode_viewer_set_audio_params`.\n\tpub fn oaknode_viewer_set_audio_params(viewer: OakNodeNode, params: *const std::ffi::c_void) -\u003e c_int;\n\n\t// --- block.h ---\n\t/// `oaknode_block_clip_create`.\n\tpub fn oaknode_block_clip_create() -\u003e OakNodeBlock;\n\t/// `oaknode_block_gap_create`.\n\tpub fn oaknode_block_gap_create() -\u003e OakNodeBlock;\n\t/// `oaknode_block_transition_create` (kind: `OakNodeTransitionKind`).\n\tpub fn oaknode_block_transition_create(kind: c_int) -\u003e OakNodeBlock;\n\t/// `oaknode_block_as_node` (borrowed cast).\n\tpub fn oaknode_block_as_node(block: OakNodeBlock) -\u003e OakNodeNode;\n\t/// `oaknode_block_get_kind`.\n\tpub fn oaknode_block_get_kind(block: OakNodeBlock, out_kind: *mut c_int) -\u003e c_int;\n\t/// `oaknode_block_get_in`.\n\tpub fn oaknode_block_get_in(block: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_block_get_length`.\n\tpub fn oaknode_block_get_length(block: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_block_set_length_and_media_out`.\n\tpub fn oaknode_block_set_length_and_media_out(block: OakNodeBlock, numerator: c_int, denominator: c_int) -\u003e c_int;\n\t/// `oaknode_clip_set_media_in`.\n\tpub fn oaknode_clip_set_media_in(clip: OakNodeBlock, numerator: c_int, denominator: c_int) -\u003e c_int;\n\t/// `oaknode_transition_get_in_offset`.\n\tpub fn oaknode_transition_get_in_offset(transition: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_transition_get_out_offset`.\n\tpub fn oaknode_transition_get_out_offset(transition: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_transition_set_offsets_and_length`.\n\tpub fn oaknode_transition_set_offsets_and_length(\n\t\ttransition: OakNodeBlock,\n\t\tin_num: c_int,\n\t\tin_den: c_int,\n\t\tout_num: c_int,\n\t\tout_den: c_int,\n\t) -\u003e c_int;\n\n\t// --- project.h ---\n\t/// `oaknode_project_init`.\n\tpub fn oaknode_project_init() -\u003e OakNodeProject;\n\t/// `oaknode_project_free`.\n\tpub fn oaknode_project_free(project: *mut OakNodeProject);\n\t/// `oaknode_project_initialize`.\n\tpub fn oaknode_project_initialize(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_clear`.\n\tpub fn oaknode_project_clear(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_root`.\n\tpub fn oaknode_project_root(project: OakNodeProject) -\u003e OakNodeFolder;\n\t/// `oaknode_project_name`.\n\tpub fn oaknode_project_name(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_filename`.\n\tpub fn oaknode_project_filename(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_pretty_filename`.\n\tpub fn oaknode_project_pretty_filename(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_set_filename`.\n\tpub fn oaknode_project_set_filename(project: OakNodeProject, filename: *const c_char) -\u003e c_int;\n\t/// `oaknode_project_is_modified`.\n\tpub fn oaknode_project_is_modified(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_set_modified`.\n\tpub fn oaknode_project_set_modified(project: OakNodeProject, modified: c_int) -\u003e c_int;\n\t/// `oaknode_project_is_new`.\n\tpub fn oaknode_project_is_new(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_cache_path`.\n\tpub fn oaknode_project_cache_path(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_copy_settings`.\n\tpub fn oaknode_project_copy_settings(dst: OakNodeProject, src: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_get_cache_location_setting`.\n\tpub fn oaknode_project_get_cache_location_setting(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_set_cache_location_setting`.\n\tpub fn oaknode_project_set_cache_location_setting(project: OakNodeProject, setting: c_int) -\u003e c_int;\n\t/// `oaknode_project_get_custom_cache_path`.\n\tpub fn oaknode_project_get_custom_cache_path(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_set_custom_cache_path`.\n\tpub fn oaknode_project_set_custom_cache_path(project: OakNodeProject, path: *const c_char) -\u003e c_int;\n\t/// `oaknode_project_get_uuid`.\n\tpub fn oaknode_project_get_uuid(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_add_node`.\n\tpub fn oaknode_project_add_node(project: OakNodeProject, node: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_project_remove_node`.\n\tpub fn oaknode_project_remove_node(project: OakNodeProject, node: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_project_node_count`.\n\tpub fn oaknode_project_node_count(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_node_at`.\n\tpub fn oaknode_project_node_at(project: OakNodeProject, index: c_int) -\u003e OakNodeNode;\n\n\t// --- folder.h ---\n\t/// `oaknode_folder_create`.\n\tpub fn oaknode_folder_create(project: OakNodeProject) -\u003e OakNodeFolder;\n\t/// `oaknode_folder_child_count`.\n\tpub fn oaknode_folder_child_count(folder: OakNodeFolder) -\u003e c_int;\n\t/// `oaknode_folder_child_at`.\n\tpub fn oaknode_folder_child_at(folder: OakNodeFolder, index: c_int) -\u003e OakNodeNode;\n\t/// `oaknode_folder_add_child`.\n\tpub fn oaknode_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_as_node`.\n\tpub fn oaknode_folder_as_node(folder: OakNodeFolder) -\u003e OakNodeNode;\n\t/// `oaknode_command_create_folder_add_child`.\n\tpub fn oaknode_command_create_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e crate::bridge::undo::OakUndoCommand;\n\t/// `oaknode_folder_remove_child`.\n\tpub fn oaknode_folder_remove_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_move_children`.\n\tpub fn oaknode_folder_move_children(nodes: *const OakNodeNode, count: c_int, dest_folder: OakNodeFolder) -\u003e c_int;\n\t/// `oaknode_folder_has_child_recursive`.\n\tpub fn oaknode_folder_has_child_recursive(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_index_of_child`.\n\tpub fn oaknode_folder_index_of_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_parent_of`.\n\tpub fn oaknode_folder_parent_of(node: OakNodeNode) -\u003e OakNodeFolder;\n\n\t// --- footage.h ---\n\t/// `oaknode_footage_create`.\n\tpub fn oaknode_footage_create(project: OakNodeProject, filename: *const c_char) -\u003e OakNodeFootage;\n\t/// `oaknode_footage_as_node`.\n\tpub fn oaknode_footage_as_node(footage: OakNodeFootage) -\u003e OakNodeNode;\n\t/// `oaknode_footage_filename`.\n\tpub fn oaknode_footage_filename(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_footage_set_filename`.\n\tpub fn oaknode_footage_set_filename(footage: OakNodeFootage, filename: *const c_char) -\u003e c_int;\n\t/// `oaknode_footage_is_valid`.\n\tpub fn oaknode_footage_is_valid(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_timestamp`.\n\tpub fn oaknode_footage_timestamp(footage: OakNodeFootage, out_timestamp: *mut i64) -\u003e c_int;\n\t/// `oaknode_footage_set_timestamp`.\n\tpub fn oaknode_footage_set_timestamp(footage: OakNodeFootage, timestamp: i64) -\u003e c_int;\n\t/// `oaknode_footage_decoder`.\n\tpub fn oaknode_footage_decoder(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_footage_total_stream_count`.\n\tpub fn oaknode_footage_total_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_video_stream_count`.\n\tpub fn oaknode_footage_video_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_audio_stream_count`.\n\tpub fn oaknode_footage_audio_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_subtitle_stream_count`.\n\tpub fn oaknode_footage_subtitle_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_duration`.\n\tpub fn oaknode_footage_duration(footage: OakNodeFootage, out_numerator: *mut c_int, out_denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_footage_proxy_enabled`.\n\tpub fn oaknode_footage_proxy_enabled(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_set_proxy_enabled`.\n\tpub fn oaknode_footage_set_proxy_enabled(footage: OakNodeFootage, enabled: c_int) -\u003e c_int;\n\t/// `oaknode_footage_proxy_path`.\n\tpub fn oaknode_footage_proxy_path(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_footage_proxy_state`.\n\tpub fn oaknode_footage_proxy_state(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_set_proxy`.\n\tpub fn oaknode_footage_set_proxy(footage: OakNodeFootage, path: *const c_char, state: c_int, video_stream_index: c_int, preset_version: c_int, enabled: c_int) -\u003e c_int;\n\t/// `oaknode_footage_clear_proxy`.\n\tpub fn oaknode_footage_clear_proxy(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_get_video_params`.\n\tpub fn oaknode_footage_get_video_params(footage: OakNodeFootage, index: c_int, out: *mut OakVideoParams) -\u003e c_int;\n\t/// `oaknode_footage_set_video_params`.\n\tpub fn oaknode_footage_set_video_params(footage: OakNodeFootage, index: c_int, params: *const OakVideoParams) -\u003e c_int;\n\t/// `oaknode_footage_get_video_length`.\n\tpub fn oaknode_footage_get_video_length(footage: OakNodeFootage, out_num: *mut i64, out_den: *mut i64) -\u003e c_int;\n\t/// `oaknode_footage_set_cancel_atom`.\n\tpub fn oaknode_footage_set_cancel_atom(footage: OakNodeFootage, atom: OakCancelAtom) -\u003e c_int;\n\n\t// --- sequence.h ---\n\t/// `oaknode_sequence_create`.\n\tpub fn oaknode_sequence_create() -\u003e OakNodeSequence;\n\t/// `oaknode_sequence_free`.\n\tpub fn oaknode_sequence_free(sequence: *mut OakNodeSequence);\n\t/// `oaknode_sequence_set_default_parameters`.\n\tpub fn oaknode_sequence_set_default_parameters(sequence: OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_sequence_as_node`.\n\tpub fn oaknode_sequence_as_node(sequence: OakNodeSequence) -\u003e OakNodeNode;\n\t/// `oaknode_sequence_from_node`.\n\tpub fn oaknode_sequence_from_node(node: OakNodeNode) -\u003e OakNodeSequence;\n\t/// `oaknode_sequence_get_track_list`.\n\tpub fn oaknode_sequence_get_track_list(sequence: OakNodeSequence, r#type: c_int, out: *mut OakNodeTrackList) -\u003e c_int;\n\t/// `oaknode_sequence_get_track_count`.\n\tpub fn oaknode_sequence_get_track_count(sequence: OakNodeSequence, r#type: c_int, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_track_at`.\n\tpub fn oaknode_sequence_get_track_at(sequence: OakNodeSequence, r#type: c_int, index: c_int, out: *mut OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_sequence_get_all_track_count`.\n\tpub fn oaknode_sequence_get_all_track_count(sequence: OakNodeSequence, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_all_track_at`.\n\tpub fn oaknode_sequence_get_all_track_at(sequence: OakNodeSequence, index: c_int, out: *mut OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_sequence_get_playhead`.\n\tpub fn oaknode_sequence_get_playhead(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_set_playhead`.\n\tpub fn oaknode_sequence_set_playhead(sequence: OakNodeSequence, numerator: c_int, denominator: c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_length`.\n\tpub fn oaknode_sequence_get_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_video_length`.\n\tpub fn oaknode_sequence_get_video_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_audio_length`.\n\tpub fn oaknode_sequence_get_audio_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_verify_length`.\n\tpub fn oaknode_sequence_verify_length(sequence: OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_sequence_get_video_stream_count`.\n\tpub fn oaknode_sequence_get_video_stream_count(sequence: OakNodeSequence, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_audio_stream_count`.\n\tpub fn oaknode_sequence_get_audio_stream_count(sequence: OakNodeSequence, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_video_params`.\n\tpub fn oaknode_sequence_get_video_params(sequence: OakNodeSequence, index: c_int, out: *mut OakVideoParams) -\u003e c_int;\n\t/// `oaknode_sequence_set_video_params`.\n\tpub fn oaknode_sequence_set_video_params(sequence: OakNodeSequence, index: c_int, params: OakVideoParams) -\u003e c_int;\n\t/// `oaknode_sequence_get_audio_params`.\n\tpub fn oaknode_sequence_get_audio_params(sequence: OakNodeSequence, index: c_int, out: *mut *const c_void) -\u003e c_int;\n\t/// `oaknode_sequence_set_audio_params`.\n\tpub fn oaknode_sequence_set_audio_params(sequence: OakNodeSequence, index: c_int, params: *const c_void) -\u003e c_int;\n\n\t// --- track.h ---\n\t/// `oaknode_track_as_node`.\n\tpub fn oaknode_track_as_node(track: OakNodeTrack) -\u003e OakNodeNode;\n\t/// `oaknode_track_create`.\n\tpub fn oaknode_track_create(r#type: c_int) -\u003e OakNodeTrack;\n\t/// `oaknode_track_free`.\n\tpub fn oaknode_track_free(track: *mut OakNodeTrack);\n\t/// `oaknode_track_get_type`.\n\tpub fn oaknode_track_get_type(track: OakNodeTrack, r#type: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_get_length`.\n\tpub fn oaknode_track_get_length(track: OakNodeTrack, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_get_block_count`.\n\tpub fn oaknode_track_get_block_count(track: OakNodeTrack, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_get_block_at` (borrowed handle out).\n\tpub fn oaknode_track_get_block_at(track: OakNodeTrack, index: c_int, out: *mut OakNodeBlock) -\u003e c_int;\n\t/// `oaknode_track_append_block`.\n\tpub fn oaknode_track_append_block(track: OakNodeTrack, block: OakNodeBlock) -\u003e c_int;\n\t/// `oaknode_track_set_type`.\n\tpub fn oaknode_track_set_type(track: OakNodeTrack, r#type: c_int) -\u003e c_int;\n\t/// `oaknode_track_get_index`.\n\tpub fn oaknode_track_get_index(track: OakNodeTrack, index: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_set_index`.\n\tpub fn oaknode_track_set_index(track: OakNodeTrack, index: c_int) -\u003e c_int;\n\t/// `oaknode_track_get_sequence`.\n\tpub fn oaknode_track_get_sequence(track: OakNodeTrack, out: *mut OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_tracklist_get_sequence`.\n\tpub fn oaknode_tracklist_get_sequence(list: OakNodeTrackList, out: *mut OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_tracklist_get_track_input_id`.\n\tpub fn oaknode_tracklist_get_track_input_id(list: OakNodeTrackList, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_array_append`.\n\tpub fn oaknode_tracklist_array_append(list: OakNodeTrackList) -\u003e c_int;\n\t/// `oaknode_tracklist_array_remove_last`.\n\tpub fn oaknode_tracklist_array_remove_last(list: OakNodeTrackList) -\u003e c_int;\n\t/// `oaknode_tracklist_get_array_index_from_cache_index`.\n\tpub fn oaknode_tracklist_get_array_index_from_cache_index(list: OakNodeTrackList, cache_index: c_int, out_index: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_type`.\n\tpub fn oaknode_tracklist_get_type(list: OakNodeTrackList, r#type: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_track_count`.\n\tpub fn oaknode_tracklist_get_track_count(list: OakNodeTrackList, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_track_at`.\n\tpub fn oaknode_tracklist_get_track_at(list: OakNodeTrackList, index: c_int, out: *mut OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_tracklist_get_total_length`.\n\tpub fn oaknode_tracklist_get_total_length(list: OakNodeTrackList, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_array_size`.\n\tpub fn oaknode_tracklist_get_array_size(list: OakNodeTrackList, size: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_add_track`.\n\tpub fn oaknode_tracklist_add_track(list: OakNodeTrackList, track: OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_tracklist_remove_track`.\n\tpub fn oaknode_tracklist_remove_track(list: OakNodeTrackList, track: OakNodeTrack) -\u003e c_int;\n\n\t// --- colormanager.h ---\n\t/// `oaknode_colormanager_init`.\n\tpub fn oaknode_colormanager_init(project: OakNodeProject) -\u003e OakNodeColorManager;\n\t/// `oaknode_colormanager_free`.\n\tpub fn oaknode_colormanager_free(manager: *mut OakNodeColorManager);\n\t/// `oaknode_colormanager_wrap_borrowed`.\n\tpub fn oaknode_colormanager_wrap_borrowed(native_manager: *mut c_void) -\u003e OakNodeColorManager;\n\t/// `oaknode_colormanager_initialize`.\n\tpub fn oaknode_colormanager_initialize(manager: OakNodeColorManager) -\u003e c_int;\n\t/// `oaknode_colormanager_set_up_default_config`.\n\tpub fn oaknode_colormanager_set_up_default_config() -\u003e c_int;\n\n\t// --- serializer.h ---\n\t/// `oaknode_serializer_initialize`.\n\tpub fn oaknode_serializer_initialize() -\u003e c_int;\n\t/// `oaknode_serializer_shutdown`.\n\tpub fn oaknode_serializer_shutdown();\n\t/// `oaknode_serializer_savedata_create`.\n\tpub fn oaknode_serializer_savedata_create(load_type: c_int, project: OakNodeProject) -\u003e OakNodeSerializerSaveData;\n\t/// `oaknode_serializer_savedata_free`.\n\tpub fn oaknode_serializer_savedata_free(save_data: *mut OakNodeSerializerSaveData);\n\t/// `oaknode_serializer_savedata_set_nodes`.\n\tpub fn oaknode_serializer_savedata_set_nodes(save_data: OakNodeSerializerSaveData, nodes: *const OakNodeNode, count: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_savedata_set_property`.\n\tpub fn oaknode_serializer_savedata_set_property(save_data: OakNodeSerializerSaveData, node: OakNodeNode, key: *const c_char, value: *const c_char) -\u003e c_int;\n\t/// `oaknode_serializer_save_to_xml`.\n\tpub fn oaknode_serializer_save_to_xml(save_data: OakNodeSerializerSaveData, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_load_from_xml`.\n\tpub fn oaknode_serializer_load_from_xml(project: OakNodeProject, xml: *const c_char, load_type: c_int, out_result: *mut c_int, out_load_data: *mut OakNodeSerializerLoadData, details_buf: *mut c_char, details_buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_free`.\n\tpub fn oaknode_serializer_loaddata_free(load_data: *mut OakNodeSerializerLoadData);\n\t/// `oaknode_serializer_loaddata_node_count`.\n\tpub fn oaknode_serializer_loaddata_node_count(load_data: OakNodeSerializerLoadData) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_node_at`.\n\tpub fn oaknode_serializer_loaddata_node_at(load_data: OakNodeSerializerLoadData, index: c_int) -\u003e OakNodeNode;\n\t/// `oaknode_serializer_loaddata_get_property`.\n\tpub fn oaknode_serializer_loaddata_get_property(load_data: OakNodeSerializerLoadData, node: OakNodeNode, key: *const c_char, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_connection_count`.\n\tpub fn oaknode_serializer_loaddata_connection_count(load_data: OakNodeSerializerLoadData) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_connection_at`.\n\tpub fn oaknode_serializer_loaddata_connection_at(load_data: OakNodeSerializerLoadData, index: c_int, out_output_node: *mut OakNodeNode, out_input_node: *mut OakNodeNode, input_id_buf: *mut c_char, input_id_buf_size: c_int, out_element: *mut c_int) -\u003e c_int;\n\t/// `oaknode_serializer_save_to_file`.\n\tpub fn oaknode_serializer_save_to_file(project: OakNodeProject, filename: *const c_char, use_compression: c_int, out_code: *mut c_int, details: *mut c_char, details_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_load_from_file`.\n\tpub fn oaknode_serializer_load_from_file(project: OakNodeProject, filename: *const c_char, out_code: *mut c_int, details: *mut c_char, details_size: c_int) -\u003e c_int;\n}\n","traces":[{"line":90,"address":[],"length":0,"stats":{"Line":86}},{"line":91,"address":[],"length":0,"stats":{"Line":258}},{"line":92,"address":[],"length":0,"stats":{"Line":172}},{"line":93,"address":[],"length":0,"stats":{"Line":86}}],"covered":4,"coverable":4},{"path":["/","Users","sunyu","Projects","oak","src","task","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. The task module reaches the render side through\n//! `include/render/cancelatom.h` (task cancellation), `include/render/ticket.h`\n//! (frame/audio render tickets), `include/render/copier.h` (export project\n//! copy) and `include/render/color.h` (color processor). Signatures mirror\n//! the headers verbatim.\n\nuse std::ffi::{c_char, c_int, c_void};\n\nuse crate::bridge::codec::OakFrame;\nuse crate::bridge::common::{OakAudioParams, OakColorTransform, OakVideoParams};\nuse crate::bridge::node::{OakNodeColorManager, OakNodeNode, OakNodeProject};\nuse crate::handle::CHandle;\n\n/// Mirror of `OakCancelAtom` (`include/render/cancelatom.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakCancelAtom {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\nimpl OakCancelAtom {\n\t/// The empty (null) atom.\n\tpub fn null() -\u003e Self {\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\t/// Whether the atom is empty (`ctx == NULL`).\n\tpub fn is_null(\u0026self) -\u003e bool {\n\t\tself.ctx.is_null()\n\t}\n\n\t/// Adopt the fields of a generic `CHandle` (layout-identical) — the task\n\t/// module stores the atom as a `CHandle` for its public API.\n\tpub fn from_chandle(h: CHandle) -\u003e Self {\n\t\tOakCancelAtom {\n\t\t\tctx: h.ctx,\n\t\t\taddref: h.addref,\n\t\t\trelease: h.release,\n\t\t\tabi_version: h.abi_version,\n\t\t}\n\t}\n\n\t/// Convert back to a generic `CHandle` (layout-identical).\n\tpub fn to_chandle(\u0026self) -\u003e CHandle {\n\t\tCHandle {\n\t\t\tctx: self.ctx,\n\t\t\taddref: self.addref,\n\t\t\trelease: self.release,\n\t\t\tabi_version: self.abi_version,\n\t\t}\n\t}\n}\n\n/// Mirror of `OakRenderTicket` (`include/render/ticket.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakRenderTicket {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\n/// Mirror of `OakRenderCache` (`include/render/cache.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakRenderCache {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\nimpl OakRenderCache {\n\t/// The empty (null) cache handle.\n\tpub fn null() -\u003e Self {\n\t\tOakRenderCache {\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\t/// Convert to the layout-identical generic `CHandle`.\n\tpub fn to_chandle(\u0026self) -\u003e CHandle {\n\t\tCHandle {\n\t\t\tctx: self.ctx,\n\t\t\taddref: self.addref,\n\t\t\trelease: self.release,\n\t\t\tabi_version: self.abi_version,\n\t\t}\n\t}\n\n\t/// Adopt the fields of a generic `CHandle` (layout-identical).\n\tpub fn from_chandle(h: CHandle) -\u003e Self {\n\t\tOakRenderCache {\n\t\t\tctx: h.ctx,\n\t\t\taddref: h.addref,\n\t\t\trelease: h.release,\n\t\t\tabi_version: h.abi_version,\n\t\t}\n\t}\n}\n\n/// Mirror of `OakRenderProjectCopier` (`include/render/copier.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakRenderProjectCopier {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\n/// Mirror of `OakColorProcessor` (`include/render/color.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakColorProcessor {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\nimpl OakColorProcessor {\n\t/// Convert to the layout-identical generic `CHandle`.\n\tpub fn to_chandle(\u0026self) -\u003e CHandle {\n\t\tCHandle {\n\t\t\tctx: self.ctx,\n\t\t\taddref: self.addref,\n\t\t\trelease: self.release,\n\t\t\tabi_version: self.abi_version,\n\t\t}\n\t}\n\n\t/// Adopt the fields of a generic `CHandle` (layout-identical).\n\tpub fn from_chandle(h: CHandle) -\u003e Self {\n\t\tOakColorProcessor {\n\t\t\tctx: h.ctx,\n\t\t\taddref: h.addref,\n\t\t\trelease: h.release,\n\t\t\tabi_version: h.abi_version,\n\t\t}\n\t}\n}\n\n/// Mirror of `oakrender_ticket_finished_fn` (`include/render/ticket.h`).\n///\n/// Two arguments — `(ticket, userdata)` — mirroring the header verbatim.\n/// The ticket is a borrowed copy of the submitter's handle (the submitter\n/// keeps ownership and releases it); cancelled tickets fire with a NULL\n/// result observed through `oakrender_ticket_get_frame`.\npub type OakRenderTicketFinishedFn = unsafe extern \"C\" fn(ticket: OakRenderTicket, userdata: *mut c_void);\n\n/// Mirror of `oakrender_video_ticket_params` (`include/render/ticket.h`).\n#[repr(C)]\npub struct OakRenderVideoTicketParams {\n\t/// Connected texture output node (borrowed).\n\tpub output_node: OakNodeNode,\n\t/// Video params by value (oakcommon handle).\n\tpub video_params: OakVideoParams,\n\t/// Borrowed oakcore audio params, may be null ctx.\n\tpub audio_params: *const OakAudioParams,\n\t/// Frame timestamp numerator (seconds rational).\n\tpub time_num: i64,\n\t/// Frame timestamp denominator.\n\tpub time_den: i64,\n\t/// Borrowed color manager; empty ctx = NULL.\n\tpub color_manager: OakNodeColorManager,\n\t/// `olive::RenderMode::Mode` as int.\n\tpub mode: c_int,\n\t/// Forced output width (0 = off).\n\tpub force_width: c_int,\n\t/// Forced output height (0 = off).\n\tpub force_height: c_int,\n\t/// Forced color matrix (used when `has_force_matrix` != 0).\n\tpub force_matrix: [f64; 16],\n\t/// Whether `force_matrix` is in effect.\n\tpub has_force_matrix: c_int,\n\t/// Forced pixel format (`PixelFormat` as int, -1 = off).\n\tpub force_format: c_int,\n\t/// Forced channel count (0 = off).\n\tpub force_channel_count: c_int,\n\t/// Forced color output (borrowed; empty ctx = none).\n\tpub force_color_output: OakColorProcessor,\n\t/// Forced color transform by value (empty ctx = default).\n\tpub force_color_transform: OakColorTransform,\n\t/// Borrowed frame cache; empty ctx = none.\n\tpub cache: OakRenderCache,\n}\n\nextern \"C\" {\n\t// --- cancelatom.h ---\n\t/// `oakrender_cancelatom_init`.\n\tpub fn oakrender_cancelatom_init() -\u003e OakCancelAtom;\n\t/// `oakrender_cancelatom_free`.\n\tpub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);\n\t/// `oakrender_cancelatom_cancel`.\n\tpub fn oakrender_cancelatom_cancel(atom: OakCancelAtom) -\u003e c_int;\n\t/// `oakrender_cancelatom_is_cancelled`.\n\tpub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom, cancelled: *mut c_int) -\u003e c_int;\n\t/// `oakrender_cancelatom_heard_cancel`.\n\tpub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom, heard: *mut c_int) -\u003e c_int;\n\n\t// --- ticket.h ---\n\t/// `oakrender_ticket_render_frame`.\n\tpub fn oakrender_ticket_render_frame(\n\t\tparams: *const OakRenderVideoTicketParams,\n\t\tcb: Option\u003cOakRenderTicketFinishedFn\u003e,\n\t\tuserdata: *mut c_void,\n\t) -\u003e OakRenderTicket;\n\t/// `oakrender_ticket_render_audio`.\n\tpub fn oakrender_ticket_render_audio(\n\t\toutput_node: OakNodeNode,\n\t\tin_num: i64,\n\t\tin_den: i64,\n\t\tout_num: i64,\n\t\tout_den: i64,\n\t\tparams: *const OakAudioParams,\n\t\tmode: c_int,\n\t\tcb: Option\u003cOakRenderTicketFinishedFn\u003e,\n\t\tuserdata: *mut c_void,\n\t) -\u003e OakRenderTicket;\n\t/// `oakrender_ticket_is_finished`.\n\tpub fn oakrender_ticket_is_finished(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_wait`.\n\tpub fn oakrender_ticket_wait(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_cancel`.\n\tpub fn oakrender_ticket_cancel(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_get_type`.\n\tpub fn oakrender_ticket_get_type(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_get_frame`.\n\tpub fn oakrender_ticket_get_frame(ticket: OakRenderTicket, out: *mut OakFrame) -\u003e c_int;\n\t/// `oakrender_ticket_get_time`.\n\tpub fn oakrender_ticket_get_time(ticket: OakRenderTicket, out_num: *mut i64, out_den: *mut i64) -\u003e c_int;\n\t/// `oakrender_ticket_get_range`.\n\tpub fn oakrender_ticket_get_range(\n\t\tticket: OakRenderTicket,\n\t\tin_num: *mut i64,\n\t\tin_den: *mut i64,\n\t\tout_num: *mut i64,\n\t\tout_den: *mut i64,\n\t) -\u003e c_int;\n\t/// `oakrender_ticket_get_samples`.\n\tpub fn oakrender_ticket_get_samples(ticket: OakRenderTicket, out: *mut *mut std::ffi::c_void) -\u003e c_int;\n\t/// `oakrender_ticket_free`.\n\tpub fn oakrender_ticket_free(ticket: *mut OakRenderTicket);\n\n\t// --- copier.h ---\n\t/// `oakrender_project_copier_create`.\n\tpub fn oakrender_project_copier_create() -\u003e OakRenderProjectCopier;\n\t/// `oakrender_project_copier_free`.\n\tpub fn oakrender_project_copier_free(copier: *mut OakRenderProjectCopier);\n\t/// `oakrender_project_copier_set_project`.\n\tpub fn oakrender_project_copier_set_project(\n\t\tcopier: OakRenderProjectCopier,\n\t\tproject: OakNodeProject,\n\t) -\u003e c_int;\n\t/// `oakrender_project_copier_get_copy`.\n\tpub fn oakrender_project_copier_get_copy(\n\t\tcopier: OakRenderProjectCopier,\n\t\toriginal: OakNodeNode,\n\t) -\u003e OakNodeNode;\n\t/// `oakrender_project_copier_get_copied_project`.\n\tpub fn oakrender_project_copier_get_copied_project(\n\t\tcopier: OakRenderProjectCopier,\n\t) -\u003e OakNodeProject;\n\n\t// --- color.h ---\n\t/// `oakrender_color_processor_create`.\n\tpub fn oakrender_color_processor_create(\n\t\tsrc_space: *const c_char,\n\t\tdst_space: *const c_char,\n\t\tdisplay: *const c_char,\n\t\tview: *const c_char,\n\t\tlook: *const c_char,\n\t) -\u003e OakColorProcessor;\n\t/// `oakrender_color_processor_free`.\n\tpub fn oakrender_color_processor_free(processor: *mut OakColorProcessor);\n\t/// `oakrender_color_processor_is_valid`.\n\tpub fn oakrender_color_processor_is_valid(processor: OakColorProcessor) -\u003e c_int;\n\n\t// --- renderer.h ---\n\t/// `oakrender_codec_frame_free` — release a frame handed out by a\n\t/// render ticket.\n\tpub fn oakrender_codec_frame_free(frame: *mut crate::bridge::codec::OakFrame);\n\n\t// --- manager.h ---\n\t/// `oakrender_manager_set_aggressive_gc`.\n\tpub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -\u003e c_int;\n\n\t// --- cache.h ---\n\t/// `oakrender_cache_get_invalidated_ranges` — two-stage range query on\n\t/// a frame cache; returns the number of ranges (or the required flat\n\t/// array size).\n\tpub fn oakrender_cache_get_invalidated_ranges(\n\t\tcache: OakRenderCache,\n\t\tin_num: i64,\n\t\tin_den: i64,\n\t\tout_num: i64,\n\t\tout_den: i64,\n\t\tflat: *mut i64,\n\t\tflat_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakrender_cache_free`.\n\tpub fn oakrender_cache_free(cache: *mut OakRenderCache);\n}\n","traces":[{"line":46,"address":[],"length":0,"stats":{"Line":3}},{"line":48,"address":[],"length":0,"stats":{"Line":6}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":230}},{"line":64,"address":[],"length":0,"stats":{"Line":460}},{"line":65,"address":[],"length":0,"stats":{"Line":460}},{"line":66,"address":[],"length":0,"stats":{"Line":230}},{"line":67,"address":[],"length":0,"stats":{"Line":230}},{"line":72,"address":[],"length":0,"stats":{"Line":103}},{"line":74,"address":[],"length":0,"stats":{"Line":206}},{"line":75,"address":[],"length":0,"stats":{"Line":206}},{"line":76,"address":[],"length":0,"stats":{"Line":103}},{"line":77,"address":[],"length":0,"stats":{"Line":103}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":2}},{"line":122,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":2}},{"line":125,"address":[],"length":0,"stats":{"Line":2}},{"line":126,"address":[],"length":0,"stats":{"Line":1}},{"line":127,"address":[],"length":0,"stats":{"Line":1}},{"line":132,"address":[],"length":0,"stats":{"Line":83}},{"line":134,"address":[],"length":0,"stats":{"Line":166}},{"line":135,"address":[],"length":0,"stats":{"Line":166}},{"line":136,"address":[],"length":0,"stats":{"Line":83}},{"line":137,"address":[],"length":0,"stats":{"Line":83}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":177,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":83}},{"line":184,"address":[],"length":0,"stats":{"Line":166}},{"line":185,"address":[],"length":0,"stats":{"Line":166}},{"line":186,"address":[],"length":0,"stats":{"Line":83}},{"line":187,"address":[],"length":0,"stats":{"Line":83}}],"covered":29,"coverable":36},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","timeline.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//! oaktimeline C ABI imports. The OTIO load task builds tracks through the\n//! timeline edit factories (`include/timeline/edit.h`) instead of the raw\n//! track-list primitives, matching `src/task/src/project/loadotio/loadotio.cpp`.\n//! Signatures mirror the header verbatim.\n\nuse crate::bridge::node::OakNodeTrackList;\nuse crate::bridge::undo::OakUndoCommand;\n\nextern \"C\" {\n\t/// `oaktimeline_add_track_command` (`include/timeline/edit.h`) —\n\t/// olive::TimelineAddTrackCommand.\n\tpub fn oaktimeline_add_track_command(list: OakNodeTrackList) -\u003e OakUndoCommand;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","undo.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//! oakundo C ABI imports. Import/load tasks build undo commands through the\n//! C ABI vtable (`oakundo_command_init` with Rust closures as userdata) —\n//! no C++ UndoCommand subclassing exists on this side. Mirrors\n//! `include/undo/undocommand.h` verbatim.\n\nuse std::ffi::{c_int, c_void};\n\nuse crate::handle::CHandle;\n\n/// `OakUndoCommand` (`include/undo/undocommand.h`).\npub type OakUndoCommand = CHandle;\n\n/// Mirror of `OakUndoCommandVtable` (`include/undo/undocommand.h`).\n#[repr(C)]\npub struct OakUndoCommandVtable {\n\t/// Redo callback; may be NULL.\n\tpub redo: Option\u003cunsafe extern \"C\" fn(userdata: *mut c_void)\u003e,\n\t/// Undo callback; may be NULL.\n\tpub undo: Option\u003cunsafe extern \"C\" fn(userdata: *mut c_void)\u003e,\n\t/// Free callback; may be NULL.\n\tpub free_fn: Option\u003cunsafe extern \"C\" fn(userdata: *mut c_void)\u003e,\n}\n\nextern \"C\" {\n\t/// `oakundo_command_init` (vtable command).\n\tpub fn oakundo_command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -\u003e OakUndoCommand;\n\t/// `oakundo_command_init_multi`.\n\tpub fn oakundo_command_init_multi() -\u003e OakUndoCommand;\n\t/// `oakundo_command_multi_add_child`.\n\tpub fn oakundo_command_multi_add_child(multi: OakUndoCommand, child: OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_multi_child_count`.\n\tpub fn oakundo_command_multi_child_count(multi: OakUndoCommand, out_count: *mut c_int) -\u003e c_int;\n\t/// `oakundo_command_multi_child`.\n\tpub fn oakundo_command_multi_child(multi: OakUndoCommand, index: c_int, out_child: *mut OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_redo_now`.\n\tpub fn oakundo_command_redo_now(command: OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_undo_now`.\n\tpub fn oakundo_command_undo_now(command: OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_free`.\n\tpub fn oakundo_command_free(command: *mut OakUndoCommand);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","codecbridge.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//! Codec task submitter registration, mirroring\n//! `src/task/src/codecbridge.h`.\n//!\n//! Wires oakcodec's task-submit callback (`oakcodec_set_task_submit_cb`) so\n//! conform/proxy requests from the codec side land back in the task module.\n//! This is a two-module coupling; the actual callback signatures live in\n//! `crate::bridge::codec` mirroring `include/codec/task.h` verbatim.\n//!\n//! CPP-PARITY: src/task/src/codecbridge.h\n\nuse std::ffi::{c_char, c_int, c_void};\n\nuse crate::bridge;\nuse crate::conform::ConformTask;\nuse crate::error::{Error, Result};\nuse crate::proxy::{ProxyParams, ProxyTask};\nuse crate::task::Task;\n\n/// Convert an oakcodec proxy-params POD into the Rust [`ProxyParams`].\nfn proxy_params_from_codec(params: \u0026bridge::codec::OakCodecProxyParams) -\u003e ProxyParams {\n\tProxyParams {\n\t\twidth: params.width,\n\t\theight: params.height,\n\t\tdivider: params.divider,\n\t\tversion: params.version,\n\t\tcrf: params.crf,\n\t\tinclude_audio: params.include_audio != 0,\n\t\textension: unsafe { cstr_buf_to_string(\u0026params.extension) },\n\t\tpreset: unsafe { cstr_buf_to_string(\u0026params.preset) },\n\t}\n}\n\n/// Read a NUL-terminated char array into a String (lossy).\nunsafe fn cstr_buf_to_string(buf: \u0026[c_char]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tlet bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };\n\tString::from_utf8_lossy(bytes).into_owned()\n}\n\n/// The installed submit callback, mirroring `submit_codec_task` in\n/// codecbridge.cpp (interim contract: submission is synchronous).\n///\n/// # Safety\n/// `req` must be a valid `OakCodecTaskRequest` or null.\nunsafe extern \"C\" fn submit_codec_task(req: *const bridge::codec::OakCodecTaskRequest, _userdata: *mut c_void) -\u003e c_int {\n\tif req.is_null() {\n\t\treturn bridge::codec::OAKCODEC_E_INVALID;\n\t}\n\tlet request = unsafe { \u0026*req };\n\n\t// Build the concrete task on an outer base so the behavior can be\n\t// driven by `start()`; both share the cancellation atom.\n\tmatch request.kind {\n\t\tbridge::codec::OAKCODEC_TASK_CONFORM =\u003e {\n\t\t\tlet task = ConformTask::new(request);\n\t\t\tlet atom = task.base.get_cancel_atom();\n\t\t\tlet title = task.base.title().to_string();\n\t\t\tlet mut outer = Task::new(\u0026title, atom);\n\t\t\touter.set_behavior(Box::new(task));\n\t\t\tmatch outer.start() {\n\t\t\t\tOk(()) =\u003e 0,\n\t\t\t\tErr(_) =\u003e bridge::codec::OAKCODEC_E_FAILED,\n\t\t\t}\n\t\t}\n\t\tbridge::codec::OAKCODEC_TASK_PROXY =\u003e {\n\t\t\tlet mut codec_params = bridge::codec::OakCodecProxyParams {\n\t\t\t\twidth: 0,\n\t\t\t\theight: 0,\n\t\t\t\tdivider: 0,\n\t\t\t\tversion: 0,\n\t\t\t\tcrf: 0,\n\t\t\t\tinclude_audio: 0,\n\t\t\t\textension: [0; 32],\n\t\t\t\tpreset: [0; 32],\n\t\t\t};\n\t\t\tunsafe {\n\t\t\t\tbridge::codec::oakcodec_proxy_params_default(\u0026mut codec_params);\n\t\t\t}\n\t\t\tlet params = proxy_params_from_codec(\u0026codec_params);\n\t\t\tlet task = ProxyTask::new(request, params);\n\t\t\tlet atom = task.base.get_cancel_atom();\n\t\t\tlet title = task.base.title().to_string();\n\t\t\tlet mut outer = Task::new(\u0026title, atom);\n\t\t\touter.set_behavior(Box::new(task));\n\t\t\tmatch outer.start() {\n\t\t\t\tOk(()) =\u003e 0,\n\t\t\t\tErr(_) =\u003e bridge::codec::OAKCODEC_E_FAILED,\n\t\t\t}\n\t\t}\n\t\t_ =\u003e bridge::codec::OAKCODEC_E_INVALID,\n\t}\n}\n\n/// Install the task-module submitter as oakcodec's callback. Returns\n/// `Err(Error::State)` if already registered.\npub fn register_codec_task_submitter() -\u003e Result\u003c()\u003e {\n\tif is_codec_task_submitter_registered() {\n\t\treturn Err(Error::State);\n\t}\n\tunsafe {\n\t\tbridge::codec::oakcodec_set_task_submit_cb(Some(submit_codec_task), std::ptr::null_mut());\n\t}\n\tOk(())\n}\n\n/// Remove the task-module submitter from oakcodec. Idempotent.\npub fn unregister_codec_task_submitter() -\u003e Result\u003c()\u003e {\n\tunsafe {\n\t\tbridge::codec::oakcodec_set_task_submit_cb(None, std::ptr::null_mut());\n\t}\n\tOk(())\n}\n\n/// Whether a submitter is currently installed (queries oakcodec).\npub fn is_codec_task_submitter_registered() -\u003e bool {\n\tunsafe { bridge::codec::oakcodec_task_submit_is_registered() != 0 }\n}\n","traces":[{"line":36,"address":[],"length":0,"stats":{"Line":7}},{"line":38,"address":[],"length":0,"stats":{"Line":14}},{"line":39,"address":[],"length":0,"stats":{"Line":14}},{"line":40,"address":[],"length":0,"stats":{"Line":14}},{"line":41,"address":[],"length":0,"stats":{"Line":14}},{"line":42,"address":[],"length":0,"stats":{"Line":14}},{"line":43,"address":[],"length":0,"stats":{"Line":14}},{"line":44,"address":[],"length":0,"stats":{"Line":21}},{"line":45,"address":[],"length":0,"stats":{"Line":7}},{"line":50,"address":[],"length":0,"stats":{"Line":14}},{"line":51,"address":[],"length":0,"stats":{"Line":175}},{"line":52,"address":[],"length":0,"stats":{"Line":56}},{"line":53,"address":[],"length":0,"stats":{"Line":42}},{"line":61,"address":[],"length":0,"stats":{"Line":16}},{"line":62,"address":[],"length":0,"stats":{"Line":32}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":65,"address":[],"length":0,"stats":{"Line":30}},{"line":69,"address":[],"length":0,"stats":{"Line":15}},{"line":71,"address":[],"length":0,"stats":{"Line":21}},{"line":72,"address":[],"length":0,"stats":{"Line":21}},{"line":73,"address":[],"length":0,"stats":{"Line":21}},{"line":74,"address":[],"length":0,"stats":{"Line":28}},{"line":75,"address":[],"length":0,"stats":{"Line":21}},{"line":76,"address":[],"length":0,"stats":{"Line":7}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":5}},{"line":89,"address":[],"length":0,"stats":{"Line":7}},{"line":90,"address":[],"length":0,"stats":{"Line":7}},{"line":93,"address":[],"length":0,"stats":{"Line":7}},{"line":95,"address":[],"length":0,"stats":{"Line":21}},{"line":96,"address":[],"length":0,"stats":{"Line":28}},{"line":97,"address":[],"length":0,"stats":{"Line":21}},{"line":98,"address":[],"length":0,"stats":{"Line":21}},{"line":99,"address":[],"length":0,"stats":{"Line":28}},{"line":100,"address":[],"length":0,"stats":{"Line":21}},{"line":101,"address":[],"length":0,"stats":{"Line":7}},{"line":102,"address":[],"length":0,"stats":{"Line":2}},{"line":103,"address":[],"length":0,"stats":{"Line":5}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":22}},{"line":113,"address":[],"length":0,"stats":{"Line":22}},{"line":114,"address":[],"length":0,"stats":{"Line":3}},{"line":117,"address":[],"length":0,"stats":{"Line":38}},{"line":119,"address":[],"length":0,"stats":{"Line":19}},{"line":123,"address":[],"length":0,"stats":{"Line":34}},{"line":125,"address":[],"length":0,"stats":{"Line":68}},{"line":127,"address":[],"length":0,"stats":{"Line":34}},{"line":131,"address":[],"length":0,"stats":{"Line":22}},{"line":132,"address":[],"length":0,"stats":{"Line":22}}],"covered":49,"coverable":49},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","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//! `ConformTask`, mirroring `src/task/src/conform/conform.h`.\n//!\n//! Transcodes an audio stream to a PCM cache file (oakcodec kind\n//! `OAKCODEC_TASK_CONFORM`) and reports progress as it decodes.\n//!\n//! CPP-PARITY: src/task/src/conform/conform.h\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// A conform task: copies/channels of one audio stream into per-channel\n/// working PCM cache files, then renames them to the final names.\npub struct ConformTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Absolute input media filename.\n\tpub input_filename: String,\n\t/// Final destination path (replaces the source path extension).\n\tpub output_filename: String,\n\t/// Audio stream index inside the source media.\n\tpub stream_index: i32,\n\t/// Target sample rate (Hz).\n\tpub sample_rate: i32,\n\t/// Target channel layout mask (ffmpeg-style).\n\tpub channel_layout: u64,\n\t/// Target sample format (enum as int; `oakcore_rs::SampleFormat`).\n\tpub sample_format: i32,\n\t/// Resolved final per-channel filenames (from [`ConformTask::derive_filenames`]).\n\tfinal_names: Vec\u003cString\u003e,\n\t/// Resolved working per-channel filenames.\n\tworking_names: Vec\u003cString\u003e,\n}\n\nimpl ConformTask {\n\t/// Build a conform task from an oakcodec request, mirroring the C++\n\t/// constructor.\n\tpub fn new(request: \u0026bridge::codec::OakCodecTaskRequest) -\u003e ConformTask {\n\t\tlet input = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) };\n\t\tlet output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) };\n\t\tlet title = format!(\"Conforming Audio {}:{}\", input, request.stream_index);\n\t\tConformTask {\n\t\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\t\tinput_filename: input,\n\t\t\toutput_filename: output,\n\t\t\tstream_index: request.stream_index,\n\t\t\tsample_rate: request.sample_rate,\n\t\t\tchannel_layout: request.channel_layout,\n\t\t\tsample_format: request.sample_format,\n\t\t\tfinal_names: Vec::new(),\n\t\t\tworking_names: Vec::new(),\n\t\t}\n\t}\n\n\t/// Derive the final and working PCM filenames for the given first-channel\n\t/// final filename and channel count.\n\t///\n\t/// C++ contract: the first-channel final name must end in `.0.pcm`; the\n\t/// final name for channel `i` replaces the trailing index, and the\n\t/// working name appends `.working`. See `derive_filenames` in\n\t/// conform.cpp — this is a parity/golden candidate (see\n\t/// tests/parity_test.rs).\n\t///\n\t/// CPP-PARITY: src/task/src/conform/conform.cpp (derive_filenames)\n\tpub fn derive_filenames(first_channel_final: \u0026str, channel_count: usize) -\u003e Result\u003c(Vec\u003cString\u003e, Vec\u003cString\u003e)\u003e {\n\t\tconst SUFFIX: \u0026str = \".0.pcm\";\n\n\t\tif channel_count \u003c 1 || first_channel_final.len() \u003c= SUFFIX.len()\n\t\t\t|| !first_channel_final.ends_with(SUFFIX)\n\t\t{\n\t\t\treturn Err(Error::Failed(\"Invalid conform output filename\".to_string()));\n\t\t}\n\n\t\tlet base = \u0026first_channel_final[..first_channel_final.len() - SUFFIX.len()];\n\t\tlet mut final_names = Vec::with_capacity(channel_count);\n\t\tlet mut working_names = Vec::with_capacity(channel_count);\n\t\tfor i in 0..channel_count {\n\t\t\tlet final_name = format!(\"{base}.{i}.pcm\");\n\t\t\tfinal_names.push(final_name.clone());\n\t\t\tworking_names.push(format!(\"{final_name}.working\"));\n\t\t}\n\t\tOk((final_names, working_names))\n\t}\n}\n\nimpl TaskBehavior for ConformTask {\n\t/// Run the conform via the oakcodec decoder (`bridge::codec`), emitting\n\t/// progress as audio is decoded and written to the working files, then\n\t/// rename working → final.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet channel_count = self.channel_layout.count_ones() as usize;\n\t\tlet (final_names, working_names) =\n\t\t\tSelf::derive_filenames(\u0026self.output_filename, channel_count).map_err(|_| {\n\t\t\t\ttask.set_error(\"Invalid conform output filename\");\n\t\t\t\tError::Failed(\"Invalid conform output filename\".to_string())\n\t\t\t})?;\n\t\tself.final_names = final_names;\n\t\tself.working_names = working_names;\n\n\t\tlet decoder = unsafe { bridge::codec::oakcodec_decoder_init() };\n\t\tif decoder.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create decoder\");\n\t\t\treturn Err(Error::Failed(\"Failed to create decoder\".to_string()));\n\t\t}\n\t\tlet mut decoder = decoder;\n\n\t\tlet result = (|| {\n\t\t\tlet open_result = unsafe {\n\t\t\t\tbridge::codec::oakcodec_decoder_open(decoder, cstr(\u0026self.input_filename), self.stream_index)\n\t\t\t};\n\t\t\tif open_result != 0 {\n\t\t\t\tlet err = decoder_error(decoder);\n\t\t\t\ttask.set_error(\u0026format!(\"Failed to open decoder for audio conform: {err}\"));\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tlet working_ptrs: Vec\u003c*const std::ffi::c_char\u003e =\n\t\t\t\tself.working_names.iter().map(|n| cstr(n)).collect();\n\t\t\tlet conform_result = unsafe {\n\t\t\t\tbridge::codec::oakcodec_decoder_conform_audio(\n\t\t\t\t\tdecoder,\n\t\t\t\t\tworking_ptrs.as_ptr(),\n\t\t\t\t\tworking_ptrs.len() as i32,\n\t\t\t\t\tself.sample_rate,\n\t\t\t\t\tself.channel_layout,\n\t\t\t\t\tself.sample_format,\n\t\t\t\t\tbridge::render::OakCancelAtom::from_chandle(task.get_cancel_atom()),\n\t\t\t\t)\n\t\t\t};\n\t\t\tunsafe {\n\t\t\t\tbridge::codec::oakcodec_decoder_close(decoder);\n\t\t\t}\n\n\t\t\tif conform_result == 0 {\n\t\t\t\t// Rename each working file into place; a failure aborts the\n\t\t\t\t// rest (mirroring the C++ loop).\n\t\t\t\tfor i in 0..self.working_names.len() {\n\t\t\t\t\tif std::fs::rename(\u0026self.working_names[i], \u0026self.final_names[i]).is_err() {\n\t\t\t\t\t\ttask.set_error(\"Failed to move conformed audio into place\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttrue\n\t\t\t} else {\n\t\t\t\t// Clean up any partial working files.\n\t\t\t\tfor name in \u0026self.working_names {\n\t\t\t\t\tlet _ = std::fs::remove_file(name);\n\t\t\t\t}\n\t\t\t\tif conform_result == bridge::codec::OAKCODEC_E_CANCELLED {\n\t\t\t\t\ttask.set_error(\"Audio conform was cancelled\");\n\t\t\t\t} else {\n\t\t\t\t\ttask.set_error(\"Audio conform failed\");\n\t\t\t\t}\n\t\t\t\tfalse\n\t\t\t}\n\t\t})();\n\n\t\tunsafe {\n\t\t\tbridge::codec::oakcodec_decoder_free(\u0026mut decoder);\n\t\t}\n\n\t\tif result {\n\t\t\tOk(())\n\t\t} else {\n\t\t\tErr(Error::Failed(\"Audio conform failed\".to_string()))\n\t\t}\n\t}\n}\n\n/// Two-stage read of the decoder's last error string.\nfn decoder_error(decoder: CHandle) -\u003e String {\n\tlet mut buf = [0i8; 256];\n\tlet needed = unsafe { bridge::codec::oakcodec_decoder_last_error(decoder, buf.as_mut_ptr(), buf.len() as i32) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n","traces":[{"line":56,"address":[],"length":0,"stats":{"Line":7}},{"line":57,"address":[],"length":0,"stats":{"Line":21}},{"line":58,"address":[],"length":0,"stats":{"Line":21}},{"line":59,"address":[],"length":0,"stats":{"Line":14}},{"line":61,"address":[],"length":0,"stats":{"Line":28}},{"line":64,"address":[],"length":0,"stats":{"Line":14}},{"line":65,"address":[],"length":0,"stats":{"Line":14}},{"line":66,"address":[],"length":0,"stats":{"Line":14}},{"line":67,"address":[],"length":0,"stats":{"Line":14}},{"line":68,"address":[],"length":0,"stats":{"Line":7}},{"line":69,"address":[],"length":0,"stats":{"Line":7}},{"line":83,"address":[],"length":0,"stats":{"Line":13}},{"line":86,"address":[],"length":0,"stats":{"Line":49}},{"line":87,"address":[],"length":0,"stats":{"Line":11}},{"line":89,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":45}},{"line":93,"address":[],"length":0,"stats":{"Line":27}},{"line":94,"address":[],"length":0,"stats":{"Line":27}},{"line":95,"address":[],"length":0,"stats":{"Line":43}},{"line":96,"address":[],"length":0,"stats":{"Line":51}},{"line":97,"address":[],"length":0,"stats":{"Line":85}},{"line":98,"address":[],"length":0,"stats":{"Line":34}},{"line":100,"address":[],"length":0,"stats":{"Line":9}},{"line":108,"address":[],"length":0,"stats":{"Line":7}},{"line":109,"address":[],"length":0,"stats":{"Line":14}},{"line":110,"address":[],"length":0,"stats":{"Line":14}},{"line":111,"address":[],"length":0,"stats":{"Line":28}},{"line":112,"address":[],"length":0,"stats":{"Line":0}},{"line":113,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":14}},{"line":116,"address":[],"length":0,"stats":{"Line":14}},{"line":118,"address":[],"length":0,"stats":{"Line":14}},{"line":119,"address":[],"length":0,"stats":{"Line":14}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":14}},{"line":125,"address":[],"length":0,"stats":{"Line":14}},{"line":126,"address":[],"length":0,"stats":{"Line":7}},{"line":127,"address":[],"length":0,"stats":{"Line":35}},{"line":129,"address":[],"length":0,"stats":{"Line":7}},{"line":130,"address":[],"length":0,"stats":{"Line":6}},{"line":131,"address":[],"length":0,"stats":{"Line":6}},{"line":132,"address":[],"length":0,"stats":{"Line":2}},{"line":135,"address":[],"length":0,"stats":{"Line":10}},{"line":136,"address":[],"length":0,"stats":{"Line":35}},{"line":137,"address":[],"length":0,"stats":{"Line":5}},{"line":138,"address":[],"length":0,"stats":{"Line":5}},{"line":139,"address":[],"length":0,"stats":{"Line":5}},{"line":140,"address":[],"length":0,"stats":{"Line":10}},{"line":141,"address":[],"length":0,"stats":{"Line":5}},{"line":142,"address":[],"length":0,"stats":{"Line":5}},{"line":143,"address":[],"length":0,"stats":{"Line":5}},{"line":144,"address":[],"length":0,"stats":{"Line":5}},{"line":145,"address":[],"length":0,"stats":{"Line":15}},{"line":148,"address":[],"length":0,"stats":{"Line":5}},{"line":149,"address":[],"length":0,"stats":{"Line":5}},{"line":152,"address":[],"length":0,"stats":{"Line":5}},{"line":155,"address":[],"length":0,"stats":{"Line":6}},{"line":156,"address":[],"length":0,"stats":{"Line":16}},{"line":157,"address":[],"length":0,"stats":{"Line":0}},{"line":158,"address":[],"length":0,"stats":{"Line":0}},{"line":161,"address":[],"length":0,"stats":{"Line":2}},{"line":164,"address":[],"length":0,"stats":{"Line":15}},{"line":165,"address":[],"length":0,"stats":{"Line":6}},{"line":167,"address":[],"length":0,"stats":{"Line":5}},{"line":168,"address":[],"length":0,"stats":{"Line":4}},{"line":170,"address":[],"length":0,"stats":{"Line":2}},{"line":172,"address":[],"length":0,"stats":{"Line":3}},{"line":177,"address":[],"length":0,"stats":{"Line":7}},{"line":180,"address":[],"length":0,"stats":{"Line":7}},{"line":181,"address":[],"length":0,"stats":{"Line":2}},{"line":183,"address":[],"length":0,"stats":{"Line":5}},{"line":189,"address":[],"length":0,"stats":{"Line":2}},{"line":190,"address":[],"length":0,"stats":{"Line":4}},{"line":191,"address":[],"length":0,"stats":{"Line":12}},{"line":192,"address":[],"length":0,"stats":{"Line":2}},{"line":193,"address":[],"length":0,"stats":{"Line":0}},{"line":195,"address":[],"length":0,"stats":{"Line":50}},{"line":196,"address":[],"length":0,"stats":{"Line":8}}],"covered":72,"coverable":79},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","customcache.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//! `CustomCacheTask`, mirroring `src/task/src/customcache/customcachetask.h`.\n//!\n//! A task that delegates its completion to an external cache fill: it does\n//! not do work itself but waits until the cache reports it is done (via\n//! [`CustomCacheTask::finish`]) or is cancelled.\n//!\n//! CPP-PARITY: src/task/src/customcache/customcachetask.h\n\nuse std::sync::{Arc, Condvar, Mutex};\n\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// Shared state between the parked task thread, the owning cache\n/// ([`CustomCacheTask::finish`]) and the cancel hook. Mirrors the C++\n/// `mutex_`/`wait_cond_`/`cancelled_through_finish_` trio; the cancel atom\n/// copy lets `finish()` wake the task through the same channel the C++\n/// `cancel()` uses.\npub struct CustomCacheState {\n\t/// Set by `finish()` before the cancellation request.\n\tcancelled_through_finish: Mutex\u003cbool\u003e,\n\t/// Parked-task wakeup.\n\twait: Condvar,\n\t/// Callback invoked when the task is cancelled (not by `finish()`).\n\tcancelled_callback: Mutex\u003cOption\u003cBox\u003cdyn FnMut() + Send\u003e\u003e\u003e,\n\t/// Copy of the task's cancellation atom.\n\tatom: CHandle,\n}\n\nimpl CustomCacheState {\n\t/// Mark the cache fill as complete and wake the parked task.\n\tpub fn finish(\u0026self) {\n\t\t*self.cancelled_through_finish.lock().unwrap() = true;\n\t\tif !self.atom.is_null() {\n\t\t\tlet atom = crate::bridge::render::OakCancelAtom::from_chandle(self.atom);\n\t\t\tunsafe {\n\t\t\t\tcrate::bridge::render::oakrender_cancelatom_cancel(atom);\n\t\t\t}\n\t\t}\n\t\tself.wait.notify_one();\n\t}\n\n\t/// The C++ `cancel_event()` hook: fires the callback unless the task was\n\t/// finished through `finish()`, then wakes the parked task.\n\tpub fn cancel_event(\u0026self) {\n\t\tif !*self.cancelled_through_finish.lock().unwrap() {\n\t\t\tif let Some(cb) = self.cancelled_callback.lock().unwrap().as_mut() {\n\t\t\t\tcb();\n\t\t\t}\n\t\t}\n\t\tself.wait.notify_one();\n\t}\n\n\t/// The full C++ `cancel()`: cancel the atom and run the cancel-event\n\t/// hook. Used by the owning cache to abort a fill.\n\tpub fn cancel(\u0026self) {\n\t\tif !self.atom.is_null() {\n\t\t\tlet atom = crate::bridge::render::OakCancelAtom::from_chandle(self.atom);\n\t\t\tunsafe {\n\t\t\t\tcrate::bridge::render::oakrender_cancelatom_cancel(atom);\n\t\t\t}\n\t\t}\n\t\tself.cancel_event();\n\t}\n\n\t/// Whether the fill was completed through `finish()`.\n\tfn is_finished_through_finish(\u0026self) -\u003e bool {\n\t\t*self.cancelled_through_finish.lock().unwrap()\n\t}\n}\n\n/// A cache-fill task that finishes only when [`CustomCacheTask::finish`] is\n/// called by the owning cache. Uses a `Mutex` + `Condvar` to park the task\n/// thread until completion or cancellation.\npub struct CustomCacheTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// The name of the sequence being cached.\n\tpub sequence_name: String,\n\t/// Shared park/notify state.\n\tstate: Arc\u003cCustomCacheState\u003e,\n}\n\nimpl CustomCacheTask {\n\t/// Create a cache-fill task with the given sequence name. The cancel\n\t/// hook that wakes the parked thread is installed on the base task.\n\tpub fn new(sequence_name: \u0026str) -\u003e CustomCacheTask {\n\t\tlet base = Task::new(\u0026format!(\"Caching custom range for \\\"{sequence_name}\\\"\"), CHandle::null());\n\t\tlet state = Arc::new(CustomCacheState {\n\t\t\tcancelled_through_finish: Mutex::new(false),\n\t\t\twait: Condvar::new(),\n\t\t\tcancelled_callback: Mutex::new(None),\n\t\t\tatom: base.get_cancel_atom(),\n\t\t});\n\t\tlet mut task = CustomCacheTask {\n\t\t\tbase,\n\t\t\tsequence_name: sequence_name.to_string(),\n\t\t\tstate,\n\t\t};\n\t\tlet hook_state = task.state.clone();\n\t\ttask.base.set_cancel_event(Box::new(move || hook_state.cancel_event()));\n\t\ttask\n\t}\n\n\t/// Mark the cache fill as complete, waking the parked task thread.\n\tpub fn finish(\u0026mut self) {\n\t\tself.state.finish();\n\t}\n\n\t/// Install the callback invoked when the task is cancelled.\n\tpub fn set_cancelled_callback(\u0026mut self, cb: Box\u003cdyn FnMut() + Send\u003e) {\n\t\t*self.state.cancelled_callback.lock().unwrap() = Some(cb);\n\t}\n\n\t/// Shared-state accessor for the owning cache (and tests) to call\n\t/// [`CustomCacheTask::finish`] from another thread.\n\tpub fn state(\u0026self) -\u003e Arc\u003cCustomCacheState\u003e {\n\t\tself.state.clone()\n\t}\n}\n\nimpl TaskBehavior for CustomCacheTask {\n\t/// Park until [`CustomCacheTask::finish`] or cancellation; return\n\t/// `Err(Error::Cancelled)` if cancelled while waiting.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Install the cancel hook on the task this behavior actually runs\n\t\t// under (the outer task driven by the manager / C ABI). The\n\t\t// constructor-installed hook on the inner `base` only fires when the\n\t\t// base itself is cancelled; without this the outer `cancel()` would\n\t\t// set the shared atom but never wake the parked thread.\n\t\tlet hook_state = self.state.clone();\n\t\ttask.set_cancel_event(Box::new(move || hook_state.cancel_event()));\n\n\t\tlet mut guard = self.state.cancelled_through_finish.lock().unwrap();\n\t\twhile !task.is_cancelled() {\n\t\t\tguard = self.state.wait.wait(guard).unwrap();\n\t\t}\n\t\tdrop(guard);\n\t\tif self.state.is_finished_through_finish() {\n\t\t\tOk(())\n\t\t} else {\n\t\t\tErr(Error::Cancelled)\n\t\t}\n\t}\n}\n","traces":[{"line":49,"address":[],"length":0,"stats":{"Line":2}},{"line":50,"address":[],"length":0,"stats":{"Line":4}},{"line":51,"address":[],"length":0,"stats":{"Line":2}},{"line":52,"address":[],"length":0,"stats":{"Line":6}},{"line":54,"address":[],"length":0,"stats":{"Line":2}},{"line":57,"address":[],"length":0,"stats":{"Line":4}},{"line":62,"address":[],"length":0,"stats":{"Line":3}},{"line":63,"address":[],"length":0,"stats":{"Line":6}},{"line":64,"address":[],"length":0,"stats":{"Line":8}},{"line":65,"address":[],"length":0,"stats":{"Line":1}},{"line":68,"address":[],"length":0,"stats":{"Line":6}},{"line":73,"address":[],"length":0,"stats":{"Line":3}},{"line":74,"address":[],"length":0,"stats":{"Line":3}},{"line":75,"address":[],"length":0,"stats":{"Line":9}},{"line":77,"address":[],"length":0,"stats":{"Line":3}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":84,"address":[],"length":0,"stats":{"Line":5}},{"line":85,"address":[],"length":0,"stats":{"Line":10}},{"line":104,"address":[],"length":0,"stats":{"Line":5}},{"line":105,"address":[],"length":0,"stats":{"Line":20}},{"line":106,"address":[],"length":0,"stats":{"Line":15}},{"line":107,"address":[],"length":0,"stats":{"Line":10}},{"line":108,"address":[],"length":0,"stats":{"Line":10}},{"line":109,"address":[],"length":0,"stats":{"Line":15}},{"line":110,"address":[],"length":0,"stats":{"Line":5}},{"line":114,"address":[],"length":0,"stats":{"Line":10}},{"line":117,"address":[],"length":0,"stats":{"Line":15}},{"line":118,"address":[],"length":0,"stats":{"Line":15}},{"line":119,"address":[],"length":0,"stats":{"Line":5}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":124,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":2}},{"line":129,"address":[],"length":0,"stats":{"Line":6}},{"line":134,"address":[],"length":0,"stats":{"Line":5}},{"line":135,"address":[],"length":0,"stats":{"Line":10}},{"line":142,"address":[],"length":0,"stats":{"Line":5}},{"line":148,"address":[],"length":0,"stats":{"Line":15}},{"line":149,"address":[],"length":0,"stats":{"Line":15}},{"line":151,"address":[],"length":0,"stats":{"Line":20}},{"line":152,"address":[],"length":0,"stats":{"Line":11}},{"line":153,"address":[],"length":0,"stats":{"Line":12}},{"line":155,"address":[],"length":0,"stats":{"Line":10}},{"line":156,"address":[],"length":0,"stats":{"Line":5}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":3}}],"covered":43,"coverable":45},{"path":["/","Users","sunyu","Projects","oak","src","task","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/task/error.h` verbatim; project-wide\n//! -MMCCCC scheme (module registry in include/common/error.h), pass-through\n//! untranslated. Module number for task is **08**; task additionally defines\n//! `OAKTASK_E_CANCELLED` (see include/task/error.h).\n\n/// Success.\npub const OAKTASK_OK: i32 = 0;\n/// Null handle or invalid argument.\npub const OAKTASK_E_INVALID: i32 = -80001;\n/// Call not valid in the current state.\npub const OAKTASK_E_STATE: i32 = -80002;\n/// The underlying operation failed.\npub const OAKTASK_E_FAILED: i32 = -80003;\n/// Index out of range / entry not found.\npub const OAKTASK_E_NOT_FOUND: i32 = -80004;\n/// Allocation failed.\npub const OAKTASK_E_NOMEM: i32 = -80005;\n/// The task was cancelled.\npub const OAKTASK_E_CANCELLED: i32 = -80006;\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 OAKTASK_E_INVALID,\n\t\t\tError::State =\u003e OAKTASK_E_STATE,\n\t\t\tError::Failed(_) =\u003e OAKTASK_E_FAILED,\n\t\t\tError::NotFound =\u003e OAKTASK_E_NOT_FOUND,\n\t\t\tError::NoMem =\u003e OAKTASK_E_NOMEM,\n\t\t\tError::Cancelled =\u003e OAKTASK_E_CANCELLED,\n\t\t}\n\t}\n}\n","traces":[{"line":59,"address":[],"length":0,"stats":{"Line":3}},{"line":60,"address":[],"length":0,"stats":{"Line":3}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":2}}],"covered":4,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","export.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//! `ExportTask`, mirroring `src/task/src/export/export.h`.\n//!\n//! Renders the viewer output through [`crate::render::RenderTask`] and writes\n//! it to a file via the oakcodec encoder (`bridge::codec`), mapping each\n//! rendered [`Rational`] frame to an `OakFrame` and each rendered\n//! [`TimeRange`] to audio samples.\n//!\n//! **Simplifications over the C++**: no temporary-file rename dance (the\n//! encoder writes straight to the requested filename), no sidecar subtitle\n//! encoder (subtitles go through the main encoder), and no frame/audio\n//! reordering maps — the [`RenderTask`] loop delivers frames and audio in\n//! timestamp order (its reorder buffer), so the encoder is written directly.\n//!\n//! CPP-PARITY: src/task/src/export/export.h\n\nuse crate::bridge;\nuse crate::bridge::codec::OakCodecEncodingParams;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::render::{ForceParams, RenderTask, RenderTaskBehavior};\nuse crate::task::{Task, TaskBehavior};\nuse oakcore_rs::{Rational, TimeRange};\n\n/// An export task. Owns the encoder and the project copier; the base\n/// [`RenderTask`] does the frame/audio production.\npub struct ExportTask {\n\t/// The render base (itself a task).\n\tpub render: RenderTask,\n\t/// The viewer node (borrowed `OakNodeNode`) being exported.\n\tpub viewer_node: CHandle,\n\t/// The color manager (borrowed `OakNodeColorManager`).\n\tpub color_manager: CHandle,\n\t/// The encoding parameters (mirror of `oakcodec_encoding_params`).\n\tpub encoding_params: EncodingParams,\n\t/// Owning copy of the export project (borrowed `OakRenderProjectCopier`).\n\t/// Left empty in this simplified implementation (the render drives the\n\t/// viewer directly).\n\tpub copier: CHandle,\n\t/// The opened encoder.\n\tencoder: CHandle,\n\t/// The encoder subtitles are written to (the main encoder in this\n\t/// simplified implementation).\n\tsubtitle_encoder: CHandle,\n\t/// Frame counter for progress reporting.\n\tframe_time: i64,\n\t/// Streak of consecutive null frames (fails the export after 8).\n\tnull_frame_streak: i32,\n}\n\n/// Mirror of `oakcodec_encoding_params` in `include/codec/encoder.h`, kept as\n/// plain fields so the export task can build the encoder params without an\n/// oakcodec handle. Only the fields the task reads are declared; see the\n/// header for the full inventory.\npub struct EncodingParams {\n\t/// Output filename.\n\tpub filename: String,\n\t/// Export format id (`olive::ExportFormat::Format`).\n\tpub format: i32,\n\t/// Whether video is exported.\n\tpub video_enabled: bool,\n\t/// Video codec id.\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/denominator).\n\tpub video_time_base_num: i32,\n\t/// Frame duration denominator.\n\tpub video_time_base_den: i32,\n\t/// Delivery pixel format (`oakcore_rs::PixelFormat` as int).\n\tpub video_pixel_format: i32,\n\t/// Whether audio is exported.\n\tpub audio_enabled: bool,\n\t/// Audio codec id.\n\tpub audio_codec: i32,\n\t/// Whether subtitles are exported.\n\tpub subtitles_enabled: bool,\n\t/// Export length numerator (seconds rational).\n\tpub export_length_num: i32,\n\t/// Export length denominator.\n\tpub export_length_den: i32,\n}\n\nimpl ExportTask {\n\t/// Build a new export task from the viewer, color manager, and encoding\n\t/// params.\n\tpub fn new(viewer: CHandle, color_manager: CHandle, params: EncodingParams) -\u003e ExportTask {\n\t\tlet label = node_label(viewer);\n\t\tlet title = format!(\"Exporting \\\"{label}\\\"\");\n\t\tlet base = Task::new(\u0026title, CHandle::null());\n\t\tlet render = RenderTask::new(base, CHandle::null(), CHandle::null(), viewer, ForceParams::default(), None);\n\t\tExportTask {\n\t\t\trender,\n\t\t\tviewer_node: viewer,\n\t\t\tcolor_manager,\n\t\t\tencoding_params: params,\n\t\t\tcopier: CHandle::null(),\n\t\t\tencoder: CHandle::null(),\n\t\t\tsubtitle_encoder: CHandle::null(),\n\t\t\tframe_time: 0,\n\t\t\tnull_frame_streak: 0,\n\t\t}\n\t}\n\n\t/// Build the C-ABI encoding-params POD for the encoder from the Rust\n\t/// mirror (zeroed fields = disabled/defaults).\n\tfn build_codec_params(\u0026self) -\u003e OakCodecEncodingParams {\n\t\tlet mut params = OakCodecEncodingParams {\n\t\t\tfilename: [0; 1024],\n\t\t\tformat: 0,\n\t\t\tvideo_enabled: 0,\n\t\t\tvideo_codec: 0,\n\t\t\tvideo_width: 0,\n\t\t\tvideo_height: 0,\n\t\t\tvideo_time_base_num: 0,\n\t\t\tvideo_time_base_den: 0,\n\t\t\tvideo_pixel_format: 0,\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: 0,\n\t\t\taudio_enabled: 0,\n\t\t\taudio_codec: 0,\n\t\t\taudio_sample_rate: 0,\n\t\t\taudio_channel_layout: 0,\n\t\t\taudio_sample_format: 0,\n\t\t\taudio_bit_rate: 0,\n\t\t\tsubtitles_enabled: 0,\n\t\t\tsubtitles_codec: 0,\n\t\t\tsubtitles_are_sidecar: 0,\n\t\t\tsubtitles_sidecar_format: 0,\n\t\t\tcolor_transform_output: [0; 256],\n\t\t\texport_length_num: 0,\n\t\t\texport_length_den: 0,\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\tlet bytes = self.encoding_params.filename.as_bytes();\n\t\tlet n = bytes.len().min(1023);\n\t\tparams.filename[..n].copy_from_slice(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const i8, n) });\n\t\tparams.format = self.encoding_params.format;\n\t\tparams.video_enabled = self.encoding_params.video_enabled as i32;\n\t\tparams.video_codec = self.encoding_params.video_codec;\n\t\tparams.video_width = self.encoding_params.video_width;\n\t\tparams.video_height = self.encoding_params.video_height;\n\t\tparams.video_time_base_num = self.encoding_params.video_time_base_num;\n\t\tparams.video_time_base_den = self.encoding_params.video_time_base_den;\n\t\tparams.video_pixel_format = self.encoding_params.video_pixel_format;\n\t\tparams.audio_enabled = self.encoding_params.audio_enabled as i32;\n\t\tparams.audio_codec = self.encoding_params.audio_codec;\n\t\tparams.subtitles_enabled = self.encoding_params.subtitles_enabled as i32;\n\t\tparams.export_length_num = self.encoding_params.export_length_num;\n\t\tparams.export_length_den = self.encoding_params.export_length_den;\n\t\tparams\n\t}\n\n\t/// Resolve the export range: the custom range when set, otherwise the\n\t/// whole sequence length.\n\tfn export_range(\u0026self) -\u003e TimeRange {\n\t\tlet mut len_num = 0;\n\t\tlet mut len_den = 1;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_sequence_get_length(self.viewer_node, \u0026mut len_num, \u0026mut len_den);\n\t\t}\n\t\tTimeRange::new(Rational::new(0, 1), Rational::new(len_num as i64, len_den as i64))\n\t}\n}\n\nimpl TaskBehavior for ExportTask {\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Share the caller's cancellation atom with the inner render base.\n\t\tself.render.base.set_cancel_atom(task.get_cancel_atom());\n\n\t\tlet codec_params = self.build_codec_params();\n\t\tlet encoder = unsafe { bridge::codec::oakcodec_encoder_init(\u0026codec_params) };\n\t\tif encoder.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create encoder\");\n\t\t\treturn Err(Error::Failed(\"Failed to create encoder\".to_string()));\n\t\t}\n\t\tself.encoder = encoder;\n\n\t\tif unsafe { bridge::codec::oakcodec_encoder_open(encoder) } != 0 {\n\t\t\tlet err = encoder_error(encoder);\n\t\t\ttask.set_error(\u0026format!(\"Failed to open file: {err}\"));\n\t\t\treturn Err(Error::Failed(\"Failed to open file\".to_string()));\n\t\t}\n\t\tself.subtitle_encoder = encoder;\n\n\t\tlet export_range = self.export_range();\n\n\t\t// Force params: force the renderer to the requested output size and\n\t\t// pixel format (no color-matrix scaling in this simplified path).\n\t\tlet mut force = ForceParams::default();\n\t\tif self.encoding_params.video_enabled {\n\t\t\tforce.force_width = self.encoding_params.video_width;\n\t\t\tforce.force_height = self.encoding_params.video_height;\n\t\t\tforce.force_format = unsafe { bridge::codec::oakcodec_encoder_get_desired_pixel_format(encoder) };\n\t\t\tforce.force_channel_count = 4; // RGBA\n\t\t}\n\t\tself.render.force_params = force;\n\t\tself.render.set_render_inputs(\n\t\t\tself.color_manager,\n\t\t\tCHandle::null(),\n\t\t\t0, // RenderMode::k_online\n\t\t\tself.encoding_params.audio_enabled,\n\t\t\texport_range,\n\t\t);\n\n\t\t// Drive the render with `self` as the subclass behavior (the C++\n\t\t// virtual dispatch receiver). The render is temporarily moved out of\n\t\t// `self` to avoid a self-referential borrow; it is put back right\n\t\t// after, before the encoder flush below.\n\t\tlet mut render = std::mem::replace(\u0026mut self.render, crate::render::RenderTask::placeholder());\n\t\tlet result = render.render(task, self);\n\t\tself.render = render;\n\t\tresult?;\n\n\t\t// Flush the encoder and surface any trailing error.\n\t\tunsafe {\n\t\t\tbridge::codec::oakcodec_encoder_flush(self.encoder);\n\t\t}\n\t\tlet err = encoder_error(self.encoder);\n\t\tif !err.is_empty() {\n\t\t\ttask.set_error(\u0026err);\n\t\t\treturn Err(Error::Failed(\"Encoder flush failed\".to_string()));\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl RenderTaskBehavior for ExportTask {\n\tfn frame_downloaded(\u0026mut self, task: \u0026mut Task, frame: CHandle) -\u003e Result\u003c()\u003e {\n\t\tif frame.ctx.is_null() {\n\t\t\tself.null_frame_streak += 1;\n\t\t\tif self.null_frame_streak \u003e= 8 {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Render workers failed to deliver {} consecutive frames; aborting export\",\n\t\t\t\t\tself.null_frame_streak\n\t\t\t\t));\n\t\t\t\treturn Err(Error::Failed(\"Too many null frames\".to_string()));\n\t\t\t}\n\t\t\treturn Ok(());\n\t\t}\n\t\tself.null_frame_streak = 0;\n\n\t\tif unsafe { bridge::codec::oakcodec_encoder_write_video(self.encoder, frame) } != 0 {\n\t\t\tlet err = encoder_error(self.encoder);\n\t\t\ttask.set_error(\u0026err);\n\t\t\treturn Err(Error::Failed(\"Failed to write video frame\".to_string()));\n\t\t}\n\n\t\tself.frame_time += 1;\n\t\t// Progress is reported by the render loop itself (native signalling);\n\t\t// the per-frame counter is kept for parity with the C++ field.\n\t\tOk(())\n\t}\n\n\tfn audio_downloaded(\u0026mut self, task: \u0026mut Task, buffer: CHandle) -\u003e Result\u003c()\u003e {\n\t\t// Simplified: audio writing is not wired through the sample buffer\n\t\t// in this rewrite (the encoder receives the interleaved samples from\n\t\t// the codec side directly). The hook exists for parity with the C++\n\t\t// virtual and always succeeds.\n\t\tlet _ = (task, buffer);\n\t\tOk(())\n\t}\n\n\tfn encode_subtitle(\u0026mut self, task: \u0026mut Task, text: \u0026str) -\u003e Result\u003c()\u003e {\n\t\tif !self.encoding_params.subtitles_enabled || self.subtitle_encoder.ctx.is_null() {\n\t\t\treturn Ok(());\n\t\t}\n\t\t// The simplified path does not carry the subtitle block's in/out\n\t\t// times; write with 0.0/0.0 (the encoder default interval).\n\t\tif unsafe { bridge::codec::oakcodec_encoder_write_subtitle(self.subtitle_encoder, cstr(text), 0.0, 0.0) } != 0 {\n\t\t\tlet err = encoder_error(self.subtitle_encoder);\n\t\t\ttask.set_error(\u0026err);\n\t\t\treturn Err(Error::Failed(\"Failed to write subtitle\".to_string()));\n\t\t}\n\t\tOk(())\n\t}\n}\n\n/// Two-stage read of the viewer node's label.\nfn node_label(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// Read a NUL-terminated char buffer into a String (lossy).\nfn buf_to_string(buf: \u0026[i8]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n\n/// Two-stage read of the encoder's last error string.\nfn encoder_error(encoder: CHandle) -\u003e String {\n\tlet mut buf = [0i8; 512];\n\tlet needed = unsafe { bridge::codec::oakcodec_encoder_last_error(encoder, buf.as_mut_ptr(), buf.len() as i32) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tbuf_to_string(\u0026buf)\n}\n","traces":[{"line":105,"address":[],"length":0,"stats":{"Line":8}},{"line":106,"address":[],"length":0,"stats":{"Line":24}},{"line":107,"address":[],"length":0,"stats":{"Line":16}},{"line":108,"address":[],"length":0,"stats":{"Line":32}},{"line":109,"address":[],"length":0,"stats":{"Line":64}},{"line":115,"address":[],"length":0,"stats":{"Line":16}},{"line":116,"address":[],"length":0,"stats":{"Line":8}},{"line":117,"address":[],"length":0,"stats":{"Line":8}},{"line":125,"address":[],"length":0,"stats":{"Line":8}},{"line":127,"address":[],"length":0,"stats":{"Line":16}},{"line":144,"address":[],"length":0,"stats":{"Line":8}},{"line":157,"address":[],"length":0,"stats":{"Line":8}},{"line":166,"address":[],"length":0,"stats":{"Line":24}},{"line":167,"address":[],"length":0,"stats":{"Line":32}},{"line":168,"address":[],"length":0,"stats":{"Line":32}},{"line":169,"address":[],"length":0,"stats":{"Line":8}},{"line":170,"address":[],"length":0,"stats":{"Line":8}},{"line":171,"address":[],"length":0,"stats":{"Line":8}},{"line":172,"address":[],"length":0,"stats":{"Line":8}},{"line":173,"address":[],"length":0,"stats":{"Line":8}},{"line":174,"address":[],"length":0,"stats":{"Line":8}},{"line":175,"address":[],"length":0,"stats":{"Line":8}},{"line":176,"address":[],"length":0,"stats":{"Line":8}},{"line":177,"address":[],"length":0,"stats":{"Line":8}},{"line":178,"address":[],"length":0,"stats":{"Line":8}},{"line":179,"address":[],"length":0,"stats":{"Line":8}},{"line":180,"address":[],"length":0,"stats":{"Line":8}},{"line":181,"address":[],"length":0,"stats":{"Line":8}},{"line":182,"address":[],"length":0,"stats":{"Line":8}},{"line":187,"address":[],"length":0,"stats":{"Line":6}},{"line":188,"address":[],"length":0,"stats":{"Line":12}},{"line":189,"address":[],"length":0,"stats":{"Line":12}},{"line":191,"address":[],"length":0,"stats":{"Line":18}},{"line":193,"address":[],"length":0,"stats":{"Line":30}},{"line":198,"address":[],"length":0,"stats":{"Line":8}},{"line":200,"address":[],"length":0,"stats":{"Line":32}},{"line":202,"address":[],"length":0,"stats":{"Line":24}},{"line":203,"address":[],"length":0,"stats":{"Line":24}},{"line":204,"address":[],"length":0,"stats":{"Line":16}},{"line":205,"address":[],"length":0,"stats":{"Line":3}},{"line":206,"address":[],"length":0,"stats":{"Line":1}},{"line":208,"address":[],"length":0,"stats":{"Line":7}},{"line":210,"address":[],"length":0,"stats":{"Line":7}},{"line":211,"address":[],"length":0,"stats":{"Line":3}},{"line":212,"address":[],"length":0,"stats":{"Line":3}},{"line":213,"address":[],"length":0,"stats":{"Line":1}},{"line":215,"address":[],"length":0,"stats":{"Line":6}},{"line":217,"address":[],"length":0,"stats":{"Line":18}},{"line":221,"address":[],"length":0,"stats":{"Line":12}},{"line":222,"address":[],"length":0,"stats":{"Line":12}},{"line":223,"address":[],"length":0,"stats":{"Line":12}},{"line":224,"address":[],"length":0,"stats":{"Line":12}},{"line":225,"address":[],"length":0,"stats":{"Line":6}},{"line":226,"address":[],"length":0,"stats":{"Line":6}},{"line":228,"address":[],"length":0,"stats":{"Line":6}},{"line":229,"address":[],"length":0,"stats":{"Line":12}},{"line":230,"address":[],"length":0,"stats":{"Line":6}},{"line":231,"address":[],"length":0,"stats":{"Line":6}},{"line":233,"address":[],"length":0,"stats":{"Line":6}},{"line":234,"address":[],"length":0,"stats":{"Line":6}},{"line":241,"address":[],"length":0,"stats":{"Line":24}},{"line":242,"address":[],"length":0,"stats":{"Line":30}},{"line":243,"address":[],"length":0,"stats":{"Line":12}},{"line":244,"address":[],"length":0,"stats":{"Line":9}},{"line":248,"address":[],"length":0,"stats":{"Line":3}},{"line":250,"address":[],"length":0,"stats":{"Line":9}},{"line":251,"address":[],"length":0,"stats":{"Line":3}},{"line":252,"address":[],"length":0,"stats":{"Line":3}},{"line":253,"address":[],"length":0,"stats":{"Line":1}},{"line":256,"address":[],"length":0,"stats":{"Line":2}},{"line":261,"address":[],"length":0,"stats":{"Line":45}},{"line":262,"address":[],"length":0,"stats":{"Line":90}},{"line":263,"address":[],"length":0,"stats":{"Line":8}},{"line":264,"address":[],"length":0,"stats":{"Line":8}},{"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":269,"address":[],"length":0,"stats":{"Line":1}},{"line":271,"address":[],"length":0,"stats":{"Line":7}},{"line":273,"address":[],"length":0,"stats":{"Line":37}},{"line":275,"address":[],"length":0,"stats":{"Line":74}},{"line":276,"address":[],"length":0,"stats":{"Line":3}},{"line":277,"address":[],"length":0,"stats":{"Line":3}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":281,"address":[],"length":0,"stats":{"Line":36}},{"line":284,"address":[],"length":0,"stats":{"Line":36}},{"line":287,"address":[],"length":0,"stats":{"Line":1}},{"line":292,"address":[],"length":0,"stats":{"Line":1}},{"line":293,"address":[],"length":0,"stats":{"Line":1}},{"line":296,"address":[],"length":0,"stats":{"Line":0}},{"line":297,"address":[],"length":0,"stats":{"Line":0}},{"line":298,"address":[],"length":0,"stats":{"Line":0}},{"line":302,"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":307,"address":[],"length":0,"stats":{"Line":0}},{"line":312,"address":[],"length":0,"stats":{"Line":8}},{"line":313,"address":[],"length":0,"stats":{"Line":32}},{"line":314,"address":[],"length":0,"stats":{"Line":8}},{"line":315,"address":[],"length":0,"stats":{"Line":0}},{"line":317,"address":[],"length":0,"stats":{"Line":24}},{"line":319,"address":[],"length":0,"stats":{"Line":32}},{"line":321,"address":[],"length":0,"stats":{"Line":16}},{"line":325,"address":[],"length":0,"stats":{"Line":9}},{"line":326,"address":[],"length":0,"stats":{"Line":81}},{"line":327,"address":[],"length":0,"stats":{"Line":36}},{"line":331,"address":[],"length":0,"stats":{"Line":5}},{"line":332,"address":[],"length":0,"stats":{"Line":10}},{"line":333,"address":[],"length":0,"stats":{"Line":30}},{"line":334,"address":[],"length":0,"stats":{"Line":5}},{"line":335,"address":[],"length":0,"stats":{"Line":4}},{"line":337,"address":[],"length":0,"stats":{"Line":2}}],"covered":104,"coverable":113},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi","manager.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//! `oaktask_manager_*` export symbols mirroring `include/task/manager.h`.\n//!\n//! Full symbol inventory (header-authoritative):\n//! - `oaktask_manager_init(void) -\u003e int`\n//! - `oaktask_manager_shutdown(void) -\u003e void`\n//! - `oaktask_register_codec_submitter(void) -\u003e int`\n//! - `oaktask_manager_count(void) -\u003e int`\n//! - `oaktask_manager_at(int i) -\u003e OakTaskTask`\n//! - `oaktask_manager_delete_finished(void) -\u003e void`\n\nuse std::ffi::c_int;\n\nuse crate::error::{OAKTASK_E_STATE, OAKTASK_OK};\nuse crate::ffi::taskhandle::wrap_borrowed;\nuse crate::handle::CHandle;\nuse crate::manager::TaskManager;\n\n/// `oaktask_manager_init` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_init() -\u003e c_int {\n\tif let Err(_) = TaskManager::init() {\n\t\treturn OAKTASK_E_STATE;\n\t}\n\t// Register the codec task submitter (mirrors the C++ init sequence).\n\tlet _ = crate::codecbridge::register_codec_task_submitter();\n\tTaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(true));\n\tOAKTASK_OK\n}\n\n/// `oaktask_manager_shutdown` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_shutdown() {\n\tTaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(false));\n\tlet _ = crate::codecbridge::unregister_codec_task_submitter();\n\tTaskManager::shutdown();\n}\n\n/// `oaktask_register_codec_submitter` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_register_codec_submitter() -\u003e c_int {\n\tmatch crate::codecbridge::register_codec_task_submitter() {\n\t\tOk(()) =\u003e OAKTASK_OK,\n\t\tErr(_) =\u003e OAKTASK_OK,\n\t}\n}\n\n/// `oaktask_manager_count` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_count() -\u003e c_int {\n\tmatch TaskManager::with_manager(|m| m.get_task_count()) {\n\t\tSome(count) =\u003e count as c_int,\n\t\tNone =\u003e OAKTASK_E_STATE,\n\t}\n}\n\n/// `oaktask_manager_at` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_at(i: c_int) -\u003e CHandle {\n\t// No manager (or out of range) -\u003e empty handle, mirroring the C++\n\t// `OakTaskTask{}` return.\n\tlet ptr = match TaskManager::with_manager(|m| m.task_ptr_at(i as usize)) {\n\t\tSome(Ok(ptr)) =\u003e ptr,\n\t\t_ =\u003e return CHandle::null(),\n\t};\n\twrap_borrowed(ptr)\n}\n\n/// `oaktask_manager_delete_finished` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_delete_finished() {\n\t// Collect the finished entries and join their threads without holding\n\t// the manager lock (a worker's finish callback may call back into the\n\t// manager).\n\tlet entries = TaskManager::with_manager_mut(|m| m.drain_finished()).unwrap_or_default();\n\tfor (_task, handle) in entries {\n\t\tlet _ = handle.join();\n\t}\n}\n","traces":[{"line":36,"address":[],"length":0,"stats":{"Line":8}},{"line":37,"address":[],"length":0,"stats":{"Line":8}},{"line":38,"address":[],"length":0,"stats":{"Line":0}},{"line":41,"address":[],"length":0,"stats":{"Line":8}},{"line":42,"address":[],"length":0,"stats":{"Line":24}},{"line":43,"address":[],"length":0,"stats":{"Line":8}},{"line":48,"address":[],"length":0,"stats":{"Line":34}},{"line":49,"address":[],"length":0,"stats":{"Line":50}},{"line":50,"address":[],"length":0,"stats":{"Line":34}},{"line":51,"address":[],"length":0,"stats":{"Line":34}},{"line":56,"address":[],"length":0,"stats":{"Line":14}},{"line":57,"address":[],"length":0,"stats":{"Line":14}},{"line":58,"address":[],"length":0,"stats":{"Line":12}},{"line":59,"address":[],"length":0,"stats":{"Line":2}},{"line":65,"address":[],"length":0,"stats":{"Line":9}},{"line":66,"address":[],"length":0,"stats":{"Line":21}},{"line":67,"address":[],"length":0,"stats":{"Line":12}},{"line":68,"address":[],"length":0,"stats":{"Line":3}},{"line":74,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":13}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":5}},{"line":81,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":6}},{"line":90,"address":[],"length":0,"stats":{"Line":26}},{"line":91,"address":[],"length":0,"stats":{"Line":6}},{"line":92,"address":[],"length":0,"stats":{"Line":0}}],"covered":25,"coverable":27},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi","project.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//! `oaktask_create_*` export symbols mirroring `include/task/project.h`.\n//!\n//! Full symbol inventory (header-authoritative):\n//! - `oaktask_create_project_load(const char *filename) -\u003e OakTaskTask`\n//! - `oaktask_load_take_project(OakTaskTask t) -\u003e OakNodeProject`\n//! - `oaktask_create_project_save(OakNodeProject, const char *filename_or_NULL, int use_compression) -\u003e OakTaskTask`\n//! - `oaktask_create_project_import(OakNodeFolder, OakNodeProject, const char *const *urls, int url_count) -\u003e OakTaskTask`\n//! - `oaktask_import_take_command(OakTaskTask t) -\u003e OakUndoCommand`\n//! - `oaktask_import_footage_count(OakTaskTask t) -\u003e int`\n//! - `oaktask_import_footage_at(OakTaskTask t, int index) -\u003e OakNodeFootage`\n//! - `oaktask_import_invalid_count(OakTaskTask t) -\u003e int`\n//! - `oaktask_import_invalid_at(OakTaskTask t, int index, char *buf, int buf_size) -\u003e int` (two-stage)\n//! - `oaktask_create_project_load_otio(const char *filename) -\u003e OakTaskTask`\n//! - `oaktask_load_otio_take_project(OakTaskTask t) -\u003e OakNodeProject`\n//! - `oaktask_create_project_save_otio(OakNodeProject, const char *filename) -\u003e OakTaskTask`\n//! - `oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, void *userdata) -\u003e void`\n//! - `oaktask_create_precache(OakNodeFootage, int index, OakNodeSequence) -\u003e OakTaskTask`\n//! - `oaktask_create_export(OakNodeNode viewer, OakNodeColorManager, const oakcodec_encoding_params *) -\u003e OakTaskTask`\n//! - `oaktask_import_set_image_sequence_confirm_cb(oaktask_image_sequence_confirm_fn fn, void *userdata) -\u003e void`\n\nuse std::ffi::{c_char, c_int, c_void};\nuse std::sync::Mutex;\n\nuse crate::bridge::codec::OakCodecEncodingParams;\nuse crate::error::{OAKTASK_E_INVALID, OAKTASK_E_NOT_FOUND};\nuse crate::export::{EncodingParams, ExportTask};\nuse crate::ffi::taskhandle::{\n\tcopy_string, cstr, cstr_to_string, get_task, wrap_owned, wrap_owned_with_impl, TaskImpl,\n};\nuse crate::handle::CHandle;\nuse crate::precache::PreCacheTask;\nuse crate::project::import::{import_file_count, import_title, ImageSequenceConfirmFn, ProjectImportTask};\nuse crate::project::load::{ProjectLoadBaseTask, ProjectLoadTask};\nuse crate::project::loadotio::{set_import_confirm_callback, LoadOTIOTask};\nuse crate::project::save::{project_filename, ProjectSaveTask};\nuse crate::project::saveotio::SaveOTIOTask;\nuse crate::task::Task;\n\n/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).\npub type OakTaskOtioImportConfirmFn =\n\tunsafe extern \"C\" fn(sequence_names: *const *const c_char, count: c_int, userdata: *mut c_void) -\u003e c_int;\n\n/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).\npub type OakTaskImageSequenceConfirmFn =\n\tunsafe extern \"C\" fn(filename: *const c_char, userdata: *mut c_void) -\u003e c_int;\n\n/// Global image-sequence confirm callback (facade concern; `null` clears\n/// it). Snapshot into each import task at creation.\nstatic IMAGE_SEQUENCE_CONFIRM: Mutex\u003cOption\u003cImageSequenceConfirmFn\u003e\u003e = Mutex::new(None);\n\n/// `oaktask_create_project_load` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_load(filename: *const c_char) -\u003e CHandle {\n\tif filename.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet filename = unsafe { cstr_to_string(filename) };\n\tlet title = format!(\"Loading '{filename}'\");\n\n\tlet inner = Task::new(\u0026title, CHandle::null());\n\tlet base = ProjectLoadBaseTask::new(inner, filename.clone());\n\tlet load_task = ProjectLoadTask { base };\n\tlet atom = load_task.base.base.get_cancel_atom();\n\tlet mut boxed = Box::new(load_task);\n\tlet base_ptr = \u0026mut boxed.base as *mut ProjectLoadBaseTask;\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr))\n}\n\n/// `oaktask_load_take_project` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_load_take_project(t: CHandle) -\u003e CHandle {\n\tload_take_project(\u0026t)\n}\n\n/// Shared implementation of `load_take_project`/`load_otio_take_project`.\nfn load_take_project(t: \u0026CHandle) -\u003e CHandle {\n\tlet Some(h) = get_task(t) else {\n\t\treturn CHandle::null();\n\t};\n\tlet TaskImpl::LoadBase(base_ptr) = h.impl_kind else {\n\t\treturn CHandle::null();\n\t};\n\tmatch unsafe { (\u0026mut *base_ptr).take_project() } {\n\t\tOk(project) =\u003e project,\n\t\tErr(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// `oaktask_create_project_save` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_save(\n\tproject: CHandle,\n\tfilename_or_null: *const c_char,\n\tuse_compression: c_int,\n) -\u003e CHandle {\n\tif project.ctx.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet title = format!(\"Saving '{}'\", project_filename(project));\n\tlet mut save_task = ProjectSaveTask {\n\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\tproject,\n\t\toverride_filename: None,\n\t\tuse_compression: use_compression != 0,\n\t};\n\tif !filename_or_null.is_null() {\n\t\tsave_task.set_override_filename(\u0026unsafe { cstr_to_string(filename_or_null) });\n\t}\n\tlet atom = save_task.base.get_cancel_atom();\n\tlet boxed = Box::new(save_task);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_create_project_import` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_import(\n\tfolder: CHandle,\n\tproject: CHandle,\n\turls: *const *const c_char,\n\turl_count: c_int,\n) -\u003e CHandle {\n\tif folder.ctx.is_null() || project.ctx.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tif url_count \u003c 0 || (urls.is_null() \u0026\u0026 url_count \u003e 0) {\n\t\treturn CHandle::null();\n\t}\n\tlet mut filenames = Vec::with_capacity(url_count as usize);\n\tfor i in 0..url_count {\n\t\tlet url = unsafe { *urls.add(i as usize) };\n\t\tif url.is_null() {\n\t\t\treturn CHandle::null();\n\t\t}\n\t\tfilenames.push(unsafe { cstr_to_string(url) });\n\t}\n\n\tlet title = import_title(\u0026filenames);\n\tlet file_count = import_file_count(\u0026filenames);\n\tlet import_task = ProjectImportTask::new(\n\t\tTask::new(\u0026title, CHandle::null()),\n\t\tfolder,\n\t\tproject,\n\t\tfilenames,\n\t\tIMAGE_SEQUENCE_CONFIRM.lock().unwrap().take(),\n\t\tfile_count,\n\t);\n\tlet atom = import_task.base.get_cancel_atom();\n\tlet mut boxed = Box::new(import_task);\n\tlet import_ptr = \u0026mut *boxed as *mut ProjectImportTask;\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned_with_impl(Box::new(outer), TaskImpl::Import(import_ptr))\n}\n\n/// Borrow the `ProjectImportTask` behind the handle (the C++ `import_impl`\n/// dynamic_cast equivalent).\nfn import_impl(t: \u0026CHandle) -\u003e Option\u003c*mut ProjectImportTask\u003e {\n\tlet h = get_task(t)?;\n\tmatch h.impl_kind {\n\t\tTaskImpl::Import(p) =\u003e Some(p),\n\t\t_ =\u003e None,\n\t}\n}\n\n/// `oaktask_import_take_command` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_take_command(t: CHandle) -\u003e CHandle {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn CHandle::null();\n\t};\n\tmatch unsafe { (\u0026mut *p).take_command() } {\n\t\tOk(command) =\u003e command,\n\t\tErr(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// `oaktask_import_footage_count` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_footage_count(t: CHandle) -\u003e c_int {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn OAKTASK_E_INVALID;\n\t};\n\tunsafe { (*p).get_file_count() as c_int }\n}\n\n/// `oaktask_import_footage_at` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_footage_at(t: CHandle, index: c_int) -\u003e CHandle {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn CHandle::null();\n\t};\n\tif index \u003c 0 {\n\t\treturn CHandle::null();\n\t}\n\tmatch unsafe { (*p).get_imported_footage(index as usize) } {\n\t\tOk(footage) =\u003e footage,\n\t\tErr(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// `oaktask_import_invalid_count` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_invalid_count(t: CHandle) -\u003e c_int {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn OAKTASK_E_INVALID;\n\t};\n\tunsafe { (*p).get_invalid_file_count() as c_int }\n}\n\n/// `oaktask_import_invalid_at` (`include/task/project.h`, two-stage string getter).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_invalid_at(\n\tt: CHandle,\n\tindex: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn OAKTASK_E_INVALID;\n\t};\n\tlet task = unsafe { \u0026*p };\n\tlet count = task.get_invalid_file_count();\n\tif index \u003c 0 || index as usize \u003e= count {\n\t\treturn OAKTASK_E_NOT_FOUND;\n\t}\n\tcopy_string(task.invalid_file_at(index as usize), buf, buf_size)\n}\n\n/// `oaktask_create_project_load_otio` (`include/task/project.h`). The\n/// interchange format is inferred from the filename extension (`.otio` /\n/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C\n/// ABI needs no format parameter.\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_load_otio(filename: *const c_char) -\u003e CHandle {\n\tif filename.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet filename = unsafe { cstr_to_string(filename) };\n\tlet title = format!(\"Loading '{filename}'\");\n\n\tlet inner = Task::new(\u0026title, CHandle::null());\n\tlet base = ProjectLoadBaseTask::new(inner, filename.clone());\n\tlet otio_task = LoadOTIOTask::new(base);\n\tlet atom = otio_task.base.base.get_cancel_atom();\n\tlet mut boxed = Box::new(otio_task);\n\tlet base_ptr = \u0026mut boxed.base as *mut ProjectLoadBaseTask;\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr))\n}\n\n/// `oaktask_load_otio_take_project` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_load_otio_take_project(t: CHandle) -\u003e CHandle {\n\tload_take_project(\u0026t)\n}\n\n/// `oaktask_create_project_save_otio` (`include/task/project.h`). The\n/// interchange format is inferred from the filename extension (`.otio` /\n/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C\n/// ABI needs no format parameter.\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -\u003e CHandle {\n\tif project.ctx.is_null() || filename.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet filename = unsafe { cstr_to_string(filename) };\n\tlet title = format!(\"Saving '{filename}'\");\n\tlet task = SaveOTIOTask {\n\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\tproject,\n\t\tfilename,\n\t};\n\tlet atom = task.base.get_cancel_atom();\n\tlet boxed = Box::new(task);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_load_otio_set_confirm_cb` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_load_otio_set_confirm_cb(cb: Option\u003cOakTaskOtioImportConfirmFn\u003e, userdata: *mut c_void) {\n\tlet Some(cb) = cb else {\n\t\tset_import_confirm_callback(None);\n\t\treturn;\n\t};\n\tlet userdata = userdata as usize;\n\tset_import_confirm_callback(Some(Box::new(move |sequence_names: \u0026[String]| {\n\t\tlet ptrs: Vec\u003c*const c_char\u003e = sequence_names.iter().map(|n| cstr(n)).collect();\n\t\tunsafe { cb(ptrs.as_ptr(), ptrs.len() as c_int, userdata as *mut c_void) != 0 }\n\t})));\n}\n\n/// `oaktask_create_precache` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -\u003e CHandle {\n\tif footage.ctx.is_null() || sequence.ctx.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet precache = PreCacheTask::new(footage, index, sequence);\n\tlet title = precache.render.base.title().to_string();\n\tlet atom = precache.render.base.get_cancel_atom();\n\tlet boxed = Box::new(precache);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_create_export` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_export(\n\tviewer: CHandle,\n\tcolor_manager: CHandle,\n\tparams: *const OakCodecEncodingParams,\n) -\u003e CHandle {\n\tif viewer.ctx.is_null() || params.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet encoding = unsafe { convert_encoding_params(\u0026*params) };\n\tlet export = ExportTask::new(viewer, color_manager, encoding);\n\tlet title = export.render.base.title().to_string();\n\tlet atom = export.render.base.get_cancel_atom();\n\tlet boxed = Box::new(export);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_import_set_image_sequence_confirm_cb` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_set_image_sequence_confirm_cb(\n\tcb: Option\u003cOakTaskImageSequenceConfirmFn\u003e,\n\tuserdata: *mut c_void,\n) {\n\tlet mut guard = IMAGE_SEQUENCE_CONFIRM.lock().unwrap();\n\tlet Some(cb) = cb else {\n\t\t*guard = None;\n\t\treturn;\n\t};\n\tlet userdata = userdata as usize;\n\t*guard = Some(Box::new(move |filename: \u0026str, _other: \u0026str| unsafe {\n\t\tcb(cstr(filename), userdata as *mut c_void) != 0\n\t}));\n}\n\n/// Copy the C-ABI encoding params into the Rust mirror (subset read by the\n/// export task).\nunsafe fn convert_encoding_params(p: \u0026OakCodecEncodingParams) -\u003e EncodingParams {\n\tEncodingParams {\n\t\tfilename: unsafe { c_char_array_to_string(\u0026p.filename) },\n\t\tformat: p.format,\n\t\tvideo_enabled: p.video_enabled != 0,\n\t\tvideo_codec: p.video_codec,\n\t\tvideo_width: p.video_width,\n\t\tvideo_height: p.video_height,\n\t\tvideo_time_base_num: p.video_time_base_num,\n\t\tvideo_time_base_den: p.video_time_base_den,\n\t\tvideo_pixel_format: p.video_pixel_format,\n\t\taudio_enabled: p.audio_enabled != 0,\n\t\taudio_codec: p.audio_codec,\n\t\tsubtitles_enabled: p.subtitles_enabled != 0,\n\t\texport_length_num: p.export_length_num,\n\t\texport_length_den: p.export_length_den,\n\t}\n}\n\n/// Read a NUL-terminated char array into a String (lossy).\nunsafe fn c_char_array_to_string(buf: \u0026[c_char]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tlet bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };\n\tString::from_utf8_lossy(bytes).into_owned()\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":1}},{"line":73,"address":[],"length":0,"stats":{"Line":6}},{"line":74,"address":[],"length":0,"stats":{"Line":4}},{"line":76,"address":[],"length":0,"stats":{"Line":8}},{"line":77,"address":[],"length":0,"stats":{"Line":10}},{"line":78,"address":[],"length":0,"stats":{"Line":4}},{"line":79,"address":[],"length":0,"stats":{"Line":6}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":4}},{"line":82,"address":[],"length":0,"stats":{"Line":8}},{"line":83,"address":[],"length":0,"stats":{"Line":6}},{"line":84,"address":[],"length":0,"stats":{"Line":8}},{"line":89,"address":[],"length":0,"stats":{"Line":3}},{"line":90,"address":[],"length":0,"stats":{"Line":6}},{"line":94,"address":[],"length":0,"stats":{"Line":23}},{"line":95,"address":[],"length":0,"stats":{"Line":46}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":46}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":23}},{"line":102,"address":[],"length":0,"stats":{"Line":26}},{"line":103,"address":[],"length":0,"stats":{"Line":10}},{"line":109,"address":[],"length":0,"stats":{"Line":17}},{"line":114,"address":[],"length":0,"stats":{"Line":34}},{"line":115,"address":[],"length":0,"stats":{"Line":2}},{"line":117,"address":[],"length":0,"stats":{"Line":60}},{"line":119,"address":[],"length":0,"stats":{"Line":60}},{"line":122,"address":[],"length":0,"stats":{"Line":15}},{"line":124,"address":[],"length":0,"stats":{"Line":18}},{"line":125,"address":[],"length":0,"stats":{"Line":9}},{"line":127,"address":[],"length":0,"stats":{"Line":45}},{"line":128,"address":[],"length":0,"stats":{"Line":45}},{"line":129,"address":[],"length":0,"stats":{"Line":60}},{"line":130,"address":[],"length":0,"stats":{"Line":45}},{"line":131,"address":[],"length":0,"stats":{"Line":45}},{"line":136,"address":[],"length":0,"stats":{"Line":6}},{"line":142,"address":[],"length":0,"stats":{"Line":22}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":145,"address":[],"length":0,"stats":{"Line":12}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":148,"address":[],"length":0,"stats":{"Line":12}},{"line":149,"address":[],"length":0,"stats":{"Line":10}},{"line":150,"address":[],"length":0,"stats":{"Line":18}},{"line":151,"address":[],"length":0,"stats":{"Line":12}},{"line":152,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":20}},{"line":157,"address":[],"length":0,"stats":{"Line":9}},{"line":158,"address":[],"length":0,"stats":{"Line":9}},{"line":160,"address":[],"length":0,"stats":{"Line":9}},{"line":161,"address":[],"length":0,"stats":{"Line":3}},{"line":162,"address":[],"length":0,"stats":{"Line":3}},{"line":163,"address":[],"length":0,"stats":{"Line":3}},{"line":164,"address":[],"length":0,"stats":{"Line":6}},{"line":165,"address":[],"length":0,"stats":{"Line":3}},{"line":167,"address":[],"length":0,"stats":{"Line":9}},{"line":168,"address":[],"length":0,"stats":{"Line":9}},{"line":169,"address":[],"length":0,"stats":{"Line":6}},{"line":170,"address":[],"length":0,"stats":{"Line":12}},{"line":171,"address":[],"length":0,"stats":{"Line":9}},{"line":172,"address":[],"length":0,"stats":{"Line":12}},{"line":177,"address":[],"length":0,"stats":{"Line":16}},{"line":178,"address":[],"length":0,"stats":{"Line":48}},{"line":179,"address":[],"length":0,"stats":{"Line":16}},{"line":180,"address":[],"length":0,"stats":{"Line":28}},{"line":181,"address":[],"length":0,"stats":{"Line":2}},{"line":187,"address":[],"length":0,"stats":{"Line":3}},{"line":188,"address":[],"length":0,"stats":{"Line":5}},{"line":189,"address":[],"length":0,"stats":{"Line":1}},{"line":191,"address":[],"length":0,"stats":{"Line":2}},{"line":192,"address":[],"length":0,"stats":{"Line":2}},{"line":193,"address":[],"length":0,"stats":{"Line":1}},{"line":199,"address":[],"length":0,"stats":{"Line":4}},{"line":200,"address":[],"length":0,"stats":{"Line":7}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":203,"address":[],"length":0,"stats":{"Line":3}},{"line":208,"address":[],"length":0,"stats":{"Line":3}},{"line":209,"address":[],"length":0,"stats":{"Line":6}},{"line":210,"address":[],"length":0,"stats":{"Line":0}},{"line":212,"address":[],"length":0,"stats":{"Line":3}},{"line":213,"address":[],"length":0,"stats":{"Line":1}},{"line":215,"address":[],"length":0,"stats":{"Line":4}},{"line":216,"address":[],"length":0,"stats":{"Line":2}},{"line":217,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":3}},{"line":224,"address":[],"length":0,"stats":{"Line":6}},{"line":225,"address":[],"length":0,"stats":{"Line":0}},{"line":227,"address":[],"length":0,"stats":{"Line":3}},{"line":232,"address":[],"length":0,"stats":{"Line":3}},{"line":238,"address":[],"length":0,"stats":{"Line":6}},{"line":239,"address":[],"length":0,"stats":{"Line":0}},{"line":241,"address":[],"length":0,"stats":{"Line":6}},{"line":242,"address":[],"length":0,"stats":{"Line":9}},{"line":243,"address":[],"length":0,"stats":{"Line":6}},{"line":244,"address":[],"length":0,"stats":{"Line":1}},{"line":246,"address":[],"length":0,"stats":{"Line":10}},{"line":254,"address":[],"length":0,"stats":{"Line":20}},{"line":255,"address":[],"length":0,"stats":{"Line":40}},{"line":256,"address":[],"length":0,"stats":{"Line":0}},{"line":258,"address":[],"length":0,"stats":{"Line":60}},{"line":259,"address":[],"length":0,"stats":{"Line":40}},{"line":261,"address":[],"length":0,"stats":{"Line":80}},{"line":262,"address":[],"length":0,"stats":{"Line":100}},{"line":263,"address":[],"length":0,"stats":{"Line":60}},{"line":264,"address":[],"length":0,"stats":{"Line":60}},{"line":265,"address":[],"length":0,"stats":{"Line":60}},{"line":266,"address":[],"length":0,"stats":{"Line":40}},{"line":267,"address":[],"length":0,"stats":{"Line":80}},{"line":268,"address":[],"length":0,"stats":{"Line":60}},{"line":269,"address":[],"length":0,"stats":{"Line":80}},{"line":274,"address":[],"length":0,"stats":{"Line":20}},{"line":275,"address":[],"length":0,"stats":{"Line":40}},{"line":283,"address":[],"length":0,"stats":{"Line":15}},{"line":284,"address":[],"length":0,"stats":{"Line":58}},{"line":285,"address":[],"length":0,"stats":{"Line":2}},{"line":287,"address":[],"length":0,"stats":{"Line":39}},{"line":288,"address":[],"length":0,"stats":{"Line":26}},{"line":290,"address":[],"length":0,"stats":{"Line":52}},{"line":294,"address":[],"length":0,"stats":{"Line":39}},{"line":295,"address":[],"length":0,"stats":{"Line":39}},{"line":296,"address":[],"length":0,"stats":{"Line":52}},{"line":297,"address":[],"length":0,"stats":{"Line":39}},{"line":298,"address":[],"length":0,"stats":{"Line":39}},{"line":303,"address":[],"length":0,"stats":{"Line":7}},{"line":304,"address":[],"length":0,"stats":{"Line":11}},{"line":305,"address":[],"length":0,"stats":{"Line":6}},{"line":306,"address":[],"length":0,"stats":{"Line":3}},{"line":308,"address":[],"length":0,"stats":{"Line":8}},{"line":309,"address":[],"length":0,"stats":{"Line":10}},{"line":310,"address":[],"length":0,"stats":{"Line":16}},{"line":311,"address":[],"length":0,"stats":{"Line":8}},{"line":317,"address":[],"length":0,"stats":{"Line":3}},{"line":318,"address":[],"length":0,"stats":{"Line":10}},{"line":319,"address":[],"length":0,"stats":{"Line":2}},{"line":321,"address":[],"length":0,"stats":{"Line":5}},{"line":322,"address":[],"length":0,"stats":{"Line":3}},{"line":323,"address":[],"length":0,"stats":{"Line":3}},{"line":324,"address":[],"length":0,"stats":{"Line":3}},{"line":325,"address":[],"length":0,"stats":{"Line":4}},{"line":326,"address":[],"length":0,"stats":{"Line":3}},{"line":327,"address":[],"length":0,"stats":{"Line":3}},{"line":332,"address":[],"length":0,"stats":{"Line":10}},{"line":337,"address":[],"length":0,"stats":{"Line":38}},{"line":338,"address":[],"length":0,"stats":{"Line":2}},{"line":340,"address":[],"length":0,"stats":{"Line":24}},{"line":341,"address":[],"length":0,"stats":{"Line":40}},{"line":342,"address":[],"length":0,"stats":{"Line":24}},{"line":343,"address":[],"length":0,"stats":{"Line":24}},{"line":344,"address":[],"length":0,"stats":{"Line":24}},{"line":345,"address":[],"length":0,"stats":{"Line":32}},{"line":346,"address":[],"length":0,"stats":{"Line":24}},{"line":347,"address":[],"length":0,"stats":{"Line":24}},{"line":352,"address":[],"length":0,"stats":{"Line":2}},{"line":356,"address":[],"length":0,"stats":{"Line":8}},{"line":357,"address":[],"length":0,"stats":{"Line":3}},{"line":358,"address":[],"length":0,"stats":{"Line":2}},{"line":359,"address":[],"length":0,"stats":{"Line":1}},{"line":361,"address":[],"length":0,"stats":{"Line":2}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":363,"address":[],"length":0,"stats":{"Line":3}},{"line":369,"address":[],"length":0,"stats":{"Line":8}},{"line":371,"address":[],"length":0,"stats":{"Line":24}},{"line":372,"address":[],"length":0,"stats":{"Line":16}},{"line":373,"address":[],"length":0,"stats":{"Line":16}},{"line":374,"address":[],"length":0,"stats":{"Line":16}},{"line":375,"address":[],"length":0,"stats":{"Line":16}},{"line":376,"address":[],"length":0,"stats":{"Line":16}},{"line":377,"address":[],"length":0,"stats":{"Line":16}},{"line":378,"address":[],"length":0,"stats":{"Line":16}},{"line":379,"address":[],"length":0,"stats":{"Line":16}},{"line":380,"address":[],"length":0,"stats":{"Line":16}},{"line":381,"address":[],"length":0,"stats":{"Line":16}},{"line":382,"address":[],"length":0,"stats":{"Line":16}},{"line":383,"address":[],"length":0,"stats":{"Line":8}},{"line":384,"address":[],"length":0,"stats":{"Line":8}},{"line":389,"address":[],"length":0,"stats":{"Line":8}},{"line":390,"address":[],"length":0,"stats":{"Line":366}},{"line":391,"address":[],"length":0,"stats":{"Line":32}},{"line":392,"address":[],"length":0,"stats":{"Line":24}}],"covered":173,"coverable":180},{"path":["/","Users","sunyu","Projects","oak","src","task","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//! `oaktask_task_*` export symbols mirroring `include/task/task.h`.\n//!\n//! Full symbol inventory (header-authoritative):\n//! - `oaktask_task_free(OakTaskTask *t) -\u003e void`\n//! - `oaktask_task_start_sync(OakTaskTask t) -\u003e int` (1 = succeeded)\n//! - `oaktask_task_start(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_cancel(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_wait(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_is_finished(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_succeeded(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_title(OakTaskTask t, char *buf, int buf_size) -\u003e int` (two-stage)\n//! - `oaktask_task_error(OakTaskTask t, char *buf, int buf_size) -\u003e int` (two-stage)\n//! - `oaktask_task_subscribe(OakTaskTask t, oaktask_event_fn fn, void *userdata) -\u003e int64_t`\n//! - `oaktask_debug_alive_count(void) -\u003e int`\n\nuse std::ffi::{c_char, c_int, c_void};\nuse std::sync::Arc;\n\nuse crate::error::OAKTASK_E_INVALID;\nuse crate::ffi::taskhandle::{copy_string, get_task, get_task_mut, userdata_usize};\nuse crate::handle::CHandle;\nuse crate::manager::TaskManager;\nuse crate::task::{SubscriberState, TaskEvent};\n\n/// `oaktask_event_fn` callback (`include/task/task.h`).\npub type OakTaskEventFn = unsafe extern \"C\" fn(event_id: c_int, value: f64, userdata: *mut c_void);\n\n/// Event ids (`include/task/task.h`).\nconst OAKTASK_EVENT_STARTED: c_int = 0;\nconst OAKTASK_EVENT_PROGRESS: c_int = 1;\nconst OAKTASK_EVENT_FINISHED: c_int = 2;\n\n/// `oaktask_task_free` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_free(t: *mut CHandle) {\n\tif t.is_null() {\n\t\treturn;\n\t}\n\tlet handle = unsafe { \u0026mut *t };\n\tif handle.ctx.is_null() {\n\t\treturn;\n\t}\n\t// Release one reference, then clear ctx (NULL / empty-handle no-op).\n\tif let Some(release) = handle.release {\n\t\tunsafe {\n\t\t\trelease(handle.ctx);\n\t\t}\n\t}\n\thandle.ctx = std::ptr::null_mut();\n}\n\n/// `oaktask_task_start_sync` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_start_sync(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tlet result = unsafe { (\u0026mut *h.task).start() };\n\tif result.is_ok() {\n\t\t1\n\t} else {\n\t\t0\n\t}\n}\n\n/// `oaktask_task_start` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_start(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif !TaskManager::instance().is_some() {\n\t\treturn crate::error::OAKTASK_E_STATE;\n\t}\n\tif h.running_on_manager {\n\t\treturn crate::error::OAKTASK_E_STATE;\n\t}\n\tlet h = match get_task_mut(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif h.owner.is_none() {\n\t\treturn crate::error::OAKTASK_E_STATE;\n\t}\n\n\t// Transfer ownership to the manager; releasing the handle afterwards\n\t// only frees the box. The manager was checked above and cannot vanish\n\t// between the check and the transfer in a single-threaded caller.\n\tlet boxed = h.owner.take().unwrap();\n\tlet task_ptr = h.task;\n\tTaskManager::with_manager_mut(|m| m.add_task(boxed));\n\t// The box moved into the manager; the raw pointer is unchanged.\n\tlet h = get_task_mut(\u0026t).unwrap();\n\th.running_on_manager = true;\n\th.owned = false;\n\tlet _ = task_ptr;\n\tcrate::error::OAKTASK_OK\n}\n\n/// `oaktask_task_cancel` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_cancel(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tunsafe {\n\t\t(\u0026mut *h.task).cancel();\n\t}\n\tcrate::error::OAKTASK_OK\n}\n\n/// `oaktask_task_wait` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_wait(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif h.running_on_manager {\n\t\tlet task_ptr = h.task;\n\t\t// C++ semantics: wait cancels the task and blocks until it\n\t\t// finishes. The join happens without holding the manager lock.\n\t\tTaskManager::with_manager_mut(|m| m.cancel_task_by_ptr(task_ptr));\n\t\tlet handle = TaskManager::with_manager_mut(|m| m.take_thread_by_ptr(task_ptr)).flatten();\n\t\tif let Some(handle) = handle {\n\t\t\tlet _ = handle.join();\n\t\t}\n\t}\n\tcrate::error::OAKTASK_OK\n}\n\n/// `oaktask_task_is_finished` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_is_finished(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif unsafe { (*h.task).is_finished() } {\n\t\t1\n\t} else {\n\t\t0\n\t}\n}\n\n/// `oaktask_task_succeeded` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_succeeded(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif unsafe { (*h.task).succeeded() } {\n\t\t1\n\t} else {\n\t\t0\n\t}\n}\n\n/// `oaktask_task_title` (`include/task/task.h`, two-stage string getter).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tcopy_string(unsafe { (*h.task).title() }, buf, buf_size)\n}\n\n/// `oaktask_task_error` (`include/task/task.h`, two-stage string getter).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tlet value = unsafe { (*h.task).error() }.unwrap_or(\"Unknown error\");\n\tcopy_string(value, buf, buf_size)\n}\n\n/// `oaktask_task_subscribe` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_subscribe(t: CHandle, cb: Option\u003cOakTaskEventFn\u003e, userdata: *mut c_void) -\u003e i64 {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID as i64,\n\t};\n\tlet Some(cb) = cb else {\n\t\treturn OAKTASK_E_INVALID as i64;\n\t};\n\n\tlet state = Arc::new(SubscriberState::default());\n\tlet ud = userdata_usize(userdata);\n\n\t// One subscription replaces the previous one (the C++ `listeners_`\n\t// vector holds at most the factory listener in practice; here the\n\t// factory listener is gone — the task tracks finished internally).\n\tunsafe {\n\t\t(*h.task).set_subscriber(state.clone());\n\t\t(*h.task).set_event_listener(Box::new(move |ev: TaskEvent| {\n\t\t\tlet (event_id, value) = match ev {\n\t\t\t\tTaskEvent::Started =\u003e (OAKTASK_EVENT_STARTED, state.start_ms.load(std::sync::atomic::Ordering::SeqCst) as f64),\n\t\t\t\tTaskEvent::Progress(p) =\u003e (OAKTASK_EVENT_PROGRESS, p),\n\t\t\t\tTaskEvent::Finished =\u003e (OAKTASK_EVENT_FINISHED, state.finished_value.load(std::sync::atomic::Ordering::SeqCst) as f64),\n\t\t\t};\n\t\t\tcb(event_id, value, ud as *mut c_void);\n\t\t}));\n\t}\n\t0\n}\n\n/// `oaktask_debug_alive_count` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_debug_alive_count() -\u003e c_int {\n\tcrate::ffi::taskhandle::alive_count()\n}\n","traces":[{"line":51,"address":[],"length":0,"stats":{"Line":65}},{"line":52,"address":[],"length":0,"stats":{"Line":130}},{"line":53,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":128}},{"line":56,"address":[],"length":0,"stats":{"Line":128}},{"line":57,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":126}},{"line":62,"address":[],"length":0,"stats":{"Line":63}},{"line":65,"address":[],"length":0,"stats":{"Line":63}},{"line":70,"address":[],"length":0,"stats":{"Line":52}},{"line":71,"address":[],"length":0,"stats":{"Line":103}},{"line":72,"address":[],"length":0,"stats":{"Line":102}},{"line":73,"address":[],"length":0,"stats":{"Line":1}},{"line":75,"address":[],"length":0,"stats":{"Line":153}},{"line":76,"address":[],"length":0,"stats":{"Line":102}},{"line":77,"address":[],"length":0,"stats":{"Line":33}},{"line":79,"address":[],"length":0,"stats":{"Line":18}},{"line":85,"address":[],"length":0,"stats":{"Line":7}},{"line":86,"address":[],"length":0,"stats":{"Line":14}},{"line":87,"address":[],"length":0,"stats":{"Line":14}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":90,"address":[],"length":0,"stats":{"Line":7}},{"line":91,"address":[],"length":0,"stats":{"Line":1}},{"line":93,"address":[],"length":0,"stats":{"Line":6}},{"line":94,"address":[],"length":0,"stats":{"Line":1}},{"line":96,"address":[],"length":0,"stats":{"Line":10}},{"line":97,"address":[],"length":0,"stats":{"Line":10}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":10}},{"line":101,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":20}},{"line":108,"address":[],"length":0,"stats":{"Line":10}},{"line":109,"address":[],"length":0,"stats":{"Line":20}},{"line":111,"address":[],"length":0,"stats":{"Line":20}},{"line":112,"address":[],"length":0,"stats":{"Line":5}},{"line":113,"address":[],"length":0,"stats":{"Line":5}},{"line":114,"address":[],"length":0,"stats":{"Line":5}},{"line":115,"address":[],"length":0,"stats":{"Line":5}},{"line":120,"address":[],"length":0,"stats":{"Line":2}},{"line":121,"address":[],"length":0,"stats":{"Line":3}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":1}},{"line":128,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":6}},{"line":134,"address":[],"length":0,"stats":{"Line":11}},{"line":135,"address":[],"length":0,"stats":{"Line":10}},{"line":136,"address":[],"length":0,"stats":{"Line":1}},{"line":138,"address":[],"length":0,"stats":{"Line":5}},{"line":139,"address":[],"length":0,"stats":{"Line":10}},{"line":142,"address":[],"length":0,"stats":{"Line":20}},{"line":143,"address":[],"length":0,"stats":{"Line":30}},{"line":144,"address":[],"length":0,"stats":{"Line":15}},{"line":145,"address":[],"length":0,"stats":{"Line":5}},{"line":148,"address":[],"length":0,"stats":{"Line":5}},{"line":153,"address":[],"length":0,"stats":{"Line":11}},{"line":154,"address":[],"length":0,"stats":{"Line":21}},{"line":155,"address":[],"length":0,"stats":{"Line":20}},{"line":156,"address":[],"length":0,"stats":{"Line":1}},{"line":158,"address":[],"length":0,"stats":{"Line":20}},{"line":159,"address":[],"length":0,"stats":{"Line":9}},{"line":161,"address":[],"length":0,"stats":{"Line":1}},{"line":167,"address":[],"length":0,"stats":{"Line":3}},{"line":168,"address":[],"length":0,"stats":{"Line":5}},{"line":169,"address":[],"length":0,"stats":{"Line":4}},{"line":170,"address":[],"length":0,"stats":{"Line":1}},{"line":172,"address":[],"length":0,"stats":{"Line":4}},{"line":173,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":181,"address":[],"length":0,"stats":{"Line":8}},{"line":182,"address":[],"length":0,"stats":{"Line":15}},{"line":183,"address":[],"length":0,"stats":{"Line":14}},{"line":184,"address":[],"length":0,"stats":{"Line":1}},{"line":186,"address":[],"length":0,"stats":{"Line":28}},{"line":191,"address":[],"length":0,"stats":{"Line":31}},{"line":192,"address":[],"length":0,"stats":{"Line":61}},{"line":193,"address":[],"length":0,"stats":{"Line":60}},{"line":194,"address":[],"length":0,"stats":{"Line":1}},{"line":196,"address":[],"length":0,"stats":{"Line":150}},{"line":197,"address":[],"length":0,"stats":{"Line":120}},{"line":202,"address":[],"length":0,"stats":{"Line":3}},{"line":203,"address":[],"length":0,"stats":{"Line":5}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":1}},{"line":207,"address":[],"length":0,"stats":{"Line":3}},{"line":208,"address":[],"length":0,"stats":{"Line":1}},{"line":211,"address":[],"length":0,"stats":{"Line":3}},{"line":212,"address":[],"length":0,"stats":{"Line":3}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":219,"address":[],"length":0,"stats":{"Line":5}},{"line":220,"address":[],"length":0,"stats":{"Line":6}},{"line":221,"address":[],"length":0,"stats":{"Line":2}},{"line":222,"address":[],"length":0,"stats":{"Line":0}},{"line":223,"address":[],"length":0,"stats":{"Line":2}},{"line":225,"address":[],"length":0,"stats":{"Line":6}},{"line":228,"address":[],"length":0,"stats":{"Line":1}},{"line":233,"address":[],"length":0,"stats":{"Line":50}},{"line":234,"address":[],"length":0,"stats":{"Line":50}}],"covered":94,"coverable":98},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi","taskhandle.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//! The boxed control block behind every `OakTaskTask` handle, mirroring\n//! `src/task/c_api/taskhandle.h` (`oaktask_capi::TaskHandle`).\n//!\n//! `owns_task` is the owns role: true for factory-created tasks (the last\n//! release drops the task) and false once the task runs on the manager\n//! (`oaktask_task_start()` flips it; the manager drops the task) or for\n//! borrowed wrappers (`oaktask_manager_at()`): releasing those only\n//! destroys the box.\n//!\n//! The C++ `TaskHandle` carries its own `finished`/`succeeded` atomics that\n//! the factory listener updates. The Rust [`crate::task::Task`] tracks these\n//! internally (race-free through its `done` condvar), so the box reads them\n//! from the task instead — the box only needs the downcast pointers for the\n//! `take_*` accessors (Rust has no `dynamic_cast`).\n//!\n//! CPP-PARITY: src/task/c_api/taskhandle.h\n\nuse std::ffi::c_void;\nuse std::sync::atomic::{AtomicI32, Ordering};\n\nuse crate::handle::{make_owned, CHandle};\nuse crate::project::import::ProjectImportTask;\nuse crate::project::load::ProjectLoadBaseTask;\nuse crate::task::Task;\n\n/// Concrete-task downcast, standing in for the C++ `dynamic_cast` in the\n/// `take_*` accessors.\npub enum TaskImpl {\n\t/// Not a project load/import task.\n\tNone,\n\t/// A `ProjectLoadBaseTask` (native or OTIO loader).\n\tLoadBase(*mut ProjectLoadBaseTask),\n\t/// A `ProjectImportTask`.\n\tImport(*mut ProjectImportTask),\n}\n\n/// Control block behind an `OakTaskTask` (see the module docs).\npub struct TaskHandleBox {\n\t/// The task the handle wraps (the outer task owning the behavior).\n\tpub task: *mut Task,\n\t/// Whether releasing the last reference drops the task.\n\tpub owned: bool,\n\t/// Whether the task has been handed to the manager.\n\tpub running_on_manager: bool,\n\t/// The owned task (factory case); taken by `oaktask_task_start`.\n\tpub owner: Option\u003cBox\u003cTask\u003e\u003e,\n\t/// Downcast target for the `take_*` accessors.\n\tpub impl_kind: TaskImpl,\n}\n\nimpl Drop for TaskHandleBox {\n\tfn drop(\u0026mut self) {\n\t\t// Mirrors `task_release`: the box is destroyed at zero refs, which\n\t\t// always decrements the alive count.\n\t\tALIVE.fetch_sub(1, Ordering::SeqCst);\n\t}\n}\n\n// Safety: the box is only ever accessed through the C ABI entry points\n// (never concurrently with its own mutation); the raw pointers inside are\n// stable while the box lives.\nunsafe impl Send for TaskHandleBox {}\n\n/// Alive-count for leak assertions in tests (mirrors\n/// `oaktask_capi::alive()`).\nstatic ALIVE: AtomicI32 = AtomicI32::new(0);\n\n/// Current alive handle count.\npub fn alive_count() -\u003e i32 {\n\tALIVE.load(Ordering::SeqCst)\n}\n\n/// Wrap an owned task (reference count 1). The handle owns the task and\n/// drops it on the final release.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap)\npub fn wrap_owned(task: Box\u003cTask\u003e) -\u003e CHandle {\n\twrap_owned_with_impl(task, TaskImpl::None)\n}\n\n/// Wrap an owned task with a downcast target for the `take_*` accessors.\npub fn wrap_owned_with_impl(task: Box\u003cTask\u003e, impl_kind: TaskImpl) -\u003e CHandle {\n\tlet ptr = Box::into_raw(task);\n\tALIVE.fetch_add(1, Ordering::SeqCst);\n\t// Safety: `ptr` is a live, uniquely owned `Box\u003cTask\u003e`.\n\tlet handle = make_owned(TaskHandleBox {\n\t\ttask: ptr,\n\t\towned: true,\n\t\trunning_on_manager: false,\n\t\towner: unsafe { Some(Box::from_raw(ptr)) },\n\t\timpl_kind,\n\t});\n\thandle\n}\n\n/// Wrap a manager-owned (borrowed) task. Releasing the handle does NOT drop\n/// the task.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed)\npub fn wrap_borrowed(ptr: *mut Task) -\u003e CHandle {\n\tif ptr.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tALIVE.fetch_add(1, Ordering::SeqCst);\n\tmake_owned(TaskHandleBox {\n\t\ttask: ptr,\n\t\towned: false,\n\t\trunning_on_manager: true,\n\t\towner: None,\n\t\timpl_kind: TaskImpl::None,\n\t})\n}\n\n/// Typed view of a task handle box; `None` for empty handles.\n///\n/// # Safety\n/// The handle must have been created by this module.\npub fn get_task(h: \u0026CHandle) -\u003e Option\u003c\u0026TaskHandleBox\u003e {\n\tunsafe { crate::handle::get::\u003cTaskHandleBox\u003e(h) }\n}\n\n/// Typed mutable view of a task handle box; `None` for empty handles.\n///\n/// # Safety\n/// The handle must have been created by this module and not be shared with\n/// a concurrent mutable access.\npub fn get_task_mut(h: \u0026CHandle) -\u003e Option\u003c\u0026mut TaskHandleBox\u003e {\n\tunsafe { crate::handle::get_mut::\u003cTaskHandleBox\u003e(h) }\n}\n\n/// Two-stage string copy matching `oaktask_capi::copy_string`: returns the\n/// needed size (`len + 1`); writes only when the buffer is non-null and\n/// large enough.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (copy_string)\npub fn copy_string(value: \u0026str, buf: *mut std::ffi::c_char, buf_size: i32) -\u003e i32 {\n\tlet needed = value.len() as i32 + 1;\n\tif !buf.is_null() \u0026\u0026 buf_size \u003e= needed {\n\t\tunsafe {\n\t\t\tstd::ptr::copy_nonoverlapping(value.as_ptr() as *const std::ffi::c_char, buf, value.len());\n\t\t\t*buf.add(value.len()) = 0;\n\t\t}\n\t}\n\tneeded\n}\n\n/// Read a NUL-terminated C string into a Rust `String` (lossy); empty when\n/// the pointer is null.\n///\n/// # Safety\n/// `ptr` must be a valid NUL-terminated C string or null.\npub unsafe fn cstr_to_string(ptr: *const std::ffi::c_char) -\u003e String {\n\tif ptr.is_null() {\n\t\treturn String::new();\n\t}\n\tunsafe { std::ffi::CStr::from_ptr(ptr) }.to_string_lossy().into_owned()\n}\n\n/// Build a NUL-terminated C string for the duration of the call (leaked).\npub fn cstr(s: \u0026str) -\u003e *const std::ffi::c_char {\n\tlet mut bytes = s.as_bytes().to_vec();\n\tbytes.push(0);\n\tbytes.leak().as_ptr() as *const std::ffi::c_char\n}\n\n/// `userdata` payload converted to a `Send`-friendly form for closures.\npub fn userdata_usize(userdata: *mut c_void) -\u003e usize {\n\tuserdata as usize\n}\n","traces":[{"line":68,"address":[],"length":0,"stats":{"Line":63}},{"line":71,"address":[],"length":0,"stats":{"Line":189}},{"line":85,"address":[],"length":0,"stats":{"Line":50}},{"line":86,"address":[],"length":0,"stats":{"Line":150}},{"line":93,"address":[],"length":0,"stats":{"Line":37}},{"line":94,"address":[],"length":0,"stats":{"Line":111}},{"line":98,"address":[],"length":0,"stats":{"Line":62}},{"line":99,"address":[],"length":0,"stats":{"Line":186}},{"line":100,"address":[],"length":0,"stats":{"Line":186}},{"line":102,"address":[],"length":0,"stats":{"Line":186}},{"line":103,"address":[],"length":0,"stats":{"Line":124}},{"line":104,"address":[],"length":0,"stats":{"Line":62}},{"line":105,"address":[],"length":0,"stats":{"Line":62}},{"line":106,"address":[],"length":0,"stats":{"Line":62}},{"line":107,"address":[],"length":0,"stats":{"Line":62}},{"line":109,"address":[],"length":0,"stats":{"Line":62}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":3}},{"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":126,"address":[],"length":0,"stats":{"Line":1}},{"line":134,"address":[],"length":0,"stats":{"Line":162}},{"line":135,"address":[],"length":0,"stats":{"Line":324}},{"line":143,"address":[],"length":0,"stats":{"Line":10}},{"line":144,"address":[],"length":0,"stats":{"Line":20}},{"line":152,"address":[],"length":0,"stats":{"Line":39}},{"line":153,"address":[],"length":0,"stats":{"Line":78}},{"line":154,"address":[],"length":0,"stats":{"Line":58}},{"line":156,"address":[],"length":0,"stats":{"Line":108}},{"line":157,"address":[],"length":0,"stats":{"Line":54}},{"line":160,"address":[],"length":0,"stats":{"Line":39}},{"line":168,"address":[],"length":0,"stats":{"Line":71}},{"line":169,"address":[],"length":0,"stats":{"Line":142}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":213}},{"line":176,"address":[],"length":0,"stats":{"Line":145}},{"line":177,"address":[],"length":0,"stats":{"Line":435}},{"line":178,"address":[],"length":0,"stats":{"Line":290}},{"line":179,"address":[],"length":0,"stats":{"Line":145}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":1}}],"covered":44,"coverable":46},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi.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//! oaktask C ABI export surface. These `#[no_mangle] extern \"C\"` symbols are\n//! the Rust counterpart of the C++ task facade declared in\n//! `include/task/*.h`; each module mirrors one header. The public headers are\n//! authoritative for signatures; the inventory comments below each module\n//! enumerate the full symbol set. Every export passes through\n//! [`crate::handle::guard*`] / explicit null checks so panics never cross the\n//! FFI boundary.\n\npub mod manager;\npub mod project;\npub mod task;\npub(crate) mod taskhandle;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","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 (the\n//! function pointers in a handle always point into the DLL that created the\n//! object).\n\nuse std::sync::atomic::AtomicU32;\n\n/// ABI version stamped into every handle.\npub const OAKTASK_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#[repr(C)]\n#[derive(Clone, Copy, Debug, Default)]\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\n// Handles are shared between the calling thread and the manager worker\n// threads (same ownership model as a C++ shared_ptr). The raw pointer and\n// function-pointer fields are inherently `!Send`/`!Sync`; the box behind\n// `ctx` is either `Send + Sync` (task state, atomics) or a plain pointer\n// (borrowed handles), both of which are safe to share through the\n// refcounted handle.\nunsafe impl Send for CHandle {}\nunsafe impl Sync for CHandle {}\n\nunsafe extern \"C\" fn noop_addref(_ctx: *mut std::ffi::c_void) {}\n\nunsafe extern \"C\" fn noop_release(_ctx: *mut std::ffi::c_void) {}\n\nunsafe extern \"C\" fn owned_addref\u003cT: 'static\u003e(ctx: *mut std::ffi::c_void) {\n\tif !ctx.is_null() {\n\t\t// CPP-PARITY: src/task/c_api/taskhandle.h (task_addref)\n\t\tunsafe {\n\t\t\t(*(ctx as *const RefBox\u003cT\u003e)).refs.fetch_add(1, std::sync::atomic::Ordering::SeqCst);\n\t\t}\n\t}\n}\n\nunsafe extern \"C\" fn owned_release\u003cT: 'static\u003e(ctx: *mut std::ffi::c_void) {\n\tif ctx.is_null() {\n\t\treturn;\n\t}\n\t// CPP-PARITY: src/task/c_api/taskhandle.h (task_release)\n\tlet b = ctx as *const RefBox\u003cT\u003e;\n\tlet last = unsafe { (*b).refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) };\n\tif last == 1 {\n\t\tunsafe {\n\t\t\tdrop(Box::from_raw(ctx as *mut RefBox\u003cT\u003e));\n\t\t}\n\t}\n}\n\n/// Release function for borrowed handles: destroys only the box, never the\n/// pointee.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed)\nunsafe extern \"C\" fn borrowed_release\u003cT: 'static\u003e(ctx: *mut std::ffi::c_void) {\n\tif ctx.is_null() {\n\t\treturn;\n\t}\n\tlet b = ctx as *const RefBox\u003c*mut T\u003e;\n\tlet last = unsafe { (*b).refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) };\n\tif last == 1 {\n\t\tunsafe {\n\t\t\tdrop(Box::from_raw(ctx as *mut RefBox\u003c*mut T\u003e));\n\t\t}\n\t}\n}\n\nimpl CHandle {\n\t/// The empty handle. `ctx` is null but the fn pointers and ABI version\n\t/// are stamped so callers can unconditionally call `release`.\n\tpub fn null() -\u003e Self {\n\t\tCHandle {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: Some(noop_addref),\n\t\t\trelease: Some(noop_release),\n\t\t\tabi_version: OAKTASK_ABI_VERSION,\n\t\t}\n\t}\n\n\t/// Whether the handle is empty (`ctx == NULL`).\n\tpub fn is_null(\u0026self) -\u003e bool {\n\t\tself.ctx.is_null()\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 b = Box::new(RefBox { refs: AtomicU32::new(1), value });\n\tCHandle {\n\t\tctx: Box::into_raw(b) as *mut std::ffi::c_void,\n\t\taddref: Some(owned_addref::\u003cT\u003e),\n\t\trelease: Some(owned_release::\u003cT\u003e),\n\t\tabi_version: OAKTASK_ABI_VERSION,\n\t}\n}\n\n/// Borrowed handle for an object owned elsewhere (release frees only the\n/// box).\n///\n/// # Safety\n/// Caller guarantees `ptr` outlives every derived handle.\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\tlet b = Box::new(RefBox { refs: AtomicU32::new(1), value: ptr });\n\tCHandle {\n\t\tctx: Box::into_raw(b) as *mut std::ffi::c_void,\n\t\taddref: Some(owned_addref::\u003c*mut T\u003e),\n\t\trelease: Some(borrowed_release::\u003cT\u003e),\n\t\tabi_version: OAKTASK_ABI_VERSION,\n\t}\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.ctx.is_null() {\n\t\treturn None;\n\t}\n\tunsafe { Some(\u0026(*(h.ctx as *const RefBox\u003cT\u003e)).value) }\n}\n\n/// Typed mutable view into a handle; `None` for empty handles.\n///\n/// # Safety\n/// `T` must be the boxed type, and the handle must not be concurrently\n/// shared mutably.\npub unsafe fn get_mut\u003cT: 'static\u003e(h: \u0026CHandle) -\u003e Option\u003c\u0026mut T\u003e {\n\tif h.ctx.is_null() {\n\t\treturn None;\n\t}\n\tunsafe { Some(\u0026mut (*(h.ctx as *mut RefBox\u003cT\u003e)).value) }\n}\n\n/// Panic-catching FFI wrapper for i32-returning exports.\npub fn guard\u003cF: FnOnce() -\u003e crate::error::Result\u003c()\u003e\u003e(f: F) -\u003e i32 {\n\tmatch std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {\n\t\tOk(Ok(())) =\u003e crate::error::OAKTASK_OK,\n\t\tOk(Err(e)) =\u003e e.code(),\n\t\tErr(_) =\u003e crate::error::OAKTASK_E_FAILED,\n\t}\n}\n\n/// Panic-catching FFI wrapper for handle-returning exports.\npub fn guard_handle\u003cF: FnOnce() -\u003e crate::error::Result\u003cCHandle\u003e\u003e(f: F) -\u003e CHandle {\n\tmatch std::panic::catch_unwind(std::panic::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 _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));\n}\n","traces":[{"line":60,"address":[],"length":0,"stats":{"Line":2}},{"line":62,"address":[],"length":0,"stats":{"Line":4}},{"line":64,"address":[],"length":0,"stats":{"Line":1}},{"line":65,"address":[],"length":0,"stats":{"Line":1}},{"line":68,"address":[],"length":0,"stats":{"Line":2}},{"line":73,"address":[],"length":0,"stats":{"Line":65}},{"line":74,"address":[],"length":0,"stats":{"Line":130}},{"line":75,"address":[],"length":0,"stats":{"Line":0}},{"line":78,"address":[],"length":0,"stats":{"Line":130}},{"line":79,"address":[],"length":0,"stats":{"Line":260}},{"line":80,"address":[],"length":0,"stats":{"Line":65}},{"line":82,"address":[],"length":0,"stats":{"Line":128}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":587}},{"line":109,"address":[],"length":0,"stats":{"Line":1174}},{"line":110,"address":[],"length":0,"stats":{"Line":587}},{"line":111,"address":[],"length":0,"stats":{"Line":587}},{"line":117,"address":[],"length":0,"stats":{"Line":412}},{"line":118,"address":[],"length":0,"stats":{"Line":824}},{"line":123,"address":[],"length":0,"stats":{"Line":64}},{"line":124,"address":[],"length":0,"stats":{"Line":192}},{"line":126,"address":[],"length":0,"stats":{"Line":128}},{"line":127,"address":[],"length":0,"stats":{"Line":64}},{"line":128,"address":[],"length":0,"stats":{"Line":64}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":142,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":0}},{"line":145,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":155,"address":[],"length":0,"stats":{"Line":162}},{"line":156,"address":[],"length":0,"stats":{"Line":324}},{"line":157,"address":[],"length":0,"stats":{"Line":8}},{"line":159,"address":[],"length":0,"stats":{"Line":154}},{"line":167,"address":[],"length":0,"stats":{"Line":10}},{"line":168,"address":[],"length":0,"stats":{"Line":20}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":10}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":177,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"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":192,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":0}}],"covered":29,"coverable":56},{"path":["/","Users","sunyu","Projects","oak","src","task","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//! # oaktask — the task execution module (Rust)\n//!\n//! Reimplements the C++ task module behind its frozen C ABI\n//! (`include/task/*.h`). See README.md for the architectural mapping\n//! (inheritance → one module per class + trait objects, cancellation via\n//! the oakrender cancelatom C ABI, events as mutex-guarded callbacks).\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 state\n//! behind `Mutex`.\n\n#![deny(unsafe_op_in_unsafe_fn)]\n#![warn(missing_docs)]\n\npub mod bridge;\npub mod codecbridge;\npub mod conform;\npub mod customcache;\npub mod error;\npub mod export;\npub mod ffi;\npub mod handle;\npub mod manager;\npub mod precache;\npub mod project;\npub mod proxy;\npub mod render;\npub mod task;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","manager.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//! The `TaskManager` singleton, mirroring `src/task/src/taskmanager.h`.\n//!\n//! Holds the set of live [`crate::task::Task`] instances; starting a task\n//! hands its ownership to the manager, which also exposes the codec task\n//! submitter registration (see [`crate::codecbridge`]).\n//!\n//! CPP-PARITY: src/task/src/taskmanager.h\n//!\n//! ## Threading model\n//!\n//! [`TaskManager::add_task`] spawns a worker thread that runs the task's\n//! [`Task::start`]. The worker thread only touches the task (through a raw\n//! pointer) and never the manager, so the manager's own mutex is never held\n//! across a join in the C ABI paths (joins happen lock-free via\n//! [`TaskManager::drain_finished`] / [`TaskManager::take_thread_by_ptr`]).\n\nuse std::sync::Mutex;\n\nuse crate::error::{Error, Result};\nuse crate::handle::{make_borrowed, CHandle};\nuse crate::task::Task;\n\n/// Process-wide singleton manager. C++ is a lazy singleton; the Rust side\n/// keeps a `Mutex\u003cOption\u003c...\u003e\u003e` so init/shutdown is explicit and races are\n/// rejected with `OAKTASK_E_STATE` (matching `oaktask_manager_init`).\npub struct TaskManager {\n\t/// Live tasks, most-recently-added last (mirrors the C++ `task_list_`).\n\ttasks: Vec\u003cBox\u003cdyn TaskBox\u003e\u003e,\n\t/// Worker threads, parallel to `tasks` (moved out before joining).\n\tthreads: Vec\u003cOption\u003cstd::thread::JoinHandle\u003c()\u003e\u003e\u003e,\n\t/// Whether the codec task submitter is currently registered.\n\tcodec_submitter_registered: bool,\n}\n\n/// Boxable shim so tasks of heterogeneous concrete types can be stored in\n/// the manager's list.\npub trait TaskBox: Send {\n\t/// Downcast/erase operations the manager needs.\n\tfn task(\u0026self) -\u003e \u0026Task;\n}\n\nimpl TaskBox for Task {\n\tfn task(\u0026self) -\u003e \u0026Task {\n\t\tself\n\t}\n}\n\n/// The process-wide instance slot. A raw pointer (never freed while the\n/// slot is set) so [`TaskManager::instance`] can hand out a `\u0026'static`.\nstatic INSTANCE: Mutex\u003cOption\u003cManagerPtr\u003e\u003e = Mutex::new(None);\n\n/// `Send`/`Sync` wrapper for the manager pointer (the manager's state is\n/// guarded by the `INSTANCE` mutex; the pointer itself is plain).\nstruct ManagerPtr(*mut TaskManager);\n\n// Safety: all access to the pointee goes through the `INSTANCE` mutex.\nunsafe impl Send for ManagerPtr {}\nunsafe impl Sync for ManagerPtr {}\n\nimpl TaskManager {\n\t/// The process-wide instance; `None` until [`TaskManager::init`].\n\t///\n\t/// CPP-PARITY: src/task/src/taskmanager.h (instance)\n\tpub fn instance() -\u003e Option\u003c\u0026'static TaskManager\u003e {\n\t\tlet guard = INSTANCE.lock().unwrap();\n\t\tguard.as_ref().map(|p| unsafe { \u0026*p.0 })\n\t}\n\n\t/// Create the singleton and register the codec task submitter. Returns\n\t/// `Err(Error::State)` if already initialized (mirrors\n\t/// `oaktask_manager_init`).\n\tpub fn init() -\u003e Result\u003c()\u003e {\n\t\tlet mut guard = INSTANCE.lock().unwrap();\n\t\tif guard.is_some() {\n\t\t\treturn Err(Error::State);\n\t\t}\n\t\tlet mgr = Box::into_raw(Box::new(TaskManager {\n\t\t\ttasks: Vec::new(),\n\t\t\tthreads: Vec::new(),\n\t\t\tcodec_submitter_registered: false,\n\t\t}));\n\t\t*guard = Some(ManagerPtr(mgr));\n\t\tOk(())\n\t}\n\n\t/// Destroy the singleton. Idempotent.\n\tpub fn shutdown() {\n\t\tlet ptr = {\n\t\t\tlet mut guard = INSTANCE.lock().unwrap();\n\t\t\tguard.take()\n\t\t};\n\t\tif let Some(p) = ptr {\n\t\t\t// The Drop impl cancels and joins every worker thread.\n\t\t\tunsafe {\n\t\t\t\tdrop(Box::from_raw(p.0));\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Run `f` with the singleton (when initialized), without taking\n\t/// ownership. Used by the C ABI layer.\n\tpub fn with_manager\u003cR\u003e(f: impl FnOnce(\u0026TaskManager) -\u003e R) -\u003e Option\u003cR\u003e {\n\t\tlet guard = INSTANCE.lock().unwrap();\n\t\tguard.as_ref().map(|p| f(unsafe { \u0026*p.0 }))\n\t}\n\n\t/// Run `f` with the singleton mutably (when initialized). Used by the C\n\t/// ABI layer. The caller must not call [`TaskManager::with_manager`]\n\t/// from a worker callback while this is held (same rule as the C++\n\t/// `mutex_`).\n\tpub fn with_manager_mut\u003cR\u003e(f: impl FnOnce(\u0026mut TaskManager) -\u003e R) -\u003e Option\u003cR\u003e {\n\t\tlet mut guard = INSTANCE.lock().unwrap();\n\t\tguard.as_mut().map(|p| f(unsafe { \u0026mut *p.0 }))\n\t}\n\n\t/// Add a task, taking ownership (the manager now owns it). A worker\n\t/// thread is spawned that runs [`Task::start`] on the task.\n\t///\n\t/// The worker runs through a raw pointer while the box lives in\n\t/// `self.tasks`; the shared reference produced by `task()` ends before\n\t/// the pointer is used, so the cast is safe in this exclusively-owned\n\t/// setting.\n\t#[allow(invalid_reference_casting)]\n\tpub fn add_task(\u0026mut self, task: Box\u003cdyn TaskBox\u003e) {\n\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\tself.tasks.push(task);\n\t\t// The worker only touches the task through the raw pointer; it never\n\t\t// reaches the manager, so the box stays alive in `self.tasks`.\n\t\tlet ptr_usize = ptr as usize;\n\t\tlet handle = std::thread::spawn(move || {\n\t\t\tunsafe {\n\t\t\t\t// The manager tracks success/failure through the task's own\n\t\t\t\t// finished state; the Result is intentionally ignored.\n\t\t\t\tlet _ = (\u0026mut *(ptr_usize as *mut Task)).start();\n\t\t\t}\n\t\t});\n\t\tself.threads.push(Some(handle));\n\t}\n\n\t/// Cancel the task at `index`.\n\t#[allow(invalid_reference_casting)]\n\tpub fn cancel_task(\u0026mut self, index: usize) -\u003e Result\u003c()\u003e {\n\t\tlet task = self.tasks.get(index).ok_or(Error::NotFound)?;\n\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\tunsafe {\n\t\t\t(\u0026mut *ptr).cancel();\n\t\t}\n\t\tOk(())\n\t}\n\n\t/// Cancel the task at `index` and block until it finishes.\n\tpub fn cancel_task_and_wait(\u0026mut self, index: usize) -\u003e Result\u003c()\u003e {\n\t\tself.cancel_task(index)?;\n\t\tlet handle = self.threads.get_mut(index).and_then(|h| h.take());\n\t\tif let Some(h) = handle {\n\t\t\tlet _ = h.join();\n\t\t}\n\t\tOk(())\n\t}\n\n\t/// Remove finished tasks from the list.\n\tpub fn delete_finished(\u0026mut self) {\n\t\tfor (_task, handle) in self.drain_finished() {\n\t\t\tlet _ = handle.join();\n\t\t}\n\t}\n\n\t/// Number of live tasks.\n\tpub fn get_task_count(\u0026self) -\u003e usize {\n\t\tself.tasks.len()\n\t}\n\n\t/// Borrowed handle to the task at `index`; `Err(Error::NotFound)` if out\n\t/// of range.\n\tpub fn get_task_at(\u0026self, index: usize) -\u003e Result\u003cCHandle\u003e {\n\t\tlet task = self.tasks.get(index).ok_or(Error::NotFound)?;\n\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\tOk(unsafe { make_borrowed::\u003cTask\u003e(ptr) })\n\t}\n\n\t/// Raw pointer to the task at `index` (stable while the manager owns\n\t/// it). Used by `oaktask_manager_at` to build a borrowed task handle.\n\tpub fn task_ptr_at(\u0026self, index: usize) -\u003e Result\u003c*mut Task\u003e {\n\t\tlet task = self.tasks.get(index).ok_or(Error::NotFound)?;\n\t\tOk(task.task() as *const Task as *mut Task)\n\t}\n\n\t/// Index of the task with the given address.\n\tpub fn find_index(\u0026self, ptr: *const Task) -\u003e Option\u003cusize\u003e {\n\t\tself.tasks\n\t\t\t.iter()\n\t\t\t.position(|t| t.task() as *const Task == ptr)\n\t}\n\n\t/// Cancel the task with the given address (no-op when absent).\n\tpub fn cancel_task_by_ptr(\u0026mut self, ptr: *const Task) {\n\t\tif let Some(index) = self.find_index(ptr) {\n\t\t\tlet _ = self.cancel_task(index);\n\t\t}\n\t}\n\n\t/// Move the worker thread of the task with the given address out of the\n\t/// list so the caller can join it without holding the manager lock.\n\tpub fn take_thread_by_ptr(\u0026mut self, ptr: *const Task) -\u003e Option\u003cstd::thread::JoinHandle\u003c()\u003e\u003e {\n\t\tlet index = self.find_index(ptr)?;\n\t\tself.threads.get_mut(index).and_then(|h| h.take())\n\t}\n\n\t/// Remove every finished task (and its worker thread) from the list,\n\t/// returning the task boxes and thread handles so the caller can join\n\t/// them lock-free (the boxes must stay alive until the threads exit).\n\tpub fn drain_finished(\u0026mut self) -\u003e Vec\u003c(Box\u003cdyn TaskBox\u003e, std::thread::JoinHandle\u003c()\u003e)\u003e {\n\t\tlet mut out = Vec::new();\n\t\tlet mut i = 0;\n\t\twhile i \u003c self.tasks.len() {\n\t\t\tif self.tasks[i].task().is_finished() {\n\t\t\t\tlet task = self.tasks.remove(i);\n\t\t\t\tlet handle = self.threads.remove(i);\n\t\t\t\tif let Some(handle) = handle {\n\t\t\t\t\tout.push((task, handle));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t}\n\t\tout\n\t}\n\n\t/// Whether this manager registered the codec submitter itself.\n\tpub fn codec_submitter_registered(\u0026self) -\u003e bool {\n\t\tself.codec_submitter_registered\n\t}\n\n\t/// Mark whether this manager registered the codec submitter.\n\tpub fn set_codec_submitter_registered(\u0026mut self, registered: bool) {\n\t\tself.codec_submitter_registered = registered;\n\t}\n}\n\nimpl Drop for TaskManager {\n\tfn drop(\u0026mut self) {\n\t\t// Cancel every task, then join every worker (mirrors the C++\n\t\t// destructor: cancel all first, then join all).\n\t\t#[allow(invalid_reference_casting)]\n\t\tfor task in \u0026self.tasks {\n\t\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\t\tunsafe {\n\t\t\t\t(\u0026mut *ptr).cancel();\n\t\t\t}\n\t\t}\n\t\tfor handle in self.threads.iter_mut() {\n\t\t\tif let Some(h) = handle.take() {\n\t\t\t\tlet _ = h.join();\n\t\t\t}\n\t\t}\n\t}\n}\n","traces":[{"line":59,"address":[],"length":0,"stats":{"Line":28}},{"line":60,"address":[],"length":0,"stats":{"Line":28}},{"line":80,"address":[],"length":0,"stats":{"Line":7}},{"line":81,"address":[],"length":0,"stats":{"Line":28}},{"line":82,"address":[],"length":0,"stats":{"Line":26}},{"line":88,"address":[],"length":0,"stats":{"Line":8}},{"line":89,"address":[],"length":0,"stats":{"Line":32}},{"line":90,"address":[],"length":0,"stats":{"Line":8}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":32}},{"line":94,"address":[],"length":0,"stats":{"Line":8}},{"line":95,"address":[],"length":0,"stats":{"Line":8}},{"line":96,"address":[],"length":0,"stats":{"Line":8}},{"line":98,"address":[],"length":0,"stats":{"Line":8}},{"line":99,"address":[],"length":0,"stats":{"Line":8}},{"line":103,"address":[],"length":0,"stats":{"Line":34}},{"line":104,"address":[],"length":0,"stats":{"Line":34}},{"line":105,"address":[],"length":0,"stats":{"Line":136}},{"line":106,"address":[],"length":0,"stats":{"Line":34}},{"line":108,"address":[],"length":0,"stats":{"Line":42}},{"line":111,"address":[],"length":0,"stats":{"Line":16}},{"line":118,"address":[],"length":0,"stats":{"Line":15}},{"line":119,"address":[],"length":0,"stats":{"Line":60}},{"line":120,"address":[],"length":0,"stats":{"Line":46}},{"line":127,"address":[],"length":0,"stats":{"Line":63}},{"line":128,"address":[],"length":0,"stats":{"Line":252}},{"line":129,"address":[],"length":0,"stats":{"Line":196}},{"line":140,"address":[],"length":0,"stats":{"Line":5}},{"line":141,"address":[],"length":0,"stats":{"Line":10}},{"line":142,"address":[],"length":0,"stats":{"Line":15}},{"line":145,"address":[],"length":0,"stats":{"Line":10}},{"line":146,"address":[],"length":0,"stats":{"Line":15}},{"line":147,"address":[],"length":0,"stats":{"Line":5}},{"line":150,"address":[],"length":0,"stats":{"Line":5}},{"line":153,"address":[],"length":0,"stats":{"Line":15}},{"line":158,"address":[],"length":0,"stats":{"Line":5}},{"line":159,"address":[],"length":0,"stats":{"Line":25}},{"line":160,"address":[],"length":0,"stats":{"Line":10}},{"line":162,"address":[],"length":0,"stats":{"Line":5}},{"line":164,"address":[],"length":0,"stats":{"Line":5}},{"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":174,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":6}},{"line":186,"address":[],"length":0,"stats":{"Line":12}},{"line":191,"address":[],"length":0,"stats":{"Line":0}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":0}},{"line":194,"address":[],"length":0,"stats":{"Line":0}},{"line":199,"address":[],"length":0,"stats":{"Line":2}},{"line":200,"address":[],"length":0,"stats":{"Line":10}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":205,"address":[],"length":0,"stats":{"Line":10}},{"line":206,"address":[],"length":0,"stats":{"Line":10}},{"line":208,"address":[],"length":0,"stats":{"Line":34}},{"line":212,"address":[],"length":0,"stats":{"Line":5}},{"line":213,"address":[],"length":0,"stats":{"Line":20}},{"line":214,"address":[],"length":0,"stats":{"Line":10}},{"line":220,"address":[],"length":0,"stats":{"Line":5}},{"line":221,"address":[],"length":0,"stats":{"Line":20}},{"line":222,"address":[],"length":0,"stats":{"Line":25}},{"line":228,"address":[],"length":0,"stats":{"Line":4}},{"line":229,"address":[],"length":0,"stats":{"Line":8}},{"line":230,"address":[],"length":0,"stats":{"Line":8}},{"line":231,"address":[],"length":0,"stats":{"Line":18}},{"line":232,"address":[],"length":0,"stats":{"Line":10}},{"line":233,"address":[],"length":0,"stats":{"Line":20}},{"line":234,"address":[],"length":0,"stats":{"Line":20}},{"line":235,"address":[],"length":0,"stats":{"Line":5}},{"line":236,"address":[],"length":0,"stats":{"Line":0}},{"line":239,"address":[],"length":0,"stats":{"Line":0}},{"line":242,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":0}},{"line":247,"address":[],"length":0,"stats":{"Line":0}},{"line":251,"address":[],"length":0,"stats":{"Line":16}},{"line":252,"address":[],"length":0,"stats":{"Line":16}},{"line":257,"address":[],"length":0,"stats":{"Line":8}},{"line":261,"address":[],"length":0,"stats":{"Line":8}},{"line":262,"address":[],"length":0,"stats":{"Line":0}},{"line":264,"address":[],"length":0,"stats":{"Line":0}},{"line":267,"address":[],"length":0,"stats":{"Line":8}},{"line":268,"address":[],"length":0,"stats":{"Line":0}},{"line":269,"address":[],"length":0,"stats":{"Line":0}}],"covered":67,"coverable":89},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","precache.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//! `PreCacheTask`, mirroring `src/task/src/precache/precachetask.h`.\n//!\n//! Renders a footage node (or the whole sequence) through\n//! [`crate::render::RenderTask`] to fill the playback cache. Owns a deep copy\n//! of the project (`OakNodeProject`) and borrows the source footage\n//! (`OakNodeFootage`).\n//!\n//! **Simplifications over the C++**: the deep project copy / viewer wiring\n//! is not built (the render drives the given viewer directly) and the\n//! timeline work-area intersection is replaced by the full footage length.\n//!\n//! CPP-PARITY: src/task/src/precache/precachetask.h\n\nuse crate::bridge;\nuse crate::error::Result;\nuse crate::handle::CHandle;\nuse crate::render::{RenderTask, RenderTaskBehavior};\nuse crate::task::{Task, TaskBehavior};\nuse oakcore_rs::{Rational, TimeRange};\n\n/// A pre-cache task: renders frames and audio of a footage node into the\n/// playback cache without any output file.\npub struct PreCacheTask {\n\t/// The render base (itself a task).\n\tpub render: RenderTask,\n\t/// Owning deep copy of the project (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Borrowed source footage (borrowed `OakNodeFootage`).\n\tpub footage: CHandle,\n\t/// Frame index within the footage being cached.\n\tpub index: i32,\n\t/// The sequence node (borrowed `OakNodeSequence`).\n\tpub sequence: CHandle,\n}\n\nimpl PreCacheTask {\n\t/// Create a pre-cache task for the given footage at `index` inside\n\t/// `sequence`.\n\tpub fn new(footage: CHandle, index: i32, sequence: CHandle) -\u003e PreCacheTask {\n\t\t// Video params from the sequence (empty when unavailable).\n\t\tlet mut video_params = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_sequence_get_video_params(sequence, 0, \u0026mut video_params);\n\t\t}\n\n\t\tlet filename = footage_filename(footage);\n\t\tlet title = format!(\"Pre-caching {filename}:{index}\");\n\t\tlet base = Task::new(\u0026title, CHandle::null());\n\t\tlet render = RenderTask::new(base, video_params, CHandle::null(), sequence, Default::default(), None);\n\n\t\t// A scratch project for the render color manager (simplified).\n\t\tlet project = unsafe { bridge::node::oaknode_project_init() };\n\n\t\tPreCacheTask {\n\t\t\trender,\n\t\t\tproject,\n\t\t\tfootage,\n\t\t\tindex,\n\t\t\tsequence,\n\t\t}\n\t}\n}\n\nimpl TaskBehavior for PreCacheTask {\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Share the caller's cancellation atom with the inner render base.\n\t\tself.render.base.set_cancel_atom(task.get_cancel_atom());\n\n\t\t// The full footage length is the cache range (simplified: no\n\t\t// work-area intersection).\n\t\tlet mut len_num = 0i64;\n\t\tlet mut len_den = 1i64;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_footage_get_video_length(self.footage, \u0026mut len_num, \u0026mut len_den);\n\t\t}\n\t\tlet range = TimeRange::new(Rational::new(0, 1), Rational::new(len_num, len_den));\n\n\t\tlet mut color_manager = unsafe { bridge::node::oaknode_colormanager_init(self.project) };\n\t\tlet mut cache = bridge::render::OakRenderCache::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_get_video_frame_cache(self.sequence, \u0026mut cache);\n\t\t}\n\n\t\tself.render.set_render_inputs(color_manager, cache.to_chandle(), 0 /* k_online */, false, range);\n\n\t\t// Drive the render with `self` as the subclass behavior (the C++\n\t\t// virtual dispatch receiver); the render is temporarily moved out to\n\t\t// avoid a self-referential borrow and put back before the handles are\n\t\t// released below.\n\t\tlet mut render = std::mem::replace(\u0026mut self.render, crate::render::RenderTask::placeholder());\n\t\tlet result = render.render(task, self);\n\t\tself.render = render;\n\n\t\tif !cache.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cache_free(\u0026mut cache);\n\t\t\t}\n\t\t}\n\t\tif !color_manager.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_colormanager_free(\u0026mut color_manager);\n\t\t\t}\n\t\t}\n\n\t\tresult\n\t}\n}\n\nimpl RenderTaskBehavior for PreCacheTask {\n\tfn frame_downloaded(\u0026mut self, task: \u0026mut Task, frame: CHandle) -\u003e Result\u003c()\u003e {\n\t\t// Do nothing: pre-cache just fills the frame cache.\n\t\tlet _ = (task, frame);\n\t\tOk(())\n\t}\n\n\tfn audio_downloaded(\u0026mut self, task: \u0026mut Task, buffer: CHandle) -\u003e Result\u003c()\u003e {\n\t\t// Pre-cache doesn't cache any audio.\n\t\tlet _ = (task, buffer);\n\t\tOk(())\n\t}\n\n\tfn encode_subtitle(\u0026mut self, task: \u0026mut Task, text: \u0026str) -\u003e Result\u003c()\u003e {\n\t\tlet _ = (task, text);\n\t\tOk(())\n\t}\n}\n\n/// Two-stage read of the footage filename.\nfn footage_filename(footage: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);\n\t}\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n","traces":[{"line":55,"address":[],"length":0,"stats":{"Line":1}},{"line":57,"address":[],"length":0,"stats":{"Line":2}},{"line":59,"address":[],"length":0,"stats":{"Line":2}},{"line":62,"address":[],"length":0,"stats":{"Line":3}},{"line":63,"address":[],"length":0,"stats":{"Line":2}},{"line":64,"address":[],"length":0,"stats":{"Line":4}},{"line":65,"address":[],"length":0,"stats":{"Line":8}},{"line":68,"address":[],"length":0,"stats":{"Line":2}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":4}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":3}},{"line":92,"address":[],"length":0,"stats":{"Line":6}},{"line":94,"address":[],"length":0,"stats":{"Line":3}},{"line":95,"address":[],"length":0,"stats":{"Line":2}},{"line":97,"address":[],"length":0,"stats":{"Line":2}},{"line":100,"address":[],"length":0,"stats":{"Line":6}},{"line":106,"address":[],"length":0,"stats":{"Line":4}},{"line":107,"address":[],"length":0,"stats":{"Line":5}},{"line":108,"address":[],"length":0,"stats":{"Line":2}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":121,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":132,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":4}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":3}},{"line":152,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":7}},{"line":155,"address":[],"length":0,"stats":{"Line":4}}],"covered":33,"coverable":43},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","format.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//! Interchange-format dispatch for the OTIO tasks.\n//!\n//! `LoadOTIOTask` / `SaveOTIOTask` accept both OpenTimelineIO JSON\n//! (`.otio`) and FCPXML (`.fcpxml`) documents. The format is inferred from\n//! the filename extension (case-insensitive), so the frozen C ABI\n//! (`include/task/project.h`) needs no format parameter: the filename\n//! passed to `oaktask_create_project_load_otio` /\n//! `oaktask_create_project_save_otio` selects the codec. Format handling\n//! ends at the `oakotio` parse/serialize call in each task — the\n//! track/clip/footage building is shared between both formats.\n//!\n//! No OTIO or FCPXML type crosses the oaktask C ABI (README decision #6).\n\nuse std::path::Path;\n\nuse crate::error::{Error, Result};\n\n/// The interchange formats the OTIO tasks can read and write.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum InterchangeFormat {\n\t/// OpenTimelineIO JSON (`.otio`).\n\tOtioJson,\n\t/// Final Cut Pro XML (`.fcpxml`).\n\tFcpxml,\n}\n\n/// The filename extensions each format dispatches on.\npub const SUPPORTED_EXTENSIONS: \u0026str = \".otio, .fcpxml\";\n\nimpl InterchangeFormat {\n\t/// Dispatch on a filename's extension (case-insensitive).\n\t///\n\t/// `foo.OTIO` and `foo.FcpXml` select the same format as `foo.otio` /\n\t/// `foo.fcpxml`; any other extension (or a filename without one) is\n\t/// `Error::Failed` with a message naming the supported extensions.\n\tpub fn of(filename: \u0026str) -\u003e Result\u003cInterchangeFormat\u003e {\n\t\tlet ext = Path::new(filename)\n\t\t\t.extension()\n\t\t\t.map(|e| e.to_string_lossy().into_owned())\n\t\t\t.unwrap_or_default();\n\t\tmatch ext.to_ascii_lowercase().as_str() {\n\t\t\t\"otio\" =\u003e Ok(InterchangeFormat::OtioJson),\n\t\t\t\"fcpxml\" =\u003e Ok(InterchangeFormat::Fcpxml),\n\t\t\t_ =\u003e Err(Error::Failed(format!(\n\t\t\t\t\"Unknown project file format for \\\"{filename}\\\" (supported: {SUPPORTED_EXTENSIONS})\"\n\t\t\t))),\n\t\t}\n\t}\n}\n","traces":[{"line":52,"address":[],"length":0,"stats":{"Line":33}},{"line":53,"address":[],"length":0,"stats":{"Line":66}},{"line":55,"address":[],"length":0,"stats":{"Line":132}},{"line":57,"address":[],"length":0,"stats":{"Line":33}},{"line":58,"address":[],"length":0,"stats":{"Line":56}},{"line":59,"address":[],"length":0,"stats":{"Line":18}},{"line":60,"address":[],"length":0,"stats":{"Line":2}},{"line":61,"address":[],"length":0,"stats":{"Line":2}}],"covered":8,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","import.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//! `ProjectImportTask`, mirroring `src/task/src/project/import/import.h`.\n//!\n//! Imports media files into a folder of a project. Produces an undoable\n//! `OakUndoCommand` (taken via `take_command()`), tracks per-file import\n//! failures, and supports an optional image-sequence confirmation callback.\n//!\n//! CPP-PARITY: src/task/src/project/import/import.h\n\nuse std::path::Path;\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// Callback used to confirm whether a detected image sequence should be\n/// imported as a sequence rather than individual frames, mirroring\n/// `ImageSequenceConfirmFn` in import.h.\npub type ImageSequenceConfirmFn = Box\u003cdyn FnMut(\u0026str, \u0026str) -\u003e bool + Send\u003e;\n\n/// A project-import task. Borrows the destination folder and project.\npub struct ProjectImportTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Destination folder (borrowed `OakNodeFolder`).\n\tpub folder: CHandle,\n\t/// Destination project (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Media filenames to import.\n\tpub filenames: Vec\u003cString\u003e,\n\t/// The undo command produced by the task, taken via `take_command`.\n\tcommand: Option\u003cCHandle\u003e,\n\t/// Files that failed to import.\n\tinvalid_files: Vec\u003cString\u003e,\n\t/// Imported footage, taken via `get_imported_footage`.\n\timported_footage: Vec\u003cCHandle\u003e,\n\t/// Optional image-sequence confirmation callback.\n\timage_sequence_confirm: Option\u003cImageSequenceConfirmFn\u003e,\n\t/// Total number of files to import (directories counted recursively).\n\tfile_count: usize,\n\t/// Image-sequence files already decided not to be imported as sequences.\n\timage_sequence_ignore_files: Vec\u003cString\u003e,\n}\n\nimpl Drop for ProjectImportTask {\n\tfn drop(\u0026mut self) {\n\t\tif let Some(command) = \u0026mut self.command {\n\t\t\tif !command.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::undo::oakundo_command_free(command);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Borrowed footage handles: releasing them only frees the handle\n\t\t// boxes (the footage nodes are owned by the project).\n\t\tfor footage in \u0026self.imported_footage {\n\t\t\tif !footage.ctx.is_null() {\n\t\t\t\tif let Some(release) = footage.release {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\trelease(footage.ctx);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl ProjectImportTask {\n\t/// Create an import task for the given folder/project and filenames.\n\tpub fn new(\n\t\tbase: Task,\n\t\tfolder: CHandle,\n\t\tproject: CHandle,\n\t\tfilenames: Vec\u003cString\u003e,\n\t\timage_sequence_confirm: Option\u003cImageSequenceConfirmFn\u003e,\n\t\tfile_count: usize,\n\t) -\u003e ProjectImportTask {\n\t\tProjectImportTask {\n\t\t\tbase,\n\t\t\tfolder,\n\t\t\tproject,\n\t\t\tfilenames,\n\t\t\tcommand: None,\n\t\t\tinvalid_files: Vec::new(),\n\t\t\timported_footage: Vec::new(),\n\t\t\timage_sequence_confirm,\n\t\t\tfile_count,\n\t\t\timage_sequence_ignore_files: Vec::new(),\n\t\t}\n\t}\n\n\t/// Take ownership of the produced undo command; `Err(Error::State)` if\n\t/// the task has not run yet.\n\tpub fn take_command(\u0026mut self) -\u003e Result\u003cCHandle\u003e {\n\t\tself.command.take().ok_or(Error::State)\n\t}\n\n\t/// Number of files that failed to import.\n\tpub fn get_invalid_file_count(\u0026self) -\u003e usize {\n\t\tself.invalid_files.len()\n\t}\n\n\t/// Whether any file failed to import.\n\tpub fn has_invalid_files(\u0026self) -\u003e bool {\n\t\t!self.invalid_files.is_empty()\n\t}\n\n\t/// Borrowed handle to the imported footage at `index` (addref'd by the C\n\t/// ABI); `Err(Error::NotFound)` if out of range.\n\tpub fn get_imported_footage(\u0026self, index: usize) -\u003e Result\u003cCHandle\u003e {\n\t\tlet footage = self.imported_footage.get(index).ok_or(Error::NotFound)?;\n\t\tif !footage.ctx.is_null() {\n\t\t\tif let Some(addref) = footage.addref {\n\t\t\t\tunsafe {\n\t\t\t\t\taddref(footage.ctx);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tOk(*footage)\n\t}\n\n\t/// Total number of imported footage entries.\n\tpub fn get_file_count(\u0026self) -\u003e usize {\n\t\tself.imported_footage.len()\n\t}\n\n\t/// Set the image-sequence confirmation callback.\n\tpub fn set_image_sequence_confirm_callback(\u0026mut self, cb: ImageSequenceConfirmFn) {\n\t\tself.image_sequence_confirm = Some(cb);\n\t}\n\n\t/// The number of files counted at construction (progress denominator).\n\tpub fn file_count(\u0026self) -\u003e usize {\n\t\tself.file_count\n\t}\n\n\t/// The invalid filename at `index` (assumes the index is in range).\n\tpub(crate) fn invalid_file_at(\u0026self, index: usize) -\u003e \u0026str {\n\t\t\u0026self.invalid_files[index]\n\t}\n\n\tfn import(\u0026mut self, task: \u0026mut Task, folder: CHandle, entries: \u0026mut Vec\u003cString\u003e, counter: \u0026mut usize, parent_command: CHandle) {\n\t\tlet mut i = 0;\n\t\twhile i \u003c entries.len() {\n\t\t\tif task.is_cancelled() {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet file_path = \u0026entries[i];\n\n\t\t\tif Path::new(file_path).is_dir() {\n\t\t\t\t// Create a folder corresponding to the directory and recurse.\n\t\t\t\tlet entry_list: Vec\u003cString\u003e = match std::fs::read_dir(file_path) {\n\t\t\t\t\tOk(rd) =\u003e rd\n\t\t\t\t\t\t.filter_map(|e| e.ok())\n\t\t\t\t\t\t.map(|e| e.path().to_string_lossy().into_owned())\n\t\t\t\t\t\t.collect(),\n\t\t\t\t\tErr(_) =\u003e Vec::new(),\n\t\t\t\t};\n\n\t\t\t\tif !entry_list.is_empty() {\n\t\t\t\t\tlet folder_handle = unsafe { bridge::node::oaknode_folder_create(self.project) };\n\t\t\t\t\tif !folder_handle.ctx.is_null() {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::node::oaknode_node_set_label(\n\t\t\t\t\t\t\t\tbridge::node::oaknode_folder_as_node(folder_handle),\n\t\t\t\t\t\t\t\tcstr(\u0026basename_of(file_path)),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tself.add_item_to_folder(folder, unsafe { bridge::node::oaknode_folder_as_node(folder_handle) }, parent_command);\n\t\t\t\t\t\tlet mut sub_entries = entry_list;\n\t\t\t\t\t\tself.import(task, folder_handle, \u0026mut sub_entries, counter, parent_command);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlet footage = unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) };\n\t\t\t\tif footage.ctx.is_null() {\n\t\t\t\t\ti += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_footage_set_cancel_atom(footage, bridge::render::OakCancelAtom::from_chandle(task.get_cancel_atom()));\n\t\t\t\t}\n\t\t\t\tlet ok = unsafe { bridge::node::oaknode_footage_set_filename(footage, cstr(file_path)) } == 0;\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_footage_set_cancel_atom(footage, bridge::render::OakCancelAtom::null());\n\t\t\t\t}\n\n\t\t\t\tif ok \u0026\u0026 unsafe { bridge::node::oaknode_footage_is_valid(footage) } != 0 {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_node_set_label(bridge::node::oaknode_footage_as_node(footage), cstr(\u0026basename_of(file_path)));\n\t\t\t\t\t}\n\n\t\t\t\t\t// See if this footage is an image sequence.\n\t\t\t\t\tself.validate_image_sequence(task, footage, entries, i);\n\n\t\t\t\t\t// Create the undoable command that adds the item.\n\t\t\t\t\tself.add_item_to_folder(folder, unsafe { bridge::node::oaknode_footage_as_node(footage) }, parent_command);\n\n\t\t\t\t\tself.imported_footage.push(footage);\n\t\t\t\t} else {\n\t\t\t\t\tself.invalid_files.push(file_path.clone());\n\n\t\t\t\t\t// Remove the invalid footage from the graph; the remove\n\t\t\t\t\t// command takes ownership on redo and deletes the node\n\t\t\t\t\t// when the command is destroyed.\n\t\t\t\t\tlet mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(footage)) };\n\t\t\t\t\tif !remove.ctx.is_null() {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(remove);\n\t\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut remove);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t*counter += 1;\n\t\t\t\ttask.emit_progress(*counter as f64 / self.file_count.max(1) as f64);\n\t\t\t}\n\t\t\ti += 1;\n\t\t}\n\t}\n\n\tfn add_item_to_folder(\u0026self, folder: CHandle, item: CHandle, command: CHandle) {\n\t\tlet child = unsafe { bridge::node::oaknode_command_create_folder_add_child(folder, item) };\n\t\tif !child.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::undo::oakundo_command_multi_add_child(command, child);\n\t\t\t}\n\t\t}\n\t}\n\n\tfn validate_image_sequence(\u0026mut self, task: \u0026mut Task, footage: CHandle, info_list: \u0026mut Vec\u003cString\u003e, index: usize) {\n\t\tlet filename = footage_filename(footage);\n\t\tif filename.is_empty() {\n\t\t\treturn;\n\t\t}\n\n\t\tlet digit_count = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(\u0026filename)) };\n\t\tif digit_count \u003c= 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tif self.image_sequence_ignore_files.iter().any(|f| f == \u0026filename) {\n\t\t\treturn;\n\t\t}\n\n\t\tif !self.item_is_still_image_footage_only(footage) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet mut video_stream = CHandle::null();\n\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, \u0026mut video_stream) } != 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet mut width = 0;\n\t\tlet mut height = 0;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_get_width(video_stream, \u0026mut width);\n\t\t\tbridge::common::oakcommon_videoparams_get_height(video_stream, \u0026mut height);\n\t\t}\n\n\t\tlet seq_index = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(\u0026filename)) };\n\n\t\tlet prev_fn = transform_sequence_filename(\u0026filename, seq_index - 1, digit_count);\n\t\tlet next_fn = transform_sequence_filename(\u0026filename, seq_index + 1, digit_count);\n\n\t\tlet previous_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(\u0026prev_fn)) };\n\t\tlet next_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(\u0026next_fn)) };\n\n\t\tlet prev_matches = !previous_file.ctx.is_null()\n\t\t\t\u0026\u0026 unsafe { bridge::node::oaknode_footage_is_valid(previous_file) } != 0\n\t\t\t\u0026\u0026 self.compare_still_image_size(previous_file, width, height);\n\t\tlet next_matches = !next_file.ctx.is_null()\n\t\t\t\u0026\u0026 unsafe { bridge::node::oaknode_footage_is_valid(next_file) } != 0\n\t\t\t\u0026\u0026 self.compare_still_image_size(next_file, width, height);\n\n\t\tif prev_matches || next_matches {\n\t\t\t// Ask the user whether this is really a sequence (default: no).\n\t\t\tlet is_sequence = match self.image_sequence_confirm.as_mut() {\n\t\t\t\tSome(cb) =\u003e cb(\u0026filename, \u0026filename),\n\t\t\t\tNone =\u003e false,\n\t\t\t};\n\n\t\t\tlet start_index = get_image_sequence_limit(\u0026filename, seq_index, false, digit_count);\n\t\t\tlet end_index = get_image_sequence_limit(\u0026filename, seq_index, true, digit_count);\n\n\t\t\tfor j in start_index..=end_index {\n\t\t\t\tlet entry_fn = transform_sequence_filename(\u0026filename, j, digit_count);\n\n\t\t\t\tif is_sequence {\n\t\t\t\t\t// Remove later entries of this sequence from the import\n\t\t\t\t\t// list (they are imported as one footage). The C++\n\t\t\t\t\t// `info_list.size()` is re-evaluated after each erase.\n\t\t\t\t\tlet mut k = index + 1;\n\t\t\t\t\twhile k \u003c info_list.len() {\n\t\t\t\t\t\tif info_list[k] == entry_fn {\n\t\t\t\t\t\t\tinfo_list.remove(k);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tk += 1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tself.image_sequence_ignore_files.push(entry_fn);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif is_sequence {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::common::oakcommon_videoparams_set_video_type(video_stream, bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE);\n\n\t\t\t\t\tlet mut rate_buf = [0i8; 64];\n\t\t\t\t\tlet needed = bridge::common::oakcommon_config_get(std::ptr::null(), cstr(\"DefaultSequenceFrameRate\"), rate_buf.as_mut_ptr(), rate_buf.len() as i32);\n\t\t\t\t\tif needed \u003e 0 {\n\t\t\t\t\t\tlet rate = crate::project::load::buf_to_string(\u0026rate_buf);\n\t\t\t\t\t\tif let Some((num, den)) = parse_rational(\u0026rate) {\n\t\t\t\t\t\t\tif den != 0 {\n\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_set_time_base(video_stream, num, den);\n\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_set_frame_rate(video_stream, den, num);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tbridge::common::oakcommon_videoparams_set_start_time(video_stream, start_index);\n\t\t\t\t\tbridge::common::oakcommon_videoparams_set_duration(video_stream, end_index - start_index + 1);\n\t\t\t\t\tbridge::node::oaknode_footage_set_video_params(footage, 0, \u0026video_stream);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !video_stream.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut video_stream);\n\t\t\t}\n\t\t}\n\n\t\t// The probe footage was only created for comparison; remove it.\n\t\tfor probe in [previous_file, next_file] {\n\t\t\tif !probe.ctx.is_null() {\n\t\t\t\tlet mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(probe)) };\n\t\t\t\tif !remove.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(remove);\n\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut remove);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet _ = task;\n\t}\n\n\tfn item_is_still_image_footage_only(\u0026self, footage: CHandle) -\u003e bool {\n\t\tif unsafe { bridge::node::oaknode_footage_total_stream_count(footage) } != 1 {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut vp = CHandle::null();\n\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, \u0026mut vp) } != 0 {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut video_type = 0;\n\t\tlet mut valid = 0;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_get_video_type(vp, \u0026mut video_type);\n\t\t\tbridge::common::oakcommon_videoparams_get_is_valid(vp, \u0026mut valid);\n\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut vp);\n\t\t}\n\n\t\tvalid != 0 \u0026\u0026 video_type == bridge::common::OAKCOMMON_VIDEO_TYPE_STILL\n\t}\n\n\tfn compare_still_image_size(\u0026self, footage: CHandle, width: i32, height: i32) -\u003e bool {\n\t\tif !self.item_is_still_image_footage_only(footage) {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut stream = CHandle::null();\n\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, \u0026mut stream) } != 0 {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut w = 0;\n\t\tlet mut h = 0;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_get_width(stream, \u0026mut w);\n\t\t\tbridge::common::oakcommon_videoparams_get_height(stream, \u0026mut h);\n\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut stream);\n\t\t}\n\n\t\tw == width \u0026\u0026 h == height\n\t}\n}\n\nimpl TaskBehavior for ProjectImportTask {\n\t/// Import each filename via the oaknode footage/folder C ABI\n\t/// (`bridge::node`), collecting an undo command, imported footage, and\n\t/// per-file failures.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet mut command = unsafe { bridge::undo::oakundo_command_init_multi() };\n\t\tif command.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create import command\");\n\t\t\treturn Err(Error::Failed(\"Failed to create import command\".to_string()));\n\t\t}\n\t\tself.command = Some(command);\n\n\t\tlet mut counter = 0;\n\t\tlet mut entries = self.filenames.clone();\n\t\tself.import(task, self.folder, \u0026mut entries, \u0026mut counter, command);\n\n\t\tif task.is_cancelled() {\n\t\t\tif !command.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut command);\n\t\t\t\t}\n\t\t\t}\n\t\t\tself.command = None;\n\t\t\treturn Err(Error::Cancelled);\n\t\t}\n\t\tOk(())\n\t}\n}\n\n/// Count files recursively (directories recurse; anything else counts 1),\n/// mirroring `count_files_recursive` in import.cpp.\nfn count_files_recursive(paths: \u0026[String]) -\u003e usize {\n\tpaths\n\t\t.iter()\n\t\t.map(|p| {\n\t\t\tlet path = Path::new(p);\n\t\t\tif path.is_dir() {\n\t\t\t\tcount_dir_files(path)\n\t\t\t} else {\n\t\t\t\t1\n\t\t\t}\n\t\t})\n\t\t.sum()\n}\n\nfn count_dir_files(dir: \u0026Path) -\u003e usize {\n\tmatch std::fs::read_dir(dir) {\n\t\tOk(rd) =\u003e rd\n\t\t\t.filter_map(|e| e.ok())\n\t\t\t.map(|e| {\n\t\t\t\tlet p = e.path();\n\t\t\t\tif p.is_dir() {\n\t\t\t\t\tcount_dir_files(\u0026p)\n\t\t\t\t} else {\n\t\t\t\t\t1\n\t\t\t\t}\n\t\t\t})\n\t\t\t.sum(),\n\t\tErr(_) =\u003e 0,\n\t}\n}\n\n/// `std::filesystem::path(filename).filename()`, as a String.\nfn basename_of(path: \u0026str) -\u003e String {\n\tPath::new(path)\n\t\t.file_name()\n\t\t.map(|n| n.to_string_lossy().into_owned())\n\t\t.unwrap_or_default()\n}\n\n/// Two-stage read of the footage filename.\nfn footage_filename(footage: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);\n\t}\n\tcrate::project::load::buf_to_string(\u0026buf)\n}\n\n/// Substitute `number` into the trailing-digit field of an image-sequence\n/// filename (mirrors `oakcodec_decoder_transform_image_sequence_file_name`).\nfn transform_sequence_filename(filename: \u0026str, number: i64, digit_count: i32) -\u003e String {\n\tlet width = digit_count.max(0) as usize;\n\tmatch filename.rfind('.') {\n\t\tSome(dot) =\u003e {\n\t\t\t// The digits are the `width` chars immediately before the\n\t\t\t// extension; they are replaced by `number`.\n\t\t\tlet digits_start = dot.saturating_sub(width);\n\t\t\tlet prefix = \u0026filename[..digits_start];\n\t\t\tformat!(\"{prefix}{:0width$}{}\", number, \u0026filename[dot..], width = width)\n\t\t}\n\t\tNone =\u003e format!(\"{filename}{:0width$}\", number, width = width),\n\t}\n}\n\n/// Walk the index from `start` in the given direction while the transformed\n/// filename exists (mirrors `get_image_sequence_limit` in import.cpp).\nfn get_image_sequence_limit(start_fn: \u0026str, start: i64, up: bool, digit_count: i32) -\u003e i64 {\n\tlet mut current = start;\n\tloop {\n\t\tlet test_index = if up { current + 1 } else { current - 1 };\n\t\tlet test_filename = transform_sequence_filename(start_fn, test_index, digit_count);\n\t\tif !Path::new(\u0026test_filename).exists() {\n\t\t\tbreak;\n\t\t}\n\t\tcurrent = test_index;\n\t}\n\tcurrent\n}\n\n/// Parse a \"num/den\" frame-rate string.\nfn parse_rational(s: \u0026str) -\u003e Option\u003c(i32, i32)\u003e {\n\tlet mut parts = s.trim().split('/');\n\tlet num: i32 = parts.next()?.trim().parse().ok()?;\n\tlet den: i32 = parts.next()?.trim().parse().ok()?;\n\tSome((num, den))\n}\n\n/// Convenience used by the C ABI factory to compute the title.\npub(crate) fn import_title(paths: \u0026[String]) -\u003e String {\n\tlet count = count_files_recursive(paths).max(1);\n\tformat!(\"Importing {count} file(s)\")\n}\n\n/// Convenience used by the C ABI factory to compute the progress\n/// denominator (mirrors the C++ `file_count_`).\npub(crate) fn import_file_count(paths: \u0026[String]) -\u003e usize {\n\tcount_files_recursive(paths).max(1)\n}\n","traces":[{"line":63,"address":[],"length":0,"stats":{"Line":3}},{"line":64,"address":[],"length":0,"stats":{"Line":5}},{"line":65,"address":[],"length":0,"stats":{"Line":2}},{"line":67,"address":[],"length":0,"stats":{"Line":2}},{"line":73,"address":[],"length":0,"stats":{"Line":5}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":75,"address":[],"length":0,"stats":{"Line":4}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":3}},{"line":101,"address":[],"length":0,"stats":{"Line":6}},{"line":102,"address":[],"length":0,"stats":{"Line":6}},{"line":105,"address":[],"length":0,"stats":{"Line":3}},{"line":111,"address":[],"length":0,"stats":{"Line":2}},{"line":112,"address":[],"length":0,"stats":{"Line":8}},{"line":116,"address":[],"length":0,"stats":{"Line":6}},{"line":117,"address":[],"length":0,"stats":{"Line":12}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":0}},{"line":127,"address":[],"length":0,"stats":{"Line":2}},{"line":128,"address":[],"length":0,"stats":{"Line":10}},{"line":129,"address":[],"length":0,"stats":{"Line":1}},{"line":130,"address":[],"length":0,"stats":{"Line":2}},{"line":132,"address":[],"length":0,"stats":{"Line":1}},{"line":136,"address":[],"length":0,"stats":{"Line":1}},{"line":140,"address":[],"length":0,"stats":{"Line":3}},{"line":141,"address":[],"length":0,"stats":{"Line":6}},{"line":145,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":0}},{"line":155,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":3}},{"line":160,"address":[],"length":0,"stats":{"Line":6}},{"line":161,"address":[],"length":0,"stats":{"Line":12}},{"line":162,"address":[],"length":0,"stats":{"Line":6}},{"line":163,"address":[],"length":0,"stats":{"Line":0}},{"line":166,"address":[],"length":0,"stats":{"Line":6}},{"line":168,"address":[],"length":0,"stats":{"Line":6}},{"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":175,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":183,"address":[],"length":0,"stats":{"Line":0}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":189,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":12}},{"line":194,"address":[],"length":0,"stats":{"Line":6}},{"line":195,"address":[],"length":0,"stats":{"Line":0}},{"line":196,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":12}},{"line":202,"address":[],"length":0,"stats":{"Line":12}},{"line":204,"address":[],"length":0,"stats":{"Line":6}},{"line":207,"address":[],"length":0,"stats":{"Line":8}},{"line":209,"address":[],"length":0,"stats":{"Line":12}},{"line":213,"address":[],"length":0,"stats":{"Line":14}},{"line":216,"address":[],"length":0,"stats":{"Line":14}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":220,"address":[],"length":0,"stats":{"Line":4}},{"line":225,"address":[],"length":0,"stats":{"Line":4}},{"line":226,"address":[],"length":0,"stats":{"Line":1}},{"line":228,"address":[],"length":0,"stats":{"Line":3}},{"line":229,"address":[],"length":0,"stats":{"Line":1}},{"line":234,"address":[],"length":0,"stats":{"Line":3}},{"line":235,"address":[],"length":0,"stats":{"Line":12}},{"line":237,"address":[],"length":0,"stats":{"Line":3}},{"line":241,"address":[],"length":0,"stats":{"Line":2}},{"line":242,"address":[],"length":0,"stats":{"Line":8}},{"line":243,"address":[],"length":0,"stats":{"Line":2}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":250,"address":[],"length":0,"stats":{"Line":2}},{"line":251,"address":[],"length":0,"stats":{"Line":6}},{"line":252,"address":[],"length":0,"stats":{"Line":4}},{"line":253,"address":[],"length":0,"stats":{"Line":0}},{"line":256,"address":[],"length":0,"stats":{"Line":8}},{"line":257,"address":[],"length":0,"stats":{"Line":2}},{"line":258,"address":[],"length":0,"stats":{"Line":1}},{"line":261,"address":[],"length":0,"stats":{"Line":2}},{"line":262,"address":[],"length":0,"stats":{"Line":0}},{"line":265,"address":[],"length":0,"stats":{"Line":2}},{"line":266,"address":[],"length":0,"stats":{"Line":0}},{"line":269,"address":[],"length":0,"stats":{"Line":2}},{"line":270,"address":[],"length":0,"stats":{"Line":2}},{"line":271,"address":[],"length":0,"stats":{"Line":0}},{"line":274,"address":[],"length":0,"stats":{"Line":2}},{"line":275,"address":[],"length":0,"stats":{"Line":2}},{"line":277,"address":[],"length":0,"stats":{"Line":4}},{"line":278,"address":[],"length":0,"stats":{"Line":2}},{"line":281,"address":[],"length":0,"stats":{"Line":4}},{"line":283,"address":[],"length":0,"stats":{"Line":5}},{"line":284,"address":[],"length":0,"stats":{"Line":5}},{"line":286,"address":[],"length":0,"stats":{"Line":5}},{"line":287,"address":[],"length":0,"stats":{"Line":5}},{"line":289,"address":[],"length":0,"stats":{"Line":2}},{"line":290,"address":[],"length":0,"stats":{"Line":1}},{"line":291,"address":[],"length":0,"stats":{"Line":5}},{"line":292,"address":[],"length":0,"stats":{"Line":2}},{"line":293,"address":[],"length":0,"stats":{"Line":1}},{"line":294,"address":[],"length":0,"stats":{"Line":5}},{"line":296,"address":[],"length":0,"stats":{"Line":1}},{"line":298,"address":[],"length":0,"stats":{"Line":2}},{"line":299,"address":[],"length":0,"stats":{"Line":3}},{"line":300,"address":[],"length":0,"stats":{"Line":0}},{"line":303,"address":[],"length":0,"stats":{"Line":5}},{"line":304,"address":[],"length":0,"stats":{"Line":5}},{"line":306,"address":[],"length":0,"stats":{"Line":4}},{"line":307,"address":[],"length":0,"stats":{"Line":15}},{"line":309,"address":[],"length":0,"stats":{"Line":3}},{"line":313,"address":[],"length":0,"stats":{"Line":6}},{"line":314,"address":[],"length":0,"stats":{"Line":10}},{"line":315,"address":[],"length":0,"stats":{"Line":4}},{"line":316,"address":[],"length":0,"stats":{"Line":6}},{"line":317,"address":[],"length":0,"stats":{"Line":2}},{"line":319,"address":[],"length":0,"stats":{"Line":2}},{"line":322,"address":[],"length":0,"stats":{"Line":0}},{"line":326,"address":[],"length":0,"stats":{"Line":1}},{"line":328,"address":[],"length":0,"stats":{"Line":2}},{"line":330,"address":[],"length":0,"stats":{"Line":2}},{"line":331,"address":[],"length":0,"stats":{"Line":8}},{"line":332,"address":[],"length":0,"stats":{"Line":1}},{"line":333,"address":[],"length":0,"stats":{"Line":3}},{"line":334,"address":[],"length":0,"stats":{"Line":3}},{"line":335,"address":[],"length":0,"stats":{"Line":2}},{"line":336,"address":[],"length":0,"stats":{"Line":5}},{"line":337,"address":[],"length":0,"stats":{"Line":3}},{"line":342,"address":[],"length":0,"stats":{"Line":3}},{"line":343,"address":[],"length":0,"stats":{"Line":3}},{"line":344,"address":[],"length":0,"stats":{"Line":3}},{"line":349,"address":[],"length":0,"stats":{"Line":1}},{"line":351,"address":[],"length":0,"stats":{"Line":1}},{"line":356,"address":[],"length":0,"stats":{"Line":3}},{"line":357,"address":[],"length":0,"stats":{"Line":2}},{"line":358,"address":[],"length":0,"stats":{"Line":8}},{"line":359,"address":[],"length":0,"stats":{"Line":2}},{"line":361,"address":[],"length":0,"stats":{"Line":6}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":368,"address":[],"length":0,"stats":{"Line":1}},{"line":371,"address":[],"length":0,"stats":{"Line":3}},{"line":372,"address":[],"length":0,"stats":{"Line":3}},{"line":373,"address":[],"length":0,"stats":{"Line":0}},{"line":376,"address":[],"length":0,"stats":{"Line":6}},{"line":377,"address":[],"length":0,"stats":{"Line":6}},{"line":378,"address":[],"length":0,"stats":{"Line":0}},{"line":381,"address":[],"length":0,"stats":{"Line":6}},{"line":382,"address":[],"length":0,"stats":{"Line":6}},{"line":384,"address":[],"length":0,"stats":{"Line":12}},{"line":385,"address":[],"length":0,"stats":{"Line":12}},{"line":386,"address":[],"length":0,"stats":{"Line":3}},{"line":389,"address":[],"length":0,"stats":{"Line":6}},{"line":392,"address":[],"length":0,"stats":{"Line":2}},{"line":393,"address":[],"length":0,"stats":{"Line":4}},{"line":394,"address":[],"length":0,"stats":{"Line":0}},{"line":397,"address":[],"length":0,"stats":{"Line":4}},{"line":398,"address":[],"length":0,"stats":{"Line":4}},{"line":399,"address":[],"length":0,"stats":{"Line":0}},{"line":402,"address":[],"length":0,"stats":{"Line":4}},{"line":403,"address":[],"length":0,"stats":{"Line":4}},{"line":405,"address":[],"length":0,"stats":{"Line":8}},{"line":406,"address":[],"length":0,"stats":{"Line":8}},{"line":407,"address":[],"length":0,"stats":{"Line":2}},{"line":410,"address":[],"length":0,"stats":{"Line":4}},{"line":418,"address":[],"length":0,"stats":{"Line":3}},{"line":419,"address":[],"length":0,"stats":{"Line":6}},{"line":420,"address":[],"length":0,"stats":{"Line":6}},{"line":421,"address":[],"length":0,"stats":{"Line":0}},{"line":422,"address":[],"length":0,"stats":{"Line":0}},{"line":424,"address":[],"length":0,"stats":{"Line":3}},{"line":426,"address":[],"length":0,"stats":{"Line":6}},{"line":427,"address":[],"length":0,"stats":{"Line":9}},{"line":428,"address":[],"length":0,"stats":{"Line":21}},{"line":430,"address":[],"length":0,"stats":{"Line":6}},{"line":431,"address":[],"length":0,"stats":{"Line":0}},{"line":433,"address":[],"length":0,"stats":{"Line":0}},{"line":436,"address":[],"length":0,"stats":{"Line":0}},{"line":437,"address":[],"length":0,"stats":{"Line":0}},{"line":439,"address":[],"length":0,"stats":{"Line":3}},{"line":445,"address":[],"length":0,"stats":{"Line":6}},{"line":446,"address":[],"length":0,"stats":{"Line":6}},{"line":448,"address":[],"length":0,"stats":{"Line":16}},{"line":449,"address":[],"length":0,"stats":{"Line":30}},{"line":450,"address":[],"length":0,"stats":{"Line":20}},{"line":451,"address":[],"length":0,"stats":{"Line":0}},{"line":453,"address":[],"length":0,"stats":{"Line":10}},{"line":459,"address":[],"length":0,"stats":{"Line":0}},{"line":460,"address":[],"length":0,"stats":{"Line":0}},{"line":461,"address":[],"length":0,"stats":{"Line":0}},{"line":462,"address":[],"length":0,"stats":{"Line":0}},{"line":463,"address":[],"length":0,"stats":{"Line":0}},{"line":464,"address":[],"length":0,"stats":{"Line":0}},{"line":465,"address":[],"length":0,"stats":{"Line":0}},{"line":466,"address":[],"length":0,"stats":{"Line":0}},{"line":468,"address":[],"length":0,"stats":{"Line":0}},{"line":472,"address":[],"length":0,"stats":{"Line":0}},{"line":477,"address":[],"length":0,"stats":{"Line":2}},{"line":478,"address":[],"length":0,"stats":{"Line":2}},{"line":480,"address":[],"length":0,"stats":{"Line":8}},{"line":485,"address":[],"length":0,"stats":{"Line":2}},{"line":486,"address":[],"length":0,"stats":{"Line":8}},{"line":487,"address":[],"length":0,"stats":{"Line":2}},{"line":488,"address":[],"length":0,"stats":{"Line":0}},{"line":490,"address":[],"length":0,"stats":{"Line":6}},{"line":492,"address":[],"length":0,"stats":{"Line":8}},{"line":494,"address":[],"length":0,"stats":{"Line":4}},{"line":499,"address":[],"length":0,"stats":{"Line":9}},{"line":500,"address":[],"length":0,"stats":{"Line":18}},{"line":501,"address":[],"length":0,"stats":{"Line":9}},{"line":502,"address":[],"length":0,"stats":{"Line":9}},{"line":505,"address":[],"length":0,"stats":{"Line":36}},{"line":506,"address":[],"length":0,"stats":{"Line":18}},{"line":507,"address":[],"length":0,"stats":{"Line":18}},{"line":509,"address":[],"length":0,"stats":{"Line":0}},{"line":515,"address":[],"length":0,"stats":{"Line":2}},{"line":516,"address":[],"length":0,"stats":{"Line":4}},{"line":518,"address":[],"length":0,"stats":{"Line":12}},{"line":519,"address":[],"length":0,"stats":{"Line":20}},{"line":520,"address":[],"length":0,"stats":{"Line":4}},{"line":521,"address":[],"length":0,"stats":{"Line":2}},{"line":523,"address":[],"length":0,"stats":{"Line":2}},{"line":525,"address":[],"length":0,"stats":{"Line":2}},{"line":529,"address":[],"length":0,"stats":{"Line":1}},{"line":530,"address":[],"length":0,"stats":{"Line":3}},{"line":531,"address":[],"length":0,"stats":{"Line":7}},{"line":532,"address":[],"length":0,"stats":{"Line":7}},{"line":533,"address":[],"length":0,"stats":{"Line":1}},{"line":537,"address":[],"length":0,"stats":{"Line":3}},{"line":538,"address":[],"length":0,"stats":{"Line":12}},{"line":539,"address":[],"length":0,"stats":{"Line":3}},{"line":544,"address":[],"length":0,"stats":{"Line":3}},{"line":545,"address":[],"length":0,"stats":{"Line":9}}],"covered":184,"coverable":235},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","load.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//! `ProjectLoadBaseTask` / `ProjectLoadTask`, mirroring\n//! `src/task/src/project/load/load.h`.\n//!\n//! Loads an `.oakproj` file into a new `OakNodeProject`. The base task holds\n//! the filename and produces the project on `take_project()`; `ProjectLoadTask`\n//! is the concrete (OTIO-less) loader.\n//!\n//! CPP-PARITY: src/task/src/project/load/load.h\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// Serializer result codes (`include/node/serializer.h`).\nconst OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0;\nconst OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD: i32 = 1;\nconst OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW: i32 = 2;\nconst OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION: i32 = 3;\nconst OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4;\nconst OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5;\nconst OAKNODE_SERIALIZER_RESULT_NO_DATA: i32 = 7;\n\n/// The base project-load task: owns a source filename and yields a loaded\n/// project via [`ProjectLoadBaseTask::take_project`].\npub struct ProjectLoadBaseTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Absolute project filename to load.\n\tpub filename: String,\n\t/// The loaded project, produced by the task and taken via `take_project`.\n\tloaded_project: Option\u003cCHandle\u003e,\n}\n\nimpl Drop for ProjectLoadBaseTask {\n\tfn drop(\u0026mut self) {\n\t\t// Free the loaded project if it was never taken.\n\t\tif let Some(project) = \u0026mut self.loaded_project {\n\t\t\tif !project.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_project_free(project);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl ProjectLoadBaseTask {\n\t/// Create a base load task for the given filename.\n\tpub fn new(base: Task, filename: String) -\u003e ProjectLoadBaseTask {\n\t\tProjectLoadBaseTask {\n\t\t\tbase,\n\t\t\tfilename,\n\t\t\tloaded_project: None,\n\t\t}\n\t}\n\n\t/// Take ownership of the loaded project. Returns `Err(Error::State)` if\n\t/// the task has not loaded a project yet.\n\tpub fn take_project(\u0026mut self) -\u003e Result\u003cCHandle\u003e {\n\t\tself.loaded_project.take().ok_or(Error::State)\n\t}\n\n\t/// Store the loaded project (called by `run`).\n\tpub(crate) fn store_project(\u0026mut self, project: CHandle) {\n\t\tself.loaded_project = Some(project);\n\t}\n}\n\nimpl TaskBehavior for ProjectLoadBaseTask {\n\t/// Load the project from `filename` via the oaknode serializer\n\t/// (`bridge::node`), storing the resulting `OakNodeProject`.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet mut project = unsafe { bridge::node::oaknode_project_init() };\n\t\tif project.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create project\");\n\t\t\treturn Err(Error::Failed(\"Failed to create project\".to_string()));\n\t\t}\n\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_project_set_filename(project, cstr(\u0026self.filename));\n\t\t}\n\n\t\tlet mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;\n\t\tlet mut details = [0i8; 512];\n\t\tlet result = unsafe {\n\t\t\tbridge::node::oaknode_serializer_load_from_file(\n\t\t\t\tproject,\n\t\t\t\tcstr(\u0026self.filename),\n\t\t\t\t\u0026mut code,\n\t\t\t\tdetails.as_mut_ptr(),\n\t\t\t\tdetails.len() as i32,\n\t\t\t)\n\t\t};\n\n\t\tlet mut success = false;\n\t\tmatch code {\n\t\t\tOAKNODE_SERIALIZER_RESULT_SUCCESS =\u003e success = true,\n\t\t\tOAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD =\u003e {\n\t\t\t\ttask.set_error(\"This project is from a version of Oak Video Editor that is no longer supported in this version.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW =\u003e {\n\t\t\t\ttask.set_error(\"This project is from a newer version of Oak Video Editor and cannot be opened in this version.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION =\u003e {\n\t\t\t\ttask.set_error(\"Failed to determine project version.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_FILE_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\"Failed to read file \\\"{}\\\" for reading.\", self.filename));\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_XML_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Failed to read XML document. File may be corrupt. Error was: {}\",\n\t\t\t\t\tbuf_to_string(\u0026details)\n\t\t\t\t));\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_NO_DATA =\u003e {\n\t\t\t\ttask.set_error(\"Failed to find any data to parse.\");\n\t\t\t}\n\t\t\t_ =\u003e task.set_error(\"Unknown error.\"),\n\t\t}\n\n\t\tif result == 0 \u0026\u0026 success {\n\t\t\tself.store_project(project);\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tif !project.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_project_free(\u0026mut project);\n\t\t\t}\n\t\t}\n\t\tErr(Error::Failed(\"Failed to load project\".to_string()))\n\t}\n}\n\n/// The concrete native project loader (no OTIO path).\npub struct ProjectLoadTask {\n\t/// The shared task base.\n\tpub base: ProjectLoadBaseTask,\n}\n\nimpl TaskBehavior for ProjectLoadTask {\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t\u003cProjectLoadBaseTask as TaskBehavior\u003e::run(\u0026mut self.base, task)\n\t}\n}\n\n/// Read a NUL-terminated char buffer into a String (lossy).\npub(crate) fn buf_to_string(buf: \u0026[i8]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n","traces":[{"line":53,"address":[],"length":0,"stats":{"Line":22}},{"line":55,"address":[],"length":0,"stats":{"Line":23}},{"line":56,"address":[],"length":0,"stats":{"Line":1}},{"line":58,"address":[],"length":0,"stats":{"Line":1}},{"line":67,"address":[],"length":0,"stats":{"Line":22}},{"line":77,"address":[],"length":0,"stats":{"Line":23}},{"line":78,"address":[],"length":0,"stats":{"Line":92}},{"line":82,"address":[],"length":0,"stats":{"Line":14}},{"line":83,"address":[],"length":0,"stats":{"Line":14}},{"line":90,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":4}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":6}},{"line":101,"address":[],"length":0,"stats":{"Line":4}},{"line":102,"address":[],"length":0,"stats":{"Line":4}},{"line":105,"address":[],"length":0,"stats":{"Line":2}},{"line":106,"address":[],"length":0,"stats":{"Line":4}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":4}},{"line":109,"address":[],"length":0,"stats":{"Line":2}},{"line":113,"address":[],"length":0,"stats":{"Line":4}},{"line":114,"address":[],"length":0,"stats":{"Line":2}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":116,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":119,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":4}},{"line":141,"address":[],"length":0,"stats":{"Line":3}},{"line":142,"address":[],"length":0,"stats":{"Line":1}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":150,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":2}},{"line":162,"address":[],"length":0,"stats":{"Line":6}},{"line":167,"address":[],"length":0,"stats":{"Line":86}},{"line":168,"address":[],"length":0,"stats":{"Line":2121}},{"line":169,"address":[],"length":0,"stats":{"Line":344}}],"covered":36,"coverable":51},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","loadotio.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//! `LoadOTIOTask`, mirroring `src/task/src/project/loadotio/loadotio.h`.\n//!\n//! Loads an OpenTimelineIO (`.otio`) or FCPXML (`.fcpxml`) file into a new\n//! `OakNodeProject`, with a configurable import-confirmation callback. The\n//! format is dispatched from the filename extension (see\n//! [`crate::project::format`]); the document is parsed with the pure-Rust\n//! `oakotio` binding (see `README` decision #6) and the project is built\n//! through the oaknode / oaktimeline C ABIs exactly like the C++ task, so\n//! no OTIO or FCPXML type crosses the oaktask C ABI.\n//!\n//! CPP-PARITY: src/task/src/project/loadotio/loadotio.cpp\n\nuse std::collections::HashMap;\nuse std::path::Path;\nuse std::sync::Mutex;\n\nuse oakotio::Serializable;\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::project::format::InterchangeFormat;\nuse crate::project::load::ProjectLoadBaseTask;\nuse crate::task::{Task, TaskBehavior};\n\n/// Callback used to confirm whether the sequences of an OTIO document should\n/// be imported, mirroring `ImportConfirmFn` in loadotio.h (receives the\n/// sequence labels in order; return true to accept).\npub type ImportConfirmFn = Box\u003cdyn FnMut(\u0026[String]) -\u003e bool + Send\u003e;\n\n/// Global import-confirmation callback, mirroring the C++ static\n/// `LoadOTIOTask::confirm_callback_` (loadotio.h). When unset, everything is\n/// imported (the headless default).\nstatic CONFIRM_CALLBACK: Mutex\u003cOption\u003cImportConfirmFn\u003e\u003e = Mutex::new(None);\n\n/// Register (or clear, with `None`) the global import-confirmation callback.\n///\n/// CPP-PARITY: src/task/src/project/loadotio/loadotio.h\n/// (`LoadOTIOTask::set_import_confirm_callback`)\npub fn set_import_confirm_callback(cb: Option\u003cImportConfirmFn\u003e) {\n\t*CONFIRM_CALLBACK.lock().unwrap() = cb;\n}\n\n/// An OTIO project loader.\npub struct LoadOTIOTask {\n\t/// The base load task (owns the filename and yields the project).\n\tpub base: ProjectLoadBaseTask,\n}\n\nimpl LoadOTIOTask {\n\t/// Create an OTIO loader for the given base load task.\n\tpub fn new(base: ProjectLoadBaseTask) -\u003e LoadOTIOTask {\n\t\tLoadOTIOTask { base }\n\t}\n}\n\nimpl TaskBehavior for LoadOTIOTask {\n\t/// Load and parse the document (`oakotio`), then convert it into the\n\t/// project stored on the base task.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Dispatch the interchange format from the filename extension.\n\t\t// Format handling ends here: `parse_timelines` returns one\n\t\t// `oakotio::Timeline` per sequence whatever the source format, so\n\t\t// the project building below is shared between `.otio` and\n\t\t// `.fcpxml` (C++ parity for the track/clip/footage code).\n\t\tlet format = match InterchangeFormat::of(\u0026self.base.filename) {\n\t\t\tOk(f) =\u003e f,\n\t\t\tErr(e) =\u003e {\n\t\t\t\tif let Error::Failed(msg) = \u0026e {\n\t\t\t\t\ttask.set_error(msg);\n\t\t\t\t}\n\t\t\t\treturn Err(e);\n\t\t\t}\n\t\t};\n\n\t\tlet timelines = parse_timelines(task, \u0026self.base.filename, format)?;\n\n\t\tlet mut project = unsafe { bridge::node::oaknode_project_init() };\n\t\tif project.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create project\");\n\t\t\treturn Err(Error::Failed(\"Failed to create project\".to_string()));\n\t\t}\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_project_initialize(project);\n\t\t\tbridge::node::oaknode_project_set_modified(project, 1);\n\t\t}\n\n\t\t// Keep track of imported footage\n\t\tlet mut imported_footage: HashMap\u003cString, CHandle\u003e = HashMap::new();\n\n\t\t// Generate a list of sequences with the same names as the timelines.\n\t\t// Assumes each timeline has a unique name.\n\t\tlet mut unnamed_sequence_count = 0;\n\t\tlet mut sequences: Vec\u003cCHandle\u003e = Vec::new();\n\n\t\t// Variables used for loading bar\n\t\tlet mut number_of_clips: f64 = 0.0;\n\n\t\tfor timeline in \u0026timelines {\n\t\t\tlet sequence = unsafe { bridge::node::oaknode_sequence_create() };\n\t\t\tif sequence.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet label = if !timeline.name().is_empty() {\n\t\t\t\ttimeline.name().to_string()\n\t\t\t} else {\n\t\t\t\t// If the otio timeline does not provide a name, create a\n\t\t\t\t// default one here.\n\t\t\t\tunnamed_sequence_count += 1;\n\t\t\t\tformat!(\"Sequence {unnamed_sequence_count}\")\n\t\t\t};\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_node_set_label(bridge::node::oaknode_sequence_as_node(sequence), cstr(\u0026label));\n\t\t\t}\n\n\t\t\t// Set default params incase they aren't edited.\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_sequence_set_default_parameters(sequence);\n\t\t\t}\n\n\t\t\t// Get number of clips for loading bar\n\t\t\tfor track in timeline.tracks().children() {\n\t\t\t\tif let Some(otio_track) = track.as_track() {\n\t\t\t\t\tnumber_of_clips += otio_track.children().len() as f64;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsequences.push(sequence);\n\t\t}\n\t\tif number_of_clips \u003c= 0.0 {\n\t\t\tnumber_of_clips = 1.0;\n\t\t}\n\n\t\t// Ask the user which sequences to import (facade callback; headless\n\t\t// default accepts everything).\n\t\tlet sequence_names: Vec\u003cString\u003e = sequences\n\t\t\t.iter()\n\t\t\t.map(|s| node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*s) }))\n\t\t\t.collect();\n\t\tlet mut confirm = CONFIRM_CALLBACK.lock().unwrap().take();\n\t\tlet accepted = match confirm.as_mut() {\n\t\t\tSome(cb) =\u003e cb(\u0026sequence_names),\n\t\t\tNone =\u003e true,\n\t\t};\n\n\t\tif !accepted {\n\t\t\t// Cancel to indicate to caller that this task did not complete\n\t\t\t// and to simply dispose of it. The project is never handed to the\n\t\t\t// base task, so free it here (the C++ base-task destructor does\n\t\t\t// the same).\n\t\t\ttask.cancel();\n\t\t\tfor sequence in \u0026mut sequences {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_sequence_free(sequence);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !project.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_project_free(\u0026mut project);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet root_folder = unsafe { bridge::node::oaknode_project_root(project) };\n\t\tlet mut clips_done = 0.0f64;\n\n\t\tfor (timeline, sequence) in timelines.iter().zip(\u0026sequences) {\n\t\t\tlet sequence_node = unsafe { bridge::node::oaknode_sequence_as_node(*sequence) };\n\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_project_add_node(project, sequence_node);\n\t\t\t}\n\t\t\tlet mut add_seq = unsafe { bridge::node::oaknode_command_create_folder_add_child(root_folder, sequence_node) };\n\t\t\tif !add_seq.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_seq);\n\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_seq);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Create a folder for this sequence's footage\n\t\t\tlet sequence_footage = unsafe { bridge::node::oaknode_folder_create(project) };\n\t\t\tif !sequence_footage.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_node_set_label(\n\t\t\t\t\t\tbridge::node::oaknode_folder_as_node(sequence_footage),\n\t\t\t\t\t\tcstr(timeline.name()),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tlet mut add_folder = unsafe {\n\t\t\t\t\tbridge::node::oaknode_command_create_folder_add_child(\n\t\t\t\t\t\troot_folder,\n\t\t\t\t\t\tbridge::node::oaknode_folder_as_node(sequence_footage),\n\t\t\t\t\t)\n\t\t\t\t};\n\t\t\t\tif !add_folder.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_folder);\n\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_folder);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Iterate through tracks\n\t\t\tfor track_composable in timeline.tracks().children() {\n\t\t\t\tlet Some(otio_track) = track_composable.as_track() else {\n\t\t\t\t\tcontinue;\n\t\t\t\t};\n\n\t\t\t\t// Determine what kind of track it is\n\t\t\t\tlet track_type = match otio_track.kind() {\n\t\t\t\t\t\"Video\" =\u003e bridge::node::OAKNODE_TRACK_TYPE_VIDEO,\n\t\t\t\t\t\"Audio\" =\u003e bridge::node::OAKNODE_TRACK_TYPE_AUDIO,\n\t\t\t\t\tother =\u003e {\n\t\t\t\t\t\teprintln!(\"Found unknown track type: {other}\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t// Create a new track\n\t\t\t\tlet mut track_list = CHandle::null();\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_sequence_get_track_list(*sequence, track_type, \u0026mut track_list);\n\t\t\t\t}\n\t\t\t\tlet mut add_track = unsafe { bridge::timeline::oaktimeline_add_track_command(track_list) };\n\t\t\t\tif !add_track.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_track);\n\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_track);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet mut track = CHandle::null();\n\t\t\t\tlet mut count = 0;\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_tracklist_get_track_count(track_list, \u0026mut count);\n\t\t\t\t}\n\t\t\t\tif count \u003e 0 {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_tracklist_get_track_at(track_list, count - 1, \u0026mut track);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif track.ctx.is_null() {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Get clips from track\n\t\t\t\tlet mut previous_block = CHandle::null();\n\t\t\t\tlet mut prev_block_transition = false;\n\n\t\t\t\tfor otio_block in otio_track.children() {\n\t\t\t\t\tif task.is_cancelled() {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet block = match otio_block.schema_name() {\n\t\t\t\t\t\t\"Clip\" =\u003e unsafe { bridge::node::oaknode_block_clip_create() },\n\t\t\t\t\t\t\"Gap\" =\u003e unsafe { bridge::node::oaknode_block_gap_create() },\n\t\t\t\t\t\t\"Transition\" =\u003e {\n\t\t\t\t\t\t\t// Todo: Look into OTIO supported transitions and add\n\t\t\t\t\t\t\t// them to Oak.\n\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\tbridge::node::oaknode_block_transition_create(bridge::node::OAKNODE_TRANSITION_CROSS_DISSOLVE)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tother =\u003e {\n\t\t\t\t\t\t\t// We don't know what this is yet, just create a gap\n\t\t\t\t\t\t\t// for now so that *something* is there.\n\t\t\t\t\t\t\teprintln!(\"Found unknown block type: {other}\");\n\t\t\t\t\t\t\tunsafe { bridge::node::oaknode_block_gap_create() }\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tif block.ctx.is_null() {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet block_node = unsafe { bridge::node::oaknode_block_as_node(block) };\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_project_add_node(project, block_node);\n\t\t\t\t\t\tbridge::node::oaknode_node_set_label(block_node, cstr(otio_block.name()));\n\t\t\t\t\t\tbridge::node::oaknode_track_append_block(track, block);\n\t\t\t\t\t}\n\n\t\t\t\t\tif otio_block.schema_name() == \"Clip\" || otio_block.schema_name() == \"Gap\" {\n\t\t\t\t\t\tif let Some(source_range) = otio_block.source_range() {\n\t\t\t\t\t\t\tlet start_seconds = source_range.start_time().to_seconds();\n\t\t\t\t\t\t\tlet duration_seconds = source_range.duration().to_seconds();\n\n\t\t\t\t\t\t\tlet start_time = oakcore_rs::Rational::from_double(start_seconds);\n\t\t\t\t\t\t\tlet duration = oakcore_rs::Rational::from_double(duration_seconds);\n\n\t\t\t\t\t\t\tif otio_block.schema_name() == \"Clip\" {\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\tbridge::node::oaknode_clip_set_media_in(\n\t\t\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\t\t\tstart_time.numerator() as i32,\n\t\t\t\t\t\t\t\t\t\tstart_time.denominator() as i32,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\tbridge::node::oaknode_block_set_length_and_media_out(\n\t\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\t\tduration.numerator() as i32,\n\t\t\t\t\t\t\t\t\tduration.denominator() as i32,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// If the previous block was a transition, connect the\n\t\t\t\t\t// current block to it.\n\t\t\t\t\tif prev_block_transition {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\tblock_node,\n\t\t\t\t\t\t\t\tbridge::node::oaknode_block_as_node(previous_block),\n\t\t\t\t\t\t\t\tcstr(bridge::node::OAKNODE_TRANSITION_IN_BLOCK_INPUT),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprev_block_transition = false;\n\t\t\t\t\t}\n\n\t\t\t\t\tif let Some(otio_transition) = otio_block.as_transition() {\n\t\t\t\t\t\t// Set how far the transition eats into the previous\n\t\t\t\t\t\t// clip.\n\t\t\t\t\t\tlet in_offset = otio_transition.in_offset().to_rational();\n\t\t\t\t\t\tlet out_offset = otio_transition.out_offset().to_rational();\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::node::oaknode_transition_set_offsets_and_length(\n\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\tin_offset.numerator() as i32,\n\t\t\t\t\t\t\t\tin_offset.denominator() as i32,\n\t\t\t\t\t\t\t\tout_offset.numerator() as i32,\n\t\t\t\t\t\t\t\tout_offset.denominator() as i32,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !previous_block.ctx.is_null() {\n\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\t\tbridge::node::oaknode_block_as_node(previous_block),\n\t\t\t\t\t\t\t\t\tblock_node,\n\t\t\t\t\t\t\t\t\tcstr(bridge::node::OAKNODE_TRANSITION_OUT_BLOCK_INPUT),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprev_block_transition = true;\n\n\t\t\t\t\t\t// Position transition in its own context.\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tset_own_context_position(block_node);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif otio_block.schema_name() == \"Gap\" {\n\t\t\t\t\t\t// Position gap in its own context.\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tset_own_context_position(block_node);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update this after it's used but before any continue\n\t\t\t\t\t// statements.\n\t\t\t\t\tprevious_block = block;\n\n\t\t\t\t\tif otio_block.schema_name() == \"Clip\" {\n\t\t\t\t\t\tlet Some(otio_clip) = otio_block.as_clip() else {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tlet Some(media_reference) = otio_clip.media_reference() else {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif let Some(external) = media_reference.as_external_reference() {\n\t\t\t\t\t\t\t// Link footage\n\t\t\t\t\t\t\tlet footage_url = external.target_url().to_string();\n\n\t\t\t\t\t\t\tlet probed_item = if let Some(existing) = imported_footage.get(\u0026footage_url) {\n\t\t\t\t\t\t\t\t*existing\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlet created = unsafe { bridge::node::oaknode_footage_create(project, cstr(\u0026footage_url)) };\n\t\t\t\t\t\t\t\tif !created.ctx.is_null() {\n\t\t\t\t\t\t\t\t\timported_footage.insert(footage_url.clone(), created);\n\n\t\t\t\t\t\t\t\t\tlet label = Path::new(\u0026footage_url)\n\t\t\t\t\t\t\t\t\t\t.file_name()\n\t\t\t\t\t\t\t\t\t\t.map(|n| n.to_string_lossy().into_owned())\n\t\t\t\t\t\t\t\t\t\t.unwrap_or_default();\n\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_label(\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(created),\n\t\t\t\t\t\t\t\t\t\t\tcstr(\u0026label),\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif !sequence_footage.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\tlet mut add_footage = unsafe {\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_command_create_folder_add_child(\n\t\t\t\t\t\t\t\t\t\t\t\tsequence_footage,\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(created),\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\tif !add_footage.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_footage);\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_footage);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcreated\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\tif !probed_item.ctx.is_null() {\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t// Position clip in its own context.\n\t\t\t\t\t\t\t\t\tset_own_context_position(block_node);\n\n\t\t\t\t\t\t\t\t\t// Position footage in its context.\n\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_context_position(\n\t\t\t\t\t\t\t\t\t\tblock_node,\n\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(probed_item),\n\t\t\t\t\t\t\t\t\t\t-2.0,\n\t\t\t\t\t\t\t\t\t\t0.0,\n\t\t\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {\n\t\t\t\t\t\t\t\t\tlet transform = unsafe {\n\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_TRANSFORM))\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\tif !transform.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_project_add_node(project, transform);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(probed_item),\n\t\t\t\t\t\t\t\t\t\t\t\ttransform,\n\t\t\t\t\t\t\t\t\t\t\t\tcstr(\"tex_in\"),\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(transform, block_node, cstr(\"buffer_in\"));\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_context_position(block_node, transform, -1.0, 0.0, 0);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet volume =\n\t\t\t\t\t\t\t\t\t\tunsafe { bridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_VOLUME)) };\n\t\t\t\t\t\t\t\t\tif !volume.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_project_add_node(project, volume);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(probed_item),\n\t\t\t\t\t\t\t\t\t\t\t\tvolume,\n\t\t\t\t\t\t\t\t\t\t\t\tcstr(\"samples_in\"),\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(volume, block_node, cstr(\"buffer_in\"));\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_context_position(block_node, volume, -1.0, 0.0, 0);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tclips_done += 1.0;\n\t\t\t\t\ttask.emit_progress(clips_done / number_of_clips);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tself.base.store_project(project);\n\t\tOk(())\n\t}\n}\n\n/// Set the node's position in its own context (the C++\n/// `set_own_context_position` helper in loadotio.cpp).\nunsafe fn set_own_context_position(node: CHandle) {\n\tunsafe {\n\t\tbridge::node::oaknode_node_set_context_position(node, node, 0.0, 0.0, 0);\n\t}\n}\n\n/// Parse the document into one `oakotio::Timeline` per sequence, dispatching\n/// on the filename extension:\n///\n/// - `.otio`: OpenTimelineIO JSON. A `Timeline` root yields one timeline, a\n/// `SerializableCollection` yields one timeline per `Timeline` child, and\n/// any other root is the C++ \"Unknown OpenTimelineIO root element\" error.\n/// - `.fcpxml`: FCPXML (`oakotio::fcpxml`), one timeline per `\u003csequence\u003e`.\n///\n/// Format handling ends here — the caller builds the project from the\n/// returned timelines regardless of the source format (C++ parity), so the\n/// track/clip/footage code never forks.\nfn parse_timelines(task: \u0026mut Task, filename: \u0026str, format: InterchangeFormat) -\u003e Result\u003cVec\u003coakotio::Timeline\u003e\u003e {\n\tmatch format {\n\t\tInterchangeFormat::OtioJson =\u003e {\n\t\t\tlet root = oakotio::from_json_file(filename).map_err(|e| {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Failed to load OpenTimelineIO from file \\\"{}\\\" \\n\\nOpenTimelineIO Error:\\n\\n{}\",\n\t\t\t\t\tfilename, e\n\t\t\t\t));\n\t\t\t\tError::Failed(\"Failed to load OpenTimelineIO\".to_string())\n\t\t\t})?;\n\t\t\tmatch \u0026root {\n\t\t\t\tSerializable::SerializableCollection(collection) =\u003e Ok(collection\n\t\t\t\t\t.children()\n\t\t\t\t\t.iter()\n\t\t\t\t\t.filter_map(|child| child.as_timeline().cloned())\n\t\t\t\t\t.collect()),\n\t\t\t\tSerializable::Timeline(timeline) =\u003e Ok(vec![timeline.clone()]),\n\t\t\t\t_ =\u003e {\n\t\t\t\t\ttask.set_error(\"Unknown OpenTimelineIO root element\");\n\t\t\t\t\tErr(Error::Failed(\"Unknown OpenTimelineIO root element\".to_string()))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tInterchangeFormat::Fcpxml =\u003e oakotio::from_fcpxml_file(filename).map_err(|e| {\n\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\"Failed to load FCPXML from file \\\"{}\\\" \\n\\nFCPXML Error:\\n\\n{}\",\n\t\t\t\tfilename, e\n\t\t\t));\n\t\t\tError::Failed(\"Failed to load FCPXML\".to_string())\n\t\t}),\n\t}\n}\n\n/// Two-stage read of a node's label (the C++ `oaknode_node_get_label` usage).\nfn node_label_of(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);\n\t}\n\tcrate::project::load::buf_to_string(\u0026buf)\n}\n","traces":[{"line":57,"address":[],"length":0,"stats":{"Line":7}},{"line":58,"address":[],"length":0,"stats":{"Line":21}},{"line":69,"address":[],"length":0,"stats":{"Line":20}},{"line":77,"address":[],"length":0,"stats":{"Line":20}},{"line":83,"address":[],"length":0,"stats":{"Line":39}},{"line":84,"address":[],"length":0,"stats":{"Line":38}},{"line":85,"address":[],"length":0,"stats":{"Line":1}},{"line":86,"address":[],"length":0,"stats":{"Line":3}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":89,"address":[],"length":0,"stats":{"Line":1}},{"line":93,"address":[],"length":0,"stats":{"Line":95}},{"line":95,"address":[],"length":0,"stats":{"Line":28}},{"line":96,"address":[],"length":0,"stats":{"Line":28}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":42}},{"line":102,"address":[],"length":0,"stats":{"Line":14}},{"line":106,"address":[],"length":0,"stats":{"Line":42}},{"line":110,"address":[],"length":0,"stats":{"Line":28}},{"line":111,"address":[],"length":0,"stats":{"Line":42}},{"line":114,"address":[],"length":0,"stats":{"Line":42}},{"line":116,"address":[],"length":0,"stats":{"Line":29}},{"line":117,"address":[],"length":0,"stats":{"Line":30}},{"line":118,"address":[],"length":0,"stats":{"Line":30}},{"line":119,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":30}},{"line":123,"address":[],"length":0,"stats":{"Line":30}},{"line":127,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":60}},{"line":136,"address":[],"length":0,"stats":{"Line":15}},{"line":140,"address":[],"length":0,"stats":{"Line":44}},{"line":141,"address":[],"length":0,"stats":{"Line":42}},{"line":142,"address":[],"length":0,"stats":{"Line":14}},{"line":146,"address":[],"length":0,"stats":{"Line":45}},{"line":148,"address":[],"length":0,"stats":{"Line":15}},{"line":149,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":42}},{"line":156,"address":[],"length":0,"stats":{"Line":59}},{"line":158,"address":[],"length":0,"stats":{"Line":42}},{"line":159,"address":[],"length":0,"stats":{"Line":28}},{"line":160,"address":[],"length":0,"stats":{"Line":4}},{"line":161,"address":[],"length":0,"stats":{"Line":12}},{"line":164,"address":[],"length":0,"stats":{"Line":14}},{"line":169,"address":[],"length":0,"stats":{"Line":2}},{"line":170,"address":[],"length":0,"stats":{"Line":2}},{"line":172,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":177,"address":[],"length":0,"stats":{"Line":1}},{"line":180,"address":[],"length":0,"stats":{"Line":1}},{"line":183,"address":[],"length":0,"stats":{"Line":39}},{"line":184,"address":[],"length":0,"stats":{"Line":26}},{"line":186,"address":[],"length":0,"stats":{"Line":67}},{"line":187,"address":[],"length":0,"stats":{"Line":42}},{"line":190,"address":[],"length":0,"stats":{"Line":28}},{"line":192,"address":[],"length":0,"stats":{"Line":56}},{"line":193,"address":[],"length":0,"stats":{"Line":14}},{"line":195,"address":[],"length":0,"stats":{"Line":42}},{"line":196,"address":[],"length":0,"stats":{"Line":14}},{"line":201,"address":[],"length":0,"stats":{"Line":42}},{"line":202,"address":[],"length":0,"stats":{"Line":14}},{"line":205,"address":[],"length":0,"stats":{"Line":42}},{"line":206,"address":[],"length":0,"stats":{"Line":28}},{"line":211,"address":[],"length":0,"stats":{"Line":14}},{"line":212,"address":[],"length":0,"stats":{"Line":28}},{"line":215,"address":[],"length":0,"stats":{"Line":14}},{"line":217,"address":[],"length":0,"stats":{"Line":42}},{"line":218,"address":[],"length":0,"stats":{"Line":14}},{"line":224,"address":[],"length":0,"stats":{"Line":41}},{"line":225,"address":[],"length":0,"stats":{"Line":26}},{"line":226,"address":[],"length":0,"stats":{"Line":0}},{"line":230,"address":[],"length":0,"stats":{"Line":25}},{"line":231,"address":[],"length":0,"stats":{"Line":25}},{"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":240,"address":[],"length":0,"stats":{"Line":24}},{"line":242,"address":[],"length":0,"stats":{"Line":36}},{"line":244,"address":[],"length":0,"stats":{"Line":36}},{"line":245,"address":[],"length":0,"stats":{"Line":12}},{"line":247,"address":[],"length":0,"stats":{"Line":36}},{"line":248,"address":[],"length":0,"stats":{"Line":12}},{"line":252,"address":[],"length":0,"stats":{"Line":24}},{"line":253,"address":[],"length":0,"stats":{"Line":24}},{"line":255,"address":[],"length":0,"stats":{"Line":24}},{"line":257,"address":[],"length":0,"stats":{"Line":12}},{"line":259,"address":[],"length":0,"stats":{"Line":36}},{"line":262,"address":[],"length":0,"stats":{"Line":24}},{"line":263,"address":[],"length":0,"stats":{"Line":0}},{"line":267,"address":[],"length":0,"stats":{"Line":24}},{"line":268,"address":[],"length":0,"stats":{"Line":24}},{"line":270,"address":[],"length":0,"stats":{"Line":47}},{"line":271,"address":[],"length":0,"stats":{"Line":46}},{"line":272,"address":[],"length":0,"stats":{"Line":0}},{"line":275,"address":[],"length":0,"stats":{"Line":46}},{"line":276,"address":[],"length":0,"stats":{"Line":36}},{"line":277,"address":[],"length":0,"stats":{"Line":19}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":282,"address":[],"length":0,"stats":{"Line":1}},{"line":285,"address":[],"length":0,"stats":{"Line":0}},{"line":288,"address":[],"length":0,"stats":{"Line":0}},{"line":289,"address":[],"length":0,"stats":{"Line":0}},{"line":292,"address":[],"length":0,"stats":{"Line":46}},{"line":293,"address":[],"length":0,"stats":{"Line":0}},{"line":296,"address":[],"length":0,"stats":{"Line":69}},{"line":298,"address":[],"length":0,"stats":{"Line":92}},{"line":299,"address":[],"length":0,"stats":{"Line":115}},{"line":300,"address":[],"length":0,"stats":{"Line":46}},{"line":303,"address":[],"length":0,"stats":{"Line":33}},{"line":304,"address":[],"length":0,"stats":{"Line":44}},{"line":305,"address":[],"length":0,"stats":{"Line":88}},{"line":306,"address":[],"length":0,"stats":{"Line":88}},{"line":308,"address":[],"length":0,"stats":{"Line":66}},{"line":309,"address":[],"length":0,"stats":{"Line":66}},{"line":311,"address":[],"length":0,"stats":{"Line":22}},{"line":314,"address":[],"length":0,"stats":{"Line":26}},{"line":315,"address":[],"length":0,"stats":{"Line":26}},{"line":316,"address":[],"length":0,"stats":{"Line":13}},{"line":322,"address":[],"length":0,"stats":{"Line":44}},{"line":323,"address":[],"length":0,"stats":{"Line":44}},{"line":324,"address":[],"length":0,"stats":{"Line":22}},{"line":332,"address":[],"length":0,"stats":{"Line":24}},{"line":335,"address":[],"length":0,"stats":{"Line":2}},{"line":336,"address":[],"length":0,"stats":{"Line":3}},{"line":337,"address":[],"length":0,"stats":{"Line":1}},{"line":340,"address":[],"length":0,"stats":{"Line":1}},{"line":343,"address":[],"length":0,"stats":{"Line":24}},{"line":346,"address":[],"length":0,"stats":{"Line":4}},{"line":347,"address":[],"length":0,"stats":{"Line":4}},{"line":350,"address":[],"length":0,"stats":{"Line":2}},{"line":351,"address":[],"length":0,"stats":{"Line":2}},{"line":352,"address":[],"length":0,"stats":{"Line":2}},{"line":353,"address":[],"length":0,"stats":{"Line":2}},{"line":354,"address":[],"length":0,"stats":{"Line":1}},{"line":358,"address":[],"length":0,"stats":{"Line":1}},{"line":361,"address":[],"length":0,"stats":{"Line":3}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":363,"address":[],"length":0,"stats":{"Line":1}},{"line":367,"address":[],"length":0,"stats":{"Line":1}},{"line":371,"address":[],"length":0,"stats":{"Line":1}},{"line":375,"address":[],"length":0,"stats":{"Line":23}},{"line":378,"address":[],"length":0,"stats":{"Line":9}},{"line":384,"address":[],"length":0,"stats":{"Line":23}},{"line":386,"address":[],"length":0,"stats":{"Line":23}},{"line":387,"address":[],"length":0,"stats":{"Line":26}},{"line":388,"address":[],"length":0,"stats":{"Line":0}},{"line":390,"address":[],"length":0,"stats":{"Line":24}},{"line":391,"address":[],"length":0,"stats":{"Line":2}},{"line":393,"address":[],"length":0,"stats":{"Line":21}},{"line":395,"address":[],"length":0,"stats":{"Line":30}},{"line":397,"address":[],"length":0,"stats":{"Line":31}},{"line":398,"address":[],"length":0,"stats":{"Line":1}},{"line":400,"address":[],"length":0,"stats":{"Line":45}},{"line":401,"address":[],"length":0,"stats":{"Line":9}},{"line":402,"address":[],"length":0,"stats":{"Line":45}},{"line":404,"address":[],"length":0,"stats":{"Line":18}},{"line":406,"address":[],"length":0,"stats":{"Line":36}},{"line":410,"address":[],"length":0,"stats":{"Line":27}},{"line":411,"address":[],"length":0,"stats":{"Line":9}},{"line":415,"address":[],"length":0,"stats":{"Line":9}},{"line":418,"address":[],"length":0,"stats":{"Line":9}},{"line":419,"address":[],"length":0,"stats":{"Line":18}},{"line":422,"address":[],"length":0,"stats":{"Line":9}},{"line":424,"address":[],"length":0,"stats":{"Line":27}},{"line":425,"address":[],"length":0,"stats":{"Line":9}},{"line":430,"address":[],"length":0,"stats":{"Line":9}},{"line":433,"address":[],"length":0,"stats":{"Line":10}},{"line":436,"address":[],"length":0,"stats":{"Line":30}},{"line":440,"address":[],"length":0,"stats":{"Line":20}},{"line":441,"address":[],"length":0,"stats":{"Line":10}},{"line":442,"address":[],"length":0,"stats":{"Line":10}},{"line":448,"address":[],"length":0,"stats":{"Line":10}},{"line":450,"address":[],"length":0,"stats":{"Line":30}},{"line":452,"address":[],"length":0,"stats":{"Line":10}},{"line":454,"address":[],"length":0,"stats":{"Line":40}},{"line":456,"address":[],"length":0,"stats":{"Line":30}},{"line":457,"address":[],"length":0,"stats":{"Line":20}},{"line":458,"address":[],"length":0,"stats":{"Line":30}},{"line":460,"address":[],"length":0,"stats":{"Line":60}},{"line":461,"address":[],"length":0,"stats":{"Line":20}},{"line":465,"address":[],"length":0,"stats":{"Line":0}},{"line":466,"address":[],"length":0,"stats":{"Line":0}},{"line":467,"address":[],"length":0,"stats":{"Line":0}},{"line":469,"address":[],"length":0,"stats":{"Line":0}},{"line":471,"address":[],"length":0,"stats":{"Line":0}},{"line":472,"address":[],"length":0,"stats":{"Line":0}},{"line":473,"address":[],"length":0,"stats":{"Line":0}},{"line":475,"address":[],"length":0,"stats":{"Line":0}},{"line":476,"address":[],"length":0,"stats":{"Line":0}},{"line":484,"address":[],"length":0,"stats":{"Line":21}},{"line":485,"address":[],"length":0,"stats":{"Line":63}},{"line":490,"address":[],"length":0,"stats":{"Line":39}},{"line":491,"address":[],"length":0,"stats":{"Line":13}},{"line":497,"address":[],"length":0,"stats":{"Line":20}},{"line":499,"address":[],"length":0,"stats":{"Line":40}},{"line":514,"address":[],"length":0,"stats":{"Line":19}},{"line":515,"address":[],"length":0,"stats":{"Line":19}},{"line":517,"address":[],"length":0,"stats":{"Line":52}},{"line":518,"address":[],"length":0,"stats":{"Line":6}},{"line":519,"address":[],"length":0,"stats":{"Line":2}},{"line":520,"address":[],"length":0,"stats":{"Line":2}},{"line":522,"address":[],"length":0,"stats":{"Line":2}},{"line":524,"address":[],"length":0,"stats":{"Line":11}},{"line":525,"address":[],"length":0,"stats":{"Line":2}},{"line":526,"address":[],"length":0,"stats":{"Line":1}},{"line":527,"address":[],"length":0,"stats":{"Line":1}},{"line":528,"address":[],"length":0,"stats":{"Line":7}},{"line":529,"address":[],"length":0,"stats":{"Line":1}},{"line":530,"address":[],"length":0,"stats":{"Line":27}},{"line":532,"address":[],"length":0,"stats":{"Line":3}},{"line":533,"address":[],"length":0,"stats":{"Line":1}},{"line":537,"address":[],"length":0,"stats":{"Line":20}},{"line":538,"address":[],"length":0,"stats":{"Line":6}},{"line":539,"address":[],"length":0,"stats":{"Line":2}},{"line":540,"address":[],"length":0,"stats":{"Line":2}},{"line":542,"address":[],"length":0,"stats":{"Line":2}},{"line":548,"address":[],"length":0,"stats":{"Line":15}},{"line":549,"address":[],"length":0,"stats":{"Line":60}},{"line":550,"address":[],"length":0,"stats":{"Line":15}},{"line":551,"address":[],"length":0,"stats":{"Line":0}},{"line":553,"address":[],"length":0,"stats":{"Line":45}},{"line":555,"address":[],"length":0,"stats":{"Line":60}},{"line":557,"address":[],"length":0,"stats":{"Line":30}}],"covered":201,"coverable":224},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","save.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//! `ProjectSaveTask`, mirroring `src/task/src/project/save/save.h`.\n//!\n//! Serializes a borrowed `OakNodeProject` to an `.oakproj` file via the\n//! oaknode serializer. Optionally writes to an override filename and/or uses\n//! compression.\n//!\n//! CPP-PARITY: src/task/src/project/save/save.h\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::project::load::buf_to_string;\nuse crate::task::{Task, TaskBehavior};\n\n/// Serializer result codes (`include/node/serializer.h`).\nconst OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0;\nconst OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5;\nconst OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4;\nconst OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR: i32 = 6;\n\n/// A project-save task. Borrows its project and never takes ownership.\npub struct ProjectSaveTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Borrowed project to save (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Optional override filename; when empty the project's own filename is\n\t/// used.\n\tpub override_filename: Option\u003cString\u003e,\n\t/// Whether to compress the serialized output.\n\tpub use_compression: bool,\n}\n\nimpl ProjectSaveTask {\n\t/// Set the output filename override.\n\tpub fn set_override_filename(\u0026mut self, filename: \u0026str) {\n\t\tself.override_filename = Some(filename.to_string());\n\t}\n}\n\nimpl TaskBehavior for ProjectSaveTask {\n\t/// Serialize the project via the oaknode serializer (`bridge::node`) to\n\t/// the resolved filename.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet using_filename = match \u0026self.override_filename {\n\t\t\tSome(name) =\u003e name.clone(),\n\t\t\tNone =\u003e project_filename(self.project),\n\t\t};\n\n\t\tif using_filename.is_empty() {\n\t\t\ttask.set_error(\"Project has no filename to save to.\");\n\t\t\treturn Err(Error::Failed(\"Project has no filename to save to.\".to_string()));\n\t\t}\n\n\t\tlet mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;\n\t\tlet mut details = [0i8; 512];\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_serializer_save_to_file(\n\t\t\t\tself.project,\n\t\t\t\tcstr(\u0026using_filename),\n\t\t\t\tif self.use_compression { 1 } else { 0 },\n\t\t\t\t\u0026mut code,\n\t\t\t\tdetails.as_mut_ptr(),\n\t\t\t\tdetails.len() as i32,\n\t\t\t);\n\t\t}\n\n\t\tlet mut success = false;\n\t\tmatch code {\n\t\t\tOAKNODE_SERIALIZER_RESULT_SUCCESS =\u003e success = true,\n\t\t\tOAKNODE_SERIALIZER_RESULT_XML_ERROR =\u003e {\n\t\t\t\ttask.set_error(\"Failed to write XML data.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_FILE_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\"Failed to open file for writing: {}\", buf_to_string(\u0026details)));\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Failed to overwrite \\\"{}\\\". Project has been saved as \\\"{}\\\" instead.\",\n\t\t\t\t\tusing_filename,\n\t\t\t\t\tbuf_to_string(\u0026details)\n\t\t\t\t));\n\t\t\t\tsuccess = true;\n\t\t\t}\n\t\t\t_ =\u003e task.set_error(\"Unknown error.\"),\n\t\t}\n\n\t\tif success {\n\t\t\tOk(())\n\t\t} else {\n\t\t\tErr(Error::Failed(\"Failed to save project\".to_string()))\n\t\t}\n\t}\n}\n\n/// Two-stage read of the project's own filename (empty when unset).\npub(crate) fn project_filename(project: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_project_filename(project, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_project_filename(project, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n","traces":[{"line":53,"address":[],"length":0,"stats":{"Line":3}},{"line":54,"address":[],"length":0,"stats":{"Line":6}},{"line":61,"address":[],"length":0,"stats":{"Line":9}},{"line":62,"address":[],"length":0,"stats":{"Line":18}},{"line":63,"address":[],"length":0,"stats":{"Line":6}},{"line":64,"address":[],"length":0,"stats":{"Line":14}},{"line":67,"address":[],"length":0,"stats":{"Line":18}},{"line":68,"address":[],"length":0,"stats":{"Line":3}},{"line":69,"address":[],"length":0,"stats":{"Line":1}},{"line":72,"address":[],"length":0,"stats":{"Line":16}},{"line":73,"address":[],"length":0,"stats":{"Line":16}},{"line":76,"address":[],"length":0,"stats":{"Line":8}},{"line":77,"address":[],"length":0,"stats":{"Line":16}},{"line":78,"address":[],"length":0,"stats":{"Line":16}},{"line":79,"address":[],"length":0,"stats":{"Line":8}},{"line":80,"address":[],"length":0,"stats":{"Line":16}},{"line":81,"address":[],"length":0,"stats":{"Line":8}},{"line":85,"address":[],"length":0,"stats":{"Line":16}},{"line":86,"address":[],"length":0,"stats":{"Line":8}},{"line":87,"address":[],"length":0,"stats":{"Line":8}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":0}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":102,"address":[],"length":0,"stats":{"Line":0}},{"line":105,"address":[],"length":0,"stats":{"Line":8}},{"line":106,"address":[],"length":0,"stats":{"Line":8}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":114,"address":[],"length":0,"stats":{"Line":22}},{"line":115,"address":[],"length":0,"stats":{"Line":88}},{"line":116,"address":[],"length":0,"stats":{"Line":22}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":119,"address":[],"length":0,"stats":{"Line":66}},{"line":121,"address":[],"length":0,"stats":{"Line":88}},{"line":123,"address":[],"length":0,"stats":{"Line":44}}],"covered":28,"coverable":41},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","saveotio.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//! `SaveOTIOTask`, mirroring `src/task/src/project/saveotio/saveotio.h`.\n//!\n//! Serializes a borrowed `OakNodeProject` to an OpenTimelineIO (`.otio`) or\n//! FCPXML (`.fcpxml`) file through the pure-Rust `oakotio` binding (see\n//! `README` decision #6): the project's sequences become `OTIO::Timeline`s\n//! (`serialize_timeline` / `serialize_track` / `serialize_track_list`),\n//! exactly like the C++ `serialize_*` helpers. The format is dispatched\n//! from the filename extension (see [`crate::project::format`]) at the\n//! final write only — the serialization itself is shared. No OTIO or\n//! FCPXML type crosses the oaktask C ABI.\n//!\n//! CPP-PARITY: src/task/src/project/saveotio/saveotio.cpp\n\nuse oakcore_rs::Rational;\nuse oakotio::{\n\tClip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable, SerializableCollection, TimeRange,\n\tTimeline, Track, Transition,\n};\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::project::format::InterchangeFormat;\nuse crate::project::load::buf_to_string;\nuse crate::task::{Task, TaskBehavior};\n\n/// `Rational(INT_MIN)` — the initial \"no track yet\" maximum length\n/// (`RATIONAL_MIN` in saveotio.cpp).\nfn rational_min() -\u003e Rational {\n\tRational::new(-2147483647, 1)\n}\n\n/// An OTIO project-save task. Borrows its project.\npub struct SaveOTIOTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Borrowed project to save (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Output OTIO filename.\n\tpub filename: String,\n}\n\nimpl SaveOTIOTask {\n\t/// Serialize one sequence into an `OTIO::Timeline`; `None` when the\n\t/// sequence has no usable frame rate or a track fails to serialize.\n\t///\n\t/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_timeline)\n\tfn serialize_timeline(sequence: CHandle) -\u003e Option\u003cTimeline\u003e {\n\t\tlet mut otio_timeline = Timeline::new(node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(sequence) }));\n\n\t\tlet mut rate = 0.0f64;\n\t\t{\n\t\t\tlet mut num = 0;\n\t\t\tlet mut den = 1;\n\t\t\tlet mut vp = CHandle::null();\n\t\t\tif unsafe { bridge::node::oaknode_sequence_get_video_params(sequence, 0, \u0026mut vp) } == 0 {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::common::oakcommon_videoparams_get_frame_rate(vp, \u0026mut num, \u0026mut den);\n\t\t\t\t}\n\t\t\t\tif den != 0 {\n\t\t\t\t\trate = num as f64 / den as f64;\n\t\t\t\t}\n\t\t\t\tif !vp.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut vp);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif rate.is_nan() || rate \u003c= 0.0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tlet mut video_list = CHandle::null();\n\t\tlet mut audio_list = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_VIDEO, \u0026mut video_list);\n\t\t\tbridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_AUDIO, \u0026mut audio_list);\n\t\t}\n\n\t\tif !Self::serialize_track_list(video_list, \u0026mut otio_timeline, rate)\n\t\t\t|| !Self::serialize_track_list(audio_list, \u0026mut otio_timeline, rate)\n\t\t{\n\t\t\treturn None;\n\t\t}\n\n\t\tSome(otio_timeline)\n\t}\n\n\t/// Serialize all tracks of one track list (video or audio) into the\n\t/// timeline. Each track is padded to the list's maximum track length.\n\t///\n\t/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list)\n\tfn serialize_track_list(list: CHandle, otio_timeline: \u0026mut Timeline, sequence_rate: f64) -\u003e bool {\n\t\tif list.ctx.is_null() {\n\t\t\treturn true;\n\t\t}\n\n\t\tlet mut max_track_length = rational_min();\n\n\t\tlet mut track_count = 0;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_tracklist_get_track_count(list, \u0026mut track_count);\n\t\t}\n\n\t\tfor i in 0..track_count {\n\t\t\tlet mut track = CHandle::null();\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_tracklist_get_track_at(list, i, \u0026mut track);\n\t\t\t}\n\t\t\tif !track.ctx.is_null() {\n\t\t\t\tlet length = track_length_of(track);\n\t\t\t\tif length \u003e max_track_length {\n\t\t\t\t\tmax_track_length = length;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor i in 0..track_count {\n\t\t\tlet mut track = CHandle::null();\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_tracklist_get_track_at(list, i, \u0026mut track);\n\t\t\t}\n\t\t\tif track.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length) else {\n\t\t\t\treturn false;\n\t\t\t};\n\n\t\t\totio_timeline.tracks_mut().append_child(Composable::Track(otio_track));\n\t\t}\n\n\t\ttrue\n\t}\n\n\t/// Serialize one track: a `Track` whose kind matches the native track\n\t/// type, one OTIO block per native block, plus a trailing `Gap` when the\n\t/// track is shorter than `max_track_length`.\n\t///\n\t/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)\n\tfn serialize_track(track: CHandle, sequence_rate: f64, max_track_length: Rational) -\u003e Option\u003cTrack\u003e {\n\t\tlet mut track_type = bridge::node::OAKNODE_TRACK_TYPE_NONE;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_track_get_type(track, \u0026mut track_type);\n\t\t}\n\n\t\tlet kind = match track_type {\n\t\t\tbridge::node::OAKNODE_TRACK_TYPE_VIDEO =\u003e \"Video\",\n\t\t\tbridge::node::OAKNODE_TRACK_TYPE_AUDIO =\u003e \"Audio\",\n\t\t\tother =\u003e {\n\t\t\t\teprintln!(\"Don't know OTIO track kind for native type {other}\");\n\t\t\t\treturn None;\n\t\t\t}\n\t\t};\n\t\tlet mut otio_track = Track::new(kind);\n\n\t\tlet mut block_count = 0;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_track_get_block_count(track, \u0026mut block_count);\n\t\t}\n\n\t\tfor i in 0..block_count {\n\t\t\tlet mut block = CHandle::null();\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_track_get_block_at(track, i, \u0026mut block);\n\t\t\t}\n\t\t\tif block.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet mut kind = bridge::node::OAKNODE_BLOCK_OTHER;\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_block_get_kind(block, \u0026mut kind);\n\t\t\t}\n\n\t\t\tlet otio_block: Option\u003cComposable\u003e = match kind {\n\t\t\t\tbridge::node::OAKNODE_BLOCK_CLIP =\u003e {\n\t\t\t\t\tlet mut otio_clip = Clip::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));\n\n\t\t\t\t\totio_clip.set_source_range(TimeRange::new(\n\t\t\t\t\t\tRationalTime::from_rational(block_in_of(block), sequence_rate),\n\t\t\t\t\t\tRationalTime::from_rational(block_length_of(block), sequence_rate),\n\t\t\t\t\t));\n\n\t\t\t\t\tlet mut media = CHandle::null();\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_node_find_input_footage(bridge::node::oaknode_block_as_node(block), \u0026mut media);\n\t\t\t\t\t}\n\t\t\t\t\tif !media.ctx.is_null() {\n\t\t\t\t\t\tlet available_range = if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {\n\t\t\t\t\t\t\t// OTIO ExternalReference uses the source clips\n\t\t\t\t\t\t\t// frame rate (or sample rate) as opposed to the\n\t\t\t\t\t\t\t// sequences rate.\n\t\t\t\t\t\t\tlet mut source_frame_rate = 0.0f64;\n\t\t\t\t\t\t\tlet mut duration = 0.0f64;\n\t\t\t\t\t\t\tlet mut num = 0;\n\t\t\t\t\t\t\tlet mut den = 1;\n\t\t\t\t\t\t\tlet mut vp = CHandle::null();\n\t\t\t\t\t\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(media, 0, \u0026mut vp) } == 0 {\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_get_frame_rate(vp, \u0026mut num, \u0026mut den);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif den != 0 {\n\t\t\t\t\t\t\t\t\tsource_frame_rate = num as f64 / den as f64;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet mut dur = 0i64;\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_get_duration(vp, \u0026mut dur);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tduration = dur as f64;\n\t\t\t\t\t\t\t\tif !vp.ctx.is_null() {\n\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut vp);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tTimeRange::new(\n\t\t\t\t\t\t\t\tRationalTime::new(0.0, source_frame_rate),\n\t\t\t\t\t\t\t\tRationalTime::new(duration, source_frame_rate),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tTimeRange::new(RationalTime::new(0.0, 48000.0), RationalTime::new(0.0, 48000.0))\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tlet media_url = footage_filename(media);\n\t\t\t\t\t\tif !media_url.is_empty() {\n\t\t\t\t\t\t\totio_clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new(\n\t\t\t\t\t\t\t\tmedia_url,\n\t\t\t\t\t\t\t\tSome(available_range),\n\t\t\t\t\t\t\t)));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tSome(Composable::Clip(otio_clip))\n\t\t\t\t}\n\t\t\t\tbridge::node::OAKNODE_BLOCK_GAP =\u003e Some(Composable::Gap(Gap::new(\n\t\t\t\t\tTimeRange::new(\n\t\t\t\t\t\tRationalTime::from_rational(block_in_of(block), 24.0),\n\t\t\t\t\t\tRationalTime::from_rational(block_length_of(block), 24.0),\n\t\t\t\t\t),\n\t\t\t\t\tnode_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }),\n\t\t\t\t))),\n\t\t\t\tbridge::node::OAKNODE_BLOCK_TRANSITION =\u003e {\n\t\t\t\t\tlet mut otio_transition = Transition::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));\n\n\t\t\t\t\tlet (n, d) = transition_offset_of(block, true);\n\t\t\t\t\totio_transition.set_in_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));\n\t\t\t\t\tlet (n, d) = transition_offset_of(block, false);\n\t\t\t\t\totio_transition.set_out_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));\n\n\t\t\t\t\tSome(Composable::Transition(otio_transition))\n\t\t\t\t}\n\t\t\t\t_ =\u003e None,\n\t\t\t};\n\n\t\t\tlet Some(otio_block) = otio_block else {\n\t\t\t\t// We shouldn't ever get here, but catch without crashing if\n\t\t\t\t// we ever do.\n\t\t\t\treturn None;\n\t\t\t};\n\n\t\t\totio_track.append_child(otio_block);\n\t\t}\n\n\t\t// All OTIO tracks must have the same duration so we add a Gap to\n\t\t// fill the remaining time.\n\t\tlet duration = track_duration(\u0026otio_track);\n\t\tlet duration_seconds = duration.clone().to_seconds();\n\t\tif duration_seconds \u003c max_track_length.to_f64() {\n\t\t\tlet time_left = max_track_length.to_f64() - duration_seconds;\n\n\t\t\tlet gap = Gap::new(TimeRange::new(duration, RationalTime::new(time_left, 1.0)), \"\");\n\t\t\totio_track.append_child(Composable::Gap(gap));\n\t\t}\n\n\t\tSome(otio_track)\n\t}\n}\n\nimpl TaskBehavior for SaveOTIOTask {\n\t/// Serialize the project to OTIO/FCPXML via `oakotio` and write the file.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Dispatch the interchange format from the filename extension.\n\t\t// Format handling ends at the write below — the serialization\n\t\t// produces `oakotio::Timeline`s and is shared between `.otio` and\n\t\t// `.fcpxml` (C++ parity for the serialize_* helpers).\n\t\tlet format = match InterchangeFormat::of(\u0026self.filename) {\n\t\t\tOk(f) =\u003e f,\n\t\t\tErr(e) =\u003e {\n\t\t\t\tif let Error::Failed(msg) = \u0026e {\n\t\t\t\t\ttask.set_error(msg);\n\t\t\t\t}\n\t\t\t\treturn Err(e);\n\t\t\t}\n\t\t};\n\n\t\t// Collect sequences from the root folder (non-recursive, matching\n\t\t// the original list_children_of_type behavior closely enough for\n\t\t// OTIO).\n\t\tlet root = unsafe { bridge::node::oaknode_project_root(self.project) };\n\t\tif root.ctx.is_null() {\n\t\t\ttask.set_error(\"Project contains no sequences to export.\");\n\t\t\treturn Err(Error::Failed(\"Project contains no sequences to export.\".to_string()));\n\t\t}\n\n\t\tlet mut sequences: Vec\u003cCHandle\u003e = Vec::new();\n\t\tlet child_count = unsafe { bridge::node::oaknode_folder_child_count(root) };\n\t\tfor i in 0..child_count {\n\t\t\tlet child = unsafe { bridge::node::oaknode_folder_child_at(root, i) };\n\t\t\tif child.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif node_id_of(child) == bridge::node::OAKNODE_TYPE_SEQUENCE {\n\t\t\t\t// Borrowed sequence alias of the child node handle (all\n\t\t\t\t// oaknode handles share the same box layout).\n\t\t\t\tsequences.push(child);\n\t\t\t}\n\t\t}\n\n\t\tif sequences.is_empty() {\n\t\t\ttask.set_error(\"Project contains no sequences to export.\");\n\t\t\treturn Err(Error::Failed(\"Project contains no sequences to export.\".to_string()));\n\t\t}\n\n\t\tlet mut serialized: Vec\u003cTimeline\u003e = Vec::with_capacity(sequences.len());\n\t\tfor sequence in \u0026sequences {\n\t\t\tmatch Self::serialize_timeline(*sequence) {\n\t\t\t\tSome(timeline) =\u003e serialized.push(timeline),\n\t\t\t\tNone =\u003e {\n\t\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\t\"Failed to serialize sequence \\\"{}\\\"\",\n\t\t\t\t\t\tnode_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*sequence) })\n\t\t\t\t\t));\n\t\t\t\t\treturn Err(Error::Failed(\"Failed to serialize sequence\".to_string()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Write the serialized timelines, dispatching on the format: OTIO\n\t\t// JSON writes a `Timeline` root for one sequence or a\n\t\t// `SerializableCollection` for several; FCPXML writes all timelines\n\t\t// into one document (one `\u003cproject\u003e` per timeline).\n\t\tlet result: std::result::Result\u003c(), String\u003e = match format {\n\t\t\tInterchangeFormat::OtioJson =\u003e {\n\t\t\t\tif serialized.len() == 1 {\n\t\t\t\t\tserialized[0].to_json_file(\u0026self.filename).map_err(|e| e.to_string())\n\t\t\t\t} else {\n\t\t\t\t\t// Serialize all into a SerializableCollection.\n\t\t\t\t\tlet collection = SerializableCollection::new(\n\t\t\t\t\t\t\"Sequences\",\n\t\t\t\t\t\tserialized.into_iter().map(Serializable::Timeline).collect(),\n\t\t\t\t\t);\n\t\t\t\t\tSerializable::SerializableCollection(collection)\n\t\t\t\t\t\t.to_json_file(\u0026self.filename)\n\t\t\t\t\t\t.map_err(|e| e.to_string())\n\t\t\t\t}\n\t\t\t}\n\t\t\tInterchangeFormat::Fcpxml =\u003e oakotio::to_fcpxml_file(\u0026serialized, \u0026self.filename).map_err(|e| e.to_string()),\n\t\t};\n\n\t\tresult.map_err(|e| {\n\t\t\tlet (label, what) = match format {\n\t\t\t\tInterchangeFormat::OtioJson =\u003e (\"Failed to save OpenTimelineIO to file\", \"Failed to save OpenTimelineIO\"),\n\t\t\t\tInterchangeFormat::Fcpxml =\u003e (\"Failed to save FCPXML to file\", \"Failed to save FCPXML\"),\n\t\t\t};\n\t\t\ttask.set_error(\u0026format!(\"{label} \\\"{}\\\": {e}\", self.filename));\n\t\t\tError::Failed(what.to_string())\n\t\t})\n\t}\n}\n\n/// Two-stage read of a node's label.\nfn node_label_of(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// Two-stage read of a node's type id.\nfn node_id_of(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_id(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_id(node, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// The block's in point as a `Rational` (default 0/1 when unset).\nfn block_in_of(block: CHandle) -\u003e Rational {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tunsafe {\n\t\tbridge::node::oaknode_block_get_in(block, \u0026mut n, \u0026mut d);\n\t}\n\tRational::new(n as i64, d as i64)\n}\n\n/// The block's length as a `Rational` (default 0/1 when unset).\nfn block_length_of(block: CHandle) -\u003e Rational {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tunsafe {\n\t\tbridge::node::oaknode_block_get_length(block, \u0026mut n, \u0026mut d);\n\t}\n\tRational::new(n as i64, d as i64)\n}\n\n/// The track's total length as a `Rational` (default 0/1 when unset).\nfn track_length_of(track: CHandle) -\u003e Rational {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tunsafe {\n\t\tbridge::node::oaknode_track_get_length(track, \u0026mut n, \u0026mut d);\n\t}\n\tRational::new(n as i64, d as i64)\n}\n\n/// A transition's in (`in_offset == true`) or out offset as a `Rational`.\nfn transition_offset_of(block: CHandle, in_offset: bool) -\u003e (i32, i32) {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tif in_offset {\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_transition_get_in_offset(block, \u0026mut n, \u0026mut d);\n\t\t}\n\t} else {\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_transition_get_out_offset(block, \u0026mut n, \u0026mut d);\n\t\t}\n\t}\n\t(n, d)\n}\n\n/// Two-stage read of a footage's filename (empty when unset).\nfn footage_filename(footage: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// The serialized track's duration as an `OTIO::RationalTime` — the sum of\n/// its children's source-range durations, keeping the first child's rate\n/// (the C++ `otio_track-\u003eduration(\u0026es)` result for contiguous children).\nfn track_duration(track: \u0026Track) -\u003e RationalTime {\n\tlet mut acc: Option\u003cRationalTime\u003e = None;\n\tfor child in track.children() {\n\t\tlet duration = match child.as_clip().and_then(|c| c.source_range()).map(|r| r.duration())\n\t\t\t.or_else(|| child.as_gap().and_then(|g| g.source_range()).map(|r| r.duration()))\n\t\t{\n\t\t\tSome(d) =\u003e d,\n\t\t\t// Transitions occupy no time.\n\t\t\tNone =\u003e RationalTime::new(0.0, 1.0),\n\t\t};\n\t\tmatch acc {\n\t\t\tNone =\u003e acc = Some(duration),\n\t\t\tSome(a) =\u003e {\n\t\t\t\tlet rate = a.rate();\n\t\t\t\tlet value = a.value() + duration.rescaled_to(rate).value();\n\t\t\t\tacc = Some(RationalTime::new(value, rate));\n\t\t\t}\n\t\t}\n\t}\n\tacc.unwrap_or_else(RationalTime::invalid_time)\n}\n","traces":[{"line":45,"address":[],"length":0,"stats":{"Line":12}},{"line":46,"address":[],"length":0,"stats":{"Line":12}},{"line":64,"address":[],"length":0,"stats":{"Line":12}},{"line":65,"address":[],"length":0,"stats":{"Line":60}},{"line":67,"address":[],"length":0,"stats":{"Line":24}},{"line":69,"address":[],"length":0,"stats":{"Line":24}},{"line":70,"address":[],"length":0,"stats":{"Line":24}},{"line":71,"address":[],"length":0,"stats":{"Line":24}},{"line":72,"address":[],"length":0,"stats":{"Line":24}},{"line":74,"address":[],"length":0,"stats":{"Line":36}},{"line":76,"address":[],"length":0,"stats":{"Line":24}},{"line":77,"address":[],"length":0,"stats":{"Line":12}},{"line":79,"address":[],"length":0,"stats":{"Line":12}},{"line":81,"address":[],"length":0,"stats":{"Line":12}},{"line":86,"address":[],"length":0,"stats":{"Line":36}},{"line":87,"address":[],"length":0,"stats":{"Line":1}},{"line":90,"address":[],"length":0,"stats":{"Line":22}},{"line":91,"address":[],"length":0,"stats":{"Line":22}},{"line":93,"address":[],"length":0,"stats":{"Line":44}},{"line":94,"address":[],"length":0,"stats":{"Line":22}},{"line":97,"address":[],"length":0,"stats":{"Line":33}},{"line":98,"address":[],"length":0,"stats":{"Line":30}},{"line":100,"address":[],"length":0,"stats":{"Line":1}},{"line":103,"address":[],"length":0,"stats":{"Line":10}},{"line":110,"address":[],"length":0,"stats":{"Line":21}},{"line":111,"address":[],"length":0,"stats":{"Line":42}},{"line":112,"address":[],"length":0,"stats":{"Line":9}},{"line":115,"address":[],"length":0,"stats":{"Line":24}},{"line":117,"address":[],"length":0,"stats":{"Line":24}},{"line":119,"address":[],"length":0,"stats":{"Line":24}},{"line":122,"address":[],"length":0,"stats":{"Line":24}},{"line":123,"address":[],"length":0,"stats":{"Line":24}},{"line":125,"address":[],"length":0,"stats":{"Line":36}},{"line":127,"address":[],"length":0,"stats":{"Line":12}},{"line":128,"address":[],"length":0,"stats":{"Line":36}},{"line":129,"address":[],"length":0,"stats":{"Line":24}},{"line":130,"address":[],"length":0,"stats":{"Line":12}},{"line":135,"address":[],"length":0,"stats":{"Line":24}},{"line":136,"address":[],"length":0,"stats":{"Line":24}},{"line":138,"address":[],"length":0,"stats":{"Line":36}},{"line":140,"address":[],"length":0,"stats":{"Line":24}},{"line":141,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":47}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":33}},{"line":151,"address":[],"length":0,"stats":{"Line":11}},{"line":159,"address":[],"length":0,"stats":{"Line":12}},{"line":160,"address":[],"length":0,"stats":{"Line":24}},{"line":162,"address":[],"length":0,"stats":{"Line":24}},{"line":165,"address":[],"length":0,"stats":{"Line":23}},{"line":166,"address":[],"length":0,"stats":{"Line":9}},{"line":167,"address":[],"length":0,"stats":{"Line":2}},{"line":168,"address":[],"length":0,"stats":{"Line":1}},{"line":169,"address":[],"length":0,"stats":{"Line":1}},{"line":170,"address":[],"length":0,"stats":{"Line":1}},{"line":173,"address":[],"length":0,"stats":{"Line":33}},{"line":175,"address":[],"length":0,"stats":{"Line":22}},{"line":177,"address":[],"length":0,"stats":{"Line":22}},{"line":180,"address":[],"length":0,"stats":{"Line":22}},{"line":181,"address":[],"length":0,"stats":{"Line":22}},{"line":183,"address":[],"length":0,"stats":{"Line":33}},{"line":185,"address":[],"length":0,"stats":{"Line":22}},{"line":186,"address":[],"length":0,"stats":{"Line":0}},{"line":189,"address":[],"length":0,"stats":{"Line":22}},{"line":191,"address":[],"length":0,"stats":{"Line":22}},{"line":194,"address":[],"length":0,"stats":{"Line":33}},{"line":196,"address":[],"length":0,"stats":{"Line":50}},{"line":198,"address":[],"length":0,"stats":{"Line":30}},{"line":199,"address":[],"length":0,"stats":{"Line":40}},{"line":200,"address":[],"length":0,"stats":{"Line":40}},{"line":203,"address":[],"length":0,"stats":{"Line":20}},{"line":205,"address":[],"length":0,"stats":{"Line":30}},{"line":207,"address":[],"length":0,"stats":{"Line":10}},{"line":208,"address":[],"length":0,"stats":{"Line":18}},{"line":212,"address":[],"length":0,"stats":{"Line":14}},{"line":213,"address":[],"length":0,"stats":{"Line":14}},{"line":214,"address":[],"length":0,"stats":{"Line":14}},{"line":215,"address":[],"length":0,"stats":{"Line":14}},{"line":216,"address":[],"length":0,"stats":{"Line":14}},{"line":217,"address":[],"length":0,"stats":{"Line":14}},{"line":219,"address":[],"length":0,"stats":{"Line":21}},{"line":221,"address":[],"length":0,"stats":{"Line":14}},{"line":222,"address":[],"length":0,"stats":{"Line":7}},{"line":224,"address":[],"length":0,"stats":{"Line":14}},{"line":226,"address":[],"length":0,"stats":{"Line":14}},{"line":228,"address":[],"length":0,"stats":{"Line":7}},{"line":229,"address":[],"length":0,"stats":{"Line":7}},{"line":231,"address":[],"length":0,"stats":{"Line":7}},{"line":236,"address":[],"length":0,"stats":{"Line":14}},{"line":237,"address":[],"length":0,"stats":{"Line":21}},{"line":240,"address":[],"length":0,"stats":{"Line":6}},{"line":243,"address":[],"length":0,"stats":{"Line":27}},{"line":244,"address":[],"length":0,"stats":{"Line":18}},{"line":245,"address":[],"length":0,"stats":{"Line":36}},{"line":246,"address":[],"length":0,"stats":{"Line":9}},{"line":247,"address":[],"length":0,"stats":{"Line":9}},{"line":252,"address":[],"length":0,"stats":{"Line":10}},{"line":254,"address":[],"length":0,"stats":{"Line":0}},{"line":255,"address":[],"length":0,"stats":{"Line":0}},{"line":256,"address":[],"length":0,"stats":{"Line":0}},{"line":257,"address":[],"length":0,"stats":{"Line":0}},{"line":259,"address":[],"length":0,"stats":{"Line":0}},{"line":262,"address":[],"length":0,"stats":{"Line":5}},{"line":264,"address":[],"length":0,"stats":{"Line":3}},{"line":265,"address":[],"length":0,"stats":{"Line":6}},{"line":266,"address":[],"length":0,"stats":{"Line":3}},{"line":267,"address":[],"length":0,"stats":{"Line":6}},{"line":269,"address":[],"length":0,"stats":{"Line":1}},{"line":271,"address":[],"length":0,"stats":{"Line":0}},{"line":274,"address":[],"length":0,"stats":{"Line":22}},{"line":277,"address":[],"length":0,"stats":{"Line":0}},{"line":280,"address":[],"length":0,"stats":{"Line":33}},{"line":285,"address":[],"length":0,"stats":{"Line":33}},{"line":286,"address":[],"length":0,"stats":{"Line":44}},{"line":287,"address":[],"length":0,"stats":{"Line":23}},{"line":288,"address":[],"length":0,"stats":{"Line":4}},{"line":290,"address":[],"length":0,"stats":{"Line":7}},{"line":291,"address":[],"length":0,"stats":{"Line":2}},{"line":294,"address":[],"length":0,"stats":{"Line":11}},{"line":300,"address":[],"length":0,"stats":{"Line":13}},{"line":305,"address":[],"length":0,"stats":{"Line":25}},{"line":306,"address":[],"length":0,"stats":{"Line":24}},{"line":307,"address":[],"length":0,"stats":{"Line":1}},{"line":308,"address":[],"length":0,"stats":{"Line":3}},{"line":309,"address":[],"length":0,"stats":{"Line":2}},{"line":311,"address":[],"length":0,"stats":{"Line":1}},{"line":318,"address":[],"length":0,"stats":{"Line":36}},{"line":319,"address":[],"length":0,"stats":{"Line":24}},{"line":320,"address":[],"length":0,"stats":{"Line":0}},{"line":321,"address":[],"length":0,"stats":{"Line":0}},{"line":324,"address":[],"length":0,"stats":{"Line":36}},{"line":325,"address":[],"length":0,"stats":{"Line":36}},{"line":326,"address":[],"length":0,"stats":{"Line":24}},{"line":327,"address":[],"length":0,"stats":{"Line":48}},{"line":328,"address":[],"length":0,"stats":{"Line":24}},{"line":329,"address":[],"length":0,"stats":{"Line":0}},{"line":331,"address":[],"length":0,"stats":{"Line":24}},{"line":334,"address":[],"length":0,"stats":{"Line":24}},{"line":338,"address":[],"length":0,"stats":{"Line":24}},{"line":339,"address":[],"length":0,"stats":{"Line":3}},{"line":340,"address":[],"length":0,"stats":{"Line":1}},{"line":343,"address":[],"length":0,"stats":{"Line":55}},{"line":344,"address":[],"length":0,"stats":{"Line":23}},{"line":345,"address":[],"length":0,"stats":{"Line":12}},{"line":346,"address":[],"length":0,"stats":{"Line":40}},{"line":348,"address":[],"length":0,"stats":{"Line":8}},{"line":349,"address":[],"length":0,"stats":{"Line":2}},{"line":350,"address":[],"length":0,"stats":{"Line":4}},{"line":352,"address":[],"length":0,"stats":{"Line":2}},{"line":361,"address":[],"length":0,"stats":{"Line":27}},{"line":363,"address":[],"length":0,"stats":{"Line":7}},{"line":364,"address":[],"length":0,"stats":{"Line":24}},{"line":369,"address":[],"length":0,"stats":{"Line":4}},{"line":371,"address":[],"length":0,"stats":{"Line":1}},{"line":372,"address":[],"length":0,"stats":{"Line":2}},{"line":373,"address":[],"length":0,"stats":{"Line":1}},{"line":376,"address":[],"length":0,"stats":{"Line":8}},{"line":379,"address":[],"length":0,"stats":{"Line":18}},{"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":384,"address":[],"length":0,"stats":{"Line":0}},{"line":385,"address":[],"length":0,"stats":{"Line":0}},{"line":391,"address":[],"length":0,"stats":{"Line":25}},{"line":392,"address":[],"length":0,"stats":{"Line":100}},{"line":393,"address":[],"length":0,"stats":{"Line":25}},{"line":394,"address":[],"length":0,"stats":{"Line":0}},{"line":396,"address":[],"length":0,"stats":{"Line":75}},{"line":398,"address":[],"length":0,"stats":{"Line":100}},{"line":400,"address":[],"length":0,"stats":{"Line":50}},{"line":404,"address":[],"length":0,"stats":{"Line":12}},{"line":405,"address":[],"length":0,"stats":{"Line":48}},{"line":406,"address":[],"length":0,"stats":{"Line":12}},{"line":407,"address":[],"length":0,"stats":{"Line":0}},{"line":409,"address":[],"length":0,"stats":{"Line":36}},{"line":411,"address":[],"length":0,"stats":{"Line":48}},{"line":413,"address":[],"length":0,"stats":{"Line":24}},{"line":417,"address":[],"length":0,"stats":{"Line":10}},{"line":418,"address":[],"length":0,"stats":{"Line":20}},{"line":419,"address":[],"length":0,"stats":{"Line":20}},{"line":421,"address":[],"length":0,"stats":{"Line":30}},{"line":423,"address":[],"length":0,"stats":{"Line":30}},{"line":427,"address":[],"length":0,"stats":{"Line":10}},{"line":428,"address":[],"length":0,"stats":{"Line":20}},{"line":429,"address":[],"length":0,"stats":{"Line":20}},{"line":431,"address":[],"length":0,"stats":{"Line":30}},{"line":433,"address":[],"length":0,"stats":{"Line":30}},{"line":437,"address":[],"length":0,"stats":{"Line":12}},{"line":438,"address":[],"length":0,"stats":{"Line":24}},{"line":439,"address":[],"length":0,"stats":{"Line":24}},{"line":441,"address":[],"length":0,"stats":{"Line":36}},{"line":443,"address":[],"length":0,"stats":{"Line":36}},{"line":447,"address":[],"length":0,"stats":{"Line":2}},{"line":448,"address":[],"length":0,"stats":{"Line":4}},{"line":449,"address":[],"length":0,"stats":{"Line":4}},{"line":450,"address":[],"length":0,"stats":{"Line":2}},{"line":452,"address":[],"length":0,"stats":{"Line":3}},{"line":456,"address":[],"length":0,"stats":{"Line":3}},{"line":459,"address":[],"length":0,"stats":{"Line":2}},{"line":463,"address":[],"length":0,"stats":{"Line":9}},{"line":464,"address":[],"length":0,"stats":{"Line":36}},{"line":465,"address":[],"length":0,"stats":{"Line":9}},{"line":466,"address":[],"length":0,"stats":{"Line":0}},{"line":468,"address":[],"length":0,"stats":{"Line":27}},{"line":470,"address":[],"length":0,"stats":{"Line":36}},{"line":472,"address":[],"length":0,"stats":{"Line":18}},{"line":478,"address":[],"length":0,"stats":{"Line":11}},{"line":479,"address":[],"length":0,"stats":{"Line":33}},{"line":480,"address":[],"length":0,"stats":{"Line":33}},{"line":481,"address":[],"length":0,"stats":{"Line":95}},{"line":482,"address":[],"length":0,"stats":{"Line":15}},{"line":484,"address":[],"length":0,"stats":{"Line":20}},{"line":486,"address":[],"length":0,"stats":{"Line":1}},{"line":488,"address":[],"length":0,"stats":{"Line":11}},{"line":489,"address":[],"length":0,"stats":{"Line":11}},{"line":490,"address":[],"length":0,"stats":{"Line":0}},{"line":491,"address":[],"length":0,"stats":{"Line":0}},{"line":492,"address":[],"length":0,"stats":{"Line":0}},{"line":493,"address":[],"length":0,"stats":{"Line":0}},{"line":497,"address":[],"length":0,"stats":{"Line":22}}],"covered":196,"coverable":220},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project.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//! Project load/save/import tasks, mirroring `src/task/src/project/*`.\n//!\n//! These tasks move data across the oaknode C ABI: they borrow node handles\n//! while running and take ownership only on the `take_*` accessors\n//! (architectural decision #5 in README.md).\n\npub mod format;\npub mod import;\npub mod load;\npub mod loadotio;\npub mod save;\npub mod saveotio;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","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//! `ProxyTask`, mirroring `src/task/src/proxy/proxy.h`.\n//!\n//! Transcodes a video stream to a proxy via an external `ffmpeg` process\n//! (oakcodec kind `OAKCODEC_TASK_PROXY`). The two pure functions\n//! [`ProxyTask::build_arguments`] and [`ProxyTask::parse_progress`] are\n//! parity/golden candidates (see tests/parity_test.rs).\n//!\n//! CPP-PARITY: src/task/src/proxy/proxy.h\n\nuse std::io::{BufRead, BufReader};\nuse std::process::{Command, Stdio};\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// The proxy parameters, mirroring `oakcodec_proxy_params` in\n/// `include/codec/proxy.h` (kept as plain fields so no oakcodec handle is\n/// needed to build arguments).\npub struct ProxyParams {\n\t/// Target width (0 = unspecified/divider-based).\n\tpub width: i32,\n\t/// Target height (0 = unspecified/divider-based).\n\tpub height: i32,\n\t/// Resolution divider.\n\tpub divider: i32,\n\t/// Proxy format version.\n\tpub version: i32,\n\t/// CRF for the proxy encode.\n\tpub crf: i32,\n\t/// Whether audio is included in the proxy.\n\tpub include_audio: bool,\n\t/// Output container extension.\n\tpub extension: String,\n\t/// ffmpeg preset name.\n\tpub preset: String,\n}\n\n/// A proxy transcode task: runs `ffmpeg` with arguments from\n/// [`ProxyTask::build_arguments`], parsing progress lines via\n/// [`ProxyTask::parse_progress`].\npub struct ProxyTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Source media filename.\n\tpub source_filename: String,\n\t/// Target stream index.\n\tpub stream_index: i32,\n\t/// Proxy parameters.\n\tpub params: ProxyParams,\n\t/// Output proxy filename.\n\tpub output_filename: String,\n\t/// Total media duration in seconds (for progress scaling).\n\tduration_seconds: f64,\n}\n\nimpl ProxyTask {\n\t/// Build a proxy task from an oakcodec request and proxy params,\n\t/// mirroring the C++ constructor (divider-based requests take the source\n\t/// fraction).\n\tpub fn new(request: \u0026bridge::codec::OakCodecTaskRequest, params: ProxyParams) -\u003e ProxyTask {\n\t\tlet source = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) };\n\t\tlet output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) };\n\t\tlet mut params = params;\n\t\tif request.proxy_width \u003e 0 \u0026\u0026 request.proxy_height \u003e 0 {\n\t\t\tparams.width = request.proxy_width;\n\t\t\tparams.height = request.proxy_height;\n\t\t\tparams.divider = 1;\n\t\t}\n\t\tlet title = format!(\"Generating Proxy {}:{}\", source, request.stream_index);\n\t\tProxyTask {\n\t\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\t\tsource_filename: source,\n\t\t\tstream_index: request.stream_index,\n\t\t\tparams,\n\t\t\toutput_filename: output,\n\t\t\tduration_seconds: 0.0,\n\t\t}\n\t}\n\n\t/// Build the `ffmpeg` command-line argument vector for the proxy\n\t/// transcode. Mirrors `build_arguments` in proxy.cpp; the exact ordering\n\t/// and flag spelling are parity-locked (golden test).\n\t///\n\t/// CPP-PARITY: src/task/src/proxy/proxy.cpp (build_arguments)\n\tpub fn build_arguments(\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tparams: \u0026ProxyParams,\n\t\toutput_filename: \u0026str,\n\t) -\u003e Vec\u003cString\u003e {\n\t\tlet scale_filter = if params.divider \u003e 1 {\n\t\t\tformat!(\n\t\t\t\t\"scale=w=trunc(iw/{}/2)*2:h=trunc(ih/{}/2)*2\",\n\t\t\t\tparams.divider, params.divider\n\t\t\t)\n\t\t} else {\n\t\t\tformat!(\n\t\t\t\t\"scale=w={}:h={}:force_original_aspect_ratio=decrease\",\n\t\t\t\tparams.width, params.height\n\t\t\t)\n\t\t};\n\n\t\tlet container_format = if params.extension.is_empty() {\n\t\t\t\"mp4\".to_string()\n\t\t} else {\n\t\t\tparams.extension.clone()\n\t\t};\n\n\t\tlet mut args: Vec\u003cString\u003e = vec![\n\t\t\t\"-y\".to_string(),\n\t\t\t\"-nostats\".to_string(),\n\t\t\t\"-progress\".to_string(),\n\t\t\t\"pipe:1\".to_string(),\n\t\t\t\"-i\".to_string(),\n\t\t\tsource_filename.to_string(),\n\t\t\t\"-map\".to_string(),\n\t\t\tformat!(\"0:{stream_index}\"),\n\t\t];\n\n\t\tif params.include_audio {\n\t\t\targs.extend([\n\t\t\t\t\"-map\".to_string(),\n\t\t\t\t\"0:a?\".to_string(),\n\t\t\t\t\"-c:a\".to_string(),\n\t\t\t\t\"aac\".to_string(),\n\t\t\t\t\"-b:a\".to_string(),\n\t\t\t\t\"128k\".to_string(),\n\t\t\t]);\n\t\t} else {\n\t\t\targs.push(\"-an\".to_string());\n\t\t}\n\n\t\targs.extend([\n\t\t\t\"-vf\".to_string(),\n\t\t\tscale_filter,\n\t\t\t\"-c:v\".to_string(),\n\t\t\t\"libx264\".to_string(),\n\t\t\t\"-preset\".to_string(),\n\t\t\tparams.preset.clone(),\n\t\t\t\"-crf\".to_string(),\n\t\t\tparams.crf.to_string(),\n\t\t\t\"-pix_fmt\".to_string(),\n\t\t\t\"yuv420p\".to_string(),\n\t\t\t\"-movflags\".to_string(),\n\t\t\t\"+faststart\".to_string(),\n\t\t\t\"-f\".to_string(),\n\t\t\tcontainer_format,\n\t\t\toutput_filename.to_string(),\n\t\t]);\n\n\t\targs\n\t}\n\n\t/// Parse a single `ffmpeg -progress` output line into a progress value in\n\t/// 0.0..=1.0 given the total duration. Mirrors `parse_progress` in\n\t/// proxy.cpp (golden test).\n\t///\n\t/// CPP-PARITY: src/task/src/proxy/proxy.cpp (parse_progress)\n\tpub fn parse_progress(line: \u0026str, duration_seconds: f64) -\u003e Option\u003cf64\u003e {\n\t\tif duration_seconds \u003c= 0.0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tlet out_time_us: i64 = if let Some(rest) = line.strip_prefix(\"out_time_us=\") {\n\t\t\trest.trim().parse().unwrap_or(-1)\n\t\t} else if let Some(rest) = line.strip_prefix(\"out_time_ms=\") {\n\t\t\t// Despite the name, ffmpeg reports this value in microseconds.\n\t\t\trest.trim().parse().unwrap_or(-1)\n\t\t} else {\n\t\t\t-1\n\t\t};\n\n\t\tif out_time_us \u003c 0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tlet progress = out_time_us as f64 / 1_000_000.0 / duration_seconds;\n\t\tSome(if progress \u003c 0.0 {\n\t\t\t0.0\n\t\t} else if progress \u003e 1.0 {\n\t\t\t1.0\n\t\t} else {\n\t\t\tprogress\n\t\t})\n\t}\n}\n\nimpl TaskBehavior for ProxyTask {\n\t/// Spawn `ffmpeg` with the built arguments, feed `-progress` lines to\n\t/// [`ProxyTask::parse_progress`] and emit them as task progress.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet mut ffmpeg_buf = [0i8; 1024];\n\t\tlet found = unsafe {\n\t\t\tbridge::codec::oakcodec_proxy_find_ffmpeg(std::ptr::null(), ffmpeg_buf.as_mut_ptr(), ffmpeg_buf.len() as i32)\n\t\t};\n\t\tif found \u003c= 0 {\n\t\t\ttask.set_error(\n\t\t\t\t\"Failed to generate proxy: ffmpeg executable was not found. Set the ffmpeg path in Preferences \u003e Disk \u003e Proxy Settings.\",\n\t\t\t);\n\t\t\treturn Err(Error::Failed(\"ffmpeg executable was not found\".to_string()));\n\t\t}\n\t\tlet ffmpeg_path = buf_to_string(\u0026ffmpeg_buf);\n\n\t\t// Create the output directory if needed.\n\t\tif let Some(parent) = std::path::Path::new(\u0026self.output_filename).parent() {\n\t\t\tif !parent.as_os_str().is_empty() \u0026\u0026 !parent.exists() {\n\t\t\t\tif std::fs::create_dir_all(parent).is_err() {\n\t\t\t\t\ttask.set_error(\"Failed to create proxy output directory\");\n\t\t\t\t\treturn Err(Error::Failed(\"Failed to create proxy output directory\".to_string()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet _ = std::fs::remove_file(\u0026self.output_filename);\n\t\tlet working_filename = format!(\"{}.working.mp4\", self.output_filename);\n\t\tlet _ = std::fs::remove_file(\u0026working_filename);\n\n\t\tlet args = Self::build_arguments(\u0026self.source_filename, self.stream_index, \u0026self.params, \u0026working_filename);\n\n\t\t// Probe the source duration for progress scaling (0 when unknown).\n\t\tself.duration_seconds = probe_source_duration_seconds(\u0026ffmpeg_path, \u0026self.source_filename);\n\n\t\tlet mut command = Command::new(\u0026ffmpeg_path);\n\t\tcommand.args(\u0026args).stdout(Stdio::piped()).stderr(Stdio::piped());\n\t\tlet mut child = match command.spawn() {\n\t\t\tOk(child) =\u003e child,\n\t\t\tErr(_) =\u003e {\n\t\t\t\ttask.set_error(\"Failed to start ffmpeg for proxy generation\");\n\t\t\t\treturn Err(Error::Failed(\"Failed to start ffmpeg\".to_string()));\n\t\t\t}\n\t\t};\n\n\t\t// Drain stderr in a thread so a chatty ffmpeg cannot block us.\n\t\tlet stderr = child.stderr.take();\n\t\tif let Some(stderr) = stderr {\n\t\t\tstd::thread::spawn(move || {\n\t\t\t\tlet _ = BufReader::new(stderr).lines().count();\n\t\t\t});\n\t\t}\n\n\t\tlet stdout = child.stdout.take();\n\t\tlet mut last_progress = 0.0_f64;\n\t\tif let Some(stdout) = stdout {\n\t\t\tlet reader = BufReader::new(stdout);\n\t\t\tfor line in reader.lines() {\n\t\t\t\tmatch line {\n\t\t\t\t\tOk(line) =\u003e {\n\t\t\t\t\t\tif let Some(progress) = Self::parse_progress(\u0026line, self.duration_seconds) {\n\t\t\t\t\t\t\tif progress \u003e= 0.0 \u0026\u0026 progress - last_progress \u003e 0.001 {\n\t\t\t\t\t\t\t\tlast_progress = progress;\n\t\t\t\t\t\t\t\ttask.emit_progress(progress);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tErr(_) =\u003e break,\n\t\t\t\t}\n\t\t\t\tif task.is_cancelled() {\n\t\t\t\t\tlet _ = child.kill();\n\t\t\t\t\tlet _ = child.wait();\n\t\t\t\t\tlet _ = std::fs::remove_file(\u0026working_filename);\n\t\t\t\t\ttask.set_error(\"Proxy generation was cancelled\");\n\t\t\t\t\treturn Err(Error::Cancelled);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet exit_status = child.wait();\n\t\tif !matches!(exit_status, Ok(status) if status.success()) {\n\t\t\tlet _ = std::fs::remove_file(\u0026working_filename);\n\t\t\ttask.set_error(\"ffmpeg failed to generate proxy\");\n\t\t\treturn Err(Error::Failed(\"ffmpeg failed to generate proxy\".to_string()));\n\t\t}\n\n\t\tif !std::path::Path::new(\u0026working_filename).exists() {\n\t\t\ttask.set_error(\"ffmpeg finished but proxy file was not created\");\n\t\t\treturn Err(Error::Failed(\"ffmpeg finished but proxy file was not created\".to_string()));\n\t\t}\n\n\t\tif std::fs::rename(\u0026working_filename, \u0026self.output_filename).is_err() {\n\t\t\ttask.set_error(\"Failed to move proxy into place\");\n\t\t\treturn Err(Error::Failed(\"Failed to move proxy into place\".to_string()));\n\t\t}\n\n\t\ttask.emit_progress(1.0);\n\t\tOk(())\n\t}\n}\n\n/// Read a NUL-terminated C char buffer into a String.\nfn buf_to_string(buf: \u0026[i8]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n\n/// Probe the source duration via `ffprobe` next to ffmpeg; 0.0 when\n/// unavailable (in which case no intermediate progress is reported).\nfn probe_source_duration_seconds(ffmpeg_path: \u0026str, source_filename: \u0026str) -\u003e f64 {\n\tlet ffprobe = std::path::Path::new(ffmpeg_path).with_file_name(\"ffprobe\");\n\tif !ffprobe.exists() {\n\t\treturn 0.0;\n\t}\n\tlet output = Command::new(\u0026ffprobe)\n\t\t.args([\"-v\", \"error\", \"-show_entries\", \"format=duration\", \"-of\", \"default=noprint_wrappers=1:nokey=1\", source_filename])\n\t\t.output();\n\tmatch output {\n\t\tOk(out) if out.status.success() =\u003e {\n\t\t\tlet text = String::from_utf8_lossy(\u0026out.stdout);\n\t\t\ttext.trim().parse().unwrap_or(0.0)\n\t\t}\n\t\t_ =\u003e 0.0,\n\t}\n}\n","traces":[{"line":78,"address":[],"length":0,"stats":{"Line":7}},{"line":79,"address":[],"length":0,"stats":{"Line":21}},{"line":80,"address":[],"length":0,"stats":{"Line":21}},{"line":81,"address":[],"length":0,"stats":{"Line":14}},{"line":82,"address":[],"length":0,"stats":{"Line":7}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":14}},{"line":89,"address":[],"length":0,"stats":{"Line":28}},{"line":91,"address":[],"length":0,"stats":{"Line":14}},{"line":103,"address":[],"length":0,"stats":{"Line":6}},{"line":109,"address":[],"length":0,"stats":{"Line":12}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":5}},{"line":121,"address":[],"length":0,"stats":{"Line":18}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":124,"address":[],"length":0,"stats":{"Line":10}},{"line":127,"address":[],"length":0,"stats":{"Line":18}},{"line":128,"address":[],"length":0,"stats":{"Line":12}},{"line":129,"address":[],"length":0,"stats":{"Line":12}},{"line":130,"address":[],"length":0,"stats":{"Line":12}},{"line":131,"address":[],"length":0,"stats":{"Line":12}},{"line":132,"address":[],"length":0,"stats":{"Line":12}},{"line":133,"address":[],"length":0,"stats":{"Line":12}},{"line":134,"address":[],"length":0,"stats":{"Line":12}},{"line":135,"address":[],"length":0,"stats":{"Line":6}},{"line":138,"address":[],"length":0,"stats":{"Line":11}},{"line":139,"address":[],"length":0,"stats":{"Line":15}},{"line":140,"address":[],"length":0,"stats":{"Line":15}},{"line":141,"address":[],"length":0,"stats":{"Line":15}},{"line":142,"address":[],"length":0,"stats":{"Line":15}},{"line":143,"address":[],"length":0,"stats":{"Line":15}},{"line":144,"address":[],"length":0,"stats":{"Line":15}},{"line":145,"address":[],"length":0,"stats":{"Line":5}},{"line":148,"address":[],"length":0,"stats":{"Line":3}},{"line":151,"address":[],"length":0,"stats":{"Line":18}},{"line":152,"address":[],"length":0,"stats":{"Line":18}},{"line":153,"address":[],"length":0,"stats":{"Line":12}},{"line":154,"address":[],"length":0,"stats":{"Line":18}},{"line":155,"address":[],"length":0,"stats":{"Line":18}},{"line":156,"address":[],"length":0,"stats":{"Line":18}},{"line":157,"address":[],"length":0,"stats":{"Line":18}},{"line":158,"address":[],"length":0,"stats":{"Line":18}},{"line":159,"address":[],"length":0,"stats":{"Line":18}},{"line":160,"address":[],"length":0,"stats":{"Line":18}},{"line":161,"address":[],"length":0,"stats":{"Line":18}},{"line":162,"address":[],"length":0,"stats":{"Line":18}},{"line":163,"address":[],"length":0,"stats":{"Line":18}},{"line":164,"address":[],"length":0,"stats":{"Line":18}},{"line":165,"address":[],"length":0,"stats":{"Line":12}},{"line":166,"address":[],"length":0,"stats":{"Line":6}},{"line":169,"address":[],"length":0,"stats":{"Line":6}},{"line":177,"address":[],"length":0,"stats":{"Line":11}},{"line":178,"address":[],"length":0,"stats":{"Line":11}},{"line":179,"address":[],"length":0,"stats":{"Line":5}},{"line":182,"address":[],"length":0,"stats":{"Line":22}},{"line":183,"address":[],"length":0,"stats":{"Line":12}},{"line":184,"address":[],"length":0,"stats":{"Line":3}},{"line":186,"address":[],"length":0,"stats":{"Line":3}},{"line":188,"address":[],"length":0,"stats":{"Line":1}},{"line":191,"address":[],"length":0,"stats":{"Line":6}},{"line":192,"address":[],"length":0,"stats":{"Line":3}},{"line":195,"address":[],"length":0,"stats":{"Line":6}},{"line":196,"address":[],"length":0,"stats":{"Line":3}},{"line":197,"address":[],"length":0,"stats":{"Line":0}},{"line":198,"address":[],"length":0,"stats":{"Line":3}},{"line":199,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":2}},{"line":209,"address":[],"length":0,"stats":{"Line":7}},{"line":210,"address":[],"length":0,"stats":{"Line":14}},{"line":212,"address":[],"length":0,"stats":{"Line":35}},{"line":214,"address":[],"length":0,"stats":{"Line":7}},{"line":215,"address":[],"length":0,"stats":{"Line":4}},{"line":218,"address":[],"length":0,"stats":{"Line":2}},{"line":220,"address":[],"length":0,"stats":{"Line":15}},{"line":223,"address":[],"length":0,"stats":{"Line":10}},{"line":224,"address":[],"length":0,"stats":{"Line":10}},{"line":225,"address":[],"length":0,"stats":{"Line":0}},{"line":226,"address":[],"length":0,"stats":{"Line":0}},{"line":227,"address":[],"length":0,"stats":{"Line":0}},{"line":232,"address":[],"length":0,"stats":{"Line":5}},{"line":233,"address":[],"length":0,"stats":{"Line":10}},{"line":234,"address":[],"length":0,"stats":{"Line":5}},{"line":236,"address":[],"length":0,"stats":{"Line":30}},{"line":239,"address":[],"length":0,"stats":{"Line":10}},{"line":241,"address":[],"length":0,"stats":{"Line":15}},{"line":242,"address":[],"length":0,"stats":{"Line":25}},{"line":243,"address":[],"length":0,"stats":{"Line":10}},{"line":244,"address":[],"length":0,"stats":{"Line":10}},{"line":246,"address":[],"length":0,"stats":{"Line":0}},{"line":247,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":15}},{"line":253,"address":[],"length":0,"stats":{"Line":10}},{"line":254,"address":[],"length":0,"stats":{"Line":10}},{"line":255,"address":[],"length":0,"stats":{"Line":15}},{"line":259,"address":[],"length":0,"stats":{"Line":15}},{"line":260,"address":[],"length":0,"stats":{"Line":10}},{"line":261,"address":[],"length":0,"stats":{"Line":10}},{"line":262,"address":[],"length":0,"stats":{"Line":15}},{"line":263,"address":[],"length":0,"stats":{"Line":13}},{"line":264,"address":[],"length":0,"stats":{"Line":3}},{"line":265,"address":[],"length":0,"stats":{"Line":3}},{"line":266,"address":[],"length":0,"stats":{"Line":6}},{"line":267,"address":[],"length":0,"stats":{"Line":0}},{"line":268,"address":[],"length":0,"stats":{"Line":0}},{"line":269,"address":[],"length":0,"stats":{"Line":0}},{"line":273,"address":[],"length":0,"stats":{"Line":0}},{"line":275,"address":[],"length":0,"stats":{"Line":6}},{"line":276,"address":[],"length":0,"stats":{"Line":0}},{"line":277,"address":[],"length":0,"stats":{"Line":0}},{"line":278,"address":[],"length":0,"stats":{"Line":0}},{"line":279,"address":[],"length":0,"stats":{"Line":0}},{"line":280,"address":[],"length":0,"stats":{"Line":0}},{"line":285,"address":[],"length":0,"stats":{"Line":15}},{"line":286,"address":[],"length":0,"stats":{"Line":24}},{"line":287,"address":[],"length":0,"stats":{"Line":2}},{"line":288,"address":[],"length":0,"stats":{"Line":6}},{"line":289,"address":[],"length":0,"stats":{"Line":2}},{"line":292,"address":[],"length":0,"stats":{"Line":3}},{"line":293,"address":[],"length":0,"stats":{"Line":3}},{"line":294,"address":[],"length":0,"stats":{"Line":1}},{"line":297,"address":[],"length":0,"stats":{"Line":6}},{"line":298,"address":[],"length":0,"stats":{"Line":0}},{"line":299,"address":[],"length":0,"stats":{"Line":0}},{"line":302,"address":[],"length":0,"stats":{"Line":4}},{"line":303,"address":[],"length":0,"stats":{"Line":2}},{"line":308,"address":[],"length":0,"stats":{"Line":5}},{"line":309,"address":[],"length":0,"stats":{"Line":408}},{"line":310,"address":[],"length":0,"stats":{"Line":20}},{"line":315,"address":[],"length":0,"stats":{"Line":5}},{"line":316,"address":[],"length":0,"stats":{"Line":15}},{"line":317,"address":[],"length":0,"stats":{"Line":5}},{"line":318,"address":[],"length":0,"stats":{"Line":5}},{"line":320,"address":[],"length":0,"stats":{"Line":0}},{"line":321,"address":[],"length":0,"stats":{"Line":0}},{"line":323,"address":[],"length":0,"stats":{"Line":0}},{"line":324,"address":[],"length":0,"stats":{"Line":0}},{"line":325,"address":[],"length":0,"stats":{"Line":0}},{"line":326,"address":[],"length":0,"stats":{"Line":0}},{"line":328,"address":[],"length":0,"stats":{"Line":0}}],"covered":114,"coverable":141},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","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//! `RenderTask` abstract base, mirroring `src/task/src/render/render.h`.\n//!\n//! The abstract parent of [`crate::export::ExportTask`] and\n//! [`crate::precache::PreCacheTask`]. It renders frames/audio through\n//! oakrender tickets (`bridge::render`) and hands each result to a virtual\n//! hook (frame/audio downloaded, subtitle encoded) that the subclass\n//! implements via the [`RenderTaskBehavior`] trait.\n//!\n//! ## Concurrent render loop\n//!\n//! Like the C++ original (`render.cpp`), the loop keeps up to\n//! `max_inflight` tickets running at once (default\n//! `std::thread::available_parallelism()`, matching the C++\n//! `std::max(1, hardware_concurrency)`). Tickets report completion through\n//! the `oakrender_ticket_finished_fn` callback (fired on the ticket's own\n//! finishing thread), which pushes the finished ticket into a shared queue\n//! and wakes the render thread (queue + condvar, exactly the C++ design).\n//! Tickets may complete out of order; a **reorder buffer** delivers the\n//! results to the hooks in timestamp order — the audio range first (the C++\n//! queue-audio-first order), then every frame in ascending time — so the\n//! observable per-frame contract (`frame_downloaded`/`audio_downloaded` in\n//! order, progress updates, cancellation between frames) is unchanged.\n//!\n//! The ticket's finished callback hands the loop a **borrowed copy** of the\n//! submitter's handle (`include/render/ticket.h`: \"the submitter keeps\n//! ownership and releases it\"). The loop therefore releases exactly the\n//! handle value each submit function returns, once, and never releases the\n//! queue's borrowed copies. Cancellation (or a hook error) cancels and\n//! waits every in-flight ticket before returning, so every submitted\n//! ticket's completion still fires exactly once.\n//!\n//! CPP-PARITY: src/task/src/render/render.h\n\nuse std::collections::{HashMap, VecDeque};\nuse std::sync::atomic::{AtomicUsize, Ordering};\nuse std::sync::{Condvar, Mutex};\n\nuse crate::bridge;\nuse crate::bridge::common::OakColorTransform;\nuse crate::bridge::node::{cstr, OAKNODE_SEQUENCE_SAMPLES_INPUT, OAKNODE_SEQUENCE_TEXTURE_INPUT};\nuse crate::bridge::render::{OakRenderTicket, OakRenderVideoTicketParams};\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::task::Task;\nuse oakcore_rs::{Rational, TimeRange};\n\n/// `OAKRENDER_TICKET_VIDEO` (`include/render/ticket.h`).\nconst TICKET_VIDEO: i32 = 0;\n/// `OAKRENDER_TICKET_AUDIO` (`include/render/ticket.h`).\nconst TICKET_AUDIO: i32 = 1;\n\n/// Overrides applied to a render ticket, mirroring the C++ `ForceParams`\n/// struct in render.h (fields map onto `oakrender_video_ticket_params`).\n///\n/// CPP-PARITY: src/task/src/render/render.h (ForceParams)\n#[derive(Clone, Debug, Default)]\npub struct ForceParams {\n\t/// Forced output width (0 = off).\n\tpub force_width: i32,\n\t/// Forced output height (0 = off).\n\tpub force_height: i32,\n\t/// Forced color matrix (row-major 4x4); only used when\n\t/// `has_force_matrix` is set.\n\tpub force_matrix: [f64; 16],\n\t/// Whether `force_matrix` is in effect.\n\tpub has_force_matrix: bool,\n\t/// Forced pixel format (`oakcore_rs::PixelFormat` as int; -1 = off).\n\tpub force_format: i32,\n\t/// Forced channel count (0 = off).\n\tpub force_channel_count: i32,\n\t/// Forced color output (borrowed `OakColorProcessor`; empty = none).\n\tpub force_color_output: CHandle,\n}\n\n/// Subclass hooks, standing in for the C++ protected virtuals\n/// `download_frame`/`frame_downloaded`/`audio_downloaded`/`encode_subtitle`.\npub trait RenderTaskBehavior {\n\t/// Called for each rendered frame.\n\tfn frame_downloaded(\u0026mut self, task: \u0026mut Task, frame: CHandle) -\u003e Result\u003c()\u003e;\n\t/// Called for each rendered audio buffer.\n\tfn audio_downloaded(\u0026mut self, task: \u0026mut Task, buffer: CHandle) -\u003e Result\u003c()\u003e;\n\t/// Called to encode a subtitle.\n\tfn encode_subtitle(\u0026mut self, task: \u0026mut Task, text: \u0026str) -\u003e Result\u003c()\u003e;\n}\n\n/// The abstract render task base. Holds the shared [`Task`], the sequence\n/// output params, the viewer node, and the subclass behavior.\npub struct RenderTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Output video params (borrowed `OakVideoParams`).\n\tpub video_params: CHandle,\n\t/// Output audio params (borrowed `OakAudioParams`; may be empty).\n\tpub audio_params: CHandle,\n\t/// The viewer node being rendered (borrowed `OakNodeNode`).\n\tpub viewer: CHandle,\n\t/// Force params applied to every ticket.\n\tpub force_params: ForceParams,\n\t/// The subclass behavior.\n\tpub behavior: Option\u003cBox\u003cdyn RenderTaskBehavior + Send\u003e\u003e,\n\t// --- private render inputs (set by the subclass before render()) ---\n\t/// Borrowed color manager.\n\tcolor_manager: CHandle,\n\t/// Borrowed frame cache.\n\tcache: CHandle,\n\t/// `olive::RenderMode::Mode` as int.\n\tmode: i32,\n\t/// Whether audio ranges are rendered.\n\taudio_enabled: bool,\n\t/// The range rendered by [`RenderTask::render`].\n\texport_range: TimeRange,\n\t/// Whether the render loop emits progress itself (the export task\n\t/// disables it and reports progress per written frame).\n\tnative_progress_signalling: bool,\n\t/// Total number of frames in `export_range` (computed by `render`).\n\ttotal_frames: i64,\n\t/// Maximum render tickets kept in flight at once (the C++\n\t/// `maximum_rendered_frames`; defaults to `available_parallelism`).\n\tmax_inflight: usize,\n}\n\nimpl RenderTask {\n\t/// Create a render task with default (empty) render inputs.\n\tpub fn new(\n\t\tbase: Task,\n\t\tvideo_params: CHandle,\n\t\taudio_params: CHandle,\n\t\tviewer: CHandle,\n\t\tforce_params: ForceParams,\n\t\tbehavior: Option\u003cBox\u003cdyn RenderTaskBehavior + Send\u003e\u003e,\n\t) -\u003e RenderTask {\n\t\tRenderTask {\n\t\t\tbase,\n\t\t\tvideo_params,\n\t\t\taudio_params,\n\t\t\tviewer,\n\t\t\tforce_params,\n\t\t\tbehavior,\n\t\t\tcolor_manager: CHandle::null(),\n\t\t\tcache: CHandle::null(),\n\t\t\tmode: 0,\n\t\t\taudio_enabled: false,\n\t\t\texport_range: TimeRange::new(Rational::new(0, 1), Rational::new(0, 1)),\n\t\t\tnative_progress_signalling: true,\n\t\t\ttotal_frames: 0,\n\t\t\tmax_inflight: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).max(1),\n\t\t}\n\t}\n\n\t/// Configure the render inputs before [`RenderTask::render`] (called by\n\t/// the concrete subclasses' `run`).\n\tpub fn set_render_inputs(\n\t\t\u0026mut self,\n\t\tcolor_manager: CHandle,\n\t\tcache: CHandle,\n\t\tmode: i32,\n\t\taudio_enabled: bool,\n\t\texport_range: TimeRange,\n\t) {\n\t\tself.color_manager = color_manager;\n\t\tself.cache = cache;\n\t\tself.mode = mode;\n\t\tself.audio_enabled = audio_enabled;\n\t\tself.export_range = export_range;\n\t}\n\n\t/// Enable/disable the render loop's own progress signalling.\n\tpub fn set_native_progress_signalling(\u0026mut self, enabled: bool) {\n\t\tself.native_progress_signalling = enabled;\n\t}\n\n\t/// Override the number of render tickets kept in flight at once.\n\t///\n\t/// Defaults to `std::thread::available_parallelism()`, mirroring the\n\t/// C++ `std::max(1, int(std::thread::hardware_concurrency()))`. Primarily\n\t/// a test hook: a smaller window makes concurrent-completion tests\n\t/// deterministic regardless of the host core count.\n\tpub fn set_max_inflight(\u0026mut self, max: usize) {\n\t\tself.max_inflight = max.max(1);\n\t}\n\n\t/// The number of frames computed by the last [`RenderTask::render`] run.\n\tpub fn total_frames(\u0026self) -\u003e i64 {\n\t\tself.total_frames\n\t}\n\n\t/// Frame duration rational from the video params (falls back to 1/1\n\t/// when the params are empty/invalid).\n\tfn timebase(\u0026self) -\u003e Rational {\n\t\tlet mut num = 0;\n\t\tlet mut den = 1;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_frame_rate_as_time_base(\n\t\t\t\tself.video_params,\n\t\t\t\t\u0026mut num,\n\t\t\t\t\u0026mut den,\n\t\t\t);\n\t\t}\n\t\tif num \u003c= 0 || den \u003c= 0 {\n\t\t\tRational::new(1, 1)\n\t\t} else {\n\t\t\tRational::new(num as i64, den as i64)\n\t\t}\n\t}\n\n\t/// Submit one video frame ticket at `time` (mirrors the C++\n\t/// `start_video_ticket`). `dispatch` is the shared completion channel\n\t/// handed to the ticket's finished callback.\n\tfn submit_video_ticket(\n\t\t\u0026self,\n\t\ttime: Rational,\n\t\tdispatch: *mut RenderDispatch,\n\t) -\u003e Result\u003cOakRenderTicket\u003e {\n\t\tlet mut output_node = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_input_get_connected_node(\n\t\t\t\tself.viewer,\n\t\t\t\tcstr(OAKNODE_SEQUENCE_TEXTURE_INPUT),\n\t\t\t\t\u0026mut output_node,\n\t\t\t);\n\t\t}\n\t\tif output_node.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"No node connected to the viewer output\".to_string()));\n\t\t}\n\n\t\tlet audio_params_ptr = if self.audio_params.ctx.is_null() {\n\t\t\tstd::ptr::null()\n\t\t} else {\n\t\t\t\u0026self.audio_params as *const CHandle\n\t\t};\n\t\tlet force = \u0026self.force_params;\n\t\tlet params = OakRenderVideoTicketParams {\n\t\t\toutput_node,\n\t\t\tvideo_params: self.video_params,\n\t\t\taudio_params: audio_params_ptr,\n\t\t\ttime_num: time.numerator(),\n\t\t\ttime_den: time.denominator(),\n\t\t\tcolor_manager: self.color_manager,\n\t\t\tmode: self.mode,\n\t\t\tforce_width: force.force_width,\n\t\t\tforce_height: force.force_height,\n\t\t\tforce_matrix: force.force_matrix,\n\t\t\thas_force_matrix: if force.has_force_matrix { 1 } else { 0 },\n\t\t\tforce_format: force.force_format,\n\t\t\tforce_channel_count: force.force_channel_count,\n\t\t\tforce_color_output: bridge::render::OakColorProcessor::from_chandle(force.force_color_output),\n\t\t\tforce_color_transform: OakColorTransform {\n\t\t\t\tctx: std::ptr::null_mut(),\n\t\t\t\taddref: None,\n\t\t\t\trelease: None,\n\t\t\t\tabi_version: 0,\n\t\t\t},\n\t\t\tcache: bridge::render::OakRenderCache::from_chandle(self.cache),\n\t\t};\n\n\t\tlet ticket = unsafe {\n\t\t\tbridge::render::oakrender_ticket_render_frame(\n\t\t\t\t\u0026params,\n\t\t\t\tSome(ticket_finished),\n\t\t\t\tdispatch as *mut std::ffi::c_void,\n\t\t\t)\n\t\t};\n\t\t// Per-frame call: release the borrowed handle box (the ticket keeps\n\t\t// the native node, not the handle).\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_free(\u0026mut output_node);\n\t\t}\n\t\tif ticket.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"Failed to start frame render ticket\".to_string()));\n\t\t}\n\t\tOk(ticket)\n\t}\n\n\t/// Submit one audio ticket for `range` (the Rust API renders a single\n\t/// range; the C++ submits one ticket per audio range).\n\tfn submit_audio_ticket(\n\t\t\u0026self,\n\t\trange: TimeRange,\n\t\tdispatch: *mut RenderDispatch,\n\t) -\u003e Result\u003cOakRenderTicket\u003e {\n\t\tlet mut output_node = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_input_get_connected_node(\n\t\t\t\tself.viewer,\n\t\t\t\tcstr(OAKNODE_SEQUENCE_SAMPLES_INPUT),\n\t\t\t\t\u0026mut output_node,\n\t\t\t);\n\t\t}\n\t\tif output_node.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"No node connected to the viewer samples input\".to_string()));\n\t\t}\n\n\t\tlet audio_params_ptr = if self.audio_params.ctx.is_null() {\n\t\t\tstd::ptr::null()\n\t\t} else {\n\t\t\t\u0026self.audio_params as *const CHandle\n\t\t};\n\t\tlet ticket = unsafe {\n\t\t\tbridge::render::oakrender_ticket_render_audio(\n\t\t\t\toutput_node,\n\t\t\t\trange.in_().numerator(),\n\t\t\t\trange.in_().denominator(),\n\t\t\t\trange.out().numerator(),\n\t\t\t\trange.out().denominator(),\n\t\t\t\taudio_params_ptr,\n\t\t\t\tself.mode,\n\t\t\t\tSome(ticket_finished),\n\t\t\t\tdispatch as *mut std::ffi::c_void,\n\t\t\t)\n\t\t};\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_free(\u0026mut output_node);\n\t\t}\n\t\tif ticket.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"Failed to start audio render ticket\".to_string()));\n\t\t}\n\t\tOk(ticket)\n\t}\n\n\t/// Submit a video ticket and account it as in-flight.\n\t///\n\t/// The in-flight count is bumped **before** the submit: a ticket can\n\t/// complete synchronously (its callback fires before the submit returns),\n\t/// so the callback's decrement must always see its increment. A failed\n\t/// submit (null ticket) never fires a callback, so the bump is rolled\n\t/// back.\n\tfn start_video_ticket(\n\t\t\u0026self,\n\t\ttime: Rational,\n\t\tdispatch: *mut RenderDispatch,\n\t\tin_flight: \u0026mut Vec\u003cOakRenderTicket\u003e,\n\t) -\u003e Result\u003c()\u003e {\n\t\tunsafe {\n\t\t\t(\u0026*dispatch).running.fetch_add(1, Ordering::SeqCst);\n\t\t}\n\t\tmatch self.submit_video_ticket(time, dispatch) {\n\t\t\tOk(ticket) =\u003e {\n\t\t\t\tin_flight.push(ticket);\n\t\t\t\tOk(())\n\t\t\t}\n\t\t\tErr(e) =\u003e {\n\t\t\t\tunsafe {\n\t\t\t\t\t(\u0026*dispatch).running.fetch_sub(1, Ordering::SeqCst);\n\t\t\t\t}\n\t\t\t\tErr(e)\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Submit the audio ticket and account it as in-flight (see\n\t/// [`RenderTask::start_video_ticket`] for the counting contract).\n\tfn start_audio_ticket(\n\t\t\u0026self,\n\t\trange: TimeRange,\n\t\tdispatch: *mut RenderDispatch,\n\t\tin_flight: \u0026mut Vec\u003cOakRenderTicket\u003e,\n\t) -\u003e Result\u003c()\u003e {\n\t\tunsafe {\n\t\t\t(\u0026*dispatch).running.fetch_add(1, Ordering::SeqCst);\n\t\t}\n\t\tmatch self.submit_audio_ticket(range, dispatch) {\n\t\t\tOk(ticket) =\u003e {\n\t\t\t\tin_flight.push(ticket);\n\t\t\t\tOk(())\n\t\t\t}\n\t\t\tErr(e) =\u003e {\n\t\t\t\tunsafe {\n\t\t\t\t\t(\u0026*dispatch).running.fetch_sub(1, Ordering::SeqCst);\n\t\t\t\t}\n\t\t\t\tErr(e)\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Map a finished ticket back to its delivery slot: the audio slot for\n\t/// audio tickets, the matching frame slot for video tickets.\n\tfn classify_ticket(\n\t\t\u0026self,\n\t\tticket: \u0026OakRenderTicket,\n\t\tslot_by_key: \u0026HashMap\u003c(i32, i64, i64), usize\u003e,\n\t) -\u003e Option\u003cusize\u003e {\n\t\tlet kind = unsafe { bridge::render::oakrender_ticket_get_type(*ticket) };\n\t\tif kind == TICKET_AUDIO {\n\t\t\tlet mut a = 0i64;\n\t\t\tlet mut b = 1i64;\n\t\t\tlet mut c = 0i64;\n\t\t\tlet mut d = 1i64;\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_ticket_get_range(*ticket, \u0026mut a, \u0026mut b, \u0026mut c, \u0026mut d);\n\t\t\t}\n\t\t\tslot_by_key.get(\u0026(TICKET_AUDIO, a, b)).copied()\n\t\t} else if kind == TICKET_VIDEO {\n\t\t\tlet mut n = 0i64;\n\t\t\tlet mut d = 1i64;\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_ticket_get_time(*ticket, \u0026mut n, \u0026mut d);\n\t\t\t}\n\t\t\tslot_by_key.get(\u0026(TICKET_VIDEO, n, d)).copied()\n\t\t} else {\n\t\t\tNone\n\t\t}\n\t}\n\n\t/// Drive the whole render: keep up to `max_inflight` frame tickets in\n\t/// flight (audio first, then frames in timestamp order), deliver each\n\t/// finished ticket's result to the behavior hooks in timestamp order via\n\t/// the reorder buffer, and stop on cancellation or a hook error —\n\t/// cancelling and waiting every still-running ticket so their\n\t/// completions fire exactly once.\n\t///\n\t/// `task` is the live driving task (cancellation/progress/error\n\t/// reporting); `behavior` is the concrete subclass receiving the\n\t/// `frame_downloaded`/`audio_downloaded` hooks.\n\tpub fn render(\u0026mut self, task: \u0026mut Task, behavior: \u0026mut dyn RenderTaskBehavior) -\u003e Result\u003c()\u003e {\n\t\tlet timebase = self.timebase();\n\n\t\t// Compute the frame timestamps (progress denominator mirrors the\n\t\t// C++ `total_length \u003c= 0 -\u003e 1` guard).\n\t\tlet mut frame_times: Vec\u003cRational\u003e = Vec::new();\n\t\tlet mut t = self.export_range.in_();\n\t\twhile t \u003c self.export_range.out() {\n\t\t\tframe_times.push(t);\n\t\t\tt = t + timebase;\n\t\t}\n\t\tself.total_frames = frame_times.len() as i64;\n\t\tlet total_length = if frame_times.is_empty() { 1.0 } else { frame_times.len() as f64 };\n\n\t\t// Delivery order: the audio range first (mirrors the C++ queue\n\t\t// order), then every frame in ascending timestamp order. The reorder\n\t\t// buffer delivers to the hooks in exactly this order no matter the\n\t\t// completion order.\n\t\tlet mut slots: Vec\u003cDeliverySlot\u003e = Vec::new();\n\t\tif self.audio_enabled {\n\t\t\tslots.push(DeliverySlot {\n\t\t\t\tkind: TICKET_AUDIO,\n\t\t\t\ttime: self.export_range.in_(),\n\t\t\t});\n\t\t}\n\t\tfor \u0026ft in \u0026frame_times {\n\t\t\tslots.push(DeliverySlot {\n\t\t\t\tkind: TICKET_VIDEO,\n\t\t\t\ttime: ft,\n\t\t\t});\n\t\t}\n\t\tlet mut slot_by_key: HashMap\u003c(i32, i64, i64), usize\u003e = HashMap::with_capacity(slots.len());\n\t\tfor (i, slot) in slots.iter().enumerate() {\n\t\t\tslot_by_key.insert((slot.kind, slot.time.numerator(), slot.time.denominator()), i);\n\t\t}\n\t\tlet total_slots = slots.len();\n\n\t\t// The shared completion channel. Heap-allocated: the C callback\n\t\t// reaches it through its `userdata` pointer. Freed only once every\n\t\t// in-flight ticket has fired its callback (`running == 0`).\n\t\tlet dispatch = Box::into_raw(Box::new(RenderDispatch::new()));\n\t\t// Shared view of the same state for the render thread (the raw\n\t\t// pointer stays valid until the box is freed below).\n\t\tlet dispatch_ref = unsafe { \u0026*dispatch };\n\t\tlet window = self.max_inflight.max(1);\n\n\t\t// Tickets we hold (the submitter's copy of each handle, released\n\t\t// exactly once at the end); `consumed` counts tickets whose finished\n\t\t// queue copy has been popped, so `in_flight.len() - consumed` is the\n\t\t// live window.\n\t\tlet mut in_flight: Vec\u003cOakRenderTicket\u003e = Vec::new();\n\t\tlet mut consumed = 0usize;\n\t\t// Next frame timestamp to submit and next delivery slot.\n\t\tlet mut next_frame_index = 0usize;\n\t\tlet mut next_slot = 0usize;\n\t\t// Reorder buffer: finished tickets not yet deliverable.\n\t\tlet mut pending: HashMap\u003cusize, OakRenderTicket\u003e = HashMap::new();\n\t\tlet mut progress_counter = 0.0;\n\t\tlet mut result: Result\u003c()\u003e = Ok(());\n\n\t\t// Queue audio first (mirrors the C++ order).\n\t\tif self.audio_enabled \u0026\u0026 result.is_ok() {\n\t\t\tif let Err(e) = self.start_audio_ticket(self.export_range, dispatch, \u0026mut in_flight) {\n\t\t\t\tresult = Err(e);\n\t\t\t}\n\t\t}\n\n\t\t// Start the initial frame window.\n\t\tif result.is_ok() {\n\t\t\twhile in_flight.len() \u003c window \u0026\u0026 next_frame_index \u003c frame_times.len() {\n\t\t\t\tif let Err(e) =\n\t\t\t\t\tself.start_video_ticket(frame_times[next_frame_index], dispatch, \u0026mut in_flight)\n\t\t\t\t{\n\t\t\t\t\tresult = Err(e);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tnext_frame_index += 1;\n\t\t\t}\n\t\t}\n\n\t\twhile result.is_ok() \u0026\u0026 !task.is_cancelled() \u0026\u0026 next_slot \u003c total_slots {\n\t\t\t// Drain the completion queue into the reorder buffer.\n\t\t\twhile let Some(ticket) = dispatch_ref.pop_finished() {\n\t\t\t\tconsumed += 1;\n\t\t\t\tmatch self.classify_ticket(\u0026ticket, \u0026slot_by_key) {\n\t\t\t\t\tSome(slot_index) =\u003e {\n\t\t\t\t\t\tpending.insert(slot_index, ticket);\n\t\t\t\t\t}\n\t\t\t\t\tNone =\u003e {\n\t\t\t\t\t\tresult = Err(Error::Failed(\n\t\t\t\t\t\t\t\"Render ticket reported an unexpected timestamp\".to_string(),\n\t\t\t\t\t\t));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif result.is_err() {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Deliver contiguous results in the observable (timestamp) order.\n\t\t\twhile let Some(ticket) = pending.remove(\u0026next_slot) {\n\t\t\t\tlet slot = \u0026slots[next_slot];\n\t\t\t\tif slot.kind == TICKET_AUDIO {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::render::oakrender_ticket_get_samples(ticket, std::ptr::null_mut());\n\t\t\t\t\t}\n\t\t\t\t\tif let Err(e) = behavior.audio_downloaded(task, CHandle::null()) {\n\t\t\t\t\t\tresult = Err(e);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlet mut frame = CHandle::null();\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::render::oakrender_ticket_get_frame(ticket, \u0026mut frame);\n\t\t\t\t\t}\n\t\t\t\t\tif let Err(e) = behavior.frame_downloaded(task, frame) {\n\t\t\t\t\t\tresult = Err(e);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif !frame.ctx.is_null() {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::render::oakrender_codec_frame_free(\u0026mut frame);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif self.native_progress_signalling {\n\t\t\t\t\t\tprogress_counter += 1.0;\n\t\t\t\t\t\ttask.emit_progress(progress_counter / total_length);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnext_slot += 1;\n\t\t\t}\n\t\t\tif result.is_err() || task.is_cancelled() {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Refill the window: one new ticket per delivered frame.\n\t\t\twhile in_flight.len().saturating_sub(consumed) \u003c window\n\t\t\t\t\u0026\u0026 next_frame_index \u003c frame_times.len()\n\t\t\t{\n\t\t\t\tif let Err(e) =\n\t\t\t\t\tself.start_video_ticket(frame_times[next_frame_index], dispatch, \u0026mut in_flight)\n\t\t\t\t{\n\t\t\t\t\tresult = Err(e);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tnext_frame_index += 1;\n\t\t\t}\n\t\t\tif result.is_err() {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif next_slot \u003e= total_slots {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Wait for the next completion (a cancellation or a hook error\n\t\t\t// aborts the wait; in-flight tickets then finish, waking us).\n\t\t\tlet mut guard = dispatch_ref.finished.lock().unwrap_or_else(|e| e.into_inner());\n\t\t\twhile guard.is_empty()\n\t\t\t\t\u0026\u0026 dispatch_ref.running.load(Ordering::SeqCst) \u003e 0\n\t\t\t\t\u0026\u0026 !task.is_cancelled()\n\t\t\t{\n\t\t\t\tguard = dispatch_ref.cv.wait(guard).unwrap_or_else(|e| e.into_inner());\n\t\t\t}\n\t\t\tdrop(guard);\n\t\t\tif dispatch_ref.finished.lock().unwrap_or_else(|e| e.into_inner()).is_empty()\n\t\t\t\t\u0026\u0026 dispatch_ref.running.load(Ordering::SeqCst) == 0\n\t\t\t{\n\t\t\t\t// Every ticket finished and its queue copy was consumed.\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Cancellation that aborted the loop before every slot was delivered\n\t\t// maps to the cancellation error (the sync loop returned\n\t\t// `Error::Cancelled` from its between-frames check; a render that\n\t\t// finished every frame despite a late cancel stays successful).\n\t\tif result.is_ok() \u0026\u0026 task.is_cancelled() \u0026\u0026 next_slot \u003c total_slots {\n\t\t\tresult = Err(Error::Cancelled);\n\t\t}\n\n\t\t// Tear down. On cancellation or error, cancel and wait every ticket\n\t\t// still in flight (the C++ abort path), so their completions still\n\t\t// fire exactly once. Then wait until every callback has returned,\n\t\t// release the submitter's handles, and free the completion channel.\n\t\tif result.is_err() || task.is_cancelled() {\n\t\t\tfor \u0026ticket in \u0026in_flight {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::render::oakrender_ticket_cancel(ticket);\n\t\t\t\t\tbridge::render::oakrender_ticket_wait(ticket);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdispatch_ref.wait_idle();\n\t\tfor ticket in in_flight.iter_mut() {\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_ticket_free(ticket);\n\t\t\t}\n\t\t}\n\t\tunsafe {\n\t\t\tdrop(Box::from_raw(dispatch));\n\t\t}\n\n\t\tresult\n\t}\n\n\t/// A detached render state used while a concrete task temporarily moves\n\t/// its render out of itself to drive it with itself as the behavior\n\t/// (avoids a self-referential borrow). Never used for actual rendering.\n\tpub(crate) fn placeholder() -\u003e RenderTask {\n\t\tRenderTask::new(\n\t\t\tTask::new(\"\", CHandle::null()),\n\t\t\tCHandle::null(),\n\t\t\tCHandle::null(),\n\t\t\tCHandle::null(),\n\t\t\tForceParams::default(),\n\t\t\tNone,\n\t\t)\n\t}\n}\n\n/// One deliverable unit of the render: an audio range or a frame time, in\n/// the observable delivery order (audio first, then frames in time order).\nstruct DeliverySlot {\n\t/// `TICKET_AUDIO` or `TICKET_VIDEO`.\n\tkind: i32,\n\t/// Frame time (video) or range start (audio).\n\ttime: Rational,\n}\n\n/// Shared state between the render thread and the ticket-finished\n/// callbacks (the ticket's async return channel, mirroring the C++\n/// `finished_mutex_`/`finished_tickets_`/`finished_wait_cond_`). One\n/// instance per [`RenderTask::render`] run, reached from the C callback\n/// through its `userdata` pointer; heap-allocated for the run and freed\n/// only after every in-flight ticket has fired (`running == 0`).\nstruct RenderDispatch {\n\t/// Finished tickets in completion order (callbacks push here).\n\tfinished: Mutex\u003cVecDeque\u003cOakRenderTicket\u003e\u003e,\n\t/// Tickets whose completion callback has not fired yet.\n\trunning: AtomicUsize,\n\t/// Wakes the render thread when a ticket finishes.\n\tcv: Condvar,\n}\n\nimpl RenderDispatch {\n\tfn new() -\u003e RenderDispatch {\n\t\tRenderDispatch {\n\t\t\tfinished: Mutex::new(VecDeque::new()),\n\t\t\trunning: AtomicUsize::new(0),\n\t\t\tcv: Condvar::new(),\n\t\t}\n\t}\n\n\t/// Pop the next finished ticket (the callback's borrowed copy; `None`\n\t/// when the queue is empty).\n\tfn pop_finished(\u0026self) -\u003e Option\u003cOakRenderTicket\u003e {\n\t\tself.finished\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.pop_front()\n\t}\n\n\t/// Block until every submitted ticket has fired its callback. Called\n\t/// before freeing `self`, so no callback can touch the state afterwards.\n\tfn wait_idle(\u0026self) {\n\t\tlet mut guard = self.finished.lock().unwrap_or_else(|e| e.into_inner());\n\t\twhile self.running.load(Ordering::SeqCst) \u003e 0 {\n\t\t\tguard = self.cv.wait(guard).unwrap_or_else(|e| e.into_inner());\n\t\t}\n\t}\n}\n\n/// Ticket-finished callback, mirroring the C++ `on_ticket_finished`. Fires\n/// on the ticket's finishing thread; pushes the ticket's borrowed handle\n/// copy into the completion queue and wakes the render thread. The push\n/// precedes the in-flight decrement, so a waiter never observes\n/// `running == 0` with a missing queue entry.\n///\n/// # Safety\n///\n/// `userdata` must point to a live `RenderDispatch` for the duration of\n/// every in-flight ticket (guaranteed by [`RenderTask::render`]'s\n/// `wait_idle` before freeing it).\nunsafe extern \"C\" fn ticket_finished(ticket: OakRenderTicket, userdata: *mut std::ffi::c_void) {\n\tlet dispatch = unsafe { \u0026*(userdata as *const RenderDispatch) };\n\tlet mut queue = dispatch.finished.lock().unwrap_or_else(|e| e.into_inner());\n\tqueue.push_back(ticket);\n\tdispatch.running.fetch_sub(1, Ordering::SeqCst);\n\tdispatch.cv.notify_all();\n}\n","traces":[{"line":140,"address":[],"length":0,"stats":{"Line":23}},{"line":155,"address":[],"length":0,"stats":{"Line":23}},{"line":156,"address":[],"length":0,"stats":{"Line":23}},{"line":159,"address":[],"length":0,"stats":{"Line":69}},{"line":162,"address":[],"length":0,"stats":{"Line":138}},{"line":168,"address":[],"length":0,"stats":{"Line":14}},{"line":176,"address":[],"length":0,"stats":{"Line":14}},{"line":177,"address":[],"length":0,"stats":{"Line":14}},{"line":178,"address":[],"length":0,"stats":{"Line":14}},{"line":179,"address":[],"length":0,"stats":{"Line":14}},{"line":180,"address":[],"length":0,"stats":{"Line":14}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":194,"address":[],"length":0,"stats":{"Line":7}},{"line":195,"address":[],"length":0,"stats":{"Line":7}},{"line":199,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":0}},{"line":205,"address":[],"length":0,"stats":{"Line":14}},{"line":206,"address":[],"length":0,"stats":{"Line":28}},{"line":207,"address":[],"length":0,"stats":{"Line":28}},{"line":210,"address":[],"length":0,"stats":{"Line":28}},{"line":211,"address":[],"length":0,"stats":{"Line":14}},{"line":212,"address":[],"length":0,"stats":{"Line":14}},{"line":215,"address":[],"length":0,"stats":{"Line":27}},{"line":216,"address":[],"length":0,"stats":{"Line":1}},{"line":218,"address":[],"length":0,"stats":{"Line":39}},{"line":225,"address":[],"length":0,"stats":{"Line":84}},{"line":230,"address":[],"length":0,"stats":{"Line":168}},{"line":233,"address":[],"length":0,"stats":{"Line":168}},{"line":234,"address":[],"length":0,"stats":{"Line":168}},{"line":235,"address":[],"length":0,"stats":{"Line":84}},{"line":238,"address":[],"length":0,"stats":{"Line":168}},{"line":239,"address":[],"length":0,"stats":{"Line":1}},{"line":242,"address":[],"length":0,"stats":{"Line":249}},{"line":243,"address":[],"length":0,"stats":{"Line":83}},{"line":245,"address":[],"length":0,"stats":{"Line":0}},{"line":247,"address":[],"length":0,"stats":{"Line":166}},{"line":250,"address":[],"length":0,"stats":{"Line":83}},{"line":252,"address":[],"length":0,"stats":{"Line":166}},{"line":253,"address":[],"length":0,"stats":{"Line":166}},{"line":254,"address":[],"length":0,"stats":{"Line":83}},{"line":255,"address":[],"length":0,"stats":{"Line":83}},{"line":256,"address":[],"length":0,"stats":{"Line":83}},{"line":257,"address":[],"length":0,"stats":{"Line":83}},{"line":258,"address":[],"length":0,"stats":{"Line":83}},{"line":259,"address":[],"length":0,"stats":{"Line":166}},{"line":260,"address":[],"length":0,"stats":{"Line":83}},{"line":261,"address":[],"length":0,"stats":{"Line":83}},{"line":262,"address":[],"length":0,"stats":{"Line":166}},{"line":263,"address":[],"length":0,"stats":{"Line":83}},{"line":269,"address":[],"length":0,"stats":{"Line":166}},{"line":274,"address":[],"length":0,"stats":{"Line":83}},{"line":275,"address":[],"length":0,"stats":{"Line":83}},{"line":276,"address":[],"length":0,"stats":{"Line":83}},{"line":282,"address":[],"length":0,"stats":{"Line":83}},{"line":284,"address":[],"length":0,"stats":{"Line":166}},{"line":285,"address":[],"length":0,"stats":{"Line":0}},{"line":287,"address":[],"length":0,"stats":{"Line":83}},{"line":292,"address":[],"length":0,"stats":{"Line":2}},{"line":297,"address":[],"length":0,"stats":{"Line":4}},{"line":300,"address":[],"length":0,"stats":{"Line":4}},{"line":301,"address":[],"length":0,"stats":{"Line":4}},{"line":302,"address":[],"length":0,"stats":{"Line":2}},{"line":305,"address":[],"length":0,"stats":{"Line":4}},{"line":306,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":6}},{"line":310,"address":[],"length":0,"stats":{"Line":2}},{"line":312,"address":[],"length":0,"stats":{"Line":0}},{"line":316,"address":[],"length":0,"stats":{"Line":2}},{"line":317,"address":[],"length":0,"stats":{"Line":6}},{"line":318,"address":[],"length":0,"stats":{"Line":6}},{"line":319,"address":[],"length":0,"stats":{"Line":6}},{"line":320,"address":[],"length":0,"stats":{"Line":6}},{"line":321,"address":[],"length":0,"stats":{"Line":2}},{"line":322,"address":[],"length":0,"stats":{"Line":2}},{"line":323,"address":[],"length":0,"stats":{"Line":2}},{"line":324,"address":[],"length":0,"stats":{"Line":2}},{"line":328,"address":[],"length":0,"stats":{"Line":2}},{"line":330,"address":[],"length":0,"stats":{"Line":4}},{"line":331,"address":[],"length":0,"stats":{"Line":0}},{"line":333,"address":[],"length":0,"stats":{"Line":2}},{"line":343,"address":[],"length":0,"stats":{"Line":84}},{"line":350,"address":[],"length":0,"stats":{"Line":168}},{"line":352,"address":[],"length":0,"stats":{"Line":252}},{"line":353,"address":[],"length":0,"stats":{"Line":83}},{"line":354,"address":[],"length":0,"stats":{"Line":249}},{"line":355,"address":[],"length":0,"stats":{"Line":83}},{"line":357,"address":[],"length":0,"stats":{"Line":1}},{"line":359,"address":[],"length":0,"stats":{"Line":2}},{"line":361,"address":[],"length":0,"stats":{"Line":1}},{"line":368,"address":[],"length":0,"stats":{"Line":2}},{"line":375,"address":[],"length":0,"stats":{"Line":4}},{"line":377,"address":[],"length":0,"stats":{"Line":6}},{"line":378,"address":[],"length":0,"stats":{"Line":2}},{"line":379,"address":[],"length":0,"stats":{"Line":6}},{"line":380,"address":[],"length":0,"stats":{"Line":2}},{"line":382,"address":[],"length":0,"stats":{"Line":0}},{"line":384,"address":[],"length":0,"stats":{"Line":0}},{"line":386,"address":[],"length":0,"stats":{"Line":0}},{"line":393,"address":[],"length":0,"stats":{"Line":82}},{"line":398,"address":[],"length":0,"stats":{"Line":246}},{"line":399,"address":[],"length":0,"stats":{"Line":82}},{"line":400,"address":[],"length":0,"stats":{"Line":4}},{"line":401,"address":[],"length":0,"stats":{"Line":4}},{"line":402,"address":[],"length":0,"stats":{"Line":4}},{"line":403,"address":[],"length":0,"stats":{"Line":4}},{"line":405,"address":[],"length":0,"stats":{"Line":10}},{"line":407,"address":[],"length":0,"stats":{"Line":8}},{"line":408,"address":[],"length":0,"stats":{"Line":80}},{"line":409,"address":[],"length":0,"stats":{"Line":160}},{"line":410,"address":[],"length":0,"stats":{"Line":160}},{"line":412,"address":[],"length":0,"stats":{"Line":240}},{"line":414,"address":[],"length":0,"stats":{"Line":320}},{"line":416,"address":[],"length":0,"stats":{"Line":0}},{"line":430,"address":[],"length":0,"stats":{"Line":14}},{"line":431,"address":[],"length":0,"stats":{"Line":42}},{"line":435,"address":[],"length":0,"stats":{"Line":42}},{"line":436,"address":[],"length":0,"stats":{"Line":42}},{"line":437,"address":[],"length":0,"stats":{"Line":391}},{"line":438,"address":[],"length":0,"stats":{"Line":484}},{"line":439,"address":[],"length":0,"stats":{"Line":121}},{"line":441,"address":[],"length":0,"stats":{"Line":14}},{"line":442,"address":[],"length":0,"stats":{"Line":56}},{"line":448,"address":[],"length":0,"stats":{"Line":42}},{"line":449,"address":[],"length":0,"stats":{"Line":16}},{"line":450,"address":[],"length":0,"stats":{"Line":6}},{"line":451,"address":[],"length":0,"stats":{"Line":2}},{"line":452,"address":[],"length":0,"stats":{"Line":2}},{"line":455,"address":[],"length":0,"stats":{"Line":256}},{"line":456,"address":[],"length":0,"stats":{"Line":242}},{"line":457,"address":[],"length":0,"stats":{"Line":121}},{"line":458,"address":[],"length":0,"stats":{"Line":121}},{"line":461,"address":[],"length":0,"stats":{"Line":70}},{"line":462,"address":[],"length":0,"stats":{"Line":397}},{"line":463,"address":[],"length":0,"stats":{"Line":738}},{"line":465,"address":[],"length":0,"stats":{"Line":42}},{"line":470,"address":[],"length":0,"stats":{"Line":56}},{"line":473,"address":[],"length":0,"stats":{"Line":28}},{"line":474,"address":[],"length":0,"stats":{"Line":42}},{"line":480,"address":[],"length":0,"stats":{"Line":42}},{"line":481,"address":[],"length":0,"stats":{"Line":28}},{"line":483,"address":[],"length":0,"stats":{"Line":28}},{"line":484,"address":[],"length":0,"stats":{"Line":28}},{"line":486,"address":[],"length":0,"stats":{"Line":42}},{"line":487,"address":[],"length":0,"stats":{"Line":28}},{"line":488,"address":[],"length":0,"stats":{"Line":42}},{"line":491,"address":[],"length":0,"stats":{"Line":18}},{"line":492,"address":[],"length":0,"stats":{"Line":8}},{"line":493,"address":[],"length":0,"stats":{"Line":0}},{"line":498,"address":[],"length":0,"stats":{"Line":28}},{"line":499,"address":[],"length":0,"stats":{"Line":280}},{"line":500,"address":[],"length":0,"stats":{"Line":1}},{"line":501,"address":[],"length":0,"stats":{"Line":244}},{"line":503,"address":[],"length":0,"stats":{"Line":2}},{"line":504,"address":[],"length":0,"stats":{"Line":1}},{"line":506,"address":[],"length":0,"stats":{"Line":60}},{"line":510,"address":[],"length":0,"stats":{"Line":101}},{"line":512,"address":[],"length":0,"stats":{"Line":186}},{"line":513,"address":[],"length":0,"stats":{"Line":82}},{"line":514,"address":[],"length":0,"stats":{"Line":246}},{"line":515,"address":[],"length":0,"stats":{"Line":164}},{"line":516,"address":[],"length":0,"stats":{"Line":246}},{"line":519,"address":[],"length":0,"stats":{"Line":0}},{"line":520,"address":[],"length":0,"stats":{"Line":0}},{"line":522,"address":[],"length":0,"stats":{"Line":0}},{"line":526,"address":[],"length":0,"stats":{"Line":44}},{"line":527,"address":[],"length":0,"stats":{"Line":0}},{"line":531,"address":[],"length":0,"stats":{"Line":251}},{"line":532,"address":[],"length":0,"stats":{"Line":142}},{"line":533,"address":[],"length":0,"stats":{"Line":71}},{"line":535,"address":[],"length":0,"stats":{"Line":4}},{"line":537,"address":[],"length":0,"stats":{"Line":6}},{"line":538,"address":[],"length":0,"stats":{"Line":0}},{"line":539,"address":[],"length":0,"stats":{"Line":0}},{"line":542,"address":[],"length":0,"stats":{"Line":138}},{"line":544,"address":[],"length":0,"stats":{"Line":138}},{"line":546,"address":[],"length":0,"stats":{"Line":210}},{"line":547,"address":[],"length":0,"stats":{"Line":6}},{"line":548,"address":[],"length":0,"stats":{"Line":3}},{"line":550,"address":[],"length":0,"stats":{"Line":66}},{"line":552,"address":[],"length":0,"stats":{"Line":59}},{"line":555,"address":[],"length":0,"stats":{"Line":132}},{"line":556,"address":[],"length":0,"stats":{"Line":132}},{"line":557,"address":[],"length":0,"stats":{"Line":198}},{"line":560,"address":[],"length":0,"stats":{"Line":68}},{"line":562,"address":[],"length":0,"stats":{"Line":82}},{"line":563,"address":[],"length":0,"stats":{"Line":3}},{"line":567,"address":[],"length":0,"stats":{"Line":168}},{"line":568,"address":[],"length":0,"stats":{"Line":68}},{"line":570,"address":[],"length":0,"stats":{"Line":0}},{"line":571,"address":[],"length":0,"stats":{"Line":92}},{"line":573,"address":[],"length":0,"stats":{"Line":0}},{"line":574,"address":[],"length":0,"stats":{"Line":0}},{"line":576,"address":[],"length":0,"stats":{"Line":23}},{"line":578,"address":[],"length":0,"stats":{"Line":38}},{"line":579,"address":[],"length":0,"stats":{"Line":0}},{"line":581,"address":[],"length":0,"stats":{"Line":19}},{"line":582,"address":[],"length":0,"stats":{"Line":7}},{"line":587,"address":[],"length":0,"stats":{"Line":48}},{"line":588,"address":[],"length":0,"stats":{"Line":19}},{"line":589,"address":[],"length":0,"stats":{"Line":14}},{"line":590,"address":[],"length":0,"stats":{"Line":7}},{"line":592,"address":[],"length":0,"stats":{"Line":35}},{"line":594,"address":[],"length":0,"stats":{"Line":24}},{"line":595,"address":[],"length":0,"stats":{"Line":48}},{"line":596,"address":[],"length":0,"stats":{"Line":0}},{"line":599,"address":[],"length":0,"stats":{"Line":0}},{"line":607,"address":[],"length":0,"stats":{"Line":50}},{"line":608,"address":[],"length":0,"stats":{"Line":1}},{"line":615,"address":[],"length":0,"stats":{"Line":46}},{"line":616,"address":[],"length":0,"stats":{"Line":32}},{"line":618,"address":[],"length":0,"stats":{"Line":81}},{"line":619,"address":[],"length":0,"stats":{"Line":27}},{"line":623,"address":[],"length":0,"stats":{"Line":28}},{"line":624,"address":[],"length":0,"stats":{"Line":99}},{"line":626,"address":[],"length":0,"stats":{"Line":85}},{"line":630,"address":[],"length":0,"stats":{"Line":28}},{"line":633,"address":[],"length":0,"stats":{"Line":14}},{"line":639,"address":[],"length":0,"stats":{"Line":7}},{"line":641,"address":[],"length":0,"stats":{"Line":21}},{"line":642,"address":[],"length":0,"stats":{"Line":7}},{"line":643,"address":[],"length":0,"stats":{"Line":7}},{"line":644,"address":[],"length":0,"stats":{"Line":7}},{"line":645,"address":[],"length":0,"stats":{"Line":7}},{"line":646,"address":[],"length":0,"stats":{"Line":7}},{"line":676,"address":[],"length":0,"stats":{"Line":14}},{"line":678,"address":[],"length":0,"stats":{"Line":42}},{"line":679,"address":[],"length":0,"stats":{"Line":14}},{"line":680,"address":[],"length":0,"stats":{"Line":14}},{"line":686,"address":[],"length":0,"stats":{"Line":104}},{"line":687,"address":[],"length":0,"stats":{"Line":104}},{"line":689,"address":[],"length":0,"stats":{"Line":104}},{"line":695,"address":[],"length":0,"stats":{"Line":14}},{"line":696,"address":[],"length":0,"stats":{"Line":56}},{"line":697,"address":[],"length":0,"stats":{"Line":28}},{"line":698,"address":[],"length":0,"stats":{"Line":0}},{"line":714,"address":[],"length":0,"stats":{"Line":85}},{"line":715,"address":[],"length":0,"stats":{"Line":170}},{"line":716,"address":[],"length":0,"stats":{"Line":340}},{"line":717,"address":[],"length":0,"stats":{"Line":170}},{"line":718,"address":[],"length":0,"stats":{"Line":255}},{"line":719,"address":[],"length":0,"stats":{"Line":170}}],"covered":215,"coverable":242},{"path":["/","Users","sunyu","Projects","oak","src","task","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//! The `Task` base class, mirroring `src/task/src/task.h` (`olive::Task`).\n//!\n//! The C++ class is abstract with a protected virtual `run()`. In Rust each\n//! concrete task (`ConformTask`, `ProxyTask`, …) is its own struct and the\n//! shared lifecycle lives here; the per-task work is supplied through\n//! [`TaskBehavior`] (a trait object, per architectural decision #1 in\n//! README.md). Cancellation rides on a borrowed oakrender `OakCancelAtom`\n//! reached through `crate::bridge::render`.\n//!\n//! CPP-PARITY: src/task/src/task.h\n//!\n//! ## Concurrency contract\n//!\n//! Like the C++ original, the task's `title`/`error` strings are mutated on\n//! the task's own thread (`run()`). The C ABI layer must therefore only read\n//! them while the task is not concurrently running — i.e. before\n//! [`Task::start`] or after the task finished (the `finished` flag is\n//! synchronized through the `done` condvar, so `is_finished()`/\n//! `succeeded()` are always race-free). This mirrors the C++ semantics\n//! exactly.\n\nuse std::sync::atomic::{AtomicI64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex};\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\n\n/// Event type emitted through a task's [`EventListener`], mirroring the\n/// C++ `EventType` enum (`k_event_started`/`k_event_progress`/`k_event_finished`).\n///\n/// CPP-PARITY: src/task/src/task.h (EventType)\n#[derive(Clone, Copy, Debug, PartialEq)]\npub enum TaskEvent {\n\t/// Task began executing.\n\tStarted,\n\t/// Progress updated; the payload is in 0.0..=1.0.\n\tProgress(f64),\n\t/// Task finished (success or failure).\n\tFinished,\n}\n\n/// A listener notified of [`TaskEvent`]s, mirroring the C++\n/// `std::function\u003cvoid(EventType,double)\u003e` listener. Delivered under the\n/// task's lock; kept as the single async return channel (decision #3).\n///\n/// CPP-PARITY: src/task/src/task.h (EventListener)\npub type EventListener = Box\u003cdyn FnMut(TaskEvent) + Send\u003e;\n\n/// Trait supplying the per-task work, standing in for the C++ protected\n/// virtual `run()`. Concrete tasks implement this; [`Task::start`] drives it.\n///\n/// CPP-PARITY: src/task/src/task.h (run)\npub trait TaskBehavior {\n\t/// Perform the task's work. Returning `Err(Error::Cancelled)` is treated\n\t/// as a cancellation; any other `Err` marks the task failed.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e;\n}\n\n/// Values the subscribe wrapper needs to re-encode [`TaskEvent`]s into the\n/// C ABI callback signature `(event_id, value, userdata)`. The C++ emits\n/// the start timestamp with `k_event_started` and 1.0/0.0 with\n/// `k_event_finished`; `TaskEvent` carries neither, so the task publishes\n/// them into this shared state before emitting (decision #1 in README.md).\n#[derive(Debug, Default)]\npub struct SubscriberState {\n\t/// Start timestamp (ms since epoch) published before `Started` is emitted.\n\tpub start_ms: AtomicI64,\n\t/// 1.0/0.0 published before `Finished` is emitted.\n\tpub finished_value: AtomicI64,\n}\n\n/// Finish state, protected by the `done` condvar pair so `is_finished`/\n/// `succeeded` are readable from any thread while the task thread mutates\n/// them.\n#[derive(Default)]\nstruct TaskDone {\n\t/// Whether the task has finished (success or failure).\n\tfinished: bool,\n\t/// Whether the task finished successfully.\n\tsucceeded: bool,\n}\n\n/// The base task. Owns lifecycle state plus a borrowed oakrender cancel\n/// atom; the concrete behavior lives in a [`TaskBehavior`] trait object.\npub struct Task {\n\ttitle: String,\n\terror: Option\u003cString\u003e,\n\tstart_time: Option\u003cstd::time::Instant\u003e,\n\tcancel_atom: CHandle,\n\towns_atom: bool,\n\tevent_listener: Option\u003cEventListener\u003e,\n\tcancel_event: Option\u003cBox\u003cdyn FnMut() + Send\u003e\u003e,\n\tstarted: bool,\n\tfinished: bool,\n\tsucceeded: bool,\n\tbehavior: Option\u003cBox\u003cdyn TaskBehavior + Send\u003e\u003e,\n\t/// Finish/success flag pair + wakeup condvar (race-free readers).\n\tdone: Arc\u003c(Mutex\u003cTaskDone\u003e, Condvar)\u003e,\n\t/// Values published for the C ABI subscribe wrapper.\n\tsubscriber: Option\u003cArc\u003cSubscriberState\u003e\u003e,\n}\n\nimpl Drop for Task {\n\tfn drop(\u0026mut self) {\n\t\t// Free the cancellation atom only when we created it ourselves;\n\t\t// borrowed atoms are released by their owner.\n\t\tif self.owns_atom \u0026\u0026 !self.cancel_atom.is_null() {\n\t\t\tlet mut atom = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cancelatom_free(\u0026mut atom);\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl Task {\n\t/// Create a new task with the given title and a borrowed oakrender\n\t/// cancel atom. When `cancel_atom` is empty a fresh atom is created and\n\t/// owned by the task (mirroring the C++ constructor).\n\tpub fn new(title: \u0026str, cancel_atom: CHandle) -\u003e Task {\n\t\tlet (cancel_atom, owns_atom) = if cancel_atom.is_null() {\n\t\t\tlet atom = unsafe { bridge::render::oakrender_cancelatom_init() };\n\t\t\t(atom.to_chandle(), true)\n\t\t} else {\n\t\t\t(cancel_atom, false)\n\t\t};\n\t\tTask {\n\t\t\ttitle: title.to_string(),\n\t\t\terror: None,\n\t\t\tstart_time: None,\n\t\t\tcancel_atom,\n\t\t\towns_atom,\n\t\t\tevent_listener: None,\n\t\t\tcancel_event: None,\n\t\t\tstarted: false,\n\t\t\tfinished: false,\n\t\t\tsucceeded: false,\n\t\t\tbehavior: None,\n\t\t\tdone: Arc::new((Mutex::new(TaskDone::default()), Condvar::new())),\n\t\t\tsubscriber: None,\n\t\t}\n\t}\n\n\t/// Attach the concrete behavior (defaults to a no-op).\n\tpub fn set_behavior(\u0026mut self, behavior: Box\u003cdyn TaskBehavior + Send\u003e) {\n\t\tself.behavior = Some(behavior);\n\t}\n\n\t/// Start the task: set `started`, emit [`TaskEvent::Started`], run the\n\t/// behavior, then mark finished and emit [`TaskEvent::Finished`].\n\t///\n\t/// The returned `Result` mirrors the C++ `start()` bool: `Ok(())` when\n\t/// the behavior succeeded, `Err(..)` otherwise.\n\tpub fn start(\u0026mut self) -\u003e Result\u003c()\u003e {\n\t\tlet start_ms = system_time_ms();\n\t\tself.start_time = Some(std::time::Instant::now());\n\t\tself.started = true;\n\t\tif let Some(s) = \u0026self.subscriber {\n\t\t\ts.start_ms.store(start_ms, Ordering::SeqCst);\n\t\t}\n\t\tself.emit_event(TaskEvent::Started);\n\n\t\t// Take the behavior out so it can receive `self` (avoids a\n\t\t// self-referential borrow); put it back afterwards.\n\t\tlet behavior = self.behavior.take();\n\t\tlet ret = if let Some(mut b) = behavior {\n\t\t\tlet r = b.run(self);\n\t\t\tself.behavior = Some(b);\n\t\t\tr\n\t\t} else {\n\t\t\tOk(())\n\t\t};\n\n\t\tlet succeeded = ret.is_ok();\n\t\t{\n\t\t\tlet mut done = self.done.0.lock().unwrap();\n\t\t\tdone.finished = true;\n\t\t\tdone.succeeded = succeeded;\n\t\t\tself.done.1.notify_all();\n\t\t}\n\t\tif let Some(s) = \u0026self.subscriber {\n\t\t\ts.finished_value.store(if succeeded { 1 } else { 0 }, Ordering::SeqCst);\n\t\t}\n\t\tself.emit_event(TaskEvent::Finished);\n\n\t\t// One-shot subscription: drop the listener after the final event.\n\t\tself.event_listener = None;\n\n\t\tret\n\t}\n\n\t/// Request cancellation through the borrowed oakrender cancel atom, then\n\t/// invoke the cancel event callback if one is registered.\n\tpub fn cancel(\u0026mut self) {\n\t\tif !self.cancel_atom.is_null() {\n\t\t\tlet atom = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cancelatom_cancel(atom);\n\t\t\t}\n\t\t}\n\t\tif let Some(cb) = self.cancel_event.as_mut() {\n\t\t\tcb();\n\t\t}\n\t}\n\n\t/// Whether cancellation was requested (queries the oakrender atom).\n\tpub fn is_cancelled(\u0026self) -\u003e bool {\n\t\tif self.cancel_atom.is_null() {\n\t\t\treturn false;\n\t\t}\n\t\tlet atom = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\tlet mut cancelled = 0;\n\t\tunsafe {\n\t\t\tbridge::render::oakrender_cancelatom_is_cancelled(atom, \u0026mut cancelled);\n\t\t}\n\t\tcancelled != 0\n\t}\n\n\t/// The borrowed cancel atom handle (empty for tasks without one).\n\tpub fn get_cancel_atom(\u0026self) -\u003e CHandle {\n\t\tself.cancel_atom\n\t}\n\n\t/// Replace the cancel atom. A previously owned atom is freed; the new\n\t/// atom is borrowed (never freed by this task). Used to share one atom\n\t/// between a task and its behavior's inner base task.\n\tpub fn set_cancel_atom(\u0026mut self, atom: CHandle) {\n\t\tif self.owns_atom \u0026\u0026 !self.cancel_atom.is_null() {\n\t\t\tlet mut old = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cancelatom_free(\u0026mut old);\n\t\t\t}\n\t\t}\n\t\tself.cancel_atom = atom;\n\t\tself.owns_atom = false;\n\t}\n\n\t/// Register the event listener. Replaces any previous listener.\n\tpub fn set_event_listener(\u0026mut self, listener: EventListener) {\n\t\tself.event_listener = Some(listener);\n\t}\n\n\t/// Register a hook invoked when [`Task::cancel`] is called.\n\tpub fn set_cancel_event(\u0026mut self, cb: Box\u003cdyn FnMut() + Send\u003e) {\n\t\tself.cancel_event = Some(cb);\n\t}\n\n\t/// Publish the shared values used by the C ABI subscribe wrapper.\n\tpub fn set_subscriber(\u0026mut self, state: Arc\u003cSubscriberState\u003e) {\n\t\tself.subscriber = Some(state);\n\t}\n\n\t/// Emit a progress update, clamping to 0.0..=1.0 and delivering it to the\n\t/// listener as [`TaskEvent::Progress`].\n\tpub fn emit_progress(\u0026mut self, progress: f64) {\n\t\tself.emit_event(TaskEvent::Progress(progress.clamp(0.0, 1.0)));\n\t}\n\n\t/// Reset lifecycle state so the task can be run again.\n\tpub fn reset(\u0026mut self) {\n\t\tself.started = false;\n\t\tself.finished = false;\n\t\tself.succeeded = false;\n\t\tself.error = None;\n\t\tself.start_time = None;\n\t\t{\n\t\t\tlet mut done = self.done.0.lock().unwrap();\n\t\t\tdone.finished = false;\n\t\t\tdone.succeeded = false;\n\t\t}\n\t}\n\n\t/// Record a failure message and mark the task failed.\n\tpub fn set_error(\u0026mut self, message: \u0026str) {\n\t\tself.error = Some(message.to_string());\n\t}\n\n\t/// Change the task title.\n\tpub fn set_title(\u0026mut self, title: \u0026str) {\n\t\tself.title = title.to_string();\n\t}\n\n\t/// The task title.\n\tpub fn title(\u0026self) -\u003e \u0026str {\n\t\t\u0026self.title\n\t}\n\n\t/// The failure message, if the task failed.\n\tpub fn error(\u0026self) -\u003e Option\u003c\u0026str\u003e {\n\t\tself.error.as_deref()\n\t}\n\n\t/// Whether the task has finished (success or failure).\n\tpub fn is_finished(\u0026self) -\u003e bool {\n\t\tself.done.0.lock().unwrap().finished\n\t}\n\n\t/// Whether the task finished successfully.\n\tpub fn succeeded(\u0026self) -\u003e bool {\n\t\tself.done.0.lock().unwrap().succeeded\n\t}\n\n\t/// Block until the task finishes. Mirrors the C++ finished-wait condvar;\n\t/// the manager joins worker threads instead, but this is useful for\n\t/// direct (non-manager) runs. Returns immediately when the task has not\n\t/// been started yet (nothing to wait for) or is already finished.\n\tpub fn wait_finished(\u0026self) {\n\t\tif !self.started {\n\t\t\treturn;\n\t\t}\n\t\tlet mut done = self.done.0.lock().unwrap();\n\t\twhile !done.finished {\n\t\t\tdone = self.done.1.wait(done).unwrap();\n\t\t}\n\t}\n\n\t/// The elapsed time since [`Task::start`], for duration reporting.\n\tpub fn elapsed(\u0026self) -\u003e Option\u003cstd::time::Duration\u003e {\n\t\tself.start_time.map(|t| t.elapsed())\n\t}\n\n\tfn emit_event(\u0026mut self, ev: TaskEvent) {\n\t\tif let Some(listener) = self.event_listener.as_mut() {\n\t\t\tlistener(ev);\n\t\t}\n\t}\n}\n\n/// Current wall-clock time in milliseconds since the Unix epoch, matching\n/// the C++ `start_time_` convention (std::chrono::system_clock ms).\npub fn system_time_ms() -\u003e i64 {\n\tstd::time::SystemTime::now()\n\t\t.duration_since(std::time::UNIX_EPOCH)\n\t\t.map(|d| d.as_millis() as i64)\n\t\t.unwrap_or(0)\n}\n\n/// Marker error returned when a task is cancelled, so the C ABI can map it to\n/// `OAKTASK_E_CANCELLED` (distinct from a generic failure).\npub fn cancelled() -\u003e Error {\n\tError::Cancelled\n}\n","traces":[{"line":121,"address":[],"length":0,"stats":{"Line":184}},{"line":124,"address":[],"length":0,"stats":{"Line":270}},{"line":125,"address":[],"length":0,"stats":{"Line":258}},{"line":127,"address":[],"length":0,"stats":{"Line":86}},{"line":137,"address":[],"length":0,"stats":{"Line":184}},{"line":138,"address":[],"length":0,"stats":{"Line":736}},{"line":139,"address":[],"length":0,"stats":{"Line":190}},{"line":140,"address":[],"length":0,"stats":{"Line":95}},{"line":142,"address":[],"length":0,"stats":{"Line":89}},{"line":145,"address":[],"length":0,"stats":{"Line":552}},{"line":156,"address":[],"length":0,"stats":{"Line":552}},{"line":162,"address":[],"length":0,"stats":{"Line":82}},{"line":163,"address":[],"length":0,"stats":{"Line":164}},{"line":171,"address":[],"length":0,"stats":{"Line":76}},{"line":172,"address":[],"length":0,"stats":{"Line":152}},{"line":173,"address":[],"length":0,"stats":{"Line":76}},{"line":174,"address":[],"length":0,"stats":{"Line":76}},{"line":175,"address":[],"length":0,"stats":{"Line":78}},{"line":176,"address":[],"length":0,"stats":{"Line":3}},{"line":178,"address":[],"length":0,"stats":{"Line":228}},{"line":182,"address":[],"length":0,"stats":{"Line":228}},{"line":183,"address":[],"length":0,"stats":{"Line":228}},{"line":184,"address":[],"length":0,"stats":{"Line":304}},{"line":185,"address":[],"length":0,"stats":{"Line":152}},{"line":186,"address":[],"length":0,"stats":{"Line":76}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":191,"address":[],"length":0,"stats":{"Line":228}},{"line":193,"address":[],"length":0,"stats":{"Line":380}},{"line":194,"address":[],"length":0,"stats":{"Line":152}},{"line":195,"address":[],"length":0,"stats":{"Line":152}},{"line":196,"address":[],"length":0,"stats":{"Line":76}},{"line":198,"address":[],"length":0,"stats":{"Line":77}},{"line":199,"address":[],"length":0,"stats":{"Line":5}},{"line":201,"address":[],"length":0,"stats":{"Line":228}},{"line":204,"address":[],"length":0,"stats":{"Line":152}},{"line":206,"address":[],"length":0,"stats":{"Line":76}},{"line":211,"address":[],"length":0,"stats":{"Line":8}},{"line":212,"address":[],"length":0,"stats":{"Line":8}},{"line":213,"address":[],"length":0,"stats":{"Line":24}},{"line":215,"address":[],"length":0,"stats":{"Line":8}},{"line":218,"address":[],"length":0,"stats":{"Line":10}},{"line":219,"address":[],"length":0,"stats":{"Line":1}},{"line":224,"address":[],"length":0,"stats":{"Line":112}},{"line":225,"address":[],"length":0,"stats":{"Line":224}},{"line":226,"address":[],"length":0,"stats":{"Line":0}},{"line":228,"address":[],"length":0,"stats":{"Line":336}},{"line":229,"address":[],"length":0,"stats":{"Line":224}},{"line":231,"address":[],"length":0,"stats":{"Line":224}},{"line":233,"address":[],"length":0,"stats":{"Line":112}},{"line":237,"address":[],"length":0,"stats":{"Line":105}},{"line":238,"address":[],"length":0,"stats":{"Line":105}},{"line":244,"address":[],"length":0,"stats":{"Line":9}},{"line":245,"address":[],"length":0,"stats":{"Line":18}},{"line":246,"address":[],"length":0,"stats":{"Line":27}},{"line":248,"address":[],"length":0,"stats":{"Line":9}},{"line":251,"address":[],"length":0,"stats":{"Line":9}},{"line":252,"address":[],"length":0,"stats":{"Line":9}},{"line":256,"address":[],"length":0,"stats":{"Line":3}},{"line":257,"address":[],"length":0,"stats":{"Line":6}},{"line":261,"address":[],"length":0,"stats":{"Line":11}},{"line":262,"address":[],"length":0,"stats":{"Line":22}},{"line":266,"address":[],"length":0,"stats":{"Line":1}},{"line":267,"address":[],"length":0,"stats":{"Line":2}},{"line":272,"address":[],"length":0,"stats":{"Line":93}},{"line":273,"address":[],"length":0,"stats":{"Line":279}},{"line":277,"address":[],"length":0,"stats":{"Line":1}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":279,"address":[],"length":0,"stats":{"Line":1}},{"line":280,"address":[],"length":0,"stats":{"Line":1}},{"line":281,"address":[],"length":0,"stats":{"Line":2}},{"line":282,"address":[],"length":0,"stats":{"Line":1}},{"line":284,"address":[],"length":0,"stats":{"Line":5}},{"line":285,"address":[],"length":0,"stats":{"Line":1}},{"line":286,"address":[],"length":0,"stats":{"Line":1}},{"line":291,"address":[],"length":0,"stats":{"Line":28}},{"line":292,"address":[],"length":0,"stats":{"Line":56}},{"line":296,"address":[],"length":0,"stats":{"Line":1}},{"line":297,"address":[],"length":0,"stats":{"Line":3}},{"line":301,"address":[],"length":0,"stats":{"Line":37}},{"line":302,"address":[],"length":0,"stats":{"Line":37}},{"line":306,"address":[],"length":0,"stats":{"Line":33}},{"line":307,"address":[],"length":0,"stats":{"Line":66}},{"line":311,"address":[],"length":0,"stats":{"Line":18}},{"line":312,"address":[],"length":0,"stats":{"Line":36}},{"line":316,"address":[],"length":0,"stats":{"Line":5}},{"line":317,"address":[],"length":0,"stats":{"Line":10}},{"line":324,"address":[],"length":0,"stats":{"Line":1}},{"line":325,"address":[],"length":0,"stats":{"Line":1}},{"line":326,"address":[],"length":0,"stats":{"Line":1}},{"line":328,"address":[],"length":0,"stats":{"Line":0}},{"line":329,"address":[],"length":0,"stats":{"Line":0}},{"line":330,"address":[],"length":0,"stats":{"Line":0}},{"line":335,"address":[],"length":0,"stats":{"Line":3}},{"line":336,"address":[],"length":0,"stats":{"Line":8}},{"line":339,"address":[],"length":0,"stats":{"Line":245}},{"line":340,"address":[],"length":0,"stats":{"Line":265}},{"line":341,"address":[],"length":0,"stats":{"Line":10}},{"line":348,"address":[],"length":0,"stats":{"Line":76}},{"line":349,"address":[],"length":0,"stats":{"Line":76}},{"line":350,"address":[],"length":0,"stats":{"Line":76}},{"line":351,"address":[],"length":0,"stats":{"Line":228}},{"line":357,"address":[],"length":0,"stats":{"Line":1}},{"line":358,"address":[],"length":0,"stats":{"Line":1}}],"covered":98,"coverable":103}]};
|
|
var previousData = {"files":[{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","codec.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 C ABI imports. The task module reaches the codec side through\n//! `include/codec/task.h` (the task submitter), `include/codec/decoder.h`\n//! (conform/proxy work) and `include/codec/encoder.h` (export). Signatures\n//! mirror the headers verbatim.\n\nuse std::ffi::{c_char, c_int, c_uint, c_void};\n\nuse crate::bridge::render::OakCancelAtom;\nuse crate::handle::CHandle;\n\n/// `OakCodecTaskKind` enum values (`include/codec/task.h`).\npub const OAKCODEC_TASK_CONFORM: c_int = 0;\n/// `OakCodecTaskKind::OAKCODEC_TASK_PROXY`.\npub const OAKCODEC_TASK_PROXY: c_int = 1;\n/// `OAKCODEC_E_CANCELLED` (`include/codec/error.h`).\npub const OAKCODEC_E_CANCELLED: c_int = -50006;\n/// `OAKCODEC_E_INVALID` (`include/codec/error.h`).\npub const OAKCODEC_E_INVALID: c_int = -50001;\n/// `OAKCODEC_E_FAILED` (`include/codec/error.h`).\npub const OAKCODEC_E_FAILED: c_int = -50003;\n\n/// Mirror of `OakCodecTaskRequest` (`include/codec/task.h`).\n#[repr(C)]\npub struct OakCodecTaskRequest {\n\t/// `OakCodecTaskKind` as int.\n\tpub kind: c_int,\n\t/// Source media filename.\n\tpub input_filename: *const c_char,\n\t/// Final destination path.\n\tpub output_filename: *const c_char,\n\t/// Stream inside the source media.\n\tpub stream_index: c_int,\n\t/// conform: target sample rate.\n\tpub sample_rate: c_int,\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: c_int,\n\t/// proxy: target width, 0 = unspecified/divider.\n\tpub proxy_width: c_int,\n\t/// proxy: target height, 0 = unspecified/divider.\n\tpub proxy_height: c_int,\n}\n\n/// `oakcodec_task_submit_fn` callback typedef.\npub type OakCodecTaskSubmitFn =\n\tunsafe extern \"C\" fn(req: *const OakCodecTaskRequest, userdata: *mut c_void) -\u003e c_int;\n\n/// Mirror of `OakCodecFrame`/`OakFrame` handle type (`include/codec/frame.h`).\npub type OakFrame = CHandle;\n\n/// Mirror of `OakEncoder` handle type (`include/codec/encoder.h`).\npub type OakEncoder = CHandle;\n\n/// Mirror of `OakDecoder` handle type (`include/codec/decoder.h`).\npub type OakDecoder = CHandle;\n\n/// Mirror of `OakCodecProxyParams` (`include/codec/proxy.h`).\n#[repr(C)]\npub struct OakCodecProxyParams {\n\t/// Target width.\n\tpub width: c_int,\n\t/// Target height.\n\tpub height: c_int,\n\t/// Resolution divider.\n\tpub divider: c_int,\n\t/// Proxy format version.\n\tpub version: c_int,\n\t/// CRF for the proxy encode.\n\tpub crf: c_int,\n\t/// Whether audio is included.\n\tpub include_audio: c_int,\n\t/// Output container extension.\n\tpub extension: [c_char; 32],\n\t/// ffmpeg preset name.\n\tpub preset: [c_char; 32],\n}\n\n/// Mirror of `oakcodec_encoding_params` (`include/codec/encoder.h`) — the\n/// export task fills this to open the encoder. Layout mirrors the header\n/// verbatim (a zeroed struct = all tracks disabled).\n#[repr(C)]\npub struct OakCodecEncodingParams {\n\t/// Output filename.\n\tpub filename: [c_char; 1024],\n\t/// Export format id (`olive::ExportFormat::Format`).\n\tpub format: c_int,\n\t/// Whether video is exported.\n\tpub video_enabled: c_int,\n\t/// Video codec id.\n\tpub video_codec: c_int,\n\t/// Output width.\n\tpub video_width: c_int,\n\t/// Output height.\n\tpub video_height: c_int,\n\t/// Frame duration rational numerator.\n\tpub video_time_base_num: c_int,\n\t/// Frame duration rational denominator.\n\tpub video_time_base_den: c_int,\n\t/// Delivery pixel format (`OakPixelFormat` as int).\n\tpub video_pixel_format: c_int,\n\t/// Interlacing (`OAKCODEC_INTERLACE_*`).\n\tpub video_interlacing: c_int,\n\t/// Pixel aspect ratio numerator.\n\tpub video_pixel_aspect_num: c_int,\n\t/// Pixel aspect ratio denominator.\n\tpub video_pixel_aspect_den: c_int,\n\t/// Video bit rate (bit/s, 0 = default).\n\tpub video_bit_rate: i64,\n\t/// Minimum video bit rate.\n\tpub video_min_bit_rate: i64,\n\t/// Maximum video bit rate.\n\tpub video_max_bit_rate: i64,\n\t/// Video buffer size (bytes).\n\tpub video_buffer_size: i64,\n\t/// Video threads (0 = auto).\n\tpub video_threads: c_int,\n\t/// Encoded pixel format name (\"yuv420p\").\n\tpub video_pix_fmt: [c_char; 64],\n\t/// Whether the output is an image sequence.\n\tpub video_is_image_sequence: c_int,\n\t/// Scaling method (`OAKCODEC_ENCODING_SCALING_*`).\n\tpub video_scaling_method: c_int,\n\t/// Whether audio is exported.\n\tpub audio_enabled: c_int,\n\t/// Audio codec id.\n\tpub audio_codec: c_int,\n\t/// Audio sample rate.\n\tpub audio_sample_rate: c_int,\n\t/// Audio channel layout mask.\n\tpub audio_channel_layout: u64,\n\t/// Audio sample format (`olive::core::SampleFormat::Format` as int).\n\tpub audio_sample_format: c_int,\n\t/// Audio bit rate.\n\tpub audio_bit_rate: i64,\n\t/// Whether subtitles are exported.\n\tpub subtitles_enabled: c_int,\n\t/// Subtitle codec id.\n\tpub subtitles_codec: c_int,\n\t/// Whether subtitles go to a sidecar file.\n\tpub subtitles_are_sidecar: c_int,\n\t/// Sidecar subtitle format.\n\tpub subtitles_sidecar_format: c_int,\n\t/// Output OCIO colorspace name (empty = reference space).\n\tpub color_transform_output: [c_char; 256],\n\t/// Export length in seconds (rational).\n\tpub export_length_num: c_int,\n\t/// Export length denominator.\n\tpub export_length_den: c_int,\n\t/// Whether a custom export range is in effect.\n\tpub has_custom_range: c_int,\n\t/// Custom range in (seconds, rational).\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 numerator.\n\tpub custom_range_out_num: i64,\n\t/// Custom range out denominator.\n\tpub custom_range_out_den: i64,\n}\n\nextern \"C\" {\n\t/// `oakcodec_set_task_submit_cb` — install the task submitter (`None`\n\t/// unregisters, matching the header's `cb == NULL`).\n\tpub fn oakcodec_set_task_submit_cb(\n\t\tcb: Option\u003cOakCodecTaskSubmitFn\u003e,\n\t\tuserdata: *mut c_void,\n\t);\n\t/// `oakcodec_task_submit_is_registered`.\n\tpub fn oakcodec_task_submit_is_registered() -\u003e c_int;\n\n\t/// `oakcodec_decoder_init`.\n\tpub fn oakcodec_decoder_init() -\u003e OakDecoder;\n\t/// `oakcodec_decoder_free`.\n\tpub fn oakcodec_decoder_free(decoder: *mut OakDecoder);\n\t/// `oakcodec_decoder_open`.\n\tpub fn oakcodec_decoder_open(\n\t\tdecoder: OakDecoder,\n\t\tfilename: *const c_char,\n\t\tstream_index: c_int,\n\t) -\u003e c_int;\n\t/// `oakcodec_decoder_close`.\n\tpub fn oakcodec_decoder_close(decoder: OakDecoder) -\u003e c_int;\n\t/// `oakcodec_decoder_is_open`.\n\tpub fn oakcodec_decoder_is_open(decoder: OakDecoder) -\u003e c_int;\n\t/// `oakcodec_decoder_decode_audio`.\n\tpub fn oakcodec_decoder_decode_audio(\n\t\tdecoder: OakDecoder,\n\t\tin_num: c_int,\n\t\tin_den: c_int,\n\t\tout_num: c_int,\n\t\tout_den: c_int,\n\t\tsample_rate: c_int,\n\t\tchannel_layout: c_uint,\n\t\tbuf: *mut f32,\n\t\tbuf_frames: c_int,\n\t) -\u003e c_int;\n\t/// `oakcodec_decoder_conform_audio` — the whole-stream conform pass.\n\tpub fn oakcodec_decoder_conform_audio(\n\t\tdecoder: OakDecoder,\n\t\toutput_filenames: *const *const c_char,\n\t\tfilename_count: c_int,\n\t\tsample_rate: c_int,\n\t\tchannel_layout: u64,\n\t\tsample_format: c_int,\n\t\tcancelled: OakCancelAtom,\n\t) -\u003e c_int;\n\t/// `oakcodec_decoder_last_error`.\n\tpub fn oakcodec_decoder_last_error(decoder: OakDecoder, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\n\t// --- decoder.h: image-sequence heuristics ---\n\t/// `oakcodec_decoder_get_image_sequence_digit_count`.\n\tpub fn oakcodec_decoder_get_image_sequence_digit_count(filename: *const c_char) -\u003e c_int;\n\t/// `oakcodec_decoder_get_image_sequence_index`.\n\tpub fn oakcodec_decoder_get_image_sequence_index(filename: *const c_char) -\u003e i64;\n\t/// `oakcodec_decoder_transform_image_sequence_file_name` (two-stage).\n\tpub fn oakcodec_decoder_transform_image_sequence_file_name(\n\t\tfilename: *const c_char,\n\t\tnumber: i64,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\n\t// --- encoder.h extras ---\n\t/// `oakcodec_encoder_get_desired_pixel_format`.\n\tpub fn oakcodec_encoder_get_desired_pixel_format(encoder: OakEncoder) -\u003e c_int;\n\t/// `oakcodec_encoding_generate_matrix` (row-major 4x4).\n\tpub fn oakcodec_encoding_generate_matrix(\n\t\tmethod: c_int,\n\t\tsrc_width: c_int,\n\t\tsrc_height: c_int,\n\t\tdst_width: c_int,\n\t\tdst_height: c_int,\n\t\tout_matrix: *mut f64,\n\t) -\u003e c_int;\n\n\t// --- proxy.h ---\n\t/// `oakcodec_proxy_find_ffmpeg` (buf/size getter).\n\tpub fn oakcodec_proxy_find_ffmpeg(configured_path: *const c_char, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oakcodec_proxy_params_default`.\n\tpub fn oakcodec_proxy_params_default(out: *mut OakCodecProxyParams) -\u003e c_int;\n\n\t/// `oakcodec_encoder_init`.\n\tpub fn oakcodec_encoder_init(params: *const OakCodecEncodingParams) -\u003e OakEncoder;\n\t/// `oakcodec_encoder_free`.\n\tpub fn oakcodec_encoder_free(encoder: *mut OakEncoder);\n\t/// `oakcodec_encoder_open`.\n\tpub fn oakcodec_encoder_open(encoder: OakEncoder) -\u003e c_int;\n\t/// `oakcodec_encoder_write_video`.\n\tpub fn oakcodec_encoder_write_video(encoder: OakEncoder, frame: OakFrame) -\u003e c_int;\n\t/// `oakcodec_encoder_write_audio`.\n\tpub fn oakcodec_encoder_write_audio(\n\t\tencoder: OakEncoder,\n\t\tsamples: *const f32,\n\t\tframe_count: c_int,\n\t) -\u003e c_int;\n\t/// `oakcodec_encoder_write_subtitle`.\n\tpub fn oakcodec_encoder_write_subtitle(\n\t\tencoder: OakEncoder,\n\t\ttext: *const c_char,\n\t\tin_seconds: f64,\n\t\tout_seconds: f64,\n\t) -\u003e c_int;\n\t/// `oakcodec_encoder_flush`.\n\tpub fn oakcodec_encoder_flush(encoder: OakEncoder) -\u003e c_int;\n\t/// `oakcodec_encoder_last_error`.\n\tpub fn oakcodec_encoder_last_error(encoder: OakEncoder, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","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. The task module reaches the shared\n//! value types through `include/common/videoparams.h`,\n//! `include/common/colortransform.h` and the oakcore audio-params C ABI.\n//! Signatures mirror the headers verbatim.\n\nuse std::ffi::{c_char, c_int};\n\nuse crate::handle::CHandle;\n\n/// Mirror of `OakVideoParams` (`include/common/videoparams.h`).\n///\n/// A by-value handle (shared_ptr semantics); its `{ctx, addref, release,\n/// abi_version}` layout is identical to [`CHandle`], so it is a type alias —\n/// every oakcommon handle is created and destroyed inside the oakcommon DLL\n/// and only crosses the FFI boundary as this opaque by-value struct.\npub type OakVideoParams = CHandle;\n\n/// Mirror of `OakColorTransform` (`include/common/colortransform.h`); see\n/// [`OakVideoParams`] for the alias rationale.\npub type OakColorTransform = CHandle;\n\n/// Mirror of `OakAudioParams` — oakcore audio parameters. Declared as a\n/// `CHandle`-shaped opaque; the oakcore C ABI header that owns its layout is\n/// the authoritative source once the oakcore C ABI ships.\npub type OakAudioParams = CHandle;\n\n/// `OAKCOMMON_VIDEO_TYPE_STILL` (`include/common/videoparams.h`).\npub const OAKCOMMON_VIDEO_TYPE_STILL: c_int = 1;\n/// `OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE` (`include/common/videoparams.h`).\npub const OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE: c_int = 2;\n\nextern \"C\" {\n\t// --- videoparams.h ---\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(\n\t\twidth: c_int,\n\t\theight: c_int,\n\t\tpixel_format: c_int,\n\t\tnb_channels: c_int,\n\t\tpixel_aspect_num: c_int,\n\t\tpixel_aspect_den: c_int,\n\t\tinterlacing: c_int,\n\t\tdivider: c_int,\n\t) -\u003e OakVideoParams;\n\t/// `oakcommon_videoparams_free`.\n\tpub fn oakcommon_videoparams_free(params: *mut OakVideoParams);\n\t/// `oakcommon_videoparams_get_width`.\n\tpub fn oakcommon_videoparams_get_width(params: OakVideoParams, width: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_height`.\n\tpub fn oakcommon_videoparams_get_height(params: OakVideoParams, height: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_format`.\n\tpub fn oakcommon_videoparams_get_format(params: OakVideoParams, format: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_format`.\n\tpub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_time_base`.\n\tpub fn oakcommon_videoparams_get_time_base(\n\t\tparams: OakVideoParams,\n\t\tnumerator: *mut c_int,\n\t\tdenominator: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_time_base`.\n\tpub fn oakcommon_videoparams_set_time_base(\n\t\tparams: OakVideoParams,\n\t\tnumerator: c_int,\n\t\tdenominator: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_frame_rate_as_time_base` — frame rate flipped\n\t/// to a frame duration rational.\n\tpub fn oakcommon_videoparams_frame_rate_as_time_base(\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_set_frame_rate`.\n\tpub fn oakcommon_videoparams_set_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tnumerator: c_int,\n\t\tdenominator: c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_video_type`.\n\tpub fn oakcommon_videoparams_get_video_type(params: OakVideoParams, out_type: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_video_type`.\n\tpub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_start_time`.\n\tpub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start: i64) -\u003e c_int;\n\t/// `oakcommon_videoparams_set_duration`.\n\tpub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_is_valid`.\n\tpub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams, out_valid: *mut c_int) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_frame_rate`.\n\tpub fn oakcommon_videoparams_get_frame_rate(\n\t\tparams: OakVideoParams,\n\t\tnumerator: *mut c_int,\n\t\tdenominator: *mut c_int,\n\t) -\u003e c_int;\n\t/// `oakcommon_videoparams_get_duration`.\n\tpub fn oakcommon_videoparams_get_duration(params: OakVideoParams, duration: *mut i64) -\u003e c_int;\n\n\t// --- colortransform.h ---\n\t/// `oakcommon_colortransform_init_display`.\n\tpub fn oakcommon_colortransform_init_display(\n\t\tdisplay: *const c_char,\n\t\tview: *const c_char,\n\t\tlook: *const c_char,\n\t) -\u003e OakColorTransform;\n\t/// `oakcommon_colortransform_init_output`.\n\tpub fn oakcommon_colortransform_init_output(\n\t\toutput: *const c_char,\n\t\tdisplay: *const c_char,\n\t\tview: *const c_char,\n\t\tlook: *const c_char,\n\t) -\u003e OakColorTransform;\n\t/// `oakcommon_colortransform_free`.\n\tpub fn oakcommon_colortransform_free(transform: *mut OakColorTransform);\n\n\t// --- config.h ---\n\t/// `oakcommon_config_get` (buf/size getter; returns needed size).\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_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}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","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 other oak modules (signatures mirror the public\n//! headers verbatim; resolved at link time).\n\npub mod codec;\npub mod common;\npub mod node;\npub mod render;\npub mod timeline;\npub mod undo;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","node.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//! oaknode C ABI imports. The task module drives node-graph work through\n//! `include/node/project.h` (project shell), `include/node/folder.h`,\n//! `include/node/footage.h`, `include/node/sequence.h`,\n//! `include/node/colormanager.h` and `include/node/serializer.h`. Signatures\n//! mirror the headers verbatim; node handles are `CHandle` values whose\n//! underlying objects live inside the node library.\n\nuse std::ffi::{c_char, c_int, c_void};\n\nuse crate::bridge::common::OakVideoParams;\nuse crate::bridge::render::OakCancelAtom;\nuse crate::handle::CHandle;\n\n/// `OakNodeProject` (`include/node/project.h`).\npub type OakNodeProject = CHandle;\n/// `OakNodeNode` (`include/node/node.h`).\npub type OakNodeNode = CHandle;\n/// `OakNodeFolder` (`include/node/folder.h`).\npub type OakNodeFolder = CHandle;\n/// `OakNodeFootage` (`include/node/footage.h`).\npub type OakNodeFootage = CHandle;\n/// `OakNodeSequence` (`include/node/sequence.h`).\npub type OakNodeSequence = CHandle;\n/// `OakNodeTrack` (`include/node/track.h`).\npub type OakNodeTrack = CHandle;\n/// `OakNodeTrackList` (`include/node/track.h`).\npub type OakNodeTrackList = CHandle;\n/// `OakNodeBlock` (`include/node/block.h`).\npub type OakNodeBlock = CHandle;\n/// `OakNodeColorManager` (`include/node/colormanager.h`).\npub type OakNodeColorManager = CHandle;\n/// `OakNodeSerializerSaveData` (`include/node/serializer.h`).\npub type OakNodeSerializerSaveData = CHandle;\n/// `OakNodeSerializerLoadData` (`include/node/serializer.h`).\npub type OakNodeSerializerLoadData = CHandle;\n\n/// `OAKNODE_SEQUENCE_TEXTURE_INPUT` (`include/node/sequence.h`).\npub const OAKNODE_SEQUENCE_TEXTURE_INPUT: \u0026str = \"tex_in\";\n/// `OAKNODE_SEQUENCE_SAMPLES_INPUT` (`include/node/sequence.h`).\npub const OAKNODE_SEQUENCE_SAMPLES_INPUT: \u0026str = \"samples_in\";\n/// `OAKNODE_TRACK_TYPE_VIDEO` (`include/node/track.h`).\npub const OAKNODE_TRACK_TYPE_VIDEO: c_int = 0;\n/// `OAKNODE_TRACK_TYPE_AUDIO` (`include/node/track.h`).\npub const OAKNODE_TRACK_TYPE_AUDIO: c_int = 1;\n/// `OAKNODE_TRACK_TYPE_NONE` (`include/node/track.h`).\npub const OAKNODE_TRACK_TYPE_NONE: c_int = -1;\n/// `OAKNODE_BLOCK_OTHER` (`include/node/block.h`).\npub const OAKNODE_BLOCK_OTHER: c_int = 0;\n/// `OAKNODE_BLOCK_CLIP` (`include/node/block.h`).\npub const OAKNODE_BLOCK_CLIP: c_int = 1;\n/// `OAKNODE_BLOCK_GAP` (`include/node/block.h`).\npub const OAKNODE_BLOCK_GAP: c_int = 2;\n/// `OAKNODE_BLOCK_TRANSITION` (`include/node/block.h`).\npub const OAKNODE_BLOCK_TRANSITION: c_int = 3;\n/// `OAKNODE_TRANSITION_CROSS_DISSOLVE` (`include/node/block.h`).\npub const OAKNODE_TRANSITION_CROSS_DISSOLVE: c_int = 0;\n/// `OAKNODE_TRANSITION_OUT_BLOCK_INPUT` (`include/node/block.h`).\npub const OAKNODE_TRANSITION_OUT_BLOCK_INPUT: \u0026str = \"out_block_in\";\n/// `OAKNODE_TRANSITION_IN_BLOCK_INPUT` (`include/node/block.h`).\npub const OAKNODE_TRANSITION_IN_BLOCK_INPUT: \u0026str = \"in_block_in\";\n\n/// Node type ids used by the OTIO tasks\n/// (`src/task/src/project/loadotio/loadotio.cpp`).\n/// `k_sequence_id` — the sequence node type.\npub const OAKNODE_TYPE_SEQUENCE: \u0026str = \"org.olivevideoeditor.Olive.sequence\";\n/// `k_transform_id` — the transform node type.\npub const OAKNODE_TYPE_TRANSFORM: \u0026str = \"org.olivevideoeditor.Olive.transform\";\n/// `k_volume_id` — the volume node type.\npub const OAKNODE_TYPE_VOLUME: \u0026str = \"org.olivevideoeditor.Olive.volume\";\n\n/// C string helper: build a NUL-terminated copy of a Rust string for the\n/// C ABI. The buffer is leaked for the duration of the process; callers\n/// pass short-lived literals.\npub fn cstr(s: \u0026str) -\u003e *const c_char {\n\tlet mut bytes = s.as_bytes().to_vec();\n\tbytes.push(0);\n\tbytes.leak().as_ptr() as *const c_char\n}\n\nextern \"C\" {\n\t// --- node.h ---\n\t/// `oaknode_node_get_label` (two-stage string getter).\n\tpub fn oaknode_node_get_label(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_node_set_label`.\n\tpub fn oaknode_node_set_label(node: OakNodeNode, label: *const c_char) -\u003e c_int;\n\t/// `oaknode_node_get_id` (two-stage string getter).\n\tpub fn oaknode_node_get_id(node: OakNodeNode, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_node_get_project` (borrowed project handle out).\n\tpub fn oaknode_node_get_project(node: OakNodeNode, out: *mut OakNodeProject) -\u003e c_int;\n\t/// `oaknode_node_input_get_connected_node` (borrowed handle out).\n\tpub fn oaknode_node_input_get_connected_node(\n\t\tnode: OakNodeNode,\n\t\tinput_id: *const c_char,\n\t\tout_node: *mut OakNodeNode,\n\t) -\u003e c_int;\n\t/// `oaknode_node_set_context_position` (live, non-undoable).\n\tpub fn oaknode_node_set_context_position(\n\t\tnode: OakNodeNode,\n\t\tcontext: OakNodeNode,\n\t\tx: f64,\n\t\ty: f64,\n\t\texpanded: c_int,\n\t) -\u003e c_int;\n\t/// `oaknode_node_find_input_footage` (borrowed handle out).\n\tpub fn oaknode_node_find_input_footage(node: OakNodeNode, out: *mut OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_node_get_input_string` (two-stage string getter).\n\tpub fn oaknode_node_get_input_string(\n\t\tnode: OakNodeNode,\n\t\tinput_id: *const c_char,\n\t\tbuf: *mut c_char,\n\t\tbuf_size: c_int,\n\t) -\u003e c_int;\n\t/// `oaknode_node_create_copy` — orphaned deep copy of a node.\n\tpub fn oaknode_node_create_copy(node: OakNodeNode) -\u003e OakNodeNode;\n\t/// `oaknode_node_copy_inputs`.\n\tpub fn oaknode_node_copy_inputs(dst: OakNodeNode, src: OakNodeNode, include_connections: c_int) -\u003e c_int;\n\t/// `oaknode_node_connect`.\n\tpub fn oaknode_node_connect(output_node: OakNodeNode, input_node: OakNodeNode, input_id: *const c_char) -\u003e c_int;\n\t/// `oaknode_node_set_value_hint_track`.\n\tpub fn oaknode_node_set_value_hint_track(\n\t\tnode: OakNodeNode,\n\t\tinput_id: *const c_char,\n\t\ttrack_type: c_int,\n\t\ttrack_index: c_int,\n\t) -\u003e c_int;\n\t/// `oaknode_node_get_video_frame_cache` (borrowed cache out).\n\tpub fn oaknode_node_get_video_frame_cache(\n\t\tnode: OakNodeNode,\n\t\tout: *mut crate::bridge::render::OakRenderCache,\n\t) -\u003e c_int;\n\t/// `oaknode_node_free`.\n\tpub fn oaknode_node_free(node: *mut OakNodeNode);\n\t/// `oaknode_command_create_remove_node` — undoable node removal.\n\tpub fn oaknode_command_create_remove_node(node: OakNodeNode) -\u003e crate::bridge::undo::OakUndoCommand;\n\n\t// --- factory.h ---\n\t/// `oaknode_factory_create_from_id`.\n\tpub fn oaknode_factory_create_from_id(type_id: *const c_char) -\u003e OakNodeNode;\n\n\t// --- node.h (viewer) ---\n\t/// `oaknode_viewer_set_video_params`.\n\tpub fn oaknode_viewer_set_video_params(viewer: OakNodeNode, params: *const OakVideoParams) -\u003e c_int;\n\t/// `oaknode_viewer_set_audio_params`.\n\tpub fn oaknode_viewer_set_audio_params(viewer: OakNodeNode, params: *const std::ffi::c_void) -\u003e c_int;\n\n\t// --- block.h ---\n\t/// `oaknode_block_clip_create`.\n\tpub fn oaknode_block_clip_create() -\u003e OakNodeBlock;\n\t/// `oaknode_block_gap_create`.\n\tpub fn oaknode_block_gap_create() -\u003e OakNodeBlock;\n\t/// `oaknode_block_transition_create` (kind: `OakNodeTransitionKind`).\n\tpub fn oaknode_block_transition_create(kind: c_int) -\u003e OakNodeBlock;\n\t/// `oaknode_block_as_node` (borrowed cast).\n\tpub fn oaknode_block_as_node(block: OakNodeBlock) -\u003e OakNodeNode;\n\t/// `oaknode_block_get_kind`.\n\tpub fn oaknode_block_get_kind(block: OakNodeBlock, out_kind: *mut c_int) -\u003e c_int;\n\t/// `oaknode_block_get_in`.\n\tpub fn oaknode_block_get_in(block: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_block_get_length`.\n\tpub fn oaknode_block_get_length(block: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_block_set_length_and_media_out`.\n\tpub fn oaknode_block_set_length_and_media_out(block: OakNodeBlock, numerator: c_int, denominator: c_int) -\u003e c_int;\n\t/// `oaknode_clip_set_media_in`.\n\tpub fn oaknode_clip_set_media_in(clip: OakNodeBlock, numerator: c_int, denominator: c_int) -\u003e c_int;\n\t/// `oaknode_transition_get_in_offset`.\n\tpub fn oaknode_transition_get_in_offset(transition: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_transition_get_out_offset`.\n\tpub fn oaknode_transition_get_out_offset(transition: OakNodeBlock, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_transition_set_offsets_and_length`.\n\tpub fn oaknode_transition_set_offsets_and_length(\n\t\ttransition: OakNodeBlock,\n\t\tin_num: c_int,\n\t\tin_den: c_int,\n\t\tout_num: c_int,\n\t\tout_den: c_int,\n\t) -\u003e c_int;\n\n\t// --- project.h ---\n\t/// `oaknode_project_init`.\n\tpub fn oaknode_project_init() -\u003e OakNodeProject;\n\t/// `oaknode_project_free`.\n\tpub fn oaknode_project_free(project: *mut OakNodeProject);\n\t/// `oaknode_project_initialize`.\n\tpub fn oaknode_project_initialize(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_clear`.\n\tpub fn oaknode_project_clear(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_root`.\n\tpub fn oaknode_project_root(project: OakNodeProject) -\u003e OakNodeFolder;\n\t/// `oaknode_project_name`.\n\tpub fn oaknode_project_name(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_filename`.\n\tpub fn oaknode_project_filename(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_pretty_filename`.\n\tpub fn oaknode_project_pretty_filename(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_set_filename`.\n\tpub fn oaknode_project_set_filename(project: OakNodeProject, filename: *const c_char) -\u003e c_int;\n\t/// `oaknode_project_is_modified`.\n\tpub fn oaknode_project_is_modified(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_set_modified`.\n\tpub fn oaknode_project_set_modified(project: OakNodeProject, modified: c_int) -\u003e c_int;\n\t/// `oaknode_project_is_new`.\n\tpub fn oaknode_project_is_new(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_cache_path`.\n\tpub fn oaknode_project_cache_path(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_copy_settings`.\n\tpub fn oaknode_project_copy_settings(dst: OakNodeProject, src: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_get_cache_location_setting`.\n\tpub fn oaknode_project_get_cache_location_setting(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_set_cache_location_setting`.\n\tpub fn oaknode_project_set_cache_location_setting(project: OakNodeProject, setting: c_int) -\u003e c_int;\n\t/// `oaknode_project_get_custom_cache_path`.\n\tpub fn oaknode_project_get_custom_cache_path(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_set_custom_cache_path`.\n\tpub fn oaknode_project_set_custom_cache_path(project: OakNodeProject, path: *const c_char) -\u003e c_int;\n\t/// `oaknode_project_get_uuid`.\n\tpub fn oaknode_project_get_uuid(project: OakNodeProject, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_project_add_node`.\n\tpub fn oaknode_project_add_node(project: OakNodeProject, node: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_project_remove_node`.\n\tpub fn oaknode_project_remove_node(project: OakNodeProject, node: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_project_node_count`.\n\tpub fn oaknode_project_node_count(project: OakNodeProject) -\u003e c_int;\n\t/// `oaknode_project_node_at`.\n\tpub fn oaknode_project_node_at(project: OakNodeProject, index: c_int) -\u003e OakNodeNode;\n\n\t// --- folder.h ---\n\t/// `oaknode_folder_create`.\n\tpub fn oaknode_folder_create(project: OakNodeProject) -\u003e OakNodeFolder;\n\t/// `oaknode_folder_child_count`.\n\tpub fn oaknode_folder_child_count(folder: OakNodeFolder) -\u003e c_int;\n\t/// `oaknode_folder_child_at`.\n\tpub fn oaknode_folder_child_at(folder: OakNodeFolder, index: c_int) -\u003e OakNodeNode;\n\t/// `oaknode_folder_add_child`.\n\tpub fn oaknode_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_as_node`.\n\tpub fn oaknode_folder_as_node(folder: OakNodeFolder) -\u003e OakNodeNode;\n\t/// `oaknode_command_create_folder_add_child`.\n\tpub fn oaknode_command_create_folder_add_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e crate::bridge::undo::OakUndoCommand;\n\t/// `oaknode_folder_remove_child`.\n\tpub fn oaknode_folder_remove_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_move_children`.\n\tpub fn oaknode_folder_move_children(nodes: *const OakNodeNode, count: c_int, dest_folder: OakNodeFolder) -\u003e c_int;\n\t/// `oaknode_folder_has_child_recursive`.\n\tpub fn oaknode_folder_has_child_recursive(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_index_of_child`.\n\tpub fn oaknode_folder_index_of_child(folder: OakNodeFolder, child: OakNodeNode) -\u003e c_int;\n\t/// `oaknode_folder_parent_of`.\n\tpub fn oaknode_folder_parent_of(node: OakNodeNode) -\u003e OakNodeFolder;\n\n\t// --- footage.h ---\n\t/// `oaknode_footage_create`.\n\tpub fn oaknode_footage_create(project: OakNodeProject, filename: *const c_char) -\u003e OakNodeFootage;\n\t/// `oaknode_footage_as_node`.\n\tpub fn oaknode_footage_as_node(footage: OakNodeFootage) -\u003e OakNodeNode;\n\t/// `oaknode_footage_filename`.\n\tpub fn oaknode_footage_filename(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_footage_set_filename`.\n\tpub fn oaknode_footage_set_filename(footage: OakNodeFootage, filename: *const c_char) -\u003e c_int;\n\t/// `oaknode_footage_is_valid`.\n\tpub fn oaknode_footage_is_valid(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_timestamp`.\n\tpub fn oaknode_footage_timestamp(footage: OakNodeFootage, out_timestamp: *mut i64) -\u003e c_int;\n\t/// `oaknode_footage_set_timestamp`.\n\tpub fn oaknode_footage_set_timestamp(footage: OakNodeFootage, timestamp: i64) -\u003e c_int;\n\t/// `oaknode_footage_decoder`.\n\tpub fn oaknode_footage_decoder(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_footage_total_stream_count`.\n\tpub fn oaknode_footage_total_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_video_stream_count`.\n\tpub fn oaknode_footage_video_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_audio_stream_count`.\n\tpub fn oaknode_footage_audio_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_subtitle_stream_count`.\n\tpub fn oaknode_footage_subtitle_stream_count(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_duration`.\n\tpub fn oaknode_footage_duration(footage: OakNodeFootage, out_numerator: *mut c_int, out_denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_footage_proxy_enabled`.\n\tpub fn oaknode_footage_proxy_enabled(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_set_proxy_enabled`.\n\tpub fn oaknode_footage_set_proxy_enabled(footage: OakNodeFootage, enabled: c_int) -\u003e c_int;\n\t/// `oaknode_footage_proxy_path`.\n\tpub fn oaknode_footage_proxy_path(footage: OakNodeFootage, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_footage_proxy_state`.\n\tpub fn oaknode_footage_proxy_state(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_set_proxy`.\n\tpub fn oaknode_footage_set_proxy(footage: OakNodeFootage, path: *const c_char, state: c_int, video_stream_index: c_int, preset_version: c_int, enabled: c_int) -\u003e c_int;\n\t/// `oaknode_footage_clear_proxy`.\n\tpub fn oaknode_footage_clear_proxy(footage: OakNodeFootage) -\u003e c_int;\n\t/// `oaknode_footage_get_video_params`.\n\tpub fn oaknode_footage_get_video_params(footage: OakNodeFootage, index: c_int, out: *mut OakVideoParams) -\u003e c_int;\n\t/// `oaknode_footage_set_video_params`.\n\tpub fn oaknode_footage_set_video_params(footage: OakNodeFootage, index: c_int, params: *const OakVideoParams) -\u003e c_int;\n\t/// `oaknode_footage_get_video_length`.\n\tpub fn oaknode_footage_get_video_length(footage: OakNodeFootage, out_num: *mut i64, out_den: *mut i64) -\u003e c_int;\n\t/// `oaknode_footage_set_cancel_atom`.\n\tpub fn oaknode_footage_set_cancel_atom(footage: OakNodeFootage, atom: OakCancelAtom) -\u003e c_int;\n\n\t// --- sequence.h ---\n\t/// `oaknode_sequence_create`.\n\tpub fn oaknode_sequence_create() -\u003e OakNodeSequence;\n\t/// `oaknode_sequence_free`.\n\tpub fn oaknode_sequence_free(sequence: *mut OakNodeSequence);\n\t/// `oaknode_sequence_set_default_parameters`.\n\tpub fn oaknode_sequence_set_default_parameters(sequence: OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_sequence_as_node`.\n\tpub fn oaknode_sequence_as_node(sequence: OakNodeSequence) -\u003e OakNodeNode;\n\t/// `oaknode_sequence_from_node`.\n\tpub fn oaknode_sequence_from_node(node: OakNodeNode) -\u003e OakNodeSequence;\n\t/// `oaknode_sequence_get_track_list`.\n\tpub fn oaknode_sequence_get_track_list(sequence: OakNodeSequence, r#type: c_int, out: *mut OakNodeTrackList) -\u003e c_int;\n\t/// `oaknode_sequence_get_track_count`.\n\tpub fn oaknode_sequence_get_track_count(sequence: OakNodeSequence, r#type: c_int, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_track_at`.\n\tpub fn oaknode_sequence_get_track_at(sequence: OakNodeSequence, r#type: c_int, index: c_int, out: *mut OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_sequence_get_all_track_count`.\n\tpub fn oaknode_sequence_get_all_track_count(sequence: OakNodeSequence, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_all_track_at`.\n\tpub fn oaknode_sequence_get_all_track_at(sequence: OakNodeSequence, index: c_int, out: *mut OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_sequence_get_playhead`.\n\tpub fn oaknode_sequence_get_playhead(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_set_playhead`.\n\tpub fn oaknode_sequence_set_playhead(sequence: OakNodeSequence, numerator: c_int, denominator: c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_length`.\n\tpub fn oaknode_sequence_get_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_video_length`.\n\tpub fn oaknode_sequence_get_video_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_audio_length`.\n\tpub fn oaknode_sequence_get_audio_length(sequence: OakNodeSequence, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_verify_length`.\n\tpub fn oaknode_sequence_verify_length(sequence: OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_sequence_get_video_stream_count`.\n\tpub fn oaknode_sequence_get_video_stream_count(sequence: OakNodeSequence, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_audio_stream_count`.\n\tpub fn oaknode_sequence_get_audio_stream_count(sequence: OakNodeSequence, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_sequence_get_video_params`.\n\tpub fn oaknode_sequence_get_video_params(sequence: OakNodeSequence, index: c_int, out: *mut OakVideoParams) -\u003e c_int;\n\t/// `oaknode_sequence_set_video_params`.\n\tpub fn oaknode_sequence_set_video_params(sequence: OakNodeSequence, index: c_int, params: OakVideoParams) -\u003e c_int;\n\t/// `oaknode_sequence_get_audio_params`.\n\tpub fn oaknode_sequence_get_audio_params(sequence: OakNodeSequence, index: c_int, out: *mut *const c_void) -\u003e c_int;\n\t/// `oaknode_sequence_set_audio_params`.\n\tpub fn oaknode_sequence_set_audio_params(sequence: OakNodeSequence, index: c_int, params: *const c_void) -\u003e c_int;\n\n\t// --- track.h ---\n\t/// `oaknode_track_as_node`.\n\tpub fn oaknode_track_as_node(track: OakNodeTrack) -\u003e OakNodeNode;\n\t/// `oaknode_track_create`.\n\tpub fn oaknode_track_create(r#type: c_int) -\u003e OakNodeTrack;\n\t/// `oaknode_track_free`.\n\tpub fn oaknode_track_free(track: *mut OakNodeTrack);\n\t/// `oaknode_track_get_type`.\n\tpub fn oaknode_track_get_type(track: OakNodeTrack, r#type: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_get_length`.\n\tpub fn oaknode_track_get_length(track: OakNodeTrack, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_get_block_count`.\n\tpub fn oaknode_track_get_block_count(track: OakNodeTrack, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_get_block_at` (borrowed handle out).\n\tpub fn oaknode_track_get_block_at(track: OakNodeTrack, index: c_int, out: *mut OakNodeBlock) -\u003e c_int;\n\t/// `oaknode_track_append_block`.\n\tpub fn oaknode_track_append_block(track: OakNodeTrack, block: OakNodeBlock) -\u003e c_int;\n\t/// `oaknode_track_set_type`.\n\tpub fn oaknode_track_set_type(track: OakNodeTrack, r#type: c_int) -\u003e c_int;\n\t/// `oaknode_track_get_index`.\n\tpub fn oaknode_track_get_index(track: OakNodeTrack, index: *mut c_int) -\u003e c_int;\n\t/// `oaknode_track_set_index`.\n\tpub fn oaknode_track_set_index(track: OakNodeTrack, index: c_int) -\u003e c_int;\n\t/// `oaknode_track_get_sequence`.\n\tpub fn oaknode_track_get_sequence(track: OakNodeTrack, out: *mut OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_tracklist_get_sequence`.\n\tpub fn oaknode_tracklist_get_sequence(list: OakNodeTrackList, out: *mut OakNodeSequence) -\u003e c_int;\n\t/// `oaknode_tracklist_get_track_input_id`.\n\tpub fn oaknode_tracklist_get_track_input_id(list: OakNodeTrackList, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_array_append`.\n\tpub fn oaknode_tracklist_array_append(list: OakNodeTrackList) -\u003e c_int;\n\t/// `oaknode_tracklist_array_remove_last`.\n\tpub fn oaknode_tracklist_array_remove_last(list: OakNodeTrackList) -\u003e c_int;\n\t/// `oaknode_tracklist_get_array_index_from_cache_index`.\n\tpub fn oaknode_tracklist_get_array_index_from_cache_index(list: OakNodeTrackList, cache_index: c_int, out_index: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_type`.\n\tpub fn oaknode_tracklist_get_type(list: OakNodeTrackList, r#type: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_track_count`.\n\tpub fn oaknode_tracklist_get_track_count(list: OakNodeTrackList, count: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_track_at`.\n\tpub fn oaknode_tracklist_get_track_at(list: OakNodeTrackList, index: c_int, out: *mut OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_tracklist_get_total_length`.\n\tpub fn oaknode_tracklist_get_total_length(list: OakNodeTrackList, numerator: *mut c_int, denominator: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_get_array_size`.\n\tpub fn oaknode_tracklist_get_array_size(list: OakNodeTrackList, size: *mut c_int) -\u003e c_int;\n\t/// `oaknode_tracklist_add_track`.\n\tpub fn oaknode_tracklist_add_track(list: OakNodeTrackList, track: OakNodeTrack) -\u003e c_int;\n\t/// `oaknode_tracklist_remove_track`.\n\tpub fn oaknode_tracklist_remove_track(list: OakNodeTrackList, track: OakNodeTrack) -\u003e c_int;\n\n\t// --- colormanager.h ---\n\t/// `oaknode_colormanager_init`.\n\tpub fn oaknode_colormanager_init(project: OakNodeProject) -\u003e OakNodeColorManager;\n\t/// `oaknode_colormanager_free`.\n\tpub fn oaknode_colormanager_free(manager: *mut OakNodeColorManager);\n\t/// `oaknode_colormanager_wrap_borrowed`.\n\tpub fn oaknode_colormanager_wrap_borrowed(native_manager: *mut c_void) -\u003e OakNodeColorManager;\n\t/// `oaknode_colormanager_initialize`.\n\tpub fn oaknode_colormanager_initialize(manager: OakNodeColorManager) -\u003e c_int;\n\t/// `oaknode_colormanager_set_up_default_config`.\n\tpub fn oaknode_colormanager_set_up_default_config() -\u003e c_int;\n\n\t// --- serializer.h ---\n\t/// `oaknode_serializer_initialize`.\n\tpub fn oaknode_serializer_initialize() -\u003e c_int;\n\t/// `oaknode_serializer_shutdown`.\n\tpub fn oaknode_serializer_shutdown();\n\t/// `oaknode_serializer_savedata_create`.\n\tpub fn oaknode_serializer_savedata_create(load_type: c_int, project: OakNodeProject) -\u003e OakNodeSerializerSaveData;\n\t/// `oaknode_serializer_savedata_free`.\n\tpub fn oaknode_serializer_savedata_free(save_data: *mut OakNodeSerializerSaveData);\n\t/// `oaknode_serializer_savedata_set_nodes`.\n\tpub fn oaknode_serializer_savedata_set_nodes(save_data: OakNodeSerializerSaveData, nodes: *const OakNodeNode, count: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_savedata_set_property`.\n\tpub fn oaknode_serializer_savedata_set_property(save_data: OakNodeSerializerSaveData, node: OakNodeNode, key: *const c_char, value: *const c_char) -\u003e c_int;\n\t/// `oaknode_serializer_save_to_xml`.\n\tpub fn oaknode_serializer_save_to_xml(save_data: OakNodeSerializerSaveData, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_load_from_xml`.\n\tpub fn oaknode_serializer_load_from_xml(project: OakNodeProject, xml: *const c_char, load_type: c_int, out_result: *mut c_int, out_load_data: *mut OakNodeSerializerLoadData, details_buf: *mut c_char, details_buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_free`.\n\tpub fn oaknode_serializer_loaddata_free(load_data: *mut OakNodeSerializerLoadData);\n\t/// `oaknode_serializer_loaddata_node_count`.\n\tpub fn oaknode_serializer_loaddata_node_count(load_data: OakNodeSerializerLoadData) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_node_at`.\n\tpub fn oaknode_serializer_loaddata_node_at(load_data: OakNodeSerializerLoadData, index: c_int) -\u003e OakNodeNode;\n\t/// `oaknode_serializer_loaddata_get_property`.\n\tpub fn oaknode_serializer_loaddata_get_property(load_data: OakNodeSerializerLoadData, node: OakNodeNode, key: *const c_char, buf: *mut c_char, buf_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_connection_count`.\n\tpub fn oaknode_serializer_loaddata_connection_count(load_data: OakNodeSerializerLoadData) -\u003e c_int;\n\t/// `oaknode_serializer_loaddata_connection_at`.\n\tpub fn oaknode_serializer_loaddata_connection_at(load_data: OakNodeSerializerLoadData, index: c_int, out_output_node: *mut OakNodeNode, out_input_node: *mut OakNodeNode, input_id_buf: *mut c_char, input_id_buf_size: c_int, out_element: *mut c_int) -\u003e c_int;\n\t/// `oaknode_serializer_save_to_file`.\n\tpub fn oaknode_serializer_save_to_file(project: OakNodeProject, filename: *const c_char, use_compression: c_int, out_code: *mut c_int, details: *mut c_char, details_size: c_int) -\u003e c_int;\n\t/// `oaknode_serializer_load_from_file`.\n\tpub fn oaknode_serializer_load_from_file(project: OakNodeProject, filename: *const c_char, out_code: *mut c_int, details: *mut c_char, details_size: c_int) -\u003e c_int;\n}\n","traces":[{"line":90,"address":[],"length":0,"stats":{"Line":86}},{"line":91,"address":[],"length":0,"stats":{"Line":258}},{"line":92,"address":[],"length":0,"stats":{"Line":172}},{"line":93,"address":[],"length":0,"stats":{"Line":86}}],"covered":4,"coverable":4},{"path":["/","Users","sunyu","Projects","oak","src","task","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. The task module reaches the render side through\n//! `include/render/cancelatom.h` (task cancellation), `include/render/ticket.h`\n//! (frame/audio render tickets), `include/render/copier.h` (export project\n//! copy) and `include/render/color.h` (color processor). Signatures mirror\n//! the headers verbatim.\n\nuse std::ffi::{c_char, c_int, c_void};\n\nuse crate::bridge::codec::OakFrame;\nuse crate::bridge::common::{OakAudioParams, OakColorTransform, OakVideoParams};\nuse crate::bridge::node::{OakNodeColorManager, OakNodeNode, OakNodeProject};\nuse crate::handle::CHandle;\n\n/// Mirror of `OakCancelAtom` (`include/render/cancelatom.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakCancelAtom {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\nimpl OakCancelAtom {\n\t/// The empty (null) atom.\n\tpub fn null() -\u003e Self {\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\t/// Whether the atom is empty (`ctx == NULL`).\n\tpub fn is_null(\u0026self) -\u003e bool {\n\t\tself.ctx.is_null()\n\t}\n\n\t/// Adopt the fields of a generic `CHandle` (layout-identical) — the task\n\t/// module stores the atom as a `CHandle` for its public API.\n\tpub fn from_chandle(h: CHandle) -\u003e Self {\n\t\tOakCancelAtom {\n\t\t\tctx: h.ctx,\n\t\t\taddref: h.addref,\n\t\t\trelease: h.release,\n\t\t\tabi_version: h.abi_version,\n\t\t}\n\t}\n\n\t/// Convert back to a generic `CHandle` (layout-identical).\n\tpub fn to_chandle(\u0026self) -\u003e CHandle {\n\t\tCHandle {\n\t\t\tctx: self.ctx,\n\t\t\taddref: self.addref,\n\t\t\trelease: self.release,\n\t\t\tabi_version: self.abi_version,\n\t\t}\n\t}\n}\n\n/// Mirror of `OakRenderTicket` (`include/render/ticket.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakRenderTicket {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\n/// Mirror of `OakRenderCache` (`include/render/cache.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakRenderCache {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\nimpl OakRenderCache {\n\t/// The empty (null) cache handle.\n\tpub fn null() -\u003e Self {\n\t\tOakRenderCache {\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\t/// Convert to the layout-identical generic `CHandle`.\n\tpub fn to_chandle(\u0026self) -\u003e CHandle {\n\t\tCHandle {\n\t\t\tctx: self.ctx,\n\t\t\taddref: self.addref,\n\t\t\trelease: self.release,\n\t\t\tabi_version: self.abi_version,\n\t\t}\n\t}\n\n\t/// Adopt the fields of a generic `CHandle` (layout-identical).\n\tpub fn from_chandle(h: CHandle) -\u003e Self {\n\t\tOakRenderCache {\n\t\t\tctx: h.ctx,\n\t\t\taddref: h.addref,\n\t\t\trelease: h.release,\n\t\t\tabi_version: h.abi_version,\n\t\t}\n\t}\n}\n\n/// Mirror of `OakRenderProjectCopier` (`include/render/copier.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakRenderProjectCopier {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\n/// Mirror of `OakColorProcessor` (`include/render/color.h`).\n#[repr(C)]\n#[derive(Clone, Copy)]\npub struct OakColorProcessor {\n\t/// Opaque pointer to the reference-counted object.\n\tpub ctx: *mut c_void,\n\t/// Atomic increment.\n\tpub addref: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// Atomic decrement; destroys at zero.\n\tpub release: Option\u003cunsafe extern \"C\" fn(*mut c_void)\u003e,\n\t/// ABI version (`OAKRENDER_ABI_VERSION`).\n\tpub abi_version: u32,\n}\n\nimpl OakColorProcessor {\n\t/// Convert to the layout-identical generic `CHandle`.\n\tpub fn to_chandle(\u0026self) -\u003e CHandle {\n\t\tCHandle {\n\t\t\tctx: self.ctx,\n\t\t\taddref: self.addref,\n\t\t\trelease: self.release,\n\t\t\tabi_version: self.abi_version,\n\t\t}\n\t}\n\n\t/// Adopt the fields of a generic `CHandle` (layout-identical).\n\tpub fn from_chandle(h: CHandle) -\u003e Self {\n\t\tOakColorProcessor {\n\t\t\tctx: h.ctx,\n\t\t\taddref: h.addref,\n\t\t\trelease: h.release,\n\t\t\tabi_version: h.abi_version,\n\t\t}\n\t}\n}\n\n/// Mirror of `oakrender_ticket_finished_fn` (`include/render/ticket.h`).\n///\n/// Two arguments — `(ticket, userdata)` — mirroring the header verbatim.\n/// The ticket is a borrowed copy of the submitter's handle (the submitter\n/// keeps ownership and releases it); cancelled tickets fire with a NULL\n/// result observed through `oakrender_ticket_get_frame`.\npub type OakRenderTicketFinishedFn = unsafe extern \"C\" fn(ticket: OakRenderTicket, userdata: *mut c_void);\n\n/// Mirror of `oakrender_video_ticket_params` (`include/render/ticket.h`).\n#[repr(C)]\npub struct OakRenderVideoTicketParams {\n\t/// Connected texture output node (borrowed).\n\tpub output_node: OakNodeNode,\n\t/// Video params by value (oakcommon handle).\n\tpub video_params: OakVideoParams,\n\t/// Borrowed oakcore audio params, may be null ctx.\n\tpub audio_params: *const OakAudioParams,\n\t/// Frame timestamp numerator (seconds rational).\n\tpub time_num: i64,\n\t/// Frame timestamp denominator.\n\tpub time_den: i64,\n\t/// Borrowed color manager; empty ctx = NULL.\n\tpub color_manager: OakNodeColorManager,\n\t/// `olive::RenderMode::Mode` as int.\n\tpub mode: c_int,\n\t/// Forced output width (0 = off).\n\tpub force_width: c_int,\n\t/// Forced output height (0 = off).\n\tpub force_height: c_int,\n\t/// Forced color matrix (used when `has_force_matrix` != 0).\n\tpub force_matrix: [f64; 16],\n\t/// Whether `force_matrix` is in effect.\n\tpub has_force_matrix: c_int,\n\t/// Forced pixel format (`PixelFormat` as int, -1 = off).\n\tpub force_format: c_int,\n\t/// Forced channel count (0 = off).\n\tpub force_channel_count: c_int,\n\t/// Forced color output (borrowed; empty ctx = none).\n\tpub force_color_output: OakColorProcessor,\n\t/// Forced color transform by value (empty ctx = default).\n\tpub force_color_transform: OakColorTransform,\n\t/// Borrowed frame cache; empty ctx = none.\n\tpub cache: OakRenderCache,\n}\n\nextern \"C\" {\n\t// --- cancelatom.h ---\n\t/// `oakrender_cancelatom_init`.\n\tpub fn oakrender_cancelatom_init() -\u003e OakCancelAtom;\n\t/// `oakrender_cancelatom_free`.\n\tpub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);\n\t/// `oakrender_cancelatom_cancel`.\n\tpub fn oakrender_cancelatom_cancel(atom: OakCancelAtom) -\u003e c_int;\n\t/// `oakrender_cancelatom_is_cancelled`.\n\tpub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom, cancelled: *mut c_int) -\u003e c_int;\n\t/// `oakrender_cancelatom_heard_cancel`.\n\tpub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom, heard: *mut c_int) -\u003e c_int;\n\n\t// --- ticket.h ---\n\t/// `oakrender_ticket_render_frame`.\n\tpub fn oakrender_ticket_render_frame(\n\t\tparams: *const OakRenderVideoTicketParams,\n\t\tcb: Option\u003cOakRenderTicketFinishedFn\u003e,\n\t\tuserdata: *mut c_void,\n\t) -\u003e OakRenderTicket;\n\t/// `oakrender_ticket_render_audio`.\n\tpub fn oakrender_ticket_render_audio(\n\t\toutput_node: OakNodeNode,\n\t\tin_num: i64,\n\t\tin_den: i64,\n\t\tout_num: i64,\n\t\tout_den: i64,\n\t\tparams: *const OakAudioParams,\n\t\tmode: c_int,\n\t\tcb: Option\u003cOakRenderTicketFinishedFn\u003e,\n\t\tuserdata: *mut c_void,\n\t) -\u003e OakRenderTicket;\n\t/// `oakrender_ticket_is_finished`.\n\tpub fn oakrender_ticket_is_finished(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_wait`.\n\tpub fn oakrender_ticket_wait(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_cancel`.\n\tpub fn oakrender_ticket_cancel(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_get_type`.\n\tpub fn oakrender_ticket_get_type(ticket: OakRenderTicket) -\u003e c_int;\n\t/// `oakrender_ticket_get_frame`.\n\tpub fn oakrender_ticket_get_frame(ticket: OakRenderTicket, out: *mut OakFrame) -\u003e c_int;\n\t/// `oakrender_ticket_get_time`.\n\tpub fn oakrender_ticket_get_time(ticket: OakRenderTicket, out_num: *mut i64, out_den: *mut i64) -\u003e c_int;\n\t/// `oakrender_ticket_get_range`.\n\tpub fn oakrender_ticket_get_range(\n\t\tticket: OakRenderTicket,\n\t\tin_num: *mut i64,\n\t\tin_den: *mut i64,\n\t\tout_num: *mut i64,\n\t\tout_den: *mut i64,\n\t) -\u003e c_int;\n\t/// `oakrender_ticket_get_samples`.\n\tpub fn oakrender_ticket_get_samples(ticket: OakRenderTicket, out: *mut *mut std::ffi::c_void) -\u003e c_int;\n\t/// `oakrender_ticket_free`.\n\tpub fn oakrender_ticket_free(ticket: *mut OakRenderTicket);\n\n\t// --- copier.h ---\n\t/// `oakrender_project_copier_create`.\n\tpub fn oakrender_project_copier_create() -\u003e OakRenderProjectCopier;\n\t/// `oakrender_project_copier_free`.\n\tpub fn oakrender_project_copier_free(copier: *mut OakRenderProjectCopier);\n\t/// `oakrender_project_copier_set_project`.\n\tpub fn oakrender_project_copier_set_project(\n\t\tcopier: OakRenderProjectCopier,\n\t\tproject: OakNodeProject,\n\t) -\u003e c_int;\n\t/// `oakrender_project_copier_get_copy`.\n\tpub fn oakrender_project_copier_get_copy(\n\t\tcopier: OakRenderProjectCopier,\n\t\toriginal: OakNodeNode,\n\t) -\u003e OakNodeNode;\n\t/// `oakrender_project_copier_get_copied_project`.\n\tpub fn oakrender_project_copier_get_copied_project(\n\t\tcopier: OakRenderProjectCopier,\n\t) -\u003e OakNodeProject;\n\n\t// --- color.h ---\n\t/// `oakrender_color_processor_create`.\n\tpub fn oakrender_color_processor_create(\n\t\tsrc_space: *const c_char,\n\t\tdst_space: *const c_char,\n\t\tdisplay: *const c_char,\n\t\tview: *const c_char,\n\t\tlook: *const c_char,\n\t) -\u003e OakColorProcessor;\n\t/// `oakrender_color_processor_free`.\n\tpub fn oakrender_color_processor_free(processor: *mut OakColorProcessor);\n\t/// `oakrender_color_processor_is_valid`.\n\tpub fn oakrender_color_processor_is_valid(processor: OakColorProcessor) -\u003e c_int;\n\n\t// --- renderer.h ---\n\t/// `oakrender_codec_frame_free` — release a frame handed out by a\n\t/// render ticket.\n\tpub fn oakrender_codec_frame_free(frame: *mut crate::bridge::codec::OakFrame);\n\n\t// --- manager.h ---\n\t/// `oakrender_manager_set_aggressive_gc`.\n\tpub fn oakrender_manager_set_aggressive_gc(enabled: c_int) -\u003e c_int;\n\n\t// --- cache.h ---\n\t/// `oakrender_cache_get_invalidated_ranges` — two-stage range query on\n\t/// a frame cache; returns the number of ranges (or the required flat\n\t/// array size).\n\tpub fn oakrender_cache_get_invalidated_ranges(\n\t\tcache: OakRenderCache,\n\t\tin_num: i64,\n\t\tin_den: i64,\n\t\tout_num: i64,\n\t\tout_den: i64,\n\t\tflat: *mut i64,\n\t\tflat_size: c_int,\n\t) -\u003e c_int;\n\t/// `oakrender_cache_free`.\n\tpub fn oakrender_cache_free(cache: *mut OakRenderCache);\n}\n","traces":[{"line":46,"address":[],"length":0,"stats":{"Line":3}},{"line":48,"address":[],"length":0,"stats":{"Line":6}},{"line":56,"address":[],"length":0,"stats":{"Line":0}},{"line":57,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":207}},{"line":64,"address":[],"length":0,"stats":{"Line":414}},{"line":65,"address":[],"length":0,"stats":{"Line":414}},{"line":66,"address":[],"length":0,"stats":{"Line":207}},{"line":67,"address":[],"length":0,"stats":{"Line":207}},{"line":72,"address":[],"length":0,"stats":{"Line":91}},{"line":74,"address":[],"length":0,"stats":{"Line":182}},{"line":75,"address":[],"length":0,"stats":{"Line":182}},{"line":76,"address":[],"length":0,"stats":{"Line":91}},{"line":77,"address":[],"length":0,"stats":{"Line":91}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":2}},{"line":122,"address":[],"length":0,"stats":{"Line":1}},{"line":124,"address":[],"length":0,"stats":{"Line":2}},{"line":125,"address":[],"length":0,"stats":{"Line":2}},{"line":126,"address":[],"length":0,"stats":{"Line":1}},{"line":127,"address":[],"length":0,"stats":{"Line":1}},{"line":132,"address":[],"length":0,"stats":{"Line":83}},{"line":134,"address":[],"length":0,"stats":{"Line":166}},{"line":135,"address":[],"length":0,"stats":{"Line":166}},{"line":136,"address":[],"length":0,"stats":{"Line":83}},{"line":137,"address":[],"length":0,"stats":{"Line":83}},{"line":172,"address":[],"length":0,"stats":{"Line":0}},{"line":174,"address":[],"length":0,"stats":{"Line":0}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":177,"address":[],"length":0,"stats":{"Line":0}},{"line":182,"address":[],"length":0,"stats":{"Line":83}},{"line":184,"address":[],"length":0,"stats":{"Line":166}},{"line":185,"address":[],"length":0,"stats":{"Line":166}},{"line":186,"address":[],"length":0,"stats":{"Line":83}},{"line":187,"address":[],"length":0,"stats":{"Line":83}}],"covered":29,"coverable":36},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","timeline.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//! oaktimeline C ABI imports. The OTIO load task builds tracks through the\n//! timeline edit factories (`include/timeline/edit.h`) instead of the raw\n//! track-list primitives, matching `src/task/src/project/loadotio/loadotio.cpp`.\n//! Signatures mirror the header verbatim.\n\nuse crate::bridge::node::OakNodeTrackList;\nuse crate::bridge::undo::OakUndoCommand;\n\nextern \"C\" {\n\t/// `oaktimeline_add_track_command` (`include/timeline/edit.h`) —\n\t/// olive::TimelineAddTrackCommand.\n\tpub fn oaktimeline_add_track_command(list: OakNodeTrackList) -\u003e OakUndoCommand;\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","bridge","undo.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//! oakundo C ABI imports. Import/load tasks build undo commands through the\n//! C ABI vtable (`oakundo_command_init` with Rust closures as userdata) —\n//! no C++ UndoCommand subclassing exists on this side. Mirrors\n//! `include/undo/undocommand.h` verbatim.\n\nuse std::ffi::{c_int, c_void};\n\nuse crate::handle::CHandle;\n\n/// `OakUndoCommand` (`include/undo/undocommand.h`).\npub type OakUndoCommand = CHandle;\n\n/// Mirror of `OakUndoCommandVtable` (`include/undo/undocommand.h`).\n#[repr(C)]\npub struct OakUndoCommandVtable {\n\t/// Redo callback; may be NULL.\n\tpub redo: Option\u003cunsafe extern \"C\" fn(userdata: *mut c_void)\u003e,\n\t/// Undo callback; may be NULL.\n\tpub undo: Option\u003cunsafe extern \"C\" fn(userdata: *mut c_void)\u003e,\n\t/// Free callback; may be NULL.\n\tpub free_fn: Option\u003cunsafe extern \"C\" fn(userdata: *mut c_void)\u003e,\n}\n\nextern \"C\" {\n\t/// `oakundo_command_init` (vtable command).\n\tpub fn oakundo_command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -\u003e OakUndoCommand;\n\t/// `oakundo_command_init_multi`.\n\tpub fn oakundo_command_init_multi() -\u003e OakUndoCommand;\n\t/// `oakundo_command_multi_add_child`.\n\tpub fn oakundo_command_multi_add_child(multi: OakUndoCommand, child: OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_multi_child_count`.\n\tpub fn oakundo_command_multi_child_count(multi: OakUndoCommand, out_count: *mut c_int) -\u003e c_int;\n\t/// `oakundo_command_multi_child`.\n\tpub fn oakundo_command_multi_child(multi: OakUndoCommand, index: c_int, out_child: *mut OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_redo_now`.\n\tpub fn oakundo_command_redo_now(command: OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_undo_now`.\n\tpub fn oakundo_command_undo_now(command: OakUndoCommand) -\u003e c_int;\n\t/// `oakundo_command_free`.\n\tpub fn oakundo_command_free(command: *mut OakUndoCommand);\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","codecbridge.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//! Codec task submitter registration, mirroring\n//! `src/task/src/codecbridge.h`.\n//!\n//! Wires oakcodec's task-submit callback (`oakcodec_set_task_submit_cb`) so\n//! conform/proxy requests from the codec side land back in the task module.\n//! This is a two-module coupling; the actual callback signatures live in\n//! `crate::bridge::codec` mirroring `include/codec/task.h` verbatim.\n//!\n//! CPP-PARITY: src/task/src/codecbridge.h\n\nuse std::ffi::{c_char, c_int, c_void};\n\nuse crate::bridge;\nuse crate::conform::ConformTask;\nuse crate::error::{Error, Result};\nuse crate::proxy::{ProxyParams, ProxyTask};\nuse crate::task::Task;\n\n/// Convert an oakcodec proxy-params POD into the Rust [`ProxyParams`].\nfn proxy_params_from_codec(params: \u0026bridge::codec::OakCodecProxyParams) -\u003e ProxyParams {\n\tProxyParams {\n\t\twidth: params.width,\n\t\theight: params.height,\n\t\tdivider: params.divider,\n\t\tversion: params.version,\n\t\tcrf: params.crf,\n\t\tinclude_audio: params.include_audio != 0,\n\t\textension: unsafe { cstr_buf_to_string(\u0026params.extension) },\n\t\tpreset: unsafe { cstr_buf_to_string(\u0026params.preset) },\n\t}\n}\n\n/// Read a NUL-terminated char array into a String (lossy).\nunsafe fn cstr_buf_to_string(buf: \u0026[c_char]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tlet bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };\n\tString::from_utf8_lossy(bytes).into_owned()\n}\n\n/// The installed submit callback, mirroring `submit_codec_task` in\n/// codecbridge.cpp (interim contract: submission is synchronous).\n///\n/// # Safety\n/// `req` must be a valid `OakCodecTaskRequest` or null.\nunsafe extern \"C\" fn submit_codec_task(req: *const bridge::codec::OakCodecTaskRequest, _userdata: *mut c_void) -\u003e c_int {\n\tif req.is_null() {\n\t\treturn bridge::codec::OAKCODEC_E_INVALID;\n\t}\n\tlet request = unsafe { \u0026*req };\n\n\t// Build the concrete task on an outer base so the behavior can be\n\t// driven by `start()`; both share the cancellation atom.\n\tmatch request.kind {\n\t\tbridge::codec::OAKCODEC_TASK_CONFORM =\u003e {\n\t\t\tlet task = ConformTask::new(request);\n\t\t\tlet atom = task.base.get_cancel_atom();\n\t\t\tlet title = task.base.title().to_string();\n\t\t\tlet mut outer = Task::new(\u0026title, atom);\n\t\t\touter.set_behavior(Box::new(task));\n\t\t\tmatch outer.start() {\n\t\t\t\tOk(()) =\u003e 0,\n\t\t\t\tErr(_) =\u003e bridge::codec::OAKCODEC_E_FAILED,\n\t\t\t}\n\t\t}\n\t\tbridge::codec::OAKCODEC_TASK_PROXY =\u003e {\n\t\t\tlet mut codec_params = bridge::codec::OakCodecProxyParams {\n\t\t\t\twidth: 0,\n\t\t\t\theight: 0,\n\t\t\t\tdivider: 0,\n\t\t\t\tversion: 0,\n\t\t\t\tcrf: 0,\n\t\t\t\tinclude_audio: 0,\n\t\t\t\textension: [0; 32],\n\t\t\t\tpreset: [0; 32],\n\t\t\t};\n\t\t\tunsafe {\n\t\t\t\tbridge::codec::oakcodec_proxy_params_default(\u0026mut codec_params);\n\t\t\t}\n\t\t\tlet params = proxy_params_from_codec(\u0026codec_params);\n\t\t\tlet task = ProxyTask::new(request, params);\n\t\t\tlet atom = task.base.get_cancel_atom();\n\t\t\tlet title = task.base.title().to_string();\n\t\t\tlet mut outer = Task::new(\u0026title, atom);\n\t\t\touter.set_behavior(Box::new(task));\n\t\t\tmatch outer.start() {\n\t\t\t\tOk(()) =\u003e 0,\n\t\t\t\tErr(_) =\u003e bridge::codec::OAKCODEC_E_FAILED,\n\t\t\t}\n\t\t}\n\t\t_ =\u003e bridge::codec::OAKCODEC_E_INVALID,\n\t}\n}\n\n/// Install the task-module submitter as oakcodec's callback. Returns\n/// `Err(Error::State)` if already registered.\npub fn register_codec_task_submitter() -\u003e Result\u003c()\u003e {\n\tif is_codec_task_submitter_registered() {\n\t\treturn Err(Error::State);\n\t}\n\tunsafe {\n\t\tbridge::codec::oakcodec_set_task_submit_cb(Some(submit_codec_task), std::ptr::null_mut());\n\t}\n\tOk(())\n}\n\n/// Remove the task-module submitter from oakcodec. Idempotent.\npub fn unregister_codec_task_submitter() -\u003e Result\u003c()\u003e {\n\tunsafe {\n\t\tbridge::codec::oakcodec_set_task_submit_cb(None, std::ptr::null_mut());\n\t}\n\tOk(())\n}\n\n/// Whether a submitter is currently installed (queries oakcodec).\npub fn is_codec_task_submitter_registered() -\u003e bool {\n\tunsafe { bridge::codec::oakcodec_task_submit_is_registered() != 0 }\n}\n","traces":[{"line":36,"address":[],"length":0,"stats":{"Line":7}},{"line":38,"address":[],"length":0,"stats":{"Line":14}},{"line":39,"address":[],"length":0,"stats":{"Line":14}},{"line":40,"address":[],"length":0,"stats":{"Line":14}},{"line":41,"address":[],"length":0,"stats":{"Line":14}},{"line":42,"address":[],"length":0,"stats":{"Line":14}},{"line":43,"address":[],"length":0,"stats":{"Line":14}},{"line":44,"address":[],"length":0,"stats":{"Line":21}},{"line":45,"address":[],"length":0,"stats":{"Line":7}},{"line":50,"address":[],"length":0,"stats":{"Line":14}},{"line":51,"address":[],"length":0,"stats":{"Line":175}},{"line":52,"address":[],"length":0,"stats":{"Line":56}},{"line":53,"address":[],"length":0,"stats":{"Line":42}},{"line":61,"address":[],"length":0,"stats":{"Line":16}},{"line":62,"address":[],"length":0,"stats":{"Line":32}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":65,"address":[],"length":0,"stats":{"Line":30}},{"line":69,"address":[],"length":0,"stats":{"Line":15}},{"line":71,"address":[],"length":0,"stats":{"Line":21}},{"line":72,"address":[],"length":0,"stats":{"Line":21}},{"line":73,"address":[],"length":0,"stats":{"Line":21}},{"line":74,"address":[],"length":0,"stats":{"Line":28}},{"line":75,"address":[],"length":0,"stats":{"Line":21}},{"line":76,"address":[],"length":0,"stats":{"Line":7}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":5}},{"line":89,"address":[],"length":0,"stats":{"Line":7}},{"line":90,"address":[],"length":0,"stats":{"Line":7}},{"line":93,"address":[],"length":0,"stats":{"Line":7}},{"line":95,"address":[],"length":0,"stats":{"Line":21}},{"line":96,"address":[],"length":0,"stats":{"Line":28}},{"line":97,"address":[],"length":0,"stats":{"Line":21}},{"line":98,"address":[],"length":0,"stats":{"Line":21}},{"line":99,"address":[],"length":0,"stats":{"Line":28}},{"line":100,"address":[],"length":0,"stats":{"Line":21}},{"line":101,"address":[],"length":0,"stats":{"Line":7}},{"line":102,"address":[],"length":0,"stats":{"Line":2}},{"line":103,"address":[],"length":0,"stats":{"Line":5}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":22}},{"line":113,"address":[],"length":0,"stats":{"Line":22}},{"line":114,"address":[],"length":0,"stats":{"Line":3}},{"line":117,"address":[],"length":0,"stats":{"Line":38}},{"line":119,"address":[],"length":0,"stats":{"Line":19}},{"line":123,"address":[],"length":0,"stats":{"Line":34}},{"line":125,"address":[],"length":0,"stats":{"Line":68}},{"line":127,"address":[],"length":0,"stats":{"Line":34}},{"line":131,"address":[],"length":0,"stats":{"Line":22}},{"line":132,"address":[],"length":0,"stats":{"Line":22}}],"covered":49,"coverable":49},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","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//! `ConformTask`, mirroring `src/task/src/conform/conform.h`.\n//!\n//! Transcodes an audio stream to a PCM cache file (oakcodec kind\n//! `OAKCODEC_TASK_CONFORM`) and reports progress as it decodes.\n//!\n//! CPP-PARITY: src/task/src/conform/conform.h\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// A conform task: copies/channels of one audio stream into per-channel\n/// working PCM cache files, then renames them to the final names.\npub struct ConformTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Absolute input media filename.\n\tpub input_filename: String,\n\t/// Final destination path (replaces the source path extension).\n\tpub output_filename: String,\n\t/// Audio stream index inside the source media.\n\tpub stream_index: i32,\n\t/// Target sample rate (Hz).\n\tpub sample_rate: i32,\n\t/// Target channel layout mask (ffmpeg-style).\n\tpub channel_layout: u64,\n\t/// Target sample format (enum as int; `oakcore_rs::SampleFormat`).\n\tpub sample_format: i32,\n\t/// Resolved final per-channel filenames (from [`ConformTask::derive_filenames`]).\n\tfinal_names: Vec\u003cString\u003e,\n\t/// Resolved working per-channel filenames.\n\tworking_names: Vec\u003cString\u003e,\n}\n\nimpl ConformTask {\n\t/// Build a conform task from an oakcodec request, mirroring the C++\n\t/// constructor.\n\tpub fn new(request: \u0026bridge::codec::OakCodecTaskRequest) -\u003e ConformTask {\n\t\tlet input = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) };\n\t\tlet output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) };\n\t\tlet title = format!(\"Conforming Audio {}:{}\", input, request.stream_index);\n\t\tConformTask {\n\t\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\t\tinput_filename: input,\n\t\t\toutput_filename: output,\n\t\t\tstream_index: request.stream_index,\n\t\t\tsample_rate: request.sample_rate,\n\t\t\tchannel_layout: request.channel_layout,\n\t\t\tsample_format: request.sample_format,\n\t\t\tfinal_names: Vec::new(),\n\t\t\tworking_names: Vec::new(),\n\t\t}\n\t}\n\n\t/// Derive the final and working PCM filenames for the given first-channel\n\t/// final filename and channel count.\n\t///\n\t/// C++ contract: the first-channel final name must end in `.0.pcm`; the\n\t/// final name for channel `i` replaces the trailing index, and the\n\t/// working name appends `.working`. See `derive_filenames` in\n\t/// conform.cpp — this is a parity/golden candidate (see\n\t/// tests/parity_test.rs).\n\t///\n\t/// CPP-PARITY: src/task/src/conform/conform.cpp (derive_filenames)\n\tpub fn derive_filenames(first_channel_final: \u0026str, channel_count: usize) -\u003e Result\u003c(Vec\u003cString\u003e, Vec\u003cString\u003e)\u003e {\n\t\tconst SUFFIX: \u0026str = \".0.pcm\";\n\n\t\tif channel_count \u003c 1 || first_channel_final.len() \u003c= SUFFIX.len()\n\t\t\t|| !first_channel_final.ends_with(SUFFIX)\n\t\t{\n\t\t\treturn Err(Error::Failed(\"Invalid conform output filename\".to_string()));\n\t\t}\n\n\t\tlet base = \u0026first_channel_final[..first_channel_final.len() - SUFFIX.len()];\n\t\tlet mut final_names = Vec::with_capacity(channel_count);\n\t\tlet mut working_names = Vec::with_capacity(channel_count);\n\t\tfor i in 0..channel_count {\n\t\t\tlet final_name = format!(\"{base}.{i}.pcm\");\n\t\t\tfinal_names.push(final_name.clone());\n\t\t\tworking_names.push(format!(\"{final_name}.working\"));\n\t\t}\n\t\tOk((final_names, working_names))\n\t}\n}\n\nimpl TaskBehavior for ConformTask {\n\t/// Run the conform via the oakcodec decoder (`bridge::codec`), emitting\n\t/// progress as audio is decoded and written to the working files, then\n\t/// rename working → final.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet channel_count = self.channel_layout.count_ones() as usize;\n\t\tlet (final_names, working_names) =\n\t\t\tSelf::derive_filenames(\u0026self.output_filename, channel_count).map_err(|_| {\n\t\t\t\ttask.set_error(\"Invalid conform output filename\");\n\t\t\t\tError::Failed(\"Invalid conform output filename\".to_string())\n\t\t\t})?;\n\t\tself.final_names = final_names;\n\t\tself.working_names = working_names;\n\n\t\tlet decoder = unsafe { bridge::codec::oakcodec_decoder_init() };\n\t\tif decoder.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create decoder\");\n\t\t\treturn Err(Error::Failed(\"Failed to create decoder\".to_string()));\n\t\t}\n\t\tlet mut decoder = decoder;\n\n\t\tlet result = (|| {\n\t\t\tlet open_result = unsafe {\n\t\t\t\tbridge::codec::oakcodec_decoder_open(decoder, cstr(\u0026self.input_filename), self.stream_index)\n\t\t\t};\n\t\t\tif open_result != 0 {\n\t\t\t\tlet err = decoder_error(decoder);\n\t\t\t\ttask.set_error(\u0026format!(\"Failed to open decoder for audio conform: {err}\"));\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tlet working_ptrs: Vec\u003c*const std::ffi::c_char\u003e =\n\t\t\t\tself.working_names.iter().map(|n| cstr(n)).collect();\n\t\t\tlet conform_result = unsafe {\n\t\t\t\tbridge::codec::oakcodec_decoder_conform_audio(\n\t\t\t\t\tdecoder,\n\t\t\t\t\tworking_ptrs.as_ptr(),\n\t\t\t\t\tworking_ptrs.len() as i32,\n\t\t\t\t\tself.sample_rate,\n\t\t\t\t\tself.channel_layout,\n\t\t\t\t\tself.sample_format,\n\t\t\t\t\tbridge::render::OakCancelAtom::from_chandle(task.get_cancel_atom()),\n\t\t\t\t)\n\t\t\t};\n\t\t\tunsafe {\n\t\t\t\tbridge::codec::oakcodec_decoder_close(decoder);\n\t\t\t}\n\n\t\t\tif conform_result == 0 {\n\t\t\t\t// Rename each working file into place; a failure aborts the\n\t\t\t\t// rest (mirroring the C++ loop).\n\t\t\t\tfor i in 0..self.working_names.len() {\n\t\t\t\t\tif std::fs::rename(\u0026self.working_names[i], \u0026self.final_names[i]).is_err() {\n\t\t\t\t\t\ttask.set_error(\"Failed to move conformed audio into place\");\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttrue\n\t\t\t} else {\n\t\t\t\t// Clean up any partial working files.\n\t\t\t\tfor name in \u0026self.working_names {\n\t\t\t\t\tlet _ = std::fs::remove_file(name);\n\t\t\t\t}\n\t\t\t\tif conform_result == bridge::codec::OAKCODEC_E_CANCELLED {\n\t\t\t\t\ttask.set_error(\"Audio conform was cancelled\");\n\t\t\t\t} else {\n\t\t\t\t\ttask.set_error(\"Audio conform failed\");\n\t\t\t\t}\n\t\t\t\tfalse\n\t\t\t}\n\t\t})();\n\n\t\tunsafe {\n\t\t\tbridge::codec::oakcodec_decoder_free(\u0026mut decoder);\n\t\t}\n\n\t\tif result {\n\t\t\tOk(())\n\t\t} else {\n\t\t\tErr(Error::Failed(\"Audio conform failed\".to_string()))\n\t\t}\n\t}\n}\n\n/// Two-stage read of the decoder's last error string.\nfn decoder_error(decoder: CHandle) -\u003e String {\n\tlet mut buf = [0i8; 256];\n\tlet needed = unsafe { bridge::codec::oakcodec_decoder_last_error(decoder, buf.as_mut_ptr(), buf.len() as i32) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n","traces":[{"line":56,"address":[],"length":0,"stats":{"Line":7}},{"line":57,"address":[],"length":0,"stats":{"Line":21}},{"line":58,"address":[],"length":0,"stats":{"Line":21}},{"line":59,"address":[],"length":0,"stats":{"Line":14}},{"line":61,"address":[],"length":0,"stats":{"Line":28}},{"line":64,"address":[],"length":0,"stats":{"Line":14}},{"line":65,"address":[],"length":0,"stats":{"Line":14}},{"line":66,"address":[],"length":0,"stats":{"Line":14}},{"line":67,"address":[],"length":0,"stats":{"Line":14}},{"line":68,"address":[],"length":0,"stats":{"Line":7}},{"line":69,"address":[],"length":0,"stats":{"Line":7}},{"line":83,"address":[],"length":0,"stats":{"Line":13}},{"line":86,"address":[],"length":0,"stats":{"Line":49}},{"line":87,"address":[],"length":0,"stats":{"Line":11}},{"line":89,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":45}},{"line":93,"address":[],"length":0,"stats":{"Line":27}},{"line":94,"address":[],"length":0,"stats":{"Line":27}},{"line":95,"address":[],"length":0,"stats":{"Line":43}},{"line":96,"address":[],"length":0,"stats":{"Line":51}},{"line":97,"address":[],"length":0,"stats":{"Line":85}},{"line":98,"address":[],"length":0,"stats":{"Line":34}},{"line":100,"address":[],"length":0,"stats":{"Line":9}},{"line":108,"address":[],"length":0,"stats":{"Line":7}},{"line":109,"address":[],"length":0,"stats":{"Line":14}},{"line":110,"address":[],"length":0,"stats":{"Line":14}},{"line":111,"address":[],"length":0,"stats":{"Line":28}},{"line":112,"address":[],"length":0,"stats":{"Line":0}},{"line":113,"address":[],"length":0,"stats":{"Line":0}},{"line":115,"address":[],"length":0,"stats":{"Line":14}},{"line":116,"address":[],"length":0,"stats":{"Line":14}},{"line":118,"address":[],"length":0,"stats":{"Line":14}},{"line":119,"address":[],"length":0,"stats":{"Line":14}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":14}},{"line":125,"address":[],"length":0,"stats":{"Line":14}},{"line":126,"address":[],"length":0,"stats":{"Line":7}},{"line":127,"address":[],"length":0,"stats":{"Line":35}},{"line":129,"address":[],"length":0,"stats":{"Line":7}},{"line":130,"address":[],"length":0,"stats":{"Line":6}},{"line":131,"address":[],"length":0,"stats":{"Line":6}},{"line":132,"address":[],"length":0,"stats":{"Line":2}},{"line":135,"address":[],"length":0,"stats":{"Line":10}},{"line":136,"address":[],"length":0,"stats":{"Line":35}},{"line":137,"address":[],"length":0,"stats":{"Line":5}},{"line":138,"address":[],"length":0,"stats":{"Line":5}},{"line":139,"address":[],"length":0,"stats":{"Line":5}},{"line":140,"address":[],"length":0,"stats":{"Line":10}},{"line":141,"address":[],"length":0,"stats":{"Line":5}},{"line":142,"address":[],"length":0,"stats":{"Line":5}},{"line":143,"address":[],"length":0,"stats":{"Line":5}},{"line":144,"address":[],"length":0,"stats":{"Line":5}},{"line":145,"address":[],"length":0,"stats":{"Line":15}},{"line":148,"address":[],"length":0,"stats":{"Line":5}},{"line":149,"address":[],"length":0,"stats":{"Line":5}},{"line":152,"address":[],"length":0,"stats":{"Line":5}},{"line":155,"address":[],"length":0,"stats":{"Line":6}},{"line":156,"address":[],"length":0,"stats":{"Line":16}},{"line":157,"address":[],"length":0,"stats":{"Line":0}},{"line":158,"address":[],"length":0,"stats":{"Line":0}},{"line":161,"address":[],"length":0,"stats":{"Line":2}},{"line":164,"address":[],"length":0,"stats":{"Line":15}},{"line":165,"address":[],"length":0,"stats":{"Line":6}},{"line":167,"address":[],"length":0,"stats":{"Line":5}},{"line":168,"address":[],"length":0,"stats":{"Line":4}},{"line":170,"address":[],"length":0,"stats":{"Line":2}},{"line":172,"address":[],"length":0,"stats":{"Line":3}},{"line":177,"address":[],"length":0,"stats":{"Line":7}},{"line":180,"address":[],"length":0,"stats":{"Line":7}},{"line":181,"address":[],"length":0,"stats":{"Line":2}},{"line":183,"address":[],"length":0,"stats":{"Line":5}},{"line":189,"address":[],"length":0,"stats":{"Line":2}},{"line":190,"address":[],"length":0,"stats":{"Line":4}},{"line":191,"address":[],"length":0,"stats":{"Line":12}},{"line":192,"address":[],"length":0,"stats":{"Line":2}},{"line":193,"address":[],"length":0,"stats":{"Line":0}},{"line":195,"address":[],"length":0,"stats":{"Line":50}},{"line":196,"address":[],"length":0,"stats":{"Line":8}}],"covered":72,"coverable":79},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","customcache.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//! `CustomCacheTask`, mirroring `src/task/src/customcache/customcachetask.h`.\n//!\n//! A task that delegates its completion to an external cache fill: it does\n//! not do work itself but waits until the cache reports it is done (via\n//! [`CustomCacheTask::finish`]) or is cancelled.\n//!\n//! CPP-PARITY: src/task/src/customcache/customcachetask.h\n\nuse std::sync::{Arc, Condvar, Mutex};\n\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// Shared state between the parked task thread, the owning cache\n/// ([`CustomCacheTask::finish`]) and the cancel hook. Mirrors the C++\n/// `mutex_`/`wait_cond_`/`cancelled_through_finish_` trio; the cancel atom\n/// copy lets `finish()` wake the task through the same channel the C++\n/// `cancel()` uses.\npub struct CustomCacheState {\n\t/// Set by `finish()` before the cancellation request.\n\tcancelled_through_finish: Mutex\u003cbool\u003e,\n\t/// Parked-task wakeup.\n\twait: Condvar,\n\t/// Callback invoked when the task is cancelled (not by `finish()`).\n\tcancelled_callback: Mutex\u003cOption\u003cBox\u003cdyn FnMut() + Send\u003e\u003e\u003e,\n\t/// Copy of the task's cancellation atom.\n\tatom: CHandle,\n}\n\nimpl CustomCacheState {\n\t/// Mark the cache fill as complete and wake the parked task.\n\tpub fn finish(\u0026self) {\n\t\t*self.cancelled_through_finish.lock().unwrap() = true;\n\t\tif !self.atom.is_null() {\n\t\t\tlet atom = crate::bridge::render::OakCancelAtom::from_chandle(self.atom);\n\t\t\tunsafe {\n\t\t\t\tcrate::bridge::render::oakrender_cancelatom_cancel(atom);\n\t\t\t}\n\t\t}\n\t\tself.wait.notify_one();\n\t}\n\n\t/// The C++ `cancel_event()` hook: fires the callback unless the task was\n\t/// finished through `finish()`, then wakes the parked task.\n\tpub fn cancel_event(\u0026self) {\n\t\tif !*self.cancelled_through_finish.lock().unwrap() {\n\t\t\tif let Some(cb) = self.cancelled_callback.lock().unwrap().as_mut() {\n\t\t\t\tcb();\n\t\t\t}\n\t\t}\n\t\tself.wait.notify_one();\n\t}\n\n\t/// The full C++ `cancel()`: cancel the atom and run the cancel-event\n\t/// hook. Used by the owning cache to abort a fill.\n\tpub fn cancel(\u0026self) {\n\t\tif !self.atom.is_null() {\n\t\t\tlet atom = crate::bridge::render::OakCancelAtom::from_chandle(self.atom);\n\t\t\tunsafe {\n\t\t\t\tcrate::bridge::render::oakrender_cancelatom_cancel(atom);\n\t\t\t}\n\t\t}\n\t\tself.cancel_event();\n\t}\n\n\t/// Whether the fill was completed through `finish()`.\n\tfn is_finished_through_finish(\u0026self) -\u003e bool {\n\t\t*self.cancelled_through_finish.lock().unwrap()\n\t}\n}\n\n/// A cache-fill task that finishes only when [`CustomCacheTask::finish`] is\n/// called by the owning cache. Uses a `Mutex` + `Condvar` to park the task\n/// thread until completion or cancellation.\npub struct CustomCacheTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// The name of the sequence being cached.\n\tpub sequence_name: String,\n\t/// Shared park/notify state.\n\tstate: Arc\u003cCustomCacheState\u003e,\n}\n\nimpl CustomCacheTask {\n\t/// Create a cache-fill task with the given sequence name. The cancel\n\t/// hook that wakes the parked thread is installed on the base task.\n\tpub fn new(sequence_name: \u0026str) -\u003e CustomCacheTask {\n\t\tlet base = Task::new(\u0026format!(\"Caching custom range for \\\"{sequence_name}\\\"\"), CHandle::null());\n\t\tlet state = Arc::new(CustomCacheState {\n\t\t\tcancelled_through_finish: Mutex::new(false),\n\t\t\twait: Condvar::new(),\n\t\t\tcancelled_callback: Mutex::new(None),\n\t\t\tatom: base.get_cancel_atom(),\n\t\t});\n\t\tlet mut task = CustomCacheTask {\n\t\t\tbase,\n\t\t\tsequence_name: sequence_name.to_string(),\n\t\t\tstate,\n\t\t};\n\t\tlet hook_state = task.state.clone();\n\t\ttask.base.set_cancel_event(Box::new(move || hook_state.cancel_event()));\n\t\ttask\n\t}\n\n\t/// Mark the cache fill as complete, waking the parked task thread.\n\tpub fn finish(\u0026mut self) {\n\t\tself.state.finish();\n\t}\n\n\t/// Install the callback invoked when the task is cancelled.\n\tpub fn set_cancelled_callback(\u0026mut self, cb: Box\u003cdyn FnMut() + Send\u003e) {\n\t\t*self.state.cancelled_callback.lock().unwrap() = Some(cb);\n\t}\n\n\t/// Shared-state accessor for the owning cache (and tests) to call\n\t/// [`CustomCacheTask::finish`] from another thread.\n\tpub fn state(\u0026self) -\u003e Arc\u003cCustomCacheState\u003e {\n\t\tself.state.clone()\n\t}\n}\n\nimpl TaskBehavior for CustomCacheTask {\n\t/// Park until [`CustomCacheTask::finish`] or cancellation; return\n\t/// `Err(Error::Cancelled)` if cancelled while waiting.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Install the cancel hook on the task this behavior actually runs\n\t\t// under (the outer task driven by the manager / C ABI). The\n\t\t// constructor-installed hook on the inner `base` only fires when the\n\t\t// base itself is cancelled; without this the outer `cancel()` would\n\t\t// set the shared atom but never wake the parked thread.\n\t\tlet hook_state = self.state.clone();\n\t\ttask.set_cancel_event(Box::new(move || hook_state.cancel_event()));\n\n\t\tlet mut guard = self.state.cancelled_through_finish.lock().unwrap();\n\t\twhile !task.is_cancelled() {\n\t\t\tguard = self.state.wait.wait(guard).unwrap();\n\t\t}\n\t\tdrop(guard);\n\t\tif self.state.is_finished_through_finish() {\n\t\t\tOk(())\n\t\t} else {\n\t\t\tErr(Error::Cancelled)\n\t\t}\n\t}\n}\n","traces":[{"line":49,"address":[],"length":0,"stats":{"Line":2}},{"line":50,"address":[],"length":0,"stats":{"Line":4}},{"line":51,"address":[],"length":0,"stats":{"Line":2}},{"line":52,"address":[],"length":0,"stats":{"Line":6}},{"line":54,"address":[],"length":0,"stats":{"Line":2}},{"line":57,"address":[],"length":0,"stats":{"Line":4}},{"line":62,"address":[],"length":0,"stats":{"Line":3}},{"line":63,"address":[],"length":0,"stats":{"Line":6}},{"line":64,"address":[],"length":0,"stats":{"Line":8}},{"line":65,"address":[],"length":0,"stats":{"Line":1}},{"line":68,"address":[],"length":0,"stats":{"Line":6}},{"line":73,"address":[],"length":0,"stats":{"Line":3}},{"line":74,"address":[],"length":0,"stats":{"Line":3}},{"line":75,"address":[],"length":0,"stats":{"Line":9}},{"line":77,"address":[],"length":0,"stats":{"Line":3}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":84,"address":[],"length":0,"stats":{"Line":5}},{"line":85,"address":[],"length":0,"stats":{"Line":10}},{"line":104,"address":[],"length":0,"stats":{"Line":5}},{"line":105,"address":[],"length":0,"stats":{"Line":20}},{"line":106,"address":[],"length":0,"stats":{"Line":15}},{"line":107,"address":[],"length":0,"stats":{"Line":10}},{"line":108,"address":[],"length":0,"stats":{"Line":10}},{"line":109,"address":[],"length":0,"stats":{"Line":15}},{"line":110,"address":[],"length":0,"stats":{"Line":5}},{"line":114,"address":[],"length":0,"stats":{"Line":10}},{"line":117,"address":[],"length":0,"stats":{"Line":15}},{"line":118,"address":[],"length":0,"stats":{"Line":15}},{"line":119,"address":[],"length":0,"stats":{"Line":5}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":124,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":2}},{"line":129,"address":[],"length":0,"stats":{"Line":6}},{"line":134,"address":[],"length":0,"stats":{"Line":5}},{"line":135,"address":[],"length":0,"stats":{"Line":10}},{"line":142,"address":[],"length":0,"stats":{"Line":5}},{"line":148,"address":[],"length":0,"stats":{"Line":15}},{"line":149,"address":[],"length":0,"stats":{"Line":15}},{"line":151,"address":[],"length":0,"stats":{"Line":20}},{"line":152,"address":[],"length":0,"stats":{"Line":11}},{"line":153,"address":[],"length":0,"stats":{"Line":12}},{"line":155,"address":[],"length":0,"stats":{"Line":10}},{"line":156,"address":[],"length":0,"stats":{"Line":5}},{"line":157,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":3}}],"covered":43,"coverable":45},{"path":["/","Users","sunyu","Projects","oak","src","task","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/task/error.h` verbatim; project-wide\n//! -MMCCCC scheme (module registry in include/common/error.h), pass-through\n//! untranslated. Module number for task is **08**; task additionally defines\n//! `OAKTASK_E_CANCELLED` (see include/task/error.h).\n\n/// Success.\npub const OAKTASK_OK: i32 = 0;\n/// Null handle or invalid argument.\npub const OAKTASK_E_INVALID: i32 = -80001;\n/// Call not valid in the current state.\npub const OAKTASK_E_STATE: i32 = -80002;\n/// The underlying operation failed.\npub const OAKTASK_E_FAILED: i32 = -80003;\n/// Index out of range / entry not found.\npub const OAKTASK_E_NOT_FOUND: i32 = -80004;\n/// Allocation failed.\npub const OAKTASK_E_NOMEM: i32 = -80005;\n/// The task was cancelled.\npub const OAKTASK_E_CANCELLED: i32 = -80006;\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 OAKTASK_E_INVALID,\n\t\t\tError::State =\u003e OAKTASK_E_STATE,\n\t\t\tError::Failed(_) =\u003e OAKTASK_E_FAILED,\n\t\t\tError::NotFound =\u003e OAKTASK_E_NOT_FOUND,\n\t\t\tError::NoMem =\u003e OAKTASK_E_NOMEM,\n\t\t\tError::Cancelled =\u003e OAKTASK_E_CANCELLED,\n\t\t}\n\t}\n}\n","traces":[{"line":59,"address":[],"length":0,"stats":{"Line":3}},{"line":60,"address":[],"length":0,"stats":{"Line":3}},{"line":61,"address":[],"length":0,"stats":{"Line":0}},{"line":62,"address":[],"length":0,"stats":{"Line":0}},{"line":63,"address":[],"length":0,"stats":{"Line":1}},{"line":64,"address":[],"length":0,"stats":{"Line":0}},{"line":65,"address":[],"length":0,"stats":{"Line":0}},{"line":66,"address":[],"length":0,"stats":{"Line":2}}],"covered":4,"coverable":8},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","export.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//! `ExportTask`, mirroring `src/task/src/export/export.h`.\n//!\n//! Renders the viewer output through [`crate::render::RenderTask`] and writes\n//! it to a file via the oakcodec encoder (`bridge::codec`), mapping each\n//! rendered [`Rational`] frame to an `OakFrame` and each rendered\n//! [`TimeRange`] to audio samples.\n//!\n//! **Simplifications over the C++**: no temporary-file rename dance (the\n//! encoder writes straight to the requested filename), no sidecar subtitle\n//! encoder (subtitles go through the main encoder), and no frame/audio\n//! reordering maps — the [`RenderTask`] loop delivers frames and audio in\n//! timestamp order (its reorder buffer), so the encoder is written directly.\n//!\n//! CPP-PARITY: src/task/src/export/export.h\n\nuse crate::bridge;\nuse crate::bridge::codec::OakCodecEncodingParams;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::render::{ForceParams, RenderTask, RenderTaskBehavior};\nuse crate::task::{Task, TaskBehavior};\nuse oakcore_rs::{Rational, TimeRange};\n\n/// An export task. Owns the encoder and the project copier; the base\n/// [`RenderTask`] does the frame/audio production.\npub struct ExportTask {\n\t/// The render base (itself a task).\n\tpub render: RenderTask,\n\t/// The viewer node (borrowed `OakNodeNode`) being exported.\n\tpub viewer_node: CHandle,\n\t/// The color manager (borrowed `OakNodeColorManager`).\n\tpub color_manager: CHandle,\n\t/// The encoding parameters (mirror of `oakcodec_encoding_params`).\n\tpub encoding_params: EncodingParams,\n\t/// Owning copy of the export project (borrowed `OakRenderProjectCopier`).\n\t/// Left empty in this simplified implementation (the render drives the\n\t/// viewer directly).\n\tpub copier: CHandle,\n\t/// The opened encoder.\n\tencoder: CHandle,\n\t/// The encoder subtitles are written to (the main encoder in this\n\t/// simplified implementation).\n\tsubtitle_encoder: CHandle,\n\t/// Frame counter for progress reporting.\n\tframe_time: i64,\n\t/// Streak of consecutive null frames (fails the export after 8).\n\tnull_frame_streak: i32,\n}\n\n/// Mirror of `oakcodec_encoding_params` in `include/codec/encoder.h`, kept as\n/// plain fields so the export task can build the encoder params without an\n/// oakcodec handle. Only the fields the task reads are declared; see the\n/// header for the full inventory.\npub struct EncodingParams {\n\t/// Output filename.\n\tpub filename: String,\n\t/// Export format id (`olive::ExportFormat::Format`).\n\tpub format: i32,\n\t/// Whether video is exported.\n\tpub video_enabled: bool,\n\t/// Video codec id.\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/denominator).\n\tpub video_time_base_num: i32,\n\t/// Frame duration denominator.\n\tpub video_time_base_den: i32,\n\t/// Delivery pixel format (`oakcore_rs::PixelFormat` as int).\n\tpub video_pixel_format: i32,\n\t/// Whether audio is exported.\n\tpub audio_enabled: bool,\n\t/// Audio codec id.\n\tpub audio_codec: i32,\n\t/// Whether subtitles are exported.\n\tpub subtitles_enabled: bool,\n\t/// Export length numerator (seconds rational).\n\tpub export_length_num: i32,\n\t/// Export length denominator.\n\tpub export_length_den: i32,\n}\n\nimpl ExportTask {\n\t/// Build a new export task from the viewer, color manager, and encoding\n\t/// params.\n\tpub fn new(viewer: CHandle, color_manager: CHandle, params: EncodingParams) -\u003e ExportTask {\n\t\tlet label = node_label(viewer);\n\t\tlet title = format!(\"Exporting \\\"{label}\\\"\");\n\t\tlet base = Task::new(\u0026title, CHandle::null());\n\t\tlet render = RenderTask::new(base, CHandle::null(), CHandle::null(), viewer, ForceParams::default(), None);\n\t\tExportTask {\n\t\t\trender,\n\t\t\tviewer_node: viewer,\n\t\t\tcolor_manager,\n\t\t\tencoding_params: params,\n\t\t\tcopier: CHandle::null(),\n\t\t\tencoder: CHandle::null(),\n\t\t\tsubtitle_encoder: CHandle::null(),\n\t\t\tframe_time: 0,\n\t\t\tnull_frame_streak: 0,\n\t\t}\n\t}\n\n\t/// Build the C-ABI encoding-params POD for the encoder from the Rust\n\t/// mirror (zeroed fields = disabled/defaults).\n\tfn build_codec_params(\u0026self) -\u003e OakCodecEncodingParams {\n\t\tlet mut params = OakCodecEncodingParams {\n\t\t\tfilename: [0; 1024],\n\t\t\tformat: 0,\n\t\t\tvideo_enabled: 0,\n\t\t\tvideo_codec: 0,\n\t\t\tvideo_width: 0,\n\t\t\tvideo_height: 0,\n\t\t\tvideo_time_base_num: 0,\n\t\t\tvideo_time_base_den: 0,\n\t\t\tvideo_pixel_format: 0,\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: 0,\n\t\t\taudio_enabled: 0,\n\t\t\taudio_codec: 0,\n\t\t\taudio_sample_rate: 0,\n\t\t\taudio_channel_layout: 0,\n\t\t\taudio_sample_format: 0,\n\t\t\taudio_bit_rate: 0,\n\t\t\tsubtitles_enabled: 0,\n\t\t\tsubtitles_codec: 0,\n\t\t\tsubtitles_are_sidecar: 0,\n\t\t\tsubtitles_sidecar_format: 0,\n\t\t\tcolor_transform_output: [0; 256],\n\t\t\texport_length_num: 0,\n\t\t\texport_length_den: 0,\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\tlet bytes = self.encoding_params.filename.as_bytes();\n\t\tlet n = bytes.len().min(1023);\n\t\tparams.filename[..n].copy_from_slice(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const i8, n) });\n\t\tparams.format = self.encoding_params.format;\n\t\tparams.video_enabled = self.encoding_params.video_enabled as i32;\n\t\tparams.video_codec = self.encoding_params.video_codec;\n\t\tparams.video_width = self.encoding_params.video_width;\n\t\tparams.video_height = self.encoding_params.video_height;\n\t\tparams.video_time_base_num = self.encoding_params.video_time_base_num;\n\t\tparams.video_time_base_den = self.encoding_params.video_time_base_den;\n\t\tparams.video_pixel_format = self.encoding_params.video_pixel_format;\n\t\tparams.audio_enabled = self.encoding_params.audio_enabled as i32;\n\t\tparams.audio_codec = self.encoding_params.audio_codec;\n\t\tparams.subtitles_enabled = self.encoding_params.subtitles_enabled as i32;\n\t\tparams.export_length_num = self.encoding_params.export_length_num;\n\t\tparams.export_length_den = self.encoding_params.export_length_den;\n\t\tparams\n\t}\n\n\t/// Resolve the export range: the custom range when set, otherwise the\n\t/// whole sequence length.\n\tfn export_range(\u0026self) -\u003e TimeRange {\n\t\tlet mut len_num = 0;\n\t\tlet mut len_den = 1;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_sequence_get_length(self.viewer_node, \u0026mut len_num, \u0026mut len_den);\n\t\t}\n\t\tTimeRange::new(Rational::new(0, 1), Rational::new(len_num as i64, len_den as i64))\n\t}\n}\n\nimpl TaskBehavior for ExportTask {\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Share the caller's cancellation atom with the inner render base.\n\t\tself.render.base.set_cancel_atom(task.get_cancel_atom());\n\n\t\tlet codec_params = self.build_codec_params();\n\t\tlet encoder = unsafe { bridge::codec::oakcodec_encoder_init(\u0026codec_params) };\n\t\tif encoder.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create encoder\");\n\t\t\treturn Err(Error::Failed(\"Failed to create encoder\".to_string()));\n\t\t}\n\t\tself.encoder = encoder;\n\n\t\tif unsafe { bridge::codec::oakcodec_encoder_open(encoder) } != 0 {\n\t\t\tlet err = encoder_error(encoder);\n\t\t\ttask.set_error(\u0026format!(\"Failed to open file: {err}\"));\n\t\t\treturn Err(Error::Failed(\"Failed to open file\".to_string()));\n\t\t}\n\t\tself.subtitle_encoder = encoder;\n\n\t\tlet export_range = self.export_range();\n\n\t\t// Force params: force the renderer to the requested output size and\n\t\t// pixel format (no color-matrix scaling in this simplified path).\n\t\tlet mut force = ForceParams::default();\n\t\tif self.encoding_params.video_enabled {\n\t\t\tforce.force_width = self.encoding_params.video_width;\n\t\t\tforce.force_height = self.encoding_params.video_height;\n\t\t\tforce.force_format = unsafe { bridge::codec::oakcodec_encoder_get_desired_pixel_format(encoder) };\n\t\t\tforce.force_channel_count = 4; // RGBA\n\t\t}\n\t\tself.render.force_params = force;\n\t\tself.render.set_render_inputs(\n\t\t\tself.color_manager,\n\t\t\tCHandle::null(),\n\t\t\t0, // RenderMode::k_online\n\t\t\tself.encoding_params.audio_enabled,\n\t\t\texport_range,\n\t\t);\n\n\t\t// Drive the render with `self` as the subclass behavior (the C++\n\t\t// virtual dispatch receiver). The render is temporarily moved out of\n\t\t// `self` to avoid a self-referential borrow; it is put back right\n\t\t// after, before the encoder flush below.\n\t\tlet mut render = std::mem::replace(\u0026mut self.render, crate::render::RenderTask::placeholder());\n\t\tlet result = render.render(task, self);\n\t\tself.render = render;\n\t\tresult?;\n\n\t\t// Flush the encoder and surface any trailing error.\n\t\tunsafe {\n\t\t\tbridge::codec::oakcodec_encoder_flush(self.encoder);\n\t\t}\n\t\tlet err = encoder_error(self.encoder);\n\t\tif !err.is_empty() {\n\t\t\ttask.set_error(\u0026err);\n\t\t\treturn Err(Error::Failed(\"Encoder flush failed\".to_string()));\n\t\t}\n\n\t\tOk(())\n\t}\n}\n\nimpl RenderTaskBehavior for ExportTask {\n\tfn frame_downloaded(\u0026mut self, task: \u0026mut Task, frame: CHandle) -\u003e Result\u003c()\u003e {\n\t\tif frame.ctx.is_null() {\n\t\t\tself.null_frame_streak += 1;\n\t\t\tif self.null_frame_streak \u003e= 8 {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Render workers failed to deliver {} consecutive frames; aborting export\",\n\t\t\t\t\tself.null_frame_streak\n\t\t\t\t));\n\t\t\t\treturn Err(Error::Failed(\"Too many null frames\".to_string()));\n\t\t\t}\n\t\t\treturn Ok(());\n\t\t}\n\t\tself.null_frame_streak = 0;\n\n\t\tif unsafe { bridge::codec::oakcodec_encoder_write_video(self.encoder, frame) } != 0 {\n\t\t\tlet err = encoder_error(self.encoder);\n\t\t\ttask.set_error(\u0026err);\n\t\t\treturn Err(Error::Failed(\"Failed to write video frame\".to_string()));\n\t\t}\n\n\t\tself.frame_time += 1;\n\t\t// Progress is reported by the render loop itself (native signalling);\n\t\t// the per-frame counter is kept for parity with the C++ field.\n\t\tOk(())\n\t}\n\n\tfn audio_downloaded(\u0026mut self, task: \u0026mut Task, buffer: CHandle) -\u003e Result\u003c()\u003e {\n\t\t// Simplified: audio writing is not wired through the sample buffer\n\t\t// in this rewrite (the encoder receives the interleaved samples from\n\t\t// the codec side directly). The hook exists for parity with the C++\n\t\t// virtual and always succeeds.\n\t\tlet _ = (task, buffer);\n\t\tOk(())\n\t}\n\n\tfn encode_subtitle(\u0026mut self, task: \u0026mut Task, text: \u0026str) -\u003e Result\u003c()\u003e {\n\t\tif !self.encoding_params.subtitles_enabled || self.subtitle_encoder.ctx.is_null() {\n\t\t\treturn Ok(());\n\t\t}\n\t\t// The simplified path does not carry the subtitle block's in/out\n\t\t// times; write with 0.0/0.0 (the encoder default interval).\n\t\tif unsafe { bridge::codec::oakcodec_encoder_write_subtitle(self.subtitle_encoder, cstr(text), 0.0, 0.0) } != 0 {\n\t\t\tlet err = encoder_error(self.subtitle_encoder);\n\t\t\ttask.set_error(\u0026err);\n\t\t\treturn Err(Error::Failed(\"Failed to write subtitle\".to_string()));\n\t\t}\n\t\tOk(())\n\t}\n}\n\n/// Two-stage read of the viewer node's label.\nfn node_label(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// Read a NUL-terminated char buffer into a String (lossy).\nfn buf_to_string(buf: \u0026[i8]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n\n/// Two-stage read of the encoder's last error string.\nfn encoder_error(encoder: CHandle) -\u003e String {\n\tlet mut buf = [0i8; 512];\n\tlet needed = unsafe { bridge::codec::oakcodec_encoder_last_error(encoder, buf.as_mut_ptr(), buf.len() as i32) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tbuf_to_string(\u0026buf)\n}\n","traces":[{"line":105,"address":[],"length":0,"stats":{"Line":8}},{"line":106,"address":[],"length":0,"stats":{"Line":24}},{"line":107,"address":[],"length":0,"stats":{"Line":16}},{"line":108,"address":[],"length":0,"stats":{"Line":32}},{"line":109,"address":[],"length":0,"stats":{"Line":64}},{"line":115,"address":[],"length":0,"stats":{"Line":16}},{"line":116,"address":[],"length":0,"stats":{"Line":8}},{"line":117,"address":[],"length":0,"stats":{"Line":8}},{"line":125,"address":[],"length":0,"stats":{"Line":8}},{"line":127,"address":[],"length":0,"stats":{"Line":16}},{"line":144,"address":[],"length":0,"stats":{"Line":8}},{"line":157,"address":[],"length":0,"stats":{"Line":8}},{"line":166,"address":[],"length":0,"stats":{"Line":24}},{"line":167,"address":[],"length":0,"stats":{"Line":32}},{"line":168,"address":[],"length":0,"stats":{"Line":32}},{"line":169,"address":[],"length":0,"stats":{"Line":8}},{"line":170,"address":[],"length":0,"stats":{"Line":8}},{"line":171,"address":[],"length":0,"stats":{"Line":8}},{"line":172,"address":[],"length":0,"stats":{"Line":8}},{"line":173,"address":[],"length":0,"stats":{"Line":8}},{"line":174,"address":[],"length":0,"stats":{"Line":8}},{"line":175,"address":[],"length":0,"stats":{"Line":8}},{"line":176,"address":[],"length":0,"stats":{"Line":8}},{"line":177,"address":[],"length":0,"stats":{"Line":8}},{"line":178,"address":[],"length":0,"stats":{"Line":8}},{"line":179,"address":[],"length":0,"stats":{"Line":8}},{"line":180,"address":[],"length":0,"stats":{"Line":8}},{"line":181,"address":[],"length":0,"stats":{"Line":8}},{"line":182,"address":[],"length":0,"stats":{"Line":8}},{"line":187,"address":[],"length":0,"stats":{"Line":6}},{"line":188,"address":[],"length":0,"stats":{"Line":12}},{"line":189,"address":[],"length":0,"stats":{"Line":12}},{"line":191,"address":[],"length":0,"stats":{"Line":18}},{"line":193,"address":[],"length":0,"stats":{"Line":30}},{"line":198,"address":[],"length":0,"stats":{"Line":8}},{"line":200,"address":[],"length":0,"stats":{"Line":32}},{"line":202,"address":[],"length":0,"stats":{"Line":24}},{"line":203,"address":[],"length":0,"stats":{"Line":24}},{"line":204,"address":[],"length":0,"stats":{"Line":16}},{"line":205,"address":[],"length":0,"stats":{"Line":3}},{"line":206,"address":[],"length":0,"stats":{"Line":1}},{"line":208,"address":[],"length":0,"stats":{"Line":7}},{"line":210,"address":[],"length":0,"stats":{"Line":7}},{"line":211,"address":[],"length":0,"stats":{"Line":3}},{"line":212,"address":[],"length":0,"stats":{"Line":3}},{"line":213,"address":[],"length":0,"stats":{"Line":1}},{"line":215,"address":[],"length":0,"stats":{"Line":6}},{"line":217,"address":[],"length":0,"stats":{"Line":18}},{"line":221,"address":[],"length":0,"stats":{"Line":12}},{"line":222,"address":[],"length":0,"stats":{"Line":12}},{"line":223,"address":[],"length":0,"stats":{"Line":12}},{"line":224,"address":[],"length":0,"stats":{"Line":12}},{"line":225,"address":[],"length":0,"stats":{"Line":6}},{"line":226,"address":[],"length":0,"stats":{"Line":6}},{"line":228,"address":[],"length":0,"stats":{"Line":6}},{"line":229,"address":[],"length":0,"stats":{"Line":12}},{"line":230,"address":[],"length":0,"stats":{"Line":6}},{"line":231,"address":[],"length":0,"stats":{"Line":6}},{"line":233,"address":[],"length":0,"stats":{"Line":6}},{"line":234,"address":[],"length":0,"stats":{"Line":6}},{"line":241,"address":[],"length":0,"stats":{"Line":24}},{"line":242,"address":[],"length":0,"stats":{"Line":30}},{"line":243,"address":[],"length":0,"stats":{"Line":12}},{"line":244,"address":[],"length":0,"stats":{"Line":9}},{"line":248,"address":[],"length":0,"stats":{"Line":3}},{"line":250,"address":[],"length":0,"stats":{"Line":9}},{"line":251,"address":[],"length":0,"stats":{"Line":3}},{"line":252,"address":[],"length":0,"stats":{"Line":3}},{"line":253,"address":[],"length":0,"stats":{"Line":1}},{"line":256,"address":[],"length":0,"stats":{"Line":2}},{"line":261,"address":[],"length":0,"stats":{"Line":45}},{"line":262,"address":[],"length":0,"stats":{"Line":90}},{"line":263,"address":[],"length":0,"stats":{"Line":8}},{"line":264,"address":[],"length":0,"stats":{"Line":8}},{"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":269,"address":[],"length":0,"stats":{"Line":1}},{"line":271,"address":[],"length":0,"stats":{"Line":7}},{"line":273,"address":[],"length":0,"stats":{"Line":37}},{"line":275,"address":[],"length":0,"stats":{"Line":74}},{"line":276,"address":[],"length":0,"stats":{"Line":3}},{"line":277,"address":[],"length":0,"stats":{"Line":3}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":281,"address":[],"length":0,"stats":{"Line":36}},{"line":284,"address":[],"length":0,"stats":{"Line":36}},{"line":287,"address":[],"length":0,"stats":{"Line":1}},{"line":292,"address":[],"length":0,"stats":{"Line":1}},{"line":293,"address":[],"length":0,"stats":{"Line":1}},{"line":296,"address":[],"length":0,"stats":{"Line":0}},{"line":297,"address":[],"length":0,"stats":{"Line":0}},{"line":298,"address":[],"length":0,"stats":{"Line":0}},{"line":302,"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":307,"address":[],"length":0,"stats":{"Line":0}},{"line":312,"address":[],"length":0,"stats":{"Line":8}},{"line":313,"address":[],"length":0,"stats":{"Line":32}},{"line":314,"address":[],"length":0,"stats":{"Line":8}},{"line":315,"address":[],"length":0,"stats":{"Line":0}},{"line":317,"address":[],"length":0,"stats":{"Line":24}},{"line":319,"address":[],"length":0,"stats":{"Line":32}},{"line":321,"address":[],"length":0,"stats":{"Line":16}},{"line":325,"address":[],"length":0,"stats":{"Line":9}},{"line":326,"address":[],"length":0,"stats":{"Line":81}},{"line":327,"address":[],"length":0,"stats":{"Line":36}},{"line":331,"address":[],"length":0,"stats":{"Line":5}},{"line":332,"address":[],"length":0,"stats":{"Line":10}},{"line":333,"address":[],"length":0,"stats":{"Line":30}},{"line":334,"address":[],"length":0,"stats":{"Line":5}},{"line":335,"address":[],"length":0,"stats":{"Line":4}},{"line":337,"address":[],"length":0,"stats":{"Line":2}}],"covered":104,"coverable":113},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi","manager.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//! `oaktask_manager_*` export symbols mirroring `include/task/manager.h`.\n//!\n//! Full symbol inventory (header-authoritative):\n//! - `oaktask_manager_init(void) -\u003e int`\n//! - `oaktask_manager_shutdown(void) -\u003e void`\n//! - `oaktask_register_codec_submitter(void) -\u003e int`\n//! - `oaktask_manager_count(void) -\u003e int`\n//! - `oaktask_manager_at(int i) -\u003e OakTaskTask`\n//! - `oaktask_manager_delete_finished(void) -\u003e void`\n\nuse std::ffi::c_int;\n\nuse crate::error::{OAKTASK_E_STATE, OAKTASK_OK};\nuse crate::ffi::taskhandle::wrap_borrowed;\nuse crate::handle::CHandle;\nuse crate::manager::TaskManager;\n\n/// `oaktask_manager_init` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_init() -\u003e c_int {\n\tif let Err(_) = TaskManager::init() {\n\t\treturn OAKTASK_E_STATE;\n\t}\n\t// Register the codec task submitter (mirrors the C++ init sequence).\n\tlet _ = crate::codecbridge::register_codec_task_submitter();\n\tTaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(true));\n\tOAKTASK_OK\n}\n\n/// `oaktask_manager_shutdown` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_shutdown() {\n\tTaskManager::with_manager_mut(|m| m.set_codec_submitter_registered(false));\n\tlet _ = crate::codecbridge::unregister_codec_task_submitter();\n\tTaskManager::shutdown();\n}\n\n/// `oaktask_register_codec_submitter` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_register_codec_submitter() -\u003e c_int {\n\tmatch crate::codecbridge::register_codec_task_submitter() {\n\t\tOk(()) =\u003e OAKTASK_OK,\n\t\tErr(_) =\u003e OAKTASK_OK,\n\t}\n}\n\n/// `oaktask_manager_count` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_count() -\u003e c_int {\n\tmatch TaskManager::with_manager(|m| m.get_task_count()) {\n\t\tSome(count) =\u003e count as c_int,\n\t\tNone =\u003e OAKTASK_E_STATE,\n\t}\n}\n\n/// `oaktask_manager_at` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_at(i: c_int) -\u003e CHandle {\n\t// No manager (or out of range) -\u003e empty handle, mirroring the C++\n\t// `OakTaskTask{}` return.\n\tlet ptr = match TaskManager::with_manager(|m| m.task_ptr_at(i as usize)) {\n\t\tSome(Ok(ptr)) =\u003e ptr,\n\t\t_ =\u003e return CHandle::null(),\n\t};\n\twrap_borrowed(ptr)\n}\n\n/// `oaktask_manager_delete_finished` (`include/task/manager.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_manager_delete_finished() {\n\t// Collect the finished entries and join their threads without holding\n\t// the manager lock (a worker's finish callback may call back into the\n\t// manager).\n\tlet entries = TaskManager::with_manager_mut(|m| m.drain_finished()).unwrap_or_default();\n\tfor (_task, handle) in entries {\n\t\tlet _ = handle.join();\n\t}\n}\n","traces":[{"line":36,"address":[],"length":0,"stats":{"Line":8}},{"line":37,"address":[],"length":0,"stats":{"Line":8}},{"line":38,"address":[],"length":0,"stats":{"Line":0}},{"line":41,"address":[],"length":0,"stats":{"Line":8}},{"line":42,"address":[],"length":0,"stats":{"Line":24}},{"line":43,"address":[],"length":0,"stats":{"Line":8}},{"line":48,"address":[],"length":0,"stats":{"Line":34}},{"line":49,"address":[],"length":0,"stats":{"Line":50}},{"line":50,"address":[],"length":0,"stats":{"Line":34}},{"line":51,"address":[],"length":0,"stats":{"Line":34}},{"line":56,"address":[],"length":0,"stats":{"Line":14}},{"line":57,"address":[],"length":0,"stats":{"Line":14}},{"line":58,"address":[],"length":0,"stats":{"Line":12}},{"line":59,"address":[],"length":0,"stats":{"Line":2}},{"line":65,"address":[],"length":0,"stats":{"Line":9}},{"line":66,"address":[],"length":0,"stats":{"Line":21}},{"line":67,"address":[],"length":0,"stats":{"Line":12}},{"line":68,"address":[],"length":0,"stats":{"Line":3}},{"line":74,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":13}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":79,"address":[],"length":0,"stats":{"Line":5}},{"line":81,"address":[],"length":0,"stats":{"Line":2}},{"line":86,"address":[],"length":0,"stats":{"Line":6}},{"line":90,"address":[],"length":0,"stats":{"Line":26}},{"line":91,"address":[],"length":0,"stats":{"Line":6}},{"line":92,"address":[],"length":0,"stats":{"Line":0}}],"covered":25,"coverable":27},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi","project.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//! `oaktask_create_*` export symbols mirroring `include/task/project.h`.\n//!\n//! Full symbol inventory (header-authoritative):\n//! - `oaktask_create_project_load(const char *filename) -\u003e OakTaskTask`\n//! - `oaktask_load_take_project(OakTaskTask t) -\u003e OakNodeProject`\n//! - `oaktask_create_project_save(OakNodeProject, const char *filename_or_NULL, int use_compression) -\u003e OakTaskTask`\n//! - `oaktask_create_project_import(OakNodeFolder, OakNodeProject, const char *const *urls, int url_count) -\u003e OakTaskTask`\n//! - `oaktask_import_take_command(OakTaskTask t) -\u003e OakUndoCommand`\n//! - `oaktask_import_footage_count(OakTaskTask t) -\u003e int`\n//! - `oaktask_import_footage_at(OakTaskTask t, int index) -\u003e OakNodeFootage`\n//! - `oaktask_import_invalid_count(OakTaskTask t) -\u003e int`\n//! - `oaktask_import_invalid_at(OakTaskTask t, int index, char *buf, int buf_size) -\u003e int` (two-stage)\n//! - `oaktask_create_project_load_otio(const char *filename) -\u003e OakTaskTask`\n//! - `oaktask_load_otio_take_project(OakTaskTask t) -\u003e OakNodeProject`\n//! - `oaktask_create_project_save_otio(OakNodeProject, const char *filename) -\u003e OakTaskTask`\n//! - `oaktask_load_otio_set_confirm_cb(oaktask_otio_import_confirm_fn fn, void *userdata) -\u003e void`\n//! - `oaktask_create_precache(OakNodeFootage, int index, OakNodeSequence) -\u003e OakTaskTask`\n//! - `oaktask_create_export(OakNodeNode viewer, OakNodeColorManager, const oakcodec_encoding_params *) -\u003e OakTaskTask`\n//! - `oaktask_import_set_image_sequence_confirm_cb(oaktask_image_sequence_confirm_fn fn, void *userdata) -\u003e void`\n\nuse std::ffi::{c_char, c_int, c_void};\nuse std::sync::Mutex;\n\nuse crate::bridge::codec::OakCodecEncodingParams;\nuse crate::error::{OAKTASK_E_INVALID, OAKTASK_E_NOT_FOUND};\nuse crate::export::{EncodingParams, ExportTask};\nuse crate::ffi::taskhandle::{\n\tcopy_string, cstr, cstr_to_string, get_task, wrap_owned, wrap_owned_with_impl, TaskImpl,\n};\nuse crate::handle::CHandle;\nuse crate::precache::PreCacheTask;\nuse crate::project::import::{import_file_count, import_title, ImageSequenceConfirmFn, ProjectImportTask};\nuse crate::project::load::{ProjectLoadBaseTask, ProjectLoadTask};\nuse crate::project::loadotio::{set_import_confirm_callback, LoadOTIOTask};\nuse crate::project::save::{project_filename, ProjectSaveTask};\nuse crate::project::saveotio::SaveOTIOTask;\nuse crate::task::Task;\n\n/// `oaktask_otio_import_confirm_fn` (`include/task/project.h`).\npub type OakTaskOtioImportConfirmFn =\n\tunsafe extern \"C\" fn(sequence_names: *const *const c_char, count: c_int, userdata: *mut c_void) -\u003e c_int;\n\n/// `oaktask_image_sequence_confirm_fn` (`include/task/project.h`).\npub type OakTaskImageSequenceConfirmFn =\n\tunsafe extern \"C\" fn(filename: *const c_char, userdata: *mut c_void) -\u003e c_int;\n\n/// Global image-sequence confirm callback (facade concern; `null` clears\n/// it). Snapshot into each import task at creation.\nstatic IMAGE_SEQUENCE_CONFIRM: Mutex\u003cOption\u003cImageSequenceConfirmFn\u003e\u003e = Mutex::new(None);\n\n/// `oaktask_create_project_load` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_load(filename: *const c_char) -\u003e CHandle {\n\tif filename.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet filename = unsafe { cstr_to_string(filename) };\n\tlet title = format!(\"Loading '{filename}'\");\n\n\tlet inner = Task::new(\u0026title, CHandle::null());\n\tlet base = ProjectLoadBaseTask::new(inner, filename.clone());\n\tlet load_task = ProjectLoadTask { base };\n\tlet atom = load_task.base.base.get_cancel_atom();\n\tlet mut boxed = Box::new(load_task);\n\tlet base_ptr = \u0026mut boxed.base as *mut ProjectLoadBaseTask;\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr))\n}\n\n/// `oaktask_load_take_project` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_load_take_project(t: CHandle) -\u003e CHandle {\n\tload_take_project(\u0026t)\n}\n\n/// Shared implementation of `load_take_project`/`load_otio_take_project`.\nfn load_take_project(t: \u0026CHandle) -\u003e CHandle {\n\tlet Some(h) = get_task(t) else {\n\t\treturn CHandle::null();\n\t};\n\tlet TaskImpl::LoadBase(base_ptr) = h.impl_kind else {\n\t\treturn CHandle::null();\n\t};\n\tmatch unsafe { (\u0026mut *base_ptr).take_project() } {\n\t\tOk(project) =\u003e project,\n\t\tErr(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// `oaktask_create_project_save` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_save(\n\tproject: CHandle,\n\tfilename_or_null: *const c_char,\n\tuse_compression: c_int,\n) -\u003e CHandle {\n\tif project.ctx.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet title = format!(\"Saving '{}'\", project_filename(project));\n\tlet mut save_task = ProjectSaveTask {\n\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\tproject,\n\t\toverride_filename: None,\n\t\tuse_compression: use_compression != 0,\n\t};\n\tif !filename_or_null.is_null() {\n\t\tsave_task.set_override_filename(\u0026unsafe { cstr_to_string(filename_or_null) });\n\t}\n\tlet atom = save_task.base.get_cancel_atom();\n\tlet boxed = Box::new(save_task);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_create_project_import` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_import(\n\tfolder: CHandle,\n\tproject: CHandle,\n\turls: *const *const c_char,\n\turl_count: c_int,\n) -\u003e CHandle {\n\tif folder.ctx.is_null() || project.ctx.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tif url_count \u003c 0 || (urls.is_null() \u0026\u0026 url_count \u003e 0) {\n\t\treturn CHandle::null();\n\t}\n\tlet mut filenames = Vec::with_capacity(url_count as usize);\n\tfor i in 0..url_count {\n\t\tlet url = unsafe { *urls.add(i as usize) };\n\t\tif url.is_null() {\n\t\t\treturn CHandle::null();\n\t\t}\n\t\tfilenames.push(unsafe { cstr_to_string(url) });\n\t}\n\n\tlet title = import_title(\u0026filenames);\n\tlet file_count = import_file_count(\u0026filenames);\n\tlet import_task = ProjectImportTask::new(\n\t\tTask::new(\u0026title, CHandle::null()),\n\t\tfolder,\n\t\tproject,\n\t\tfilenames,\n\t\tIMAGE_SEQUENCE_CONFIRM.lock().unwrap().take(),\n\t\tfile_count,\n\t);\n\tlet atom = import_task.base.get_cancel_atom();\n\tlet mut boxed = Box::new(import_task);\n\tlet import_ptr = \u0026mut *boxed as *mut ProjectImportTask;\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned_with_impl(Box::new(outer), TaskImpl::Import(import_ptr))\n}\n\n/// Borrow the `ProjectImportTask` behind the handle (the C++ `import_impl`\n/// dynamic_cast equivalent).\nfn import_impl(t: \u0026CHandle) -\u003e Option\u003c*mut ProjectImportTask\u003e {\n\tlet h = get_task(t)?;\n\tmatch h.impl_kind {\n\t\tTaskImpl::Import(p) =\u003e Some(p),\n\t\t_ =\u003e None,\n\t}\n}\n\n/// `oaktask_import_take_command` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_take_command(t: CHandle) -\u003e CHandle {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn CHandle::null();\n\t};\n\tmatch unsafe { (\u0026mut *p).take_command() } {\n\t\tOk(command) =\u003e command,\n\t\tErr(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// `oaktask_import_footage_count` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_footage_count(t: CHandle) -\u003e c_int {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn OAKTASK_E_INVALID;\n\t};\n\tunsafe { (*p).get_file_count() as c_int }\n}\n\n/// `oaktask_import_footage_at` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_footage_at(t: CHandle, index: c_int) -\u003e CHandle {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn CHandle::null();\n\t};\n\tif index \u003c 0 {\n\t\treturn CHandle::null();\n\t}\n\tmatch unsafe { (*p).get_imported_footage(index as usize) } {\n\t\tOk(footage) =\u003e footage,\n\t\tErr(_) =\u003e CHandle::null(),\n\t}\n}\n\n/// `oaktask_import_invalid_count` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_invalid_count(t: CHandle) -\u003e c_int {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn OAKTASK_E_INVALID;\n\t};\n\tunsafe { (*p).get_invalid_file_count() as c_int }\n}\n\n/// `oaktask_import_invalid_at` (`include/task/project.h`, two-stage string getter).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_invalid_at(\n\tt: CHandle,\n\tindex: c_int,\n\tbuf: *mut c_char,\n\tbuf_size: c_int,\n) -\u003e c_int {\n\tlet Some(p) = import_impl(\u0026t) else {\n\t\treturn OAKTASK_E_INVALID;\n\t};\n\tlet task = unsafe { \u0026*p };\n\tlet count = task.get_invalid_file_count();\n\tif index \u003c 0 || index as usize \u003e= count {\n\t\treturn OAKTASK_E_NOT_FOUND;\n\t}\n\tcopy_string(task.invalid_file_at(index as usize), buf, buf_size)\n}\n\n/// `oaktask_create_project_load_otio` (`include/task/project.h`). The\n/// interchange format is inferred from the filename extension (`.otio` /\n/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C\n/// ABI needs no format parameter.\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_load_otio(filename: *const c_char) -\u003e CHandle {\n\tif filename.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet filename = unsafe { cstr_to_string(filename) };\n\tlet title = format!(\"Loading '{filename}'\");\n\n\tlet inner = Task::new(\u0026title, CHandle::null());\n\tlet base = ProjectLoadBaseTask::new(inner, filename.clone());\n\tlet otio_task = LoadOTIOTask::new(base);\n\tlet atom = otio_task.base.base.get_cancel_atom();\n\tlet mut boxed = Box::new(otio_task);\n\tlet base_ptr = \u0026mut boxed.base as *mut ProjectLoadBaseTask;\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned_with_impl(Box::new(outer), TaskImpl::LoadBase(base_ptr))\n}\n\n/// `oaktask_load_otio_take_project` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_load_otio_take_project(t: CHandle) -\u003e CHandle {\n\tload_take_project(\u0026t)\n}\n\n/// `oaktask_create_project_save_otio` (`include/task/project.h`). The\n/// interchange format is inferred from the filename extension (`.otio` /\n/// `.fcpxml`, case-insensitive; see `crate::project::format`), so the C\n/// ABI needs no format parameter.\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_project_save_otio(project: CHandle, filename: *const c_char) -\u003e CHandle {\n\tif project.ctx.is_null() || filename.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet filename = unsafe { cstr_to_string(filename) };\n\tlet title = format!(\"Saving '{filename}'\");\n\tlet task = SaveOTIOTask {\n\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\tproject,\n\t\tfilename,\n\t};\n\tlet atom = task.base.get_cancel_atom();\n\tlet boxed = Box::new(task);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_load_otio_set_confirm_cb` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_load_otio_set_confirm_cb(cb: Option\u003cOakTaskOtioImportConfirmFn\u003e, userdata: *mut c_void) {\n\tlet Some(cb) = cb else {\n\t\tset_import_confirm_callback(None);\n\t\treturn;\n\t};\n\tlet userdata = userdata as usize;\n\tset_import_confirm_callback(Some(Box::new(move |sequence_names: \u0026[String]| {\n\t\tlet ptrs: Vec\u003c*const c_char\u003e = sequence_names.iter().map(|n| cstr(n)).collect();\n\t\tunsafe { cb(ptrs.as_ptr(), ptrs.len() as c_int, userdata as *mut c_void) != 0 }\n\t})));\n}\n\n/// `oaktask_create_precache` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_precache(footage: CHandle, index: c_int, sequence: CHandle) -\u003e CHandle {\n\tif footage.ctx.is_null() || sequence.ctx.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet precache = PreCacheTask::new(footage, index, sequence);\n\tlet title = precache.render.base.title().to_string();\n\tlet atom = precache.render.base.get_cancel_atom();\n\tlet boxed = Box::new(precache);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_create_export` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_create_export(\n\tviewer: CHandle,\n\tcolor_manager: CHandle,\n\tparams: *const OakCodecEncodingParams,\n) -\u003e CHandle {\n\tif viewer.ctx.is_null() || params.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tlet encoding = unsafe { convert_encoding_params(\u0026*params) };\n\tlet export = ExportTask::new(viewer, color_manager, encoding);\n\tlet title = export.render.base.title().to_string();\n\tlet atom = export.render.base.get_cancel_atom();\n\tlet boxed = Box::new(export);\n\tlet mut outer = Task::new(\u0026title, atom);\n\touter.set_behavior(boxed);\n\twrap_owned(Box::new(outer))\n}\n\n/// `oaktask_import_set_image_sequence_confirm_cb` (`include/task/project.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_import_set_image_sequence_confirm_cb(\n\tcb: Option\u003cOakTaskImageSequenceConfirmFn\u003e,\n\tuserdata: *mut c_void,\n) {\n\tlet mut guard = IMAGE_SEQUENCE_CONFIRM.lock().unwrap();\n\tlet Some(cb) = cb else {\n\t\t*guard = None;\n\t\treturn;\n\t};\n\tlet userdata = userdata as usize;\n\t*guard = Some(Box::new(move |filename: \u0026str, _other: \u0026str| unsafe {\n\t\tcb(cstr(filename), userdata as *mut c_void) != 0\n\t}));\n}\n\n/// Copy the C-ABI encoding params into the Rust mirror (subset read by the\n/// export task).\nunsafe fn convert_encoding_params(p: \u0026OakCodecEncodingParams) -\u003e EncodingParams {\n\tEncodingParams {\n\t\tfilename: unsafe { c_char_array_to_string(\u0026p.filename) },\n\t\tformat: p.format,\n\t\tvideo_enabled: p.video_enabled != 0,\n\t\tvideo_codec: p.video_codec,\n\t\tvideo_width: p.video_width,\n\t\tvideo_height: p.video_height,\n\t\tvideo_time_base_num: p.video_time_base_num,\n\t\tvideo_time_base_den: p.video_time_base_den,\n\t\tvideo_pixel_format: p.video_pixel_format,\n\t\taudio_enabled: p.audio_enabled != 0,\n\t\taudio_codec: p.audio_codec,\n\t\tsubtitles_enabled: p.subtitles_enabled != 0,\n\t\texport_length_num: p.export_length_num,\n\t\texport_length_den: p.export_length_den,\n\t}\n}\n\n/// Read a NUL-terminated char array into a String (lossy).\nunsafe fn c_char_array_to_string(buf: \u0026[c_char]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tlet bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };\n\tString::from_utf8_lossy(bytes).into_owned()\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":1}},{"line":73,"address":[],"length":0,"stats":{"Line":6}},{"line":74,"address":[],"length":0,"stats":{"Line":4}},{"line":76,"address":[],"length":0,"stats":{"Line":8}},{"line":77,"address":[],"length":0,"stats":{"Line":10}},{"line":78,"address":[],"length":0,"stats":{"Line":4}},{"line":79,"address":[],"length":0,"stats":{"Line":6}},{"line":80,"address":[],"length":0,"stats":{"Line":6}},{"line":81,"address":[],"length":0,"stats":{"Line":4}},{"line":82,"address":[],"length":0,"stats":{"Line":8}},{"line":83,"address":[],"length":0,"stats":{"Line":6}},{"line":84,"address":[],"length":0,"stats":{"Line":8}},{"line":89,"address":[],"length":0,"stats":{"Line":3}},{"line":90,"address":[],"length":0,"stats":{"Line":6}},{"line":94,"address":[],"length":0,"stats":{"Line":13}},{"line":95,"address":[],"length":0,"stats":{"Line":26}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":26}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":101,"address":[],"length":0,"stats":{"Line":13}},{"line":102,"address":[],"length":0,"stats":{"Line":14}},{"line":103,"address":[],"length":0,"stats":{"Line":6}},{"line":109,"address":[],"length":0,"stats":{"Line":17}},{"line":114,"address":[],"length":0,"stats":{"Line":34}},{"line":115,"address":[],"length":0,"stats":{"Line":2}},{"line":117,"address":[],"length":0,"stats":{"Line":60}},{"line":119,"address":[],"length":0,"stats":{"Line":60}},{"line":122,"address":[],"length":0,"stats":{"Line":15}},{"line":124,"address":[],"length":0,"stats":{"Line":18}},{"line":125,"address":[],"length":0,"stats":{"Line":9}},{"line":127,"address":[],"length":0,"stats":{"Line":45}},{"line":128,"address":[],"length":0,"stats":{"Line":45}},{"line":129,"address":[],"length":0,"stats":{"Line":60}},{"line":130,"address":[],"length":0,"stats":{"Line":45}},{"line":131,"address":[],"length":0,"stats":{"Line":45}},{"line":136,"address":[],"length":0,"stats":{"Line":6}},{"line":142,"address":[],"length":0,"stats":{"Line":22}},{"line":143,"address":[],"length":0,"stats":{"Line":2}},{"line":145,"address":[],"length":0,"stats":{"Line":12}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":148,"address":[],"length":0,"stats":{"Line":12}},{"line":149,"address":[],"length":0,"stats":{"Line":10}},{"line":150,"address":[],"length":0,"stats":{"Line":18}},{"line":151,"address":[],"length":0,"stats":{"Line":12}},{"line":152,"address":[],"length":0,"stats":{"Line":1}},{"line":154,"address":[],"length":0,"stats":{"Line":20}},{"line":157,"address":[],"length":0,"stats":{"Line":9}},{"line":158,"address":[],"length":0,"stats":{"Line":9}},{"line":160,"address":[],"length":0,"stats":{"Line":9}},{"line":161,"address":[],"length":0,"stats":{"Line":3}},{"line":162,"address":[],"length":0,"stats":{"Line":3}},{"line":163,"address":[],"length":0,"stats":{"Line":3}},{"line":164,"address":[],"length":0,"stats":{"Line":6}},{"line":165,"address":[],"length":0,"stats":{"Line":3}},{"line":167,"address":[],"length":0,"stats":{"Line":9}},{"line":168,"address":[],"length":0,"stats":{"Line":9}},{"line":169,"address":[],"length":0,"stats":{"Line":6}},{"line":170,"address":[],"length":0,"stats":{"Line":12}},{"line":171,"address":[],"length":0,"stats":{"Line":9}},{"line":172,"address":[],"length":0,"stats":{"Line":12}},{"line":177,"address":[],"length":0,"stats":{"Line":16}},{"line":178,"address":[],"length":0,"stats":{"Line":48}},{"line":179,"address":[],"length":0,"stats":{"Line":16}},{"line":180,"address":[],"length":0,"stats":{"Line":28}},{"line":181,"address":[],"length":0,"stats":{"Line":2}},{"line":187,"address":[],"length":0,"stats":{"Line":3}},{"line":188,"address":[],"length":0,"stats":{"Line":5}},{"line":189,"address":[],"length":0,"stats":{"Line":1}},{"line":191,"address":[],"length":0,"stats":{"Line":2}},{"line":192,"address":[],"length":0,"stats":{"Line":2}},{"line":193,"address":[],"length":0,"stats":{"Line":1}},{"line":199,"address":[],"length":0,"stats":{"Line":4}},{"line":200,"address":[],"length":0,"stats":{"Line":7}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":203,"address":[],"length":0,"stats":{"Line":3}},{"line":208,"address":[],"length":0,"stats":{"Line":3}},{"line":209,"address":[],"length":0,"stats":{"Line":6}},{"line":210,"address":[],"length":0,"stats":{"Line":0}},{"line":212,"address":[],"length":0,"stats":{"Line":3}},{"line":213,"address":[],"length":0,"stats":{"Line":1}},{"line":215,"address":[],"length":0,"stats":{"Line":4}},{"line":216,"address":[],"length":0,"stats":{"Line":2}},{"line":217,"address":[],"length":0,"stats":{"Line":1}},{"line":223,"address":[],"length":0,"stats":{"Line":3}},{"line":224,"address":[],"length":0,"stats":{"Line":6}},{"line":225,"address":[],"length":0,"stats":{"Line":0}},{"line":227,"address":[],"length":0,"stats":{"Line":3}},{"line":232,"address":[],"length":0,"stats":{"Line":3}},{"line":238,"address":[],"length":0,"stats":{"Line":6}},{"line":239,"address":[],"length":0,"stats":{"Line":0}},{"line":241,"address":[],"length":0,"stats":{"Line":6}},{"line":242,"address":[],"length":0,"stats":{"Line":9}},{"line":243,"address":[],"length":0,"stats":{"Line":6}},{"line":244,"address":[],"length":0,"stats":{"Line":1}},{"line":246,"address":[],"length":0,"stats":{"Line":10}},{"line":251,"address":[],"length":0,"stats":{"Line":11}},{"line":252,"address":[],"length":0,"stats":{"Line":22}},{"line":253,"address":[],"length":0,"stats":{"Line":0}},{"line":255,"address":[],"length":0,"stats":{"Line":33}},{"line":256,"address":[],"length":0,"stats":{"Line":22}},{"line":258,"address":[],"length":0,"stats":{"Line":44}},{"line":259,"address":[],"length":0,"stats":{"Line":55}},{"line":260,"address":[],"length":0,"stats":{"Line":33}},{"line":261,"address":[],"length":0,"stats":{"Line":33}},{"line":262,"address":[],"length":0,"stats":{"Line":33}},{"line":263,"address":[],"length":0,"stats":{"Line":22}},{"line":264,"address":[],"length":0,"stats":{"Line":44}},{"line":265,"address":[],"length":0,"stats":{"Line":33}},{"line":266,"address":[],"length":0,"stats":{"Line":44}},{"line":271,"address":[],"length":0,"stats":{"Line":10}},{"line":272,"address":[],"length":0,"stats":{"Line":20}},{"line":277,"address":[],"length":0,"stats":{"Line":12}},{"line":278,"address":[],"length":0,"stats":{"Line":46}},{"line":279,"address":[],"length":0,"stats":{"Line":2}},{"line":281,"address":[],"length":0,"stats":{"Line":30}},{"line":282,"address":[],"length":0,"stats":{"Line":20}},{"line":284,"address":[],"length":0,"stats":{"Line":40}},{"line":288,"address":[],"length":0,"stats":{"Line":30}},{"line":289,"address":[],"length":0,"stats":{"Line":30}},{"line":290,"address":[],"length":0,"stats":{"Line":40}},{"line":291,"address":[],"length":0,"stats":{"Line":30}},{"line":292,"address":[],"length":0,"stats":{"Line":30}},{"line":297,"address":[],"length":0,"stats":{"Line":7}},{"line":298,"address":[],"length":0,"stats":{"Line":11}},{"line":299,"address":[],"length":0,"stats":{"Line":6}},{"line":300,"address":[],"length":0,"stats":{"Line":3}},{"line":302,"address":[],"length":0,"stats":{"Line":8}},{"line":303,"address":[],"length":0,"stats":{"Line":10}},{"line":304,"address":[],"length":0,"stats":{"Line":16}},{"line":305,"address":[],"length":0,"stats":{"Line":8}},{"line":311,"address":[],"length":0,"stats":{"Line":3}},{"line":312,"address":[],"length":0,"stats":{"Line":10}},{"line":313,"address":[],"length":0,"stats":{"Line":2}},{"line":315,"address":[],"length":0,"stats":{"Line":5}},{"line":316,"address":[],"length":0,"stats":{"Line":3}},{"line":317,"address":[],"length":0,"stats":{"Line":3}},{"line":318,"address":[],"length":0,"stats":{"Line":3}},{"line":319,"address":[],"length":0,"stats":{"Line":4}},{"line":320,"address":[],"length":0,"stats":{"Line":3}},{"line":321,"address":[],"length":0,"stats":{"Line":3}},{"line":326,"address":[],"length":0,"stats":{"Line":10}},{"line":331,"address":[],"length":0,"stats":{"Line":38}},{"line":332,"address":[],"length":0,"stats":{"Line":2}},{"line":334,"address":[],"length":0,"stats":{"Line":24}},{"line":335,"address":[],"length":0,"stats":{"Line":40}},{"line":336,"address":[],"length":0,"stats":{"Line":24}},{"line":337,"address":[],"length":0,"stats":{"Line":24}},{"line":338,"address":[],"length":0,"stats":{"Line":24}},{"line":339,"address":[],"length":0,"stats":{"Line":32}},{"line":340,"address":[],"length":0,"stats":{"Line":24}},{"line":341,"address":[],"length":0,"stats":{"Line":24}},{"line":346,"address":[],"length":0,"stats":{"Line":2}},{"line":350,"address":[],"length":0,"stats":{"Line":8}},{"line":351,"address":[],"length":0,"stats":{"Line":3}},{"line":352,"address":[],"length":0,"stats":{"Line":2}},{"line":353,"address":[],"length":0,"stats":{"Line":1}},{"line":355,"address":[],"length":0,"stats":{"Line":2}},{"line":356,"address":[],"length":0,"stats":{"Line":2}},{"line":357,"address":[],"length":0,"stats":{"Line":3}},{"line":363,"address":[],"length":0,"stats":{"Line":8}},{"line":365,"address":[],"length":0,"stats":{"Line":24}},{"line":366,"address":[],"length":0,"stats":{"Line":16}},{"line":367,"address":[],"length":0,"stats":{"Line":16}},{"line":368,"address":[],"length":0,"stats":{"Line":16}},{"line":369,"address":[],"length":0,"stats":{"Line":16}},{"line":370,"address":[],"length":0,"stats":{"Line":16}},{"line":371,"address":[],"length":0,"stats":{"Line":16}},{"line":372,"address":[],"length":0,"stats":{"Line":16}},{"line":373,"address":[],"length":0,"stats":{"Line":16}},{"line":374,"address":[],"length":0,"stats":{"Line":16}},{"line":375,"address":[],"length":0,"stats":{"Line":16}},{"line":376,"address":[],"length":0,"stats":{"Line":16}},{"line":377,"address":[],"length":0,"stats":{"Line":8}},{"line":378,"address":[],"length":0,"stats":{"Line":8}},{"line":383,"address":[],"length":0,"stats":{"Line":8}},{"line":384,"address":[],"length":0,"stats":{"Line":366}},{"line":385,"address":[],"length":0,"stats":{"Line":32}},{"line":386,"address":[],"length":0,"stats":{"Line":24}}],"covered":173,"coverable":180},{"path":["/","Users","sunyu","Projects","oak","src","task","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//! `oaktask_task_*` export symbols mirroring `include/task/task.h`.\n//!\n//! Full symbol inventory (header-authoritative):\n//! - `oaktask_task_free(OakTaskTask *t) -\u003e void`\n//! - `oaktask_task_start_sync(OakTaskTask t) -\u003e int` (1 = succeeded)\n//! - `oaktask_task_start(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_cancel(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_wait(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_is_finished(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_succeeded(OakTaskTask t) -\u003e int`\n//! - `oaktask_task_title(OakTaskTask t, char *buf, int buf_size) -\u003e int` (two-stage)\n//! - `oaktask_task_error(OakTaskTask t, char *buf, int buf_size) -\u003e int` (two-stage)\n//! - `oaktask_task_subscribe(OakTaskTask t, oaktask_event_fn fn, void *userdata) -\u003e int64_t`\n//! - `oaktask_debug_alive_count(void) -\u003e int`\n\nuse std::ffi::{c_char, c_int, c_void};\nuse std::sync::Arc;\n\nuse crate::error::OAKTASK_E_INVALID;\nuse crate::ffi::taskhandle::{copy_string, get_task, get_task_mut, userdata_usize};\nuse crate::handle::CHandle;\nuse crate::manager::TaskManager;\nuse crate::task::{SubscriberState, TaskEvent};\n\n/// `oaktask_event_fn` callback (`include/task/task.h`).\npub type OakTaskEventFn = unsafe extern \"C\" fn(event_id: c_int, value: f64, userdata: *mut c_void);\n\n/// Event ids (`include/task/task.h`).\nconst OAKTASK_EVENT_STARTED: c_int = 0;\nconst OAKTASK_EVENT_PROGRESS: c_int = 1;\nconst OAKTASK_EVENT_FINISHED: c_int = 2;\n\n/// `oaktask_task_free` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_free(t: *mut CHandle) {\n\tif t.is_null() {\n\t\treturn;\n\t}\n\tlet handle = unsafe { \u0026mut *t };\n\tif handle.ctx.is_null() {\n\t\treturn;\n\t}\n\t// Release one reference, then clear ctx (NULL / empty-handle no-op).\n\tif let Some(release) = handle.release {\n\t\tunsafe {\n\t\t\trelease(handle.ctx);\n\t\t}\n\t}\n\thandle.ctx = std::ptr::null_mut();\n}\n\n/// `oaktask_task_start_sync` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_start_sync(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tlet result = unsafe { (\u0026mut *h.task).start() };\n\tif result.is_ok() {\n\t\t1\n\t} else {\n\t\t0\n\t}\n}\n\n/// `oaktask_task_start` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_start(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif !TaskManager::instance().is_some() {\n\t\treturn crate::error::OAKTASK_E_STATE;\n\t}\n\tif h.running_on_manager {\n\t\treturn crate::error::OAKTASK_E_STATE;\n\t}\n\tlet h = match get_task_mut(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif h.owner.is_none() {\n\t\treturn crate::error::OAKTASK_E_STATE;\n\t}\n\n\t// Transfer ownership to the manager; releasing the handle afterwards\n\t// only frees the box. The manager was checked above and cannot vanish\n\t// between the check and the transfer in a single-threaded caller.\n\tlet boxed = h.owner.take().unwrap();\n\tlet task_ptr = h.task;\n\tTaskManager::with_manager_mut(|m| m.add_task(boxed));\n\t// The box moved into the manager; the raw pointer is unchanged.\n\tlet h = get_task_mut(\u0026t).unwrap();\n\th.running_on_manager = true;\n\th.owned = false;\n\tlet _ = task_ptr;\n\tcrate::error::OAKTASK_OK\n}\n\n/// `oaktask_task_cancel` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_cancel(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tunsafe {\n\t\t(\u0026mut *h.task).cancel();\n\t}\n\tcrate::error::OAKTASK_OK\n}\n\n/// `oaktask_task_wait` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_wait(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif h.running_on_manager {\n\t\tlet task_ptr = h.task;\n\t\t// C++ semantics: wait cancels the task and blocks until it\n\t\t// finishes. The join happens without holding the manager lock.\n\t\tTaskManager::with_manager_mut(|m| m.cancel_task_by_ptr(task_ptr));\n\t\tlet handle = TaskManager::with_manager_mut(|m| m.take_thread_by_ptr(task_ptr)).flatten();\n\t\tif let Some(handle) = handle {\n\t\t\tlet _ = handle.join();\n\t\t}\n\t}\n\tcrate::error::OAKTASK_OK\n}\n\n/// `oaktask_task_is_finished` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_is_finished(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif unsafe { (*h.task).is_finished() } {\n\t\t1\n\t} else {\n\t\t0\n\t}\n}\n\n/// `oaktask_task_succeeded` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_succeeded(t: CHandle) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tif unsafe { (*h.task).succeeded() } {\n\t\t1\n\t} else {\n\t\t0\n\t}\n}\n\n/// `oaktask_task_title` (`include/task/task.h`, two-stage string getter).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_title(t: CHandle, buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tcopy_string(unsafe { (*h.task).title() }, buf, buf_size)\n}\n\n/// `oaktask_task_error` (`include/task/task.h`, two-stage string getter).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_error(t: CHandle, buf: *mut c_char, buf_size: c_int) -\u003e c_int {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID,\n\t};\n\tlet value = unsafe { (*h.task).error() }.unwrap_or(\"Unknown error\");\n\tcopy_string(value, buf, buf_size)\n}\n\n/// `oaktask_task_subscribe` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_task_subscribe(t: CHandle, cb: Option\u003cOakTaskEventFn\u003e, userdata: *mut c_void) -\u003e i64 {\n\tlet h = match get_task(\u0026t) {\n\t\tSome(h) =\u003e h,\n\t\tNone =\u003e return OAKTASK_E_INVALID as i64,\n\t};\n\tlet Some(cb) = cb else {\n\t\treturn OAKTASK_E_INVALID as i64;\n\t};\n\n\tlet state = Arc::new(SubscriberState::default());\n\tlet ud = userdata_usize(userdata);\n\n\t// One subscription replaces the previous one (the C++ `listeners_`\n\t// vector holds at most the factory listener in practice; here the\n\t// factory listener is gone — the task tracks finished internally).\n\tunsafe {\n\t\t(*h.task).set_subscriber(state.clone());\n\t\t(*h.task).set_event_listener(Box::new(move |ev: TaskEvent| {\n\t\t\tlet (event_id, value) = match ev {\n\t\t\t\tTaskEvent::Started =\u003e (OAKTASK_EVENT_STARTED, state.start_ms.load(std::sync::atomic::Ordering::SeqCst) as f64),\n\t\t\t\tTaskEvent::Progress(p) =\u003e (OAKTASK_EVENT_PROGRESS, p),\n\t\t\t\tTaskEvent::Finished =\u003e (OAKTASK_EVENT_FINISHED, state.finished_value.load(std::sync::atomic::Ordering::SeqCst) as f64),\n\t\t\t};\n\t\t\tcb(event_id, value, ud as *mut c_void);\n\t\t}));\n\t}\n\t0\n}\n\n/// `oaktask_debug_alive_count` (`include/task/task.h`).\n#[no_mangle]\npub unsafe extern \"C\" fn oaktask_debug_alive_count() -\u003e c_int {\n\tcrate::ffi::taskhandle::alive_count()\n}\n","traces":[{"line":51,"address":[],"length":0,"stats":{"Line":53}},{"line":52,"address":[],"length":0,"stats":{"Line":106}},{"line":53,"address":[],"length":0,"stats":{"Line":1}},{"line":55,"address":[],"length":0,"stats":{"Line":104}},{"line":56,"address":[],"length":0,"stats":{"Line":104}},{"line":57,"address":[],"length":0,"stats":{"Line":1}},{"line":60,"address":[],"length":0,"stats":{"Line":102}},{"line":62,"address":[],"length":0,"stats":{"Line":51}},{"line":65,"address":[],"length":0,"stats":{"Line":51}},{"line":70,"address":[],"length":0,"stats":{"Line":40}},{"line":71,"address":[],"length":0,"stats":{"Line":79}},{"line":72,"address":[],"length":0,"stats":{"Line":78}},{"line":73,"address":[],"length":0,"stats":{"Line":1}},{"line":75,"address":[],"length":0,"stats":{"Line":117}},{"line":76,"address":[],"length":0,"stats":{"Line":78}},{"line":77,"address":[],"length":0,"stats":{"Line":25}},{"line":79,"address":[],"length":0,"stats":{"Line":14}},{"line":85,"address":[],"length":0,"stats":{"Line":7}},{"line":86,"address":[],"length":0,"stats":{"Line":14}},{"line":87,"address":[],"length":0,"stats":{"Line":14}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":90,"address":[],"length":0,"stats":{"Line":7}},{"line":91,"address":[],"length":0,"stats":{"Line":1}},{"line":93,"address":[],"length":0,"stats":{"Line":6}},{"line":94,"address":[],"length":0,"stats":{"Line":1}},{"line":96,"address":[],"length":0,"stats":{"Line":10}},{"line":97,"address":[],"length":0,"stats":{"Line":10}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":10}},{"line":101,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":20}},{"line":108,"address":[],"length":0,"stats":{"Line":10}},{"line":109,"address":[],"length":0,"stats":{"Line":20}},{"line":111,"address":[],"length":0,"stats":{"Line":20}},{"line":112,"address":[],"length":0,"stats":{"Line":5}},{"line":113,"address":[],"length":0,"stats":{"Line":5}},{"line":114,"address":[],"length":0,"stats":{"Line":5}},{"line":115,"address":[],"length":0,"stats":{"Line":5}},{"line":120,"address":[],"length":0,"stats":{"Line":2}},{"line":121,"address":[],"length":0,"stats":{"Line":3}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":123,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":1}},{"line":128,"address":[],"length":0,"stats":{"Line":1}},{"line":133,"address":[],"length":0,"stats":{"Line":6}},{"line":134,"address":[],"length":0,"stats":{"Line":11}},{"line":135,"address":[],"length":0,"stats":{"Line":10}},{"line":136,"address":[],"length":0,"stats":{"Line":1}},{"line":138,"address":[],"length":0,"stats":{"Line":5}},{"line":139,"address":[],"length":0,"stats":{"Line":10}},{"line":142,"address":[],"length":0,"stats":{"Line":20}},{"line":143,"address":[],"length":0,"stats":{"Line":30}},{"line":144,"address":[],"length":0,"stats":{"Line":15}},{"line":145,"address":[],"length":0,"stats":{"Line":5}},{"line":148,"address":[],"length":0,"stats":{"Line":5}},{"line":153,"address":[],"length":0,"stats":{"Line":11}},{"line":154,"address":[],"length":0,"stats":{"Line":21}},{"line":155,"address":[],"length":0,"stats":{"Line":20}},{"line":156,"address":[],"length":0,"stats":{"Line":1}},{"line":158,"address":[],"length":0,"stats":{"Line":20}},{"line":159,"address":[],"length":0,"stats":{"Line":9}},{"line":161,"address":[],"length":0,"stats":{"Line":1}},{"line":167,"address":[],"length":0,"stats":{"Line":3}},{"line":168,"address":[],"length":0,"stats":{"Line":5}},{"line":169,"address":[],"length":0,"stats":{"Line":4}},{"line":170,"address":[],"length":0,"stats":{"Line":1}},{"line":172,"address":[],"length":0,"stats":{"Line":4}},{"line":173,"address":[],"length":0,"stats":{"Line":1}},{"line":175,"address":[],"length":0,"stats":{"Line":1}},{"line":181,"address":[],"length":0,"stats":{"Line":8}},{"line":182,"address":[],"length":0,"stats":{"Line":15}},{"line":183,"address":[],"length":0,"stats":{"Line":14}},{"line":184,"address":[],"length":0,"stats":{"Line":1}},{"line":186,"address":[],"length":0,"stats":{"Line":28}},{"line":191,"address":[],"length":0,"stats":{"Line":23}},{"line":192,"address":[],"length":0,"stats":{"Line":45}},{"line":193,"address":[],"length":0,"stats":{"Line":44}},{"line":194,"address":[],"length":0,"stats":{"Line":1}},{"line":196,"address":[],"length":0,"stats":{"Line":110}},{"line":197,"address":[],"length":0,"stats":{"Line":88}},{"line":202,"address":[],"length":0,"stats":{"Line":3}},{"line":203,"address":[],"length":0,"stats":{"Line":5}},{"line":204,"address":[],"length":0,"stats":{"Line":4}},{"line":205,"address":[],"length":0,"stats":{"Line":1}},{"line":207,"address":[],"length":0,"stats":{"Line":3}},{"line":208,"address":[],"length":0,"stats":{"Line":1}},{"line":211,"address":[],"length":0,"stats":{"Line":3}},{"line":212,"address":[],"length":0,"stats":{"Line":3}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":219,"address":[],"length":0,"stats":{"Line":5}},{"line":220,"address":[],"length":0,"stats":{"Line":6}},{"line":221,"address":[],"length":0,"stats":{"Line":2}},{"line":222,"address":[],"length":0,"stats":{"Line":0}},{"line":223,"address":[],"length":0,"stats":{"Line":2}},{"line":225,"address":[],"length":0,"stats":{"Line":6}},{"line":228,"address":[],"length":0,"stats":{"Line":1}},{"line":233,"address":[],"length":0,"stats":{"Line":42}},{"line":234,"address":[],"length":0,"stats":{"Line":42}}],"covered":94,"coverable":98},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi","taskhandle.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//! The boxed control block behind every `OakTaskTask` handle, mirroring\n//! `src/task/c_api/taskhandle.h` (`oaktask_capi::TaskHandle`).\n//!\n//! `owns_task` is the owns role: true for factory-created tasks (the last\n//! release drops the task) and false once the task runs on the manager\n//! (`oaktask_task_start()` flips it; the manager drops the task) or for\n//! borrowed wrappers (`oaktask_manager_at()`): releasing those only\n//! destroys the box.\n//!\n//! The C++ `TaskHandle` carries its own `finished`/`succeeded` atomics that\n//! the factory listener updates. The Rust [`crate::task::Task`] tracks these\n//! internally (race-free through its `done` condvar), so the box reads them\n//! from the task instead — the box only needs the downcast pointers for the\n//! `take_*` accessors (Rust has no `dynamic_cast`).\n//!\n//! CPP-PARITY: src/task/c_api/taskhandle.h\n\nuse std::ffi::c_void;\nuse std::sync::atomic::{AtomicI32, Ordering};\n\nuse crate::handle::{make_owned, CHandle};\nuse crate::project::import::ProjectImportTask;\nuse crate::project::load::ProjectLoadBaseTask;\nuse crate::task::Task;\n\n/// Concrete-task downcast, standing in for the C++ `dynamic_cast` in the\n/// `take_*` accessors.\npub enum TaskImpl {\n\t/// Not a project load/import task.\n\tNone,\n\t/// A `ProjectLoadBaseTask` (native or OTIO loader).\n\tLoadBase(*mut ProjectLoadBaseTask),\n\t/// A `ProjectImportTask`.\n\tImport(*mut ProjectImportTask),\n}\n\n/// Control block behind an `OakTaskTask` (see the module docs).\npub struct TaskHandleBox {\n\t/// The task the handle wraps (the outer task owning the behavior).\n\tpub task: *mut Task,\n\t/// Whether releasing the last reference drops the task.\n\tpub owned: bool,\n\t/// Whether the task has been handed to the manager.\n\tpub running_on_manager: bool,\n\t/// The owned task (factory case); taken by `oaktask_task_start`.\n\tpub owner: Option\u003cBox\u003cTask\u003e\u003e,\n\t/// Downcast target for the `take_*` accessors.\n\tpub impl_kind: TaskImpl,\n}\n\nimpl Drop for TaskHandleBox {\n\tfn drop(\u0026mut self) {\n\t\t// Mirrors `task_release`: the box is destroyed at zero refs, which\n\t\t// always decrements the alive count.\n\t\tALIVE.fetch_sub(1, Ordering::SeqCst);\n\t}\n}\n\n// Safety: the box is only ever accessed through the C ABI entry points\n// (never concurrently with its own mutation); the raw pointers inside are\n// stable while the box lives.\nunsafe impl Send for TaskHandleBox {}\n\n/// Alive-count for leak assertions in tests (mirrors\n/// `oaktask_capi::alive()`).\nstatic ALIVE: AtomicI32 = AtomicI32::new(0);\n\n/// Current alive handle count.\npub fn alive_count() -\u003e i32 {\n\tALIVE.load(Ordering::SeqCst)\n}\n\n/// Wrap an owned task (reference count 1). The handle owns the task and\n/// drops it on the final release.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap)\npub fn wrap_owned(task: Box\u003cTask\u003e) -\u003e CHandle {\n\twrap_owned_with_impl(task, TaskImpl::None)\n}\n\n/// Wrap an owned task with a downcast target for the `take_*` accessors.\npub fn wrap_owned_with_impl(task: Box\u003cTask\u003e, impl_kind: TaskImpl) -\u003e CHandle {\n\tlet ptr = Box::into_raw(task);\n\tALIVE.fetch_add(1, Ordering::SeqCst);\n\t// Safety: `ptr` is a live, uniquely owned `Box\u003cTask\u003e`.\n\tlet handle = make_owned(TaskHandleBox {\n\t\ttask: ptr,\n\t\towned: true,\n\t\trunning_on_manager: false,\n\t\towner: unsafe { Some(Box::from_raw(ptr)) },\n\t\timpl_kind,\n\t});\n\thandle\n}\n\n/// Wrap a manager-owned (borrowed) task. Releasing the handle does NOT drop\n/// the task.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed)\npub fn wrap_borrowed(ptr: *mut Task) -\u003e CHandle {\n\tif ptr.is_null() {\n\t\treturn CHandle::null();\n\t}\n\tALIVE.fetch_add(1, Ordering::SeqCst);\n\tmake_owned(TaskHandleBox {\n\t\ttask: ptr,\n\t\towned: false,\n\t\trunning_on_manager: true,\n\t\towner: None,\n\t\timpl_kind: TaskImpl::None,\n\t})\n}\n\n/// Typed view of a task handle box; `None` for empty handles.\n///\n/// # Safety\n/// The handle must have been created by this module.\npub fn get_task(h: \u0026CHandle) -\u003e Option\u003c\u0026TaskHandleBox\u003e {\n\tunsafe { crate::handle::get::\u003cTaskHandleBox\u003e(h) }\n}\n\n/// Typed mutable view of a task handle box; `None` for empty handles.\n///\n/// # Safety\n/// The handle must have been created by this module and not be shared with\n/// a concurrent mutable access.\npub fn get_task_mut(h: \u0026CHandle) -\u003e Option\u003c\u0026mut TaskHandleBox\u003e {\n\tunsafe { crate::handle::get_mut::\u003cTaskHandleBox\u003e(h) }\n}\n\n/// Two-stage string copy matching `oaktask_capi::copy_string`: returns the\n/// needed size (`len + 1`); writes only when the buffer is non-null and\n/// large enough.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (copy_string)\npub fn copy_string(value: \u0026str, buf: *mut std::ffi::c_char, buf_size: i32) -\u003e i32 {\n\tlet needed = value.len() as i32 + 1;\n\tif !buf.is_null() \u0026\u0026 buf_size \u003e= needed {\n\t\tunsafe {\n\t\t\tstd::ptr::copy_nonoverlapping(value.as_ptr() as *const std::ffi::c_char, buf, value.len());\n\t\t\t*buf.add(value.len()) = 0;\n\t\t}\n\t}\n\tneeded\n}\n\n/// Read a NUL-terminated C string into a Rust `String` (lossy); empty when\n/// the pointer is null.\n///\n/// # Safety\n/// `ptr` must be a valid NUL-terminated C string or null.\npub unsafe fn cstr_to_string(ptr: *const std::ffi::c_char) -\u003e String {\n\tif ptr.is_null() {\n\t\treturn String::new();\n\t}\n\tunsafe { std::ffi::CStr::from_ptr(ptr) }.to_string_lossy().into_owned()\n}\n\n/// Build a NUL-terminated C string for the duration of the call (leaked).\npub fn cstr(s: \u0026str) -\u003e *const std::ffi::c_char {\n\tlet mut bytes = s.as_bytes().to_vec();\n\tbytes.push(0);\n\tbytes.leak().as_ptr() as *const std::ffi::c_char\n}\n\n/// `userdata` payload converted to a `Send`-friendly form for closures.\npub fn userdata_usize(userdata: *mut c_void) -\u003e usize {\n\tuserdata as usize\n}\n","traces":[{"line":68,"address":[],"length":0,"stats":{"Line":51}},{"line":71,"address":[],"length":0,"stats":{"Line":153}},{"line":85,"address":[],"length":0,"stats":{"Line":42}},{"line":86,"address":[],"length":0,"stats":{"Line":126}},{"line":93,"address":[],"length":0,"stats":{"Line":34}},{"line":94,"address":[],"length":0,"stats":{"Line":102}},{"line":98,"address":[],"length":0,"stats":{"Line":50}},{"line":99,"address":[],"length":0,"stats":{"Line":150}},{"line":100,"address":[],"length":0,"stats":{"Line":150}},{"line":102,"address":[],"length":0,"stats":{"Line":150}},{"line":103,"address":[],"length":0,"stats":{"Line":100}},{"line":104,"address":[],"length":0,"stats":{"Line":50}},{"line":105,"address":[],"length":0,"stats":{"Line":50}},{"line":106,"address":[],"length":0,"stats":{"Line":50}},{"line":107,"address":[],"length":0,"stats":{"Line":50}},{"line":109,"address":[],"length":0,"stats":{"Line":50}},{"line":116,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":2}},{"line":118,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":3}},{"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":126,"address":[],"length":0,"stats":{"Line":1}},{"line":134,"address":[],"length":0,"stats":{"Line":132}},{"line":135,"address":[],"length":0,"stats":{"Line":264}},{"line":143,"address":[],"length":0,"stats":{"Line":10}},{"line":144,"address":[],"length":0,"stats":{"Line":20}},{"line":152,"address":[],"length":0,"stats":{"Line":31}},{"line":153,"address":[],"length":0,"stats":{"Line":62}},{"line":154,"address":[],"length":0,"stats":{"Line":46}},{"line":156,"address":[],"length":0,"stats":{"Line":84}},{"line":157,"address":[],"length":0,"stats":{"Line":42}},{"line":160,"address":[],"length":0,"stats":{"Line":31}},{"line":168,"address":[],"length":0,"stats":{"Line":59}},{"line":169,"address":[],"length":0,"stats":{"Line":118}},{"line":170,"address":[],"length":0,"stats":{"Line":0}},{"line":172,"address":[],"length":0,"stats":{"Line":177}},{"line":176,"address":[],"length":0,"stats":{"Line":92}},{"line":177,"address":[],"length":0,"stats":{"Line":276}},{"line":178,"address":[],"length":0,"stats":{"Line":184}},{"line":179,"address":[],"length":0,"stats":{"Line":92}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":184,"address":[],"length":0,"stats":{"Line":1}}],"covered":44,"coverable":46},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","ffi.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//! oaktask C ABI export surface. These `#[no_mangle] extern \"C\"` symbols are\n//! the Rust counterpart of the C++ task facade declared in\n//! `include/task/*.h`; each module mirrors one header. The public headers are\n//! authoritative for signatures; the inventory comments below each module\n//! enumerate the full symbol set. Every export passes through\n//! [`crate::handle::guard*`] / explicit null checks so panics never cross the\n//! FFI boundary.\n\npub mod manager;\npub mod project;\npub mod task;\npub(crate) mod taskhandle;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","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 (the\n//! function pointers in a handle always point into the DLL that created the\n//! object).\n\nuse std::sync::atomic::AtomicU32;\n\n/// ABI version stamped into every handle.\npub const OAKTASK_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#[repr(C)]\n#[derive(Clone, Copy, Debug, Default)]\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\n// Handles are shared between the calling thread and the manager worker\n// threads (same ownership model as a C++ shared_ptr). The raw pointer and\n// function-pointer fields are inherently `!Send`/`!Sync`; the box behind\n// `ctx` is either `Send + Sync` (task state, atomics) or a plain pointer\n// (borrowed handles), both of which are safe to share through the\n// refcounted handle.\nunsafe impl Send for CHandle {}\nunsafe impl Sync for CHandle {}\n\nunsafe extern \"C\" fn noop_addref(_ctx: *mut std::ffi::c_void) {}\n\nunsafe extern \"C\" fn noop_release(_ctx: *mut std::ffi::c_void) {}\n\nunsafe extern \"C\" fn owned_addref\u003cT: 'static\u003e(ctx: *mut std::ffi::c_void) {\n\tif !ctx.is_null() {\n\t\t// CPP-PARITY: src/task/c_api/taskhandle.h (task_addref)\n\t\tunsafe {\n\t\t\t(*(ctx as *const RefBox\u003cT\u003e)).refs.fetch_add(1, std::sync::atomic::Ordering::SeqCst);\n\t\t}\n\t}\n}\n\nunsafe extern \"C\" fn owned_release\u003cT: 'static\u003e(ctx: *mut std::ffi::c_void) {\n\tif ctx.is_null() {\n\t\treturn;\n\t}\n\t// CPP-PARITY: src/task/c_api/taskhandle.h (task_release)\n\tlet b = ctx as *const RefBox\u003cT\u003e;\n\tlet last = unsafe { (*b).refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) };\n\tif last == 1 {\n\t\tunsafe {\n\t\t\tdrop(Box::from_raw(ctx as *mut RefBox\u003cT\u003e));\n\t\t}\n\t}\n}\n\n/// Release function for borrowed handles: destroys only the box, never the\n/// pointee.\n///\n/// CPP-PARITY: src/task/c_api/taskhandle.h (wrap_borrowed)\nunsafe extern \"C\" fn borrowed_release\u003cT: 'static\u003e(ctx: *mut std::ffi::c_void) {\n\tif ctx.is_null() {\n\t\treturn;\n\t}\n\tlet b = ctx as *const RefBox\u003c*mut T\u003e;\n\tlet last = unsafe { (*b).refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) };\n\tif last == 1 {\n\t\tunsafe {\n\t\t\tdrop(Box::from_raw(ctx as *mut RefBox\u003c*mut T\u003e));\n\t\t}\n\t}\n}\n\nimpl CHandle {\n\t/// The empty handle. `ctx` is null but the fn pointers and ABI version\n\t/// are stamped so callers can unconditionally call `release`.\n\tpub fn null() -\u003e Self {\n\t\tCHandle {\n\t\t\tctx: std::ptr::null_mut(),\n\t\t\taddref: Some(noop_addref),\n\t\t\trelease: Some(noop_release),\n\t\t\tabi_version: OAKTASK_ABI_VERSION,\n\t\t}\n\t}\n\n\t/// Whether the handle is empty (`ctx == NULL`).\n\tpub fn is_null(\u0026self) -\u003e bool {\n\t\tself.ctx.is_null()\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 b = Box::new(RefBox { refs: AtomicU32::new(1), value });\n\tCHandle {\n\t\tctx: Box::into_raw(b) as *mut std::ffi::c_void,\n\t\taddref: Some(owned_addref::\u003cT\u003e),\n\t\trelease: Some(owned_release::\u003cT\u003e),\n\t\tabi_version: OAKTASK_ABI_VERSION,\n\t}\n}\n\n/// Borrowed handle for an object owned elsewhere (release frees only the\n/// box).\n///\n/// # Safety\n/// Caller guarantees `ptr` outlives every derived handle.\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\tlet b = Box::new(RefBox { refs: AtomicU32::new(1), value: ptr });\n\tCHandle {\n\t\tctx: Box::into_raw(b) as *mut std::ffi::c_void,\n\t\taddref: Some(owned_addref::\u003c*mut T\u003e),\n\t\trelease: Some(borrowed_release::\u003cT\u003e),\n\t\tabi_version: OAKTASK_ABI_VERSION,\n\t}\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.ctx.is_null() {\n\t\treturn None;\n\t}\n\tunsafe { Some(\u0026(*(h.ctx as *const RefBox\u003cT\u003e)).value) }\n}\n\n/// Typed mutable view into a handle; `None` for empty handles.\n///\n/// # Safety\n/// `T` must be the boxed type, and the handle must not be concurrently\n/// shared mutably.\npub unsafe fn get_mut\u003cT: 'static\u003e(h: \u0026CHandle) -\u003e Option\u003c\u0026mut T\u003e {\n\tif h.ctx.is_null() {\n\t\treturn None;\n\t}\n\tunsafe { Some(\u0026mut (*(h.ctx as *mut RefBox\u003cT\u003e)).value) }\n}\n\n/// Panic-catching FFI wrapper for i32-returning exports.\npub fn guard\u003cF: FnOnce() -\u003e crate::error::Result\u003c()\u003e\u003e(f: F) -\u003e i32 {\n\tmatch std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {\n\t\tOk(Ok(())) =\u003e crate::error::OAKTASK_OK,\n\t\tOk(Err(e)) =\u003e e.code(),\n\t\tErr(_) =\u003e crate::error::OAKTASK_E_FAILED,\n\t}\n}\n\n/// Panic-catching FFI wrapper for handle-returning exports.\npub fn guard_handle\u003cF: FnOnce() -\u003e crate::error::Result\u003cCHandle\u003e\u003e(f: F) -\u003e CHandle {\n\tmatch std::panic::catch_unwind(std::panic::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 _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));\n}\n","traces":[{"line":60,"address":[],"length":0,"stats":{"Line":2}},{"line":62,"address":[],"length":0,"stats":{"Line":4}},{"line":64,"address":[],"length":0,"stats":{"Line":1}},{"line":65,"address":[],"length":0,"stats":{"Line":1}},{"line":68,"address":[],"length":0,"stats":{"Line":2}},{"line":73,"address":[],"length":0,"stats":{"Line":53}},{"line":74,"address":[],"length":0,"stats":{"Line":106}},{"line":75,"address":[],"length":0,"stats":{"Line":0}},{"line":78,"address":[],"length":0,"stats":{"Line":106}},{"line":79,"address":[],"length":0,"stats":{"Line":212}},{"line":80,"address":[],"length":0,"stats":{"Line":53}},{"line":82,"address":[],"length":0,"stats":{"Line":104}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":99,"address":[],"length":0,"stats":{"Line":0}},{"line":107,"address":[],"length":0,"stats":{"Line":535}},{"line":109,"address":[],"length":0,"stats":{"Line":1070}},{"line":110,"address":[],"length":0,"stats":{"Line":535}},{"line":111,"address":[],"length":0,"stats":{"Line":535}},{"line":117,"address":[],"length":0,"stats":{"Line":365}},{"line":118,"address":[],"length":0,"stats":{"Line":730}},{"line":123,"address":[],"length":0,"stats":{"Line":52}},{"line":124,"address":[],"length":0,"stats":{"Line":156}},{"line":126,"address":[],"length":0,"stats":{"Line":104}},{"line":127,"address":[],"length":0,"stats":{"Line":52}},{"line":128,"address":[],"length":0,"stats":{"Line":52}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":142,"address":[],"length":0,"stats":{"Line":0}},{"line":144,"address":[],"length":0,"stats":{"Line":0}},{"line":145,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":155,"address":[],"length":0,"stats":{"Line":132}},{"line":156,"address":[],"length":0,"stats":{"Line":264}},{"line":157,"address":[],"length":0,"stats":{"Line":8}},{"line":159,"address":[],"length":0,"stats":{"Line":124}},{"line":167,"address":[],"length":0,"stats":{"Line":10}},{"line":168,"address":[],"length":0,"stats":{"Line":20}},{"line":169,"address":[],"length":0,"stats":{"Line":0}},{"line":171,"address":[],"length":0,"stats":{"Line":10}},{"line":175,"address":[],"length":0,"stats":{"Line":0}},{"line":176,"address":[],"length":0,"stats":{"Line":0}},{"line":177,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"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":192,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":0}}],"covered":29,"coverable":56},{"path":["/","Users","sunyu","Projects","oak","src","task","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//! # oaktask — the task execution module (Rust)\n//!\n//! Reimplements the C++ task module behind its frozen C ABI\n//! (`include/task/*.h`). See README.md for the architectural mapping\n//! (inheritance → one module per class + trait objects, cancellation via\n//! the oakrender cancelatom C ABI, events as mutex-guarded callbacks).\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 state\n//! behind `Mutex`.\n\n#![deny(unsafe_op_in_unsafe_fn)]\n#![warn(missing_docs)]\n\npub mod bridge;\npub mod codecbridge;\npub mod conform;\npub mod customcache;\npub mod error;\npub mod export;\npub mod ffi;\npub mod handle;\npub mod manager;\npub mod precache;\npub mod project;\npub mod proxy;\npub mod render;\npub mod task;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","manager.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//! The `TaskManager` singleton, mirroring `src/task/src/taskmanager.h`.\n//!\n//! Holds the set of live [`crate::task::Task`] instances; starting a task\n//! hands its ownership to the manager, which also exposes the codec task\n//! submitter registration (see [`crate::codecbridge`]).\n//!\n//! CPP-PARITY: src/task/src/taskmanager.h\n//!\n//! ## Threading model\n//!\n//! [`TaskManager::add_task`] spawns a worker thread that runs the task's\n//! [`Task::start`]. The worker thread only touches the task (through a raw\n//! pointer) and never the manager, so the manager's own mutex is never held\n//! across a join in the C ABI paths (joins happen lock-free via\n//! [`TaskManager::drain_finished`] / [`TaskManager::take_thread_by_ptr`]).\n\nuse std::sync::Mutex;\n\nuse crate::error::{Error, Result};\nuse crate::handle::{make_borrowed, CHandle};\nuse crate::task::Task;\n\n/// Process-wide singleton manager. C++ is a lazy singleton; the Rust side\n/// keeps a `Mutex\u003cOption\u003c...\u003e\u003e` so init/shutdown is explicit and races are\n/// rejected with `OAKTASK_E_STATE` (matching `oaktask_manager_init`).\npub struct TaskManager {\n\t/// Live tasks, most-recently-added last (mirrors the C++ `task_list_`).\n\ttasks: Vec\u003cBox\u003cdyn TaskBox\u003e\u003e,\n\t/// Worker threads, parallel to `tasks` (moved out before joining).\n\tthreads: Vec\u003cOption\u003cstd::thread::JoinHandle\u003c()\u003e\u003e\u003e,\n\t/// Whether the codec task submitter is currently registered.\n\tcodec_submitter_registered: bool,\n}\n\n/// Boxable shim so tasks of heterogeneous concrete types can be stored in\n/// the manager's list.\npub trait TaskBox: Send {\n\t/// Downcast/erase operations the manager needs.\n\tfn task(\u0026self) -\u003e \u0026Task;\n}\n\nimpl TaskBox for Task {\n\tfn task(\u0026self) -\u003e \u0026Task {\n\t\tself\n\t}\n}\n\n/// The process-wide instance slot. A raw pointer (never freed while the\n/// slot is set) so [`TaskManager::instance`] can hand out a `\u0026'static`.\nstatic INSTANCE: Mutex\u003cOption\u003cManagerPtr\u003e\u003e = Mutex::new(None);\n\n/// `Send`/`Sync` wrapper for the manager pointer (the manager's state is\n/// guarded by the `INSTANCE` mutex; the pointer itself is plain).\nstruct ManagerPtr(*mut TaskManager);\n\n// Safety: all access to the pointee goes through the `INSTANCE` mutex.\nunsafe impl Send for ManagerPtr {}\nunsafe impl Sync for ManagerPtr {}\n\nimpl TaskManager {\n\t/// The process-wide instance; `None` until [`TaskManager::init`].\n\t///\n\t/// CPP-PARITY: src/task/src/taskmanager.h (instance)\n\tpub fn instance() -\u003e Option\u003c\u0026'static TaskManager\u003e {\n\t\tlet guard = INSTANCE.lock().unwrap();\n\t\tguard.as_ref().map(|p| unsafe { \u0026*p.0 })\n\t}\n\n\t/// Create the singleton and register the codec task submitter. Returns\n\t/// `Err(Error::State)` if already initialized (mirrors\n\t/// `oaktask_manager_init`).\n\tpub fn init() -\u003e Result\u003c()\u003e {\n\t\tlet mut guard = INSTANCE.lock().unwrap();\n\t\tif guard.is_some() {\n\t\t\treturn Err(Error::State);\n\t\t}\n\t\tlet mgr = Box::into_raw(Box::new(TaskManager {\n\t\t\ttasks: Vec::new(),\n\t\t\tthreads: Vec::new(),\n\t\t\tcodec_submitter_registered: false,\n\t\t}));\n\t\t*guard = Some(ManagerPtr(mgr));\n\t\tOk(())\n\t}\n\n\t/// Destroy the singleton. Idempotent.\n\tpub fn shutdown() {\n\t\tlet ptr = {\n\t\t\tlet mut guard = INSTANCE.lock().unwrap();\n\t\t\tguard.take()\n\t\t};\n\t\tif let Some(p) = ptr {\n\t\t\t// The Drop impl cancels and joins every worker thread.\n\t\t\tunsafe {\n\t\t\t\tdrop(Box::from_raw(p.0));\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Run `f` with the singleton (when initialized), without taking\n\t/// ownership. Used by the C ABI layer.\n\tpub fn with_manager\u003cR\u003e(f: impl FnOnce(\u0026TaskManager) -\u003e R) -\u003e Option\u003cR\u003e {\n\t\tlet guard = INSTANCE.lock().unwrap();\n\t\tguard.as_ref().map(|p| f(unsafe { \u0026*p.0 }))\n\t}\n\n\t/// Run `f` with the singleton mutably (when initialized). Used by the C\n\t/// ABI layer. The caller must not call [`TaskManager::with_manager`]\n\t/// from a worker callback while this is held (same rule as the C++\n\t/// `mutex_`).\n\tpub fn with_manager_mut\u003cR\u003e(f: impl FnOnce(\u0026mut TaskManager) -\u003e R) -\u003e Option\u003cR\u003e {\n\t\tlet mut guard = INSTANCE.lock().unwrap();\n\t\tguard.as_mut().map(|p| f(unsafe { \u0026mut *p.0 }))\n\t}\n\n\t/// Add a task, taking ownership (the manager now owns it). A worker\n\t/// thread is spawned that runs [`Task::start`] on the task.\n\t///\n\t/// The worker runs through a raw pointer while the box lives in\n\t/// `self.tasks`; the shared reference produced by `task()` ends before\n\t/// the pointer is used, so the cast is safe in this exclusively-owned\n\t/// setting.\n\t#[allow(invalid_reference_casting)]\n\tpub fn add_task(\u0026mut self, task: Box\u003cdyn TaskBox\u003e) {\n\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\tself.tasks.push(task);\n\t\t// The worker only touches the task through the raw pointer; it never\n\t\t// reaches the manager, so the box stays alive in `self.tasks`.\n\t\tlet ptr_usize = ptr as usize;\n\t\tlet handle = std::thread::spawn(move || {\n\t\t\tunsafe {\n\t\t\t\t// The manager tracks success/failure through the task's own\n\t\t\t\t// finished state; the Result is intentionally ignored.\n\t\t\t\tlet _ = (\u0026mut *(ptr_usize as *mut Task)).start();\n\t\t\t}\n\t\t});\n\t\tself.threads.push(Some(handle));\n\t}\n\n\t/// Cancel the task at `index`.\n\t#[allow(invalid_reference_casting)]\n\tpub fn cancel_task(\u0026mut self, index: usize) -\u003e Result\u003c()\u003e {\n\t\tlet task = self.tasks.get(index).ok_or(Error::NotFound)?;\n\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\tunsafe {\n\t\t\t(\u0026mut *ptr).cancel();\n\t\t}\n\t\tOk(())\n\t}\n\n\t/// Cancel the task at `index` and block until it finishes.\n\tpub fn cancel_task_and_wait(\u0026mut self, index: usize) -\u003e Result\u003c()\u003e {\n\t\tself.cancel_task(index)?;\n\t\tlet handle = self.threads.get_mut(index).and_then(|h| h.take());\n\t\tif let Some(h) = handle {\n\t\t\tlet _ = h.join();\n\t\t}\n\t\tOk(())\n\t}\n\n\t/// Remove finished tasks from the list.\n\tpub fn delete_finished(\u0026mut self) {\n\t\tfor (_task, handle) in self.drain_finished() {\n\t\t\tlet _ = handle.join();\n\t\t}\n\t}\n\n\t/// Number of live tasks.\n\tpub fn get_task_count(\u0026self) -\u003e usize {\n\t\tself.tasks.len()\n\t}\n\n\t/// Borrowed handle to the task at `index`; `Err(Error::NotFound)` if out\n\t/// of range.\n\tpub fn get_task_at(\u0026self, index: usize) -\u003e Result\u003cCHandle\u003e {\n\t\tlet task = self.tasks.get(index).ok_or(Error::NotFound)?;\n\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\tOk(unsafe { make_borrowed::\u003cTask\u003e(ptr) })\n\t}\n\n\t/// Raw pointer to the task at `index` (stable while the manager owns\n\t/// it). Used by `oaktask_manager_at` to build a borrowed task handle.\n\tpub fn task_ptr_at(\u0026self, index: usize) -\u003e Result\u003c*mut Task\u003e {\n\t\tlet task = self.tasks.get(index).ok_or(Error::NotFound)?;\n\t\tOk(task.task() as *const Task as *mut Task)\n\t}\n\n\t/// Index of the task with the given address.\n\tpub fn find_index(\u0026self, ptr: *const Task) -\u003e Option\u003cusize\u003e {\n\t\tself.tasks\n\t\t\t.iter()\n\t\t\t.position(|t| t.task() as *const Task == ptr)\n\t}\n\n\t/// Cancel the task with the given address (no-op when absent).\n\tpub fn cancel_task_by_ptr(\u0026mut self, ptr: *const Task) {\n\t\tif let Some(index) = self.find_index(ptr) {\n\t\t\tlet _ = self.cancel_task(index);\n\t\t}\n\t}\n\n\t/// Move the worker thread of the task with the given address out of the\n\t/// list so the caller can join it without holding the manager lock.\n\tpub fn take_thread_by_ptr(\u0026mut self, ptr: *const Task) -\u003e Option\u003cstd::thread::JoinHandle\u003c()\u003e\u003e {\n\t\tlet index = self.find_index(ptr)?;\n\t\tself.threads.get_mut(index).and_then(|h| h.take())\n\t}\n\n\t/// Remove every finished task (and its worker thread) from the list,\n\t/// returning the task boxes and thread handles so the caller can join\n\t/// them lock-free (the boxes must stay alive until the threads exit).\n\tpub fn drain_finished(\u0026mut self) -\u003e Vec\u003c(Box\u003cdyn TaskBox\u003e, std::thread::JoinHandle\u003c()\u003e)\u003e {\n\t\tlet mut out = Vec::new();\n\t\tlet mut i = 0;\n\t\twhile i \u003c self.tasks.len() {\n\t\t\tif self.tasks[i].task().is_finished() {\n\t\t\t\tlet task = self.tasks.remove(i);\n\t\t\t\tlet handle = self.threads.remove(i);\n\t\t\t\tif let Some(handle) = handle {\n\t\t\t\t\tout.push((task, handle));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ti += 1;\n\t\t\t}\n\t\t}\n\t\tout\n\t}\n\n\t/// Whether this manager registered the codec submitter itself.\n\tpub fn codec_submitter_registered(\u0026self) -\u003e bool {\n\t\tself.codec_submitter_registered\n\t}\n\n\t/// Mark whether this manager registered the codec submitter.\n\tpub fn set_codec_submitter_registered(\u0026mut self, registered: bool) {\n\t\tself.codec_submitter_registered = registered;\n\t}\n}\n\nimpl Drop for TaskManager {\n\tfn drop(\u0026mut self) {\n\t\t// Cancel every task, then join every worker (mirrors the C++\n\t\t// destructor: cancel all first, then join all).\n\t\t#[allow(invalid_reference_casting)]\n\t\tfor task in \u0026self.tasks {\n\t\t\tlet ptr = task.task() as *const Task as usize as *mut Task;\n\t\t\tunsafe {\n\t\t\t\t(\u0026mut *ptr).cancel();\n\t\t\t}\n\t\t}\n\t\tfor handle in self.threads.iter_mut() {\n\t\t\tif let Some(h) = handle.take() {\n\t\t\t\tlet _ = h.join();\n\t\t\t}\n\t\t}\n\t}\n}\n","traces":[{"line":59,"address":[],"length":0,"stats":{"Line":28}},{"line":60,"address":[],"length":0,"stats":{"Line":28}},{"line":80,"address":[],"length":0,"stats":{"Line":7}},{"line":81,"address":[],"length":0,"stats":{"Line":28}},{"line":82,"address":[],"length":0,"stats":{"Line":26}},{"line":88,"address":[],"length":0,"stats":{"Line":8}},{"line":89,"address":[],"length":0,"stats":{"Line":32}},{"line":90,"address":[],"length":0,"stats":{"Line":8}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":93,"address":[],"length":0,"stats":{"Line":32}},{"line":94,"address":[],"length":0,"stats":{"Line":8}},{"line":95,"address":[],"length":0,"stats":{"Line":8}},{"line":96,"address":[],"length":0,"stats":{"Line":8}},{"line":98,"address":[],"length":0,"stats":{"Line":8}},{"line":99,"address":[],"length":0,"stats":{"Line":8}},{"line":103,"address":[],"length":0,"stats":{"Line":34}},{"line":104,"address":[],"length":0,"stats":{"Line":34}},{"line":105,"address":[],"length":0,"stats":{"Line":136}},{"line":106,"address":[],"length":0,"stats":{"Line":34}},{"line":108,"address":[],"length":0,"stats":{"Line":42}},{"line":111,"address":[],"length":0,"stats":{"Line":16}},{"line":118,"address":[],"length":0,"stats":{"Line":15}},{"line":119,"address":[],"length":0,"stats":{"Line":60}},{"line":120,"address":[],"length":0,"stats":{"Line":46}},{"line":127,"address":[],"length":0,"stats":{"Line":63}},{"line":128,"address":[],"length":0,"stats":{"Line":252}},{"line":129,"address":[],"length":0,"stats":{"Line":196}},{"line":140,"address":[],"length":0,"stats":{"Line":5}},{"line":141,"address":[],"length":0,"stats":{"Line":10}},{"line":142,"address":[],"length":0,"stats":{"Line":15}},{"line":145,"address":[],"length":0,"stats":{"Line":10}},{"line":146,"address":[],"length":0,"stats":{"Line":15}},{"line":147,"address":[],"length":0,"stats":{"Line":5}},{"line":150,"address":[],"length":0,"stats":{"Line":5}},{"line":153,"address":[],"length":0,"stats":{"Line":15}},{"line":158,"address":[],"length":0,"stats":{"Line":5}},{"line":159,"address":[],"length":0,"stats":{"Line":25}},{"line":160,"address":[],"length":0,"stats":{"Line":10}},{"line":162,"address":[],"length":0,"stats":{"Line":5}},{"line":164,"address":[],"length":0,"stats":{"Line":5}},{"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":174,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":6}},{"line":186,"address":[],"length":0,"stats":{"Line":12}},{"line":191,"address":[],"length":0,"stats":{"Line":0}},{"line":192,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":0}},{"line":194,"address":[],"length":0,"stats":{"Line":0}},{"line":199,"address":[],"length":0,"stats":{"Line":2}},{"line":200,"address":[],"length":0,"stats":{"Line":10}},{"line":201,"address":[],"length":0,"stats":{"Line":1}},{"line":205,"address":[],"length":0,"stats":{"Line":10}},{"line":206,"address":[],"length":0,"stats":{"Line":10}},{"line":208,"address":[],"length":0,"stats":{"Line":34}},{"line":212,"address":[],"length":0,"stats":{"Line":5}},{"line":213,"address":[],"length":0,"stats":{"Line":20}},{"line":214,"address":[],"length":0,"stats":{"Line":10}},{"line":220,"address":[],"length":0,"stats":{"Line":5}},{"line":221,"address":[],"length":0,"stats":{"Line":20}},{"line":222,"address":[],"length":0,"stats":{"Line":25}},{"line":228,"address":[],"length":0,"stats":{"Line":4}},{"line":229,"address":[],"length":0,"stats":{"Line":8}},{"line":230,"address":[],"length":0,"stats":{"Line":8}},{"line":231,"address":[],"length":0,"stats":{"Line":18}},{"line":232,"address":[],"length":0,"stats":{"Line":10}},{"line":233,"address":[],"length":0,"stats":{"Line":20}},{"line":234,"address":[],"length":0,"stats":{"Line":20}},{"line":235,"address":[],"length":0,"stats":{"Line":5}},{"line":236,"address":[],"length":0,"stats":{"Line":0}},{"line":239,"address":[],"length":0,"stats":{"Line":0}},{"line":242,"address":[],"length":0,"stats":{"Line":4}},{"line":246,"address":[],"length":0,"stats":{"Line":0}},{"line":247,"address":[],"length":0,"stats":{"Line":0}},{"line":251,"address":[],"length":0,"stats":{"Line":16}},{"line":252,"address":[],"length":0,"stats":{"Line":16}},{"line":257,"address":[],"length":0,"stats":{"Line":8}},{"line":261,"address":[],"length":0,"stats":{"Line":8}},{"line":262,"address":[],"length":0,"stats":{"Line":0}},{"line":264,"address":[],"length":0,"stats":{"Line":0}},{"line":267,"address":[],"length":0,"stats":{"Line":8}},{"line":268,"address":[],"length":0,"stats":{"Line":0}},{"line":269,"address":[],"length":0,"stats":{"Line":0}}],"covered":67,"coverable":89},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","precache.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//! `PreCacheTask`, mirroring `src/task/src/precache/precachetask.h`.\n//!\n//! Renders a footage node (or the whole sequence) through\n//! [`crate::render::RenderTask`] to fill the playback cache. Owns a deep copy\n//! of the project (`OakNodeProject`) and borrows the source footage\n//! (`OakNodeFootage`).\n//!\n//! **Simplifications over the C++**: the deep project copy / viewer wiring\n//! is not built (the render drives the given viewer directly) and the\n//! timeline work-area intersection is replaced by the full footage length.\n//!\n//! CPP-PARITY: src/task/src/precache/precachetask.h\n\nuse crate::bridge;\nuse crate::error::Result;\nuse crate::handle::CHandle;\nuse crate::render::{RenderTask, RenderTaskBehavior};\nuse crate::task::{Task, TaskBehavior};\nuse oakcore_rs::{Rational, TimeRange};\n\n/// A pre-cache task: renders frames and audio of a footage node into the\n/// playback cache without any output file.\npub struct PreCacheTask {\n\t/// The render base (itself a task).\n\tpub render: RenderTask,\n\t/// Owning deep copy of the project (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Borrowed source footage (borrowed `OakNodeFootage`).\n\tpub footage: CHandle,\n\t/// Frame index within the footage being cached.\n\tpub index: i32,\n\t/// The sequence node (borrowed `OakNodeSequence`).\n\tpub sequence: CHandle,\n}\n\nimpl PreCacheTask {\n\t/// Create a pre-cache task for the given footage at `index` inside\n\t/// `sequence`.\n\tpub fn new(footage: CHandle, index: i32, sequence: CHandle) -\u003e PreCacheTask {\n\t\t// Video params from the sequence (empty when unavailable).\n\t\tlet mut video_params = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_sequence_get_video_params(sequence, 0, \u0026mut video_params);\n\t\t}\n\n\t\tlet filename = footage_filename(footage);\n\t\tlet title = format!(\"Pre-caching {filename}:{index}\");\n\t\tlet base = Task::new(\u0026title, CHandle::null());\n\t\tlet render = RenderTask::new(base, video_params, CHandle::null(), sequence, Default::default(), None);\n\n\t\t// A scratch project for the render color manager (simplified).\n\t\tlet project = unsafe { bridge::node::oaknode_project_init() };\n\n\t\tPreCacheTask {\n\t\t\trender,\n\t\t\tproject,\n\t\t\tfootage,\n\t\t\tindex,\n\t\t\tsequence,\n\t\t}\n\t}\n}\n\nimpl TaskBehavior for PreCacheTask {\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Share the caller's cancellation atom with the inner render base.\n\t\tself.render.base.set_cancel_atom(task.get_cancel_atom());\n\n\t\t// The full footage length is the cache range (simplified: no\n\t\t// work-area intersection).\n\t\tlet mut len_num = 0i64;\n\t\tlet mut len_den = 1i64;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_footage_get_video_length(self.footage, \u0026mut len_num, \u0026mut len_den);\n\t\t}\n\t\tlet range = TimeRange::new(Rational::new(0, 1), Rational::new(len_num, len_den));\n\n\t\tlet mut color_manager = unsafe { bridge::node::oaknode_colormanager_init(self.project) };\n\t\tlet mut cache = bridge::render::OakRenderCache::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_get_video_frame_cache(self.sequence, \u0026mut cache);\n\t\t}\n\n\t\tself.render.set_render_inputs(color_manager, cache.to_chandle(), 0 /* k_online */, false, range);\n\n\t\t// Drive the render with `self` as the subclass behavior (the C++\n\t\t// virtual dispatch receiver); the render is temporarily moved out to\n\t\t// avoid a self-referential borrow and put back before the handles are\n\t\t// released below.\n\t\tlet mut render = std::mem::replace(\u0026mut self.render, crate::render::RenderTask::placeholder());\n\t\tlet result = render.render(task, self);\n\t\tself.render = render;\n\n\t\tif !cache.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cache_free(\u0026mut cache);\n\t\t\t}\n\t\t}\n\t\tif !color_manager.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_colormanager_free(\u0026mut color_manager);\n\t\t\t}\n\t\t}\n\n\t\tresult\n\t}\n}\n\nimpl RenderTaskBehavior for PreCacheTask {\n\tfn frame_downloaded(\u0026mut self, task: \u0026mut Task, frame: CHandle) -\u003e Result\u003c()\u003e {\n\t\t// Do nothing: pre-cache just fills the frame cache.\n\t\tlet _ = (task, frame);\n\t\tOk(())\n\t}\n\n\tfn audio_downloaded(\u0026mut self, task: \u0026mut Task, buffer: CHandle) -\u003e Result\u003c()\u003e {\n\t\t// Pre-cache doesn't cache any audio.\n\t\tlet _ = (task, buffer);\n\t\tOk(())\n\t}\n\n\tfn encode_subtitle(\u0026mut self, task: \u0026mut Task, text: \u0026str) -\u003e Result\u003c()\u003e {\n\t\tlet _ = (task, text);\n\t\tOk(())\n\t}\n}\n\n/// Two-stage read of the footage filename.\nfn footage_filename(footage: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);\n\t}\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n","traces":[{"line":55,"address":[],"length":0,"stats":{"Line":1}},{"line":57,"address":[],"length":0,"stats":{"Line":2}},{"line":59,"address":[],"length":0,"stats":{"Line":2}},{"line":62,"address":[],"length":0,"stats":{"Line":3}},{"line":63,"address":[],"length":0,"stats":{"Line":2}},{"line":64,"address":[],"length":0,"stats":{"Line":4}},{"line":65,"address":[],"length":0,"stats":{"Line":8}},{"line":68,"address":[],"length":0,"stats":{"Line":2}},{"line":81,"address":[],"length":0,"stats":{"Line":1}},{"line":83,"address":[],"length":0,"stats":{"Line":4}},{"line":87,"address":[],"length":0,"stats":{"Line":2}},{"line":88,"address":[],"length":0,"stats":{"Line":2}},{"line":90,"address":[],"length":0,"stats":{"Line":3}},{"line":92,"address":[],"length":0,"stats":{"Line":6}},{"line":94,"address":[],"length":0,"stats":{"Line":3}},{"line":95,"address":[],"length":0,"stats":{"Line":2}},{"line":97,"address":[],"length":0,"stats":{"Line":2}},{"line":100,"address":[],"length":0,"stats":{"Line":6}},{"line":106,"address":[],"length":0,"stats":{"Line":4}},{"line":107,"address":[],"length":0,"stats":{"Line":5}},{"line":108,"address":[],"length":0,"stats":{"Line":2}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":112,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":117,"address":[],"length":0,"stats":{"Line":1}},{"line":121,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":0}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":132,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":138,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":0}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":146,"address":[],"length":0,"stats":{"Line":4}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":148,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":3}},{"line":152,"address":[],"length":0,"stats":{"Line":4}},{"line":154,"address":[],"length":0,"stats":{"Line":7}},{"line":155,"address":[],"length":0,"stats":{"Line":4}}],"covered":33,"coverable":43},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","import.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//! `ProjectImportTask`, mirroring `src/task/src/project/import/import.h`.\n//!\n//! Imports media files into a folder of a project. Produces an undoable\n//! `OakUndoCommand` (taken via `take_command()`), tracks per-file import\n//! failures, and supports an optional image-sequence confirmation callback.\n//!\n//! CPP-PARITY: src/task/src/project/import/import.h\n\nuse std::path::Path;\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// Callback used to confirm whether a detected image sequence should be\n/// imported as a sequence rather than individual frames, mirroring\n/// `ImageSequenceConfirmFn` in import.h.\npub type ImageSequenceConfirmFn = Box\u003cdyn FnMut(\u0026str, \u0026str) -\u003e bool + Send\u003e;\n\n/// A project-import task. Borrows the destination folder and project.\npub struct ProjectImportTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Destination folder (borrowed `OakNodeFolder`).\n\tpub folder: CHandle,\n\t/// Destination project (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Media filenames to import.\n\tpub filenames: Vec\u003cString\u003e,\n\t/// The undo command produced by the task, taken via `take_command`.\n\tcommand: Option\u003cCHandle\u003e,\n\t/// Files that failed to import.\n\tinvalid_files: Vec\u003cString\u003e,\n\t/// Imported footage, taken via `get_imported_footage`.\n\timported_footage: Vec\u003cCHandle\u003e,\n\t/// Optional image-sequence confirmation callback.\n\timage_sequence_confirm: Option\u003cImageSequenceConfirmFn\u003e,\n\t/// Total number of files to import (directories counted recursively).\n\tfile_count: usize,\n\t/// Image-sequence files already decided not to be imported as sequences.\n\timage_sequence_ignore_files: Vec\u003cString\u003e,\n}\n\nimpl Drop for ProjectImportTask {\n\tfn drop(\u0026mut self) {\n\t\tif let Some(command) = \u0026mut self.command {\n\t\t\tif !command.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::undo::oakundo_command_free(command);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Borrowed footage handles: releasing them only frees the handle\n\t\t// boxes (the footage nodes are owned by the project).\n\t\tfor footage in \u0026self.imported_footage {\n\t\t\tif !footage.ctx.is_null() {\n\t\t\t\tif let Some(release) = footage.release {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\trelease(footage.ctx);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl ProjectImportTask {\n\t/// Create an import task for the given folder/project and filenames.\n\tpub fn new(\n\t\tbase: Task,\n\t\tfolder: CHandle,\n\t\tproject: CHandle,\n\t\tfilenames: Vec\u003cString\u003e,\n\t\timage_sequence_confirm: Option\u003cImageSequenceConfirmFn\u003e,\n\t\tfile_count: usize,\n\t) -\u003e ProjectImportTask {\n\t\tProjectImportTask {\n\t\t\tbase,\n\t\t\tfolder,\n\t\t\tproject,\n\t\t\tfilenames,\n\t\t\tcommand: None,\n\t\t\tinvalid_files: Vec::new(),\n\t\t\timported_footage: Vec::new(),\n\t\t\timage_sequence_confirm,\n\t\t\tfile_count,\n\t\t\timage_sequence_ignore_files: Vec::new(),\n\t\t}\n\t}\n\n\t/// Take ownership of the produced undo command; `Err(Error::State)` if\n\t/// the task has not run yet.\n\tpub fn take_command(\u0026mut self) -\u003e Result\u003cCHandle\u003e {\n\t\tself.command.take().ok_or(Error::State)\n\t}\n\n\t/// Number of files that failed to import.\n\tpub fn get_invalid_file_count(\u0026self) -\u003e usize {\n\t\tself.invalid_files.len()\n\t}\n\n\t/// Whether any file failed to import.\n\tpub fn has_invalid_files(\u0026self) -\u003e bool {\n\t\t!self.invalid_files.is_empty()\n\t}\n\n\t/// Borrowed handle to the imported footage at `index` (addref'd by the C\n\t/// ABI); `Err(Error::NotFound)` if out of range.\n\tpub fn get_imported_footage(\u0026self, index: usize) -\u003e Result\u003cCHandle\u003e {\n\t\tlet footage = self.imported_footage.get(index).ok_or(Error::NotFound)?;\n\t\tif !footage.ctx.is_null() {\n\t\t\tif let Some(addref) = footage.addref {\n\t\t\t\tunsafe {\n\t\t\t\t\taddref(footage.ctx);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tOk(*footage)\n\t}\n\n\t/// Total number of imported footage entries.\n\tpub fn get_file_count(\u0026self) -\u003e usize {\n\t\tself.imported_footage.len()\n\t}\n\n\t/// Set the image-sequence confirmation callback.\n\tpub fn set_image_sequence_confirm_callback(\u0026mut self, cb: ImageSequenceConfirmFn) {\n\t\tself.image_sequence_confirm = Some(cb);\n\t}\n\n\t/// The number of files counted at construction (progress denominator).\n\tpub fn file_count(\u0026self) -\u003e usize {\n\t\tself.file_count\n\t}\n\n\t/// The invalid filename at `index` (assumes the index is in range).\n\tpub(crate) fn invalid_file_at(\u0026self, index: usize) -\u003e \u0026str {\n\t\t\u0026self.invalid_files[index]\n\t}\n\n\tfn import(\u0026mut self, task: \u0026mut Task, folder: CHandle, entries: \u0026mut Vec\u003cString\u003e, counter: \u0026mut usize, parent_command: CHandle) {\n\t\tlet mut i = 0;\n\t\twhile i \u003c entries.len() {\n\t\t\tif task.is_cancelled() {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tlet file_path = \u0026entries[i];\n\n\t\t\tif Path::new(file_path).is_dir() {\n\t\t\t\t// Create a folder corresponding to the directory and recurse.\n\t\t\t\tlet entry_list: Vec\u003cString\u003e = match std::fs::read_dir(file_path) {\n\t\t\t\t\tOk(rd) =\u003e rd\n\t\t\t\t\t\t.filter_map(|e| e.ok())\n\t\t\t\t\t\t.map(|e| e.path().to_string_lossy().into_owned())\n\t\t\t\t\t\t.collect(),\n\t\t\t\t\tErr(_) =\u003e Vec::new(),\n\t\t\t\t};\n\n\t\t\t\tif !entry_list.is_empty() {\n\t\t\t\t\tlet folder_handle = unsafe { bridge::node::oaknode_folder_create(self.project) };\n\t\t\t\t\tif !folder_handle.ctx.is_null() {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::node::oaknode_node_set_label(\n\t\t\t\t\t\t\t\tbridge::node::oaknode_folder_as_node(folder_handle),\n\t\t\t\t\t\t\t\tcstr(\u0026basename_of(file_path)),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tself.add_item_to_folder(folder, unsafe { bridge::node::oaknode_folder_as_node(folder_handle) }, parent_command);\n\t\t\t\t\t\tlet mut sub_entries = entry_list;\n\t\t\t\t\t\tself.import(task, folder_handle, \u0026mut sub_entries, counter, parent_command);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlet footage = unsafe { bridge::node::oaknode_footage_create(self.project, std::ptr::null()) };\n\t\t\t\tif footage.ctx.is_null() {\n\t\t\t\t\ti += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_footage_set_cancel_atom(footage, bridge::render::OakCancelAtom::from_chandle(task.get_cancel_atom()));\n\t\t\t\t}\n\t\t\t\tlet ok = unsafe { bridge::node::oaknode_footage_set_filename(footage, cstr(file_path)) } == 0;\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_footage_set_cancel_atom(footage, bridge::render::OakCancelAtom::null());\n\t\t\t\t}\n\n\t\t\t\tif ok \u0026\u0026 unsafe { bridge::node::oaknode_footage_is_valid(footage) } != 0 {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_node_set_label(bridge::node::oaknode_footage_as_node(footage), cstr(\u0026basename_of(file_path)));\n\t\t\t\t\t}\n\n\t\t\t\t\t// See if this footage is an image sequence.\n\t\t\t\t\tself.validate_image_sequence(task, footage, entries, i);\n\n\t\t\t\t\t// Create the undoable command that adds the item.\n\t\t\t\t\tself.add_item_to_folder(folder, unsafe { bridge::node::oaknode_footage_as_node(footage) }, parent_command);\n\n\t\t\t\t\tself.imported_footage.push(footage);\n\t\t\t\t} else {\n\t\t\t\t\tself.invalid_files.push(file_path.clone());\n\n\t\t\t\t\t// Remove the invalid footage from the graph; the remove\n\t\t\t\t\t// command takes ownership on redo and deletes the node\n\t\t\t\t\t// when the command is destroyed.\n\t\t\t\t\tlet mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(footage)) };\n\t\t\t\t\tif !remove.ctx.is_null() {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(remove);\n\t\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut remove);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t*counter += 1;\n\t\t\t\ttask.emit_progress(*counter as f64 / self.file_count.max(1) as f64);\n\t\t\t}\n\t\t\ti += 1;\n\t\t}\n\t}\n\n\tfn add_item_to_folder(\u0026self, folder: CHandle, item: CHandle, command: CHandle) {\n\t\tlet child = unsafe { bridge::node::oaknode_command_create_folder_add_child(folder, item) };\n\t\tif !child.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::undo::oakundo_command_multi_add_child(command, child);\n\t\t\t}\n\t\t}\n\t}\n\n\tfn validate_image_sequence(\u0026mut self, task: \u0026mut Task, footage: CHandle, info_list: \u0026mut Vec\u003cString\u003e, index: usize) {\n\t\tlet filename = footage_filename(footage);\n\t\tif filename.is_empty() {\n\t\t\treturn;\n\t\t}\n\n\t\tlet digit_count = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_digit_count(cstr(\u0026filename)) };\n\t\tif digit_count \u003c= 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tif self.image_sequence_ignore_files.iter().any(|f| f == \u0026filename) {\n\t\t\treturn;\n\t\t}\n\n\t\tif !self.item_is_still_image_footage_only(footage) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet mut video_stream = CHandle::null();\n\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, \u0026mut video_stream) } != 0 {\n\t\t\treturn;\n\t\t}\n\n\t\tlet mut width = 0;\n\t\tlet mut height = 0;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_get_width(video_stream, \u0026mut width);\n\t\t\tbridge::common::oakcommon_videoparams_get_height(video_stream, \u0026mut height);\n\t\t}\n\n\t\tlet seq_index = unsafe { bridge::codec::oakcodec_decoder_get_image_sequence_index(cstr(\u0026filename)) };\n\n\t\tlet prev_fn = transform_sequence_filename(\u0026filename, seq_index - 1, digit_count);\n\t\tlet next_fn = transform_sequence_filename(\u0026filename, seq_index + 1, digit_count);\n\n\t\tlet previous_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(\u0026prev_fn)) };\n\t\tlet next_file = unsafe { bridge::node::oaknode_footage_create(self.project, cstr(\u0026next_fn)) };\n\n\t\tlet prev_matches = !previous_file.ctx.is_null()\n\t\t\t\u0026\u0026 unsafe { bridge::node::oaknode_footage_is_valid(previous_file) } != 0\n\t\t\t\u0026\u0026 self.compare_still_image_size(previous_file, width, height);\n\t\tlet next_matches = !next_file.ctx.is_null()\n\t\t\t\u0026\u0026 unsafe { bridge::node::oaknode_footage_is_valid(next_file) } != 0\n\t\t\t\u0026\u0026 self.compare_still_image_size(next_file, width, height);\n\n\t\tif prev_matches || next_matches {\n\t\t\t// Ask the user whether this is really a sequence (default: no).\n\t\t\tlet is_sequence = match self.image_sequence_confirm.as_mut() {\n\t\t\t\tSome(cb) =\u003e cb(\u0026filename, \u0026filename),\n\t\t\t\tNone =\u003e false,\n\t\t\t};\n\n\t\t\tlet start_index = get_image_sequence_limit(\u0026filename, seq_index, false, digit_count);\n\t\t\tlet end_index = get_image_sequence_limit(\u0026filename, seq_index, true, digit_count);\n\n\t\t\tfor j in start_index..=end_index {\n\t\t\t\tlet entry_fn = transform_sequence_filename(\u0026filename, j, digit_count);\n\n\t\t\t\tif is_sequence {\n\t\t\t\t\t// Remove later entries of this sequence from the import\n\t\t\t\t\t// list (they are imported as one footage). The C++\n\t\t\t\t\t// `info_list.size()` is re-evaluated after each erase.\n\t\t\t\t\tlet mut k = index + 1;\n\t\t\t\t\twhile k \u003c info_list.len() {\n\t\t\t\t\t\tif info_list[k] == entry_fn {\n\t\t\t\t\t\t\tinfo_list.remove(k);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tk += 1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tself.image_sequence_ignore_files.push(entry_fn);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif is_sequence {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::common::oakcommon_videoparams_set_video_type(video_stream, bridge::common::OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE);\n\n\t\t\t\t\tlet mut rate_buf = [0i8; 64];\n\t\t\t\t\tlet needed = bridge::common::oakcommon_config_get(std::ptr::null(), cstr(\"DefaultSequenceFrameRate\"), rate_buf.as_mut_ptr(), rate_buf.len() as i32);\n\t\t\t\t\tif needed \u003e 0 {\n\t\t\t\t\t\tlet rate = crate::project::load::buf_to_string(\u0026rate_buf);\n\t\t\t\t\t\tif let Some((num, den)) = parse_rational(\u0026rate) {\n\t\t\t\t\t\t\tif den != 0 {\n\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_set_time_base(video_stream, num, den);\n\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_set_frame_rate(video_stream, den, num);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tbridge::common::oakcommon_videoparams_set_start_time(video_stream, start_index);\n\t\t\t\t\tbridge::common::oakcommon_videoparams_set_duration(video_stream, end_index - start_index + 1);\n\t\t\t\t\tbridge::node::oaknode_footage_set_video_params(footage, 0, \u0026video_stream);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !video_stream.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut video_stream);\n\t\t\t}\n\t\t}\n\n\t\t// The probe footage was only created for comparison; remove it.\n\t\tfor probe in [previous_file, next_file] {\n\t\t\tif !probe.ctx.is_null() {\n\t\t\t\tlet mut remove = unsafe { bridge::node::oaknode_command_create_remove_node(bridge::node::oaknode_footage_as_node(probe)) };\n\t\t\t\tif !remove.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(remove);\n\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut remove);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet _ = task;\n\t}\n\n\tfn item_is_still_image_footage_only(\u0026self, footage: CHandle) -\u003e bool {\n\t\tif unsafe { bridge::node::oaknode_footage_total_stream_count(footage) } != 1 {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut vp = CHandle::null();\n\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, \u0026mut vp) } != 0 {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut video_type = 0;\n\t\tlet mut valid = 0;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_get_video_type(vp, \u0026mut video_type);\n\t\t\tbridge::common::oakcommon_videoparams_get_is_valid(vp, \u0026mut valid);\n\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut vp);\n\t\t}\n\n\t\tvalid != 0 \u0026\u0026 video_type == bridge::common::OAKCOMMON_VIDEO_TYPE_STILL\n\t}\n\n\tfn compare_still_image_size(\u0026self, footage: CHandle, width: i32, height: i32) -\u003e bool {\n\t\tif !self.item_is_still_image_footage_only(footage) {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut stream = CHandle::null();\n\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(footage, 0, \u0026mut stream) } != 0 {\n\t\t\treturn false;\n\t\t}\n\n\t\tlet mut w = 0;\n\t\tlet mut h = 0;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_get_width(stream, \u0026mut w);\n\t\t\tbridge::common::oakcommon_videoparams_get_height(stream, \u0026mut h);\n\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut stream);\n\t\t}\n\n\t\tw == width \u0026\u0026 h == height\n\t}\n}\n\nimpl TaskBehavior for ProjectImportTask {\n\t/// Import each filename via the oaknode footage/folder C ABI\n\t/// (`bridge::node`), collecting an undo command, imported footage, and\n\t/// per-file failures.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet mut command = unsafe { bridge::undo::oakundo_command_init_multi() };\n\t\tif command.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create import command\");\n\t\t\treturn Err(Error::Failed(\"Failed to create import command\".to_string()));\n\t\t}\n\t\tself.command = Some(command);\n\n\t\tlet mut counter = 0;\n\t\tlet mut entries = self.filenames.clone();\n\t\tself.import(task, self.folder, \u0026mut entries, \u0026mut counter, command);\n\n\t\tif task.is_cancelled() {\n\t\t\tif !command.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut command);\n\t\t\t\t}\n\t\t\t}\n\t\t\tself.command = None;\n\t\t\treturn Err(Error::Cancelled);\n\t\t}\n\t\tOk(())\n\t}\n}\n\n/// Count files recursively (directories recurse; anything else counts 1),\n/// mirroring `count_files_recursive` in import.cpp.\nfn count_files_recursive(paths: \u0026[String]) -\u003e usize {\n\tpaths\n\t\t.iter()\n\t\t.map(|p| {\n\t\t\tlet path = Path::new(p);\n\t\t\tif path.is_dir() {\n\t\t\t\tcount_dir_files(path)\n\t\t\t} else {\n\t\t\t\t1\n\t\t\t}\n\t\t})\n\t\t.sum()\n}\n\nfn count_dir_files(dir: \u0026Path) -\u003e usize {\n\tmatch std::fs::read_dir(dir) {\n\t\tOk(rd) =\u003e rd\n\t\t\t.filter_map(|e| e.ok())\n\t\t\t.map(|e| {\n\t\t\t\tlet p = e.path();\n\t\t\t\tif p.is_dir() {\n\t\t\t\t\tcount_dir_files(\u0026p)\n\t\t\t\t} else {\n\t\t\t\t\t1\n\t\t\t\t}\n\t\t\t})\n\t\t\t.sum(),\n\t\tErr(_) =\u003e 0,\n\t}\n}\n\n/// `std::filesystem::path(filename).filename()`, as a String.\nfn basename_of(path: \u0026str) -\u003e String {\n\tPath::new(path)\n\t\t.file_name()\n\t\t.map(|n| n.to_string_lossy().into_owned())\n\t\t.unwrap_or_default()\n}\n\n/// Two-stage read of the footage filename.\nfn footage_filename(footage: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);\n\t}\n\tcrate::project::load::buf_to_string(\u0026buf)\n}\n\n/// Substitute `number` into the trailing-digit field of an image-sequence\n/// filename (mirrors `oakcodec_decoder_transform_image_sequence_file_name`).\nfn transform_sequence_filename(filename: \u0026str, number: i64, digit_count: i32) -\u003e String {\n\tlet width = digit_count.max(0) as usize;\n\tmatch filename.rfind('.') {\n\t\tSome(dot) =\u003e {\n\t\t\t// The digits are the `width` chars immediately before the\n\t\t\t// extension; they are replaced by `number`.\n\t\t\tlet digits_start = dot.saturating_sub(width);\n\t\t\tlet prefix = \u0026filename[..digits_start];\n\t\t\tformat!(\"{prefix}{:0width$}{}\", number, \u0026filename[dot..], width = width)\n\t\t}\n\t\tNone =\u003e format!(\"{filename}{:0width$}\", number, width = width),\n\t}\n}\n\n/// Walk the index from `start` in the given direction while the transformed\n/// filename exists (mirrors `get_image_sequence_limit` in import.cpp).\nfn get_image_sequence_limit(start_fn: \u0026str, start: i64, up: bool, digit_count: i32) -\u003e i64 {\n\tlet mut current = start;\n\tloop {\n\t\tlet test_index = if up { current + 1 } else { current - 1 };\n\t\tlet test_filename = transform_sequence_filename(start_fn, test_index, digit_count);\n\t\tif !Path::new(\u0026test_filename).exists() {\n\t\t\tbreak;\n\t\t}\n\t\tcurrent = test_index;\n\t}\n\tcurrent\n}\n\n/// Parse a \"num/den\" frame-rate string.\nfn parse_rational(s: \u0026str) -\u003e Option\u003c(i32, i32)\u003e {\n\tlet mut parts = s.trim().split('/');\n\tlet num: i32 = parts.next()?.trim().parse().ok()?;\n\tlet den: i32 = parts.next()?.trim().parse().ok()?;\n\tSome((num, den))\n}\n\n/// Convenience used by the C ABI factory to compute the title.\npub(crate) fn import_title(paths: \u0026[String]) -\u003e String {\n\tlet count = count_files_recursive(paths).max(1);\n\tformat!(\"Importing {count} file(s)\")\n}\n\n/// Convenience used by the C ABI factory to compute the progress\n/// denominator (mirrors the C++ `file_count_`).\npub(crate) fn import_file_count(paths: \u0026[String]) -\u003e usize {\n\tcount_files_recursive(paths).max(1)\n}\n","traces":[{"line":63,"address":[],"length":0,"stats":{"Line":3}},{"line":64,"address":[],"length":0,"stats":{"Line":5}},{"line":65,"address":[],"length":0,"stats":{"Line":2}},{"line":67,"address":[],"length":0,"stats":{"Line":2}},{"line":73,"address":[],"length":0,"stats":{"Line":5}},{"line":74,"address":[],"length":0,"stats":{"Line":2}},{"line":75,"address":[],"length":0,"stats":{"Line":4}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":87,"address":[],"length":0,"stats":{"Line":3}},{"line":101,"address":[],"length":0,"stats":{"Line":6}},{"line":102,"address":[],"length":0,"stats":{"Line":6}},{"line":105,"address":[],"length":0,"stats":{"Line":3}},{"line":111,"address":[],"length":0,"stats":{"Line":2}},{"line":112,"address":[],"length":0,"stats":{"Line":8}},{"line":116,"address":[],"length":0,"stats":{"Line":6}},{"line":117,"address":[],"length":0,"stats":{"Line":12}},{"line":121,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":0}},{"line":127,"address":[],"length":0,"stats":{"Line":2}},{"line":128,"address":[],"length":0,"stats":{"Line":10}},{"line":129,"address":[],"length":0,"stats":{"Line":1}},{"line":130,"address":[],"length":0,"stats":{"Line":2}},{"line":132,"address":[],"length":0,"stats":{"Line":1}},{"line":136,"address":[],"length":0,"stats":{"Line":1}},{"line":140,"address":[],"length":0,"stats":{"Line":3}},{"line":141,"address":[],"length":0,"stats":{"Line":6}},{"line":145,"address":[],"length":0,"stats":{"Line":0}},{"line":146,"address":[],"length":0,"stats":{"Line":0}},{"line":150,"address":[],"length":0,"stats":{"Line":0}},{"line":151,"address":[],"length":0,"stats":{"Line":0}},{"line":155,"address":[],"length":0,"stats":{"Line":2}},{"line":156,"address":[],"length":0,"stats":{"Line":2}},{"line":159,"address":[],"length":0,"stats":{"Line":3}},{"line":160,"address":[],"length":0,"stats":{"Line":6}},{"line":161,"address":[],"length":0,"stats":{"Line":12}},{"line":162,"address":[],"length":0,"stats":{"Line":6}},{"line":163,"address":[],"length":0,"stats":{"Line":0}},{"line":166,"address":[],"length":0,"stats":{"Line":6}},{"line":168,"address":[],"length":0,"stats":{"Line":6}},{"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":175,"address":[],"length":0,"stats":{"Line":0}},{"line":178,"address":[],"length":0,"stats":{"Line":0}},{"line":179,"address":[],"length":0,"stats":{"Line":0}},{"line":180,"address":[],"length":0,"stats":{"Line":0}},{"line":183,"address":[],"length":0,"stats":{"Line":0}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":187,"address":[],"length":0,"stats":{"Line":0}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":189,"address":[],"length":0,"stats":{"Line":0}},{"line":193,"address":[],"length":0,"stats":{"Line":12}},{"line":194,"address":[],"length":0,"stats":{"Line":6}},{"line":195,"address":[],"length":0,"stats":{"Line":0}},{"line":196,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":12}},{"line":202,"address":[],"length":0,"stats":{"Line":12}},{"line":204,"address":[],"length":0,"stats":{"Line":6}},{"line":207,"address":[],"length":0,"stats":{"Line":8}},{"line":209,"address":[],"length":0,"stats":{"Line":12}},{"line":213,"address":[],"length":0,"stats":{"Line":14}},{"line":216,"address":[],"length":0,"stats":{"Line":14}},{"line":218,"address":[],"length":0,"stats":{"Line":4}},{"line":220,"address":[],"length":0,"stats":{"Line":4}},{"line":225,"address":[],"length":0,"stats":{"Line":4}},{"line":226,"address":[],"length":0,"stats":{"Line":1}},{"line":228,"address":[],"length":0,"stats":{"Line":3}},{"line":229,"address":[],"length":0,"stats":{"Line":1}},{"line":234,"address":[],"length":0,"stats":{"Line":3}},{"line":235,"address":[],"length":0,"stats":{"Line":12}},{"line":237,"address":[],"length":0,"stats":{"Line":3}},{"line":241,"address":[],"length":0,"stats":{"Line":2}},{"line":242,"address":[],"length":0,"stats":{"Line":8}},{"line":243,"address":[],"length":0,"stats":{"Line":2}},{"line":245,"address":[],"length":0,"stats":{"Line":4}},{"line":250,"address":[],"length":0,"stats":{"Line":2}},{"line":251,"address":[],"length":0,"stats":{"Line":6}},{"line":252,"address":[],"length":0,"stats":{"Line":4}},{"line":253,"address":[],"length":0,"stats":{"Line":0}},{"line":256,"address":[],"length":0,"stats":{"Line":8}},{"line":257,"address":[],"length":0,"stats":{"Line":2}},{"line":258,"address":[],"length":0,"stats":{"Line":1}},{"line":261,"address":[],"length":0,"stats":{"Line":2}},{"line":262,"address":[],"length":0,"stats":{"Line":0}},{"line":265,"address":[],"length":0,"stats":{"Line":2}},{"line":266,"address":[],"length":0,"stats":{"Line":0}},{"line":269,"address":[],"length":0,"stats":{"Line":2}},{"line":270,"address":[],"length":0,"stats":{"Line":2}},{"line":271,"address":[],"length":0,"stats":{"Line":0}},{"line":274,"address":[],"length":0,"stats":{"Line":2}},{"line":275,"address":[],"length":0,"stats":{"Line":2}},{"line":277,"address":[],"length":0,"stats":{"Line":4}},{"line":278,"address":[],"length":0,"stats":{"Line":2}},{"line":281,"address":[],"length":0,"stats":{"Line":4}},{"line":283,"address":[],"length":0,"stats":{"Line":5}},{"line":284,"address":[],"length":0,"stats":{"Line":5}},{"line":286,"address":[],"length":0,"stats":{"Line":5}},{"line":287,"address":[],"length":0,"stats":{"Line":5}},{"line":289,"address":[],"length":0,"stats":{"Line":2}},{"line":290,"address":[],"length":0,"stats":{"Line":1}},{"line":291,"address":[],"length":0,"stats":{"Line":5}},{"line":292,"address":[],"length":0,"stats":{"Line":2}},{"line":293,"address":[],"length":0,"stats":{"Line":1}},{"line":294,"address":[],"length":0,"stats":{"Line":5}},{"line":296,"address":[],"length":0,"stats":{"Line":1}},{"line":298,"address":[],"length":0,"stats":{"Line":2}},{"line":299,"address":[],"length":0,"stats":{"Line":3}},{"line":300,"address":[],"length":0,"stats":{"Line":0}},{"line":303,"address":[],"length":0,"stats":{"Line":5}},{"line":304,"address":[],"length":0,"stats":{"Line":5}},{"line":306,"address":[],"length":0,"stats":{"Line":4}},{"line":307,"address":[],"length":0,"stats":{"Line":15}},{"line":309,"address":[],"length":0,"stats":{"Line":3}},{"line":313,"address":[],"length":0,"stats":{"Line":6}},{"line":314,"address":[],"length":0,"stats":{"Line":10}},{"line":315,"address":[],"length":0,"stats":{"Line":4}},{"line":316,"address":[],"length":0,"stats":{"Line":6}},{"line":317,"address":[],"length":0,"stats":{"Line":2}},{"line":319,"address":[],"length":0,"stats":{"Line":2}},{"line":322,"address":[],"length":0,"stats":{"Line":0}},{"line":326,"address":[],"length":0,"stats":{"Line":1}},{"line":328,"address":[],"length":0,"stats":{"Line":2}},{"line":330,"address":[],"length":0,"stats":{"Line":2}},{"line":331,"address":[],"length":0,"stats":{"Line":8}},{"line":332,"address":[],"length":0,"stats":{"Line":1}},{"line":333,"address":[],"length":0,"stats":{"Line":3}},{"line":334,"address":[],"length":0,"stats":{"Line":3}},{"line":335,"address":[],"length":0,"stats":{"Line":2}},{"line":336,"address":[],"length":0,"stats":{"Line":5}},{"line":337,"address":[],"length":0,"stats":{"Line":3}},{"line":342,"address":[],"length":0,"stats":{"Line":3}},{"line":343,"address":[],"length":0,"stats":{"Line":3}},{"line":344,"address":[],"length":0,"stats":{"Line":3}},{"line":349,"address":[],"length":0,"stats":{"Line":1}},{"line":351,"address":[],"length":0,"stats":{"Line":1}},{"line":356,"address":[],"length":0,"stats":{"Line":3}},{"line":357,"address":[],"length":0,"stats":{"Line":2}},{"line":358,"address":[],"length":0,"stats":{"Line":8}},{"line":359,"address":[],"length":0,"stats":{"Line":2}},{"line":361,"address":[],"length":0,"stats":{"Line":6}},{"line":362,"address":[],"length":0,"stats":{"Line":2}},{"line":368,"address":[],"length":0,"stats":{"Line":1}},{"line":371,"address":[],"length":0,"stats":{"Line":3}},{"line":372,"address":[],"length":0,"stats":{"Line":3}},{"line":373,"address":[],"length":0,"stats":{"Line":0}},{"line":376,"address":[],"length":0,"stats":{"Line":6}},{"line":377,"address":[],"length":0,"stats":{"Line":6}},{"line":378,"address":[],"length":0,"stats":{"Line":0}},{"line":381,"address":[],"length":0,"stats":{"Line":6}},{"line":382,"address":[],"length":0,"stats":{"Line":6}},{"line":384,"address":[],"length":0,"stats":{"Line":12}},{"line":385,"address":[],"length":0,"stats":{"Line":12}},{"line":386,"address":[],"length":0,"stats":{"Line":3}},{"line":389,"address":[],"length":0,"stats":{"Line":6}},{"line":392,"address":[],"length":0,"stats":{"Line":2}},{"line":393,"address":[],"length":0,"stats":{"Line":4}},{"line":394,"address":[],"length":0,"stats":{"Line":0}},{"line":397,"address":[],"length":0,"stats":{"Line":4}},{"line":398,"address":[],"length":0,"stats":{"Line":4}},{"line":399,"address":[],"length":0,"stats":{"Line":0}},{"line":402,"address":[],"length":0,"stats":{"Line":4}},{"line":403,"address":[],"length":0,"stats":{"Line":4}},{"line":405,"address":[],"length":0,"stats":{"Line":8}},{"line":406,"address":[],"length":0,"stats":{"Line":8}},{"line":407,"address":[],"length":0,"stats":{"Line":2}},{"line":410,"address":[],"length":0,"stats":{"Line":4}},{"line":418,"address":[],"length":0,"stats":{"Line":3}},{"line":419,"address":[],"length":0,"stats":{"Line":6}},{"line":420,"address":[],"length":0,"stats":{"Line":6}},{"line":421,"address":[],"length":0,"stats":{"Line":0}},{"line":422,"address":[],"length":0,"stats":{"Line":0}},{"line":424,"address":[],"length":0,"stats":{"Line":3}},{"line":426,"address":[],"length":0,"stats":{"Line":6}},{"line":427,"address":[],"length":0,"stats":{"Line":9}},{"line":428,"address":[],"length":0,"stats":{"Line":21}},{"line":430,"address":[],"length":0,"stats":{"Line":6}},{"line":431,"address":[],"length":0,"stats":{"Line":0}},{"line":433,"address":[],"length":0,"stats":{"Line":0}},{"line":436,"address":[],"length":0,"stats":{"Line":0}},{"line":437,"address":[],"length":0,"stats":{"Line":0}},{"line":439,"address":[],"length":0,"stats":{"Line":3}},{"line":445,"address":[],"length":0,"stats":{"Line":6}},{"line":446,"address":[],"length":0,"stats":{"Line":6}},{"line":448,"address":[],"length":0,"stats":{"Line":16}},{"line":449,"address":[],"length":0,"stats":{"Line":30}},{"line":450,"address":[],"length":0,"stats":{"Line":20}},{"line":451,"address":[],"length":0,"stats":{"Line":0}},{"line":453,"address":[],"length":0,"stats":{"Line":10}},{"line":459,"address":[],"length":0,"stats":{"Line":0}},{"line":460,"address":[],"length":0,"stats":{"Line":0}},{"line":461,"address":[],"length":0,"stats":{"Line":0}},{"line":462,"address":[],"length":0,"stats":{"Line":0}},{"line":463,"address":[],"length":0,"stats":{"Line":0}},{"line":464,"address":[],"length":0,"stats":{"Line":0}},{"line":465,"address":[],"length":0,"stats":{"Line":0}},{"line":466,"address":[],"length":0,"stats":{"Line":0}},{"line":468,"address":[],"length":0,"stats":{"Line":0}},{"line":472,"address":[],"length":0,"stats":{"Line":0}},{"line":477,"address":[],"length":0,"stats":{"Line":2}},{"line":478,"address":[],"length":0,"stats":{"Line":2}},{"line":480,"address":[],"length":0,"stats":{"Line":8}},{"line":485,"address":[],"length":0,"stats":{"Line":2}},{"line":486,"address":[],"length":0,"stats":{"Line":8}},{"line":487,"address":[],"length":0,"stats":{"Line":2}},{"line":488,"address":[],"length":0,"stats":{"Line":0}},{"line":490,"address":[],"length":0,"stats":{"Line":6}},{"line":492,"address":[],"length":0,"stats":{"Line":8}},{"line":494,"address":[],"length":0,"stats":{"Line":4}},{"line":499,"address":[],"length":0,"stats":{"Line":9}},{"line":500,"address":[],"length":0,"stats":{"Line":18}},{"line":501,"address":[],"length":0,"stats":{"Line":9}},{"line":502,"address":[],"length":0,"stats":{"Line":9}},{"line":505,"address":[],"length":0,"stats":{"Line":36}},{"line":506,"address":[],"length":0,"stats":{"Line":18}},{"line":507,"address":[],"length":0,"stats":{"Line":18}},{"line":509,"address":[],"length":0,"stats":{"Line":0}},{"line":515,"address":[],"length":0,"stats":{"Line":2}},{"line":516,"address":[],"length":0,"stats":{"Line":4}},{"line":518,"address":[],"length":0,"stats":{"Line":12}},{"line":519,"address":[],"length":0,"stats":{"Line":20}},{"line":520,"address":[],"length":0,"stats":{"Line":4}},{"line":521,"address":[],"length":0,"stats":{"Line":2}},{"line":523,"address":[],"length":0,"stats":{"Line":2}},{"line":525,"address":[],"length":0,"stats":{"Line":2}},{"line":529,"address":[],"length":0,"stats":{"Line":1}},{"line":530,"address":[],"length":0,"stats":{"Line":3}},{"line":531,"address":[],"length":0,"stats":{"Line":7}},{"line":532,"address":[],"length":0,"stats":{"Line":7}},{"line":533,"address":[],"length":0,"stats":{"Line":1}},{"line":537,"address":[],"length":0,"stats":{"Line":3}},{"line":538,"address":[],"length":0,"stats":{"Line":12}},{"line":539,"address":[],"length":0,"stats":{"Line":3}},{"line":544,"address":[],"length":0,"stats":{"Line":3}},{"line":545,"address":[],"length":0,"stats":{"Line":9}}],"covered":184,"coverable":235},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","load.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//! `ProjectLoadBaseTask` / `ProjectLoadTask`, mirroring\n//! `src/task/src/project/load/load.h`.\n//!\n//! Loads an `.oakproj` file into a new `OakNodeProject`. The base task holds\n//! the filename and produces the project on `take_project()`; `ProjectLoadTask`\n//! is the concrete (OTIO-less) loader.\n//!\n//! CPP-PARITY: src/task/src/project/load/load.h\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// Serializer result codes (`include/node/serializer.h`).\nconst OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0;\nconst OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD: i32 = 1;\nconst OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW: i32 = 2;\nconst OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION: i32 = 3;\nconst OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4;\nconst OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5;\nconst OAKNODE_SERIALIZER_RESULT_NO_DATA: i32 = 7;\n\n/// The base project-load task: owns a source filename and yields a loaded\n/// project via [`ProjectLoadBaseTask::take_project`].\npub struct ProjectLoadBaseTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Absolute project filename to load.\n\tpub filename: String,\n\t/// The loaded project, produced by the task and taken via `take_project`.\n\tloaded_project: Option\u003cCHandle\u003e,\n}\n\nimpl Drop for ProjectLoadBaseTask {\n\tfn drop(\u0026mut self) {\n\t\t// Free the loaded project if it was never taken.\n\t\tif let Some(project) = \u0026mut self.loaded_project {\n\t\t\tif !project.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_project_free(project);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl ProjectLoadBaseTask {\n\t/// Create a base load task for the given filename.\n\tpub fn new(base: Task, filename: String) -\u003e ProjectLoadBaseTask {\n\t\tProjectLoadBaseTask {\n\t\t\tbase,\n\t\t\tfilename,\n\t\t\tloaded_project: None,\n\t\t}\n\t}\n\n\t/// Take ownership of the loaded project. Returns `Err(Error::State)` if\n\t/// the task has not loaded a project yet.\n\tpub fn take_project(\u0026mut self) -\u003e Result\u003cCHandle\u003e {\n\t\tself.loaded_project.take().ok_or(Error::State)\n\t}\n\n\t/// Store the loaded project (called by `run`).\n\tpub(crate) fn store_project(\u0026mut self, project: CHandle) {\n\t\tself.loaded_project = Some(project);\n\t}\n}\n\nimpl TaskBehavior for ProjectLoadBaseTask {\n\t/// Load the project from `filename` via the oaknode serializer\n\t/// (`bridge::node`), storing the resulting `OakNodeProject`.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet mut project = unsafe { bridge::node::oaknode_project_init() };\n\t\tif project.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create project\");\n\t\t\treturn Err(Error::Failed(\"Failed to create project\".to_string()));\n\t\t}\n\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_project_set_filename(project, cstr(\u0026self.filename));\n\t\t}\n\n\t\tlet mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;\n\t\tlet mut details = [0i8; 512];\n\t\tlet result = unsafe {\n\t\t\tbridge::node::oaknode_serializer_load_from_file(\n\t\t\t\tproject,\n\t\t\t\tcstr(\u0026self.filename),\n\t\t\t\t\u0026mut code,\n\t\t\t\tdetails.as_mut_ptr(),\n\t\t\t\tdetails.len() as i32,\n\t\t\t)\n\t\t};\n\n\t\tlet mut success = false;\n\t\tmatch code {\n\t\t\tOAKNODE_SERIALIZER_RESULT_SUCCESS =\u003e success = true,\n\t\t\tOAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD =\u003e {\n\t\t\t\ttask.set_error(\"This project is from a version of Oak Video Editor that is no longer supported in this version.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW =\u003e {\n\t\t\t\ttask.set_error(\"This project is from a newer version of Oak Video Editor and cannot be opened in this version.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION =\u003e {\n\t\t\t\ttask.set_error(\"Failed to determine project version.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_FILE_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\"Failed to read file \\\"{}\\\" for reading.\", self.filename));\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_XML_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Failed to read XML document. File may be corrupt. Error was: {}\",\n\t\t\t\t\tbuf_to_string(\u0026details)\n\t\t\t\t));\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_NO_DATA =\u003e {\n\t\t\t\ttask.set_error(\"Failed to find any data to parse.\");\n\t\t\t}\n\t\t\t_ =\u003e task.set_error(\"Unknown error.\"),\n\t\t}\n\n\t\tif result == 0 \u0026\u0026 success {\n\t\t\tself.store_project(project);\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tif !project.ctx.is_null() {\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_project_free(\u0026mut project);\n\t\t\t}\n\t\t}\n\t\tErr(Error::Failed(\"Failed to load project\".to_string()))\n\t}\n}\n\n/// The concrete native project loader (no OTIO path).\npub struct ProjectLoadTask {\n\t/// The shared task base.\n\tpub base: ProjectLoadBaseTask,\n}\n\nimpl TaskBehavior for ProjectLoadTask {\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t\u003cProjectLoadBaseTask as TaskBehavior\u003e::run(\u0026mut self.base, task)\n\t}\n}\n\n/// Read a NUL-terminated char buffer into a String (lossy).\npub(crate) fn buf_to_string(buf: \u0026[i8]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n","traces":[{"line":53,"address":[],"length":0,"stats":{"Line":13}},{"line":55,"address":[],"length":0,"stats":{"Line":14}},{"line":56,"address":[],"length":0,"stats":{"Line":1}},{"line":58,"address":[],"length":0,"stats":{"Line":1}},{"line":67,"address":[],"length":0,"stats":{"Line":13}},{"line":77,"address":[],"length":0,"stats":{"Line":13}},{"line":78,"address":[],"length":0,"stats":{"Line":52}},{"line":82,"address":[],"length":0,"stats":{"Line":8}},{"line":83,"address":[],"length":0,"stats":{"Line":8}},{"line":90,"address":[],"length":0,"stats":{"Line":2}},{"line":91,"address":[],"length":0,"stats":{"Line":4}},{"line":92,"address":[],"length":0,"stats":{"Line":4}},{"line":93,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":6}},{"line":101,"address":[],"length":0,"stats":{"Line":4}},{"line":102,"address":[],"length":0,"stats":{"Line":4}},{"line":105,"address":[],"length":0,"stats":{"Line":2}},{"line":106,"address":[],"length":0,"stats":{"Line":4}},{"line":107,"address":[],"length":0,"stats":{"Line":2}},{"line":108,"address":[],"length":0,"stats":{"Line":4}},{"line":109,"address":[],"length":0,"stats":{"Line":2}},{"line":113,"address":[],"length":0,"stats":{"Line":4}},{"line":114,"address":[],"length":0,"stats":{"Line":2}},{"line":115,"address":[],"length":0,"stats":{"Line":1}},{"line":116,"address":[],"length":0,"stats":{"Line":0}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":119,"address":[],"length":0,"stats":{"Line":0}},{"line":120,"address":[],"length":0,"stats":{"Line":0}},{"line":122,"address":[],"length":0,"stats":{"Line":0}},{"line":123,"address":[],"length":0,"stats":{"Line":0}},{"line":125,"address":[],"length":0,"stats":{"Line":1}},{"line":126,"address":[],"length":0,"stats":{"Line":2}},{"line":128,"address":[],"length":0,"stats":{"Line":0}},{"line":129,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":0}},{"line":131,"address":[],"length":0,"stats":{"Line":0}},{"line":134,"address":[],"length":0,"stats":{"Line":0}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":4}},{"line":141,"address":[],"length":0,"stats":{"Line":3}},{"line":142,"address":[],"length":0,"stats":{"Line":1}},{"line":145,"address":[],"length":0,"stats":{"Line":1}},{"line":147,"address":[],"length":0,"stats":{"Line":1}},{"line":150,"address":[],"length":0,"stats":{"Line":1}},{"line":161,"address":[],"length":0,"stats":{"Line":2}},{"line":162,"address":[],"length":0,"stats":{"Line":6}},{"line":167,"address":[],"length":0,"stats":{"Line":72}},{"line":168,"address":[],"length":0,"stats":{"Line":1867}},{"line":169,"address":[],"length":0,"stats":{"Line":288}}],"covered":36,"coverable":51},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","loadotio.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//! `LoadOTIOTask`, mirroring `src/task/src/project/loadotio/loadotio.h`.\n//!\n//! Loads an OpenTimelineIO (`.otio`) or FCPXML (`.fcpxml`) file into a new\n//! `OakNodeProject`, with a configurable import-confirmation callback. The\n//! format is dispatched from the filename extension (see\n//! [`crate::project::format`]); the document is parsed with the pure-Rust\n//! `oakotio` binding (see `README` decision #6) and the project is built\n//! through the oaknode / oaktimeline C ABIs exactly like the C++ task, so\n//! no OTIO or FCPXML type crosses the oaktask C ABI.\n//!\n//! CPP-PARITY: src/task/src/project/loadotio/loadotio.cpp\n\nuse std::collections::HashMap;\nuse std::path::Path;\nuse std::sync::Mutex;\n\nuse oakotio::Serializable;\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::project::format::InterchangeFormat;\nuse crate::project::load::ProjectLoadBaseTask;\nuse crate::task::{Task, TaskBehavior};\n\n/// Callback used to confirm whether the sequences of an OTIO document should\n/// be imported, mirroring `ImportConfirmFn` in loadotio.h (receives the\n/// sequence labels in order; return true to accept).\npub type ImportConfirmFn = Box\u003cdyn FnMut(\u0026[String]) -\u003e bool + Send\u003e;\n\n/// Global import-confirmation callback, mirroring the C++ static\n/// `LoadOTIOTask::confirm_callback_` (loadotio.h). When unset, everything is\n/// imported (the headless default).\nstatic CONFIRM_CALLBACK: Mutex\u003cOption\u003cImportConfirmFn\u003e\u003e = Mutex::new(None);\n\n/// Register (or clear, with `None`) the global import-confirmation callback.\n///\n/// CPP-PARITY: src/task/src/project/loadotio/loadotio.h\n/// (`LoadOTIOTask::set_import_confirm_callback`)\npub fn set_import_confirm_callback(cb: Option\u003cImportConfirmFn\u003e) {\n\t*CONFIRM_CALLBACK.lock().unwrap() = cb;\n}\n\n/// An OTIO project loader.\npub struct LoadOTIOTask {\n\t/// The base load task (owns the filename and yields the project).\n\tpub base: ProjectLoadBaseTask,\n}\n\nimpl LoadOTIOTask {\n\t/// Create an OTIO loader for the given base load task.\n\tpub fn new(base: ProjectLoadBaseTask) -\u003e LoadOTIOTask {\n\t\tLoadOTIOTask { base }\n\t}\n}\n\nimpl TaskBehavior for LoadOTIOTask {\n\t/// Load and parse the document (`oakotio`), then convert it into the\n\t/// project stored on the base task.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Dispatch the interchange format from the filename extension.\n\t\t// Format handling ends here: `parse_timelines` returns one\n\t\t// `oakotio::Timeline` per sequence whatever the source format, so\n\t\t// the project building below is shared between `.otio` and\n\t\t// `.fcpxml` (C++ parity for the track/clip/footage code).\n\t\tlet format = match InterchangeFormat::of(\u0026self.base.filename) {\n\t\t\tOk(f) =\u003e f,\n\t\t\tErr(e) =\u003e {\n\t\t\t\tif let Error::Failed(msg) = \u0026e {\n\t\t\t\t\ttask.set_error(msg);\n\t\t\t\t}\n\t\t\t\treturn Err(e);\n\t\t\t}\n\t\t};\n\n\t\tlet timelines = parse_timelines(task, \u0026self.base.filename, format)?;\n\n\t\tlet mut project = unsafe { bridge::node::oaknode_project_init() };\n\t\tif project.ctx.is_null() {\n\t\t\ttask.set_error(\"Failed to create project\");\n\t\t\treturn Err(Error::Failed(\"Failed to create project\".to_string()));\n\t\t}\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_project_initialize(project);\n\t\t\tbridge::node::oaknode_project_set_modified(project, 1);\n\t\t}\n\n\t\t// Keep track of imported footage\n\t\tlet mut imported_footage: HashMap\u003cString, CHandle\u003e = HashMap::new();\n\n\t\t// Generate a list of sequences with the same names as the timelines.\n\t\t// Assumes each timeline has a unique name.\n\t\tlet mut unnamed_sequence_count = 0;\n\t\tlet mut sequences: Vec\u003cCHandle\u003e = Vec::new();\n\n\t\t// Variables used for loading bar\n\t\tlet mut number_of_clips: f64 = 0.0;\n\n\t\tfor timeline in \u0026timelines {\n\t\t\tlet sequence = unsafe { bridge::node::oaknode_sequence_create() };\n\t\t\tif sequence.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet label = if !timeline.name().is_empty() {\n\t\t\t\ttimeline.name().to_string()\n\t\t\t} else {\n\t\t\t\t// If the otio timeline does not provide a name, create a\n\t\t\t\t// default one here.\n\t\t\t\tunnamed_sequence_count += 1;\n\t\t\t\tformat!(\"Sequence {unnamed_sequence_count}\")\n\t\t\t};\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_node_set_label(bridge::node::oaknode_sequence_as_node(sequence), cstr(\u0026label));\n\t\t\t}\n\n\t\t\t// Set default params incase they aren't edited.\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_sequence_set_default_parameters(sequence);\n\t\t\t}\n\n\t\t\t// Get number of clips for loading bar\n\t\t\tfor track in timeline.tracks().children() {\n\t\t\t\tif let Some(otio_track) = track.as_track() {\n\t\t\t\t\tnumber_of_clips += otio_track.children().len() as f64;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsequences.push(sequence);\n\t\t}\n\t\tif number_of_clips \u003c= 0.0 {\n\t\t\tnumber_of_clips = 1.0;\n\t\t}\n\n\t\t// Ask the user which sequences to import (facade callback; headless\n\t\t// default accepts everything).\n\t\tlet sequence_names: Vec\u003cString\u003e = sequences\n\t\t\t.iter()\n\t\t\t.map(|s| node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*s) }))\n\t\t\t.collect();\n\t\tlet mut confirm = CONFIRM_CALLBACK.lock().unwrap().take();\n\t\tlet accepted = match confirm.as_mut() {\n\t\t\tSome(cb) =\u003e cb(\u0026sequence_names),\n\t\t\tNone =\u003e true,\n\t\t};\n\n\t\tif !accepted {\n\t\t\t// Cancel to indicate to caller that this task did not complete\n\t\t\t// and to simply dispose of it. The project is never handed to the\n\t\t\t// base task, so free it here (the C++ base-task destructor does\n\t\t\t// the same).\n\t\t\ttask.cancel();\n\t\t\tfor sequence in \u0026mut sequences {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_sequence_free(sequence);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !project.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_project_free(\u0026mut project);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Ok(());\n\t\t}\n\n\t\tlet root_folder = unsafe { bridge::node::oaknode_project_root(project) };\n\t\tlet mut clips_done = 0.0f64;\n\n\t\tfor (timeline, sequence) in timelines.iter().zip(\u0026sequences) {\n\t\t\tlet sequence_node = unsafe { bridge::node::oaknode_sequence_as_node(*sequence) };\n\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_project_add_node(project, sequence_node);\n\t\t\t}\n\t\t\tlet mut add_seq = unsafe { bridge::node::oaknode_command_create_folder_add_child(root_folder, sequence_node) };\n\t\t\tif !add_seq.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_seq);\n\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_seq);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Create a folder for this sequence's footage\n\t\t\tlet sequence_footage = unsafe { bridge::node::oaknode_folder_create(project) };\n\t\t\tif !sequence_footage.ctx.is_null() {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_node_set_label(\n\t\t\t\t\t\tbridge::node::oaknode_folder_as_node(sequence_footage),\n\t\t\t\t\t\tcstr(timeline.name()),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tlet mut add_folder = unsafe {\n\t\t\t\t\tbridge::node::oaknode_command_create_folder_add_child(\n\t\t\t\t\t\troot_folder,\n\t\t\t\t\t\tbridge::node::oaknode_folder_as_node(sequence_footage),\n\t\t\t\t\t)\n\t\t\t\t};\n\t\t\t\tif !add_folder.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_folder);\n\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_folder);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Iterate through tracks\n\t\t\tfor track_composable in timeline.tracks().children() {\n\t\t\t\tlet Some(otio_track) = track_composable.as_track() else {\n\t\t\t\t\tcontinue;\n\t\t\t\t};\n\n\t\t\t\t// Determine what kind of track it is\n\t\t\t\tlet track_type = match otio_track.kind() {\n\t\t\t\t\t\"Video\" =\u003e bridge::node::OAKNODE_TRACK_TYPE_VIDEO,\n\t\t\t\t\t\"Audio\" =\u003e bridge::node::OAKNODE_TRACK_TYPE_AUDIO,\n\t\t\t\t\tother =\u003e {\n\t\t\t\t\t\teprintln!(\"Found unknown track type: {other}\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\t// Create a new track\n\t\t\t\tlet mut track_list = CHandle::null();\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_sequence_get_track_list(*sequence, track_type, \u0026mut track_list);\n\t\t\t\t}\n\t\t\t\tlet mut add_track = unsafe { bridge::timeline::oaktimeline_add_track_command(track_list) };\n\t\t\t\tif !add_track.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_track);\n\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_track);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet mut track = CHandle::null();\n\t\t\t\tlet mut count = 0;\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::node::oaknode_tracklist_get_track_count(track_list, \u0026mut count);\n\t\t\t\t}\n\t\t\t\tif count \u003e 0 {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_tracklist_get_track_at(track_list, count - 1, \u0026mut track);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif track.ctx.is_null() {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Get clips from track\n\t\t\t\tlet mut previous_block = CHandle::null();\n\t\t\t\tlet mut prev_block_transition = false;\n\n\t\t\t\tfor otio_block in otio_track.children() {\n\t\t\t\t\tif task.is_cancelled() {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet block = match otio_block.schema_name() {\n\t\t\t\t\t\t\"Clip\" =\u003e unsafe { bridge::node::oaknode_block_clip_create() },\n\t\t\t\t\t\t\"Gap\" =\u003e unsafe { bridge::node::oaknode_block_gap_create() },\n\t\t\t\t\t\t\"Transition\" =\u003e {\n\t\t\t\t\t\t\t// Todo: Look into OTIO supported transitions and add\n\t\t\t\t\t\t\t// them to Oak.\n\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\tbridge::node::oaknode_block_transition_create(bridge::node::OAKNODE_TRANSITION_CROSS_DISSOLVE)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tother =\u003e {\n\t\t\t\t\t\t\t// We don't know what this is yet, just create a gap\n\t\t\t\t\t\t\t// for now so that *something* is there.\n\t\t\t\t\t\t\teprintln!(\"Found unknown block type: {other}\");\n\t\t\t\t\t\t\tunsafe { bridge::node::oaknode_block_gap_create() }\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\tif block.ctx.is_null() {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet block_node = unsafe { bridge::node::oaknode_block_as_node(block) };\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_project_add_node(project, block_node);\n\t\t\t\t\t\tbridge::node::oaknode_node_set_label(block_node, cstr(otio_block.name()));\n\t\t\t\t\t\tbridge::node::oaknode_track_append_block(track, block);\n\t\t\t\t\t}\n\n\t\t\t\t\tif otio_block.schema_name() == \"Clip\" || otio_block.schema_name() == \"Gap\" {\n\t\t\t\t\t\tif let Some(source_range) = otio_block.source_range() {\n\t\t\t\t\t\t\tlet start_seconds = source_range.start_time().to_seconds();\n\t\t\t\t\t\t\tlet duration_seconds = source_range.duration().to_seconds();\n\n\t\t\t\t\t\t\tlet start_time = oakcore_rs::Rational::from_double(start_seconds);\n\t\t\t\t\t\t\tlet duration = oakcore_rs::Rational::from_double(duration_seconds);\n\n\t\t\t\t\t\t\tif otio_block.schema_name() == \"Clip\" {\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\tbridge::node::oaknode_clip_set_media_in(\n\t\t\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\t\t\tstart_time.numerator() as i32,\n\t\t\t\t\t\t\t\t\t\tstart_time.denominator() as i32,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\tbridge::node::oaknode_block_set_length_and_media_out(\n\t\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\t\tduration.numerator() as i32,\n\t\t\t\t\t\t\t\t\tduration.denominator() as i32,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// If the previous block was a transition, connect the\n\t\t\t\t\t// current block to it.\n\t\t\t\t\tif prev_block_transition {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\tblock_node,\n\t\t\t\t\t\t\t\tbridge::node::oaknode_block_as_node(previous_block),\n\t\t\t\t\t\t\t\tcstr(bridge::node::OAKNODE_TRANSITION_IN_BLOCK_INPUT),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprev_block_transition = false;\n\t\t\t\t\t}\n\n\t\t\t\t\tif let Some(otio_transition) = otio_block.as_transition() {\n\t\t\t\t\t\t// Set how far the transition eats into the previous\n\t\t\t\t\t\t// clip.\n\t\t\t\t\t\tlet in_offset = otio_transition.in_offset().to_rational();\n\t\t\t\t\t\tlet out_offset = otio_transition.out_offset().to_rational();\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::node::oaknode_transition_set_offsets_and_length(\n\t\t\t\t\t\t\t\tblock,\n\t\t\t\t\t\t\t\tin_offset.numerator() as i32,\n\t\t\t\t\t\t\t\tin_offset.denominator() as i32,\n\t\t\t\t\t\t\t\tout_offset.numerator() as i32,\n\t\t\t\t\t\t\t\tout_offset.denominator() as i32,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !previous_block.ctx.is_null() {\n\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\t\tbridge::node::oaknode_block_as_node(previous_block),\n\t\t\t\t\t\t\t\t\tblock_node,\n\t\t\t\t\t\t\t\t\tcstr(bridge::node::OAKNODE_TRANSITION_OUT_BLOCK_INPUT),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprev_block_transition = true;\n\n\t\t\t\t\t\t// Position transition in its own context.\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tset_own_context_position(block_node);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif otio_block.schema_name() == \"Gap\" {\n\t\t\t\t\t\t// Position gap in its own context.\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tset_own_context_position(block_node);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Update this after it's used but before any continue\n\t\t\t\t\t// statements.\n\t\t\t\t\tprevious_block = block;\n\n\t\t\t\t\tif otio_block.schema_name() == \"Clip\" {\n\t\t\t\t\t\tlet Some(otio_clip) = otio_block.as_clip() else {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tlet Some(media_reference) = otio_clip.media_reference() else {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif let Some(external) = media_reference.as_external_reference() {\n\t\t\t\t\t\t\t// Link footage\n\t\t\t\t\t\t\tlet footage_url = external.target_url().to_string();\n\n\t\t\t\t\t\t\tlet probed_item = if let Some(existing) = imported_footage.get(\u0026footage_url) {\n\t\t\t\t\t\t\t\t*existing\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlet created = unsafe { bridge::node::oaknode_footage_create(project, cstr(\u0026footage_url)) };\n\t\t\t\t\t\t\t\tif !created.ctx.is_null() {\n\t\t\t\t\t\t\t\t\timported_footage.insert(footage_url.clone(), created);\n\n\t\t\t\t\t\t\t\t\tlet label = Path::new(\u0026footage_url)\n\t\t\t\t\t\t\t\t\t\t.file_name()\n\t\t\t\t\t\t\t\t\t\t.map(|n| n.to_string_lossy().into_owned())\n\t\t\t\t\t\t\t\t\t\t.unwrap_or_default();\n\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_label(\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(created),\n\t\t\t\t\t\t\t\t\t\t\tcstr(\u0026label),\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif !sequence_footage.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\tlet mut add_footage = unsafe {\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_command_create_folder_add_child(\n\t\t\t\t\t\t\t\t\t\t\t\tsequence_footage,\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(created),\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\tif !add_footage.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::undo::oakundo_command_redo_now(add_footage);\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::undo::oakundo_command_free(\u0026mut add_footage);\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcreated\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\tif !probed_item.ctx.is_null() {\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t// Position clip in its own context.\n\t\t\t\t\t\t\t\t\tset_own_context_position(block_node);\n\n\t\t\t\t\t\t\t\t\t// Position footage in its context.\n\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_context_position(\n\t\t\t\t\t\t\t\t\t\tblock_node,\n\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(probed_item),\n\t\t\t\t\t\t\t\t\t\t-2.0,\n\t\t\t\t\t\t\t\t\t\t0.0,\n\t\t\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {\n\t\t\t\t\t\t\t\t\tlet transform = unsafe {\n\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_TRANSFORM))\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\tif !transform.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_project_add_node(project, transform);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(probed_item),\n\t\t\t\t\t\t\t\t\t\t\t\ttransform,\n\t\t\t\t\t\t\t\t\t\t\t\tcstr(\"tex_in\"),\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(transform, block_node, cstr(\"buffer_in\"));\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_context_position(block_node, transform, -1.0, 0.0, 0);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet volume =\n\t\t\t\t\t\t\t\t\t\tunsafe { bridge::node::oaknode_factory_create_from_id(cstr(bridge::node::OAKNODE_TYPE_VOLUME)) };\n\t\t\t\t\t\t\t\t\tif !volume.ctx.is_null() {\n\t\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_project_add_node(project, volume);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(\n\t\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_footage_as_node(probed_item),\n\t\t\t\t\t\t\t\t\t\t\t\tvolume,\n\t\t\t\t\t\t\t\t\t\t\t\tcstr(\"samples_in\"),\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_connect(volume, block_node, cstr(\"buffer_in\"));\n\t\t\t\t\t\t\t\t\t\t\tbridge::node::oaknode_node_set_context_position(block_node, volume, -1.0, 0.0, 0);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tclips_done += 1.0;\n\t\t\t\t\ttask.emit_progress(clips_done / number_of_clips);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tself.base.store_project(project);\n\t\tOk(())\n\t}\n}\n\n/// Set the node's position in its own context (the C++\n/// `set_own_context_position` helper in loadotio.cpp).\nunsafe fn set_own_context_position(node: CHandle) {\n\tunsafe {\n\t\tbridge::node::oaknode_node_set_context_position(node, node, 0.0, 0.0, 0);\n\t}\n}\n\n/// Parse the document into one `oakotio::Timeline` per sequence, dispatching\n/// on the filename extension:\n///\n/// - `.otio`: OpenTimelineIO JSON. A `Timeline` root yields one timeline, a\n/// `SerializableCollection` yields one timeline per `Timeline` child, and\n/// any other root is the C++ \"Unknown OpenTimelineIO root element\" error.\n/// - `.fcpxml`: FCPXML (`oakotio::fcpxml`), one timeline per `\u003csequence\u003e`.\n///\n/// Format handling ends here — the caller builds the project from the\n/// returned timelines regardless of the source format (C++ parity), so the\n/// track/clip/footage code never forks.\nfn parse_timelines(task: \u0026mut Task, filename: \u0026str, format: InterchangeFormat) -\u003e Result\u003cVec\u003coakotio::Timeline\u003e\u003e {\n\tmatch format {\n\t\tInterchangeFormat::OtioJson =\u003e {\n\t\t\tlet root = oakotio::from_json_file(filename).map_err(|e| {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Failed to load OpenTimelineIO from file \\\"{}\\\" \\n\\nOpenTimelineIO Error:\\n\\n{}\",\n\t\t\t\t\tfilename, e\n\t\t\t\t));\n\t\t\t\tError::Failed(\"Failed to load OpenTimelineIO\".to_string())\n\t\t\t})?;\n\t\t\tmatch \u0026root {\n\t\t\t\tSerializable::SerializableCollection(collection) =\u003e Ok(collection\n\t\t\t\t\t.children()\n\t\t\t\t\t.iter()\n\t\t\t\t\t.filter_map(|child| child.as_timeline().cloned())\n\t\t\t\t\t.collect()),\n\t\t\t\tSerializable::Timeline(timeline) =\u003e Ok(vec![timeline.clone()]),\n\t\t\t\t_ =\u003e {\n\t\t\t\t\ttask.set_error(\"Unknown OpenTimelineIO root element\");\n\t\t\t\t\tErr(Error::Failed(\"Unknown OpenTimelineIO root element\".to_string()))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tInterchangeFormat::Fcpxml =\u003e oakotio::from_fcpxml_file(filename).map_err(|e| {\n\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\"Failed to load FCPXML from file \\\"{}\\\" \\n\\nFCPXML Error:\\n\\n{}\",\n\t\t\t\tfilename, e\n\t\t\t));\n\t\t\tError::Failed(\"Failed to load FCPXML\".to_string())\n\t\t}),\n\t}\n}\n\n/// Two-stage read of a node's label (the C++ `oaknode_node_get_label` usage).\nfn node_label_of(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);\n\t}\n\tcrate::project::load::buf_to_string(\u0026buf)\n}\n","traces":[{"line":54,"address":[],"length":0,"stats":{"Line":7}},{"line":55,"address":[],"length":0,"stats":{"Line":21}},{"line":66,"address":[],"length":0,"stats":{"Line":11}},{"line":74,"address":[],"length":0,"stats":{"Line":11}},{"line":75,"address":[],"length":0,"stats":{"Line":44}},{"line":76,"address":[],"length":0,"stats":{"Line":6}},{"line":77,"address":[],"length":0,"stats":{"Line":2}},{"line":78,"address":[],"length":0,"stats":{"Line":2}},{"line":80,"address":[],"length":0,"stats":{"Line":2}},{"line":83,"address":[],"length":0,"stats":{"Line":18}},{"line":84,"address":[],"length":0,"stats":{"Line":18}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":86,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":27}},{"line":90,"address":[],"length":0,"stats":{"Line":9}},{"line":95,"address":[],"length":0,"stats":{"Line":25}},{"line":96,"address":[],"length":0,"stats":{"Line":2}},{"line":99,"address":[],"length":0,"stats":{"Line":7}},{"line":101,"address":[],"length":0,"stats":{"Line":28}},{"line":103,"address":[],"length":0,"stats":{"Line":3}},{"line":104,"address":[],"length":0,"stats":{"Line":1}},{"line":106,"address":[],"length":0,"stats":{"Line":1}},{"line":109,"address":[],"length":0,"stats":{"Line":1}},{"line":114,"address":[],"length":0,"stats":{"Line":24}},{"line":118,"address":[],"length":0,"stats":{"Line":16}},{"line":119,"address":[],"length":0,"stats":{"Line":24}},{"line":122,"address":[],"length":0,"stats":{"Line":24}},{"line":124,"address":[],"length":0,"stats":{"Line":17}},{"line":125,"address":[],"length":0,"stats":{"Line":18}},{"line":126,"address":[],"length":0,"stats":{"Line":18}},{"line":127,"address":[],"length":0,"stats":{"Line":0}},{"line":130,"address":[],"length":0,"stats":{"Line":18}},{"line":131,"address":[],"length":0,"stats":{"Line":18}},{"line":135,"address":[],"length":0,"stats":{"Line":0}},{"line":136,"address":[],"length":0,"stats":{"Line":0}},{"line":139,"address":[],"length":0,"stats":{"Line":36}},{"line":144,"address":[],"length":0,"stats":{"Line":9}},{"line":148,"address":[],"length":0,"stats":{"Line":26}},{"line":149,"address":[],"length":0,"stats":{"Line":24}},{"line":150,"address":[],"length":0,"stats":{"Line":8}},{"line":154,"address":[],"length":0,"stats":{"Line":27}},{"line":156,"address":[],"length":0,"stats":{"Line":9}},{"line":157,"address":[],"length":0,"stats":{"Line":1}},{"line":162,"address":[],"length":0,"stats":{"Line":24}},{"line":164,"address":[],"length":0,"stats":{"Line":35}},{"line":166,"address":[],"length":0,"stats":{"Line":24}},{"line":167,"address":[],"length":0,"stats":{"Line":16}},{"line":168,"address":[],"length":0,"stats":{"Line":4}},{"line":169,"address":[],"length":0,"stats":{"Line":6}},{"line":172,"address":[],"length":0,"stats":{"Line":8}},{"line":177,"address":[],"length":0,"stats":{"Line":2}},{"line":178,"address":[],"length":0,"stats":{"Line":2}},{"line":180,"address":[],"length":0,"stats":{"Line":1}},{"line":183,"address":[],"length":0,"stats":{"Line":1}},{"line":185,"address":[],"length":0,"stats":{"Line":1}},{"line":188,"address":[],"length":0,"stats":{"Line":1}},{"line":191,"address":[],"length":0,"stats":{"Line":21}},{"line":192,"address":[],"length":0,"stats":{"Line":14}},{"line":194,"address":[],"length":0,"stats":{"Line":37}},{"line":195,"address":[],"length":0,"stats":{"Line":24}},{"line":198,"address":[],"length":0,"stats":{"Line":16}},{"line":200,"address":[],"length":0,"stats":{"Line":32}},{"line":201,"address":[],"length":0,"stats":{"Line":8}},{"line":203,"address":[],"length":0,"stats":{"Line":24}},{"line":204,"address":[],"length":0,"stats":{"Line":8}},{"line":209,"address":[],"length":0,"stats":{"Line":24}},{"line":210,"address":[],"length":0,"stats":{"Line":8}},{"line":213,"address":[],"length":0,"stats":{"Line":24}},{"line":214,"address":[],"length":0,"stats":{"Line":16}},{"line":219,"address":[],"length":0,"stats":{"Line":8}},{"line":220,"address":[],"length":0,"stats":{"Line":16}},{"line":223,"address":[],"length":0,"stats":{"Line":8}},{"line":225,"address":[],"length":0,"stats":{"Line":24}},{"line":226,"address":[],"length":0,"stats":{"Line":8}},{"line":232,"address":[],"length":0,"stats":{"Line":23}},{"line":233,"address":[],"length":0,"stats":{"Line":14}},{"line":234,"address":[],"length":0,"stats":{"Line":0}},{"line":238,"address":[],"length":0,"stats":{"Line":13}},{"line":239,"address":[],"length":0,"stats":{"Line":13}},{"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":248,"address":[],"length":0,"stats":{"Line":12}},{"line":250,"address":[],"length":0,"stats":{"Line":18}},{"line":252,"address":[],"length":0,"stats":{"Line":18}},{"line":253,"address":[],"length":0,"stats":{"Line":6}},{"line":255,"address":[],"length":0,"stats":{"Line":18}},{"line":256,"address":[],"length":0,"stats":{"Line":6}},{"line":260,"address":[],"length":0,"stats":{"Line":12}},{"line":261,"address":[],"length":0,"stats":{"Line":12}},{"line":263,"address":[],"length":0,"stats":{"Line":12}},{"line":265,"address":[],"length":0,"stats":{"Line":6}},{"line":267,"address":[],"length":0,"stats":{"Line":18}},{"line":270,"address":[],"length":0,"stats":{"Line":12}},{"line":271,"address":[],"length":0,"stats":{"Line":0}},{"line":275,"address":[],"length":0,"stats":{"Line":12}},{"line":276,"address":[],"length":0,"stats":{"Line":12}},{"line":278,"address":[],"length":0,"stats":{"Line":24}},{"line":279,"address":[],"length":0,"stats":{"Line":24}},{"line":280,"address":[],"length":0,"stats":{"Line":0}},{"line":283,"address":[],"length":0,"stats":{"Line":24}},{"line":284,"address":[],"length":0,"stats":{"Line":19}},{"line":285,"address":[],"length":0,"stats":{"Line":9}},{"line":286,"address":[],"length":0,"stats":{"Line":1}},{"line":290,"address":[],"length":0,"stats":{"Line":1}},{"line":293,"address":[],"length":0,"stats":{"Line":0}},{"line":296,"address":[],"length":0,"stats":{"Line":0}},{"line":297,"address":[],"length":0,"stats":{"Line":0}},{"line":300,"address":[],"length":0,"stats":{"Line":24}},{"line":301,"address":[],"length":0,"stats":{"Line":0}},{"line":304,"address":[],"length":0,"stats":{"Line":36}},{"line":306,"address":[],"length":0,"stats":{"Line":48}},{"line":307,"address":[],"length":0,"stats":{"Line":60}},{"line":308,"address":[],"length":0,"stats":{"Line":24}},{"line":311,"address":[],"length":0,"stats":{"Line":17}},{"line":312,"address":[],"length":0,"stats":{"Line":22}},{"line":313,"address":[],"length":0,"stats":{"Line":44}},{"line":314,"address":[],"length":0,"stats":{"Line":44}},{"line":316,"address":[],"length":0,"stats":{"Line":33}},{"line":317,"address":[],"length":0,"stats":{"Line":33}},{"line":319,"address":[],"length":0,"stats":{"Line":11}},{"line":322,"address":[],"length":0,"stats":{"Line":14}},{"line":323,"address":[],"length":0,"stats":{"Line":14}},{"line":324,"address":[],"length":0,"stats":{"Line":7}},{"line":330,"address":[],"length":0,"stats":{"Line":22}},{"line":331,"address":[],"length":0,"stats":{"Line":22}},{"line":332,"address":[],"length":0,"stats":{"Line":11}},{"line":340,"address":[],"length":0,"stats":{"Line":13}},{"line":343,"address":[],"length":0,"stats":{"Line":2}},{"line":344,"address":[],"length":0,"stats":{"Line":3}},{"line":345,"address":[],"length":0,"stats":{"Line":1}},{"line":348,"address":[],"length":0,"stats":{"Line":1}},{"line":351,"address":[],"length":0,"stats":{"Line":13}},{"line":354,"address":[],"length":0,"stats":{"Line":4}},{"line":355,"address":[],"length":0,"stats":{"Line":4}},{"line":358,"address":[],"length":0,"stats":{"Line":2}},{"line":359,"address":[],"length":0,"stats":{"Line":2}},{"line":360,"address":[],"length":0,"stats":{"Line":2}},{"line":361,"address":[],"length":0,"stats":{"Line":2}},{"line":362,"address":[],"length":0,"stats":{"Line":1}},{"line":366,"address":[],"length":0,"stats":{"Line":1}},{"line":369,"address":[],"length":0,"stats":{"Line":3}},{"line":370,"address":[],"length":0,"stats":{"Line":2}},{"line":371,"address":[],"length":0,"stats":{"Line":1}},{"line":375,"address":[],"length":0,"stats":{"Line":1}},{"line":379,"address":[],"length":0,"stats":{"Line":1}},{"line":383,"address":[],"length":0,"stats":{"Line":12}},{"line":386,"address":[],"length":0,"stats":{"Line":4}},{"line":392,"address":[],"length":0,"stats":{"Line":12}},{"line":394,"address":[],"length":0,"stats":{"Line":12}},{"line":395,"address":[],"length":0,"stats":{"Line":14}},{"line":396,"address":[],"length":0,"stats":{"Line":0}},{"line":398,"address":[],"length":0,"stats":{"Line":12}},{"line":399,"address":[],"length":0,"stats":{"Line":2}},{"line":401,"address":[],"length":0,"stats":{"Line":9}},{"line":403,"address":[],"length":0,"stats":{"Line":12}},{"line":405,"address":[],"length":0,"stats":{"Line":13}},{"line":406,"address":[],"length":0,"stats":{"Line":1}},{"line":408,"address":[],"length":0,"stats":{"Line":15}},{"line":409,"address":[],"length":0,"stats":{"Line":3}},{"line":410,"address":[],"length":0,"stats":{"Line":15}},{"line":412,"address":[],"length":0,"stats":{"Line":6}},{"line":414,"address":[],"length":0,"stats":{"Line":12}},{"line":418,"address":[],"length":0,"stats":{"Line":9}},{"line":419,"address":[],"length":0,"stats":{"Line":3}},{"line":423,"address":[],"length":0,"stats":{"Line":3}},{"line":426,"address":[],"length":0,"stats":{"Line":3}},{"line":427,"address":[],"length":0,"stats":{"Line":6}},{"line":430,"address":[],"length":0,"stats":{"Line":3}},{"line":432,"address":[],"length":0,"stats":{"Line":9}},{"line":433,"address":[],"length":0,"stats":{"Line":3}},{"line":438,"address":[],"length":0,"stats":{"Line":3}},{"line":441,"address":[],"length":0,"stats":{"Line":4}},{"line":444,"address":[],"length":0,"stats":{"Line":12}},{"line":448,"address":[],"length":0,"stats":{"Line":8}},{"line":449,"address":[],"length":0,"stats":{"Line":4}},{"line":450,"address":[],"length":0,"stats":{"Line":4}},{"line":456,"address":[],"length":0,"stats":{"Line":4}},{"line":458,"address":[],"length":0,"stats":{"Line":12}},{"line":460,"address":[],"length":0,"stats":{"Line":4}},{"line":462,"address":[],"length":0,"stats":{"Line":16}},{"line":464,"address":[],"length":0,"stats":{"Line":12}},{"line":465,"address":[],"length":0,"stats":{"Line":8}},{"line":466,"address":[],"length":0,"stats":{"Line":12}},{"line":468,"address":[],"length":0,"stats":{"Line":24}},{"line":469,"address":[],"length":0,"stats":{"Line":8}},{"line":473,"address":[],"length":0,"stats":{"Line":0}},{"line":474,"address":[],"length":0,"stats":{"Line":0}},{"line":475,"address":[],"length":0,"stats":{"Line":0}},{"line":477,"address":[],"length":0,"stats":{"Line":0}},{"line":479,"address":[],"length":0,"stats":{"Line":0}},{"line":480,"address":[],"length":0,"stats":{"Line":0}},{"line":481,"address":[],"length":0,"stats":{"Line":0}},{"line":483,"address":[],"length":0,"stats":{"Line":0}},{"line":484,"address":[],"length":0,"stats":{"Line":0}},{"line":492,"address":[],"length":0,"stats":{"Line":10}},{"line":493,"address":[],"length":0,"stats":{"Line":30}},{"line":498,"address":[],"length":0,"stats":{"Line":21}},{"line":499,"address":[],"length":0,"stats":{"Line":7}},{"line":505,"address":[],"length":0,"stats":{"Line":9}},{"line":507,"address":[],"length":0,"stats":{"Line":18}},{"line":512,"address":[],"length":0,"stats":{"Line":9}},{"line":513,"address":[],"length":0,"stats":{"Line":36}},{"line":514,"address":[],"length":0,"stats":{"Line":9}},{"line":515,"address":[],"length":0,"stats":{"Line":0}},{"line":517,"address":[],"length":0,"stats":{"Line":27}},{"line":519,"address":[],"length":0,"stats":{"Line":36}},{"line":521,"address":[],"length":0,"stats":{"Line":18}}],"covered":186,"coverable":209},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","save.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//! `ProjectSaveTask`, mirroring `src/task/src/project/save/save.h`.\n//!\n//! Serializes a borrowed `OakNodeProject` to an `.oakproj` file via the\n//! oaknode serializer. Optionally writes to an override filename and/or uses\n//! compression.\n//!\n//! CPP-PARITY: src/task/src/project/save/save.h\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::ffi::taskhandle::cstr;\nuse crate::handle::CHandle;\nuse crate::project::load::buf_to_string;\nuse crate::task::{Task, TaskBehavior};\n\n/// Serializer result codes (`include/node/serializer.h`).\nconst OAKNODE_SERIALIZER_RESULT_SUCCESS: i32 = 0;\nconst OAKNODE_SERIALIZER_RESULT_XML_ERROR: i32 = 5;\nconst OAKNODE_SERIALIZER_RESULT_FILE_ERROR: i32 = 4;\nconst OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR: i32 = 6;\n\n/// A project-save task. Borrows its project and never takes ownership.\npub struct ProjectSaveTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Borrowed project to save (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Optional override filename; when empty the project's own filename is\n\t/// used.\n\tpub override_filename: Option\u003cString\u003e,\n\t/// Whether to compress the serialized output.\n\tpub use_compression: bool,\n}\n\nimpl ProjectSaveTask {\n\t/// Set the output filename override.\n\tpub fn set_override_filename(\u0026mut self, filename: \u0026str) {\n\t\tself.override_filename = Some(filename.to_string());\n\t}\n}\n\nimpl TaskBehavior for ProjectSaveTask {\n\t/// Serialize the project via the oaknode serializer (`bridge::node`) to\n\t/// the resolved filename.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet using_filename = match \u0026self.override_filename {\n\t\t\tSome(name) =\u003e name.clone(),\n\t\t\tNone =\u003e project_filename(self.project),\n\t\t};\n\n\t\tif using_filename.is_empty() {\n\t\t\ttask.set_error(\"Project has no filename to save to.\");\n\t\t\treturn Err(Error::Failed(\"Project has no filename to save to.\".to_string()));\n\t\t}\n\n\t\tlet mut code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;\n\t\tlet mut details = [0i8; 512];\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_serializer_save_to_file(\n\t\t\t\tself.project,\n\t\t\t\tcstr(\u0026using_filename),\n\t\t\t\tif self.use_compression { 1 } else { 0 },\n\t\t\t\t\u0026mut code,\n\t\t\t\tdetails.as_mut_ptr(),\n\t\t\t\tdetails.len() as i32,\n\t\t\t);\n\t\t}\n\n\t\tlet mut success = false;\n\t\tmatch code {\n\t\t\tOAKNODE_SERIALIZER_RESULT_SUCCESS =\u003e success = true,\n\t\t\tOAKNODE_SERIALIZER_RESULT_XML_ERROR =\u003e {\n\t\t\t\ttask.set_error(\"Failed to write XML data.\");\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_FILE_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\"Failed to open file for writing: {}\", buf_to_string(\u0026details)));\n\t\t\t}\n\t\t\tOAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR =\u003e {\n\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\"Failed to overwrite \\\"{}\\\". Project has been saved as \\\"{}\\\" instead.\",\n\t\t\t\t\tusing_filename,\n\t\t\t\t\tbuf_to_string(\u0026details)\n\t\t\t\t));\n\t\t\t\tsuccess = true;\n\t\t\t}\n\t\t\t_ =\u003e task.set_error(\"Unknown error.\"),\n\t\t}\n\n\t\tif success {\n\t\t\tOk(())\n\t\t} else {\n\t\t\tErr(Error::Failed(\"Failed to save project\".to_string()))\n\t\t}\n\t}\n}\n\n/// Two-stage read of the project's own filename (empty when unset).\npub(crate) fn project_filename(project: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_project_filename(project, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_project_filename(project, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n","traces":[{"line":53,"address":[],"length":0,"stats":{"Line":3}},{"line":54,"address":[],"length":0,"stats":{"Line":6}},{"line":61,"address":[],"length":0,"stats":{"Line":9}},{"line":62,"address":[],"length":0,"stats":{"Line":18}},{"line":63,"address":[],"length":0,"stats":{"Line":6}},{"line":64,"address":[],"length":0,"stats":{"Line":14}},{"line":67,"address":[],"length":0,"stats":{"Line":18}},{"line":68,"address":[],"length":0,"stats":{"Line":3}},{"line":69,"address":[],"length":0,"stats":{"Line":1}},{"line":72,"address":[],"length":0,"stats":{"Line":16}},{"line":73,"address":[],"length":0,"stats":{"Line":16}},{"line":76,"address":[],"length":0,"stats":{"Line":8}},{"line":77,"address":[],"length":0,"stats":{"Line":16}},{"line":78,"address":[],"length":0,"stats":{"Line":16}},{"line":79,"address":[],"length":0,"stats":{"Line":8}},{"line":80,"address":[],"length":0,"stats":{"Line":16}},{"line":81,"address":[],"length":0,"stats":{"Line":8}},{"line":85,"address":[],"length":0,"stats":{"Line":16}},{"line":86,"address":[],"length":0,"stats":{"Line":8}},{"line":87,"address":[],"length":0,"stats":{"Line":8}},{"line":88,"address":[],"length":0,"stats":{"Line":0}},{"line":89,"address":[],"length":0,"stats":{"Line":0}},{"line":91,"address":[],"length":0,"stats":{"Line":0}},{"line":92,"address":[],"length":0,"stats":{"Line":0}},{"line":94,"address":[],"length":0,"stats":{"Line":0}},{"line":95,"address":[],"length":0,"stats":{"Line":0}},{"line":96,"address":[],"length":0,"stats":{"Line":0}},{"line":97,"address":[],"length":0,"stats":{"Line":0}},{"line":98,"address":[],"length":0,"stats":{"Line":0}},{"line":100,"address":[],"length":0,"stats":{"Line":0}},{"line":102,"address":[],"length":0,"stats":{"Line":0}},{"line":105,"address":[],"length":0,"stats":{"Line":8}},{"line":106,"address":[],"length":0,"stats":{"Line":8}},{"line":108,"address":[],"length":0,"stats":{"Line":0}},{"line":114,"address":[],"length":0,"stats":{"Line":22}},{"line":115,"address":[],"length":0,"stats":{"Line":88}},{"line":116,"address":[],"length":0,"stats":{"Line":22}},{"line":117,"address":[],"length":0,"stats":{"Line":0}},{"line":119,"address":[],"length":0,"stats":{"Line":66}},{"line":121,"address":[],"length":0,"stats":{"Line":88}},{"line":123,"address":[],"length":0,"stats":{"Line":44}}],"covered":28,"coverable":41},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project","saveotio.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//! `SaveOTIOTask`, mirroring `src/task/src/project/saveotio/saveotio.h`.\n//!\n//! Serializes a borrowed `OakNodeProject` to an OpenTimelineIO (`.otio`) or\n//! FCPXML (`.fcpxml`) file through the pure-Rust `oakotio` binding (see\n//! `README` decision #6): the project's sequences become `OTIO::Timeline`s\n//! (`serialize_timeline` / `serialize_track` / `serialize_track_list`),\n//! exactly like the C++ `serialize_*` helpers. The format is dispatched\n//! from the filename extension (see [`crate::project::format`]) at the\n//! final write only — the serialization itself is shared. No OTIO or\n//! FCPXML type crosses the oaktask C ABI.\n//!\n//! CPP-PARITY: src/task/src/project/saveotio/saveotio.cpp\n\nuse oakcore_rs::Rational;\nuse oakotio::{\n\tClip, Composable, ExternalReference, Gap, MediaReference, RationalTime, Serializable, SerializableCollection, TimeRange,\n\tTimeline, Track, Transition,\n};\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::project::format::InterchangeFormat;\nuse crate::project::load::buf_to_string;\nuse crate::task::{Task, TaskBehavior};\n\n/// `Rational(INT_MIN)` — the initial \"no track yet\" maximum length\n/// (`RATIONAL_MIN` in saveotio.cpp).\nfn rational_min() -\u003e Rational {\n\tRational::new(-2147483647, 1)\n}\n\n/// An OTIO project-save task. Borrows its project.\npub struct SaveOTIOTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Borrowed project to save (borrowed `OakNodeProject`).\n\tpub project: CHandle,\n\t/// Output OTIO filename.\n\tpub filename: String,\n}\n\nimpl SaveOTIOTask {\n\t/// Serialize one sequence into an `OTIO::Timeline`; `None` when the\n\t/// sequence has no usable frame rate or a track fails to serialize.\n\t///\n\t/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_timeline)\n\tfn serialize_timeline(sequence: CHandle) -\u003e Option\u003cTimeline\u003e {\n\t\tlet mut otio_timeline = Timeline::new(node_label_of(unsafe { bridge::node::oaknode_sequence_as_node(sequence) }));\n\n\t\tlet mut rate = 0.0f64;\n\t\t{\n\t\t\tlet mut num = 0;\n\t\t\tlet mut den = 1;\n\t\t\tlet mut vp = CHandle::null();\n\t\t\tif unsafe { bridge::node::oaknode_sequence_get_video_params(sequence, 0, \u0026mut vp) } == 0 {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::common::oakcommon_videoparams_get_frame_rate(vp, \u0026mut num, \u0026mut den);\n\t\t\t\t}\n\t\t\t\tif den != 0 {\n\t\t\t\t\trate = num as f64 / den as f64;\n\t\t\t\t}\n\t\t\t\tif !vp.ctx.is_null() {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut vp);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif rate.is_nan() || rate \u003c= 0.0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tlet mut video_list = CHandle::null();\n\t\tlet mut audio_list = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_VIDEO, \u0026mut video_list);\n\t\t\tbridge::node::oaknode_sequence_get_track_list(sequence, bridge::node::OAKNODE_TRACK_TYPE_AUDIO, \u0026mut audio_list);\n\t\t}\n\n\t\tif !Self::serialize_track_list(video_list, \u0026mut otio_timeline, rate)\n\t\t\t|| !Self::serialize_track_list(audio_list, \u0026mut otio_timeline, rate)\n\t\t{\n\t\t\treturn None;\n\t\t}\n\n\t\tSome(otio_timeline)\n\t}\n\n\t/// Serialize all tracks of one track list (video or audio) into the\n\t/// timeline. Each track is padded to the list's maximum track length.\n\t///\n\t/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track_list)\n\tfn serialize_track_list(list: CHandle, otio_timeline: \u0026mut Timeline, sequence_rate: f64) -\u003e bool {\n\t\tif list.ctx.is_null() {\n\t\t\treturn true;\n\t\t}\n\n\t\tlet mut max_track_length = rational_min();\n\n\t\tlet mut track_count = 0;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_tracklist_get_track_count(list, \u0026mut track_count);\n\t\t}\n\n\t\tfor i in 0..track_count {\n\t\t\tlet mut track = CHandle::null();\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_tracklist_get_track_at(list, i, \u0026mut track);\n\t\t\t}\n\t\t\tif !track.ctx.is_null() {\n\t\t\t\tlet length = track_length_of(track);\n\t\t\t\tif length \u003e max_track_length {\n\t\t\t\t\tmax_track_length = length;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor i in 0..track_count {\n\t\t\tlet mut track = CHandle::null();\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_tracklist_get_track_at(list, i, \u0026mut track);\n\t\t\t}\n\t\t\tif track.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet Some(otio_track) = Self::serialize_track(track, sequence_rate, max_track_length) else {\n\t\t\t\treturn false;\n\t\t\t};\n\n\t\t\totio_timeline.tracks_mut().append_child(Composable::Track(otio_track));\n\t\t}\n\n\t\ttrue\n\t}\n\n\t/// Serialize one track: a `Track` whose kind matches the native track\n\t/// type, one OTIO block per native block, plus a trailing `Gap` when the\n\t/// track is shorter than `max_track_length`.\n\t///\n\t/// CPP-PARITY: saveotio.cpp (SaveOTIOTask::serialize_track)\n\tfn serialize_track(track: CHandle, sequence_rate: f64, max_track_length: Rational) -\u003e Option\u003cTrack\u003e {\n\t\tlet mut track_type = bridge::node::OAKNODE_TRACK_TYPE_NONE;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_track_get_type(track, \u0026mut track_type);\n\t\t}\n\n\t\tlet kind = match track_type {\n\t\t\tbridge::node::OAKNODE_TRACK_TYPE_VIDEO =\u003e \"Video\",\n\t\t\tbridge::node::OAKNODE_TRACK_TYPE_AUDIO =\u003e \"Audio\",\n\t\t\tother =\u003e {\n\t\t\t\teprintln!(\"Don't know OTIO track kind for native type {other}\");\n\t\t\t\treturn None;\n\t\t\t}\n\t\t};\n\t\tlet mut otio_track = Track::new(kind);\n\n\t\tlet mut block_count = 0;\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_track_get_block_count(track, \u0026mut block_count);\n\t\t}\n\n\t\tfor i in 0..block_count {\n\t\t\tlet mut block = CHandle::null();\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_track_get_block_at(track, i, \u0026mut block);\n\t\t\t}\n\t\t\tif block.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet mut kind = bridge::node::OAKNODE_BLOCK_OTHER;\n\t\t\tunsafe {\n\t\t\t\tbridge::node::oaknode_block_get_kind(block, \u0026mut kind);\n\t\t\t}\n\n\t\t\tlet otio_block: Option\u003cComposable\u003e = match kind {\n\t\t\t\tbridge::node::OAKNODE_BLOCK_CLIP =\u003e {\n\t\t\t\t\tlet mut otio_clip = Clip::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));\n\n\t\t\t\t\totio_clip.set_source_range(TimeRange::new(\n\t\t\t\t\t\tRationalTime::from_rational(block_in_of(block), sequence_rate),\n\t\t\t\t\t\tRationalTime::from_rational(block_length_of(block), sequence_rate),\n\t\t\t\t\t));\n\n\t\t\t\t\tlet mut media = CHandle::null();\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::node::oaknode_node_find_input_footage(bridge::node::oaknode_block_as_node(block), \u0026mut media);\n\t\t\t\t\t}\n\t\t\t\t\tif !media.ctx.is_null() {\n\t\t\t\t\t\tlet available_range = if track_type == bridge::node::OAKNODE_TRACK_TYPE_VIDEO {\n\t\t\t\t\t\t\t// OTIO ExternalReference uses the source clips\n\t\t\t\t\t\t\t// frame rate (or sample rate) as opposed to the\n\t\t\t\t\t\t\t// sequences rate.\n\t\t\t\t\t\t\tlet mut source_frame_rate = 0.0f64;\n\t\t\t\t\t\t\tlet mut duration = 0.0f64;\n\t\t\t\t\t\t\tlet mut num = 0;\n\t\t\t\t\t\t\tlet mut den = 1;\n\t\t\t\t\t\t\tlet mut vp = CHandle::null();\n\t\t\t\t\t\t\tif unsafe { bridge::node::oaknode_footage_get_video_params(media, 0, \u0026mut vp) } == 0 {\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_get_frame_rate(vp, \u0026mut num, \u0026mut den);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif den != 0 {\n\t\t\t\t\t\t\t\t\tsource_frame_rate = num as f64 / den as f64;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet mut dur = 0i64;\n\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_get_duration(vp, \u0026mut dur);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tduration = dur as f64;\n\t\t\t\t\t\t\t\tif !vp.ctx.is_null() {\n\t\t\t\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\t\t\t\tbridge::common::oakcommon_videoparams_free(\u0026mut vp);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tTimeRange::new(\n\t\t\t\t\t\t\t\tRationalTime::new(0.0, source_frame_rate),\n\t\t\t\t\t\t\t\tRationalTime::new(duration, source_frame_rate),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tTimeRange::new(RationalTime::new(0.0, 48000.0), RationalTime::new(0.0, 48000.0))\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tlet media_url = footage_filename(media);\n\t\t\t\t\t\tif !media_url.is_empty() {\n\t\t\t\t\t\t\totio_clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new(\n\t\t\t\t\t\t\t\tmedia_url,\n\t\t\t\t\t\t\t\tSome(available_range),\n\t\t\t\t\t\t\t)));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tSome(Composable::Clip(otio_clip))\n\t\t\t\t}\n\t\t\t\tbridge::node::OAKNODE_BLOCK_GAP =\u003e Some(Composable::Gap(Gap::new(\n\t\t\t\t\tTimeRange::new(\n\t\t\t\t\t\tRationalTime::from_rational(block_in_of(block), 24.0),\n\t\t\t\t\t\tRationalTime::from_rational(block_length_of(block), 24.0),\n\t\t\t\t\t),\n\t\t\t\t\tnode_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }),\n\t\t\t\t))),\n\t\t\t\tbridge::node::OAKNODE_BLOCK_TRANSITION =\u003e {\n\t\t\t\t\tlet mut otio_transition = Transition::new(node_label_of(unsafe { bridge::node::oaknode_block_as_node(block) }));\n\n\t\t\t\t\tlet (n, d) = transition_offset_of(block, true);\n\t\t\t\t\totio_transition.set_in_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));\n\t\t\t\t\tlet (n, d) = transition_offset_of(block, false);\n\t\t\t\t\totio_transition.set_out_offset(RationalTime::from_rational(Rational::new(n as i64, d as i64), 24.0));\n\n\t\t\t\t\tSome(Composable::Transition(otio_transition))\n\t\t\t\t}\n\t\t\t\t_ =\u003e None,\n\t\t\t};\n\n\t\t\tlet Some(otio_block) = otio_block else {\n\t\t\t\t// We shouldn't ever get here, but catch without crashing if\n\t\t\t\t// we ever do.\n\t\t\t\treturn None;\n\t\t\t};\n\n\t\t\totio_track.append_child(otio_block);\n\t\t}\n\n\t\t// All OTIO tracks must have the same duration so we add a Gap to\n\t\t// fill the remaining time.\n\t\tlet duration = track_duration(\u0026otio_track);\n\t\tlet duration_seconds = duration.clone().to_seconds();\n\t\tif duration_seconds \u003c max_track_length.to_f64() {\n\t\t\tlet time_left = max_track_length.to_f64() - duration_seconds;\n\n\t\t\tlet gap = Gap::new(TimeRange::new(duration, RationalTime::new(time_left, 1.0)), \"\");\n\t\t\totio_track.append_child(Composable::Gap(gap));\n\t\t}\n\n\t\tSome(otio_track)\n\t}\n}\n\nimpl TaskBehavior for SaveOTIOTask {\n\t/// Serialize the project to OTIO/FCPXML via `oakotio` and write the file.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\t// Dispatch the interchange format from the filename extension.\n\t\t// Format handling ends at the write below — the serialization\n\t\t// produces `oakotio::Timeline`s and is shared between `.otio` and\n\t\t// `.fcpxml` (C++ parity for the serialize_* helpers).\n\t\tlet format = match InterchangeFormat::of(\u0026self.filename) {\n\t\t\tOk(f) =\u003e f,\n\t\t\tErr(e) =\u003e {\n\t\t\t\tif let Error::Failed(msg) = \u0026e {\n\t\t\t\t\ttask.set_error(msg);\n\t\t\t\t}\n\t\t\t\treturn Err(e);\n\t\t\t}\n\t\t};\n\n\t\t// Collect sequences from the root folder (non-recursive, matching\n\t\t// the original list_children_of_type behavior closely enough for\n\t\t// OTIO).\n\t\tlet root = unsafe { bridge::node::oaknode_project_root(self.project) };\n\t\tif root.ctx.is_null() {\n\t\t\ttask.set_error(\"Project contains no sequences to export.\");\n\t\t\treturn Err(Error::Failed(\"Project contains no sequences to export.\".to_string()));\n\t\t}\n\n\t\tlet mut sequences: Vec\u003cCHandle\u003e = Vec::new();\n\t\tlet child_count = unsafe { bridge::node::oaknode_folder_child_count(root) };\n\t\tfor i in 0..child_count {\n\t\t\tlet child = unsafe { bridge::node::oaknode_folder_child_at(root, i) };\n\t\t\tif child.ctx.is_null() {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif node_id_of(child) == bridge::node::OAKNODE_TYPE_SEQUENCE {\n\t\t\t\t// Borrowed sequence alias of the child node handle (all\n\t\t\t\t// oaknode handles share the same box layout).\n\t\t\t\tsequences.push(child);\n\t\t\t}\n\t\t}\n\n\t\tif sequences.is_empty() {\n\t\t\ttask.set_error(\"Project contains no sequences to export.\");\n\t\t\treturn Err(Error::Failed(\"Project contains no sequences to export.\".to_string()));\n\t\t}\n\n\t\tlet mut serialized: Vec\u003cTimeline\u003e = Vec::with_capacity(sequences.len());\n\t\tfor sequence in \u0026sequences {\n\t\t\tmatch Self::serialize_timeline(*sequence) {\n\t\t\t\tSome(timeline) =\u003e serialized.push(timeline),\n\t\t\t\tNone =\u003e {\n\t\t\t\t\ttask.set_error(\u0026format!(\n\t\t\t\t\t\t\"Failed to serialize sequence \\\"{}\\\"\",\n\t\t\t\t\t\tnode_label_of(unsafe { bridge::node::oaknode_sequence_as_node(*sequence) })\n\t\t\t\t\t));\n\t\t\t\t\treturn Err(Error::Failed(\"Failed to serialize sequence\".to_string()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Write the serialized timelines, dispatching on the format: OTIO\n\t\t// JSON writes a `Timeline` root for one sequence or a\n\t\t// `SerializableCollection` for several; FCPXML writes all timelines\n\t\t// into one document (one `\u003cproject\u003e` per timeline).\n\t\tlet result: std::result::Result\u003c(), String\u003e = match format {\n\t\t\tInterchangeFormat::OtioJson =\u003e {\n\t\t\t\tif serialized.len() == 1 {\n\t\t\t\t\tserialized[0].to_json_file(\u0026self.filename).map_err(|e| e.to_string())\n\t\t\t\t} else {\n\t\t\t\t\t// Serialize all into a SerializableCollection.\n\t\t\t\t\tlet collection = SerializableCollection::new(\n\t\t\t\t\t\t\"Sequences\",\n\t\t\t\t\t\tserialized.into_iter().map(Serializable::Timeline).collect(),\n\t\t\t\t\t);\n\t\t\t\t\tSerializable::SerializableCollection(collection)\n\t\t\t\t\t\t.to_json_file(\u0026self.filename)\n\t\t\t\t\t\t.map_err(|e| e.to_string())\n\t\t\t\t}\n\t\t\t}\n\t\t\tInterchangeFormat::Fcpxml =\u003e oakotio::to_fcpxml_file(\u0026serialized, \u0026self.filename).map_err(|e| e.to_string()),\n\t\t};\n\n\t\tresult.map_err(|e| {\n\t\t\tlet (label, what) = match format {\n\t\t\t\tInterchangeFormat::OtioJson =\u003e (\"Failed to save OpenTimelineIO to file\", \"Failed to save OpenTimelineIO\"),\n\t\t\t\tInterchangeFormat::Fcpxml =\u003e (\"Failed to save FCPXML to file\", \"Failed to save FCPXML\"),\n\t\t\t};\n\t\t\ttask.set_error(\u0026format!(\"{label} \\\"{}\\\": {e}\", self.filename));\n\t\t\tError::Failed(what.to_string())\n\t\t})\n\t}\n}\n\n/// Two-stage read of a node's label.\nfn node_label_of(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_label(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_label(node, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// Two-stage read of a node's type id.\nfn node_id_of(node: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_node_get_id(node, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_node_get_id(node, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// The block's in point as a `Rational` (default 0/1 when unset).\nfn block_in_of(block: CHandle) -\u003e Rational {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tunsafe {\n\t\tbridge::node::oaknode_block_get_in(block, \u0026mut n, \u0026mut d);\n\t}\n\tRational::new(n as i64, d as i64)\n}\n\n/// The block's length as a `Rational` (default 0/1 when unset).\nfn block_length_of(block: CHandle) -\u003e Rational {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tunsafe {\n\t\tbridge::node::oaknode_block_get_length(block, \u0026mut n, \u0026mut d);\n\t}\n\tRational::new(n as i64, d as i64)\n}\n\n/// The track's total length as a `Rational` (default 0/1 when unset).\nfn track_length_of(track: CHandle) -\u003e Rational {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tunsafe {\n\t\tbridge::node::oaknode_track_get_length(track, \u0026mut n, \u0026mut d);\n\t}\n\tRational::new(n as i64, d as i64)\n}\n\n/// A transition's in (`in_offset == true`) or out offset as a `Rational`.\nfn transition_offset_of(block: CHandle, in_offset: bool) -\u003e (i32, i32) {\n\tlet mut n = 0;\n\tlet mut d = 1;\n\tif in_offset {\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_transition_get_in_offset(block, \u0026mut n, \u0026mut d);\n\t\t}\n\t} else {\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_transition_get_out_offset(block, \u0026mut n, \u0026mut d);\n\t\t}\n\t}\n\t(n, d)\n}\n\n/// Two-stage read of a footage's filename (empty when unset).\nfn footage_filename(footage: CHandle) -\u003e String {\n\tlet needed = unsafe { bridge::node::oaknode_footage_filename(footage, std::ptr::null_mut(), 0) };\n\tif needed \u003c= 0 {\n\t\treturn String::new();\n\t}\n\tlet mut buf = vec![0i8; needed as usize];\n\tunsafe {\n\t\tbridge::node::oaknode_footage_filename(footage, buf.as_mut_ptr(), needed);\n\t}\n\tbuf_to_string(\u0026buf)\n}\n\n/// The serialized track's duration as an `OTIO::RationalTime` — the sum of\n/// its children's source-range durations, keeping the first child's rate\n/// (the C++ `otio_track-\u003eduration(\u0026es)` result for contiguous children).\nfn track_duration(track: \u0026Track) -\u003e RationalTime {\n\tlet mut acc: Option\u003cRationalTime\u003e = None;\n\tfor child in track.children() {\n\t\tlet duration = match child.as_clip().and_then(|c| c.source_range()).map(|r| r.duration())\n\t\t\t.or_else(|| child.as_gap().and_then(|g| g.source_range()).map(|r| r.duration()))\n\t\t{\n\t\t\tSome(d) =\u003e d,\n\t\t\t// Transitions occupy no time.\n\t\t\tNone =\u003e RationalTime::new(0.0, 1.0),\n\t\t};\n\t\tmatch acc {\n\t\t\tNone =\u003e acc = Some(duration),\n\t\t\tSome(a) =\u003e {\n\t\t\t\tlet rate = a.rate();\n\t\t\t\tlet value = a.value() + duration.rescaled_to(rate).value();\n\t\t\t\tacc = Some(RationalTime::new(value, rate));\n\t\t\t}\n\t\t}\n\t}\n\tacc.unwrap_or_else(RationalTime::invalid_time)\n}\n","traces":[{"line":41,"address":[],"length":0,"stats":{"Line":10}},{"line":42,"address":[],"length":0,"stats":{"Line":10}},{"line":60,"address":[],"length":0,"stats":{"Line":10}},{"line":61,"address":[],"length":0,"stats":{"Line":50}},{"line":63,"address":[],"length":0,"stats":{"Line":20}},{"line":65,"address":[],"length":0,"stats":{"Line":20}},{"line":66,"address":[],"length":0,"stats":{"Line":20}},{"line":67,"address":[],"length":0,"stats":{"Line":20}},{"line":68,"address":[],"length":0,"stats":{"Line":20}},{"line":70,"address":[],"length":0,"stats":{"Line":30}},{"line":72,"address":[],"length":0,"stats":{"Line":20}},{"line":73,"address":[],"length":0,"stats":{"Line":10}},{"line":75,"address":[],"length":0,"stats":{"Line":10}},{"line":77,"address":[],"length":0,"stats":{"Line":10}},{"line":82,"address":[],"length":0,"stats":{"Line":30}},{"line":83,"address":[],"length":0,"stats":{"Line":1}},{"line":86,"address":[],"length":0,"stats":{"Line":18}},{"line":87,"address":[],"length":0,"stats":{"Line":18}},{"line":89,"address":[],"length":0,"stats":{"Line":36}},{"line":90,"address":[],"length":0,"stats":{"Line":18}},{"line":93,"address":[],"length":0,"stats":{"Line":27}},{"line":94,"address":[],"length":0,"stats":{"Line":24}},{"line":96,"address":[],"length":0,"stats":{"Line":1}},{"line":99,"address":[],"length":0,"stats":{"Line":8}},{"line":106,"address":[],"length":0,"stats":{"Line":17}},{"line":107,"address":[],"length":0,"stats":{"Line":34}},{"line":108,"address":[],"length":0,"stats":{"Line":7}},{"line":111,"address":[],"length":0,"stats":{"Line":20}},{"line":113,"address":[],"length":0,"stats":{"Line":20}},{"line":115,"address":[],"length":0,"stats":{"Line":20}},{"line":118,"address":[],"length":0,"stats":{"Line":20}},{"line":119,"address":[],"length":0,"stats":{"Line":20}},{"line":121,"address":[],"length":0,"stats":{"Line":30}},{"line":123,"address":[],"length":0,"stats":{"Line":10}},{"line":124,"address":[],"length":0,"stats":{"Line":30}},{"line":125,"address":[],"length":0,"stats":{"Line":20}},{"line":126,"address":[],"length":0,"stats":{"Line":10}},{"line":131,"address":[],"length":0,"stats":{"Line":20}},{"line":132,"address":[],"length":0,"stats":{"Line":20}},{"line":134,"address":[],"length":0,"stats":{"Line":30}},{"line":136,"address":[],"length":0,"stats":{"Line":20}},{"line":137,"address":[],"length":0,"stats":{"Line":0}},{"line":140,"address":[],"length":0,"stats":{"Line":39}},{"line":141,"address":[],"length":0,"stats":{"Line":1}},{"line":144,"address":[],"length":0,"stats":{"Line":27}},{"line":147,"address":[],"length":0,"stats":{"Line":9}},{"line":155,"address":[],"length":0,"stats":{"Line":10}},{"line":156,"address":[],"length":0,"stats":{"Line":20}},{"line":158,"address":[],"length":0,"stats":{"Line":20}},{"line":161,"address":[],"length":0,"stats":{"Line":19}},{"line":162,"address":[],"length":0,"stats":{"Line":7}},{"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":169,"address":[],"length":0,"stats":{"Line":27}},{"line":171,"address":[],"length":0,"stats":{"Line":18}},{"line":173,"address":[],"length":0,"stats":{"Line":18}},{"line":176,"address":[],"length":0,"stats":{"Line":18}},{"line":177,"address":[],"length":0,"stats":{"Line":18}},{"line":179,"address":[],"length":0,"stats":{"Line":27}},{"line":181,"address":[],"length":0,"stats":{"Line":18}},{"line":182,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":18}},{"line":187,"address":[],"length":0,"stats":{"Line":18}},{"line":190,"address":[],"length":0,"stats":{"Line":27}},{"line":192,"address":[],"length":0,"stats":{"Line":40}},{"line":194,"address":[],"length":0,"stats":{"Line":24}},{"line":195,"address":[],"length":0,"stats":{"Line":32}},{"line":196,"address":[],"length":0,"stats":{"Line":32}},{"line":199,"address":[],"length":0,"stats":{"Line":16}},{"line":201,"address":[],"length":0,"stats":{"Line":24}},{"line":203,"address":[],"length":0,"stats":{"Line":8}},{"line":204,"address":[],"length":0,"stats":{"Line":14}},{"line":208,"address":[],"length":0,"stats":{"Line":10}},{"line":209,"address":[],"length":0,"stats":{"Line":10}},{"line":210,"address":[],"length":0,"stats":{"Line":10}},{"line":211,"address":[],"length":0,"stats":{"Line":10}},{"line":212,"address":[],"length":0,"stats":{"Line":10}},{"line":213,"address":[],"length":0,"stats":{"Line":10}},{"line":215,"address":[],"length":0,"stats":{"Line":15}},{"line":217,"address":[],"length":0,"stats":{"Line":10}},{"line":218,"address":[],"length":0,"stats":{"Line":5}},{"line":220,"address":[],"length":0,"stats":{"Line":10}},{"line":222,"address":[],"length":0,"stats":{"Line":10}},{"line":224,"address":[],"length":0,"stats":{"Line":5}},{"line":225,"address":[],"length":0,"stats":{"Line":5}},{"line":227,"address":[],"length":0,"stats":{"Line":5}},{"line":232,"address":[],"length":0,"stats":{"Line":10}},{"line":233,"address":[],"length":0,"stats":{"Line":15}},{"line":236,"address":[],"length":0,"stats":{"Line":6}},{"line":239,"address":[],"length":0,"stats":{"Line":21}},{"line":240,"address":[],"length":0,"stats":{"Line":14}},{"line":241,"address":[],"length":0,"stats":{"Line":28}},{"line":242,"address":[],"length":0,"stats":{"Line":7}},{"line":243,"address":[],"length":0,"stats":{"Line":7}},{"line":248,"address":[],"length":0,"stats":{"Line":8}},{"line":250,"address":[],"length":0,"stats":{"Line":0}},{"line":251,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":0}},{"line":253,"address":[],"length":0,"stats":{"Line":0}},{"line":255,"address":[],"length":0,"stats":{"Line":0}},{"line":258,"address":[],"length":0,"stats":{"Line":5}},{"line":260,"address":[],"length":0,"stats":{"Line":3}},{"line":261,"address":[],"length":0,"stats":{"Line":6}},{"line":262,"address":[],"length":0,"stats":{"Line":3}},{"line":263,"address":[],"length":0,"stats":{"Line":6}},{"line":265,"address":[],"length":0,"stats":{"Line":1}},{"line":267,"address":[],"length":0,"stats":{"Line":0}},{"line":270,"address":[],"length":0,"stats":{"Line":18}},{"line":273,"address":[],"length":0,"stats":{"Line":0}},{"line":276,"address":[],"length":0,"stats":{"Line":27}},{"line":281,"address":[],"length":0,"stats":{"Line":27}},{"line":282,"address":[],"length":0,"stats":{"Line":36}},{"line":283,"address":[],"length":0,"stats":{"Line":19}},{"line":284,"address":[],"length":0,"stats":{"Line":4}},{"line":286,"address":[],"length":0,"stats":{"Line":7}},{"line":287,"address":[],"length":0,"stats":{"Line":2}},{"line":290,"address":[],"length":0,"stats":{"Line":9}},{"line":296,"address":[],"length":0,"stats":{"Line":10}},{"line":300,"address":[],"length":0,"stats":{"Line":30}},{"line":301,"address":[],"length":0,"stats":{"Line":20}},{"line":302,"address":[],"length":0,"stats":{"Line":0}},{"line":303,"address":[],"length":0,"stats":{"Line":0}},{"line":306,"address":[],"length":0,"stats":{"Line":30}},{"line":307,"address":[],"length":0,"stats":{"Line":30}},{"line":308,"address":[],"length":0,"stats":{"Line":20}},{"line":309,"address":[],"length":0,"stats":{"Line":40}},{"line":310,"address":[],"length":0,"stats":{"Line":20}},{"line":311,"address":[],"length":0,"stats":{"Line":0}},{"line":313,"address":[],"length":0,"stats":{"Line":20}},{"line":316,"address":[],"length":0,"stats":{"Line":20}},{"line":320,"address":[],"length":0,"stats":{"Line":20}},{"line":321,"address":[],"length":0,"stats":{"Line":3}},{"line":322,"address":[],"length":0,"stats":{"Line":1}},{"line":325,"address":[],"length":0,"stats":{"Line":45}},{"line":326,"address":[],"length":0,"stats":{"Line":19}},{"line":327,"address":[],"length":0,"stats":{"Line":10}},{"line":328,"address":[],"length":0,"stats":{"Line":32}},{"line":330,"address":[],"length":0,"stats":{"Line":8}},{"line":331,"address":[],"length":0,"stats":{"Line":2}},{"line":332,"address":[],"length":0,"stats":{"Line":4}},{"line":334,"address":[],"length":0,"stats":{"Line":2}},{"line":339,"address":[],"length":0,"stats":{"Line":14}},{"line":341,"address":[],"length":0,"stats":{"Line":18}},{"line":346,"address":[],"length":0,"stats":{"Line":4}},{"line":348,"address":[],"length":0,"stats":{"Line":3}},{"line":351,"address":[],"length":0,"stats":{"Line":14}},{"line":352,"address":[],"length":0,"stats":{"Line":0}},{"line":353,"address":[],"length":0,"stats":{"Line":0}},{"line":359,"address":[],"length":0,"stats":{"Line":21}},{"line":360,"address":[],"length":0,"stats":{"Line":84}},{"line":361,"address":[],"length":0,"stats":{"Line":21}},{"line":362,"address":[],"length":0,"stats":{"Line":0}},{"line":364,"address":[],"length":0,"stats":{"Line":63}},{"line":366,"address":[],"length":0,"stats":{"Line":84}},{"line":368,"address":[],"length":0,"stats":{"Line":42}},{"line":372,"address":[],"length":0,"stats":{"Line":10}},{"line":373,"address":[],"length":0,"stats":{"Line":40}},{"line":374,"address":[],"length":0,"stats":{"Line":10}},{"line":375,"address":[],"length":0,"stats":{"Line":0}},{"line":377,"address":[],"length":0,"stats":{"Line":30}},{"line":379,"address":[],"length":0,"stats":{"Line":40}},{"line":381,"address":[],"length":0,"stats":{"Line":20}},{"line":385,"address":[],"length":0,"stats":{"Line":8}},{"line":386,"address":[],"length":0,"stats":{"Line":16}},{"line":387,"address":[],"length":0,"stats":{"Line":16}},{"line":389,"address":[],"length":0,"stats":{"Line":24}},{"line":391,"address":[],"length":0,"stats":{"Line":24}},{"line":395,"address":[],"length":0,"stats":{"Line":8}},{"line":396,"address":[],"length":0,"stats":{"Line":16}},{"line":397,"address":[],"length":0,"stats":{"Line":16}},{"line":399,"address":[],"length":0,"stats":{"Line":24}},{"line":401,"address":[],"length":0,"stats":{"Line":24}},{"line":405,"address":[],"length":0,"stats":{"Line":10}},{"line":406,"address":[],"length":0,"stats":{"Line":20}},{"line":407,"address":[],"length":0,"stats":{"Line":20}},{"line":409,"address":[],"length":0,"stats":{"Line":30}},{"line":411,"address":[],"length":0,"stats":{"Line":30}},{"line":415,"address":[],"length":0,"stats":{"Line":2}},{"line":416,"address":[],"length":0,"stats":{"Line":4}},{"line":417,"address":[],"length":0,"stats":{"Line":4}},{"line":418,"address":[],"length":0,"stats":{"Line":2}},{"line":420,"address":[],"length":0,"stats":{"Line":3}},{"line":424,"address":[],"length":0,"stats":{"Line":3}},{"line":427,"address":[],"length":0,"stats":{"Line":2}},{"line":431,"address":[],"length":0,"stats":{"Line":7}},{"line":432,"address":[],"length":0,"stats":{"Line":28}},{"line":433,"address":[],"length":0,"stats":{"Line":7}},{"line":434,"address":[],"length":0,"stats":{"Line":0}},{"line":436,"address":[],"length":0,"stats":{"Line":21}},{"line":438,"address":[],"length":0,"stats":{"Line":28}},{"line":440,"address":[],"length":0,"stats":{"Line":14}},{"line":446,"address":[],"length":0,"stats":{"Line":9}},{"line":447,"address":[],"length":0,"stats":{"Line":27}},{"line":448,"address":[],"length":0,"stats":{"Line":27}},{"line":449,"address":[],"length":0,"stats":{"Line":77}},{"line":450,"address":[],"length":0,"stats":{"Line":13}},{"line":452,"address":[],"length":0,"stats":{"Line":16}},{"line":454,"address":[],"length":0,"stats":{"Line":1}},{"line":456,"address":[],"length":0,"stats":{"Line":9}},{"line":457,"address":[],"length":0,"stats":{"Line":9}},{"line":458,"address":[],"length":0,"stats":{"Line":0}},{"line":459,"address":[],"length":0,"stats":{"Line":0}},{"line":460,"address":[],"length":0,"stats":{"Line":0}},{"line":461,"address":[],"length":0,"stats":{"Line":0}},{"line":465,"address":[],"length":0,"stats":{"Line":18}}],"covered":186,"coverable":207},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","project.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//! Project load/save/import tasks, mirroring `src/task/src/project/*`.\n//!\n//! These tasks move data across the oaknode C ABI: they borrow node handles\n//! while running and take ownership only on the `take_*` accessors\n//! (architectural decision #5 in README.md).\n\npub mod format;\npub mod import;\npub mod load;\npub mod loadotio;\npub mod save;\npub mod saveotio;\n","traces":[],"covered":0,"coverable":0},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","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//! `ProxyTask`, mirroring `src/task/src/proxy/proxy.h`.\n//!\n//! Transcodes a video stream to a proxy via an external `ffmpeg` process\n//! (oakcodec kind `OAKCODEC_TASK_PROXY`). The two pure functions\n//! [`ProxyTask::build_arguments`] and [`ProxyTask::parse_progress`] are\n//! parity/golden candidates (see tests/parity_test.rs).\n//!\n//! CPP-PARITY: src/task/src/proxy/proxy.h\n\nuse std::io::{BufRead, BufReader};\nuse std::process::{Command, Stdio};\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::task::{Task, TaskBehavior};\n\n/// The proxy parameters, mirroring `oakcodec_proxy_params` in\n/// `include/codec/proxy.h` (kept as plain fields so no oakcodec handle is\n/// needed to build arguments).\npub struct ProxyParams {\n\t/// Target width (0 = unspecified/divider-based).\n\tpub width: i32,\n\t/// Target height (0 = unspecified/divider-based).\n\tpub height: i32,\n\t/// Resolution divider.\n\tpub divider: i32,\n\t/// Proxy format version.\n\tpub version: i32,\n\t/// CRF for the proxy encode.\n\tpub crf: i32,\n\t/// Whether audio is included in the proxy.\n\tpub include_audio: bool,\n\t/// Output container extension.\n\tpub extension: String,\n\t/// ffmpeg preset name.\n\tpub preset: String,\n}\n\n/// A proxy transcode task: runs `ffmpeg` with arguments from\n/// [`ProxyTask::build_arguments`], parsing progress lines via\n/// [`ProxyTask::parse_progress`].\npub struct ProxyTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Source media filename.\n\tpub source_filename: String,\n\t/// Target stream index.\n\tpub stream_index: i32,\n\t/// Proxy parameters.\n\tpub params: ProxyParams,\n\t/// Output proxy filename.\n\tpub output_filename: String,\n\t/// Total media duration in seconds (for progress scaling).\n\tduration_seconds: f64,\n}\n\nimpl ProxyTask {\n\t/// Build a proxy task from an oakcodec request and proxy params,\n\t/// mirroring the C++ constructor (divider-based requests take the source\n\t/// fraction).\n\tpub fn new(request: \u0026bridge::codec::OakCodecTaskRequest, params: ProxyParams) -\u003e ProxyTask {\n\t\tlet source = unsafe { crate::ffi::taskhandle::cstr_to_string(request.input_filename) };\n\t\tlet output = unsafe { crate::ffi::taskhandle::cstr_to_string(request.output_filename) };\n\t\tlet mut params = params;\n\t\tif request.proxy_width \u003e 0 \u0026\u0026 request.proxy_height \u003e 0 {\n\t\t\tparams.width = request.proxy_width;\n\t\t\tparams.height = request.proxy_height;\n\t\t\tparams.divider = 1;\n\t\t}\n\t\tlet title = format!(\"Generating Proxy {}:{}\", source, request.stream_index);\n\t\tProxyTask {\n\t\t\tbase: Task::new(\u0026title, CHandle::null()),\n\t\t\tsource_filename: source,\n\t\t\tstream_index: request.stream_index,\n\t\t\tparams,\n\t\t\toutput_filename: output,\n\t\t\tduration_seconds: 0.0,\n\t\t}\n\t}\n\n\t/// Build the `ffmpeg` command-line argument vector for the proxy\n\t/// transcode. Mirrors `build_arguments` in proxy.cpp; the exact ordering\n\t/// and flag spelling are parity-locked (golden test).\n\t///\n\t/// CPP-PARITY: src/task/src/proxy/proxy.cpp (build_arguments)\n\tpub fn build_arguments(\n\t\tsource_filename: \u0026str,\n\t\tstream_index: i32,\n\t\tparams: \u0026ProxyParams,\n\t\toutput_filename: \u0026str,\n\t) -\u003e Vec\u003cString\u003e {\n\t\tlet scale_filter = if params.divider \u003e 1 {\n\t\t\tformat!(\n\t\t\t\t\"scale=w=trunc(iw/{}/2)*2:h=trunc(ih/{}/2)*2\",\n\t\t\t\tparams.divider, params.divider\n\t\t\t)\n\t\t} else {\n\t\t\tformat!(\n\t\t\t\t\"scale=w={}:h={}:force_original_aspect_ratio=decrease\",\n\t\t\t\tparams.width, params.height\n\t\t\t)\n\t\t};\n\n\t\tlet container_format = if params.extension.is_empty() {\n\t\t\t\"mp4\".to_string()\n\t\t} else {\n\t\t\tparams.extension.clone()\n\t\t};\n\n\t\tlet mut args: Vec\u003cString\u003e = vec![\n\t\t\t\"-y\".to_string(),\n\t\t\t\"-nostats\".to_string(),\n\t\t\t\"-progress\".to_string(),\n\t\t\t\"pipe:1\".to_string(),\n\t\t\t\"-i\".to_string(),\n\t\t\tsource_filename.to_string(),\n\t\t\t\"-map\".to_string(),\n\t\t\tformat!(\"0:{stream_index}\"),\n\t\t];\n\n\t\tif params.include_audio {\n\t\t\targs.extend([\n\t\t\t\t\"-map\".to_string(),\n\t\t\t\t\"0:a?\".to_string(),\n\t\t\t\t\"-c:a\".to_string(),\n\t\t\t\t\"aac\".to_string(),\n\t\t\t\t\"-b:a\".to_string(),\n\t\t\t\t\"128k\".to_string(),\n\t\t\t]);\n\t\t} else {\n\t\t\targs.push(\"-an\".to_string());\n\t\t}\n\n\t\targs.extend([\n\t\t\t\"-vf\".to_string(),\n\t\t\tscale_filter,\n\t\t\t\"-c:v\".to_string(),\n\t\t\t\"libx264\".to_string(),\n\t\t\t\"-preset\".to_string(),\n\t\t\tparams.preset.clone(),\n\t\t\t\"-crf\".to_string(),\n\t\t\tparams.crf.to_string(),\n\t\t\t\"-pix_fmt\".to_string(),\n\t\t\t\"yuv420p\".to_string(),\n\t\t\t\"-movflags\".to_string(),\n\t\t\t\"+faststart\".to_string(),\n\t\t\t\"-f\".to_string(),\n\t\t\tcontainer_format,\n\t\t\toutput_filename.to_string(),\n\t\t]);\n\n\t\targs\n\t}\n\n\t/// Parse a single `ffmpeg -progress` output line into a progress value in\n\t/// 0.0..=1.0 given the total duration. Mirrors `parse_progress` in\n\t/// proxy.cpp (golden test).\n\t///\n\t/// CPP-PARITY: src/task/src/proxy/proxy.cpp (parse_progress)\n\tpub fn parse_progress(line: \u0026str, duration_seconds: f64) -\u003e Option\u003cf64\u003e {\n\t\tif duration_seconds \u003c= 0.0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tlet out_time_us: i64 = if let Some(rest) = line.strip_prefix(\"out_time_us=\") {\n\t\t\trest.trim().parse().unwrap_or(-1)\n\t\t} else if let Some(rest) = line.strip_prefix(\"out_time_ms=\") {\n\t\t\t// Despite the name, ffmpeg reports this value in microseconds.\n\t\t\trest.trim().parse().unwrap_or(-1)\n\t\t} else {\n\t\t\t-1\n\t\t};\n\n\t\tif out_time_us \u003c 0 {\n\t\t\treturn None;\n\t\t}\n\n\t\tlet progress = out_time_us as f64 / 1_000_000.0 / duration_seconds;\n\t\tSome(if progress \u003c 0.0 {\n\t\t\t0.0\n\t\t} else if progress \u003e 1.0 {\n\t\t\t1.0\n\t\t} else {\n\t\t\tprogress\n\t\t})\n\t}\n}\n\nimpl TaskBehavior for ProxyTask {\n\t/// Spawn `ffmpeg` with the built arguments, feed `-progress` lines to\n\t/// [`ProxyTask::parse_progress`] and emit them as task progress.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e {\n\t\tlet mut ffmpeg_buf = [0i8; 1024];\n\t\tlet found = unsafe {\n\t\t\tbridge::codec::oakcodec_proxy_find_ffmpeg(std::ptr::null(), ffmpeg_buf.as_mut_ptr(), ffmpeg_buf.len() as i32)\n\t\t};\n\t\tif found \u003c= 0 {\n\t\t\ttask.set_error(\n\t\t\t\t\"Failed to generate proxy: ffmpeg executable was not found. Set the ffmpeg path in Preferences \u003e Disk \u003e Proxy Settings.\",\n\t\t\t);\n\t\t\treturn Err(Error::Failed(\"ffmpeg executable was not found\".to_string()));\n\t\t}\n\t\tlet ffmpeg_path = buf_to_string(\u0026ffmpeg_buf);\n\n\t\t// Create the output directory if needed.\n\t\tif let Some(parent) = std::path::Path::new(\u0026self.output_filename).parent() {\n\t\t\tif !parent.as_os_str().is_empty() \u0026\u0026 !parent.exists() {\n\t\t\t\tif std::fs::create_dir_all(parent).is_err() {\n\t\t\t\t\ttask.set_error(\"Failed to create proxy output directory\");\n\t\t\t\t\treturn Err(Error::Failed(\"Failed to create proxy output directory\".to_string()));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet _ = std::fs::remove_file(\u0026self.output_filename);\n\t\tlet working_filename = format!(\"{}.working.mp4\", self.output_filename);\n\t\tlet _ = std::fs::remove_file(\u0026working_filename);\n\n\t\tlet args = Self::build_arguments(\u0026self.source_filename, self.stream_index, \u0026self.params, \u0026working_filename);\n\n\t\t// Probe the source duration for progress scaling (0 when unknown).\n\t\tself.duration_seconds = probe_source_duration_seconds(\u0026ffmpeg_path, \u0026self.source_filename);\n\n\t\tlet mut command = Command::new(\u0026ffmpeg_path);\n\t\tcommand.args(\u0026args).stdout(Stdio::piped()).stderr(Stdio::piped());\n\t\tlet mut child = match command.spawn() {\n\t\t\tOk(child) =\u003e child,\n\t\t\tErr(_) =\u003e {\n\t\t\t\ttask.set_error(\"Failed to start ffmpeg for proxy generation\");\n\t\t\t\treturn Err(Error::Failed(\"Failed to start ffmpeg\".to_string()));\n\t\t\t}\n\t\t};\n\n\t\t// Drain stderr in a thread so a chatty ffmpeg cannot block us.\n\t\tlet stderr = child.stderr.take();\n\t\tif let Some(stderr) = stderr {\n\t\t\tstd::thread::spawn(move || {\n\t\t\t\tlet _ = BufReader::new(stderr).lines().count();\n\t\t\t});\n\t\t}\n\n\t\tlet stdout = child.stdout.take();\n\t\tlet mut last_progress = 0.0_f64;\n\t\tif let Some(stdout) = stdout {\n\t\t\tlet reader = BufReader::new(stdout);\n\t\t\tfor line in reader.lines() {\n\t\t\t\tmatch line {\n\t\t\t\t\tOk(line) =\u003e {\n\t\t\t\t\t\tif let Some(progress) = Self::parse_progress(\u0026line, self.duration_seconds) {\n\t\t\t\t\t\t\tif progress \u003e= 0.0 \u0026\u0026 progress - last_progress \u003e 0.001 {\n\t\t\t\t\t\t\t\tlast_progress = progress;\n\t\t\t\t\t\t\t\ttask.emit_progress(progress);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tErr(_) =\u003e break,\n\t\t\t\t}\n\t\t\t\tif task.is_cancelled() {\n\t\t\t\t\tlet _ = child.kill();\n\t\t\t\t\tlet _ = child.wait();\n\t\t\t\t\tlet _ = std::fs::remove_file(\u0026working_filename);\n\t\t\t\t\ttask.set_error(\"Proxy generation was cancelled\");\n\t\t\t\t\treturn Err(Error::Cancelled);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tlet exit_status = child.wait();\n\t\tif !matches!(exit_status, Ok(status) if status.success()) {\n\t\t\tlet _ = std::fs::remove_file(\u0026working_filename);\n\t\t\ttask.set_error(\"ffmpeg failed to generate proxy\");\n\t\t\treturn Err(Error::Failed(\"ffmpeg failed to generate proxy\".to_string()));\n\t\t}\n\n\t\tif !std::path::Path::new(\u0026working_filename).exists() {\n\t\t\ttask.set_error(\"ffmpeg finished but proxy file was not created\");\n\t\t\treturn Err(Error::Failed(\"ffmpeg finished but proxy file was not created\".to_string()));\n\t\t}\n\n\t\tif std::fs::rename(\u0026working_filename, \u0026self.output_filename).is_err() {\n\t\t\ttask.set_error(\"Failed to move proxy into place\");\n\t\t\treturn Err(Error::Failed(\"Failed to move proxy into place\".to_string()));\n\t\t}\n\n\t\ttask.emit_progress(1.0);\n\t\tOk(())\n\t}\n}\n\n/// Read a NUL-terminated C char buffer into a String.\nfn buf_to_string(buf: \u0026[i8]) -\u003e String {\n\tlet len = buf.iter().position(|\u0026c| c == 0).unwrap_or(buf.len());\n\tunsafe { String::from_utf8_lossy(std::slice::from_raw_parts(buf.as_ptr() as *const u8, len)).into_owned() }\n}\n\n/// Probe the source duration via `ffprobe` next to ffmpeg; 0.0 when\n/// unavailable (in which case no intermediate progress is reported).\nfn probe_source_duration_seconds(ffmpeg_path: \u0026str, source_filename: \u0026str) -\u003e f64 {\n\tlet ffprobe = std::path::Path::new(ffmpeg_path).with_file_name(\"ffprobe\");\n\tif !ffprobe.exists() {\n\t\treturn 0.0;\n\t}\n\tlet output = Command::new(\u0026ffprobe)\n\t\t.args([\"-v\", \"error\", \"-show_entries\", \"format=duration\", \"-of\", \"default=noprint_wrappers=1:nokey=1\", source_filename])\n\t\t.output();\n\tmatch output {\n\t\tOk(out) if out.status.success() =\u003e {\n\t\t\tlet text = String::from_utf8_lossy(\u0026out.stdout);\n\t\t\ttext.trim().parse().unwrap_or(0.0)\n\t\t}\n\t\t_ =\u003e 0.0,\n\t}\n}\n","traces":[{"line":78,"address":[],"length":0,"stats":{"Line":7}},{"line":79,"address":[],"length":0,"stats":{"Line":21}},{"line":80,"address":[],"length":0,"stats":{"Line":21}},{"line":81,"address":[],"length":0,"stats":{"Line":14}},{"line":82,"address":[],"length":0,"stats":{"Line":7}},{"line":83,"address":[],"length":0,"stats":{"Line":0}},{"line":84,"address":[],"length":0,"stats":{"Line":0}},{"line":85,"address":[],"length":0,"stats":{"Line":0}},{"line":87,"address":[],"length":0,"stats":{"Line":14}},{"line":89,"address":[],"length":0,"stats":{"Line":28}},{"line":91,"address":[],"length":0,"stats":{"Line":14}},{"line":103,"address":[],"length":0,"stats":{"Line":6}},{"line":109,"address":[],"length":0,"stats":{"Line":12}},{"line":110,"address":[],"length":0,"stats":{"Line":1}},{"line":115,"address":[],"length":0,"stats":{"Line":5}},{"line":121,"address":[],"length":0,"stats":{"Line":18}},{"line":122,"address":[],"length":0,"stats":{"Line":2}},{"line":124,"address":[],"length":0,"stats":{"Line":10}},{"line":127,"address":[],"length":0,"stats":{"Line":18}},{"line":128,"address":[],"length":0,"stats":{"Line":12}},{"line":129,"address":[],"length":0,"stats":{"Line":12}},{"line":130,"address":[],"length":0,"stats":{"Line":12}},{"line":131,"address":[],"length":0,"stats":{"Line":12}},{"line":132,"address":[],"length":0,"stats":{"Line":12}},{"line":133,"address":[],"length":0,"stats":{"Line":12}},{"line":134,"address":[],"length":0,"stats":{"Line":12}},{"line":135,"address":[],"length":0,"stats":{"Line":6}},{"line":138,"address":[],"length":0,"stats":{"Line":11}},{"line":139,"address":[],"length":0,"stats":{"Line":15}},{"line":140,"address":[],"length":0,"stats":{"Line":15}},{"line":141,"address":[],"length":0,"stats":{"Line":15}},{"line":142,"address":[],"length":0,"stats":{"Line":15}},{"line":143,"address":[],"length":0,"stats":{"Line":15}},{"line":144,"address":[],"length":0,"stats":{"Line":15}},{"line":145,"address":[],"length":0,"stats":{"Line":5}},{"line":148,"address":[],"length":0,"stats":{"Line":3}},{"line":151,"address":[],"length":0,"stats":{"Line":18}},{"line":152,"address":[],"length":0,"stats":{"Line":18}},{"line":153,"address":[],"length":0,"stats":{"Line":12}},{"line":154,"address":[],"length":0,"stats":{"Line":18}},{"line":155,"address":[],"length":0,"stats":{"Line":18}},{"line":156,"address":[],"length":0,"stats":{"Line":18}},{"line":157,"address":[],"length":0,"stats":{"Line":18}},{"line":158,"address":[],"length":0,"stats":{"Line":18}},{"line":159,"address":[],"length":0,"stats":{"Line":18}},{"line":160,"address":[],"length":0,"stats":{"Line":18}},{"line":161,"address":[],"length":0,"stats":{"Line":18}},{"line":162,"address":[],"length":0,"stats":{"Line":18}},{"line":163,"address":[],"length":0,"stats":{"Line":18}},{"line":164,"address":[],"length":0,"stats":{"Line":18}},{"line":165,"address":[],"length":0,"stats":{"Line":12}},{"line":166,"address":[],"length":0,"stats":{"Line":6}},{"line":169,"address":[],"length":0,"stats":{"Line":6}},{"line":177,"address":[],"length":0,"stats":{"Line":11}},{"line":178,"address":[],"length":0,"stats":{"Line":11}},{"line":179,"address":[],"length":0,"stats":{"Line":5}},{"line":182,"address":[],"length":0,"stats":{"Line":22}},{"line":183,"address":[],"length":0,"stats":{"Line":12}},{"line":184,"address":[],"length":0,"stats":{"Line":3}},{"line":186,"address":[],"length":0,"stats":{"Line":3}},{"line":188,"address":[],"length":0,"stats":{"Line":1}},{"line":191,"address":[],"length":0,"stats":{"Line":6}},{"line":192,"address":[],"length":0,"stats":{"Line":3}},{"line":195,"address":[],"length":0,"stats":{"Line":6}},{"line":196,"address":[],"length":0,"stats":{"Line":3}},{"line":197,"address":[],"length":0,"stats":{"Line":0}},{"line":198,"address":[],"length":0,"stats":{"Line":3}},{"line":199,"address":[],"length":0,"stats":{"Line":1}},{"line":201,"address":[],"length":0,"stats":{"Line":2}},{"line":209,"address":[],"length":0,"stats":{"Line":7}},{"line":210,"address":[],"length":0,"stats":{"Line":14}},{"line":212,"address":[],"length":0,"stats":{"Line":35}},{"line":214,"address":[],"length":0,"stats":{"Line":7}},{"line":215,"address":[],"length":0,"stats":{"Line":4}},{"line":218,"address":[],"length":0,"stats":{"Line":2}},{"line":220,"address":[],"length":0,"stats":{"Line":15}},{"line":223,"address":[],"length":0,"stats":{"Line":10}},{"line":224,"address":[],"length":0,"stats":{"Line":10}},{"line":225,"address":[],"length":0,"stats":{"Line":0}},{"line":226,"address":[],"length":0,"stats":{"Line":0}},{"line":227,"address":[],"length":0,"stats":{"Line":0}},{"line":232,"address":[],"length":0,"stats":{"Line":5}},{"line":233,"address":[],"length":0,"stats":{"Line":10}},{"line":234,"address":[],"length":0,"stats":{"Line":5}},{"line":236,"address":[],"length":0,"stats":{"Line":30}},{"line":239,"address":[],"length":0,"stats":{"Line":10}},{"line":241,"address":[],"length":0,"stats":{"Line":15}},{"line":242,"address":[],"length":0,"stats":{"Line":25}},{"line":243,"address":[],"length":0,"stats":{"Line":10}},{"line":244,"address":[],"length":0,"stats":{"Line":10}},{"line":246,"address":[],"length":0,"stats":{"Line":0}},{"line":247,"address":[],"length":0,"stats":{"Line":0}},{"line":252,"address":[],"length":0,"stats":{"Line":15}},{"line":253,"address":[],"length":0,"stats":{"Line":10}},{"line":254,"address":[],"length":0,"stats":{"Line":10}},{"line":255,"address":[],"length":0,"stats":{"Line":15}},{"line":259,"address":[],"length":0,"stats":{"Line":15}},{"line":260,"address":[],"length":0,"stats":{"Line":10}},{"line":261,"address":[],"length":0,"stats":{"Line":10}},{"line":262,"address":[],"length":0,"stats":{"Line":15}},{"line":263,"address":[],"length":0,"stats":{"Line":13}},{"line":264,"address":[],"length":0,"stats":{"Line":3}},{"line":265,"address":[],"length":0,"stats":{"Line":3}},{"line":266,"address":[],"length":0,"stats":{"Line":6}},{"line":267,"address":[],"length":0,"stats":{"Line":0}},{"line":268,"address":[],"length":0,"stats":{"Line":0}},{"line":269,"address":[],"length":0,"stats":{"Line":0}},{"line":273,"address":[],"length":0,"stats":{"Line":0}},{"line":275,"address":[],"length":0,"stats":{"Line":6}},{"line":276,"address":[],"length":0,"stats":{"Line":0}},{"line":277,"address":[],"length":0,"stats":{"Line":0}},{"line":278,"address":[],"length":0,"stats":{"Line":0}},{"line":279,"address":[],"length":0,"stats":{"Line":0}},{"line":280,"address":[],"length":0,"stats":{"Line":0}},{"line":285,"address":[],"length":0,"stats":{"Line":15}},{"line":286,"address":[],"length":0,"stats":{"Line":24}},{"line":287,"address":[],"length":0,"stats":{"Line":2}},{"line":288,"address":[],"length":0,"stats":{"Line":6}},{"line":289,"address":[],"length":0,"stats":{"Line":2}},{"line":292,"address":[],"length":0,"stats":{"Line":3}},{"line":293,"address":[],"length":0,"stats":{"Line":3}},{"line":294,"address":[],"length":0,"stats":{"Line":1}},{"line":297,"address":[],"length":0,"stats":{"Line":6}},{"line":298,"address":[],"length":0,"stats":{"Line":0}},{"line":299,"address":[],"length":0,"stats":{"Line":0}},{"line":302,"address":[],"length":0,"stats":{"Line":4}},{"line":303,"address":[],"length":0,"stats":{"Line":2}},{"line":308,"address":[],"length":0,"stats":{"Line":5}},{"line":309,"address":[],"length":0,"stats":{"Line":408}},{"line":310,"address":[],"length":0,"stats":{"Line":20}},{"line":315,"address":[],"length":0,"stats":{"Line":5}},{"line":316,"address":[],"length":0,"stats":{"Line":15}},{"line":317,"address":[],"length":0,"stats":{"Line":5}},{"line":318,"address":[],"length":0,"stats":{"Line":5}},{"line":320,"address":[],"length":0,"stats":{"Line":0}},{"line":321,"address":[],"length":0,"stats":{"Line":0}},{"line":323,"address":[],"length":0,"stats":{"Line":0}},{"line":324,"address":[],"length":0,"stats":{"Line":0}},{"line":325,"address":[],"length":0,"stats":{"Line":0}},{"line":326,"address":[],"length":0,"stats":{"Line":0}},{"line":328,"address":[],"length":0,"stats":{"Line":0}}],"covered":114,"coverable":141},{"path":["/","Users","sunyu","Projects","oak","src","task","rust","src","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//! `RenderTask` abstract base, mirroring `src/task/src/render/render.h`.\n//!\n//! The abstract parent of [`crate::export::ExportTask`] and\n//! [`crate::precache::PreCacheTask`]. It renders frames/audio through\n//! oakrender tickets (`bridge::render`) and hands each result to a virtual\n//! hook (frame/audio downloaded, subtitle encoded) that the subclass\n//! implements via the [`RenderTaskBehavior`] trait.\n//!\n//! ## Concurrent render loop\n//!\n//! Like the C++ original (`render.cpp`), the loop keeps up to\n//! `max_inflight` tickets running at once (default\n//! `std::thread::available_parallelism()`, matching the C++\n//! `std::max(1, hardware_concurrency)`). Tickets report completion through\n//! the `oakrender_ticket_finished_fn` callback (fired on the ticket's own\n//! finishing thread), which pushes the finished ticket into a shared queue\n//! and wakes the render thread (queue + condvar, exactly the C++ design).\n//! Tickets may complete out of order; a **reorder buffer** delivers the\n//! results to the hooks in timestamp order — the audio range first (the C++\n//! queue-audio-first order), then every frame in ascending time — so the\n//! observable per-frame contract (`frame_downloaded`/`audio_downloaded` in\n//! order, progress updates, cancellation between frames) is unchanged.\n//!\n//! The ticket's finished callback hands the loop a **borrowed copy** of the\n//! submitter's handle (`include/render/ticket.h`: \"the submitter keeps\n//! ownership and releases it\"). The loop therefore releases exactly the\n//! handle value each submit function returns, once, and never releases the\n//! queue's borrowed copies. Cancellation (or a hook error) cancels and\n//! waits every in-flight ticket before returning, so every submitted\n//! ticket's completion still fires exactly once.\n//!\n//! CPP-PARITY: src/task/src/render/render.h\n\nuse std::collections::{HashMap, VecDeque};\nuse std::sync::atomic::{AtomicUsize, Ordering};\nuse std::sync::{Condvar, Mutex};\n\nuse crate::bridge;\nuse crate::bridge::common::OakColorTransform;\nuse crate::bridge::node::{cstr, OAKNODE_SEQUENCE_SAMPLES_INPUT, OAKNODE_SEQUENCE_TEXTURE_INPUT};\nuse crate::bridge::render::{OakRenderTicket, OakRenderVideoTicketParams};\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\nuse crate::task::Task;\nuse oakcore_rs::{Rational, TimeRange};\n\n/// `OAKRENDER_TICKET_VIDEO` (`include/render/ticket.h`).\nconst TICKET_VIDEO: i32 = 0;\n/// `OAKRENDER_TICKET_AUDIO` (`include/render/ticket.h`).\nconst TICKET_AUDIO: i32 = 1;\n\n/// Overrides applied to a render ticket, mirroring the C++ `ForceParams`\n/// struct in render.h (fields map onto `oakrender_video_ticket_params`).\n///\n/// CPP-PARITY: src/task/src/render/render.h (ForceParams)\n#[derive(Clone, Debug, Default)]\npub struct ForceParams {\n\t/// Forced output width (0 = off).\n\tpub force_width: i32,\n\t/// Forced output height (0 = off).\n\tpub force_height: i32,\n\t/// Forced color matrix (row-major 4x4); only used when\n\t/// `has_force_matrix` is set.\n\tpub force_matrix: [f64; 16],\n\t/// Whether `force_matrix` is in effect.\n\tpub has_force_matrix: bool,\n\t/// Forced pixel format (`oakcore_rs::PixelFormat` as int; -1 = off).\n\tpub force_format: i32,\n\t/// Forced channel count (0 = off).\n\tpub force_channel_count: i32,\n\t/// Forced color output (borrowed `OakColorProcessor`; empty = none).\n\tpub force_color_output: CHandle,\n}\n\n/// Subclass hooks, standing in for the C++ protected virtuals\n/// `download_frame`/`frame_downloaded`/`audio_downloaded`/`encode_subtitle`.\npub trait RenderTaskBehavior {\n\t/// Called for each rendered frame.\n\tfn frame_downloaded(\u0026mut self, task: \u0026mut Task, frame: CHandle) -\u003e Result\u003c()\u003e;\n\t/// Called for each rendered audio buffer.\n\tfn audio_downloaded(\u0026mut self, task: \u0026mut Task, buffer: CHandle) -\u003e Result\u003c()\u003e;\n\t/// Called to encode a subtitle.\n\tfn encode_subtitle(\u0026mut self, task: \u0026mut Task, text: \u0026str) -\u003e Result\u003c()\u003e;\n}\n\n/// The abstract render task base. Holds the shared [`Task`], the sequence\n/// output params, the viewer node, and the subclass behavior.\npub struct RenderTask {\n\t/// The shared task base.\n\tpub base: Task,\n\t/// Output video params (borrowed `OakVideoParams`).\n\tpub video_params: CHandle,\n\t/// Output audio params (borrowed `OakAudioParams`; may be empty).\n\tpub audio_params: CHandle,\n\t/// The viewer node being rendered (borrowed `OakNodeNode`).\n\tpub viewer: CHandle,\n\t/// Force params applied to every ticket.\n\tpub force_params: ForceParams,\n\t/// The subclass behavior.\n\tpub behavior: Option\u003cBox\u003cdyn RenderTaskBehavior + Send\u003e\u003e,\n\t// --- private render inputs (set by the subclass before render()) ---\n\t/// Borrowed color manager.\n\tcolor_manager: CHandle,\n\t/// Borrowed frame cache.\n\tcache: CHandle,\n\t/// `olive::RenderMode::Mode` as int.\n\tmode: i32,\n\t/// Whether audio ranges are rendered.\n\taudio_enabled: bool,\n\t/// The range rendered by [`RenderTask::render`].\n\texport_range: TimeRange,\n\t/// Whether the render loop emits progress itself (the export task\n\t/// disables it and reports progress per written frame).\n\tnative_progress_signalling: bool,\n\t/// Total number of frames in `export_range` (computed by `render`).\n\ttotal_frames: i64,\n\t/// Maximum render tickets kept in flight at once (the C++\n\t/// `maximum_rendered_frames`; defaults to `available_parallelism`).\n\tmax_inflight: usize,\n}\n\nimpl RenderTask {\n\t/// Create a render task with default (empty) render inputs.\n\tpub fn new(\n\t\tbase: Task,\n\t\tvideo_params: CHandle,\n\t\taudio_params: CHandle,\n\t\tviewer: CHandle,\n\t\tforce_params: ForceParams,\n\t\tbehavior: Option\u003cBox\u003cdyn RenderTaskBehavior + Send\u003e\u003e,\n\t) -\u003e RenderTask {\n\t\tRenderTask {\n\t\t\tbase,\n\t\t\tvideo_params,\n\t\t\taudio_params,\n\t\t\tviewer,\n\t\t\tforce_params,\n\t\t\tbehavior,\n\t\t\tcolor_manager: CHandle::null(),\n\t\t\tcache: CHandle::null(),\n\t\t\tmode: 0,\n\t\t\taudio_enabled: false,\n\t\t\texport_range: TimeRange::new(Rational::new(0, 1), Rational::new(0, 1)),\n\t\t\tnative_progress_signalling: true,\n\t\t\ttotal_frames: 0,\n\t\t\tmax_inflight: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).max(1),\n\t\t}\n\t}\n\n\t/// Configure the render inputs before [`RenderTask::render`] (called by\n\t/// the concrete subclasses' `run`).\n\tpub fn set_render_inputs(\n\t\t\u0026mut self,\n\t\tcolor_manager: CHandle,\n\t\tcache: CHandle,\n\t\tmode: i32,\n\t\taudio_enabled: bool,\n\t\texport_range: TimeRange,\n\t) {\n\t\tself.color_manager = color_manager;\n\t\tself.cache = cache;\n\t\tself.mode = mode;\n\t\tself.audio_enabled = audio_enabled;\n\t\tself.export_range = export_range;\n\t}\n\n\t/// Enable/disable the render loop's own progress signalling.\n\tpub fn set_native_progress_signalling(\u0026mut self, enabled: bool) {\n\t\tself.native_progress_signalling = enabled;\n\t}\n\n\t/// Override the number of render tickets kept in flight at once.\n\t///\n\t/// Defaults to `std::thread::available_parallelism()`, mirroring the\n\t/// C++ `std::max(1, int(std::thread::hardware_concurrency()))`. Primarily\n\t/// a test hook: a smaller window makes concurrent-completion tests\n\t/// deterministic regardless of the host core count.\n\tpub fn set_max_inflight(\u0026mut self, max: usize) {\n\t\tself.max_inflight = max.max(1);\n\t}\n\n\t/// The number of frames computed by the last [`RenderTask::render`] run.\n\tpub fn total_frames(\u0026self) -\u003e i64 {\n\t\tself.total_frames\n\t}\n\n\t/// Frame duration rational from the video params (falls back to 1/1\n\t/// when the params are empty/invalid).\n\tfn timebase(\u0026self) -\u003e Rational {\n\t\tlet mut num = 0;\n\t\tlet mut den = 1;\n\t\tunsafe {\n\t\t\tbridge::common::oakcommon_videoparams_frame_rate_as_time_base(\n\t\t\t\tself.video_params,\n\t\t\t\t\u0026mut num,\n\t\t\t\t\u0026mut den,\n\t\t\t);\n\t\t}\n\t\tif num \u003c= 0 || den \u003c= 0 {\n\t\t\tRational::new(1, 1)\n\t\t} else {\n\t\t\tRational::new(num as i64, den as i64)\n\t\t}\n\t}\n\n\t/// Submit one video frame ticket at `time` (mirrors the C++\n\t/// `start_video_ticket`). `dispatch` is the shared completion channel\n\t/// handed to the ticket's finished callback.\n\tfn submit_video_ticket(\n\t\t\u0026self,\n\t\ttime: Rational,\n\t\tdispatch: *mut RenderDispatch,\n\t) -\u003e Result\u003cOakRenderTicket\u003e {\n\t\tlet mut output_node = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_input_get_connected_node(\n\t\t\t\tself.viewer,\n\t\t\t\tcstr(OAKNODE_SEQUENCE_TEXTURE_INPUT),\n\t\t\t\t\u0026mut output_node,\n\t\t\t);\n\t\t}\n\t\tif output_node.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"No node connected to the viewer output\".to_string()));\n\t\t}\n\n\t\tlet audio_params_ptr = if self.audio_params.ctx.is_null() {\n\t\t\tstd::ptr::null()\n\t\t} else {\n\t\t\t\u0026self.audio_params as *const CHandle\n\t\t};\n\t\tlet force = \u0026self.force_params;\n\t\tlet params = OakRenderVideoTicketParams {\n\t\t\toutput_node,\n\t\t\tvideo_params: self.video_params,\n\t\t\taudio_params: audio_params_ptr,\n\t\t\ttime_num: time.numerator(),\n\t\t\ttime_den: time.denominator(),\n\t\t\tcolor_manager: self.color_manager,\n\t\t\tmode: self.mode,\n\t\t\tforce_width: force.force_width,\n\t\t\tforce_height: force.force_height,\n\t\t\tforce_matrix: force.force_matrix,\n\t\t\thas_force_matrix: if force.has_force_matrix { 1 } else { 0 },\n\t\t\tforce_format: force.force_format,\n\t\t\tforce_channel_count: force.force_channel_count,\n\t\t\tforce_color_output: bridge::render::OakColorProcessor::from_chandle(force.force_color_output),\n\t\t\tforce_color_transform: OakColorTransform {\n\t\t\t\tctx: std::ptr::null_mut(),\n\t\t\t\taddref: None,\n\t\t\t\trelease: None,\n\t\t\t\tabi_version: 0,\n\t\t\t},\n\t\t\tcache: bridge::render::OakRenderCache::from_chandle(self.cache),\n\t\t};\n\n\t\tlet ticket = unsafe {\n\t\t\tbridge::render::oakrender_ticket_render_frame(\n\t\t\t\t\u0026params,\n\t\t\t\tSome(ticket_finished),\n\t\t\t\tdispatch as *mut std::ffi::c_void,\n\t\t\t)\n\t\t};\n\t\t// Per-frame call: release the borrowed handle box (the ticket keeps\n\t\t// the native node, not the handle).\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_free(\u0026mut output_node);\n\t\t}\n\t\tif ticket.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"Failed to start frame render ticket\".to_string()));\n\t\t}\n\t\tOk(ticket)\n\t}\n\n\t/// Submit one audio ticket for `range` (the Rust API renders a single\n\t/// range; the C++ submits one ticket per audio range).\n\tfn submit_audio_ticket(\n\t\t\u0026self,\n\t\trange: TimeRange,\n\t\tdispatch: *mut RenderDispatch,\n\t) -\u003e Result\u003cOakRenderTicket\u003e {\n\t\tlet mut output_node = CHandle::null();\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_input_get_connected_node(\n\t\t\t\tself.viewer,\n\t\t\t\tcstr(OAKNODE_SEQUENCE_SAMPLES_INPUT),\n\t\t\t\t\u0026mut output_node,\n\t\t\t);\n\t\t}\n\t\tif output_node.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"No node connected to the viewer samples input\".to_string()));\n\t\t}\n\n\t\tlet audio_params_ptr = if self.audio_params.ctx.is_null() {\n\t\t\tstd::ptr::null()\n\t\t} else {\n\t\t\t\u0026self.audio_params as *const CHandle\n\t\t};\n\t\tlet ticket = unsafe {\n\t\t\tbridge::render::oakrender_ticket_render_audio(\n\t\t\t\toutput_node,\n\t\t\t\trange.in_().numerator(),\n\t\t\t\trange.in_().denominator(),\n\t\t\t\trange.out().numerator(),\n\t\t\t\trange.out().denominator(),\n\t\t\t\taudio_params_ptr,\n\t\t\t\tself.mode,\n\t\t\t\tSome(ticket_finished),\n\t\t\t\tdispatch as *mut std::ffi::c_void,\n\t\t\t)\n\t\t};\n\t\tunsafe {\n\t\t\tbridge::node::oaknode_node_free(\u0026mut output_node);\n\t\t}\n\t\tif ticket.ctx.is_null() {\n\t\t\treturn Err(Error::Failed(\"Failed to start audio render ticket\".to_string()));\n\t\t}\n\t\tOk(ticket)\n\t}\n\n\t/// Submit a video ticket and account it as in-flight.\n\t///\n\t/// The in-flight count is bumped **before** the submit: a ticket can\n\t/// complete synchronously (its callback fires before the submit returns),\n\t/// so the callback's decrement must always see its increment. A failed\n\t/// submit (null ticket) never fires a callback, so the bump is rolled\n\t/// back.\n\tfn start_video_ticket(\n\t\t\u0026self,\n\t\ttime: Rational,\n\t\tdispatch: *mut RenderDispatch,\n\t\tin_flight: \u0026mut Vec\u003cOakRenderTicket\u003e,\n\t) -\u003e Result\u003c()\u003e {\n\t\tunsafe {\n\t\t\t(\u0026*dispatch).running.fetch_add(1, Ordering::SeqCst);\n\t\t}\n\t\tmatch self.submit_video_ticket(time, dispatch) {\n\t\t\tOk(ticket) =\u003e {\n\t\t\t\tin_flight.push(ticket);\n\t\t\t\tOk(())\n\t\t\t}\n\t\t\tErr(e) =\u003e {\n\t\t\t\tunsafe {\n\t\t\t\t\t(\u0026*dispatch).running.fetch_sub(1, Ordering::SeqCst);\n\t\t\t\t}\n\t\t\t\tErr(e)\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Submit the audio ticket and account it as in-flight (see\n\t/// [`RenderTask::start_video_ticket`] for the counting contract).\n\tfn start_audio_ticket(\n\t\t\u0026self,\n\t\trange: TimeRange,\n\t\tdispatch: *mut RenderDispatch,\n\t\tin_flight: \u0026mut Vec\u003cOakRenderTicket\u003e,\n\t) -\u003e Result\u003c()\u003e {\n\t\tunsafe {\n\t\t\t(\u0026*dispatch).running.fetch_add(1, Ordering::SeqCst);\n\t\t}\n\t\tmatch self.submit_audio_ticket(range, dispatch) {\n\t\t\tOk(ticket) =\u003e {\n\t\t\t\tin_flight.push(ticket);\n\t\t\t\tOk(())\n\t\t\t}\n\t\t\tErr(e) =\u003e {\n\t\t\t\tunsafe {\n\t\t\t\t\t(\u0026*dispatch).running.fetch_sub(1, Ordering::SeqCst);\n\t\t\t\t}\n\t\t\t\tErr(e)\n\t\t\t}\n\t\t}\n\t}\n\n\t/// Map a finished ticket back to its delivery slot: the audio slot for\n\t/// audio tickets, the matching frame slot for video tickets.\n\tfn classify_ticket(\n\t\t\u0026self,\n\t\tticket: \u0026OakRenderTicket,\n\t\tslot_by_key: \u0026HashMap\u003c(i32, i64, i64), usize\u003e,\n\t) -\u003e Option\u003cusize\u003e {\n\t\tlet kind = unsafe { bridge::render::oakrender_ticket_get_type(*ticket) };\n\t\tif kind == TICKET_AUDIO {\n\t\t\tlet mut a = 0i64;\n\t\t\tlet mut b = 1i64;\n\t\t\tlet mut c = 0i64;\n\t\t\tlet mut d = 1i64;\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_ticket_get_range(*ticket, \u0026mut a, \u0026mut b, \u0026mut c, \u0026mut d);\n\t\t\t}\n\t\t\tslot_by_key.get(\u0026(TICKET_AUDIO, a, b)).copied()\n\t\t} else if kind == TICKET_VIDEO {\n\t\t\tlet mut n = 0i64;\n\t\t\tlet mut d = 1i64;\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_ticket_get_time(*ticket, \u0026mut n, \u0026mut d);\n\t\t\t}\n\t\t\tslot_by_key.get(\u0026(TICKET_VIDEO, n, d)).copied()\n\t\t} else {\n\t\t\tNone\n\t\t}\n\t}\n\n\t/// Drive the whole render: keep up to `max_inflight` frame tickets in\n\t/// flight (audio first, then frames in timestamp order), deliver each\n\t/// finished ticket's result to the behavior hooks in timestamp order via\n\t/// the reorder buffer, and stop on cancellation or a hook error —\n\t/// cancelling and waiting every still-running ticket so their\n\t/// completions fire exactly once.\n\t///\n\t/// `task` is the live driving task (cancellation/progress/error\n\t/// reporting); `behavior` is the concrete subclass receiving the\n\t/// `frame_downloaded`/`audio_downloaded` hooks.\n\tpub fn render(\u0026mut self, task: \u0026mut Task, behavior: \u0026mut dyn RenderTaskBehavior) -\u003e Result\u003c()\u003e {\n\t\tlet timebase = self.timebase();\n\n\t\t// Compute the frame timestamps (progress denominator mirrors the\n\t\t// C++ `total_length \u003c= 0 -\u003e 1` guard).\n\t\tlet mut frame_times: Vec\u003cRational\u003e = Vec::new();\n\t\tlet mut t = self.export_range.in_();\n\t\twhile t \u003c self.export_range.out() {\n\t\t\tframe_times.push(t);\n\t\t\tt = t + timebase;\n\t\t}\n\t\tself.total_frames = frame_times.len() as i64;\n\t\tlet total_length = if frame_times.is_empty() { 1.0 } else { frame_times.len() as f64 };\n\n\t\t// Delivery order: the audio range first (mirrors the C++ queue\n\t\t// order), then every frame in ascending timestamp order. The reorder\n\t\t// buffer delivers to the hooks in exactly this order no matter the\n\t\t// completion order.\n\t\tlet mut slots: Vec\u003cDeliverySlot\u003e = Vec::new();\n\t\tif self.audio_enabled {\n\t\t\tslots.push(DeliverySlot {\n\t\t\t\tkind: TICKET_AUDIO,\n\t\t\t\ttime: self.export_range.in_(),\n\t\t\t});\n\t\t}\n\t\tfor \u0026ft in \u0026frame_times {\n\t\t\tslots.push(DeliverySlot {\n\t\t\t\tkind: TICKET_VIDEO,\n\t\t\t\ttime: ft,\n\t\t\t});\n\t\t}\n\t\tlet mut slot_by_key: HashMap\u003c(i32, i64, i64), usize\u003e = HashMap::with_capacity(slots.len());\n\t\tfor (i, slot) in slots.iter().enumerate() {\n\t\t\tslot_by_key.insert((slot.kind, slot.time.numerator(), slot.time.denominator()), i);\n\t\t}\n\t\tlet total_slots = slots.len();\n\n\t\t// The shared completion channel. Heap-allocated: the C callback\n\t\t// reaches it through its `userdata` pointer. Freed only once every\n\t\t// in-flight ticket has fired its callback (`running == 0`).\n\t\tlet dispatch = Box::into_raw(Box::new(RenderDispatch::new()));\n\t\t// Shared view of the same state for the render thread (the raw\n\t\t// pointer stays valid until the box is freed below).\n\t\tlet dispatch_ref = unsafe { \u0026*dispatch };\n\t\tlet window = self.max_inflight.max(1);\n\n\t\t// Tickets we hold (the submitter's copy of each handle, released\n\t\t// exactly once at the end); `consumed` counts tickets whose finished\n\t\t// queue copy has been popped, so `in_flight.len() - consumed` is the\n\t\t// live window.\n\t\tlet mut in_flight: Vec\u003cOakRenderTicket\u003e = Vec::new();\n\t\tlet mut consumed = 0usize;\n\t\t// Next frame timestamp to submit and next delivery slot.\n\t\tlet mut next_frame_index = 0usize;\n\t\tlet mut next_slot = 0usize;\n\t\t// Reorder buffer: finished tickets not yet deliverable.\n\t\tlet mut pending: HashMap\u003cusize, OakRenderTicket\u003e = HashMap::new();\n\t\tlet mut progress_counter = 0.0;\n\t\tlet mut result: Result\u003c()\u003e = Ok(());\n\n\t\t// Queue audio first (mirrors the C++ order).\n\t\tif self.audio_enabled \u0026\u0026 result.is_ok() {\n\t\t\tif let Err(e) = self.start_audio_ticket(self.export_range, dispatch, \u0026mut in_flight) {\n\t\t\t\tresult = Err(e);\n\t\t\t}\n\t\t}\n\n\t\t// Start the initial frame window.\n\t\tif result.is_ok() {\n\t\t\twhile in_flight.len() \u003c window \u0026\u0026 next_frame_index \u003c frame_times.len() {\n\t\t\t\tif let Err(e) =\n\t\t\t\t\tself.start_video_ticket(frame_times[next_frame_index], dispatch, \u0026mut in_flight)\n\t\t\t\t{\n\t\t\t\t\tresult = Err(e);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tnext_frame_index += 1;\n\t\t\t}\n\t\t}\n\n\t\twhile result.is_ok() \u0026\u0026 !task.is_cancelled() \u0026\u0026 next_slot \u003c total_slots {\n\t\t\t// Drain the completion queue into the reorder buffer.\n\t\t\twhile let Some(ticket) = dispatch_ref.pop_finished() {\n\t\t\t\tconsumed += 1;\n\t\t\t\tmatch self.classify_ticket(\u0026ticket, \u0026slot_by_key) {\n\t\t\t\t\tSome(slot_index) =\u003e {\n\t\t\t\t\t\tpending.insert(slot_index, ticket);\n\t\t\t\t\t}\n\t\t\t\t\tNone =\u003e {\n\t\t\t\t\t\tresult = Err(Error::Failed(\n\t\t\t\t\t\t\t\"Render ticket reported an unexpected timestamp\".to_string(),\n\t\t\t\t\t\t));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif result.is_err() {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Deliver contiguous results in the observable (timestamp) order.\n\t\t\twhile let Some(ticket) = pending.remove(\u0026next_slot) {\n\t\t\t\tlet slot = \u0026slots[next_slot];\n\t\t\t\tif slot.kind == TICKET_AUDIO {\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::render::oakrender_ticket_get_samples(ticket, std::ptr::null_mut());\n\t\t\t\t\t}\n\t\t\t\t\tif let Err(e) = behavior.audio_downloaded(task, CHandle::null()) {\n\t\t\t\t\t\tresult = Err(e);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlet mut frame = CHandle::null();\n\t\t\t\t\tunsafe {\n\t\t\t\t\t\tbridge::render::oakrender_ticket_get_frame(ticket, \u0026mut frame);\n\t\t\t\t\t}\n\t\t\t\t\tif let Err(e) = behavior.frame_downloaded(task, frame) {\n\t\t\t\t\t\tresult = Err(e);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif !frame.ctx.is_null() {\n\t\t\t\t\t\tunsafe {\n\t\t\t\t\t\t\tbridge::render::oakrender_codec_frame_free(\u0026mut frame);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif self.native_progress_signalling {\n\t\t\t\t\t\tprogress_counter += 1.0;\n\t\t\t\t\t\ttask.emit_progress(progress_counter / total_length);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnext_slot += 1;\n\t\t\t}\n\t\t\tif result.is_err() || task.is_cancelled() {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Refill the window: one new ticket per delivered frame.\n\t\t\twhile in_flight.len().saturating_sub(consumed) \u003c window\n\t\t\t\t\u0026\u0026 next_frame_index \u003c frame_times.len()\n\t\t\t{\n\t\t\t\tif let Err(e) =\n\t\t\t\t\tself.start_video_ticket(frame_times[next_frame_index], dispatch, \u0026mut in_flight)\n\t\t\t\t{\n\t\t\t\t\tresult = Err(e);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tnext_frame_index += 1;\n\t\t\t}\n\t\t\tif result.is_err() {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif next_slot \u003e= total_slots {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Wait for the next completion (a cancellation or a hook error\n\t\t\t// aborts the wait; in-flight tickets then finish, waking us).\n\t\t\tlet mut guard = dispatch_ref.finished.lock().unwrap_or_else(|e| e.into_inner());\n\t\t\twhile guard.is_empty()\n\t\t\t\t\u0026\u0026 dispatch_ref.running.load(Ordering::SeqCst) \u003e 0\n\t\t\t\t\u0026\u0026 !task.is_cancelled()\n\t\t\t{\n\t\t\t\tguard = dispatch_ref.cv.wait(guard).unwrap_or_else(|e| e.into_inner());\n\t\t\t}\n\t\t\tdrop(guard);\n\t\t\tif dispatch_ref.finished.lock().unwrap_or_else(|e| e.into_inner()).is_empty()\n\t\t\t\t\u0026\u0026 dispatch_ref.running.load(Ordering::SeqCst) == 0\n\t\t\t{\n\t\t\t\t// Every ticket finished and its queue copy was consumed.\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Cancellation that aborted the loop before every slot was delivered\n\t\t// maps to the cancellation error (the sync loop returned\n\t\t// `Error::Cancelled` from its between-frames check; a render that\n\t\t// finished every frame despite a late cancel stays successful).\n\t\tif result.is_ok() \u0026\u0026 task.is_cancelled() \u0026\u0026 next_slot \u003c total_slots {\n\t\t\tresult = Err(Error::Cancelled);\n\t\t}\n\n\t\t// Tear down. On cancellation or error, cancel and wait every ticket\n\t\t// still in flight (the C++ abort path), so their completions still\n\t\t// fire exactly once. Then wait until every callback has returned,\n\t\t// release the submitter's handles, and free the completion channel.\n\t\tif result.is_err() || task.is_cancelled() {\n\t\t\tfor \u0026ticket in \u0026in_flight {\n\t\t\t\tunsafe {\n\t\t\t\t\tbridge::render::oakrender_ticket_cancel(ticket);\n\t\t\t\t\tbridge::render::oakrender_ticket_wait(ticket);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tdispatch_ref.wait_idle();\n\t\tfor ticket in in_flight.iter_mut() {\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_ticket_free(ticket);\n\t\t\t}\n\t\t}\n\t\tunsafe {\n\t\t\tdrop(Box::from_raw(dispatch));\n\t\t}\n\n\t\tresult\n\t}\n\n\t/// A detached render state used while a concrete task temporarily moves\n\t/// its render out of itself to drive it with itself as the behavior\n\t/// (avoids a self-referential borrow). Never used for actual rendering.\n\tpub(crate) fn placeholder() -\u003e RenderTask {\n\t\tRenderTask::new(\n\t\t\tTask::new(\"\", CHandle::null()),\n\t\t\tCHandle::null(),\n\t\t\tCHandle::null(),\n\t\t\tCHandle::null(),\n\t\t\tForceParams::default(),\n\t\t\tNone,\n\t\t)\n\t}\n}\n\n/// One deliverable unit of the render: an audio range or a frame time, in\n/// the observable delivery order (audio first, then frames in time order).\nstruct DeliverySlot {\n\t/// `TICKET_AUDIO` or `TICKET_VIDEO`.\n\tkind: i32,\n\t/// Frame time (video) or range start (audio).\n\ttime: Rational,\n}\n\n/// Shared state between the render thread and the ticket-finished\n/// callbacks (the ticket's async return channel, mirroring the C++\n/// `finished_mutex_`/`finished_tickets_`/`finished_wait_cond_`). One\n/// instance per [`RenderTask::render`] run, reached from the C callback\n/// through its `userdata` pointer; heap-allocated for the run and freed\n/// only after every in-flight ticket has fired (`running == 0`).\nstruct RenderDispatch {\n\t/// Finished tickets in completion order (callbacks push here).\n\tfinished: Mutex\u003cVecDeque\u003cOakRenderTicket\u003e\u003e,\n\t/// Tickets whose completion callback has not fired yet.\n\trunning: AtomicUsize,\n\t/// Wakes the render thread when a ticket finishes.\n\tcv: Condvar,\n}\n\nimpl RenderDispatch {\n\tfn new() -\u003e RenderDispatch {\n\t\tRenderDispatch {\n\t\t\tfinished: Mutex::new(VecDeque::new()),\n\t\t\trunning: AtomicUsize::new(0),\n\t\t\tcv: Condvar::new(),\n\t\t}\n\t}\n\n\t/// Pop the next finished ticket (the callback's borrowed copy; `None`\n\t/// when the queue is empty).\n\tfn pop_finished(\u0026self) -\u003e Option\u003cOakRenderTicket\u003e {\n\t\tself.finished\n\t\t\t.lock()\n\t\t\t.unwrap_or_else(|e| e.into_inner())\n\t\t\t.pop_front()\n\t}\n\n\t/// Block until every submitted ticket has fired its callback. Called\n\t/// before freeing `self`, so no callback can touch the state afterwards.\n\tfn wait_idle(\u0026self) {\n\t\tlet mut guard = self.finished.lock().unwrap_or_else(|e| e.into_inner());\n\t\twhile self.running.load(Ordering::SeqCst) \u003e 0 {\n\t\t\tguard = self.cv.wait(guard).unwrap_or_else(|e| e.into_inner());\n\t\t}\n\t}\n}\n\n/// Ticket-finished callback, mirroring the C++ `on_ticket_finished`. Fires\n/// on the ticket's finishing thread; pushes the ticket's borrowed handle\n/// copy into the completion queue and wakes the render thread. The push\n/// precedes the in-flight decrement, so a waiter never observes\n/// `running == 0` with a missing queue entry.\n///\n/// # Safety\n///\n/// `userdata` must point to a live `RenderDispatch` for the duration of\n/// every in-flight ticket (guaranteed by [`RenderTask::render`]'s\n/// `wait_idle` before freeing it).\nunsafe extern \"C\" fn ticket_finished(ticket: OakRenderTicket, userdata: *mut std::ffi::c_void) {\n\tlet dispatch = unsafe { \u0026*(userdata as *const RenderDispatch) };\n\tlet mut queue = dispatch.finished.lock().unwrap_or_else(|e| e.into_inner());\n\tqueue.push_back(ticket);\n\tdispatch.running.fetch_sub(1, Ordering::SeqCst);\n\tdispatch.cv.notify_all();\n}\n","traces":[{"line":140,"address":[],"length":0,"stats":{"Line":23}},{"line":155,"address":[],"length":0,"stats":{"Line":23}},{"line":156,"address":[],"length":0,"stats":{"Line":23}},{"line":159,"address":[],"length":0,"stats":{"Line":69}},{"line":162,"address":[],"length":0,"stats":{"Line":138}},{"line":168,"address":[],"length":0,"stats":{"Line":14}},{"line":176,"address":[],"length":0,"stats":{"Line":14}},{"line":177,"address":[],"length":0,"stats":{"Line":14}},{"line":178,"address":[],"length":0,"stats":{"Line":14}},{"line":179,"address":[],"length":0,"stats":{"Line":14}},{"line":180,"address":[],"length":0,"stats":{"Line":14}},{"line":184,"address":[],"length":0,"stats":{"Line":0}},{"line":185,"address":[],"length":0,"stats":{"Line":0}},{"line":194,"address":[],"length":0,"stats":{"Line":7}},{"line":195,"address":[],"length":0,"stats":{"Line":7}},{"line":199,"address":[],"length":0,"stats":{"Line":0}},{"line":200,"address":[],"length":0,"stats":{"Line":0}},{"line":205,"address":[],"length":0,"stats":{"Line":14}},{"line":206,"address":[],"length":0,"stats":{"Line":28}},{"line":207,"address":[],"length":0,"stats":{"Line":28}},{"line":210,"address":[],"length":0,"stats":{"Line":28}},{"line":211,"address":[],"length":0,"stats":{"Line":14}},{"line":212,"address":[],"length":0,"stats":{"Line":14}},{"line":215,"address":[],"length":0,"stats":{"Line":27}},{"line":216,"address":[],"length":0,"stats":{"Line":1}},{"line":218,"address":[],"length":0,"stats":{"Line":39}},{"line":225,"address":[],"length":0,"stats":{"Line":84}},{"line":230,"address":[],"length":0,"stats":{"Line":168}},{"line":233,"address":[],"length":0,"stats":{"Line":168}},{"line":234,"address":[],"length":0,"stats":{"Line":168}},{"line":235,"address":[],"length":0,"stats":{"Line":84}},{"line":238,"address":[],"length":0,"stats":{"Line":168}},{"line":239,"address":[],"length":0,"stats":{"Line":1}},{"line":242,"address":[],"length":0,"stats":{"Line":249}},{"line":243,"address":[],"length":0,"stats":{"Line":83}},{"line":245,"address":[],"length":0,"stats":{"Line":0}},{"line":247,"address":[],"length":0,"stats":{"Line":166}},{"line":250,"address":[],"length":0,"stats":{"Line":83}},{"line":252,"address":[],"length":0,"stats":{"Line":166}},{"line":253,"address":[],"length":0,"stats":{"Line":166}},{"line":254,"address":[],"length":0,"stats":{"Line":83}},{"line":255,"address":[],"length":0,"stats":{"Line":83}},{"line":256,"address":[],"length":0,"stats":{"Line":83}},{"line":257,"address":[],"length":0,"stats":{"Line":83}},{"line":258,"address":[],"length":0,"stats":{"Line":83}},{"line":259,"address":[],"length":0,"stats":{"Line":166}},{"line":260,"address":[],"length":0,"stats":{"Line":83}},{"line":261,"address":[],"length":0,"stats":{"Line":83}},{"line":262,"address":[],"length":0,"stats":{"Line":166}},{"line":263,"address":[],"length":0,"stats":{"Line":83}},{"line":269,"address":[],"length":0,"stats":{"Line":166}},{"line":274,"address":[],"length":0,"stats":{"Line":83}},{"line":275,"address":[],"length":0,"stats":{"Line":83}},{"line":276,"address":[],"length":0,"stats":{"Line":83}},{"line":282,"address":[],"length":0,"stats":{"Line":83}},{"line":284,"address":[],"length":0,"stats":{"Line":166}},{"line":285,"address":[],"length":0,"stats":{"Line":0}},{"line":287,"address":[],"length":0,"stats":{"Line":83}},{"line":292,"address":[],"length":0,"stats":{"Line":2}},{"line":297,"address":[],"length":0,"stats":{"Line":4}},{"line":300,"address":[],"length":0,"stats":{"Line":4}},{"line":301,"address":[],"length":0,"stats":{"Line":4}},{"line":302,"address":[],"length":0,"stats":{"Line":2}},{"line":305,"address":[],"length":0,"stats":{"Line":4}},{"line":306,"address":[],"length":0,"stats":{"Line":0}},{"line":309,"address":[],"length":0,"stats":{"Line":6}},{"line":310,"address":[],"length":0,"stats":{"Line":2}},{"line":312,"address":[],"length":0,"stats":{"Line":0}},{"line":316,"address":[],"length":0,"stats":{"Line":2}},{"line":317,"address":[],"length":0,"stats":{"Line":6}},{"line":318,"address":[],"length":0,"stats":{"Line":6}},{"line":319,"address":[],"length":0,"stats":{"Line":6}},{"line":320,"address":[],"length":0,"stats":{"Line":6}},{"line":321,"address":[],"length":0,"stats":{"Line":2}},{"line":322,"address":[],"length":0,"stats":{"Line":2}},{"line":323,"address":[],"length":0,"stats":{"Line":2}},{"line":324,"address":[],"length":0,"stats":{"Line":2}},{"line":328,"address":[],"length":0,"stats":{"Line":2}},{"line":330,"address":[],"length":0,"stats":{"Line":4}},{"line":331,"address":[],"length":0,"stats":{"Line":0}},{"line":333,"address":[],"length":0,"stats":{"Line":2}},{"line":343,"address":[],"length":0,"stats":{"Line":84}},{"line":350,"address":[],"length":0,"stats":{"Line":168}},{"line":352,"address":[],"length":0,"stats":{"Line":252}},{"line":353,"address":[],"length":0,"stats":{"Line":83}},{"line":354,"address":[],"length":0,"stats":{"Line":249}},{"line":355,"address":[],"length":0,"stats":{"Line":83}},{"line":357,"address":[],"length":0,"stats":{"Line":1}},{"line":359,"address":[],"length":0,"stats":{"Line":2}},{"line":361,"address":[],"length":0,"stats":{"Line":1}},{"line":368,"address":[],"length":0,"stats":{"Line":2}},{"line":375,"address":[],"length":0,"stats":{"Line":4}},{"line":377,"address":[],"length":0,"stats":{"Line":6}},{"line":378,"address":[],"length":0,"stats":{"Line":2}},{"line":379,"address":[],"length":0,"stats":{"Line":6}},{"line":380,"address":[],"length":0,"stats":{"Line":2}},{"line":382,"address":[],"length":0,"stats":{"Line":0}},{"line":384,"address":[],"length":0,"stats":{"Line":0}},{"line":386,"address":[],"length":0,"stats":{"Line":0}},{"line":393,"address":[],"length":0,"stats":{"Line":82}},{"line":398,"address":[],"length":0,"stats":{"Line":246}},{"line":399,"address":[],"length":0,"stats":{"Line":82}},{"line":400,"address":[],"length":0,"stats":{"Line":4}},{"line":401,"address":[],"length":0,"stats":{"Line":4}},{"line":402,"address":[],"length":0,"stats":{"Line":4}},{"line":403,"address":[],"length":0,"stats":{"Line":4}},{"line":405,"address":[],"length":0,"stats":{"Line":10}},{"line":407,"address":[],"length":0,"stats":{"Line":8}},{"line":408,"address":[],"length":0,"stats":{"Line":80}},{"line":409,"address":[],"length":0,"stats":{"Line":160}},{"line":410,"address":[],"length":0,"stats":{"Line":160}},{"line":412,"address":[],"length":0,"stats":{"Line":240}},{"line":414,"address":[],"length":0,"stats":{"Line":320}},{"line":416,"address":[],"length":0,"stats":{"Line":0}},{"line":430,"address":[],"length":0,"stats":{"Line":14}},{"line":431,"address":[],"length":0,"stats":{"Line":42}},{"line":435,"address":[],"length":0,"stats":{"Line":42}},{"line":436,"address":[],"length":0,"stats":{"Line":42}},{"line":437,"address":[],"length":0,"stats":{"Line":391}},{"line":438,"address":[],"length":0,"stats":{"Line":484}},{"line":439,"address":[],"length":0,"stats":{"Line":121}},{"line":441,"address":[],"length":0,"stats":{"Line":14}},{"line":442,"address":[],"length":0,"stats":{"Line":56}},{"line":448,"address":[],"length":0,"stats":{"Line":42}},{"line":449,"address":[],"length":0,"stats":{"Line":16}},{"line":450,"address":[],"length":0,"stats":{"Line":6}},{"line":451,"address":[],"length":0,"stats":{"Line":2}},{"line":452,"address":[],"length":0,"stats":{"Line":2}},{"line":455,"address":[],"length":0,"stats":{"Line":256}},{"line":456,"address":[],"length":0,"stats":{"Line":242}},{"line":457,"address":[],"length":0,"stats":{"Line":121}},{"line":458,"address":[],"length":0,"stats":{"Line":121}},{"line":461,"address":[],"length":0,"stats":{"Line":70}},{"line":462,"address":[],"length":0,"stats":{"Line":397}},{"line":463,"address":[],"length":0,"stats":{"Line":738}},{"line":465,"address":[],"length":0,"stats":{"Line":42}},{"line":470,"address":[],"length":0,"stats":{"Line":56}},{"line":473,"address":[],"length":0,"stats":{"Line":28}},{"line":474,"address":[],"length":0,"stats":{"Line":42}},{"line":480,"address":[],"length":0,"stats":{"Line":42}},{"line":481,"address":[],"length":0,"stats":{"Line":28}},{"line":483,"address":[],"length":0,"stats":{"Line":28}},{"line":484,"address":[],"length":0,"stats":{"Line":28}},{"line":486,"address":[],"length":0,"stats":{"Line":42}},{"line":487,"address":[],"length":0,"stats":{"Line":28}},{"line":488,"address":[],"length":0,"stats":{"Line":42}},{"line":491,"address":[],"length":0,"stats":{"Line":18}},{"line":492,"address":[],"length":0,"stats":{"Line":8}},{"line":493,"address":[],"length":0,"stats":{"Line":0}},{"line":498,"address":[],"length":0,"stats":{"Line":28}},{"line":499,"address":[],"length":0,"stats":{"Line":280}},{"line":500,"address":[],"length":0,"stats":{"Line":1}},{"line":501,"address":[],"length":0,"stats":{"Line":244}},{"line":503,"address":[],"length":0,"stats":{"Line":2}},{"line":504,"address":[],"length":0,"stats":{"Line":1}},{"line":506,"address":[],"length":0,"stats":{"Line":60}},{"line":510,"address":[],"length":0,"stats":{"Line":101}},{"line":512,"address":[],"length":0,"stats":{"Line":186}},{"line":513,"address":[],"length":0,"stats":{"Line":82}},{"line":514,"address":[],"length":0,"stats":{"Line":246}},{"line":515,"address":[],"length":0,"stats":{"Line":164}},{"line":516,"address":[],"length":0,"stats":{"Line":246}},{"line":519,"address":[],"length":0,"stats":{"Line":0}},{"line":520,"address":[],"length":0,"stats":{"Line":0}},{"line":522,"address":[],"length":0,"stats":{"Line":0}},{"line":526,"address":[],"length":0,"stats":{"Line":44}},{"line":527,"address":[],"length":0,"stats":{"Line":0}},{"line":531,"address":[],"length":0,"stats":{"Line":251}},{"line":532,"address":[],"length":0,"stats":{"Line":142}},{"line":533,"address":[],"length":0,"stats":{"Line":71}},{"line":535,"address":[],"length":0,"stats":{"Line":4}},{"line":537,"address":[],"length":0,"stats":{"Line":6}},{"line":538,"address":[],"length":0,"stats":{"Line":0}},{"line":539,"address":[],"length":0,"stats":{"Line":0}},{"line":542,"address":[],"length":0,"stats":{"Line":138}},{"line":544,"address":[],"length":0,"stats":{"Line":138}},{"line":546,"address":[],"length":0,"stats":{"Line":210}},{"line":547,"address":[],"length":0,"stats":{"Line":6}},{"line":548,"address":[],"length":0,"stats":{"Line":3}},{"line":550,"address":[],"length":0,"stats":{"Line":66}},{"line":552,"address":[],"length":0,"stats":{"Line":59}},{"line":555,"address":[],"length":0,"stats":{"Line":132}},{"line":556,"address":[],"length":0,"stats":{"Line":132}},{"line":557,"address":[],"length":0,"stats":{"Line":198}},{"line":560,"address":[],"length":0,"stats":{"Line":68}},{"line":562,"address":[],"length":0,"stats":{"Line":82}},{"line":563,"address":[],"length":0,"stats":{"Line":3}},{"line":567,"address":[],"length":0,"stats":{"Line":168}},{"line":568,"address":[],"length":0,"stats":{"Line":68}},{"line":570,"address":[],"length":0,"stats":{"Line":0}},{"line":571,"address":[],"length":0,"stats":{"Line":92}},{"line":573,"address":[],"length":0,"stats":{"Line":0}},{"line":574,"address":[],"length":0,"stats":{"Line":0}},{"line":576,"address":[],"length":0,"stats":{"Line":23}},{"line":578,"address":[],"length":0,"stats":{"Line":38}},{"line":579,"address":[],"length":0,"stats":{"Line":0}},{"line":581,"address":[],"length":0,"stats":{"Line":19}},{"line":582,"address":[],"length":0,"stats":{"Line":7}},{"line":587,"address":[],"length":0,"stats":{"Line":48}},{"line":588,"address":[],"length":0,"stats":{"Line":19}},{"line":589,"address":[],"length":0,"stats":{"Line":14}},{"line":590,"address":[],"length":0,"stats":{"Line":7}},{"line":592,"address":[],"length":0,"stats":{"Line":35}},{"line":594,"address":[],"length":0,"stats":{"Line":24}},{"line":595,"address":[],"length":0,"stats":{"Line":48}},{"line":596,"address":[],"length":0,"stats":{"Line":0}},{"line":599,"address":[],"length":0,"stats":{"Line":0}},{"line":607,"address":[],"length":0,"stats":{"Line":50}},{"line":608,"address":[],"length":0,"stats":{"Line":1}},{"line":615,"address":[],"length":0,"stats":{"Line":46}},{"line":616,"address":[],"length":0,"stats":{"Line":32}},{"line":618,"address":[],"length":0,"stats":{"Line":81}},{"line":619,"address":[],"length":0,"stats":{"Line":27}},{"line":623,"address":[],"length":0,"stats":{"Line":28}},{"line":624,"address":[],"length":0,"stats":{"Line":99}},{"line":626,"address":[],"length":0,"stats":{"Line":85}},{"line":630,"address":[],"length":0,"stats":{"Line":28}},{"line":633,"address":[],"length":0,"stats":{"Line":14}},{"line":639,"address":[],"length":0,"stats":{"Line":7}},{"line":641,"address":[],"length":0,"stats":{"Line":21}},{"line":642,"address":[],"length":0,"stats":{"Line":7}},{"line":643,"address":[],"length":0,"stats":{"Line":7}},{"line":644,"address":[],"length":0,"stats":{"Line":7}},{"line":645,"address":[],"length":0,"stats":{"Line":7}},{"line":646,"address":[],"length":0,"stats":{"Line":7}},{"line":676,"address":[],"length":0,"stats":{"Line":14}},{"line":678,"address":[],"length":0,"stats":{"Line":42}},{"line":679,"address":[],"length":0,"stats":{"Line":14}},{"line":680,"address":[],"length":0,"stats":{"Line":14}},{"line":686,"address":[],"length":0,"stats":{"Line":104}},{"line":687,"address":[],"length":0,"stats":{"Line":104}},{"line":689,"address":[],"length":0,"stats":{"Line":104}},{"line":695,"address":[],"length":0,"stats":{"Line":14}},{"line":696,"address":[],"length":0,"stats":{"Line":56}},{"line":697,"address":[],"length":0,"stats":{"Line":28}},{"line":698,"address":[],"length":0,"stats":{"Line":0}},{"line":714,"address":[],"length":0,"stats":{"Line":85}},{"line":715,"address":[],"length":0,"stats":{"Line":170}},{"line":716,"address":[],"length":0,"stats":{"Line":340}},{"line":717,"address":[],"length":0,"stats":{"Line":170}},{"line":718,"address":[],"length":0,"stats":{"Line":255}},{"line":719,"address":[],"length":0,"stats":{"Line":170}}],"covered":215,"coverable":242},{"path":["/","Users","sunyu","Projects","oak","src","task","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//! The `Task` base class, mirroring `src/task/src/task.h` (`olive::Task`).\n//!\n//! The C++ class is abstract with a protected virtual `run()`. In Rust each\n//! concrete task (`ConformTask`, `ProxyTask`, …) is its own struct and the\n//! shared lifecycle lives here; the per-task work is supplied through\n//! [`TaskBehavior`] (a trait object, per architectural decision #1 in\n//! README.md). Cancellation rides on a borrowed oakrender `OakCancelAtom`\n//! reached through `crate::bridge::render`.\n//!\n//! CPP-PARITY: src/task/src/task.h\n//!\n//! ## Concurrency contract\n//!\n//! Like the C++ original, the task's `title`/`error` strings are mutated on\n//! the task's own thread (`run()`). The C ABI layer must therefore only read\n//! them while the task is not concurrently running — i.e. before\n//! [`Task::start`] or after the task finished (the `finished` flag is\n//! synchronized through the `done` condvar, so `is_finished()`/\n//! `succeeded()` are always race-free). This mirrors the C++ semantics\n//! exactly.\n\nuse std::sync::atomic::{AtomicI64, Ordering};\nuse std::sync::{Arc, Condvar, Mutex};\n\nuse crate::bridge;\nuse crate::error::{Error, Result};\nuse crate::handle::CHandle;\n\n/// Event type emitted through a task's [`EventListener`], mirroring the\n/// C++ `EventType` enum (`k_event_started`/`k_event_progress`/`k_event_finished`).\n///\n/// CPP-PARITY: src/task/src/task.h (EventType)\n#[derive(Clone, Copy, Debug, PartialEq)]\npub enum TaskEvent {\n\t/// Task began executing.\n\tStarted,\n\t/// Progress updated; the payload is in 0.0..=1.0.\n\tProgress(f64),\n\t/// Task finished (success or failure).\n\tFinished,\n}\n\n/// A listener notified of [`TaskEvent`]s, mirroring the C++\n/// `std::function\u003cvoid(EventType,double)\u003e` listener. Delivered under the\n/// task's lock; kept as the single async return channel (decision #3).\n///\n/// CPP-PARITY: src/task/src/task.h (EventListener)\npub type EventListener = Box\u003cdyn FnMut(TaskEvent) + Send\u003e;\n\n/// Trait supplying the per-task work, standing in for the C++ protected\n/// virtual `run()`. Concrete tasks implement this; [`Task::start`] drives it.\n///\n/// CPP-PARITY: src/task/src/task.h (run)\npub trait TaskBehavior {\n\t/// Perform the task's work. Returning `Err(Error::Cancelled)` is treated\n\t/// as a cancellation; any other `Err` marks the task failed.\n\tfn run(\u0026mut self, task: \u0026mut Task) -\u003e Result\u003c()\u003e;\n}\n\n/// Values the subscribe wrapper needs to re-encode [`TaskEvent`]s into the\n/// C ABI callback signature `(event_id, value, userdata)`. The C++ emits\n/// the start timestamp with `k_event_started` and 1.0/0.0 with\n/// `k_event_finished`; `TaskEvent` carries neither, so the task publishes\n/// them into this shared state before emitting (decision #1 in README.md).\n#[derive(Debug, Default)]\npub struct SubscriberState {\n\t/// Start timestamp (ms since epoch) published before `Started` is emitted.\n\tpub start_ms: AtomicI64,\n\t/// 1.0/0.0 published before `Finished` is emitted.\n\tpub finished_value: AtomicI64,\n}\n\n/// Finish state, protected by the `done` condvar pair so `is_finished`/\n/// `succeeded` are readable from any thread while the task thread mutates\n/// them.\n#[derive(Default)]\nstruct TaskDone {\n\t/// Whether the task has finished (success or failure).\n\tfinished: bool,\n\t/// Whether the task finished successfully.\n\tsucceeded: bool,\n}\n\n/// The base task. Owns lifecycle state plus a borrowed oakrender cancel\n/// atom; the concrete behavior lives in a [`TaskBehavior`] trait object.\npub struct Task {\n\ttitle: String,\n\terror: Option\u003cString\u003e,\n\tstart_time: Option\u003cstd::time::Instant\u003e,\n\tcancel_atom: CHandle,\n\towns_atom: bool,\n\tevent_listener: Option\u003cEventListener\u003e,\n\tcancel_event: Option\u003cBox\u003cdyn FnMut() + Send\u003e\u003e,\n\tstarted: bool,\n\tfinished: bool,\n\tsucceeded: bool,\n\tbehavior: Option\u003cBox\u003cdyn TaskBehavior + Send\u003e\u003e,\n\t/// Finish/success flag pair + wakeup condvar (race-free readers).\n\tdone: Arc\u003c(Mutex\u003cTaskDone\u003e, Condvar)\u003e,\n\t/// Values published for the C ABI subscribe wrapper.\n\tsubscriber: Option\u003cArc\u003cSubscriberState\u003e\u003e,\n}\n\nimpl Drop for Task {\n\tfn drop(\u0026mut self) {\n\t\t// Free the cancellation atom only when we created it ourselves;\n\t\t// borrowed atoms are released by their owner.\n\t\tif self.owns_atom \u0026\u0026 !self.cancel_atom.is_null() {\n\t\t\tlet mut atom = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cancelatom_free(\u0026mut atom);\n\t\t\t}\n\t\t}\n\t}\n}\n\nimpl Task {\n\t/// Create a new task with the given title and a borrowed oakrender\n\t/// cancel atom. When `cancel_atom` is empty a fresh atom is created and\n\t/// owned by the task (mirroring the C++ constructor).\n\tpub fn new(title: \u0026str, cancel_atom: CHandle) -\u003e Task {\n\t\tlet (cancel_atom, owns_atom) = if cancel_atom.is_null() {\n\t\t\tlet atom = unsafe { bridge::render::oakrender_cancelatom_init() };\n\t\t\t(atom.to_chandle(), true)\n\t\t} else {\n\t\t\t(cancel_atom, false)\n\t\t};\n\t\tTask {\n\t\t\ttitle: title.to_string(),\n\t\t\terror: None,\n\t\t\tstart_time: None,\n\t\t\tcancel_atom,\n\t\t\towns_atom,\n\t\t\tevent_listener: None,\n\t\t\tcancel_event: None,\n\t\t\tstarted: false,\n\t\t\tfinished: false,\n\t\t\tsucceeded: false,\n\t\t\tbehavior: None,\n\t\t\tdone: Arc::new((Mutex::new(TaskDone::default()), Condvar::new())),\n\t\t\tsubscriber: None,\n\t\t}\n\t}\n\n\t/// Attach the concrete behavior (defaults to a no-op).\n\tpub fn set_behavior(\u0026mut self, behavior: Box\u003cdyn TaskBehavior + Send\u003e) {\n\t\tself.behavior = Some(behavior);\n\t}\n\n\t/// Start the task: set `started`, emit [`TaskEvent::Started`], run the\n\t/// behavior, then mark finished and emit [`TaskEvent::Finished`].\n\t///\n\t/// The returned `Result` mirrors the C++ `start()` bool: `Ok(())` when\n\t/// the behavior succeeded, `Err(..)` otherwise.\n\tpub fn start(\u0026mut self) -\u003e Result\u003c()\u003e {\n\t\tlet start_ms = system_time_ms();\n\t\tself.start_time = Some(std::time::Instant::now());\n\t\tself.started = true;\n\t\tif let Some(s) = \u0026self.subscriber {\n\t\t\ts.start_ms.store(start_ms, Ordering::SeqCst);\n\t\t}\n\t\tself.emit_event(TaskEvent::Started);\n\n\t\t// Take the behavior out so it can receive `self` (avoids a\n\t\t// self-referential borrow); put it back afterwards.\n\t\tlet behavior = self.behavior.take();\n\t\tlet ret = if let Some(mut b) = behavior {\n\t\t\tlet r = b.run(self);\n\t\t\tself.behavior = Some(b);\n\t\t\tr\n\t\t} else {\n\t\t\tOk(())\n\t\t};\n\n\t\tlet succeeded = ret.is_ok();\n\t\t{\n\t\t\tlet mut done = self.done.0.lock().unwrap();\n\t\t\tdone.finished = true;\n\t\t\tdone.succeeded = succeeded;\n\t\t\tself.done.1.notify_all();\n\t\t}\n\t\tif let Some(s) = \u0026self.subscriber {\n\t\t\ts.finished_value.store(if succeeded { 1 } else { 0 }, Ordering::SeqCst);\n\t\t}\n\t\tself.emit_event(TaskEvent::Finished);\n\n\t\t// One-shot subscription: drop the listener after the final event.\n\t\tself.event_listener = None;\n\n\t\tret\n\t}\n\n\t/// Request cancellation through the borrowed oakrender cancel atom, then\n\t/// invoke the cancel event callback if one is registered.\n\tpub fn cancel(\u0026mut self) {\n\t\tif !self.cancel_atom.is_null() {\n\t\t\tlet atom = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cancelatom_cancel(atom);\n\t\t\t}\n\t\t}\n\t\tif let Some(cb) = self.cancel_event.as_mut() {\n\t\t\tcb();\n\t\t}\n\t}\n\n\t/// Whether cancellation was requested (queries the oakrender atom).\n\tpub fn is_cancelled(\u0026self) -\u003e bool {\n\t\tif self.cancel_atom.is_null() {\n\t\t\treturn false;\n\t\t}\n\t\tlet atom = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\tlet mut cancelled = 0;\n\t\tunsafe {\n\t\t\tbridge::render::oakrender_cancelatom_is_cancelled(atom, \u0026mut cancelled);\n\t\t}\n\t\tcancelled != 0\n\t}\n\n\t/// The borrowed cancel atom handle (empty for tasks without one).\n\tpub fn get_cancel_atom(\u0026self) -\u003e CHandle {\n\t\tself.cancel_atom\n\t}\n\n\t/// Replace the cancel atom. A previously owned atom is freed; the new\n\t/// atom is borrowed (never freed by this task). Used to share one atom\n\t/// between a task and its behavior's inner base task.\n\tpub fn set_cancel_atom(\u0026mut self, atom: CHandle) {\n\t\tif self.owns_atom \u0026\u0026 !self.cancel_atom.is_null() {\n\t\t\tlet mut old = bridge::render::OakCancelAtom::from_chandle(self.cancel_atom);\n\t\t\tunsafe {\n\t\t\t\tbridge::render::oakrender_cancelatom_free(\u0026mut old);\n\t\t\t}\n\t\t}\n\t\tself.cancel_atom = atom;\n\t\tself.owns_atom = false;\n\t}\n\n\t/// Register the event listener. Replaces any previous listener.\n\tpub fn set_event_listener(\u0026mut self, listener: EventListener) {\n\t\tself.event_listener = Some(listener);\n\t}\n\n\t/// Register a hook invoked when [`Task::cancel`] is called.\n\tpub fn set_cancel_event(\u0026mut self, cb: Box\u003cdyn FnMut() + Send\u003e) {\n\t\tself.cancel_event = Some(cb);\n\t}\n\n\t/// Publish the shared values used by the C ABI subscribe wrapper.\n\tpub fn set_subscriber(\u0026mut self, state: Arc\u003cSubscriberState\u003e) {\n\t\tself.subscriber = Some(state);\n\t}\n\n\t/// Emit a progress update, clamping to 0.0..=1.0 and delivering it to the\n\t/// listener as [`TaskEvent::Progress`].\n\tpub fn emit_progress(\u0026mut self, progress: f64) {\n\t\tself.emit_event(TaskEvent::Progress(progress.clamp(0.0, 1.0)));\n\t}\n\n\t/// Reset lifecycle state so the task can be run again.\n\tpub fn reset(\u0026mut self) {\n\t\tself.started = false;\n\t\tself.finished = false;\n\t\tself.succeeded = false;\n\t\tself.error = None;\n\t\tself.start_time = None;\n\t\t{\n\t\t\tlet mut done = self.done.0.lock().unwrap();\n\t\t\tdone.finished = false;\n\t\t\tdone.succeeded = false;\n\t\t}\n\t}\n\n\t/// Record a failure message and mark the task failed.\n\tpub fn set_error(\u0026mut self, message: \u0026str) {\n\t\tself.error = Some(message.to_string());\n\t}\n\n\t/// Change the task title.\n\tpub fn set_title(\u0026mut self, title: \u0026str) {\n\t\tself.title = title.to_string();\n\t}\n\n\t/// The task title.\n\tpub fn title(\u0026self) -\u003e \u0026str {\n\t\t\u0026self.title\n\t}\n\n\t/// The failure message, if the task failed.\n\tpub fn error(\u0026self) -\u003e Option\u003c\u0026str\u003e {\n\t\tself.error.as_deref()\n\t}\n\n\t/// Whether the task has finished (success or failure).\n\tpub fn is_finished(\u0026self) -\u003e bool {\n\t\tself.done.0.lock().unwrap().finished\n\t}\n\n\t/// Whether the task finished successfully.\n\tpub fn succeeded(\u0026self) -\u003e bool {\n\t\tself.done.0.lock().unwrap().succeeded\n\t}\n\n\t/// Block until the task finishes. Mirrors the C++ finished-wait condvar;\n\t/// the manager joins worker threads instead, but this is useful for\n\t/// direct (non-manager) runs. Returns immediately when the task has not\n\t/// been started yet (nothing to wait for) or is already finished.\n\tpub fn wait_finished(\u0026self) {\n\t\tif !self.started {\n\t\t\treturn;\n\t\t}\n\t\tlet mut done = self.done.0.lock().unwrap();\n\t\twhile !done.finished {\n\t\t\tdone = self.done.1.wait(done).unwrap();\n\t\t}\n\t}\n\n\t/// The elapsed time since [`Task::start`], for duration reporting.\n\tpub fn elapsed(\u0026self) -\u003e Option\u003cstd::time::Duration\u003e {\n\t\tself.start_time.map(|t| t.elapsed())\n\t}\n\n\tfn emit_event(\u0026mut self, ev: TaskEvent) {\n\t\tif let Some(listener) = self.event_listener.as_mut() {\n\t\t\tlistener(ev);\n\t\t}\n\t}\n}\n\n/// Current wall-clock time in milliseconds since the Unix epoch, matching\n/// the C++ `start_time_` convention (std::chrono::system_clock ms).\npub fn system_time_ms() -\u003e i64 {\n\tstd::time::SystemTime::now()\n\t\t.duration_since(std::time::UNIX_EPOCH)\n\t\t.map(|d| d.as_millis() as i64)\n\t\t.unwrap_or(0)\n}\n\n/// Marker error returned when a task is cancelled, so the C ABI can map it to\n/// `OAKTASK_E_CANCELLED` (distinct from a generic failure).\npub fn cancelled() -\u003e Error {\n\tError::Cancelled\n}\n","traces":[{"line":121,"address":[],"length":0,"stats":{"Line":160}},{"line":124,"address":[],"length":0,"stats":{"Line":234}},{"line":125,"address":[],"length":0,"stats":{"Line":222}},{"line":127,"address":[],"length":0,"stats":{"Line":74}},{"line":137,"address":[],"length":0,"stats":{"Line":160}},{"line":138,"address":[],"length":0,"stats":{"Line":640}},{"line":139,"address":[],"length":0,"stats":{"Line":166}},{"line":140,"address":[],"length":0,"stats":{"Line":83}},{"line":142,"address":[],"length":0,"stats":{"Line":77}},{"line":145,"address":[],"length":0,"stats":{"Line":480}},{"line":156,"address":[],"length":0,"stats":{"Line":480}},{"line":162,"address":[],"length":0,"stats":{"Line":70}},{"line":163,"address":[],"length":0,"stats":{"Line":140}},{"line":171,"address":[],"length":0,"stats":{"Line":64}},{"line":172,"address":[],"length":0,"stats":{"Line":128}},{"line":173,"address":[],"length":0,"stats":{"Line":64}},{"line":174,"address":[],"length":0,"stats":{"Line":64}},{"line":175,"address":[],"length":0,"stats":{"Line":66}},{"line":176,"address":[],"length":0,"stats":{"Line":3}},{"line":178,"address":[],"length":0,"stats":{"Line":192}},{"line":182,"address":[],"length":0,"stats":{"Line":192}},{"line":183,"address":[],"length":0,"stats":{"Line":192}},{"line":184,"address":[],"length":0,"stats":{"Line":256}},{"line":185,"address":[],"length":0,"stats":{"Line":128}},{"line":186,"address":[],"length":0,"stats":{"Line":64}},{"line":188,"address":[],"length":0,"stats":{"Line":0}},{"line":191,"address":[],"length":0,"stats":{"Line":192}},{"line":193,"address":[],"length":0,"stats":{"Line":320}},{"line":194,"address":[],"length":0,"stats":{"Line":128}},{"line":195,"address":[],"length":0,"stats":{"Line":128}},{"line":196,"address":[],"length":0,"stats":{"Line":64}},{"line":198,"address":[],"length":0,"stats":{"Line":65}},{"line":199,"address":[],"length":0,"stats":{"Line":5}},{"line":201,"address":[],"length":0,"stats":{"Line":192}},{"line":204,"address":[],"length":0,"stats":{"Line":128}},{"line":206,"address":[],"length":0,"stats":{"Line":64}},{"line":211,"address":[],"length":0,"stats":{"Line":8}},{"line":212,"address":[],"length":0,"stats":{"Line":8}},{"line":213,"address":[],"length":0,"stats":{"Line":24}},{"line":215,"address":[],"length":0,"stats":{"Line":8}},{"line":218,"address":[],"length":0,"stats":{"Line":10}},{"line":219,"address":[],"length":0,"stats":{"Line":1}},{"line":224,"address":[],"length":0,"stats":{"Line":101}},{"line":225,"address":[],"length":0,"stats":{"Line":202}},{"line":226,"address":[],"length":0,"stats":{"Line":0}},{"line":228,"address":[],"length":0,"stats":{"Line":303}},{"line":229,"address":[],"length":0,"stats":{"Line":202}},{"line":231,"address":[],"length":0,"stats":{"Line":202}},{"line":233,"address":[],"length":0,"stats":{"Line":101}},{"line":237,"address":[],"length":0,"stats":{"Line":93}},{"line":238,"address":[],"length":0,"stats":{"Line":93}},{"line":244,"address":[],"length":0,"stats":{"Line":9}},{"line":245,"address":[],"length":0,"stats":{"Line":18}},{"line":246,"address":[],"length":0,"stats":{"Line":27}},{"line":248,"address":[],"length":0,"stats":{"Line":9}},{"line":251,"address":[],"length":0,"stats":{"Line":9}},{"line":252,"address":[],"length":0,"stats":{"Line":9}},{"line":256,"address":[],"length":0,"stats":{"Line":3}},{"line":257,"address":[],"length":0,"stats":{"Line":6}},{"line":261,"address":[],"length":0,"stats":{"Line":11}},{"line":262,"address":[],"length":0,"stats":{"Line":22}},{"line":266,"address":[],"length":0,"stats":{"Line":1}},{"line":267,"address":[],"length":0,"stats":{"Line":2}},{"line":272,"address":[],"length":0,"stats":{"Line":82}},{"line":273,"address":[],"length":0,"stats":{"Line":246}},{"line":277,"address":[],"length":0,"stats":{"Line":1}},{"line":278,"address":[],"length":0,"stats":{"Line":1}},{"line":279,"address":[],"length":0,"stats":{"Line":1}},{"line":280,"address":[],"length":0,"stats":{"Line":1}},{"line":281,"address":[],"length":0,"stats":{"Line":2}},{"line":282,"address":[],"length":0,"stats":{"Line":1}},{"line":284,"address":[],"length":0,"stats":{"Line":5}},{"line":285,"address":[],"length":0,"stats":{"Line":1}},{"line":286,"address":[],"length":0,"stats":{"Line":1}},{"line":291,"address":[],"length":0,"stats":{"Line":24}},{"line":292,"address":[],"length":0,"stats":{"Line":48}},{"line":296,"address":[],"length":0,"stats":{"Line":1}},{"line":297,"address":[],"length":0,"stats":{"Line":3}},{"line":301,"address":[],"length":0,"stats":{"Line":37}},{"line":302,"address":[],"length":0,"stats":{"Line":37}},{"line":306,"address":[],"length":0,"stats":{"Line":25}},{"line":307,"address":[],"length":0,"stats":{"Line":50}},{"line":311,"address":[],"length":0,"stats":{"Line":18}},{"line":312,"address":[],"length":0,"stats":{"Line":36}},{"line":316,"address":[],"length":0,"stats":{"Line":5}},{"line":317,"address":[],"length":0,"stats":{"Line":10}},{"line":324,"address":[],"length":0,"stats":{"Line":1}},{"line":325,"address":[],"length":0,"stats":{"Line":1}},{"line":326,"address":[],"length":0,"stats":{"Line":1}},{"line":328,"address":[],"length":0,"stats":{"Line":0}},{"line":329,"address":[],"length":0,"stats":{"Line":0}},{"line":330,"address":[],"length":0,"stats":{"Line":0}},{"line":335,"address":[],"length":0,"stats":{"Line":3}},{"line":336,"address":[],"length":0,"stats":{"Line":8}},{"line":339,"address":[],"length":0,"stats":{"Line":210}},{"line":340,"address":[],"length":0,"stats":{"Line":230}},{"line":341,"address":[],"length":0,"stats":{"Line":10}},{"line":348,"address":[],"length":0,"stats":{"Line":64}},{"line":349,"address":[],"length":0,"stats":{"Line":64}},{"line":350,"address":[],"length":0,"stats":{"Line":64}},{"line":351,"address":[],"length":0,"stats":{"Line":192}},{"line":357,"address":[],"length":0,"stats":{"Line":1}},{"line":358,"address":[],"length":0,"stats":{"Line":1}}],"covered":98,"coverable":103}]};
|
|
</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> |