# 白閃轉場讓 iPhone 卡住：同一支 H.264 混了 4:4:4

- URL: https://justfly.idv.tw/%e7%99%bd%e9%96%83%e8%bd%89%e5%a0%b4%e8%ae%93-iphone-%e5%8d%a1%e4%bd%8f%ef%bc%9a%e5%90%8c%e4%b8%80%e6%94%af-h-264-%e6%b7%b7%e4%ba%86-444/
- 日期: 2026-11-02
- 分類: 技術筆記
- 標籤: ffmpeg, H.264, iphone, web

![白閃轉場讓 iPhone 卡住：同一支 H.264 混了 4:4:4]
環境：ffmpeg＋libx264，直式短影片，轉場用 `fade=color=white` 做白閃，每段各自壓好再用 `-c copy` 串成一支。症狀：Chrome 播放正常，iPhone／Safari 播到轉場段就停住。轉場位置在 3.0s 和 16.8s 兩處穿白場，卡的就是那兩處。會遇到的場景：分段濾鏡處理、最後用 stream copy 拼接 H.264 的流程。

##### 為什麼只有 iPhone 卡

Chrome 這邊走的是軟體解碼，對混格式的串流容忍度高，出片時看起來一切正常。iPhone／Safari 用的是硬體解碼，播到轉場段就停。所以只在桌機 Chrome 驗片，這個問題不會浮出來。

##### 根因：fade 走 RGB 路徑，libx264 又自己挑格式

`fade` 帶 `color=white` 時，畫面走 RGB 路徑。濾鏡鏈尾端沒有指定像素格式，libx264 就依輸入格式自己挑，那幾段被壓成 H.264 4:4:4（`yuv444p`）。沒有轉場的段落仍是 `yuv420p`。

之後用 `-c copy` 串接，不重編碼，各段的編碼參數原封不動進同一條串流。實測同一支檔案裡有 342 格 `yuv420p` 和 386 格 `yuv444p` 混在一起。硬體解碼器播到格式切換的位置就停住。

一個細節：只要有任何一段漏了 `-pix_fmt`，就可能混進 4:4:4。不是只有 `fade`，其他會讓畫面走 RGB 路徑的濾鏡同理。

##### 修法

- 每一段編碼都明確指定 `-pix_fmt yuv420p`，並在濾鏡鏈尾端加 `format=yuv420p`，同時鎖 `-profile:v high`，不靠 libx264 自己挑。

- 串接前逐段用 `ffprobe` 檢查，任何一段不是 `yuv420p` 就中止。

- 出片後逐幀統計像素格式，混到兩種以上不放行。

- 已經出去的成品，整支重壓成統一的 `yuv420p` High profile 後替換。

```
# 1) 加白閃轉場時明確指定像素格式與 profile，不讓 libx264 自己挑 4:4:4
ffmpeg -i seg_in.mp4 \
  -vf "fade=t=out:st=2.7:d=0.3:color=white,format=yuv420p" \
  -c:v libx264 -pix_fmt yuv420p -profile:v high \
  -c:a copy seg_out.mp4

# 2) 串接前逐段檢查：每段都要是 yuv420p
for f in seg_*.mp4; do
  pf=$(ffprobe -v error -select_streams v:0 \
        -show_entries stream=pix_fmt -of csv=p=0 "$f")
  echo "$f $pf"
  [ "$pf" = "yuv420p" ] || { echo "NG: $f 不是 yuv420p"; exit 1; }
done

# 3) 出片後逐幀統計像素格式，混到兩種以上就不放行
ffprobe -v error -select_streams v:0 -show_frames \
  -show_entries frame=pix_fmt -of csv=p=0 final.mp4 | sort | uniq -c

# 4) 已經混格式的成品：整支重壓成統一的 yuv420p High profile
ffmpeg -i final.mp4 -c:v libx264 -pix_fmt yuv420p -profile:v high \
  -c:a copy final_fixed.mp4
```

##### 驗證

第 3 段指令是關鍵。`stream=pix_fmt` 只看串流標頭，標頭可能只寫第一種格式；`-show_frames` 逐幀列出 `pix_fmt` 再 `uniq -c`，才看得出實際混了什麼。修正前輸出同時有 `yuv420p` 和 `yuv444p` 兩行；修正後只剩 `yuv420p` 一行。

這次重壓後的成片是 735 格、24.5 秒，和原檔逐格比對一致，735 幀全為 `yuv420p`，逐幀 PTS 間隔全是 33.3ms。白閃處滿白（255）、黑場在 6.17–6.20s，內容沒被動到。重壓完再用 `-c copy` remux 也正常。最後一步不能省：拿 iPhone／Safari 實際播一遍，看轉場段能不能順過。

##### 適用範圍與限制

適用於用 ffmpeg 濾鏡（`fade` 帶 color，或其他會讓畫面走 RGB 路徑的濾鏡）分段處理、再用 `-c copy` 串接的 H.264 影片。目前只驗證了 iPhone／Safari 硬體解碼會卡、Chrome 不會；其他播放器沒有實測，不能推斷它們一定正常或一定會壞。

另外，`-c copy` 串接的前提是各段編碼參數一致，像素格式只是其中一項。檢查清單裡可以順手把 profile 也加進去。

— 邱柏宇

### A White Flash Froze Safari: Mixed 4:4:4 in H.264

Environment: ffmpeg with libx264, vertical short video, a white-flash transition made with `fade=color=white`, each segment encoded separately and joined with `-c copy`. Symptom: plays fine in Chrome, but iPhone/Safari stops at the transition. The white passes at 3.0s and 16.8s, and those are exactly the places where playback froze. Any pipeline that encodes segments through filters and then concatenates H.264 with stream copy can hit this.

##### Why only the iPhone froze

Chrome was decoding in software, which tolerated the mixed stream, so the file looked fine at release time. iPhone/Safari uses hardware decoding and stops when it reaches the transition segments. Checking only in desktop Chrome will never surface this.

##### Root cause: fade goes through RGB, libx264 picks the format itself

With `color=white`, `fade` sends frames through an RGB path. With no pixel format specified at the end of the filter chain, libx264 chose one from the input, and those segments came out as H.264 4:4:4 (`yuv444p`). Segments without a transition stayed `yuv420p`.

Joining with `-c copy` does not re-encode, so each segment’s parameters go into the same stream untouched. The final file contained 342 frames of `yuv420p` and 386 frames of `yuv444p` mixed together. The hardware decoder stops where the format switches.

One detail: any segment that misses `-pix_fmt` can pull 4:4:4 in. It is not specific to `fade`; other filters that push frames through RGB behave the same way. See [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling) for what 4:2:0 versus 4:4:4 means.

##### The fix

- Set `-pix_fmt yuv420p` explicitly on every segment encode, add `format=yuv420p` at the end of the filter chain, and pin `-profile:v high`. Do not let libx264 choose.

- Before concatenating, check each segment with `ffprobe` and abort if any is not `yuv420p`.

- After producing the file, count pixel formats per frame and reject anything with more than one.

- For files already shipped, re-encode the whole thing to a single `yuv420p` High profile stream and replace it.

```
# 1) 加白閃轉場時明確指定像素格式與 profile，不讓 libx264 自己挑 4:4:4
ffmpeg -i seg_in.mp4 \
  -vf "fade=t=out:st=2.7:d=0.3:color=white,format=yuv420p" \
  -c:v libx264 -pix_fmt yuv420p -profile:v high \
  -c:a copy seg_out.mp4

# 2) 串接前逐段檢查：每段都要是 yuv420p
for f in seg_*.mp4; do
  pf=$(ffprobe -v error -select_streams v:0 \
        -show_entries stream=pix_fmt -of csv=p=0 "$f")
  echo "$f $pf"
  [ "$pf" = "yuv420p" ] || { echo "NG: $f 不是 yuv420p"; exit 1; }
done

# 3) 出片後逐幀統計像素格式，混到兩種以上就不放行
ffprobe -v error -select_streams v:0 -show_frames \
  -show_entries frame=pix_fmt -of csv=p=0 final.mp4 | sort | uniq -c

# 4) 已經混格式的成品：整支重壓成統一的 yuv420p High profile
ffmpeg -i final.mp4 -c:v libx264 -pix_fmt yuv420p -profile:v high \
  -c:a copy final_fixed.mp4
```

##### Verification

Command 3 is the one that matters. `stream=pix_fmt` only reads the stream header, which may report just the first format. `-show_frames` lists `pix_fmt` per frame, and `uniq -c` shows what is really in there. Before the fix the output has two lines, `yuv420p` and `yuv444p`; after, only `yuv420p`.

The re-encoded file has 735 frames and a duration of 24.5 seconds, identical to the original frame by frame, all 735 frames `yuv420p`, every PTS step 33.3ms. The white flash still peaks at full white (255) and the black gap sits at 6.17–6.20s, so the content was untouched. A `-c copy` remux of the fixed file also plays normally. The last step is not optional: play it on an iPhone in Safari and see whether the transitions pass. See also the [H.264 article](https://en.wikipedia.org/wiki/Advanced_Video_Coding) for profile background.

##### Scope and limits

This applies to H.264 video processed per segment with ffmpeg filters (`fade` with a color, or any filter that pushes frames through RGB) and then joined with `-c copy`. Only iPhone/Safari hardware decoding (stalls) and Chrome (fine) were tested. Other players were not tried, so nothing can be assumed either way.

Stream-copy concatenation also assumes all segments share encoding parameters; pixel format is just one of them. Adding profile to the per-segment check is cheap.

— 邱柏宇
