use std::io::{self, BufReader, Read, Seek};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use shiguredo_webrtc::{
AdaptFrameResult, AdaptedVideoTrackSource, CodecSpecificInfo, EncodedImage, EncodedImageBuffer,
H264PacketizationMode, I420Buffer, SdpVideoFormat, SdpVideoFormatRef, TimestampAligner,
VideoCodecRef, VideoCodecStatus, VideoCodecType, VideoEncoder,
VideoEncoderEncodedImageCallbackPtr, VideoEncoderEncodedImageCallbackRef,
VideoEncoderEncodedImageCallbackResultError, VideoEncoderEncoderInfo, VideoEncoderHandler,
VideoEncoderRateControlParametersRef, VideoEncoderSettingsRef, VideoFrame, VideoFrameBuffer,
VideoFrameBufferHandler, VideoFrameRef, VideoFrameType, VideoFrameTypeVectorRef,
VideoTrackSource, rtc_log_error, rtc_log_info, rtc_log_verbose, rtc_log_warning,
};
use crate::video_codec_capability::{
CodecDirection, VideoCodecCapability, VideoCodecImplementation,
};
#[derive(Debug)]
pub enum Mp4Error {
Io(io::Error),
Demux(shiguredo_mp4::demux::DemuxError),
NoVideoTrack,
NoVideoSamples,
UnsupportedVideoCodec,
InvalidNalLengthSize(u8),
InputPositionOutOfRange {
position: u64,
file_size: u64,
},
InconsistentSampleTable {
index: usize,
offset: u64,
size: usize,
file_size: u64,
},
UnsupportedCompositionTimeOffset {
index: usize,
codec_type: VideoCodecType,
},
InconsistentSampleDescription {
index: usize,
fields: Vec<&'static str>,
},
}
impl std::fmt::Display for Mp4Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(err) => write!(f, "読み込みに失敗しました: {err}"),
Self::Demux(err) => write!(f, "デマルチプレクスに失敗しました: {err}"),
Self::NoVideoTrack => f.write_str("映像トラックがありません"),
Self::NoVideoSamples => f.write_str("映像サンプルがありません"),
Self::UnsupportedVideoCodec => {
f.write_str("映像コーデックが未対応です (H.264, H.265, VP8, VP9, AV1 のみ対応)")
}
Self::InvalidNalLengthSize(size) => {
write!(
f,
"NAL 長プレフィックスのバイト数が不正です: {size} (1, 2, 4 のみ有効)"
)
}
Self::InputPositionOutOfRange {
position,
file_size,
} => {
write!(
f,
"入力位置がファイルサイズ範囲外です: position={position}, file_size={file_size}"
)
}
Self::InconsistentSampleTable {
index,
offset,
size,
file_size,
} => {
write!(
f,
"サンプルテーブルに不整合があります: sample={index} offset={offset} size={size} file_size={file_size}"
)
}
Self::UnsupportedCompositionTimeOffset { index, codec_type } => {
write!(
f,
"サンプルの composition time offset が非ゼロです: sample={index} codec={codec_type:?} (B フレームには未対応)"
)
}
Self::InconsistentSampleDescription { index, fields } => {
write!(
f,
"サンプルエントリーが最初の設定と一致しません: sample={index} fields={fields:?}"
)
}
}
}
}
impl std::error::Error for Mp4Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Demux(err) => Some(err),
Self::NoVideoTrack
| Self::NoVideoSamples
| Self::UnsupportedVideoCodec
| Self::InvalidNalLengthSize(_)
| Self::InputPositionOutOfRange { .. }
| Self::InconsistentSampleTable { .. }
| Self::UnsupportedCompositionTimeOffset { .. }
| Self::InconsistentSampleDescription { .. } => None,
}
}
}
impl From<io::Error> for Mp4Error {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}
impl From<shiguredo_mp4::demux::DemuxError> for Mp4Error {
fn from(err: shiguredo_mp4::demux::DemuxError) -> Self {
Self::Demux(err)
}
}
type Result<T> = std::result::Result<T, Mp4Error>;
pub(crate) struct Mp4EncodedSample {
pub data: Vec<u8>,
pub is_keyframe: bool,
pub width: u32,
pub height: u32,
pub codec_type: VideoCodecType,
}
impl VideoFrameBufferHandler for Mp4EncodedSample {
fn width(&self) -> i32 {
self.width as i32
}
fn height(&self) -> i32 {
self.height as i32
}
fn to_i420(&mut self) -> Option<I420Buffer> {
None
}
}
struct Mp4VideoTrackInfo {
codec_type: VideoCodecType,
width: u16,
height: u16,
timescale: u32,
parameter_sets: Option<Vec<u8>>,
nal_length_size: u8,
}
struct Mp4SampleMeta {
data_offset: u64,
data_size: usize,
is_keyframe: bool,
duration: u32,
}
struct Mp4Timestamp {
ticks: u64,
timescale: u32,
}
impl Mp4Timestamp {
fn to_duration(&self) -> std::time::Duration {
let secs = self.ticks / self.timescale as u64;
let nanos = (self.ticks % self.timescale as u64) * 1_000_000_000 / self.timescale as u64;
std::time::Duration::new(secs, nanos as u32)
}
}
pub struct Mp4SampleReader {
file: BufReader<std::fs::File>,
track_info: Mp4VideoTrackInfo,
samples: Vec<Mp4SampleMeta>,
cumulative: Vec<Mp4Timestamp>,
}
impl Mp4SampleReader {
pub fn new<P: AsRef<Path>>(path: P) -> crate::error::Result<Self> {
Self::new_inner(path.as_ref()).map_err(crate::error::Error::from)
}
fn new_inner(path: &Path) -> Result<Self> {
use shiguredo_mp4::demux::{Input, Mp4FileDemuxer};
let mut file = BufReader::new(std::fs::File::open(path)?);
let file_size = file.get_ref().metadata()?.len();
let mut demuxer = Mp4FileDemuxer::new();
while let Some(required) = demuxer.required_input() {
if required.position > file_size {
return Err(Mp4Error::InputPositionOutOfRange {
position: required.position,
file_size,
});
}
let remaining = file_size - required.position;
let size = usize::try_from(
required
.size
.map_or(remaining, |size| (size as u64).min(remaining)),
)
.map_err(|_| io::Error::other("required input size exceeds usize"))?;
let data = read_bytes_at(&mut file, required.position, size)?;
demuxer.handle_input(Input {
position: required.position,
data: &data,
});
}
let tracks = demuxer.tracks()?;
let video_track = tracks
.iter()
.find(|t| t.kind == shiguredo_mp4::TrackKind::Video)
.ok_or(Mp4Error::NoVideoTrack)?;
let video_track_id = video_track.track_id;
let timescale = video_track.timescale.get();
let mut track_info: Option<Mp4VideoTrackInfo> = None;
let mut samples = Vec::new();
while let Some(sample) = demuxer.next_sample()? {
if sample.track.track_id != video_track_id {
continue;
}
if let Some(entry) = sample.sample_entry {
let info = Self::extract_track_info(entry, timescale)?;
if let Some(ref first) = track_info {
let mismatched = Self::collect_mismatched_track_info_fields(first, &info);
if !mismatched.is_empty() {
return Err(Mp4Error::InconsistentSampleDescription {
index: samples.len(),
fields: mismatched,
});
}
} else {
track_info = Some(info);
}
}
if sample.composition_time_offset.unwrap_or(0) != 0 {
return Err(Mp4Error::UnsupportedCompositionTimeOffset {
index: samples.len(),
codec_type: track_info
.as_ref()
.map(|info| info.codec_type)
.unwrap_or(VideoCodecType::Generic),
});
}
samples.push(Mp4SampleMeta {
data_offset: sample.data_offset,
data_size: sample.data_size,
is_keyframe: sample.keyframe,
duration: sample.duration,
});
}
let track_info = track_info.ok_or(Mp4Error::NoVideoSamples)?;
if samples.is_empty() {
return Err(Mp4Error::NoVideoSamples);
}
for (index, sample) in samples.iter().enumerate() {
let data_size_u64 = sample.data_size as u64;
if sample
.data_offset
.checked_add(data_size_u64)
.is_none_or(|end| end > file_size)
{
return Err(Mp4Error::InconsistentSampleTable {
index,
offset: sample.data_offset,
size: sample.data_size,
file_size,
});
}
}
let timescale = track_info.timescale;
let mut cumulative = Vec::new();
let mut acc: u64 = 0;
cumulative.push(Mp4Timestamp {
ticks: 0,
timescale,
});
for sample in &samples {
acc += sample.duration as u64;
cumulative.push(Mp4Timestamp {
ticks: acc,
timescale,
});
}
Ok(Self {
file,
track_info,
samples,
cumulative,
})
}
fn extract_track_info(
entry: &shiguredo_mp4::boxes::SampleEntry,
timescale: u32,
) -> Result<Mp4VideoTrackInfo> {
use shiguredo_mp4::boxes::SampleEntry;
match entry {
SampleEntry::Avc1(avc1) => {
let (width, height) = (avc1.visual.width, avc1.visual.height);
let mut parameter_sets = Vec::new();
for sps in &avc1.avcc_box.sps_list {
parameter_sets.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
parameter_sets.extend_from_slice(sps);
}
for pps in &avc1.avcc_box.pps_list {
parameter_sets.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
parameter_sets.extend_from_slice(pps);
}
let nal_length_size =
Self::validated_nal_length_size(avc1.avcc_box.length_size_minus_one.get())?;
Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::H264,
width,
height,
timescale,
parameter_sets: Some(parameter_sets),
nal_length_size,
})
}
SampleEntry::Hev1(hev1) => {
let (width, height) = (hev1.visual.width, hev1.visual.height);
let parameter_sets = Self::extract_hevc_parameter_sets(&hev1.hvcc_box);
let nal_length_size =
Self::validated_nal_length_size(hev1.hvcc_box.length_size_minus_one.get())?;
Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::H265,
width,
height,
timescale,
parameter_sets: Some(parameter_sets),
nal_length_size,
})
}
SampleEntry::Hvc1(hvc1) => {
let (width, height) = (hvc1.visual.width, hvc1.visual.height);
let parameter_sets = Self::extract_hevc_parameter_sets(&hvc1.hvcc_box);
let nal_length_size =
Self::validated_nal_length_size(hvc1.hvcc_box.length_size_minus_one.get())?;
Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::H265,
width,
height,
timescale,
parameter_sets: Some(parameter_sets),
nal_length_size,
})
}
SampleEntry::Vp08(vp08) => Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::Vp8,
width: vp08.visual.width,
height: vp08.visual.height,
timescale,
parameter_sets: None,
nal_length_size: 4,
}),
SampleEntry::Vp09(vp09) => Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::Vp9,
width: vp09.visual.width,
height: vp09.visual.height,
timescale,
parameter_sets: None,
nal_length_size: 4,
}),
SampleEntry::Av01(av01) => Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::Av1,
width: av01.visual.width,
height: av01.visual.height,
timescale,
parameter_sets: None,
nal_length_size: 4,
}),
_ => Err(Mp4Error::UnsupportedVideoCodec),
}
}
fn extract_hevc_parameter_sets(hvcc: &shiguredo_mp4::boxes::HvccBox) -> Vec<u8> {
let mut parameter_sets = Vec::new();
for array in &hvcc.nalu_arrays {
for nalu in &array.nalus {
parameter_sets.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
parameter_sets.extend_from_slice(nalu);
}
}
parameter_sets
}
fn validated_nal_length_size(length_size_minus_one: u8) -> Result<u8> {
match length_size_minus_one {
0 => Ok(1),
1 => Ok(2),
3 => Ok(4),
_ => Err(Mp4Error::InvalidNalLengthSize(
length_size_minus_one.saturating_add(1),
)),
}
}
fn collect_mismatched_track_info_fields(
first: &Mp4VideoTrackInfo,
current: &Mp4VideoTrackInfo,
) -> Vec<&'static str> {
let Mp4VideoTrackInfo {
codec_type: first_codec_type,
width: first_width,
height: first_height,
timescale: _,
parameter_sets: first_parameter_sets,
nal_length_size: first_nal_length_size,
} = first;
let Mp4VideoTrackInfo {
codec_type: current_codec_type,
width: current_width,
height: current_height,
timescale: _,
parameter_sets: current_parameter_sets,
nal_length_size: current_nal_length_size,
} = current;
let mut mismatched = Vec::new();
if first_codec_type != current_codec_type {
mismatched.push("codec_type");
}
if first_width != current_width {
mismatched.push("width");
}
if first_height != current_height {
mismatched.push("height");
}
if first_nal_length_size != current_nal_length_size {
mismatched.push("nal_length_size");
}
if first_parameter_sets != current_parameter_sets {
mismatched.push("parameter_sets");
}
mismatched
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn codec_type(&self) -> VideoCodecType {
self.track_info.codec_type
}
pub fn passthrough_capability(&self) -> Mp4PassthroughVideoCodecCapability {
Mp4PassthroughVideoCodecCapability {
codec_type: self.track_info.codec_type,
required_format: self.required_sdp_format(),
}
}
fn required_sdp_format(&self) -> SdpVideoFormat {
match self.track_info.codec_type {
VideoCodecType::H264 => {
let mut format = SdpVideoFormat::new("H264");
format.parameters_mut().set("packetization-mode", "1");
format
}
VideoCodecType::H265 => SdpVideoFormat::new("H265"),
VideoCodecType::Vp8 => SdpVideoFormat::new("VP8"),
VideoCodecType::Vp9 => SdpVideoFormat::new("VP9"),
VideoCodecType::Av1 => SdpVideoFormat::new("AV1"),
VideoCodecType::Generic | VideoCodecType::Unknown(_) => {
unreachable!("unsupported codec is rejected in Mp4SampleReader::new_inner")
}
}
}
fn get_sample(&mut self, index: usize) -> Result<Mp4EncodedSample> {
let sample = &self.samples[index];
let raw_data = read_bytes_at(&mut self.file, sample.data_offset, sample.data_size)?;
let data = match self.track_info.codec_type {
VideoCodecType::H264 | VideoCodecType::H265 => {
let mut annex_b = Vec::new();
if sample.is_keyframe
&& let Some(ref ps) = self.track_info.parameter_sets
{
annex_b.extend_from_slice(ps);
}
annex_b.extend_from_slice(&length_prefixed_nalu_to_annex_b(
&raw_data,
self.track_info.nal_length_size,
));
annex_b
}
_ => raw_data,
};
Ok(Mp4EncodedSample {
data,
is_keyframe: sample.is_keyframe,
width: self.track_info.width as u32,
height: self.track_info.height as u32,
codec_type: self.track_info.codec_type,
})
}
fn cumulative_duration(&self, index: usize) -> std::time::Duration {
self.cumulative[index].to_duration()
}
}
fn read_bytes_at(
file: &mut BufReader<std::fs::File>,
position: u64,
size: usize,
) -> Result<Vec<u8>> {
let mut data = vec![0; size];
file.seek(std::io::SeekFrom::Start(position))?;
file.read_exact(&mut data)?;
Ok(data)
}
fn length_prefixed_nalu_to_annex_b(data: &[u8], nal_length_size: u8) -> Vec<u8> {
debug_assert!(
nal_length_size == 1 || nal_length_size == 2 || nal_length_size == 4,
"nal_length_size must be 1, 2, or 4"
);
let nal_length_size = nal_length_size as usize;
let mut result = Vec::new();
let mut offset = 0;
while offset + nal_length_size <= data.len() {
let nal_size = match nal_length_size {
1 => data[offset] as usize,
2 => u16::from_be_bytes([data[offset], data[offset + 1]]) as usize,
4 => u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize,
_ => unreachable!(),
};
offset += nal_length_size;
if offset + nal_size > data.len() {
break;
}
result.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
result.extend_from_slice(&data[offset..offset + nal_size]);
offset += nal_size;
}
result
}
struct Mp4PassthroughEncoder {
callback: Option<VideoEncoderEncodedImageCallbackPtr>,
}
impl VideoEncoderHandler for Mp4PassthroughEncoder {
fn init_encode(
&mut self,
codec: VideoCodecRef<'_>,
_settings: VideoEncoderSettingsRef<'_>,
) -> VideoCodecStatus {
rtc_log_info!(
"MP4Passthrough: init_encode() codec_type={:?} {}x{} bitrate={}kbps",
codec.codec_type(),
codec.width(),
codec.height(),
codec.start_bitrate_kbps()
);
VideoCodecStatus::Ok
}
fn encode(
&mut self,
frame: VideoFrameRef<'_>,
_frame_types: Option<VideoFrameTypeVectorRef<'_>>,
) -> VideoCodecStatus {
let callback = match self.callback {
Some(callback) => callback,
None => return VideoCodecStatus::Uninitialized,
};
let frame_buffer = frame.buffer();
let sample = match unsafe { frame_buffer.as_native_ref::<Mp4EncodedSample>() } {
Some(sample) => sample,
None => {
rtc_log_warning!(
"MP4Passthrough: failed to get Mp4EncodedSample from frame buffer"
);
return VideoCodecStatus::Error;
}
};
rtc_log_verbose!(
"MP4Passthrough: encode() keyframe={} size={} bytes",
sample.is_keyframe,
sample.data.len()
);
let mut encoded_image = EncodedImage::new();
let encoded_buffer = EncodedImageBuffer::from_bytes(&sample.data);
encoded_image.set_encoded_data(&encoded_buffer);
encoded_image.set_rtp_timestamp(frame.rtp_timestamp());
encoded_image.set_encoded_width(sample.width);
encoded_image.set_encoded_height(sample.height);
encoded_image.set_frame_type(if sample.is_keyframe {
VideoFrameType::Key
} else {
VideoFrameType::Delta
});
let mut codec_specific_info = CodecSpecificInfo::new();
codec_specific_info.set_codec_type(sample.codec_type);
if sample.codec_type == VideoCodecType::H264 {
codec_specific_info.set_h264_packetization_mode(H264PacketizationMode::NonInterleaved);
codec_specific_info.set_h264_idr_frame(sample.is_keyframe);
}
let result = unsafe {
callback.on_encoded_image(encoded_image.as_ref(), Some(codec_specific_info.as_ref()))
};
if result.error() != VideoEncoderEncodedImageCallbackResultError::Ok {
rtc_log_warning!(
"MP4Passthrough: on_encoded_image returned non-Ok status; continue encoding to avoid libwebrtc crash"
);
}
VideoCodecStatus::Ok
}
fn register_encode_complete_callback(
&mut self,
callback: Option<VideoEncoderEncodedImageCallbackRef<'_>>,
) -> VideoCodecStatus {
self.callback = callback
.map(|callback| unsafe { VideoEncoderEncodedImageCallbackPtr::from_ref(callback) });
VideoCodecStatus::Ok
}
fn release(&mut self) -> VideoCodecStatus {
rtc_log_info!("MP4Passthrough: release()");
self.callback = None;
VideoCodecStatus::Ok
}
fn set_rates(&mut self, parameters: VideoEncoderRateControlParametersRef<'_>) {
rtc_log_info!(
"MP4Passthrough: set_rates() bitrate={}bps fps={}",
parameters.bitrate_sum_bps(),
parameters.framerate_fps()
);
}
fn get_encoder_info(&mut self) -> VideoEncoderEncoderInfo {
let mut info = VideoEncoderEncoderInfo::new();
info.set_implementation_name("MP4Passthrough");
info.set_is_hardware_accelerated(false);
info.set_has_trusted_rate_controller(true);
info
}
}
pub struct Mp4PassthroughVideoCodecCapability {
codec_type: VideoCodecType,
required_format: SdpVideoFormat,
}
impl VideoCodecCapability for Mp4PassthroughVideoCodecCapability {
fn get_implementation(&self) -> VideoCodecImplementation {
VideoCodecImplementation::new("mp4-passthrough", "MP4 Passthrough")
}
fn get_supported_formats(&self, direction: CodecDirection) -> Vec<SdpVideoFormat> {
if direction != CodecDirection::Encoder {
return Vec::new();
}
vec![self.required_format.clone()]
}
fn is_supported(&self, direction: CodecDirection, codec_type: VideoCodecType) -> bool {
direction == CodecDirection::Encoder && codec_type == self.codec_type
}
fn create_video_encoder(
&self,
_env: shiguredo_webrtc::EnvironmentRef<'_>,
format: SdpVideoFormatRef<'_>,
) -> Option<VideoEncoder> {
let Ok(format_name) = format.name() else {
return None;
};
let Ok(format_codec_type) = VideoCodecType::try_from(format_name.as_str()) else {
return None;
};
if format_codec_type != self.codec_type {
return None;
}
Some(VideoEncoder::new_with_handler(Box::new(
Mp4PassthroughEncoder { callback: None },
)))
}
}
pub struct Mp4VideoCapturer {
video_source: VideoTrackSource,
stop: Arc<AtomicBool>,
thread_handle: Option<thread::JoinHandle<()>>,
}
const MAX_SLEEP_DURATION: std::time::Duration = std::time::Duration::from_millis(100);
fn wait_until_or_stop(stop: &AtomicBool, deadline: std::time::Instant) -> bool {
loop {
if stop.load(Ordering::Acquire) {
return true;
}
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
return false;
}
thread::sleep(remaining.min(MAX_SLEEP_DURATION));
}
}
impl Mp4VideoCapturer {
pub fn new(mut reader: Mp4SampleReader) -> crate::error::Result<Self> {
let width = reader.track_info.width as i32;
let height = reader.track_info.height as i32;
let source = AdaptedVideoTrackSource::new();
let video_source = source.cast_to_video_track_source();
let stop = Arc::new(AtomicBool::new(false));
let stop_clone = stop.clone();
let thread_handle = thread::spawn(move || {
let mut source = source;
let mut aligner = TimestampAligner::new();
loop {
let loop_start = std::time::Instant::now();
for i in 0..reader.len() {
if stop_clone.load(Ordering::Acquire) {
return;
}
let timestamp_us = shiguredo_webrtc::time_millis() * 1000;
let AdaptFrameResult { applied, .. } =
source.adapt_frame(width, height, timestamp_us);
if applied {
let sample = match reader.get_sample(i) {
Ok(sample) => sample,
Err(err) => {
rtc_log_error!("MP4: failed to read sample: {err:?}");
return;
}
};
let frame_buffer = VideoFrameBuffer::new_with_handler(Box::new(sample));
let ts =
aligner.translate(timestamp_us, shiguredo_webrtc::time_millis() * 1000);
let video_frame = VideoFrame::builder(&frame_buffer)
.set_timestamp_us(ts)
.set_rtp_timestamp(0)
.build();
source.on_frame(&video_frame);
}
let next_frame_time = reader.cumulative_duration(i + 1);
let Some(target) = loop_start.checked_add(next_frame_time) else {
rtc_log_warning!("MP4: loop deadline overflow, stopping feeder thread");
return;
};
if wait_until_or_stop(&stop_clone, target) {
return;
}
}
rtc_log_info!("MP4 reached end of file, looping back to the beginning");
}
});
Ok(Self {
video_source,
stop,
thread_handle: Some(thread_handle),
})
}
pub fn video_source(&self) -> VideoTrackSource {
self.video_source.clone()
}
}
impl Drop for Mp4VideoCapturer {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.thread_handle.take() {
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::video_codec_preference::VideoCodecPreference;
struct FixtureFile {
path: PathBuf,
}
impl Drop for FixtureFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn h264_reader_from_fixture(tag: &str) -> (Mp4SampleReader, FixtureFile) {
let fixture = include_bytes!("../../testdata/red-320x320-h264.mp4");
let tmp_name = format!(
"sora-sdk-mp4-passthrough-{}-{}-{}.mp4",
tag,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let path = std::env::temp_dir().join(tmp_name);
std::fs::write(&path, fixture).expect("一時 fixture の書き込みに失敗しました");
let reader = Mp4SampleReader::new(&path).expect("fixture MP4 のパースに失敗しました");
(reader, FixtureFile { path })
}
#[test]
fn passthrough_capability_advertises_only_reader_required_format() {
let (reader, _fixture) = h264_reader_from_fixture("required-format");
let capability = reader.passthrough_capability();
assert_eq!(capability.get_implementation().name(), "mp4-passthrough");
let encoder_formats = capability.get_supported_formats(CodecDirection::Encoder);
assert_eq!(
encoder_formats.len(),
1,
"Encoder 側は required_sdp_format() の 1 件だけを広告するはずです"
);
let required = reader.required_sdp_format();
assert_eq!(
encoder_formats[0]
.name()
.expect("format 名を取得できるはず"),
required.name().expect("required の name を取得できるはず")
);
let mut owned = encoder_formats[0].clone();
let params: std::collections::HashMap<String, String> =
owned.parameters_mut().iter().collect();
assert_eq!(
params.get("packetization-mode").map(String::as_str),
Some("1"),
"H.264 required format は packetization-mode=1 を保持するはずです"
);
assert!(
capability
.get_supported_formats(CodecDirection::Decoder)
.is_empty(),
"Decoder 方向は空を返すはずです"
);
}
#[test]
fn passthrough_capability_is_supported_only_for_encoder_and_reader_codec_type() {
let (reader, _fixture) = h264_reader_from_fixture("is-supported");
let capability = reader.passthrough_capability();
assert!(
capability.is_supported(CodecDirection::Encoder, VideoCodecType::H264),
"Encoder かつ H.264 は true を返すはずです"
);
assert!(
!capability.is_supported(CodecDirection::Encoder, VideoCodecType::Vp9),
"Encoder でも別 codec type は false を返すはずです"
);
assert!(
!capability.is_supported(CodecDirection::Decoder, VideoCodecType::H264),
"Decoder 方向は同じ codec type でも false を返すはずです"
);
}
#[test]
fn passthrough_capability_creates_encoder_only_for_reader_codec_type() {
let (reader, _fixture) = h264_reader_from_fixture("create-encoder");
let capability = reader.passthrough_capability();
let env = shiguredo_webrtc::Environment::new();
assert!(
capability
.create_video_encoder(env.as_ref(), SdpVideoFormat::new("H264").as_ref())
.is_some(),
"reader の codec type と一致する H.264 は encoder を生成できるはずです"
);
assert!(
capability
.create_video_encoder(env.as_ref(), SdpVideoFormat::new("VP9").as_ref())
.is_none(),
"別の codec type の format は encoder を生成しないはずです"
);
assert!(
capability
.create_video_decoder(env.as_ref(), SdpVideoFormat::new("H264").as_ref())
.is_none(),
"Decoder は生成しないはずです (send only)"
);
}
#[test]
fn passthrough_capability_preference_registers_encoder_entry() {
let (reader, _fixture) = h264_reader_from_fixture("preference");
let capability = reader.passthrough_capability();
let preference = VideoCodecPreference::new_from_capability(&capability);
let codecs = preference.codecs();
assert_eq!(
codecs.len(),
1,
"preference は Encoder + H.264 のエントリを 1 件だけ持つはずです"
);
let entry = &codecs[0];
assert_eq!(entry.direction(), CodecDirection::Encoder);
assert_eq!(entry.codec_type(), VideoCodecType::H264);
assert_eq!(
entry.implementation(),
&capability.get_implementation(),
"エントリの implementation は passthrough capability のものと一致するはずです"
);
}
fn base_track_info_for_consistency_test() -> Mp4VideoTrackInfo {
Mp4VideoTrackInfo {
codec_type: VideoCodecType::H264,
width: 640,
height: 360,
timescale: 1000,
parameter_sets: Some(vec![0x00, 0x00, 0x00, 0x01, 0x67]),
nal_length_size: 4,
}
}
#[test]
fn sample_description_consistency_check_reports_field_mismatches() {
let base = base_track_info_for_consistency_test();
assert!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &base).is_empty(),
"完全一致では相違が報告されないはずです"
);
let mut modified = Mp4VideoTrackInfo {
codec_type: VideoCodecType::H265,
width: base.width,
height: base.height,
timescale: base.timescale,
parameter_sets: base.parameter_sets.clone(),
nal_length_size: base.nal_length_size,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["codec_type"],
"codec_type だけの相違は codec_type のみを返すはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: 1280,
height: base.height,
timescale: base.timescale,
parameter_sets: base.parameter_sets.clone(),
nal_length_size: base.nal_length_size,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["width"],
"width だけの相違は width のみを返すはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: 720,
timescale: base.timescale,
parameter_sets: base.parameter_sets.clone(),
nal_length_size: base.nal_length_size,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["height"],
"height だけの相違は height のみを返すはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: base.height,
timescale: base.timescale,
parameter_sets: base.parameter_sets.clone(),
nal_length_size: 2,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["nal_length_size"],
"nal_length_size だけの相違は nal_length_size のみを返すはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: base.height,
timescale: base.timescale,
parameter_sets: Some(vec![0xff]),
nal_length_size: base.nal_length_size,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["parameter_sets"],
"parameter_sets の byte 列の相違は parameter_sets のみを返すはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: base.height,
timescale: base.timescale,
parameter_sets: None,
nal_length_size: base.nal_length_size,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["parameter_sets"],
"parameter_sets の Some から None への遷移は parameter_sets のみを返すはずです"
);
let base_without_params = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: base.height,
timescale: base.timescale,
parameter_sets: None,
nal_length_size: base.nal_length_size,
};
let modified_with_params = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: base.height,
timescale: base.timescale,
parameter_sets: Some(vec![0x00, 0x00, 0x00, 0x01, 0x67]),
nal_length_size: base.nal_length_size,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(
&base_without_params,
&modified_with_params
),
vec!["parameter_sets"],
"parameter_sets の None から Some への遷移は parameter_sets のみを返すはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: VideoCodecType::H265,
width: base.width,
height: 720,
timescale: base.timescale,
parameter_sets: None,
nal_length_size: 2,
};
assert_eq!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified),
vec!["codec_type", "height", "nal_length_size", "parameter_sets"],
"複数フィールドの相違は codec_type -> width -> height -> nal_length_size -> parameter_sets の順で並ぶはずです"
);
modified = Mp4VideoTrackInfo {
codec_type: base.codec_type,
width: base.width,
height: base.height,
timescale: 90_000,
parameter_sets: base.parameter_sets.clone(),
nal_length_size: base.nal_length_size,
};
assert!(
Mp4SampleReader::collect_mismatched_track_info_fields(&base, &modified).is_empty(),
"timescale は比較対象外なので相違として報告されないはずです"
);
}
#[test]
fn inconsistent_sample_description_display_and_source() {
let err = Mp4Error::InconsistentSampleDescription {
index: 3,
fields: vec!["codec_type", "width", "parameter_sets"],
};
let message = format!("{err}");
assert!(
message.contains("sample=3"),
"sample index が Display 出力に含まれるはずです: {message}"
);
for expected_field in ["codec_type", "width", "parameter_sets"] {
assert!(
message.contains(expected_field),
"相違したフィールド名 {expected_field} が Display 出力に含まれるはずです: {message}"
);
}
use std::error::Error as _;
assert!(
err.source().is_none(),
"InconsistentSampleDescription は source を持たないはずです"
);
}
#[test]
fn annex_b_conversion_converts_multiple_nalus() {
let input = [
0x00, 0x00, 0x00, 0x02, 0x11, 0x22, 0x00, 0x00, 0x00, 0x03, 0x33, 0x44, 0x55,
];
let output = length_prefixed_nalu_to_annex_b(&input, 4);
assert_eq!(
output,
vec![
0x00, 0x00, 0x00, 0x01, 0x11, 0x22, 0x00, 0x00, 0x00, 0x01, 0x33, 0x44, 0x55,
]
);
}
#[test]
fn annex_b_conversion_ignores_truncated_nalu() {
let input = [0x00, 0x00, 0x00, 0x05, 0x11, 0x22, 0x33];
let output = length_prefixed_nalu_to_annex_b(&input, 4);
assert!(output.is_empty());
}
#[test]
fn annex_b_conversion_1byte_nal_length_single_nalu() {
let input = [0x03, 0x11, 0x22, 0x33];
let output = length_prefixed_nalu_to_annex_b(&input, 1);
assert_eq!(output, vec![0x00, 0x00, 0x00, 0x01, 0x11, 0x22, 0x33]);
}
#[test]
fn annex_b_conversion_1byte_nal_length_multiple_nalus() {
let input = [0x02, 0xAA, 0xBB, 0x03, 0xCC, 0xDD, 0xEE];
let output = length_prefixed_nalu_to_annex_b(&input, 1);
assert_eq!(
output,
vec![
0x00, 0x00, 0x00, 0x01, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x01, 0xCC, 0xDD, 0xEE
]
);
}
#[test]
fn annex_b_conversion_1byte_nal_length_truncated_nalu() {
let input = [0x05, 0x11, 0x22];
let output = length_prefixed_nalu_to_annex_b(&input, 1);
assert!(output.is_empty());
}
#[test]
fn annex_b_conversion_2byte_nal_length_single_nalu() {
let input = [0x00, 0x03, 0x11, 0x22, 0x33];
let output = length_prefixed_nalu_to_annex_b(&input, 2);
assert_eq!(output, vec![0x00, 0x00, 0x00, 0x01, 0x11, 0x22, 0x33]);
}
#[test]
fn annex_b_conversion_2byte_nal_length_multiple_nalus() {
let input = [0x00, 0x02, 0xAA, 0xBB, 0x00, 0x03, 0xCC, 0xDD, 0xEE];
let output = length_prefixed_nalu_to_annex_b(&input, 2);
assert_eq!(
output,
vec![
0x00, 0x00, 0x00, 0x01, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x01, 0xCC, 0xDD, 0xEE
]
);
}
#[test]
fn annex_b_conversion_2byte_nal_length_truncated_nalu() {
let input = [0x00, 0x05, 0x11, 0x22];
let output = length_prefixed_nalu_to_annex_b(&input, 2);
assert!(output.is_empty());
}
#[test]
fn sample_reader_reads_fixture_h264_mp4() {
let fixture = include_bytes!("../../testdata/red-320x320-h264.mp4");
let tmp_name = format!(
"sora-sdk-mp4-test-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, fixture).expect("一時フィクスチャの書き込みに失敗しました");
let reader = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
)
.expect("フィクスチャ MP4 のパースに失敗しました");
let mut reader = reader;
assert_eq!(reader.codec_type(), VideoCodecType::H264);
assert!(!reader.is_empty());
let sample = reader
.get_sample(0)
.expect("サンプルデータの読み込みに失敗しました");
let stco_offset = fixture
.windows(4)
.position(|w| w == b"stco")
.expect("フィクスチャに stco ボックスが必要です");
let stco_entry_count = u32::from_be_bytes(
fixture[stco_offset + 8..stco_offset + 12]
.try_into()
.expect("stco の entry_count は 4 バイトで読める必要があります"),
);
assert_eq!(
stco_entry_count, 1,
"フィクスチャの stco エントリ数が移動しています"
);
let sample_offset = u32::from_be_bytes(
fixture[stco_offset + 12..stco_offset + 16]
.try_into()
.expect("stco の先頭エントリは 4 バイトで読める必要があります"),
);
assert_eq!(
sample_offset, 48,
"フィクスチャのサンプル 0 のオフセットが移動しています"
);
let stsz_offset = fixture
.windows(4)
.position(|w| w == b"stsz")
.expect("フィクスチャに stsz ボックスが必要です");
let sample_size = u32::from_be_bytes(
fixture[stsz_offset + 16..stsz_offset + 20]
.try_into()
.expect("stsz の先頭エントリは 4 バイトで読める必要があります"),
);
assert_eq!(
sample_size, 702,
"フィクスチャのサンプル 0 のサイズが移動しています"
);
let avcc_offset = fixture
.windows(4)
.position(|w| w == b"avcC")
.expect("フィクスチャに avcC ボックスが必要です");
let num_of_sps = (fixture[avcc_offset + 9] & 0x1f) as usize;
let sps_length = u16::from_be_bytes(
fixture[avcc_offset + 10..avcc_offset + 12]
.try_into()
.expect("sps_length は 2 バイトで読める必要があります"),
) as usize;
let sps = &fixture[avcc_offset + 12..avcc_offset + 12 + sps_length];
let num_of_pps = fixture[avcc_offset + 12 + sps_length] as usize;
let pps_length = u16::from_be_bytes(
fixture[avcc_offset + 13 + sps_length..avcc_offset + 15 + sps_length]
.try_into()
.expect("pps_length は 2 バイトで読める必要があります"),
) as usize;
let pps =
&fixture[avcc_offset + 15 + sps_length..avcc_offset + 15 + sps_length + pps_length];
assert_eq!(num_of_sps, 1, "フィクスチャの SPS 数が移動しています");
assert_eq!(num_of_pps, 1, "フィクスチャの PPS 数が移動しています");
assert_eq!(
sps[0], 0x67,
"フィクスチャの SPS の先頭バイトが移動しています"
);
assert_eq!(
pps[0], 0x68,
"フィクスチャの PPS の先頭バイトが移動しています"
);
let sample_start = sample_offset as usize;
let sample_end = sample_start + sample_size as usize;
let expected_annex_b =
length_prefixed_nalu_to_annex_b(&fixture[sample_start..sample_end], 4);
let expected_len = 4 + sps_length + 4 + pps_length + expected_annex_b.len();
assert_eq!(
sample.data.len(),
expected_len,
"サンプル 0 のデータ長が期待値と異なります"
);
assert_eq!(
&sample.data[0..4],
&[0x00, 0x00, 0x00, 0x01],
"SPS のスタートコードがありません"
);
assert_eq!(
&sample.data[4..4 + sps_length],
sps,
"SPS が変換後データの先頭に現れるべきです"
);
assert_eq!(
&sample.data[4 + sps_length..8 + sps_length],
&[0x00, 0x00, 0x00, 0x01],
"PPS のスタートコードがありません"
);
assert_eq!(
&sample.data[8 + sps_length..8 + sps_length + pps_length],
pps,
"PPS が SPS の後に現れるべきです"
);
assert_eq!(
&sample.data[8 + sps_length + pps_length..],
expected_annex_b,
"サンプル NAL データがファイルから正しく読み込まれていません"
);
for i in 0..=reader.len() {
assert_eq!(
reader.cumulative_duration(i),
std::time::Duration::from_micros(i as u64 * 40000),
"cumulative_duration[{i}] が期待値と異なります"
);
}
let _ = std::fs::remove_file(&tmp_path);
}
#[test]
fn mp4_timestamp_converts_to_duration() {
assert_eq!(
Mp4Timestamp {
ticks: 0,
timescale: 12800
}
.to_duration(),
std::time::Duration::ZERO
);
assert_eq!(
Mp4Timestamp {
ticks: 12800,
timescale: 12800
}
.to_duration(),
std::time::Duration::from_secs(1)
);
assert_eq!(
Mp4Timestamp {
ticks: 1,
timescale: 12800
}
.to_duration(),
std::time::Duration::from_nanos(78125)
);
assert_eq!(
Mp4Timestamp {
ticks: u64::MAX,
timescale: 1
}
.to_duration(),
std::time::Duration::new(u64::MAX, 0)
);
let max_mul = (u32::MAX as u64 - 1) * 1_000_000_000 / u32::MAX as u64;
assert_eq!(
Mp4Timestamp {
ticks: u32::MAX as u64 - 1,
timescale: u32::MAX
}
.to_duration(),
std::time::Duration::from_nanos(max_mul)
);
assert_eq!(
Mp4Timestamp {
ticks: u32::MAX as u64,
timescale: u32::MAX
}
.to_duration(),
std::time::Duration::from_secs(1)
);
}
#[test]
fn sample_reader_get_sample_returns_io_error_after_file_truncation() {
let fixture = include_bytes!("../../testdata/red-320x320-h264.mp4");
let tmp_name = format!(
"sora-sdk-mp4-test-truncate-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, fixture).expect("一時フィクスチャの書き込みに失敗しました");
let reader = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
)
.expect("フィクスチャ MP4 のパースに失敗しました");
let mut reader = reader;
let file = std::fs::File::options()
.write(true)
.open(&tmp_path)
.expect("縮小用ハンドルのオープンに失敗しました");
file.set_len(0).expect("ファイルの縮小に失敗しました");
drop(file);
let result = reader.get_sample(0);
assert!(
matches!(result, Err(Mp4Error::Io(_))),
"縮小されたファイルからの読み込みは Io エラーになるべきです"
);
let _ = std::fs::remove_file(&tmp_path);
}
#[test]
fn validated_nal_length_size_accepts_valid_values() {
assert_eq!(
Mp4SampleReader::validated_nal_length_size(0)
.expect("length_size_minus_one=0 は受け入れられる必要があります"),
1
);
assert_eq!(
Mp4SampleReader::validated_nal_length_size(1)
.expect("length_size_minus_one=1 は受け入れられる必要があります"),
2
);
assert_eq!(
Mp4SampleReader::validated_nal_length_size(3)
.expect("length_size_minus_one=3 は受け入れられる必要があります"),
4
);
}
#[test]
fn validated_nal_length_size_rejects_reserved_value() {
let result = Mp4SampleReader::validated_nal_length_size(2);
assert!(result.is_err());
assert!(matches!(
result.expect_err("reserved 値はエラーになる必要があります"),
Mp4Error::InvalidNalLengthSize(3)
));
}
#[test]
fn sample_reader_rejects_invalid_length_size_minus_one() {
let fixture = include_bytes!("../../testdata/red-320x320-h264.mp4");
let mut patched = fixture.to_vec();
assert_eq!(
patched[0x6ea], 0xFF,
"フィクスチャの lengthSizeMinusOne バイトが移動しています"
);
patched[0x6ea] = 0xFE;
let tmp_name = format!(
"sora-sdk-mp4-test-invalid-nal-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, &patched).expect("一時フィクスチャの書き込みに失敗しました");
let result = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
);
let _ = std::fs::remove_file(&tmp_path);
match result {
Err(crate::error::Error::Mp4 { source }) => {
assert!(
matches!(source, Mp4Error::InvalidNalLengthSize(_)),
"InvalidNalLengthSize エラーを期待しましたが、実際は: {source:?}"
);
}
Err(e) => panic!("Mp4 エラーを期待しましたが、実際は: {e}"),
Ok(_) => panic!("Err を期待しましたが、Ok でした"),
}
}
#[test]
fn sample_reader_rejects_truncated_mp4_with_oversized_input_position() {
let fixture = include_bytes!("../../testdata/red-320x320-h264.mp4");
let truncated = &fixture[..128];
let tmp_name = format!(
"sora-sdk-mp4-test-truncated-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, truncated).expect("一時フィクスチャの書き込みに失敗しました");
let result = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
);
let _ = std::fs::remove_file(&tmp_path);
assert!(result.is_err(), "切り詰め MP4 は Err になるべきです");
}
#[test]
fn sample_reader_rejects_inconsistent_sample_table_offset_exceeds_file_size() {
let fixture = include_bytes!("../../testdata/red-320x320-h264.mp4");
let mut patched = fixture.to_vec();
let file_size = patched.len();
let stco_offset = fixture
.windows(4)
.position(|w| w == b"stco")
.expect("fixture に stco ボックスが必要です");
let data_start = stco_offset + 8 + 4;
let bad_offset = (file_size + 1) as u32;
patched[data_start..data_start + 4].copy_from_slice(&bad_offset.to_be_bytes());
let tmp_name = format!(
"sora-sdk-mp4-test-stco-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, &patched).expect("一時フィクスチャの書き込みに失敗しました");
let result = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
);
let _ = std::fs::remove_file(&tmp_path);
assert!(
matches!(
result,
Err(crate::error::Error::Mp4 {
source: Mp4Error::InconsistentSampleTable { .. },
})
),
"不正な stco を持つ MP4 は InconsistentSampleTable エラーになるべきです"
);
}
#[test]
fn sample_reader_rejects_b_frame_fixture() {
let fixture = include_bytes!("../../testdata/red-bframe-320x320-h264.mp4");
let tmp_name = format!(
"sora-sdk-mp4-test-bframe-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, fixture).expect("一時フィクスチャの書き込みに失敗しました");
let result = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
);
let _ = std::fs::remove_file(&tmp_path);
match result {
Err(crate::error::Error::Mp4 { source }) => {
assert!(
matches!(
source,
Mp4Error::UnsupportedCompositionTimeOffset {
index: 0,
codec_type: VideoCodecType::H264,
}
),
"UnsupportedCompositionTimeOffset エラーを期待しましたが、実際は: {source:?}"
);
}
Err(e) => panic!("Mp4 エラーを期待しましたが、実際は: {e}"),
Ok(_) => panic!("Err を期待しましたが、Ok でした"),
}
}
#[test]
fn sample_reader_accepts_zero_composition_time_offset_fixture() {
let fixture = include_bytes!("../../testdata/red-bframe-320x320-h264.mp4");
let mut patched = fixture.to_vec();
let ctts_offset = patched
.windows(4)
.position(|w| w == b"ctts")
.expect("fixture に ctts ボックスが必要です");
let entry_count_offset = ctts_offset + 8;
let entry_count = u32::from_be_bytes(
patched[entry_count_offset..entry_count_offset + 4]
.try_into()
.expect("entry_count は 4 バイトで読める必要があります"),
);
assert_eq!(
u32::from_be_bytes(
patched[entry_count_offset + 8..entry_count_offset + 12]
.try_into()
.expect("先頭エントリの sample_offset は 4 バイトで読める必要があります")
),
1024,
"フィクスチャの先頭エントリの sample_offset が移動しています"
);
for i in 0..entry_count {
let offset_pos = entry_count_offset + 4 + i as usize * 8 + 4;
patched[offset_pos..offset_pos + 4].copy_from_slice(&0u32.to_be_bytes());
}
let tmp_name = format!(
"sora-sdk-mp4-test-ctts-zero-{}-{}.mp4",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("システム時刻は UNIX_EPOCH より後である必要があります")
.as_nanos()
);
let tmp_path = std::env::temp_dir().join(tmp_name);
std::fs::write(&tmp_path, &patched).expect("一時フィクスチャの書き込みに失敗しました");
let result = Mp4SampleReader::new(
tmp_path
.to_str()
.expect("パスは有効な UTF-8 である必要があります"),
);
let _ = std::fs::remove_file(&tmp_path);
match result {
Ok(reader) => {
assert_eq!(
reader.codec_type(),
VideoCodecType::H264,
"offset 0 の MP4 は H.264 reader として読み込めるべきです"
);
}
Err(e) => panic!("offset 0 の MP4 は Ok を期待しましたが、実際は: {e}"),
}
}
#[test]
fn wait_until_or_stop_stops_immediately_when_stop_is_set() {
let stop = AtomicBool::new(true);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
assert!(
wait_until_or_stop(&stop, deadline),
"停止フラグ設定済みなら即座に true を返すべきです"
);
}
#[test]
fn wait_until_or_stop_returns_false_when_deadline_passed() {
let stop = AtomicBool::new(false);
let deadline = std::time::Instant::now() - std::time::Duration::from_secs(1);
assert!(
!wait_until_or_stop(&stop, deadline),
"deadline 到達済みなら即座に false を返すべきです"
);
}
#[test]
fn wait_until_or_stop_stops_within_sleep_limit() {
let stop = Arc::new(AtomicBool::new(false));
let barrier = Arc::new(std::sync::Barrier::new(2));
let (done_tx, done_rx) = std::sync::mpsc::channel();
let stop_clone = stop.clone();
let barrier_clone = barrier.clone();
thread::spawn(move || {
barrier_clone.wait();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
let result = wait_until_or_stop(&stop_clone, deadline);
done_tx.send(result).expect("終了通知の送信に失敗しました");
});
barrier.wait();
thread::sleep(MAX_SLEEP_DURATION / 2);
stop.store(true, Ordering::Release);
let stopped = done_rx
.recv_timeout(MAX_SLEEP_DURATION + std::time::Duration::from_millis(100))
.expect("待機中のスレッドは停止フラグ設定から MAX_SLEEP_DURATION に余裕を加えた時間以内に終了するべきです");
assert!(
stopped,
"stop による停止 (true) を期待しましたが、実際は: {stopped:?}"
);
}
}