現(xiàn)RTSP流Web播放:轉(zhuǎn)碼推流到HTTP-FLV的完整實(shí)踐)
前陣子接了個(gè)監(jiān)控平臺(tái)的項(xiàng)目需求本身一句話就能講完管理后臺(tái)里要能點(diǎn)開攝像頭實(shí)時(shí)看到車間畫面。攝像頭是現(xiàn)成的???、大華RTSP 地址也能從設(shè)備管理頁面拿到可真正卡住的地方在于——Web 前端怎么把 RTSP 流播放出來。這個(gè)問題我估摸做過安防、物聯(lián)網(wǎng)、線上巡檢的同學(xué)都遇到過。瀏覽器不認(rèn) RTSP 協(xié)議直接把rtsp://admin:xxx192.168.1.64:554/...甩給video標(biāo)簽肯定不通。大多數(shù)人第一反應(yīng)是“用前端插件”或者“讓瀏覽器裝客戶端”但正經(jīng)做項(xiàng)目前端面對(duì)的是一堆不能裝插件的員工電腦和手機(jī)瀏覽器只能在服務(wù)端想辦法。這篇我把 Java 側(cè)接收 RTSP 流、解碼轉(zhuǎn)碼、再推給前端實(shí)時(shí)播放的完整鏈路拆開講一遍包含我從選型、編碼、聯(lián)調(diào)到排坑的實(shí)操記錄適合正在做類似功能的后端、全棧和流媒體入門開發(fā)者參考。1. 從 RTSP 到 Web 播放中間到底少了哪幾環(huán)先別急著寫 Java 代碼得先搞清楚為什么瀏覽器不能直接播 RTSP。RTSPReal Time Streaming Protocol本身是一個(gè)會(huì)話控制協(xié)議負(fù)責(zé)發(fā)起、暫停、結(jié)束流媒體會(huì)話實(shí)際音視頻數(shù)據(jù)靠 RTP 包傳輸另外還有 RTCP 做質(zhì)量控制。這一套組合拳在網(wǎng)絡(luò)攝像頭領(lǐng)域非常成熟但它和 Web 生態(tài)完全是兩個(gè)世界。打個(gè)比方RTSP 像是餐廳里的“點(diǎn)菜流程現(xiàn)炒現(xiàn)送”服務(wù)員問你吃什么、后廚炒完直接端到你面前。這個(gè)過程靈活但需要維持一整條獨(dú)立的溝通通道。瀏覽器里的video標(biāo)簽則更像是“便利店自取”只想通過 HTTP 地址從貨架上拿標(biāo)好格式的商品根本不關(guān)心你后廚怎么運(yùn)作。所以要讓攝像頭畫面進(jìn) Web 頁面服務(wù)端必須充當(dāng)一個(gè)“外賣中轉(zhuǎn)站”把 RTSP 的會(huì)話控制、RTP 傳輸轉(zhuǎn)換為瀏覽器認(rèn)識(shí)的 HTTP 流。除了協(xié)議差異還有編碼層面的問題。絕大多數(shù)網(wǎng)絡(luò)攝像頭輸出 H.264這個(gè)瀏覽器能解但很多新款設(shè)備默認(rèn)或可選輸出 H.265HEVC問題就來了Chrome 長期以來不原生支持 H.265 硬解只在部分系統(tǒng)和硬件環(huán)境下能軟解或通過系統(tǒng)解碼器間接播放Safari 對(duì) H.265 的支持也不統(tǒng)一。也就是說哪怕協(xié)議打通了編碼不合適依然白搭。這里有兩種處理思路一種是不動(dòng)編碼只把 H.264 裸流重新封裝成適合 Web 播放的容器格式這叫“轉(zhuǎn)封裝”transmux另一種是正兒八經(jīng)解碼后再重新編碼成 H.264這叫“轉(zhuǎn)碼”transcode。轉(zhuǎn)封裝性能開銷低但要求源流本來就是 H.264轉(zhuǎn)碼靈活任何 H.265 甚至 H.264 都能統(tǒng)一輸出成 H.264代價(jià)是吃 CPU/GPU。題目標(biāo)題里帶著“解碼”兩個(gè)字我理解就是包含了轉(zhuǎn)碼這個(gè)動(dòng)作后面我也著重講這條路。再有一個(gè)維度是延遲。監(jiān)控場(chǎng)景講究實(shí)時(shí)你對(duì)著畫面喊話或者盯著設(shè)備動(dòng)作延遲 3 秒還能忍延遲 10 秒基本沒法用。常見的分發(fā)協(xié)議里HLS兼容性最好大部分手機(jī)瀏覽器直接支持Safari 也能播但切片機(jī)制決定了延遲普遍在 3-15 秒HTTP-FLV延遲可以控制在 1-3 秒PC 端有 flv.js 這類 MSE 封裝庫支持移動(dòng)端兼容性差一些WebRTC延遲最低能到幾百毫秒但服務(wù)端接入復(fù)雜度高需要另外做信令服務(wù)和媒體協(xié)商。所以“先選傳輸再寫代碼”是最重要的。你要是沒想清楚前端到底跑在 PC 還是手機(jī)、延遲要求多高、并發(fā)幾路上來就寫 JavaCV 拉流后面很可能推倒重來。2. Java 拉流轉(zhuǎn)推的技術(shù)選型別急著寫代碼我見過不少人一拿到需求就直奔代碼用 JavaCV 把 RTSP 流接進(jìn)來然后塞進(jìn) WebSocket 里往瀏覽器推。這個(gè)方案在單路、內(nèi)網(wǎng)、實(shí)驗(yàn)性項(xiàng)目里能跑通但稍微上點(diǎn)規(guī)模就麻煩。選型階段多花半小時(shí)后面能省幾天。先把 Java 生態(tài)里常見的四條路線攤開看方案實(shí)現(xiàn)要點(diǎn)延遲優(yōu)點(diǎn)缺點(diǎn)JavaCV 流媒體服務(wù)器JavaCV 拉流轉(zhuǎn)碼推 RTMP/RTSP 給 SRS 或 MediaMTX前端從服務(wù)器取 HTTP-FLV / HLS / WebRTC1-3 秒分層清晰穩(wěn)定支持并發(fā)擴(kuò)展多部署一個(gè)流媒體服務(wù)JavaCV 自建 HTTP-FLV 輸出自己用 Netty/Tomcat 寫一個(gè)簡易分發(fā)服務(wù)1-3 秒減少外部依賴要處理大量連接細(xì)節(jié)運(yùn)維和排錯(cuò)成本高純 FFmpeg 命令行進(jìn)程Java 里ProcessBuilder調(diào) ffmpeg1-3 秒靈活不用寫 Java 編解碼進(jìn)程管理麻煩回傳狀態(tài)、停止重啟都不優(yōu)雅只上流媒體服務(wù)器SRS/MediaMTX 直接拉攝像頭 RTSP 并分發(fā)1-3 秒部署最簡單脫離了 Java 業(yè)務(wù)邏輯權(quán)限控制、動(dòng)態(tài)管理不方便我自己最終選了第一種也就是 JavaCV 負(fù)責(zé)“接入轉(zhuǎn)碼”SRS或者輕量一點(diǎn)的 MediaMTX負(fù)責(zé)“分發(fā)”。理由很現(xiàn)實(shí)Java 端的強(qiáng)項(xiàng)是業(yè)務(wù)集成、攝像頭上線下線管理、權(quán)限控制、和現(xiàn)有后臺(tái)系統(tǒng)對(duì)接而真正的流分發(fā)比如 HTTP-FLV 的 chunked 響應(yīng)、WebRTC 的 ICE/DTLS 協(xié)商、HLS 切片緩存SRS 這類成熟服務(wù)器已經(jīng)處理得非常好沒有必要自己在 Java 里重造輪子。那么問題來了什么時(shí)候才需要 Java 參與如果只是臨時(shí)看一路畫面你甚至可以不用寫代碼直接命令行跑/usr/local/srs/etc/...那樣的配置就能拉流。但如果你的系統(tǒng)里攝像頭有幾十路、幾百路需要?jiǎng)討B(tài)從數(shù)據(jù)庫讀取攝像頭列表、按用戶權(quán)限點(diǎn)播、隨時(shí)控制開啟和關(guān)閉這時(shí)候 Java 服務(wù)就是必不可少的“管家”。它負(fù)責(zé)按照業(yè)務(wù)規(guī)則拉起和停止一路一路的轉(zhuǎn)推任務(wù)而分發(fā)這件事繼續(xù)交給專業(yè)流服務(wù)器。還有一點(diǎn)我覺得值得提醒JavaCV 的依賴體積很大javacv-platform會(huì)帶入 OpenCV、FFmpeg、OpenBLAS 等一堆平臺(tái)動(dòng)態(tài)庫。如果你的服務(wù)器在內(nèi)網(wǎng)Maven 中央倉庫不一定能順利拉這些大包建議只引入真正需要的模塊dependency groupIdorg.bytedeco/groupId artifactIdjavacv/artifactId version1.5.9/version /dependency dependency groupIdorg.bytedeco/groupId artifactIdffmpeg-platform/artifactId version6.1.1-1.5.9/version /dependency這樣只帶 FFmpeg不拖 OpenCV 那些無關(guān)包部署包能小一大截。版本之間要注意對(duì)應(yīng)關(guān)系javacv和ffmpeg-platform的版本號(hào)后綴經(jīng)常一致比如1.5.9對(duì)應(yīng)6.1.1-1.5.9。3. JavaCV 上場(chǎng)拉流、解碼、再推流的完整鏈路JavaCV 本質(zhì)上是 JavaCPP 對(duì) FFmpeg 的封裝所以你幾乎可以把 FFmpeg 的命令行參數(shù)思維平移到代碼里。下面是我在一路攝像頭從“拉取”到“推送”過程中會(huì)涉及的核心代碼。3.1 拉流端配置參數(shù)比代碼更重要初始化一個(gè)FFmpegFrameGrabber看似簡單真正決定穩(wěn)不穩(wěn)定的是幾個(gè)隱藏參數(shù)。FFmpegFrameGrabber grabber new FFmpegFrameGrabber(rtspUrl); // 用 TCP 而不是 UDP 拉流。UDP 在內(nèi)網(wǎng)偶爾丟包畫面會(huì)花屏TCP 犧牲一點(diǎn)延遲換來穩(wěn)定。 grabber.setOption(rtsp_transport, tcp); // 設(shè)置套接字超時(shí)單位是微秒這里設(shè) 5 秒。攝像頭斷網(wǎng)后拉流線程不至于一直掛死。 grabber.setOption(stimeout, 5000000); // 減少緩沖區(qū)對(duì)實(shí)時(shí)性有幫助。 grabber.setOption(fflags, nobuffer); // 遇到損壞的幀嘗試跳過而不是直接中斷。 grabber.setOption(err_detect, ignore_err); grabber.start();stimeout這個(gè)參數(shù)我強(qiáng)烈建議設(shè)置。攝像頭或者交換機(jī)電一斷如果沒有超時(shí)grabber.start()或grabber.grab()會(huì)阻塞很久你的“重連機(jī)制”就沒法及時(shí)觸發(fā)。別問我怎么知道連續(xù)燒掉幾個(gè)重連線程之后我就把它列成默認(rèn)配置了。3.2 轉(zhuǎn)碼與推流配置盡量向低延遲編碼參數(shù)靠攏接下來是FFmpegFrameRecorder。這一步的目標(biāo)是把攝像頭原始流解碼后重新編碼成 H.264/AAC封裝成 FLV然后推給上游流媒體服務(wù)器。FFmpegFrameRecorder recorder new FFmpegFrameRecorder(pushUrl, width, height, 1); recorder.setFormat(flv); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); recorder.setVideoOption(preset, veryfast); recorder.setVideoOption(tune, zerolatency); recorder.setVideoBitrate(2000 * 1000); recorder.setFrameRate(frameRate); recorder.setGopSize((int) frameRate * 2); if (hasAudio) { recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); recorder.setAudioBitrate(64 * 1000); recorder.setSampleRate(44100); recorder.setAudioChannels(1); } recorder.start();這里幾個(gè)選項(xiàng)要解釋一下presetveryfast犧牲一點(diǎn)壓縮率換取更快的編碼速度對(duì)實(shí)時(shí)轉(zhuǎn)碼來說比文件體積重要得多tunezerolatency要求編碼器不引入額外延遲會(huì)減少 B 幀的使用對(duì)低延遲很關(guān)鍵setGopSizeGOP 是關(guān)鍵幀間隔。如果設(shè)得太小比如 1 秒一個(gè)關(guān)鍵幀帶寬占用高設(shè)得太大比如 100 幀以上前端起播時(shí)可能要等一個(gè)關(guān)鍵幀黑屏?xí)r間會(huì)變長。我一般取幀率×2也就是 2 秒一個(gè)關(guān)鍵幀平衡起播速度和帶寬。pushUrl如果是推給 SRS/MediaMTX一般長這樣rtmp://127.0.0.1:1935/live/cam_001SRS 默認(rèn)監(jiān)聽 RTMP隨后對(duì)外提供 HTTP-FLV地址就是http://127.0.0.1:8080/live/cam_001.flv這塊前置服務(wù)怎么搭不是 Java 代碼的事我會(huì)放到后面第 5 章結(jié)合部署經(jīng)驗(yàn)一起說。3.3 主循環(huán)別把“拉取”和“錄制”寫死成一步核心的循環(huán)其實(shí)很短Frame frame; long lastTime System.currentTimeMillis(); while (running) { try { frame grabber.grab(); // 同時(shí)拉取視頻幀和音頻幀 if (frame ! null) { recorder.record(frame); } } catch (Exception e) { log.error(拉流/推流異常: {}, e.getMessage()); break; // 跳出循環(huán)交給上層重連邏輯 } }這里有個(gè)容易踩的坑grab()會(huì)同時(shí)返回視頻幀和音頻幀而grabImage()只返回視頻幀。如果你的攝像頭帶音頻又不想保存聲音那就別用帶音軌的參數(shù)初始化recorder否則會(huì)出現(xiàn)“av_interleaved_write_frame(): Invalid data”之類的報(bào)錯(cuò)。最穩(wěn)妥的做法是先用ffprobe看看攝像頭實(shí)際有沒有音頻軌道沒有音頻的設(shè)備就老老實(shí)實(shí)讓recorder保持純視頻模式。grab()返回的Frame本質(zhì)上是解碼后的原始數(shù)據(jù)所以recorder.record(frame)再編碼這就是完整的“解碼轉(zhuǎn)碼”過程。如果源流本身就是 H.264 且你不想折騰硬件編碼也可以走更低層的 AVPacket 接口做轉(zhuǎn)封裝但 JavaCV 里這套 API 比較繞一般項(xiàng)目沒必要。3.4 不要重復(fù)創(chuàng)建 grabber一個(gè)攝像頭一個(gè)任務(wù)線程真實(shí)項(xiàng)目中你會(huì)面對(duì)很多路攝像頭。我見過同學(xué)寫“開啟視頻”的接口每次請(qǐng)求都new FFmpegFrameGrabber().start()然后也不存引用過一會(huì)兒連接數(shù)爆炸。正確做法是給每路攝像頭維護(hù)一個(gè)獨(dú)立的“拉流轉(zhuǎn)推任務(wù)”任務(wù)內(nèi)部管理 grabber 和 recorder 的生命周期并提供停止、重啟兩個(gè)方法。我這里給一個(gè)簡單的任務(wù)骨架public class RtspPushTask implements Runnable { private final String rtspUrl; private final String pushUrl; private volatile boolean running true; public void stop() { running false; } Override public void run() { while (running) { FFmpegFrameGrabber grabber null; FFmpegFrameRecorder recorder null; try { grabber createGrabber(); grabber.start(); recorder createRecorder(pushUrl, grabber); recorder.start(); log.info(轉(zhuǎn)推已啟動(dòng): {} - {}, rtspUrl, pushUrl); Frame frame; while (running (frame grabber.grab()) ! null) { recorder.record(frame); } } catch (Exception e) { log.warn(轉(zhuǎn)推中斷: {}, e.getMessage()); } finally { closeQuietly(recorder); closeQuietly(grabber); } if (running !Thread.currentThread().isInterrupted()) { sleepSeconds(3); // 重連等待 } } } }這樣斷線之后任務(wù)會(huì)自動(dòng)重連不會(huì)讓一個(gè)異常就把整個(gè)服務(wù)拖垮。真正的業(yè)務(wù)層只需要維護(hù)一個(gè)MapString, RtspPushTask用戶點(diǎn)播就start()關(guān)閉就stop()。3.5 如果想脫離 SRS純 Java 輸出 HTTP-FLV 的思路有朋友會(huì)問我不想額外部署流媒體服務(wù)器Java 能不能直接把 FLV 推給前端能但要自己處理很多分發(fā)邊界。思路是FFmpegFrameRecorder支持把 FLV 數(shù)據(jù)寫入一個(gè)自定義OutputStream你的 HTTP 服務(wù)器Netty/Tomcat NIO把這個(gè) OutputStream 接到請(qǐng)求通道上flv.js 請(qǐng)求 URL 時(shí)你先把 FLV 文件頭寫出去然后不斷把編碼后的 FLV tag 寫進(jìn)響應(yīng)體保持連接不關(guān)。聽起來不算復(fù)雜實(shí)際做起來要命的是連接管理瀏覽器刷新要清理舊連接網(wǎng)速慢要背壓并發(fā)一多要控制內(nèi)存。我第一版為了圖省事這么搞過一次后來發(fā)現(xiàn)“能播”和“能穩(wěn)定播”之間差了十萬八千里。如果你不是特別明確知道自己在做什么還是讓 SRS/MediaMTX 去干這事Java 這邊做好拉流和轉(zhuǎn)碼就夠了。4. 前端播放接入flv.js 為主、HLS 兜底的落地姿勢(shì)后端推流鏈路通了前端反而簡單但也有一些細(xì)節(jié)直接影響用戶體驗(yàn)。4.1 flv.js 播放 HTTP-FLV先裝包npm install flv.js然后初始化播放器。這里面的參數(shù)優(yōu)化我實(shí)際調(diào)過很多遍import flvjs from flv.js; if (flvjs.isSupported()) { const videoElement document.getElementById(video); const player flvjs.createPlayer({ type: flv, isLive: true, url: http://your-server:8080/live/cam_001.flv, }, { enableStashBuffer: false, // 關(guān)掉緩存顯著降低延遲 isLive: true, lazyLoad: false, autoCleanupSourceBuffer: true, }); player.attachMediaElement(videoElement); player.load(); player.play().catch(console.error); }enableStashBuffer: false是低延遲的關(guān)鍵。默認(rèn)情況下 flv.js 會(huì)緩沖一部分?jǐn)?shù)據(jù)網(wǎng)絡(luò)波動(dòng)時(shí)不那么容易卡但實(shí)時(shí)性會(huì)變差。如果是監(jiān)控類應(yīng)用我會(huì)選擇關(guān)閉讓畫面盡量貼近實(shí)時(shí)如果是直播帶貨那種要極穩(wěn)的反而建議保留緩沖。在 Vue 3 里接入時(shí)要注意播放器實(shí)例的生命周期script setup import flvjs from flv.js; let player null; function playStream(url) { if (player) { player.destroy(); } if (!flvjs.isSupported()) { alert(當(dāng)前瀏覽器不支持 flv.js); return; } player flvjs.createPlayer({ type: flv, isLive: true, url }, { enableStashBuffer: false }); player.attachMediaElement(videoRef.value); player.load(); player.play(); } onBeforeUnmount(() { player player.destroy(); }); /script4.2 移動(dòng)端 H5 的兜底HLSflv.js 的兼容性在 Chrome、Edge、Firefox 上都很穩(wěn)唯獨(dú) iOS Safari 和部分 Android 廠商瀏覽器沒法用 MSE 處理 HTTP-FLV。遇到跨平臺(tái)需求我常規(guī)做法是后端同時(shí)讓 SRS 開啟 HLS 輸出前端用 hls.js 或者原生 HLS 播放。HLS 在 SRS 里幾乎不需要額外配置生成的是這樣的地址http://your-server:8080/live/cam_001.m3u8前端判斷邏輯可以簡單一點(diǎn)PC 上用 flv.js移動(dòng)端優(yōu)先走原生video標(biāo)簽的 HLS 播放能力。Safari 直接支持 HLSAndroid 上用 hls.js 兜底。function createPlayer(url) { const isIOS /iPhone|iPad|Mac/.test(navigator.platform) || (navigator.userAgent.includes(Mac) ontouchend in document); if (isIOS) { const video document.getElementById(video); video.src url.replace(.flv, .m3u8); video.play(); return; } // 否則 flv.js }4.3 必須說的 WebRTC 選項(xiàng)如果你的項(xiàng)目對(duì)延遲要求更高比如遠(yuǎn)程控制、在線對(duì)講HTTP-FLV 1-3 秒仍然不夠那就要考慮 WebRTC。SRS 4.0 以上已經(jīng)支持 WebRTC 分發(fā)可以直接拉 RTSP 轉(zhuǎn) WebRTC前端通過RTCPeerConnection拿流。但 WebRTC 的坑在于信令服務(wù)和客戶端的 ICE 配置不是一段代碼能跑起來的這里不展開。我只建議把 WebRTC 當(dāng)作延遲敏感型功能的進(jìn)階方案能不用就不用畢竟項(xiàng)目交付的穩(wěn)定性優(yōu)先級(jí)大于極致延遲。5. 真實(shí)項(xiàng)目里更值錢的部分兼容性、重連與性能調(diào)優(yōu)代碼能跑起來只是起點(diǎn)。下面這幾類問題是我在每個(gè)攝像頭流項(xiàng)目里幾乎都會(huì)遇到的提前寫在這能少走點(diǎn)彎路。5.1 先驗(yàn)證 RTSP 地址再用 ffprobe 摸清設(shè)備底細(xì)很多同事拿到的“RTSP 地址”是廠商或者施工人員隨手給的格式五花八門。??党R姷母袷绞莚tsp://admin:password192.168.1.64:554/Streaming/Channels/101大華類似的格式是rtsp://admin:password192.168.1.64:554/cam/realmonitor?channel1subtype0其中/Streaming/Channels/101里的101通常是主碼流102是子碼流。大華subtype0也是主碼流subtype1是子碼流。地址不對(duì)后面所有操作都是白費(fèi)所以第一步建議先在服務(wù)器上用 ffprobe 驗(yàn)證ffprobe -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101ffprobe 會(huì)打印出流的編碼、分辨率、音頻參數(shù)。知道了這些你才知道該讓 JavaCV 按什么寬度高度去初始化 recorder。另外注意密碼里如果包含、:、/這些特殊字符一定要做 URL 編碼否則 RTSP 解析器會(huì)斷錯(cuò)位置導(dǎo)致認(rèn)證失敗。5.2 主碼流還是子碼流性能與畫質(zhì)的平衡攝像頭通常有兩路碼流主碼流分辨率高、碼率高適合事后查證子碼流分辨率低、碼率低適合實(shí)時(shí)預(yù)覽。做實(shí)時(shí)播放時(shí)如果設(shè)備數(shù)量不多、服務(wù)器 CPU 充足可以直接取主碼流如果一路 4K 都要在服務(wù)端解碼再轉(zhuǎn) H.264CPU 壓力會(huì)很大我遇到的實(shí)際場(chǎng)景里4K 轉(zhuǎn)碼單路就要吃掉不少 CPU 核。處理辦法很簡單實(shí)時(shí)預(yù)覽默認(rèn)用子碼流關(guān)鍵畫面“回放原始錄像”時(shí)再切主碼流。這在業(yè)務(wù)設(shè)計(jì)上要前置而不是等性能報(bào)警了再改。編碼格式上盡量在攝像頭后臺(tái)把編碼格式設(shè)為 H.264。如果設(shè)備強(qiáng)制輸出 H.265而前端又要 H.264JavaCV 轉(zhuǎn)碼是兜得住但那就是純 CPU 轉(zhuǎn)碼成本很高。有 NVIDIA 顯卡的服務(wù)器可以試試把編碼器切到硬件recorder.setVideoCodecName(h264_nvenc); recorder.setVideoOption(preset, p1);注意setVideoCodecName和setVideoCodec是兩種設(shè)置方式不要混用。硬件編碼能大幅降低 CPU 占用但部署機(jī)器必須帶對(duì)應(yīng)顯卡云服務(wù)器一般要選 GPU 實(shí)例成本自己權(quán)衡。5.3 斷線重連核心循環(huán)的異常處理攝像頭設(shè)備不像應(yīng)用服務(wù)器那么穩(wěn)定斷電、重啟、網(wǎng)絡(luò)波動(dòng)都很常見。如果轉(zhuǎn)推任務(wù)斷開后不重連前端畫面就是一把黑屏用戶只能刷新。前面給的任務(wù)骨架里有一個(gè)while (running)外層循環(huán)配合Thread.sleep(3000)就是斷線重連的基本實(shí)現(xiàn)。另外要小心一種情況攝像頭重啟后分辨率、幀率可能發(fā)生變化舊recorder還在用原來的寬高FFmpeg 會(huì)報(bào)Width or height change之類的錯(cuò)誤。遇到這種情況常規(guī)做法是捕獲異常后把舊的 recorder 關(guān)掉重新從 grabber 讀取新的寬高再創(chuàng)建 recorder。我在代碼里推薦的做法是每次異常退回到外層重連邏輯而不是在循環(huán)里嘗試“修復(fù)”這樣狀態(tài)管理最簡單。5.4 首屏黑屏和延遲調(diào)優(yōu)一個(gè)容易被忽略的源頭首屏打開慢經(jīng)常不是網(wǎng)絡(luò)問題而是 GOP 問題。攝像頭端的關(guān)鍵幀間隔GOP如果設(shè)置是 4 秒那新播放器接入時(shí)必須等到下一個(gè)關(guān)鍵幀才開始有畫面平均黑屏 2 秒。這個(gè)問題有兩個(gè)解法調(diào)小攝像頭的 I 幀間隔比如 2 秒一次在流媒體服務(wù)器上開啟 GOP 緩存。SRS 里 HTTP-FLV 域默認(rèn)會(huì)緩存最近一個(gè) GOP新用戶接入時(shí)直接用緩存的關(guān)鍵幀起播黑屏?xí)r間能降到幾百毫秒。代價(jià)是多占一點(diǎn)內(nèi)存但監(jiān)控場(chǎng)景這點(diǎn)開銷非常值。延遲問題則要前后端配合。前端關(guān)掉 flv.js 的 stash 緩沖后端編碼時(shí)用zerolatency參數(shù)如果中間還經(jīng)過了 SRSSRS 的mrmerged-reduce算法默認(rèn)開啟一般不需要額外動(dòng)。我在多路攝像頭、全鏈路 HTTP-FLV、局域網(wǎng)環(huán)境下實(shí)測(cè)延遲普遍能穩(wěn)定在 1 秒以內(nèi)公網(wǎng)環(huán)境會(huì)到 1-2 秒已經(jīng)足夠大多數(shù)業(yè)務(wù)使用。5.5 部署形態(tài)Java 和流媒體服務(wù)器怎么擺都行但邊界要清晰最后一句話給個(gè)實(shí)在建議別把 Java 服務(wù)里塞滿流媒體分發(fā)邏輯。我的部署習(xí)慣是一臺(tái)服務(wù)器上同時(shí)跑 Java 服務(wù)和 SRSDocker 起Java 只負(fù)責(zé)拉流、轉(zhuǎn)碼、推 RTMP 給 SRSSRS 對(duì)外提供 HTTP-FLV/HLS。這樣排查問題時(shí)有非常清晰的邊界前端放不出畫面先試 SRS 的 HTTP-FLV 地址能不能直接播放能播說明問題在前端不能播就看 SRS 日志再不行看 Java 日志五秒鐘就能定位問題出在哪一段。我自己也是踩過幾次坑才堅(jiān)定這個(gè)分層思路的。第一次做類似系統(tǒng)圖省事把 HTTP-FLV 直接寫在 Java 里結(jié)果同時(shí)在線十幾路時(shí)頻繁出現(xiàn)連接掛起和內(nèi)存上漲天天被運(yùn)維催著看堆棧。后來換成 SRS 做分發(fā)Java 端代碼反而更簡單了穩(wěn)定性還明顯提升。所以說選型階段不用迷信“All in Java”該交給專業(yè)組件的就交出去Java 把業(yè)務(wù)邏輯這塊做好做扎實(shí)這個(gè)項(xiàng)目基本就成了一半。