use std::io::{self, BufReader, Read, Seek};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Sender};
use std::thread;
use shiguredo_webrtc::{
AdaptFrameResult, AdaptedVideoTrackSource, CodecSpecificInfo, EncodedImage, EncodedImageBuffer,
H264PacketizationMode, I420Buffer, SdpVideoFormat, SdpVideoFormatRef, TimestampAligner,
VideoCodecRef, VideoCodecStatus, VideoCodecType, VideoEncoder,
VideoEncoderEncodedImageCallbackPtr, VideoEncoderEncodedImageCallbackRefMut,
VideoEncoderEncodedImageCallbackResultError, VideoEncoderEncoderInfo, VideoEncoderHandler,
VideoEncoderRateControlParametersRef, VideoEncoderSettingsRef, VideoFrame, VideoFrameBuffer,
VideoFrameBufferHandler, VideoFrameRef, VideoFrameType, VideoFrameTypeVectorRef,
VideoTrackSource, fuzzy_match_sdp_video_format, rtc_log_error, rtc_log_info, rtc_log_verbose,
rtc_log_warning,
};
use crate::codec_direction::CodecDirection;
use crate::video_codec_capability::{VideoCodecCapability, VideoCodecImplementation};
use crate::video_codecs::av1::{
Av1TrackConfig, assemble_av1_encoded_sample_data, av1_required_sdp_format,
resolve_av1_incoming, validate_av1_track,
};
use crate::video_codecs::h264::{
H264TrackConfig, h264_required_sdp_format, parse_profile_level_id, resolve_h264_incoming,
};
#[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,
},
InvalidAv1Track(String),
InvalidH264Track(String),
}
impl std::fmt::Display for Mp4Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(err) => write!(f, "failed to read: {err}"),
Self::Demux(err) => write!(f, "failed to demux: {err}"),
Self::NoVideoTrack => f.write_str("no video track"),
Self::NoVideoSamples => f.write_str("no video samples"),
Self::UnsupportedVideoCodec => {
f.write_str("unsupported video codec (H.264, H.265, VP8, VP9, AV1 only)")
}
Self::InvalidNalLengthSize(size) => {
write!(
f,
"invalid NAL length prefix size: {size} (only 1, 2, or 4 are valid)"
)
}
Self::InputPositionOutOfRange {
position,
file_size,
} => {
write!(
f,
"input position is out of file size range: position={position}, file_size={file_size}"
)
}
Self::InconsistentSampleTable {
index,
offset,
size,
file_size,
} => {
write!(
f,
"inconsistent sample table: sample={index} offset={offset} size={size} file_size={file_size}"
)
}
Self::UnsupportedCompositionTimeOffset { index, codec_type } => {
write!(
f,
"sample composition time offset is non-zero: sample={index} codec={codec_type:?} (B frames are not supported)"
)
}
Self::InconsistentSampleDescription { index } => {
write!(
f,
"sample description does not match the first entry: sample={index}"
)
}
Self::InvalidAv1Track(err) => {
write!(f, "AV1 track validation failed: {err}")
}
Self::InvalidH264Track(err) => {
write!(f, "H.264 track validation failed: {err}")
}
}
}
}
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 { .. }
| Self::InvalidAv1Track(_)
| Self::InvalidH264Track(_) => 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>;
#[derive(Clone)]
pub(crate) struct Mp4EncodedSample {
pub data: Vec<u8>,
pub is_keyframe: bool,
pub width: u32,
pub height: u32,
pub codec_type: VideoCodecType,
playback_serial: u64,
}
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
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Mp4VideoTrackInfo {
codec_type: VideoCodecType,
width: u16,
height: u16,
timescale: u32,
parameter_sets: Option<Vec<u8>>,
nal_length_size: u8,
av1_config: Option<Av1TrackConfig>,
h264_config: Option<H264TrackConfig>,
}
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)
}
}
#[derive(Clone)]
pub struct Mp4SampleReader {
inner: Arc<Mp4SampleReaderInner>,
}
struct Mp4SampleReaderInner {
track_info: Mp4VideoTrackInfo,
sample_count: usize,
cumulative: Vec<Mp4Timestamp>,
io: Mp4SampleReaderIo,
}
struct Mp4SampleIoRequest {
index: usize,
response: Sender<Result<Mp4EncodedSample>>,
}
struct Mp4SampleReaderIo {
sender: Option<Sender<Mp4SampleIoRequest>>,
thread: Option<thread::JoinHandle<()>>,
}
impl Drop for Mp4SampleReaderIo {
fn drop(&mut self) {
self.sender.take();
if let Some(handle) = self.thread.take() {
if let Err(payload) = handle.join() {
rtc_log_error!(
"MP4: I/O thread panicked: {:?}",
payload.downcast_ref::<&str>()
);
}
}
}
}
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 {
if first != &info {
return Err(Mp4Error::InconsistentSampleDescription {
index: samples.len(),
});
}
} 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,
});
}
}
if track_info.codec_type == VideoCodecType::Av1
&& let Some(av1_config) = track_info.av1_config.as_ref()
{
let av1_config_owned = av1_config.clone();
let is_keyframes: Vec<bool> = samples.iter().map(|s| s.is_keyframe).collect();
validate_av1_track(&is_keyframes, &av1_config_owned, |index| {
let sample = &samples[index];
read_bytes_at(&mut file, sample.data_offset, sample.data_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,
});
}
let sample_count = samples.len();
let (io_sender, io_receiver) = mpsc::channel::<Mp4SampleIoRequest>();
let io_track_info = track_info.clone();
let io_thread = thread::spawn(move || {
while let Ok(request) = io_receiver.recv() {
let result = read_sample(&mut file, &io_track_info, &samples, request.index);
let _ = request.response.send(result);
}
});
Ok(Self {
inner: Arc::new(Mp4SampleReaderInner {
track_info,
sample_count,
cumulative,
io: Mp4SampleReaderIo {
sender: Some(io_sender),
thread: Some(io_thread),
},
}),
})
}
fn extract_track_info(
entry: &shiguredo_mp4::boxes::SampleEntry,
timescale: u32,
) -> Result<Mp4VideoTrackInfo> {
use shiguredo_mp4::boxes::SampleEntry;
match entry {
SampleEntry::Avc1(avc1) => {
if avc1.avcc_box.sps_list.is_empty() {
return Err(Mp4Error::InvalidH264Track(
"SPS list must not be empty".to_string(),
));
}
if avc1.avcc_box.pps_list.is_empty() {
return Err(Mp4Error::InvalidH264Track(
"PPS list must not be empty".to_string(),
));
}
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())?;
let avc_profile_level_id = shiguredo_mp4::bitstream::h264::H264ProfileLevelId {
profile_idc: avc1.avcc_box.avc_profile_indication,
profile_iop: avc1.avcc_box.profile_compatibility,
level_idc: avc1.avcc_box.avc_level_indication,
};
for (sps_index, sps) in avc1.avcc_box.sps_list.iter().enumerate() {
let sps_info =
shiguredo_mp4::bitstream::h264::parse_sps(sps).map_err(|err| {
Mp4Error::InvalidH264Track(format!(
"failed to parse SPS #{sps_index}: {err}"
))
})?;
if sps_info.profile_level_id != avc_profile_level_id {
return Err(Mp4Error::InvalidH264Track(format!(
"SPS #{sps_index} profile-level-id does not match avcC: \
sps={} avcC={}",
sps_info.profile_level_id.to_hex(),
avc_profile_level_id.to_hex(),
)));
}
if sps_info.width != width || sps_info.height != height {
return Err(Mp4Error::InvalidH264Track(format!(
"SPS #{sps_index} dimensions do not match avc1: \
sps={}x{} avc1={width}x{height}",
sps_info.width, sps_info.height,
)));
}
}
for (pps_index, pps) in avc1.avcc_box.pps_list.iter().enumerate() {
let Some(&header) = pps.first() else {
return Err(Mp4Error::InvalidH264Track(format!(
"PPS #{pps_index} is empty"
)));
};
if header & 0b1000_0000 != 0 {
return Err(Mp4Error::InvalidH264Track(format!(
"PPS #{pps_index} forbidden_zero_bit must be 0"
)));
}
if header & 0b0001_1111 != 8 {
return Err(Mp4Error::InvalidH264Track(format!(
"PPS #{pps_index} NAL unit type must be 8"
)));
}
}
if parse_profile_level_id(avc_profile_level_id).is_none() {
return Err(Mp4Error::InvalidH264Track(format!(
"H.264 profile / level not recognized by the fixed libwebrtc: {}",
avc_profile_level_id.to_hex(),
)));
}
let avcc_box = if avc1.avcc_box.chroma_format.is_some()
|| matches!(avc1.avcc_box.avc_profile_indication, 66 | 77 | 88)
{
Some(
shiguredo_mp4::Encode::encode_to_vec(&avc1.avcc_box).map_err(|err| {
Mp4Error::InvalidH264Track(format!("failed to re-encode avcC: {err}"))
})?,
)
} else {
None
};
Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::H264,
width,
height,
timescale,
parameter_sets: Some(parameter_sets),
nal_length_size,
av1_config: None,
h264_config: Some(H264TrackConfig {
profile_level_id: avc_profile_level_id,
avcc_box,
}),
})
}
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,
av1_config: None,
h264_config: None,
})
}
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,
av1_config: None,
h264_config: None,
})
}
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,
av1_config: None,
h264_config: None,
}),
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,
av1_config: None,
h264_config: None,
}),
SampleEntry::Av01(av01) => {
let av1c = &av01.av1c_box;
Ok(Mp4VideoTrackInfo {
codec_type: VideoCodecType::Av1,
width: av01.visual.width,
height: av01.visual.height,
timescale,
parameter_sets: None,
nal_length_size: 4,
av1_config: Some(Av1TrackConfig {
seq_profile: av1c.seq_profile.get(),
seq_level_idx_0: av1c.seq_level_idx_0.get(),
seq_tier_0: av1c.seq_tier_0.get(),
high_bitdepth: av1c.high_bitdepth.get() != 0,
twelve_bit: av1c.twelve_bit.get() != 0,
monochrome: av1c.monochrome.get() != 0,
chroma_subsampling_x: av1c.chroma_subsampling_x.get(),
chroma_subsampling_y: av1c.chroma_subsampling_y.get(),
chroma_sample_position: av1c.chroma_sample_position.get(),
initial_presentation_delay_minus_one: av1c
.initial_presentation_delay_minus_one
.map(|v| v.get()),
config_obus: av1c.config_obus.clone(),
}),
h264_config: None,
})
}
_ => 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),
)),
}
}
pub fn len(&self) -> usize {
self.inner.sample_count
}
pub fn is_empty(&self) -> bool {
self.inner.sample_count == 0
}
pub fn codec_type(&self) -> VideoCodecType {
self.inner.track_info.codec_type
}
pub fn passthrough_capability(&self) -> Mp4PassthroughVideoCodecCapability {
Mp4PassthroughVideoCodecCapability {
codec_type: self.inner.track_info.codec_type,
required_format: self.required_sdp_format(),
}
}
fn required_sdp_format(&self) -> SdpVideoFormat {
match self.inner.track_info.codec_type {
VideoCodecType::H264 => {
let config = self
.inner
.track_info
.h264_config
.as_ref()
.expect("BUG: H.264 track must have h264_config");
h264_required_sdp_format(config)
}
VideoCodecType::H265 => SdpVideoFormat::new("H265"),
VideoCodecType::Vp8 => SdpVideoFormat::new("VP8"),
VideoCodecType::Vp9 => SdpVideoFormat::new("VP9"),
VideoCodecType::Av1 => {
av1_required_sdp_format(self.inner.track_info.av1_config.as_ref())
}
VideoCodecType::Generic | VideoCodecType::Unknown(_) => {
unreachable!("unsupported codec is rejected in Mp4SampleReader::new_inner")
}
}
}
fn get_sample(&self, index: usize, stop: &AtomicBool) -> Result<Option<Mp4EncodedSample>> {
assert!(
index < self.inner.sample_count,
"BUG: sample index {index} is out of range (sample_count={})",
self.inner.sample_count
);
let (response_tx, response_rx) = mpsc::channel();
let sender = self.inner.io.sender.as_ref().expect(
"BUG: I/O thread sender must be available while an Mp4SampleReader clone is alive",
);
sender
.send(Mp4SampleIoRequest {
index,
response: response_tx,
})
.expect("BUG: I/O thread receiver must not be closed while an Mp4SampleReader clone is alive");
loop {
if stop.load(Ordering::Acquire) {
return Ok(None);
}
match response_rx.recv_timeout(std::time::Duration::from_millis(1)) {
Ok(result) => return result.map(Some),
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => {
panic!("BUG: I/O thread must respond exactly once per request")
}
}
}
}
fn cumulative_duration(&self, index: usize) -> std::time::Duration {
self.inner.cumulative[index].to_duration()
}
}
fn read_sample(
file: &mut BufReader<std::fs::File>,
track_info: &Mp4VideoTrackInfo,
samples: &[Mp4SampleMeta],
index: usize,
) -> Result<Mp4EncodedSample> {
let sample = &samples[index];
let raw_data = read_bytes_at(file, sample.data_offset, sample.data_size)?;
let data = match track_info.codec_type {
VideoCodecType::H264 | VideoCodecType::H265 => {
let mut annex_b = Vec::new();
if sample.is_keyframe
&& let Some(ref ps) = track_info.parameter_sets
{
annex_b.extend_from_slice(ps);
}
annex_b.extend_from_slice(&length_prefixed_nalu_to_annex_b(
&raw_data,
track_info.nal_length_size,
));
annex_b
}
VideoCodecType::Av1 => assemble_av1_encoded_sample_data(
raw_data,
sample.is_keyframe,
track_info.av1_config.as_ref(),
),
_ => raw_data,
};
Ok(Mp4EncodedSample {
data,
is_keyframe: sample.is_keyframe,
width: track_info.width as u32,
height: track_info.height as u32,
codec_type: track_info.codec_type,
playback_serial: 0,
})
}
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>,
waiting_for_keyframe: bool,
last_playback_serial: Option<u64>,
}
impl Mp4PassthroughEncoder {
fn new() -> Self {
Self {
callback: None,
waiting_for_keyframe: true,
last_playback_serial: None,
}
}
fn enter_keyframe_wait(&mut self) {
self.waiting_for_keyframe = true;
self.last_playback_serial = None;
}
fn should_forward_sample(&mut self, is_keyframe: bool, playback_serial: u64) -> bool {
if self.waiting_for_keyframe {
if is_keyframe {
rtc_log_info!(
"MP4Passthrough: forwarding keyframe after wait playback_serial={playback_serial}"
);
self.waiting_for_keyframe = false;
self.last_playback_serial = Some(playback_serial);
true
} else {
rtc_log_verbose!(
"MP4Passthrough: dropping delta while waiting for keyframe playback_serial={playback_serial}"
);
false
}
} else if self
.last_playback_serial
.and_then(|last| last.checked_add(1))
== Some(playback_serial)
{
self.last_playback_serial = Some(playback_serial);
true
} else if is_keyframe {
rtc_log_info!(
"MP4Passthrough: playback_serial gap then keyframe last={:?} current={playback_serial}",
self.last_playback_serial
);
self.last_playback_serial = Some(playback_serial);
true
} else {
rtc_log_info!(
"MP4Passthrough: playback_serial gap, waiting for keyframe last={:?} current={playback_serial}",
self.last_playback_serial
);
self.enter_keyframe_wait();
false
}
}
}
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()
);
self.enter_keyframe_wait();
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={} playback_serial={} size={} bytes",
sample.is_keyframe,
sample.playback_serial,
sample.data.len()
);
if !self.should_forward_sample(sample.is_keyframe, sample.playback_serial) {
return VideoCodecStatus::Ok;
}
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<VideoEncoderEncodedImageCallbackRefMut<'_>>,
) -> VideoCodecStatus {
self.callback = callback
.map(|callback| unsafe { VideoEncoderEncodedImageCallbackPtr::from_mut(&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 resolve_sdp_format(
&self,
direction: CodecDirection,
format: SdpVideoFormatRef<'_>,
) -> Option<SdpVideoFormat> {
if self.codec_type == VideoCodecType::H264 && direction == CodecDirection::Encoder {
return resolve_h264_incoming(&self.required_format, format);
}
if self.codec_type == VideoCodecType::Av1 && direction == CodecDirection::Encoder {
return resolve_av1_incoming(&self.required_format, format);
}
fuzzy_match_sdp_video_format(&self.get_supported_formats(direction), format)
}
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::new(),
)))
}
}
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(reader: Mp4SampleReader) -> crate::error::Result<Self> {
let width = reader.inner.track_info.width as i32;
let height = reader.inner.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 source = source;
let mut aligner = TimestampAligner::new();
let mut next_playback_serial: u64 = 0;
loop {
let loop_start = std::time::Instant::now();
for i in 0..reader.len() {
if stop_clone.load(Ordering::Acquire) {
return;
}
let playback_serial = next_playback_serial;
next_playback_serial = match next_playback_serial.checked_add(1) {
Some(serial) => serial,
None => {
rtc_log_warning!(
"MP4: playback_serial overflow, stopping feeder thread"
);
return;
}
};
let timestamp_us = shiguredo_webrtc::time_millis() * 1000;
let AdaptFrameResult { applied, .. } =
source.adapt_frame(width, height, timestamp_us);
if applied {
let mut sample = match reader.get_sample(i, &stop_clone) {
Ok(Some(sample)) => sample,
Ok(None) => return,
Err(err) => {
rtc_log_error!("MP4: failed to read sample: {err:?}");
return;
}
};
sample.playback_serial = playback_serial;
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() {
if let Err(payload) = handle.join() {
rtc_log_error!(
"MP4: feeder thread panicked: {:?}",
payload.downcast_ref::<&str>()
);
}
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use shiguredo_webrtc::{VideoFrameRef, VideoSink, VideoSinkHandler, VideoSinkWants};
use super::*;
use crate::video_codec_preference::VideoCodecPreference;
use shiguredo_mp4::bitstream::av1::{Av1ObuParseContext, parse_obus, parse_sequence_header};
use shiguredo_webrtc::{
CodecSpecificInfoRef, EncodedImageRef, VideoEncoderEncodedImageCallback,
VideoEncoderEncodedImageCallbackHandler, VideoEncoderEncodedImageCallbackResult,
VideoFrameType,
};
struct FixtureFile {
path: PathBuf,
}
impl Drop for FixtureFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn h264_reader_from_bytes(tag: &str, fixture: &[u8]) -> (Mp4SampleReader, FixtureFile) {
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 })
}
fn h264_reader_from_fixture(tag: &str) -> (Mp4SampleReader, FixtureFile) {
h264_reader_from_bytes(tag, include_bytes!("../../testdata/red-320x320-h264.mp4"))
}
#[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 のものと一致するはずです"
);
}
#[test]
fn inconsistent_sample_description_display_and_source() {
let err = Mp4Error::InconsistentSampleDescription { index: 3 };
let message = format!("{err}");
assert!(
message.contains("sample=3"),
"sample index が 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 のパースに失敗しました");
assert_eq!(reader.codec_type(), VideoCodecType::H264);
assert!(!reader.is_empty());
let stop = AtomicBool::new(false);
let sample = reader
.get_sample(0, &stop)
.expect("サンプルデータの読み込みに失敗しました")
.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 file = std::fs::File::options()
.write(true)
.open(&tmp_path)
.expect("縮小用ハンドルのオープンに失敗しました");
file.set_len(0).expect("ファイルの縮小に失敗しました");
drop(file);
let stop = AtomicBool::new(false);
let result = reader.get_sample(0, &stop);
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:?}"
);
}
#[test]
fn shared_reader_clones_read_samples_concurrently() {
let (reader, _fixture) = h264_reader_from_fixture("shared-reader");
let sample_count = reader.len();
let thread_count = 4;
let handles: Vec<_> = (0..thread_count)
.map(|_| {
let reader = reader.clone();
thread::spawn(move || {
let stop = AtomicBool::new(false);
let mut samples = Vec::new();
for i in 0..sample_count {
let sample = reader
.get_sample(i, &stop)
.expect("共有 reader からのサンプル読み出しに失敗しました")
.expect("停止フラグは未設定のため中断されないはずです");
samples.push(sample);
}
samples
})
})
.collect();
let results: Vec<_> = handles
.into_iter()
.map(|handle| {
handle
.join()
.expect("共有 reader の読み出しスレッドが panic しました")
})
.collect();
for (i, reference) in results[0].iter().enumerate() {
for other_results in results.iter().skip(1) {
assert_eq!(
other_results[i].data, reference.data,
"index {i} のサンプルデータは全スレッドで一致するはずです"
);
assert_eq!(
other_results[i].is_keyframe, reference.is_keyframe,
"index {i} のキーフレーム判定は全スレッドで一致するはずです"
);
}
}
let first = &results[0][0];
assert!(first.is_keyframe, "先頭サンプルはキーフレームのはずです");
assert!(
first.data.starts_with(&[0x00, 0x00, 0x00, 0x01]),
"先頭サンプルは SPS/PPS のスタートコードで始まるはずです"
);
assert_eq!(first.width, 320, "先頭サンプルの幅は 320 のはずです");
assert_eq!(first.height, 320, "先頭サンプルの高さは 320 のはずです");
assert_eq!(
first.codec_type,
VideoCodecType::H264,
"先頭サンプルのコーデックは H.264 のはずです"
);
}
#[test]
fn get_sample_returns_none_when_stopped() {
let (reader, _fixture) = h264_reader_from_fixture("get-sample-stopped");
let stop = AtomicBool::new(true);
let result = reader
.get_sample(0, &stop)
.expect("停止時の読み出しは I/O エラーにはならないはずです");
assert!(
result.is_none(),
"停止フラグ設定時は応答を待たずに None を返すはずです"
);
}
struct TestVideoSink {
tx: std::sync::mpsc::Sender<TestFrameInfo>,
}
struct TestFrameInfo {
width: i32,
height: i32,
codec_type: VideoCodecType,
playback_serial: u64,
}
impl VideoSinkHandler for TestVideoSink {
fn on_frame(&mut self, frame: VideoFrameRef<'_>) {
let buffer = frame.buffer();
let (codec_type, playback_serial) = {
let sample = unsafe { buffer.as_native_ref::<Mp4EncodedSample>() }
.expect("パススルーサンプルは VideoFrameBuffer に内包されているはずです");
(sample.codec_type, sample.playback_serial)
};
let _ = self.tx.send(TestFrameInfo {
width: buffer.width(),
height: buffer.height(),
codec_type,
playback_serial,
});
}
}
#[test]
fn multiple_capturers_share_single_reader() {
let context = crate::connection_context::SoraConnectionContext::new()
.expect("SoraConnectionContext の生成に失敗しました");
let (reader, _fixture) = h264_reader_from_fixture("shared-capturer");
let capturer_count = 2;
let sample_count = reader.len() as u64;
let frames_to_receive = reader.len() + 2;
let mut capturers = Vec::new();
let mut tracks = Vec::new();
let mut sinks = Vec::new();
for _ in 0..capturer_count {
let capturer = Mp4VideoCapturer::new(reader.clone())
.expect("共有 reader からの Mp4VideoCapturer 生成に失敗しました");
let (tx, rx) = std::sync::mpsc::channel();
let sink = VideoSink::new_with_handler(Box::new(TestVideoSink { tx }));
let track = context
.create_video_track(&capturer.video_source())
.expect("VideoTrack の生成に失敗しました");
track.add_or_update_sink(&sink, &VideoSinkWants::new());
capturers.push(capturer);
tracks.push(track);
sinks.push((sink, rx));
}
for (index, (_sink, rx)) in sinks.iter().enumerate() {
let mut first_serial = None;
let mut last_serial = None;
for _ in 0..frames_to_receive {
let frame = rx
.recv_timeout(std::time::Duration::from_secs(5))
.unwrap_or_else(|_| {
panic!(
"capturer {index} は 5 秒以内に {frames_to_receive} フレーム供給できるはずです"
)
});
assert_eq!(
frame.width, 320,
"capturer {index} のフレーム幅は 320 のはずです"
);
assert_eq!(
frame.height, 320,
"capturer {index} のフレーム高さは 320 のはずです"
);
assert_eq!(
frame.codec_type,
VideoCodecType::H264,
"capturer {index} のフレームコーデックは H.264 のはずです"
);
if let Some(last) = last_serial {
assert!(
frame.playback_serial > last,
"capturer {index} の playback_serial は単調に増加するはずです: 直前={last} 現在={}",
frame.playback_serial
);
} else {
first_serial = Some(frame.playback_serial);
}
last_serial = Some(frame.playback_serial);
}
let first_serial = first_serial.expect("最初のフレームを受信しているはずです");
let last_serial = last_serial.expect("最後のフレームを受信しているはずです");
assert!(
last_serial / sample_count > first_serial / sample_count,
"capturer {index} は loop 境界を越えて通し番号を進めるはずです: 先頭={first_serial} 末尾={last_serial}"
);
}
drop(capturers);
for (index, (sink, _rx)) in sinks.iter().enumerate() {
tracks[index].remove_sink(sink);
}
}
fn av1_reader_from_fixture(tag: &str) -> (Mp4SampleReader, FixtureFile) {
let fixture = include_bytes!("../../testdata/red-320x320-av1.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 })
}
fn demux_av1_fixture() -> (Vec<u8>, Vec<(Vec<u8>, bool)>) {
use shiguredo_mp4::TrackKind;
use shiguredo_mp4::demux::{Input, Mp4FileDemuxer};
let fixture = include_bytes!("../../testdata/red-320x320-av1.mp4");
let mut demuxer = Mp4FileDemuxer::new();
while let Some(required) = demuxer.required_input() {
let size = required.size.unwrap_or(fixture.len());
let end = (required.position as usize + size).min(fixture.len());
demuxer.handle_input(Input {
position: required.position,
data: &fixture[required.position as usize..end],
});
}
let tracks = demuxer
.tracks()
.expect("fixture の track 解析に失敗しました");
let video_track = tracks
.iter()
.find(|t| t.kind == TrackKind::Video)
.expect("fixture に video track が存在するはずです");
let video_track_id = video_track.track_id;
let mut config_obus = Vec::new();
let mut samples = Vec::new();
while let Some(sample) = demuxer
.next_sample()
.expect("fixture の sample 解析に失敗しました")
{
if sample.track.track_id != video_track_id {
continue;
}
if let Some(shiguredo_mp4::boxes::SampleEntry::Av01(av01)) = &sample.sample_entry {
config_obus = av01.av1c_box.config_obus.clone();
}
let start = sample.data_offset as usize;
let end = start + sample.data_size;
samples.push((fixture[start..end].to_vec(), sample.keyframe));
}
(config_obus, samples)
}
#[test]
fn sample_reader_reads_fixture_av1_mp4() {
let (reader, _fixture) = av1_reader_from_fixture("av1-read");
assert_eq!(reader.codec_type(), VideoCodecType::Av1);
assert_eq!(reader.len(), 50, "fixture は 50 サンプルを持つはずです");
assert_eq!(reader.inner.track_info.width, 320);
assert_eq!(reader.inner.track_info.height, 320);
let av1_config = reader
.inner
.track_info
.av1_config
.as_ref()
.expect("AV1 track は av1_config を持つはずです");
assert_eq!(av1_config.seq_profile, 0);
assert_eq!(av1_config.seq_level_idx_0, 0);
assert_eq!(av1_config.seq_tier_0, 0);
assert!(!av1_config.high_bitdepth);
assert!(!av1_config.twelve_bit);
assert!(!av1_config.monochrome);
assert_eq!(av1_config.chroma_subsampling_x, 1);
assert_eq!(av1_config.chroma_subsampling_y, 1);
assert_eq!(av1_config.chroma_sample_position, 0);
assert_eq!(av1_config.initial_presentation_delay_minus_one, None);
assert_eq!(
av1_config.config_obus,
vec![
0x0A, 0x0B, 0x00, 0x00, 0x00, 0x04, 0x44, 0xFE, 0x7E, 0x7F, 0xFC, 0xC0, 0x20
],
"configOBUs は実測値と byte 一致するはずです"
);
let config_obus = parse_obus(&av1_config.config_obus, Av1ObuParseContext::ConfigObus)
.expect("configOBUs は parse できるはずです");
assert_eq!(config_obus.len(), 1, "configOBUs は SH OBU 1 個のはずです");
let sh = parse_sequence_header(config_obus[0].payload)
.expect("config SH は parse できるはずです");
assert_eq!(sh.seq_profile, 0);
assert_eq!(sh.operating_points_cnt_minus_1, 0);
assert_eq!(sh.operating_point_idc_0, 0);
let stop = AtomicBool::new(false);
let sync_indices: Vec<usize> = (0..reader.len())
.filter(|&i| {
reader
.get_sample(i, &stop)
.expect("fixture の sample を読み出せるはずです")
.expect("停止フラグは未設定のため中断されないはずです")
.is_keyframe
})
.collect();
assert_eq!(
sync_indices,
vec![0, 8, 16, 24, 32, 40, 48],
"sync sample の index は実測値と一致するはずです"
);
}
#[test]
fn sample_reader_get_sample_prepends_config_obus_only_for_sync_samples() {
let (reader, _fixture) = av1_reader_from_fixture("av1-sample");
let stop = AtomicBool::new(false);
let (config_obus, raw_samples) = demux_av1_fixture();
assert!(
!config_obus.is_empty(),
"fixture の configOBUs は非空のはずです"
);
for (i, (raw, is_keyframe)) in raw_samples.iter().enumerate() {
let sample = reader
.get_sample(i, &stop)
.expect("fixture の sample を読み出せるはずです")
.expect("停止フラグは未設定のため中断されないはずです");
let expected = if *is_keyframe {
let mut combined = Vec::new();
combined.extend_from_slice(&config_obus);
combined.extend_from_slice(raw);
combined
} else {
raw.clone()
};
assert_eq!(
sample.data, expected,
"sample[{i}] の payload は configOBUs || sample data の規則に従うはずです"
);
assert_eq!(sample.is_keyframe, *is_keyframe);
}
}
#[test]
fn passthrough_encoder_forwards_av1_reconstructed_payload() {
let (reader, _fixture) = av1_reader_from_fixture("av1-encode");
let stop = AtomicBool::new(false);
let (config_obus, raw_samples) = demux_av1_fixture();
let (tx, rx) = std::sync::mpsc::channel();
let mut callback =
VideoEncoderEncodedImageCallback::new_with_handler(Box::new(RecordingHandler { tx }));
let mut encoder = Mp4PassthroughEncoder::new();
assert_eq!(
encoder.register_encode_complete_callback(Some(callback.as_mut())),
VideoCodecStatus::Ok
);
for i in 0..reader.len() {
let mut sample = reader
.get_sample(i, &stop)
.expect("fixture の sample を読み出せるはずです")
.expect("停止フラグは未設定のため中断されないはずです");
sample.playback_serial = i as u64;
encode_passthrough_sample(&mut encoder, sample, i as u32);
}
drop(callback);
let images: Vec<(VideoFrameType, Vec<u8>)> = rx.iter().collect();
assert_eq!(images.len(), 50, "全 sample が callback に渡されるはずです");
for (i, ((raw, is_keyframe), (frame_type, data))) in
raw_samples.iter().zip(images).enumerate()
{
let expected = if *is_keyframe {
let mut combined = Vec::new();
combined.extend_from_slice(&config_obus);
combined.extend_from_slice(raw);
combined
} else {
raw.clone()
};
assert_eq!(
data, expected,
"sample[{i}] の EncodedImage は configOBUs || sample data の規則に従うはずです"
);
assert_eq!(
frame_type,
if *is_keyframe {
VideoFrameType::Key
} else {
VideoFrameType::Delta
},
"sample[{i}] の frame type は sync で Key / non-sync で Delta のはずです"
);
}
}
struct RecordingHandler {
tx: std::sync::mpsc::Sender<(VideoFrameType, Vec<u8>)>,
}
impl VideoEncoderEncodedImageCallbackHandler for RecordingHandler {
fn on_encoded_image(
&mut self,
encoded_image: EncodedImageRef<'_>,
_codec_specific_info: Option<CodecSpecificInfoRef<'_>>,
) -> VideoEncoderEncodedImageCallbackResult {
let data = encoded_image
.encoded_data()
.map(|buf| buf.data().to_vec())
.unwrap_or_default();
self.tx
.send((encoded_image.frame_type(), data))
.expect("callback 結果の送信に失敗しました");
VideoEncoderEncodedImageCallbackResult::new(
VideoEncoderEncodedImageCallbackResultError::Ok,
)
}
}
fn encode_passthrough_sample(
encoder: &mut Mp4PassthroughEncoder,
sample: Mp4EncodedSample,
rtp_timestamp: u32,
) {
let frame_buffer = VideoFrameBuffer::new_with_handler(Box::new(sample));
let video_frame = VideoFrame::builder(&frame_buffer)
.set_timestamp_us(0)
.set_rtp_timestamp(rtp_timestamp)
.build();
assert_eq!(
encoder.encode(video_frame.as_ref(), None),
VideoCodecStatus::Ok,
"encode() は Ok を返すはずです"
);
}
fn read_fixture_samples(reader: &Mp4SampleReader) -> Vec<Mp4EncodedSample> {
let stop = AtomicBool::new(false);
(0..reader.len())
.map(|i| {
reader
.get_sample(i, &stop)
.expect("fixture の sample を読み出せるはずです")
.expect("停止フラグは未設定のため中断されないはずです")
})
.collect()
}
fn expected_av1_payload(config_obus: &[u8], raw: &[u8], is_keyframe: bool) -> Vec<u8> {
if is_keyframe {
let mut combined = Vec::new();
combined.extend_from_slice(config_obus);
combined.extend_from_slice(raw);
combined
} else {
raw.to_vec()
}
}
fn forward_samples_through_encoder(
encoder: &mut Mp4PassthroughEncoder,
samples: Vec<Mp4EncodedSample>,
) -> Vec<(VideoFrameType, Vec<u8>)> {
let (tx, rx) = std::sync::mpsc::channel();
let mut callback =
VideoEncoderEncodedImageCallback::new_with_handler(Box::new(RecordingHandler { tx }));
assert_eq!(
encoder.register_encode_complete_callback(Some(callback.as_mut())),
VideoCodecStatus::Ok
);
for (i, sample) in samples.into_iter().enumerate() {
encode_passthrough_sample(encoder, sample, i as u32);
}
drop(callback);
rx.iter().collect()
}
#[test]
fn passthrough_encoder_drops_initial_delta_until_keyframe() {
let (reader, _fixture) = av1_reader_from_fixture("av1-initial-delta");
let (config_obus, raw_samples) = demux_av1_fixture();
let samples = read_fixture_samples(&reader);
assert!(
!samples[1].is_keyframe,
"AV1 fixture の sample[1] は delta のはずです"
);
assert!(
samples[8].is_keyframe,
"AV1 fixture の sample[8] はキーフレームのはずです"
);
let mut first_delta = samples[1].clone();
first_delta.playback_serial = 0;
let mut key = samples[8].clone();
key.playback_serial = 1;
let mut after_key = samples[9].clone();
after_key.playback_serial = 2;
let images = forward_samples_through_encoder(
&mut Mp4PassthroughEncoder::new(),
vec![first_delta, key, after_key],
);
assert_eq!(
images.len(),
2,
"最初の delta sample を破棄し、キーフレームと直後の delta sample だけを出力するはずです"
);
assert_eq!(images[0].0, VideoFrameType::Key);
assert_eq!(
images[0].1,
expected_av1_payload(&config_obus, &raw_samples[8].0, true)
);
assert_eq!(images[1].0, VideoFrameType::Delta);
assert_eq!(
images[1].1,
expected_av1_payload(&config_obus, &raw_samples[9].0, false)
);
}
#[test]
fn passthrough_encoder_drops_deltas_after_playback_serial_gap() {
let (reader, _fixture) = av1_reader_from_fixture("av1-serial-gap-delta");
let (config_obus, raw_samples) = demux_av1_fixture();
let samples = read_fixture_samples(&reader);
assert!(samples[0].is_keyframe);
assert!(!samples[1].is_keyframe);
assert!(!samples[3].is_keyframe);
assert!(!samples[4].is_keyframe);
assert!(samples[8].is_keyframe);
assert!(!samples[9].is_keyframe);
let mut key0 = samples[0].clone();
key0.playback_serial = 0;
let mut delta1 = samples[1].clone();
delta1.playback_serial = 1;
let mut dropped_delta = samples[3].clone();
dropped_delta.playback_serial = 3;
let mut delta4 = samples[4].clone();
delta4.playback_serial = 4;
let mut key8 = samples[8].clone();
key8.playback_serial = 8;
let mut delta9 = samples[9].clone();
delta9.playback_serial = 9;
let dropped_payload = dropped_delta.data.clone();
let images = forward_samples_through_encoder(
&mut Mp4PassthroughEncoder::new(),
vec![key0, delta1, dropped_delta, delta4, key8, delta9],
);
assert_eq!(
images.len(),
4,
"欠落後の delta sample は出力されないはずです"
);
assert_eq!(images[0].0, VideoFrameType::Key);
assert_eq!(
images[0].1,
expected_av1_payload(&config_obus, &raw_samples[0].0, true)
);
assert_eq!(images[1].0, VideoFrameType::Delta);
assert_eq!(
images[1].1,
expected_av1_payload(&config_obus, &raw_samples[1].0, false)
);
assert_eq!(images[2].0, VideoFrameType::Key);
assert_eq!(
images[2].1,
expected_av1_payload(&config_obus, &raw_samples[8].0, true)
);
assert_eq!(images[3].0, VideoFrameType::Delta);
assert_eq!(
images[3].1,
expected_av1_payload(&config_obus, &raw_samples[9].0, false)
);
assert!(
!images
.iter()
.any(|(_, data)| data.as_slice() == dropped_payload.as_slice()),
"欠落直後の delta sample の payload は callback に渡されないはずです"
);
}
#[test]
fn passthrough_encoder_resumes_on_keyframe_immediately_after_gap() {
let (reader, _fixture) = av1_reader_from_fixture("av1-serial-gap-key");
let (config_obus, raw_samples) = demux_av1_fixture();
let samples = read_fixture_samples(&reader);
let mut key0 = samples[0].clone();
key0.playback_serial = 0;
let mut key8 = samples[8].clone();
key8.playback_serial = 8;
let mut delta9 = samples[9].clone();
delta9.playback_serial = 9;
let images = forward_samples_through_encoder(
&mut Mp4PassthroughEncoder::new(),
vec![key0, key8, delta9],
);
assert_eq!(
images.len(),
3,
"不連続直後のキーフレームから再開するはずです"
);
assert_eq!(images[0].0, VideoFrameType::Key);
assert_eq!(
images[0].1,
expected_av1_payload(&config_obus, &raw_samples[0].0, true)
);
assert_eq!(images[1].0, VideoFrameType::Key);
assert_eq!(
images[1].1,
expected_av1_payload(&config_obus, &raw_samples[8].0, true)
);
assert_eq!(images[2].0, VideoFrameType::Delta);
assert_eq!(
images[2].1,
expected_av1_payload(&config_obus, &raw_samples[9].0, false)
);
}
#[test]
fn passthrough_encoder_keeps_forwarding_across_loop_boundary_serial() {
let (reader, _fixture) = av1_reader_from_fixture("av1-loop-serial");
let (config_obus, raw_samples) = demux_av1_fixture();
let samples = read_fixture_samples(&reader);
let last = samples.len() - 1;
assert!(
samples[last - 1].is_keyframe,
"AV1 fixture の sample[48] はキーフレームのはずです"
);
assert!(
!samples[last].is_keyframe,
"AV1 fixture の末尾 sample は delta のはずです"
);
assert!(samples[0].is_keyframe);
assert!(!samples[1].is_keyframe);
let mut loop_end_key = samples[last - 1].clone();
loop_end_key.playback_serial = (last - 1) as u64;
let mut loop_end_delta = samples[last].clone();
loop_end_delta.playback_serial = last as u64;
let mut loop_key = samples[0].clone();
loop_key.playback_serial = last as u64 + 1;
let mut loop_delta = samples[1].clone();
loop_delta.playback_serial = last as u64 + 2;
let mut encoder = Mp4PassthroughEncoder::new();
let images = forward_samples_through_encoder(
&mut encoder,
vec![loop_end_key, loop_end_delta, loop_key, loop_delta],
);
assert_eq!(
images.len(),
4,
"ループ境界で serial が連続していれば、4 個の sample をすべて出力するはずです"
);
assert!(
!encoder.waiting_for_keyframe,
"ループ境界で serial が連続している場合、キーフレーム待ちへ移行してはいけません"
);
assert_eq!(encoder.last_playback_serial, Some(last as u64 + 2));
assert_eq!(images[0].0, VideoFrameType::Key);
assert_eq!(
images[0].1,
expected_av1_payload(&config_obus, &raw_samples[last - 1].0, true)
);
assert_eq!(images[1].0, VideoFrameType::Delta);
assert_eq!(
images[1].1,
expected_av1_payload(&config_obus, &raw_samples[last].0, false)
);
assert_eq!(images[2].0, VideoFrameType::Key);
assert_eq!(
images[2].1,
expected_av1_payload(&config_obus, &raw_samples[0].0, true)
);
assert_eq!(images[3].0, VideoFrameType::Delta);
assert_eq!(
images[3].1,
expected_av1_payload(&config_obus, &raw_samples[1].0, false)
);
}
#[test]
fn passthrough_encoders_do_not_share_playback_serial_state() {
let (reader, _fixture) = av1_reader_from_fixture("av1-encoder-isolation");
let samples = read_fixture_samples(&reader);
let mut key = samples[0].clone();
key.playback_serial = 0;
let mut delta = samples[1].clone();
delta.playback_serial = 1;
let mut first_encoder = Mp4PassthroughEncoder::new();
let mut second_encoder = Mp4PassthroughEncoder::new();
let first = forward_samples_through_encoder(&mut first_encoder, vec![key]);
let second = forward_samples_through_encoder(&mut second_encoder, vec![delta]);
assert_eq!(
first.len(),
1,
"最初の encoder はキーフレームを出力するはずです"
);
assert_eq!(first[0].0, VideoFrameType::Key);
assert!(
second.is_empty(),
"別の encoder は最初の delta sample を出力しないはずです"
);
}
#[test]
fn passthrough_encoder_reenters_keyframe_wait_after_enter_keyframe_wait() {
let (reader, _fixture) = av1_reader_from_fixture("av1-reenter-wait");
let (config_obus, raw_samples) = demux_av1_fixture();
let samples = read_fixture_samples(&reader);
let mut key0 = samples[0].clone();
key0.playback_serial = 0;
let mut delta1 = samples[1].clone();
delta1.playback_serial = 1;
let mut delta2 = samples[2].clone();
delta2.playback_serial = 2;
let mut key8 = samples[8].clone();
key8.playback_serial = 8;
let (tx, rx) = std::sync::mpsc::channel();
let mut callback =
VideoEncoderEncodedImageCallback::new_with_handler(Box::new(RecordingHandler { tx }));
let mut encoder = Mp4PassthroughEncoder::new();
assert_eq!(
encoder.register_encode_complete_callback(Some(callback.as_mut())),
VideoCodecStatus::Ok
);
encode_passthrough_sample(&mut encoder, key0, 0);
encode_passthrough_sample(&mut encoder, delta1, 1);
encoder.enter_keyframe_wait();
encode_passthrough_sample(&mut encoder, delta2, 2);
encode_passthrough_sample(&mut encoder, key8, 8);
drop(callback);
let images: Vec<(VideoFrameType, Vec<u8>)> = rx.iter().collect();
assert_eq!(
images.len(),
3,
"キーフレーム待ちへ戻った後は delta sample を破棄し、次のキーフレームから再開するはずです"
);
assert_eq!(images[0].0, VideoFrameType::Key);
assert_eq!(
images[0].1,
expected_av1_payload(&config_obus, &raw_samples[0].0, true)
);
assert_eq!(images[1].0, VideoFrameType::Delta);
assert_eq!(
images[1].1,
expected_av1_payload(&config_obus, &raw_samples[1].0, false)
);
assert_eq!(images[2].0, VideoFrameType::Key);
assert_eq!(
images[2].1,
expected_av1_payload(&config_obus, &raw_samples[8].0, true)
);
}
fn h264_main_reader_from_fixture(tag: &str) -> (Mp4SampleReader, FixtureFile) {
h264_reader_from_bytes(
tag,
include_bytes!("../../testdata/red-320x320-h264-main.mp4"),
)
}
fn h264_profile_level_id_hex(reader: &Mp4SampleReader) -> String {
reader
.inner
.track_info
.h264_config
.as_ref()
.expect("H.264 track は h264_config を持つはずです")
.profile_level_id
.to_hex()
}
#[test]
fn sample_reader_reads_fixture_h264_main_mp4() {
let (reader, _fixture) = h264_main_reader_from_fixture("main-read");
assert_eq!(reader.codec_type(), VideoCodecType::H264);
assert_eq!(reader.len(), 50, "fixture は 50 サンプルを持つはずです");
assert_eq!(reader.inner.track_info.width, 320);
assert_eq!(reader.inner.track_info.height, 320);
assert_eq!(
h264_profile_level_id_hex(&reader),
"4d4015",
"Main Profile fixture の profile-level-id は 4d4015 のはずです"
);
}
#[test]
fn h264_profile_level_id_matches_high_profile_fixture() {
let (reader, _fixture) = h264_reader_from_fixture("high-plid");
assert_eq!(
h264_profile_level_id_hex(&reader),
"640015",
"High Profile fixture の profile-level-id は 640015 のはずです"
);
}
#[test]
fn required_format_advertises_h264_profile_level_id() {
let (main_reader, _mf) = h264_main_reader_from_fixture("req-main");
let mut main_format = main_reader.required_sdp_format();
let main_params: std::collections::HashMap<String, String> =
main_format.parameters_mut().iter().collect();
assert_eq!(
main_params.get("packetization-mode").map(String::as_str),
Some("1"),
"packetization-mode=1 を広告するはずです"
);
assert_eq!(
main_params.get("profile-level-id").map(String::as_str),
Some("4d4015"),
"Main Profile fixture の profile-level-id を広告するはずです"
);
let (high_reader, _hf) = h264_reader_from_fixture("req-high");
let mut high_format = high_reader.required_sdp_format();
let high_params: std::collections::HashMap<String, String> =
high_format.parameters_mut().iter().collect();
assert_eq!(
high_params.get("profile-level-id").map(String::as_str),
Some("640015"),
"High Profile fixture の profile-level-id を広告するはずです"
);
}
#[test]
fn capability_resolve_sdp_format_validates_h264_profile_level_id() {
let (reader, _fixture) = h264_main_reader_from_fixture("resolve");
let capability = reader.passthrough_capability();
let env = shiguredo_webrtc::Environment::new();
let mut no_plid = SdpVideoFormat::new("H264");
no_plid.parameters_mut().set("packetization-mode", "1");
assert!(
capability
.resolve_sdp_format(CodecDirection::Encoder, no_plid.as_ref())
.is_none(),
"profile-level-id のない format は拒否されるはずです"
);
let mut higher = SdpVideoFormat::new("H264");
higher.parameters_mut().set("packetization-mode", "1");
higher.parameters_mut().set("profile-level-id", "4d0032"); let resolved = capability
.resolve_sdp_format(CodecDirection::Encoder, higher.as_ref())
.expect("互換な higher level は受理されるはずです");
assert!(
capability
.create_video_encoder(env.as_ref(), resolved.as_ref())
.is_some(),
"解決された format で encoder を生成できるはずです"
);
let mut incompatible = SdpVideoFormat::new("H264");
incompatible.parameters_mut().set("packetization-mode", "1");
incompatible
.parameters_mut()
.set("profile-level-id", "640015");
assert!(
capability
.resolve_sdp_format(CodecDirection::Encoder, incompatible.as_ref())
.is_none(),
"sub-profile が異なる format は拒否されるはずです"
);
let mut lower = SdpVideoFormat::new("H264");
lower.parameters_mut().set("packetization-mode", "1");
lower.parameters_mut().set("profile-level-id", "4d000a"); assert!(
capability
.resolve_sdp_format(CodecDirection::Encoder, lower.as_ref())
.is_none(),
"required より低い level は拒否されるはずです"
);
}
const H264_MAIN_SPS: &[u8] = &[
0x67, 0x4d, 0x40, 0x15, 0xd9, 0x01, 0x40, 0xa6, 0xc0, 0x44, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xc8, 0x3c, 0x58, 0xb9, 0x20,
];
const H264_MAIN_PPS: &[u8] = &[0x68, 0xeb, 0xc3, 0xcb, 0x20];
const H264_HIGH_SPS: &[u8] = &[
0x67, 0x64, 0x00, 0x15, 0xac, 0xb2, 0x02, 0x81, 0x4d, 0x80, 0x88, 0x00, 0x00, 0x03, 0x00,
0x08, 0x00, 0x00, 0x03, 0x01, 0x90, 0x78, 0xb1, 0x72, 0x40,
];
const H264_HIGH_PPS: &[u8] = &[0x68, 0xeb, 0xc3, 0xcb, 0x22, 0xc0];
fn build_avc1_sample_entry(
sps_list: &[Vec<u8>],
pps_list: &[Vec<u8>],
avcc_header: (u8, u8, u8),
chroma_format: Option<u8>,
width: u16,
height: u16,
) -> shiguredo_mp4::boxes::SampleEntry {
use shiguredo_mp4::Uint;
use shiguredo_mp4::boxes::{Avc1Box, AvccBox, SampleEntry, VisualSampleEntryFields};
SampleEntry::Avc1(Avc1Box {
visual: VisualSampleEntryFields {
data_reference_index: VisualSampleEntryFields::DEFAULT_DATA_REFERENCE_INDEX,
width,
height,
horizresolution: VisualSampleEntryFields::DEFAULT_HORIZRESOLUTION,
vertresolution: VisualSampleEntryFields::DEFAULT_VERTRESOLUTION,
frame_count: VisualSampleEntryFields::DEFAULT_FRAME_COUNT,
compressorname: VisualSampleEntryFields::NULL_COMPRESSORNAME,
depth: VisualSampleEntryFields::DEFAULT_DEPTH,
},
avcc_box: AvccBox {
avc_profile_indication: avcc_header.0,
profile_compatibility: avcc_header.1,
avc_level_indication: avcc_header.2,
length_size_minus_one: Uint::new(3),
sps_list: sps_list.to_vec(),
pps_list: pps_list.to_vec(),
chroma_format: chroma_format.map(Uint::new),
bit_depth_luma_minus8: chroma_format.map(|_| Uint::new(0)),
bit_depth_chroma_minus8: chroma_format.map(|_| Uint::new(0)),
sps_ext_list: vec![],
},
unknown_boxes: vec![],
})
}
fn new_reader_for_h264_mp4(
tag: &str,
entries: &[shiguredo_mp4::boxes::SampleEntry],
) -> crate::error::Result<Mp4SampleReader> {
use std::num::NonZeroU32;
use shiguredo_mp4::TrackKind;
use shiguredo_mp4::mux::{Mp4FileMuxer, Sample};
let mut muxer = Mp4FileMuxer::new().expect("muxer の作成に失敗しました");
let data_offset_first = muxer.initial_boxes_bytes().len() as u64;
let data_size = 4;
for (index, entry) in entries.iter().enumerate() {
let sample = Sample {
track_kind: TrackKind::Video,
sample_entry: Some(entry.clone()),
keyframe: true,
timescale: NonZeroU32::new(12800).expect("12800 は非ゼロ"),
duration: 12800 / 25,
composition_time_offset: None,
data_offset: data_offset_first + index as u64 * data_size as u64,
data_size,
};
muxer
.append_sample(&sample)
.expect("sample の追加に失敗しました");
}
let initial_bytes = muxer.initial_boxes_bytes().to_vec();
let finalized = muxer.finalize().expect("finalize に失敗しました");
let sample_data_size = data_size * entries.len();
let total_size = initial_bytes.len() + sample_data_size + finalized.moov_box_size() + 1024;
let mut file_data = vec![0u8; total_size];
file_data[..initial_bytes.len()].copy_from_slice(&initial_bytes);
for (offset, bytes) in finalized.offset_and_bytes_pairs() {
let offset = offset as usize;
file_data[offset..offset + bytes.len()].copy_from_slice(bytes);
}
let mut max_end = initial_bytes.len() + sample_data_size;
for (offset, bytes) in finalized.offset_and_bytes_pairs() {
let end = offset as usize + bytes.len();
if end > max_end {
max_end = end;
}
}
file_data.truncate(max_end);
let tmp_name = format!(
"sora-sdk-mp4-h264-synth-{tag}-{}-{}.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, &file_data).expect("一時 MP4 の書き込みに失敗しました");
let result = Mp4SampleReader::new(&tmp_path);
let _ = std::fs::remove_file(&tmp_path);
result
}
#[test]
fn sample_reader_rejects_h264_sample_description_with_different_avcc_box() {
let sps = H264_HIGH_SPS.to_vec();
let pps = H264_HIGH_PPS.to_vec();
let entry1 = build_avc1_sample_entry(
std::slice::from_ref(&sps),
std::slice::from_ref(&pps),
(0x64, 0x00, 0x15),
Some(1),
320,
320,
);
let entry2 = build_avc1_sample_entry(
std::slice::from_ref(&sps),
std::slice::from_ref(&pps),
(0x64, 0x00, 0x15),
Some(2),
320,
320,
);
let result = new_reader_for_h264_mp4("different-avcc-box", &[entry1, entry2]);
match result {
Err(crate::error::Error::Mp4 { source }) => {
assert!(
matches!(source, Mp4Error::InconsistentSampleDescription { index: 1 }),
"avcC box の相違は InconsistentSampleDescription で拒否されるはずです: {source:?}"
);
}
Err(e) => panic!("Mp4 エラーを期待しましたが、実際は: {e}"),
Ok(_) => panic!("Err を期待しましたが、Ok でした"),
}
}
#[test]
fn sample_reader_rejects_h264_sample_description_with_different_profile_level_id() {
let main_sps = H264_MAIN_SPS.to_vec();
let main_pps = H264_MAIN_PPS.to_vec();
let high_sps = H264_HIGH_SPS.to_vec();
let high_pps = H264_HIGH_PPS.to_vec();
let entry1 =
build_avc1_sample_entry(&[main_sps], &[main_pps], (0x4d, 0x40, 0x15), None, 320, 320);
let entry2 = build_avc1_sample_entry(
&[high_sps],
&[high_pps],
(0x64, 0x00, 0x15),
Some(1),
320,
320,
);
let result = new_reader_for_h264_mp4("different-plid", &[entry1, entry2]);
match result {
Err(crate::error::Error::Mp4 { source }) => {
assert!(
matches!(source, Mp4Error::InconsistentSampleDescription { index: 1 }),
"H.264 抽出結果の相違は InconsistentSampleDescription で拒否されるはずです: {source:?}"
);
}
Err(e) => panic!("Mp4 エラーを期待しましたが、実際は: {e}"),
Ok(_) => panic!("Err を期待しましたが、Ok でした"),
}
}
#[test]
fn sample_reader_rejects_h264_truncated_sps() {
let truncated_sps: Vec<u8> = vec![0x67];
let pps = H264_MAIN_PPS.to_vec();
let entry =
build_avc1_sample_entry(&[truncated_sps], &[pps], (0x4d, 0x40, 0x15), None, 320, 320);
let result = new_reader_for_h264_mp4("truncated-sps", &[entry]);
assert_invalid_h264_track(result, "SPS");
}
#[test]
fn sample_reader_rejects_h264_avcc_sps_profile_level_id_mismatch() {
let sps = H264_MAIN_SPS.to_vec();
let pps = H264_MAIN_PPS.to_vec();
let entry = build_avc1_sample_entry(&[sps], &[pps], (0x64, 0x00, 0x15), Some(1), 320, 320);
let result = new_reader_for_h264_mp4("avcc-sps-mismatch", &[entry]);
assert_invalid_h264_track(result, "profile-level-id does not match avcC");
}
#[test]
fn sample_reader_rejects_h264_multiple_sps_inconsistency() {
let sps = H264_MAIN_SPS.to_vec();
let mut sps_level6 = sps.clone();
sps_level6[3] = 0x3c;
let pps = H264_MAIN_PPS.to_vec();
let entry = build_avc1_sample_entry(
&[sps, sps_level6],
&[pps],
(0x4d, 0x40, 0x15),
None,
320,
320,
);
let result = new_reader_for_h264_mp4("multiple-sps", &[entry]);
assert_invalid_h264_track(result, "profile-level-id does not match avcC");
}
#[test]
fn sample_reader_rejects_h264_sps_visual_dimension_mismatch() {
let sps = H264_MAIN_SPS.to_vec();
let pps = H264_MAIN_PPS.to_vec();
let entry = build_avc1_sample_entry(&[sps], &[pps], (0x4d, 0x40, 0x15), None, 640, 640);
let result = new_reader_for_h264_mp4("dimension-mismatch", &[entry]);
assert_invalid_h264_track(result, "dimensions do not match avc1");
}
#[test]
fn sample_reader_rejects_h264_unrecognized_level() {
let mut sps = H264_MAIN_SPS.to_vec();
sps[3] = 0x3c;
let pps = H264_MAIN_PPS.to_vec();
let entry = build_avc1_sample_entry(&[sps], &[pps], (0x4d, 0x40, 0x3c), None, 320, 320);
let result = new_reader_for_h264_mp4("unrecognized-level", &[entry]);
assert_invalid_h264_track(result, "not recognized by the fixed libwebrtc");
}
#[test]
fn sample_reader_rejects_h264_empty_sps_list() {
let pps = H264_MAIN_PPS.to_vec();
let entry = build_avc1_sample_entry(&[], &[pps], (0x4d, 0x40, 0x15), None, 320, 320);
let result = new_reader_for_h264_mp4("empty-sps", &[entry]);
assert_invalid_h264_track(result, "SPS list must not be empty");
}
#[test]
fn sample_reader_rejects_h264_empty_pps_list() {
let sps = H264_MAIN_SPS.to_vec();
let entry = build_avc1_sample_entry(&[sps], &[], (0x4d, 0x40, 0x15), None, 320, 320);
let result = new_reader_for_h264_mp4("empty-pps", &[entry]);
assert_invalid_h264_track(result, "PPS list must not be empty");
}
#[test]
fn sample_reader_rejects_h264_invalid_pps() {
let sps = H264_MAIN_SPS.to_vec();
let empty_pps: Vec<u8> = vec![];
let entry = build_avc1_sample_entry(
std::slice::from_ref(&sps),
&[empty_pps],
(0x4d, 0x40, 0x15),
None,
320,
320,
);
let result = new_reader_for_h264_mp4("invalid-pps-empty", &[entry]);
assert_invalid_h264_track(result, "PPS #0 is empty");
let fzb_pps: Vec<u8> = vec![0xe8, 0xeb, 0xc3, 0xcb, 0x20];
let entry = build_avc1_sample_entry(
std::slice::from_ref(&sps),
&[fzb_pps],
(0x4d, 0x40, 0x15),
None,
320,
320,
);
let result = new_reader_for_h264_mp4("invalid-pps-fzb", &[entry]);
assert_invalid_h264_track(result, "PPS #0 forbidden_zero_bit must be 0");
let sps_as_pps: Vec<u8> = vec![0x67, 0xeb, 0xc3, 0xcb, 0x20];
let entry =
build_avc1_sample_entry(&[sps], &[sps_as_pps], (0x4d, 0x40, 0x15), None, 320, 320);
let result = new_reader_for_h264_mp4("invalid-pps-naltype", &[entry]);
assert_invalid_h264_track(result, "PPS #0 NAL unit type must be 8");
}
fn assert_invalid_h264_track(result: crate::error::Result<Mp4SampleReader>, expected: &str) {
match result {
Err(crate::error::Error::Mp4 { source }) => {
let message = format!("{source}");
assert!(
matches!(source, Mp4Error::InvalidH264Track(_)),
"InvalidH264Track エラーを期待しましたが、実際は: {source:?}"
);
assert!(
message.contains(expected),
"エラーメッセージに {expected:?} が含まれるはずです: {message}"
);
}
Err(e) => panic!("Mp4 エラーを期待しましたが、実際は: {e}"),
Ok(_) => panic!("Err を期待しましたが、Ok でした"),
}
}
#[test]
fn factory_creates_encoder_based_on_h264_profile_level_id_negotiation() {
use shiguredo_webrtc::VideoEncoderFactoryHandler;
use crate::video_codec::SoraVideoEncoderFactory;
use crate::video_codec_preference::{
VideoCodecPreference, validate_video_codec_preference,
};
let (reader, _fixture) = h264_main_reader_from_fixture("factory");
let capability = reader.passthrough_capability();
let preference = VideoCodecPreference::new_from_capability(&capability);
let capabilities: Vec<Box<dyn VideoCodecCapability>> = vec![Box::new(capability)];
validate_video_codec_preference(&preference, &capabilities)
.expect("preference の検証は成功するはずです");
let shared = Arc::new(std::sync::Mutex::new(capabilities));
let mut factory = SoraVideoEncoderFactory::new(preference, shared);
let env = shiguredo_webrtc::Environment::new();
let mut compatible = SdpVideoFormat::new("H264");
compatible.parameters_mut().set("packetization-mode", "1");
compatible
.parameters_mut()
.set("profile-level-id", "4d0032"); assert!(
VideoEncoderFactoryHandler::create(&mut factory, env.as_ref(), compatible.as_ref())
.is_some(),
"互換な higher level では encoder を生成するはずです"
);
let no_plid = SdpVideoFormat::new("H264");
assert!(
VideoEncoderFactoryHandler::create(&mut factory, env.as_ref(), no_plid.as_ref())
.is_none(),
"profile-level-id のない format では encoder を生成しないはずです"
);
let mut incompatible = SdpVideoFormat::new("H264");
incompatible.parameters_mut().set("packetization-mode", "1");
incompatible
.parameters_mut()
.set("profile-level-id", "640015"); assert!(
VideoEncoderFactoryHandler::create(&mut factory, env.as_ref(), incompatible.as_ref())
.is_none(),
"sub-profile が異なる format では encoder を生成しないはずです"
);
let mut lower = SdpVideoFormat::new("H264");
lower.parameters_mut().set("packetization-mode", "1");
lower.parameters_mut().set("profile-level-id", "4d000a"); assert!(
VideoEncoderFactoryHandler::create(&mut factory, env.as_ref(), lower.as_ref())
.is_none(),
"required より低い level の format では encoder を生成しないはずです"
);
}
}