Skip to content

Commit acd9073

Browse files
authored
0002 mux_mp4_file.rs の映像解像度 i16 符号反転を修正する (#53)
- src/mux_mp4_file.rs の build_video_trak_box で max_width / max_height の 暗黙キャスト `as i16` を i16::try_from() に置き換え、i16::MAX を超える 解像度では MuxError::EncodeError を返すようにする - エラー種別とメッセージは mux_fmp4_segment.rs:613-636 の防御パターンに 揃え、"video width exceeds i16::MAX" / "video height exceeds i16::MAX" を採用する - テストモジュールに共通ヘルパー finalize_after_appending_video_sample を 追加し、let-else でサンプルエントリ取り出しの silent skip リスクを排除 する - テスト 4 件 (境界値成功 / faststart 経路 / 幅超過エラー / 高さ超過エラー) を追加する
1 parent f15acc8 commit acd9073

3 files changed

Lines changed: 116 additions & 2 deletions

File tree

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
- `u32::try_from()` で明示的にチェックし、超過時は `MuxError::EncodeError` を返すように変更した
1717
- 同様に `build_stbl_box` 内の `sample_per_chunk` でも `c.samples.len()``u32` 暗黙キャストを防御する
1818
- @voluntas
19+
- [FIX] `Mp4FileMuxer``build_video_trak_box()` で映像解像度の幅と高さが `i16::MAX` を超える場合にエラーを返すようにする
20+
- これまでは `u16` から `i16` への暗黙キャストで符号が反転し、`tkhd``width` / `height` が負の値になる可能性があった
21+
- `i16::try_from()` で明示的にチェックし、超過時は `MuxError::EncodeError` を返すように変更した
22+
- @voluntas
1923

2024
## 2026.3.0
2125

issues/0002-bug-mux-mp4-file-resolution-sign-inversion.md renamed to issues/closed/0002-bug-mux-mp4-file-resolution-sign-inversion.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# mux_mp4_file.rs で映像解像度の u16→i16 キャストにより符号反転が発生する
22

33
Created: 2026-05-20
4+
Completed: 2026-05-20
45
Model: opencode mimo-v2.5-pro
6+
Branch: feature/fix-mux-mp4-file-resolution-sign-inversion
57

68
## 概要
79

@@ -108,3 +110,21 @@ height: FixedPointNumber::new(i16::try_from(max_height).map_err(|_| {
108110
## CHANGES.md
109111

110112
`[FIX]` で記載する。
113+
114+
## 解決方法
115+
116+
- `src/mux_mp4_file.rs``build_video_trak_box``max_width as i16` / `max_height as i16`
117+
暗黙キャストを `i16::try_from()` ベースに置き換えた。エラー時は
118+
`MuxError::EncodeError(Error::invalid_data("video {width|height} exceeds i16::MAX"))` を返す。
119+
`mux_fmp4_segment.rs:613-636` と同じ防御パターンに揃えている。
120+
- `src/mux_mp4_file.rs``#[cfg(test)] mod tests` に共通ヘルパー
121+
`finalize_after_appending_video_sample(muxer, width, height)` を追加し、
122+
以下 4 テストを実装した:
123+
- `test_finalize_video_resolution_i16_max_succeeds`: width/height が `i16::MAX` (32767)
124+
の境界値で `finalize()` が成功することを検証する
125+
- `test_finalize_video_resolution_i16_max_with_faststart`: faststart 有効化
126+
(`Mp4FileMuxerOptions::reserved_moov_box_size > 0`) 経路でも同じ境界値挙動を検証する
127+
- `test_finalize_video_width_exceeds_i16_max`: width = 32768 で `MuxError::EncodeError`
128+
が返り、Display 出力が `"video width exceeds i16::MAX"` を含むことを検証する
129+
- `test_finalize_video_height_exceeds_i16_max`: height = 32768 で同様に
130+
`"video height exceeds i16::MAX"` を含むことを検証する

src/mux_mp4_file.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,13 @@ impl Mp4FileMuxer {
855855
(max_w.max(w), max_h.max(h))
856856
});
857857

858+
let width = i16::try_from(max_width).map_err(|_| {
859+
MuxError::EncodeError(Error::invalid_data("video width exceeds i16::MAX"))
860+
})?;
861+
let height = i16::try_from(max_height).map_err(|_| {
862+
MuxError::EncodeError(Error::invalid_data("video height exceeds i16::MAX"))
863+
})?;
864+
858865
let creation_time = Mp4FileTime::from_unix_time(self.options.creation_timestamp);
859866
let tkhd_box = TkhdBox {
860867
flag_track_enabled: true,
@@ -869,8 +876,8 @@ impl Mp4FileMuxer {
869876
alternate_group: TkhdBox::DEFAULT_ALTERNATE_GROUP,
870877
volume: TkhdBox::DEFAULT_VIDEO_VOLUME,
871878
matrix: TkhdBox::DEFAULT_MATRIX,
872-
width: FixedPointNumber::new(max_width as i16, 0),
873-
height: FixedPointNumber::new(max_height as i16, 0),
879+
width: FixedPointNumber::new(width, 0),
880+
height: FixedPointNumber::new(height, 0),
874881
};
875882

876883
Ok(TrakBox {
@@ -1412,6 +1419,89 @@ mod tests {
14121419
muxer.finalize().expect("failed to finalize muxer");
14131420
}
14141421

1422+
/// `muxer` に解像度 `width` x `height` の AVC1 サンプルを 1 つ追加して `finalize()` を呼ぶ
1423+
fn finalize_after_appending_video_sample(
1424+
muxer: &mut Mp4FileMuxer,
1425+
width: u16,
1426+
height: u16,
1427+
) -> Result<(), MuxError> {
1428+
let initial_size = muxer.initial_boxes_bytes().len() as u64;
1429+
let mut entry = create_avc1_sample_entry();
1430+
let SampleEntry::Avc1(avc1) = &mut entry else {
1431+
panic!("create_avc1_sample_entry must return SampleEntry::Avc1");
1432+
};
1433+
avc1.visual.width = width;
1434+
avc1.visual.height = height;
1435+
1436+
let sample = Sample {
1437+
track_kind: TrackKind::Video,
1438+
sample_entry: Some(entry),
1439+
keyframe: true,
1440+
timescale: NonZeroU32::MIN.saturating_add(30 - 1),
1441+
duration: 1,
1442+
composition_time_offset: None,
1443+
data_offset: initial_size,
1444+
data_size: 1024,
1445+
};
1446+
muxer
1447+
.append_sample(&sample)
1448+
.expect("failed to append sample");
1449+
muxer.finalize().map(|_| ())
1450+
}
1451+
1452+
/// 映像解像度の幅と高さが i16::MAX (32767) の境界値で finalize が成功するテスト
1453+
#[test]
1454+
fn test_finalize_video_resolution_i16_max_succeeds() {
1455+
let mut muxer = Mp4FileMuxer::new().expect("failed to create muxer");
1456+
finalize_after_appending_video_sample(&mut muxer, i16::MAX as u16, i16::MAX as u16)
1457+
.expect("failed to finalize at i16::MAX resolution");
1458+
}
1459+
1460+
/// faststart 有効化 (with_options) 経路でも i16::MAX 境界値で finalize が成功するテスト
1461+
#[test]
1462+
fn test_finalize_video_resolution_i16_max_with_faststart() {
1463+
let options = Mp4FileMuxerOptions {
1464+
reserved_moov_box_size: 8192,
1465+
..Default::default()
1466+
};
1467+
let mut muxer =
1468+
Mp4FileMuxer::with_options(options).expect("failed to create muxer with options");
1469+
finalize_after_appending_video_sample(&mut muxer, i16::MAX as u16, i16::MAX as u16)
1470+
.expect("failed to finalize at i16::MAX resolution under faststart");
1471+
}
1472+
1473+
/// 映像幅が i16::MAX を超える場合に width 側のエラーメッセージが返ることを検証するテスト
1474+
#[test]
1475+
fn test_finalize_video_width_exceeds_i16_max() {
1476+
// i16::MAX を超える最小値の u16 (= 32768) を渡す
1477+
let mut muxer = Mp4FileMuxer::new().expect("failed to create muxer");
1478+
let err = finalize_after_appending_video_sample(&mut muxer, 32_768, 1)
1479+
.expect_err("expected encode error for width exceeding i16::MAX");
1480+
assert!(matches!(err, MuxError::EncodeError(_)));
1481+
// MuxError::EncodeError は他原因でも返るためメッセージ内容まで確認する
1482+
let message = format!("{err}");
1483+
assert!(
1484+
message.contains("video width exceeds i16::MAX"),
1485+
"unexpected error message: {message}",
1486+
);
1487+
}
1488+
1489+
/// 映像高さが i16::MAX を超える場合に height 側のエラーメッセージが返ることを検証するテスト
1490+
#[test]
1491+
fn test_finalize_video_height_exceeds_i16_max() {
1492+
// i16::MAX を超える最小値の u16 (= 32768) を渡す
1493+
let mut muxer = Mp4FileMuxer::new().expect("failed to create muxer");
1494+
let err = finalize_after_appending_video_sample(&mut muxer, 1, 32_768)
1495+
.expect_err("expected encode error for height exceeding i16::MAX");
1496+
assert!(matches!(err, MuxError::EncodeError(_)));
1497+
// MuxError::EncodeError は他原因でも返るためメッセージ内容まで確認する
1498+
let message = format!("{err}");
1499+
assert!(
1500+
message.contains("video height exceeds i16::MAX"),
1501+
"unexpected error message: {message}",
1502+
);
1503+
}
1504+
14151505
/// 音声と映像の複数トラックのテスト
14161506
#[test]
14171507
fn test_audio_and_video_tracks() {

0 commit comments

Comments
 (0)