scythe/src/muxer.rs
oiupoyt fd95beaa11 fix(muxer): fix mp4 video corruption and timestamp synchronization
- Add AV_CODEC_FLAG_GLOBAL_HEADER to VaapiEncoder, WindowsHwEncoder, and AudioEncoder to generate SPS/PPS extradata for MP4 avcC containers
- Filter replay buffer for video IDR keyframes instead of audio packets
- Rebase video and audio PTS/DTS to start at zero for both replay and normal recordings
- Sort interleaved packets chronologically by microsecond timestamps before muxing
- Set avoid_negative_ts to force start-at-zero container alignment
2026-09-04 19:08:29 +05:30

157 lines
5.5 KiB
Rust

use ffmpeg_next::ffi::*;
use crate::ring::Packet;
use std::ffi::CString;
pub struct Muxer {
fmt_ctx: *mut AVFormatContext,
video_time_base: AVRational,
audio_time_base: Option<AVRational>,
}
impl Muxer {
/// # Safety
/// `codec_ctx` must be a valid, initialized pointer to an `AVCodecContext`.
/// `audio_codec_ctx`, if provided, must also be a valid, initialized pointer.
pub unsafe fn new(
path: &str,
codec_ctx: *mut AVCodecContext,
audio_codec_ctx: Option<*mut AVCodecContext>,
) -> Result<Self, String> {
unsafe {
let path_cstr = CString::new(path).map_err(|e| e.to_string())?;
let mut fmt_ctx: *mut AVFormatContext = std::ptr::null_mut();
let ret = avformat_alloc_output_context2(
&mut fmt_ctx,
std::ptr::null(),
std::ptr::null(),
path_cstr.as_ptr(),
);
if ret < 0 || fmt_ctx.is_null() {
return Err("Failed to alloc output context".into());
}
let stream = avformat_new_stream(fmt_ctx, std::ptr::null());
if stream.is_null() {
avformat_free_context(fmt_ctx);
return Err("Failed to create video stream".into());
}
(*stream).id = 0;
let ret = avcodec_parameters_from_context((*stream).codecpar, codec_ctx);
if ret < 0 {
avformat_free_context(fmt_ctx);
return Err("Failed to copy video codec parameters".into());
}
let video_time_base = (*codec_ctx).time_base;
(*stream).time_base = video_time_base;
let mut audio_time_base = None;
if let Some(actx) = audio_codec_ctx {
let astream = avformat_new_stream(fmt_ctx, std::ptr::null());
if !astream.is_null() {
(*astream).id = 1;
if avcodec_parameters_from_context((*astream).codecpar, actx) < 0 {
eprintln!("Warning: Failed to copy audio codec parameters");
}
let atb = (*actx).time_base;
(*astream).time_base = atb;
audio_time_base = Some(atb);
}
}
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
let ret = avio_open(&mut (*fmt_ctx).pb, path_cstr.as_ptr(), AVIO_FLAG_WRITE);
if ret < 0 {
avformat_free_context(fmt_ctx);
return Err("Failed to open output file".into());
}
}
let mut opts: *mut AVDictionary = std::ptr::null_mut();
av_dict_set(&mut opts, c"movflags".as_ptr(), c"+faststart".as_ptr(), 0);
(*fmt_ctx).avoid_negative_ts = 2; // AVFMT_AVOID_NEG_TS_MAKE_ZERO: Shift timestamps so that they start at 0
(*fmt_ctx).max_interleave_delta = 1_000_000;
let ret = avformat_write_header(fmt_ctx, &mut opts);
av_dict_free(&mut opts);
if ret < 0 {
if ((*(*fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
avio_closep(&mut (*fmt_ctx).pb);
}
avformat_free_context(fmt_ctx);
return Err("Failed to write header".into());
}
Ok(Self {
fmt_ctx,
video_time_base,
audio_time_base,
})
}
}
pub fn video_time_base(&self) -> AVRational {
self.video_time_base
}
pub fn audio_time_base(&self) -> Option<AVRational> {
self.audio_time_base
}
pub fn write_packet(&mut self, packet: &Packet) -> Result<(), String> {
unsafe {
if packet.ptr.is_null() {
return Ok(());
}
let mut new_pkt = av_packet_alloc();
if new_pkt.is_null() {
return Err("Failed to allocate packet".into());
}
av_packet_ref(new_pkt, packet.ptr);
let stream_idx = (*new_pkt).stream_index;
if stream_idx >= 0 && (stream_idx as u32) < (*self.fmt_ctx).nb_streams {
let out_stream = *(*self.fmt_ctx).streams.add(stream_idx as usize);
let in_tb = if stream_idx == 0 {
self.video_time_base
} else {
self.audio_time_base.unwrap_or(self.video_time_base)
};
av_packet_rescale_ts(new_pkt, in_tb, (*out_stream).time_base);
}
let ret = av_interleaved_write_frame(self.fmt_ctx, new_pkt);
av_packet_free(&mut new_pkt);
if ret < 0 {
return Err(format!("Failed to write frame: {}", ret));
}
}
Ok(())
}
pub fn finalize(&mut self) -> Result<(), String> {
unsafe {
let ret = av_write_trailer(self.fmt_ctx);
if ret < 0 {
return Err(format!("Failed to write trailer: {}", ret));
}
}
Ok(())
}
}
impl Drop for Muxer {
fn drop(&mut self) {
unsafe {
if !self.fmt_ctx.is_null() {
if !(*self.fmt_ctx).pb.is_null() && ((*(*self.fmt_ctx).oformat).flags & AVFMT_NOFILE) == 0 {
avio_closep(&mut (*self.fmt_ctx).pb);
}
avformat_free_context(self.fmt_ctx);
self.fmt_ctx = std::ptr::null_mut();
}
}
}
}