Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
93 lines
3.1 KiB
Rust
93 lines
3.1 KiB
Rust
use std::ffi::CString;
|
|
use std::path::Path;
|
|
use std::ptr;
|
|
|
|
use anyhow::{bail, Result};
|
|
use ffmpeg_next as ff;
|
|
use ffmpeg_next::ffi;
|
|
|
|
use super::ff_err;
|
|
|
|
pub(super) fn create_output_context(
|
|
output_path: &Path,
|
|
enc_video: &ff::codec::encoder::video::Video,
|
|
) -> Result<ff::format::context::Output> {
|
|
let output_cstr = CString::new(output_path.to_str().unwrap())?;
|
|
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
|
|
|
|
// SAFETY: avformat_alloc_output_context2 creates format context from
|
|
// the file extension. Does NOT open the file.
|
|
let ret = unsafe {
|
|
ffi::avformat_alloc_output_context2(
|
|
&mut fmt_ctx_ptr,
|
|
ptr::null_mut(),
|
|
ptr::null(),
|
|
output_cstr.as_ptr(),
|
|
)
|
|
};
|
|
if ret < 0 || fmt_ctx_ptr.is_null() {
|
|
bail!("Failed to allocate output format context: {}", ff_err(ret));
|
|
}
|
|
|
|
// SAFETY: enc_video is a valid AVCodecContext pointer; codec_id is a plain
|
|
// i32 enum discriminant read from it. fmt_ctx_ptr is a valid AVFormatContext
|
|
// allocated above; oformat is a const pointer field read from it.
|
|
// avformat_query_codec checks codec+format compatibility; both pointers are
|
|
// valid and FF_COMPLIANCE_NORMAL is a constant. All three reads happen in one
|
|
// block so a single SAFETY rationale covers them.
|
|
let compat = unsafe {
|
|
let codec_id = (*enc_video.as_ptr()).codec_id;
|
|
let oformat = (*fmt_ctx_ptr).oformat;
|
|
ffi::avformat_query_codec(oformat, codec_id, ffi::FF_COMPLIANCE_NORMAL)
|
|
};
|
|
if compat < 0 {
|
|
bail!("H.264 codec not supported by output container format");
|
|
}
|
|
|
|
// SAFETY: avformat_new_stream creates a new stream in the format context.
|
|
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
|
|
if stream_ptr.is_null() {
|
|
bail!("Failed to create new stream in output context");
|
|
}
|
|
|
|
// SAFETY: avcodec_parameters_from_context copies encoder params + extradata.
|
|
let ret =
|
|
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
|
|
if ret < 0 {
|
|
bail!(
|
|
"Failed to copy encoder parameters to stream: {}",
|
|
ff_err(ret)
|
|
);
|
|
}
|
|
|
|
// SAFETY: Copy encoder time_base to stream.
|
|
unsafe {
|
|
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
|
}
|
|
|
|
// SAFETY: avio_open opens the output file for writing.
|
|
let ret = unsafe {
|
|
ffi::avio_open(
|
|
&mut (*fmt_ctx_ptr).pb,
|
|
output_cstr.as_ptr(),
|
|
ffi::AVIO_FLAG_WRITE,
|
|
)
|
|
};
|
|
if ret < 0 {
|
|
bail!(
|
|
"Failed to open output file '{}': {}",
|
|
output_path.display(),
|
|
ff_err(ret)
|
|
);
|
|
}
|
|
|
|
// SAFETY: avformat_write_header writes the container header.
|
|
let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) };
|
|
if ret < 0 {
|
|
bail!("Failed to write output header: {}", ff_err(ret));
|
|
}
|
|
|
|
// SAFETY: We created fmt_ctx_ptr above and it's valid.
|
|
Ok(unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) })
|
|
}
|