戰(zhàn)與參數(shù)調(diào)優(yōu))
做漫劇平臺(tái)最容易被低估的模塊就是視頻處理。我一開(kāi)始以為無(wú)非就是把 MP4 扔給 FFmpeg 轉(zhuǎn)個(gè)碼、切個(gè)片等真正接到動(dòng)漫短劇這種高密度動(dòng)態(tài)畫(huà)面和大量文字字幕的內(nèi)容時(shí)才發(fā)現(xiàn)細(xì)節(jié)遠(yuǎn)比想象中復(fù)雜。Java 后端在這一環(huán)里承擔(dān)的不只是“調(diào)起命令行”而是完整的任務(wù)編排、參數(shù)策略、狀態(tài)管理和異常兜底。這篇文章我結(jié)合自己做漫劇系統(tǒng)的經(jīng)驗(yàn)把視頻處理這條鏈路從選型到落地、從參數(shù)調(diào)優(yōu)到線上排障完整拆開(kāi)講一遍。內(nèi)容偏實(shí)戰(zhàn)適合正在做短視頻平臺(tái)、短劇 App、漫畫(huà)動(dòng)態(tài)化產(chǎn)品以及任何在 Java 技術(shù)棧里集成 FFmpeg 的團(tuán)隊(duì)參考。1. 漫劇系統(tǒng)對(duì)視頻處理的特殊要求為什么通用轉(zhuǎn)碼方案不夠用1.1 漫劇內(nèi)容的特點(diǎn)決定了轉(zhuǎn)碼策略不能照搬普通影視劇、短視頻和漫劇的差異很多人一開(kāi)始沒(méi)意識(shí)到。漫劇本質(zhì)上是漫畫(huà)的動(dòng)態(tài)化畫(huà)面往往是高對(duì)比度的線條、大面積色塊、密集的文字氣泡這些內(nèi)容對(duì)編碼器的“邊緣保持”能力要求極高。用一套默認(rèn)的 CRF 參數(shù)去轉(zhuǎn)出來(lái)的畫(huà)面很容易出現(xiàn)文字邊緣發(fā)虛、線條抖動(dòng)、色塊斑駁觀感非常差。另外漫劇的時(shí)長(zhǎng)結(jié)構(gòu)也不一樣。一集可能只有 1 到 3 分鐘但動(dòng)輒幾百上千集而且更新頻率高、熱點(diǎn)期可能一次性批量上傳幾十集。這意味著視頻處理鏈路必須有很強(qiáng)的吞吐能力任務(wù)調(diào)度必須能扛住突發(fā)壓力而不是單機(jī)串行處理。還有一點(diǎn)容易被忽略漫劇通常需要多語(yǔ)言字幕。中文語(yǔ)音軌、日配、中文字幕、雙語(yǔ)字幕這些都要求在轉(zhuǎn)碼階段處理或者至少為后續(xù)的字幕軌道封裝預(yù)留設(shè)計(jì)否則等人氣起來(lái)了再改成本會(huì)成倍增加。1.2 整體視頻處理鏈路應(yīng)該長(zhǎng)什么樣漫劇系統(tǒng)的視頻處理我用一條主線來(lái)概括上傳 → 校驗(yàn) → 轉(zhuǎn)碼 → 切片 → 分發(fā) → 播放。六個(gè)環(huán)節(jié)缺一不可但真正決定用戶體驗(yàn)的往往是中間的轉(zhuǎn)碼和切片。一個(gè)可落地的架構(gòu)大概是這樣的客戶端上傳原始 MP4 到對(duì)象存儲(chǔ)上傳完成后回調(diào) Java 后端生成轉(zhuǎn)碼任務(wù)。任務(wù)進(jìn)入消息隊(duì)列異步消費(fèi)避免上傳接口被轉(zhuǎn)碼耗時(shí)拖垮。轉(zhuǎn)碼服務(wù)拉取原文件調(diào)用 FFmpeg 輸出多碼率視頻和 HLS 切片。轉(zhuǎn)碼產(chǎn)物回傳對(duì)象存儲(chǔ)CDN 加速分發(fā)。數(shù)據(jù)庫(kù)記錄任務(wù)狀態(tài)和產(chǎn)物地址播放器側(cè)請(qǐng)求帶簽名或防盜鏈的播放地址。這個(gè)鏈路我在多個(gè)項(xiàng)目里驗(yàn)證過(guò)最穩(wěn)的方案是 Java RabbitMQ FFmpeg Redis對(duì)象存儲(chǔ)選哪家都能適配因?yàn)楹诵倪壿嫸荚?Java 側(cè)封裝。1.3 技術(shù)選型Java 在視頻處理里的位置不是替代 FFmpeg而是駕馭它很多團(tuán)隊(duì)糾結(jié)要不要用 JavaCV其實(shí)沒(méi)必要把 JavaCV 當(dāng)成主方案。JavaCV 本質(zhì)上是對(duì) FFmpeg 的 JNI 封裝適合做輕量的實(shí)時(shí)處理、抽幀、邊解碼邊推流但在批量的高質(zhì)量轉(zhuǎn)碼場(chǎng)景下直接調(diào)用 FFmpeg 的可執(zhí)行文件反而更穩(wěn)定。原因很簡(jiǎn)單FFmpeg 迭代太快JavaCV 版本滯后容易出現(xiàn)編碼器支持不全的問(wèn)題而且它自身的內(nèi)存管理容易把 JVM 搞得很難受。我在項(xiàng)目中用的方案是機(jī)器上安裝 FFmpeg 可執(zhí)行文件Java 側(cè)通過(guò) ProcessBuilder 拉起進(jìn)程核心轉(zhuǎn)碼邏輯全部用 FFmpeg 參數(shù)表達(dá)。這樣既能保證編解碼器的完整能力又不會(huì)讓 JVM 直接承擔(dān) C 庫(kù)崩潰的風(fēng)險(xiǎn)。進(jìn)程退出碼加日志輸出足夠覆蓋絕大多數(shù)異常場(chǎng)景。2. 核心鏈路落地Java 與 FFmpeg 的集成細(xì)節(jié)2.1 三種進(jìn)程調(diào)用方式我只推薦 ProcessBuilderJava 里調(diào)用 FFmpeg 常見(jiàn)有三種方式Runtime.exec、ProcessBuilder、JavaCV。Runtime.exec 雖然寫(xiě)起來(lái)簡(jiǎn)單但多個(gè)參數(shù)拼接在一條命令字符串里很容易出現(xiàn)空格、引號(hào)、特殊字符的轉(zhuǎn)義問(wèn)題尤其是文件路徑帶中文或者帶空格時(shí)線上會(huì)頻繁踩雷。ProcessBuilder 直接傳參數(shù)列表不走 shell 解析徹底規(guī)避了注入和轉(zhuǎn)義問(wèn)題這也是我最推薦的方式。JavaCV 適合做實(shí)時(shí)流和抽幀但對(duì)轉(zhuǎn)碼任務(wù)的過(guò)程控制不夠直接而且如果遇到 FFmpeg 需要?jiǎng)討B(tài)加載第三方庫(kù)的場(chǎng)景JavaCV 的配置會(huì)更頭疼。一個(gè)最基礎(chǔ)的封裝看起來(lái)是這樣public class FFmpegCommandRunner { private static final String FFMPEG_PATH /usr/bin/ffmpeg; public boolean execute(ListString args, long timeoutSeconds) throws IOException, InterruptedException { ListString command new ArrayList(); command.add(FFMPEG_PATH); command.addAll(args); ProcessBuilder pb new ProcessBuilder(command); pb.redirectErrorStream(false); Process process pb.start(); // 必須異步消費(fèi) stderr否則緩沖區(qū)占滿會(huì)阻塞進(jìn)程 CompletableFutureString errorFuture CompletableFuture.supplyAsync(() - readStream(process.getErrorStream())); CompletableFutureString outputFuture CompletableFuture.supplyAsync(() - readStream(process.getInputStream())); boolean finished process.waitFor(timeoutSeconds, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); return false; } int exitCode process.exitValue(); return exitCode 0; } }這段代碼里藏著幾個(gè)容易踩的坑第一waitFor一定要帶超時(shí)時(shí)間否則 FFmpeg 因?yàn)閰?shù)錯(cuò)誤或輸入源異常掛起任務(wù)會(huì)卡死在隊(duì)列里第二stderr 必須異步讀因?yàn)?FFmpeg 的進(jìn)度日志輸出在 stderr不消費(fèi)就會(huì)把管道緩沖區(qū)寫(xiě)滿導(dǎo)致進(jìn)程阻塞第三超時(shí)后要用destroyForcibly光調(diào)destroy可能不夠FFmpeg 的子進(jìn)程有可能會(huì)殘留。2.2 參數(shù)注入用列表構(gòu)建命令不拼字符串構(gòu)建 FFmpeg 參數(shù)時(shí)我最常犯過(guò)的錯(cuò)誤就是試圖拼接成一條字符串命令。后來(lái)我把參數(shù)構(gòu)建單獨(dú)抽了一層所有參數(shù)用ListString組裝其中只有固定開(kāi)關(guān)和路徑變量避免任何人肉拼裝。以一段最基礎(chǔ)的轉(zhuǎn)碼為例ListString args convertArgs(inputPath, outputPath, libx264, 20, 1280x720);這個(gè)方法的內(nèi)部邏輯就是通過(guò)command.add(-i)、command.add(inputPath)這種方式把-i和它的值分成兩個(gè)獨(dú)立元素傳入。FFmpeg 天然支持這種參數(shù)數(shù)組調(diào)用不需要 shell 解釋這就把路徑里的空格、中文、特殊符號(hào)問(wèn)題全部隔離掉了。2.3 轉(zhuǎn)碼進(jìn)度的可視化與監(jiān)控怎么做用戶上傳視頻后前端經(jīng)常會(huì)展示進(jìn)度條這個(gè)需求躲不掉。FFmpeg 本身會(huì)向 stderr 輸出形如time00:01:23.45 bitrate 512kbps的日志Java 側(cè)只需要在讀取 stderr 的流里做正則解析把當(dāng)前時(shí)間戳換算成百分比寫(xiě)入 Redis 即可。但這里有一個(gè)關(guān)鍵細(xì)節(jié)如果用-progress pipe:1參數(shù)FFmpeg 會(huì)把進(jìn)度信息輸出到 stdout 并用 keyvalue 的結(jié)構(gòu)化格式給出解析起來(lái)比正則匹配簡(jiǎn)單得多而且穩(wěn)定性更好。我在線上用的就是這個(gè)方式每 500 毫秒刷新一次 Redis 進(jìn)度值前端通過(guò)輪詢或者 WebSocket 獲取。3. 轉(zhuǎn)碼參數(shù)調(diào)優(yōu)動(dòng)漫短劇畫(huà)質(zhì)不是越高越好要匹配人眼觀感3.1 碼率控制為什么我不建議無(wú)腦用 CRF 18很多人看到 1080P、高碼率就覺(jué)得畫(huà)質(zhì)好但在漫劇場(chǎng)景里畫(huà)質(zhì)觀感不僅依賴碼率更依賴編碼器對(duì)邊緣和文字的處理。漫畫(huà)線條是高頻信息碼率給得不夠就會(huì)產(chǎn)生振鈴效應(yīng)文字邊緣出現(xiàn)白色光暈碼率給得過(guò)高又會(huì)導(dǎo)致文件體積暴漲CDN 成本直線上升。我實(shí)測(cè)下來(lái)x264 編碼器 -preset veryfast-crf 20是漫劇內(nèi)容一個(gè)比較均衡的起點(diǎn)。CRF 值越低畫(huà)質(zhì)越好但 18 和 20 在大多數(shù)手機(jī)屏幕上幾乎沒(méi)有肉眼差異而文件體積能差 15% 到 25%。如果你的漫劇有大量靜態(tài)幀、少動(dòng)態(tài)場(chǎng)景CRF 20 足夠如果是打斗、爆炸特效比較多的短劇可以降到 18。另外x264 有一個(gè)專門(mén)針對(duì)動(dòng)畫(huà)內(nèi)容的參數(shù)叫-tune animation。這個(gè)參數(shù)會(huì)調(diào)整 psychovisual 優(yōu)化策略對(duì)平坦色塊區(qū)域少花碼率把更多 bit 分配給邊緣和高頻細(xì)節(jié)對(duì)漫劇畫(huà)面非常友好。3.2 多碼率輸出分辨率階梯不能一刀切漫劇的播放端覆蓋手機(jī)、平板、電視、Web我需要輸出至少三檔清晰度清晰度檔位分辨率視頻碼率參考適用場(chǎng)景高清1920x10803000-4000 kbps電視、大屏 Web標(biāo)清1280x7201500-2000 kbps手機(jī) Wi-Fi流暢854x480600-900 kbps移動(dòng)網(wǎng)絡(luò)弱網(wǎng)環(huán)境切片時(shí)三檔共用同一個(gè)時(shí)間軸這樣播放器可以在同一時(shí)刻無(wú)縫切換清晰度不需要重新加載。這里要注意的是不要把所有檔位都用原始畫(huà)幅直接壓短劇多數(shù)是豎屏 9:16 素材但也有橫屏內(nèi)容轉(zhuǎn)碼前最好先用ffprobe探測(cè)原始分辨率再?zèng)Q定是否做裁剪或加黑邊。3.3 字幕燒錄與多語(yǔ)言軌道的取舍漫劇的字幕處理有兩種思路硬字幕和軟字幕。硬字幕就是把字幕直接燒進(jìn)畫(huà)面優(yōu)點(diǎn)是全端通用、樣式統(tǒng)一缺點(diǎn)是后期想改字幕必須重新轉(zhuǎn)碼。軟字幕需要播放器支持而且不同平臺(tái)的解析能力差異很大。我的做法是默認(rèn)硬字幕中文字幕在轉(zhuǎn)碼時(shí)通過(guò)-vf subtitlessubtitle.ass燒錄進(jìn)去因?yàn)榇蟛糠致∮脩艟褪莵?lái)看中文字幕的。多語(yǔ)言支持則是額外抽一條軌或者做成音軌選擇。用 Java 拼字幕濾鏡時(shí)有個(gè)坑濾鏡內(nèi)部路徑中的冒號(hào)和逗號(hào)需要轉(zhuǎn)義Windows 路徑尤其麻煩。我建議先把字幕文件復(fù)制到工作目錄用相對(duì)路徑配合在參數(shù)中加上filename*xxx的方式規(guī)避轉(zhuǎn)義問(wèn)題。3.4 用 H.265 還是 H.264當(dāng)前階段我仍以 H.264 為主。原因很現(xiàn)實(shí)H.265 雖然能在同樣畫(huà)質(zhì)下省 30% 到 50% 碼率但瀏覽器兼容性和老設(shè)備硬解支持還是參差不齊尤其是 Web 端和低端 Android 機(jī)容易出現(xiàn)花屏或無(wú)法播放。短劇內(nèi)容的時(shí)長(zhǎng)本來(lái)就不長(zhǎng)碼率省下來(lái)的流量成本不如直接談 CDN 價(jià)格來(lái)得實(shí)際。如果以后用戶量大、存儲(chǔ)和帶寬成本占比明顯上升再考慮對(duì)老片源做 H.265 二壓。這是成本驅(qū)動(dòng)的選擇技術(shù)上隨時(shí)可以切。4. 切片與分發(fā)HLS 切片不是簡(jiǎn)單加幾個(gè)參數(shù)的事4.1 一次完整的 HLS 切片命令漫劇系統(tǒng)我選用 HLS 協(xié)議來(lái)做播放因?yàn)樗烊恢С侄啻a率自適應(yīng)、易于 CDN 邊緣緩存、天然支持訪問(wèn)控制Java 后端只需要生成好 m3u8 文件和 ts 分片即可。核心命令如下ffmpeg -y -i input.mp4 \ -vf scale1280:720:force_original_aspect_ratiodecrease,pad1280:720:(ow-iw)/2:(oh-ih)/2 \ -c:v libx264 -preset veryfast -crf 20 -tune animation \ -c:a aac -b:a 128k -ac 2 \ -pix_fmt yuv420p \ -force_key_frames expr:gte(t,n_forced*4) \ -hls_time 4 \ -hls_list_size 0 \ -hls_playlist_type vod \ -hls_segment_filename output/720p_%04d.ts \ output/720p.m3u8-force_key_frames這個(gè)參數(shù)是我強(qiáng)烈建議加的它保證每 4 秒一個(gè)關(guān)鍵幀這樣切片點(diǎn)對(duì)齊后多碼率切換時(shí)幀同步更干凈用戶拖動(dòng)進(jìn)度條也能更快定位。如果不加這個(gè)參數(shù)切片點(diǎn)會(huì)由編碼器自動(dòng)決定很容易出現(xiàn)首幀不是關(guān)鍵幀的切片播放器兼容性會(huì)下降。4.2 切片產(chǎn)物如何命名和存儲(chǔ)切片文件名絕不能是原始視頻名更不能是中文。我全部用任務(wù) ID 做前綴比如task_20250115_001_720p_0001.ts每個(gè)轉(zhuǎn)碼任務(wù)有獨(dú)立的輸出目錄結(jié)構(gòu)。這樣做有兩個(gè)好處一是命名穩(wěn)定CDN 緩存 key 命中率高二是排查問(wèn)題的時(shí)候能直接從文件名定位到是哪一次轉(zhuǎn)碼任務(wù)。存儲(chǔ)路徑我習(xí)慣按這個(gè)規(guī)則組織/vod/{taskId}/master.m3u8 /vod/{taskId}/1080p/index.m3u8 /vod/{taskId}/1080p/segment_0001.tsmaster.m3u8 是總播放列表里面通過(guò)相對(duì)路徑引用各清晰度的 index.m3u8這樣切換清晰度不用重新拼絕對(duì)地址播放器兼容性也好。4.3 防盜鏈與 CDN 簽名漫劇內(nèi)容版權(quán)意識(shí)強(qiáng)防盜鏈必須做。我推薦的是“時(shí)間戳簽名 防盜鏈 Key”的方式Java 后端給前端下發(fā)播放地址時(shí)在 URL 上拼接expires和sign參數(shù)CDN 邊緣節(jié)點(diǎn)校驗(yàn)通過(guò)才回源。這里有個(gè)容易忽略的細(xì)節(jié)簽名用文件的相對(duì)路徑計(jì)算而不是完整 URL否則換域名或者加參數(shù)順序變了簽名就會(huì)失效。HLS 切片場(chǎng)景下播放器會(huì)頻繁請(qǐng)求 ts 文件如果每個(gè) ts 都要單獨(dú)簽名URL 會(huì)非常長(zhǎng)而且部分播放器對(duì)帶 query 參數(shù)的 ts 切片支持不好。更穩(wěn)的方案是m3u8 用短時(shí)簽名ts 分片通過(guò) CDN 的 Referer 黑白名單保護(hù)或者用固定的私有前綴加不透?jìng)麒b權(quán)。5. 任務(wù)調(diào)度與重試機(jī)制讓視頻處理不再阻塞主流程5.1 為什么轉(zhuǎn)碼任務(wù)必須異步化我曾經(jīng)見(jiàn)過(guò)有團(tuán)隊(duì)在用戶上傳視頻的 HTTP 請(qǐng)求里直接同步轉(zhuǎn)碼結(jié)果轉(zhuǎn)碼 3 分鐘連接超時(shí)前端只能干等。視頻處理這類任務(wù)耗時(shí)不可控千萬(wàn)不能和請(qǐng)求線程綁定。漫劇系統(tǒng)里每次可能有幾十集批量上傳同步轉(zhuǎn)碼會(huì)把數(shù)據(jù)庫(kù)連接池、線程池全部拖垮。合理做法是上傳接口只負(fù)責(zé)存文件、寫(xiě)任務(wù)記錄、發(fā)消息然后立即返回“上傳成功處理中”的狀態(tài)。后端異步消費(fèi)任務(wù)把轉(zhuǎn)碼進(jìn)度反饋到 Redis前端通過(guò)輪詢或推送獲取進(jìn)度。5.2 基于消息隊(duì)列的任務(wù)狀態(tài)機(jī)我用的消息隊(duì)列是 RabbitMQ因?yàn)椴渴鸷?jiǎn)單、生態(tài)成熟配合死信隊(duì)列做重試非常順手。任務(wù)狀態(tài)用一個(gè)枚舉維護(hù)public enum VideoTaskState { PENDING(0, 等待處理), PROCESSING(1, 轉(zhuǎn)碼中), SUCCESS(2, 成功), FAILED(3, 失敗), RETRYING(4, 重試中); }消費(fèi)邏輯大致是收到消息后先把任務(wù)狀態(tài)從 PENDING 改成 PROCESSING然后執(zhí)行轉(zhuǎn)碼轉(zhuǎn)碼成功后更新產(chǎn)物地址最后返回 ACK。如果轉(zhuǎn)碼失敗不直接標(biāo)記最終失敗而是先做有限次重試。我在 RabbitMQ 里配置了重試隊(duì)列和死信隊(duì)列第一次失敗延遲 30 秒重試第二次失敗延遲 5 分鐘重試第三次失敗進(jìn)入死信隊(duì)列人工介入重試次數(shù)太多會(huì)掩蓋真實(shí)問(wèn)題太少又會(huì)因?yàn)榕及l(fā)網(wǎng)絡(luò)抖動(dòng)導(dǎo)致任務(wù)失敗率偏高3 次是一個(gè)比較平衡的閾值。5.3 并發(fā)控制不能只靠隊(duì)列堆積消息隊(duì)列只解決任務(wù)分發(fā)不解決 FFmpeg 進(jìn)程對(duì)機(jī)器資源的爭(zhēng)搶。一臺(tái)轉(zhuǎn)碼機(jī)同時(shí)拉起 8 個(gè) FFmpeg 進(jìn)程CPU 跑滿、內(nèi)存吃緊、磁盤(pán) IO 爆炸每一個(gè)任務(wù)的轉(zhuǎn)碼時(shí)間都會(huì)成倍拉長(zhǎng)整體吞吐反而下降。我在轉(zhuǎn)碼機(jī)上用信號(hào)量控制并發(fā)數(shù)建議按物理核心數(shù)的一半設(shè)置。8 核機(jī)器并發(fā)轉(zhuǎn)碼數(shù)控制在 4 個(gè)左右16 核機(jī)器控制在 8 個(gè)左右。這樣 CPU 利用率能穩(wěn)定在 80% 左右每個(gè)任務(wù)的轉(zhuǎn)碼耗時(shí)可預(yù)測(cè)任務(wù)隊(duì)列也不會(huì)積壓得太離譜。Java 里的實(shí)現(xiàn)很直接private final Semaphore semaphore new Semaphore(4); public void processTask(VideoTask task) { try { semaphore.acquire(); doTranscode(task); } finally { semaphore.release(); } }如果機(jī)器還承載了 Web 服務(wù)并發(fā)數(shù)再往下調(diào)一檔不要讓 FFmpeg 把 CPU 吃光后連健康檢查的請(qǐng)求都響應(yīng)不了。5.4 冪等與防重消息重復(fù)消費(fèi)要如何處理RabbitMQ 在極端場(chǎng)景下會(huì)有消息重復(fù)投遞比如消費(fèi)者處理完成后還沒(méi)來(lái)得及 ACK 就宕機(jī)了消息會(huì)被重新投遞。如果不做冪等同一個(gè)任務(wù)會(huì)被重復(fù)轉(zhuǎn)碼兩次浪費(fèi)資源不說(shuō)還會(huì)把產(chǎn)物文件覆蓋成相同內(nèi)容雖然最終結(jié)果沒(méi)大問(wèn)題但中間狀態(tài)會(huì)干擾監(jiān)控?cái)?shù)據(jù)。我的做法是在數(shù)據(jù)庫(kù)表上對(duì)task_id建唯一索引消費(fèi)消息前先嘗試插入一條處理記錄如果唯一索引沖突說(shuō)明任務(wù)已經(jīng)被處理過(guò)直接 ACK 丟棄即可。同時(shí)轉(zhuǎn)碼產(chǎn)物采用“先寫(xiě)臨時(shí)目錄成功后原子改名”的提交方式保證任務(wù)狀態(tài)和產(chǎn)物文件的一致性。6. 線上環(huán)境必須重視的幾個(gè)隱形坑6.1 僵尸 FFmpeg 進(jìn)程是怎么產(chǎn)生的Java 進(jìn)程拉起 FFmpeg 后如果 JVM 發(fā)生 OOM 或者被強(qiáng)制 KillFFmpeg 子進(jìn)程會(huì)變成孤兒進(jìn)程繼續(xù)跑。它可能占著幾個(gè) G 內(nèi)存但已經(jīng)沒(méi)有任何人在管理等它結(jié)束了。時(shí)間一長(zhǎng)機(jī)器上會(huì)積壓一堆僵尸轉(zhuǎn)碼進(jìn)程資源被白白消耗。我現(xiàn)在的做法是雙保險(xiǎn)啟動(dòng) FFmpeg 時(shí)用獨(dú)立的進(jìn)程組JVM 退出時(shí)通過(guò) shutdown hook 去銷毀同時(shí)寫(xiě)一個(gè)定時(shí)任務(wù)掃描超過(guò)任務(wù)超時(shí)時(shí)間仍然存活的 FFmpeg 進(jìn)程直接 Kill 掉。這個(gè)掃描腳本不用寫(xiě)復(fù)雜邏輯匹配命令行里包含對(duì)應(yīng)任務(wù) ID 的進(jìn)程即可。6.2 轉(zhuǎn)碼速度預(yù)估一集短劇到底要等多久轉(zhuǎn)碼時(shí)長(zhǎng)取決于原始分辨率、編碼器、CPU 性能。拿一個(gè) 2 分鐘 1080P 的漫劇素材來(lái)算用 x264 的veryfast預(yù)設(shè)、8 核 CPU轉(zhuǎn)成 720P HLS 切片實(shí)測(cè)大概需要 15 到 25 秒。如果三檔清晰度都?jí)嚎偤臅r(shí)大概在 40 到 60 秒。這個(gè)量級(jí)決定了異步任務(wù)和進(jìn)度反饋是剛需也決定了并發(fā)控制的上限。如果要優(yōu)化耗時(shí)優(yōu)先考慮上 NVIDIA NVENC 硬件編碼。-c:v h264_nvenc的編碼速度比 x264 快 5 到 10 倍畫(huà)質(zhì)在低碼率下不如 x264但對(duì)短劇內(nèi)容來(lái)說(shuō)NVENC 的默認(rèn)配置已經(jīng)夠用。加了 GPU 之后一臺(tái)帶 Tesla T4 或同等性能卡的機(jī)器能扛幾十路并發(fā)轉(zhuǎn)碼。6.3 磁盤(pán) IO 和臨時(shí)目錄的清理轉(zhuǎn)碼過(guò)程中中間文件的讀寫(xiě)非常頻繁尤其是一邊讀原文件、一邊寫(xiě)多個(gè)清晰度的切片的場(chǎng)景對(duì)磁盤(pán) IO 壓力很大。不要貪便宜用機(jī)械盤(pán)或共享存儲(chǔ)做轉(zhuǎn)碼工作目錄本地 SSD 是底線。我通常給每個(gè)任務(wù)建獨(dú)立臨時(shí)目錄任務(wù)結(jié)束后無(wú)論成功失敗都遞歸刪除避免臨時(shí)文件把磁盤(pán)塞滿。一個(gè)容易忽略的點(diǎn)是對(duì)象存儲(chǔ)回源拉流的臨時(shí)文件也會(huì)占磁盤(pán)所以要為工作目錄單獨(dú)掛盤(pán)并設(shè)置容量告警。有一次我線上磁盤(pán)告警排查發(fā)現(xiàn)是某個(gè)異常任務(wù)反復(fù)重試每次拉取原文件到本地都沒(méi)清理最后把盤(pán)寫(xiě)滿了。6.4 日志規(guī)范出了故障能快速定位到具體環(huán)節(jié)視頻處理鏈路涉及上傳、消息、轉(zhuǎn)碼、上傳對(duì)象存儲(chǔ)、更新數(shù)據(jù)庫(kù)、CDN 預(yù)熱等多個(gè)環(huán)節(jié)必須把日志打全。我在每個(gè)任務(wù)的處理過(guò)程中會(huì)把 taskId、當(dāng)前步驟、耗時(shí)、退出碼都打在一個(gè)統(tǒng)一的日志結(jié)構(gòu)里比如taskId20250115_001 steptranscode encodelibx264 crf20 duration18342ms exit0 taskId20250115_001 stepupload oss cost321ms object/vod/20250115_001/master.m3u8這樣線上定位問(wèn)題的時(shí)候直接按 taskId 一篩整條鏈路的耗時(shí)和結(jié)果一目了然。不要小看這個(gè)習(xí)慣漫劇系統(tǒng)一次批量更新幾十集出問(wèn)題的時(shí)候逐個(gè)翻裸日志真的會(huì)瘋掉。視頻處理不是核心業(yè)務(wù)中最“性感”的部分但它直接決定了用戶的播放體驗(yàn)和平臺(tái)的帶寬成本。Java 后端能做的是把這條鏈路管得足夠穩(wěn)讓 FFmpeg 這種強(qiáng)大的底層工具在最合適的位置發(fā)揮力量。參數(shù)可以慢慢調(diào)架構(gòu)必須一開(kāi)始就扛住沖擊。