環境: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就中止。 - 出片後逐幀統計像素格式,混到兩種以上不放行。
- 已經出去的成品,整支重壓成統一的
yuv420pHigh 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 for what 4:2:0 versus 4:4:4 means.
The fix
- Set
-pix_fmt yuv420pexplicitly on every segment encode, addformat=yuv420pat the end of the filter chain, and pin-profile:v high. Do not let libx264 choose. - Before concatenating, check each segment with
ffprobeand abort if any is notyuv420p. - 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
yuv420pHigh 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 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.
— 邱柏宇
延伸閱讀
- 金蘋果樂園 1.21.0,朋友家門口可以插木牌了
- 日本商圈感受到的「特色」,其實是一套治理制度
- 400 不等於壞掉,只是還沒好
- 過濾器沒有出錯,它只是把連結全部吃掉了
- robots.txt 一直都在,只是裡面裝的是 HTML
https://justfly.idv.tw/s/d6cuxr9