javaCV開發詳解之8:轉封裝在rtsp轉rtmp流中的應用(無須轉碼,更低的資源消耗)

  • 2019 年 11 月 1 日
  • 筆記

版權聲明:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://blog.csdn.net/eguid_1/article/details/83025621

補充:解決javaCV的FFmpegFrameRecorder中dts為空導致播放器過快解碼進而導致畫面時快時慢等影響視頻正常解碼播放的問題,目前解決辦法如下:

注意:本代碼已提交給javacv,目前1.4.4-snapshot版本已修復該問題

修改 FFmpegFrameRecorder中的recordPacket(AVPacket pkt) 方法 (1)注釋掉pkt.dts(AV_NOPTS_VALUE); (2)在視頻幀writePacket之前增加: pkt.dts(av_rescale_q_rnd(pkt.dts(), in_stream.time_base(), video_st.time_base(),(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX))); (3)在音視幀writePacket之前增加: pkt.dts(av_rescale_q_rnd(pkt.dts(), in_stream.time_base(), audio_st.time_base(),(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)));

javaCV開發詳解之4:轉流器實現中我們使用了Grabber和Recorder的garbFrame和recordFrame實現轉流,但是這種方式消耗很大,通過javacv源碼發現garbFrame實際上進行decode操作(也就是把h264編碼數據解碼為yuv數據並保存到Frame對象中,然後在recordFrame中把Frame中的yuv圖像像素數據又通過encode為h264編碼數據,音頻部分則是在garbFrame時先解碼成pcm數據,然後在garbFrame中編碼為aac),這兩部分的編解碼操作非常耗資源,顯然會影響到轉流的整體效率。

既然rtsp和rtmp本身都支持h264視頻編碼,那麼視頻編碼這塊完全可以跳過視頻編解碼的步驟,音頻如果也都是aac編碼那當然更好,這樣我們可以避免很多不必要的編解碼操作。

什麼是轉封裝?為什麼轉封裝比轉碼消耗更少?為什麼轉封裝無法改動視頻尺寸? 先舉個栗子:假設視頻格式(mp4,flv,avi等)是盒子,裏面的視頻編碼數據(h264,hevc)是蘋果,我們把這個蘋果從盒子里取出來放到另一個盒子里,盒子是變了,蘋果是沒有變動的,因此視頻相關的尺寸數據是沒有改動的,這個就是轉封裝的概念。 有了上面這個例子,我們可以把「轉碼」理解為:把這個盒子里的蘋果(hevc)拿出來削皮切塊後再加工成櫻桃(h264)後再裝到另一個盒子里,多了一步對蘋果(hevc)轉換為櫻桃(h264)的操作,自然比直接把蘋果拿到另一個盒子(轉封裝)要消耗更多機器性能。

假設rtsp的視頻編碼和音頻編碼是h264和aac,那麼我們只需要一步轉封裝即可完成轉流,代碼參考如下:

 import static org.bytedeco.javacpp.avcodec.*;   import static org.bytedeco.javacpp.avformat.*;   import java.io.IOException;   import org.bytedeco.javacv.FFmpegFrameGrabber;   import org.bytedeco.javacv.FrameGrabber.Exception;   /**    * rtsp轉rtmp(轉封裝方式)    * @author eguid    */   public class ConvertVideoPakcet {       FFmpegFrameGrabber grabber = null;       FFmpegFrameRecorderPlus record = null;       int width = -1, height = -1;       // 視頻參數       protected int audiocodecid;       protected int codecid;       protected double framerate;// 幀率       protected int bitrate;// 比特率       // 音頻參數       // 想要錄製音頻,這三個參數必須有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0       private int audioChannels;       private int audioBitrate;       private int sampleRate;       /**        * 選擇視頻源        * @param src        * @author eguid        * @throws Exception        */       public ConvertVideoPakcet from(String src) throws Exception {           // 採集/抓取器           grabber = new FFmpegFrameGrabber(src);           if(src.indexOf("rtsp")>=0) {               grabber.setOption("rtsp_transport","tcp");           }           grabber.start();// 開始之後ffmpeg會採集視頻信息,之後就可以獲取音視頻信息           if (width < 0 || height < 0) {               width = grabber.getImageWidth();               height = grabber.getImageHeight();           }           // 視頻參數           audiocodecid = grabber.getAudioCodec();           System.err.println("音頻編碼:" + audiocodecid);           codecid = grabber.getVideoCodec();           framerate = grabber.getVideoFrameRate();// 幀率           bitrate = grabber.getVideoBitrate();// 比特率           // 音頻參數           // 想要錄製音頻,這三個參數必須有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0           audioChannels = grabber.getAudioChannels();           audioBitrate = grabber.getAudioBitrate();           if (audioBitrate < 1) {               audioBitrate = 128 * 1000;// 默認音頻比特率           }           return this;       }       /**        * 選擇輸出        * @param out        * @author eguid        * @throws IOException        */       public ConvertVideoPakcet to(String out) throws IOException {           // 錄製/推流器           record = new FFmpegFrameRecorderPlus(out, width, height);           record.setVideoOption("crf", "18");           record.setGopSize(2);           record.setFrameRate(framerate);           record.setVideoBitrate(bitrate);           record.setAudioChannels(audioChannels);           record.setAudioBitrate(audioBitrate);           record.setSampleRate(sampleRate);           AVFormatContext fc = null;           if (out.indexOf("rtmp") >= 0 || out.indexOf("flv") > 0) {               // 封裝格式flv               record.setFormat("flv");               record.setAudioCodecName("aac");               record.setVideoCodec(codecid);               fc = grabber.getFormatContext();           }           record.start(fc);           return this;       }         /**        * 轉封裝        * @author eguid        * @throws IOException        */       public ConvertVideoPakcet go() throws IOException {           long err_index = 0;//採集或推流導致的錯誤次數           //連續五次沒有採集到幀則認為視頻採集結束,程序錯誤次數超過1次即中斷程序           for(int no_frame_index=0;no_frame_index<5||err_index>1;) {               AVPacket pkt=null;               try {                   //沒有解碼的音視頻幀                   pkt=grabber.grabPacket();                   if(pkt==null||pkt.size()<=0||pkt.data()==null) {                       //空包記錄次數跳過                       no_frame_index++;                       continue;                   }                   //不需要編碼直接把音視頻幀推出去                   err_index+=(record.recordPacket(pkt)?0:1);//如果失敗err_index自增1                   av_free_packet(pkt);               }catch (Exception e) {//推流失敗                   err_index++;               } catch (IOException e) {                   err_index++;               }           }           return this;       }       public static void main(String[] args) throws Exception, IOException {   //運行,設置視頻源和推流地址           new ConvertVideoPakcet().from("rtsp://184.72.239.149/vod/mp4://BigBuckBunny_175k.mov")           .to("rtmp://eguid.cc:1935/rtmp/eguid")           .go();       }   }

資源佔用降低了十倍都不止,性能提升還是不錯的,延遲也降低很多。