JavaCV推流實戰(zhàn)(MP4文件)

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內(nèi)容:所有原創(chuàng)文章分類匯總及配套源碼锥累,涉及Java谅阿、Docker盏筐、Kubernetes、DevOPS等赤赊;

歡迎訪問我的GitHub

這里分類和匯總了欣宸的全部原創(chuàng)(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 自己的mp4文件,如何讓更多的人遠(yuǎn)程播放咐鹤?如下圖所示:
在這里插入圖片描述
  • 這里簡單解釋一下上圖的功能:
  1. 部署開源流媒體服務(wù)器<font color="blue">SRS</font>
  2. 開發(fā)名為<font color="blue">PushMp4</font>的java應(yīng)用霸旗,該應(yīng)用會讀取本機(jī)磁盤上的Mp4文件,讀取每一幀秕脓,推送到SRS上
  3. 每個想看視頻的人柒瓣,就在自己電腦上用流媒體播放軟件(例如VLC)連接SRS,播放PushMp4推上來的視頻
  • 今天咱們就來完成上圖中的實戰(zhàn)吠架,整個過程分為以下步驟:
  1. 環(huán)境信息
  2. 準(zhǔn)備MP4文件
  3. 用docker部署SRS
  4. java應(yīng)用開發(fā)和運行
  5. VLC播放

環(huán)境信息

  • 本次實戰(zhàn)芙贫,我這邊涉及的環(huán)境信息如下,供您參考:
  1. 操作系統(tǒng):macOS Monterey
  2. JDK:1.8.0_211
  3. JavaCV:1.5.6
  4. SRS:3

準(zhǔn)備MP4文件

用docker部署SRS

  • SRS是著名的開源的媒體服務(wù)器,推到這里的流怔檩,都可以用媒體播放器在線播放褪秀,為了簡單起見,我在docker環(huán)境下一行命令完成部署:
docker run -p 11935:1935 -p 1985:1985 -p 8080:8080 ossrs/srs:3
  • 此刻SRS服務(wù)正在運行中薛训,可以推流上去了

開發(fā)JavaCV應(yīng)用

  • 接下來進(jìn)入最重要的編碼階段媒吗,新建名為<font color="blue">simple-grab-push</font>的maven工程,pom.xml如下(那個名為<font color="blue">javacv-tutorials</font>的父工程其實沒有什么作用乙埃,我這里只是為了方便管理多個工程的代碼而已闸英,您可以刪除這個父工程節(jié)點):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>javacv-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.bolingcavalry</groupId>
    <version>1.0-SNAPSHOT</version>
    <artifactId>simple-grab-push</artifactId>
    <packaging>jar</packaging>

    <properties>
        <!-- javacpp當(dāng)前版本 -->
        <javacpp.version>1.5.6</javacpp.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.13.3</version>
        </dependency>

        <!-- javacv相關(guān)依賴,一個就夠了 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacpp.version}</version>
        </dependency>
    </dependencies>
</project>
  • 從上述文件可見介袜,JavaCV的依賴只有一個<font color="blue">javacv-platform</font>甫何,挺簡潔

  • 接下來開始編碼,在編碼前遇伞,先把整個流程畫出來辙喂,這樣寫代碼就清晰多了:

在這里插入圖片描述
  • 從上圖可見流程很簡單,這里將所有代碼寫在一個java類中:
package com.bolingcavalry.grabpush;

import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FFmpegLogCallback;
import org.bytedeco.javacv.Frame;

/**
 * @author willzhao
 * @version 1.0
 * @description 讀取指定的mp4文件鸠珠,推送到SRS服務(wù)器
 * @date 2021/11/19 8:49
 */
@Slf4j
public class PushMp4 {
    /**
     * 本地MP4文件的完整路徑(兩分零五秒的視頻)
     */
    private static final String MP4_FILE_PATH = "/Users/zhaoqin/temp/202111/20/sample-mp4-file.mp4";

    /**
     * SRS的推流地址
     */
    private static final String SRS_PUSH_ADDRESS = "rtmp://192.168.50.43:11935/live/livestream";

    /**
     * 讀取指定的mp4文件巍耗,推送到SRS服務(wù)器
     * @param sourceFilePath 視頻文件的絕對路徑
     * @param PUSH_ADDRESS 推流地址
     * @throws Exception
     */
    private static void grabAndPush(String sourceFilePath, String PUSH_ADDRESS) throws Exception {
        // ffmepg日志級別
        avutil.av_log_set_level(avutil.AV_LOG_ERROR);
        FFmpegLogCallback.set();

        // 實例化幀抓取器對象,將文件路徑傳入
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(MP4_FILE_PATH);

        long startTime = System.currentTimeMillis();

        log.info("開始初始化幀抓取器");

        // 初始化幀抓取器渐排,例如數(shù)據(jù)結(jié)構(gòu)(時間戳炬太、編碼器上下文、幀對象等)驯耻,
        // 如果入?yún)⒌扔趖rue亲族,還會調(diào)用avformat_find_stream_info方法獲取流的信息,放入AVFormatContext類型的成員變量oc中
        grabber.start(true);

        log.info("幀抓取器初始化完成可缚,耗時[{}]毫秒", System.currentTimeMillis()-startTime);

        // grabber.start方法中孽水,初始化的解碼器信息存在放在grabber的成員變量oc中
        AVFormatContext avFormatContext = grabber.getFormatContext();

        // 文件內(nèi)有幾個媒體流(一般是視頻流+音頻流)
        int streamNum = avFormatContext.nb_streams();

        // 沒有媒體流就不用繼續(xù)了
        if (streamNum<1) {
            log.error("文件內(nèi)不存在媒體流");
            return;
        }

        // 取得視頻的幀率
        int frameRate = (int)grabber.getVideoFrameRate();

        log.info("視頻幀率[{}],視頻時長[{}]秒城看,媒體流數(shù)量[{}]",
                frameRate,
                avFormatContext.duration()/1000000,
                avFormatContext.nb_streams());

        // 遍歷每一個流女气,檢查其類型
        for (int i=0; i< streamNum; i++) {
            AVStream avStream = avFormatContext.streams(i);
            AVCodecParameters avCodecParameters = avStream.codecpar();
            log.info("流的索引[{}],編碼器類型[{}]测柠,編碼器ID[{}]", i, avCodecParameters.codec_type(), avCodecParameters.codec_id());
        }

        // 視頻寬度
        int frameWidth = grabber.getImageWidth();
        // 視頻高度
        int frameHeight = grabber.getImageHeight();
        // 音頻通道數(shù)量
        int audioChannels = grabber.getAudioChannels();

        log.info("視頻寬度[{}]炼鞠,視頻高度[{}],音頻通道數(shù)[{}]",
                frameWidth,
                frameHeight,
                audioChannels);

        // 實例化FFmpegFrameRecorder轰胁,將SRS的推送地址傳入
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(SRS_PUSH_ADDRESS,
                frameWidth,
                frameHeight,
                audioChannels);

        // 設(shè)置編碼格式
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);

        // 設(shè)置封裝格式
        recorder.setFormat("flv");

        // 一秒內(nèi)的幀數(shù)
        recorder.setFrameRate(frameRate);

        // 兩個關(guān)鍵幀之間的幀數(shù)
        recorder.setGopSize(frameRate);

        // 設(shè)置音頻通道數(shù)谒主,與視頻源的通道數(shù)相等
        recorder.setAudioChannels(grabber.getAudioChannels());

        startTime = System.currentTimeMillis();
        log.info("開始初始化幀抓取器");

        // 初始化幀錄制器,例如數(shù)據(jù)結(jié)構(gòu)(音頻流赃阀、視頻流指針霎肯,編碼器),
        // 調(diào)用av_guess_format方法,確定視頻輸出時的封裝方式观游,
        // 媒體上下文對象的內(nèi)存分配搂捧,
        // 編碼器的各項參數(shù)設(shè)置
        recorder.start();

        log.info("幀錄制初始化完成,耗時[{}]毫秒", System.currentTimeMillis()-startTime);

        Frame frame;

        startTime = System.currentTimeMillis();

        log.info("開始推流");

        long videoTS = 0;

        int videoFrameNum = 0;
        int audioFrameNum = 0;
        int dataFrameNum = 0;

        // 假設(shè)一秒鐘15幀懂缕,那么兩幀間隔就是(1000/15)毫秒
        int interVal = 1000/frameRate;
        // 發(fā)送完一幀后sleep的時間允跑,不能完全等于(1000/frameRate),不然會卡頓搪柑,
        // 要更小一些聋丝,這里取八分之一
        interVal/=8;

        // 持續(xù)從視頻源取幀
        while (null!=(frame=grabber.grab())) {
            videoTS = 1000 * (System.currentTimeMillis() - startTime);

            // 時間戳
            recorder.setTimestamp(videoTS);

            // 有圖像,就把視頻幀加一
            if (null!=frame.image) {
                videoFrameNum++;
            }

            // 有聲音工碾,就把音頻幀加一
            if (null!=frame.samples) {
                audioFrameNum++;
            }

            // 有數(shù)據(jù)弱睦,就把數(shù)據(jù)幀加一
            if (null!=frame.data) {
                dataFrameNum++;
            }

            // 取出的每一幀,都推送到SRS
            recorder.record(frame);

            // 停頓一下再推送
            Thread.sleep(interVal);
        }

        log.info("推送完成渊额,視頻幀[{}]每篷,音頻幀[{}],數(shù)據(jù)幀[{}]端圈,耗時[{}]秒",
                videoFrameNum,
                audioFrameNum,
                dataFrameNum,
                (System.currentTimeMillis()-startTime)/1000);

        // 關(guān)閉幀錄制器
        recorder.close();
        // 關(guān)閉幀抓取器
        grabber.close();
    }

    public static void main(String[] args) throws Exception {
        grabAndPush(MP4_FILE_PATH, SRS_PUSH_ADDRESS);
    }
}
  • 上述代碼中每一行都有詳細(xì)注釋焦读,就不多贅述了,只有下面這四處關(guān)鍵需要注意:
  1. <font color="blue">MP4_FILE_PATH</font>是本地MP4文件存放的地方舱权,請改為自己電腦上MP4文件存放的位置
  2. <font color="blue">SRS_PUSH_ADDRESS</font>是SRS服務(wù)的推流地址矗晃,請改為自己的SRS服務(wù)部署的地址
  3. <font color="blue">grabber.start(true)</font>方法執(zhí)行的時候,內(nèi)部是幀抓取器的初始化流程宴倍,會取得MP4文件的相關(guān)信息
  4. <font color="blue">recorder.record(frame)</font>方法執(zhí)行的時候张症,會將幀推送到SRS服務(wù)器
  • 編碼完成后運行此類,控制臺日志如下所示鸵贬,可見成功的取到了MP4文件的幀率俗他、時長、解碼器阔逼、媒體流等信息兆衅,然后開始推流了:
23:21:48.107 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 開始初始化幀抓取器
23:21:48.267 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 幀抓取器初始化完成,耗時[163]毫秒
23:21:48.277 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 視頻幀率[15]嗜浮,視頻時長[125]秒羡亩,媒體流數(shù)量[2]
23:21:48.277 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 流的索引[0],編碼器類型[0]危融,編碼器ID[27]
23:21:48.277 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 流的索引[1]畏铆,編碼器類型[1],編碼器ID[86018]
23:21:48.279 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 視頻寬度[320]吉殃,視頻高度[240]辞居,音頻通道數(shù)[6]
23:21:48.294 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 開始初始化幀抓取器
23:21:48.727 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 幀錄制初始化完成楷怒,耗時[433]毫秒
23:21:48.727 [main] INFO com.bolingcavalry.grabpush.PushMp4 - 開始推流
  • 接下來試試能不能拉流播放

用VLC播放

  • 請安裝VLC軟件,并打開

  • 如下圖紅框瓦灶,點擊菜單中的<font color="blue">Open Network...</font>鸠删,然后輸入前面代碼中寫的推流地址(我這里是<font color="red">rtmp://192.168.50.43:11935/live/livestream</font>):

在這里插入圖片描述
  • 如下圖,成功播放倚搬,而且聲音也正常:
在這里插入圖片描述

附加知識點

  • 經(jīng)過上面的實戰(zhàn),我們熟悉了播放和推流的基本操作乾蛤,掌握了常規(guī)信息的獲取以及參數(shù)設(shè)置每界,除了代碼中的知識,還有以下幾個隱藏的知識點也值得關(guān)注
  1. 設(shè)置ffmpeg日志級別的代碼是<font color="blue">avutil.av_log_set_level(avutil.AV_LOG_ERROR)</font>家卖,把參數(shù)改為<font color="red">avutil.AV_LOG_INFO</font>后眨层,可以在控制臺看到更豐富的日志,如下圖紅色區(qū)域上荡,里面顯示了MP4文件的詳細(xì)信息趴樱,例如兩個媒體流(音頻流和視頻流):
在這里插入圖片描述
  1. 第二個知識點是關(guān)于編碼器類型和編碼器ID的,如下圖酪捡,兩個媒體流(AVStream)的編碼器類型分別是<font color="red">0</font>和<font color="red">1</font>叁征,兩個編碼器ID分別是<font color="red">27</font>和<font color="red">86018</font>,這四個數(shù)字分別代表什么呢逛薇?
在這里插入圖片描述
  1. 先看編碼器類型捺疼,用IDEA的反編譯功能打開<font color="blue">avutil.class</font>,如下圖永罚,編碼器類型等于0表示視頻(VIDEO)啤呼,類型等于1表示音頻(AUDIO):
在這里插入圖片描述
  1. 再看編碼器ID,打開<font color="blue">avcodec.java</font>呢袱,看到編碼器ID為<font color="red">27</font>表示H264:
在這里插入圖片描述
  1. 編碼器ID值<font color="red">86018</font>的十六進(jìn)制是<font color="red">0x15002</font>官扣,對應(yīng)的編碼器如下圖紅框:
在這里插入圖片描述
  • 至此,JavaCV推流實戰(zhàn)(MP4文件)已經(jīng)全部完成羞福,希望通過本文咱們可以一起熟悉JavaCV處理推拉流的常規(guī)操作惕蹄;
    https://github.com/zq2599/blog_demos
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市治专,隨后出現(xiàn)的幾起案子焊唬,更是在濱河造成了極大的恐慌,老刑警劉巖看靠,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赶促,死亡現(xiàn)場離奇詭異,居然都是意外死亡挟炬,警方通過查閱死者的電腦和手機(jī)鸥滨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門嗦哆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人婿滓,你說我怎么就攤上這事老速。” “怎么了凸主?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵橘券,是天一觀的道長。 經(jīng)常有香客問我卿吐,道長旁舰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任嗡官,我火速辦了婚禮箭窜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衍腥。我一直安慰自己磺樱,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布婆咸。 她就那樣靜靜地躺著竹捉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尚骄。 梳的紋絲不亂的頭發(fā)上活孩,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音乖仇,去河邊找鬼憾儒。 笑死,一個胖子當(dāng)著我的面吹牛乃沙,可吹牛的內(nèi)容都是我干的起趾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼警儒,長吁一口氣:“原來是場噩夢啊……” “哼训裆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜀铲,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤边琉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后记劝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體变姨,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年厌丑,在試婚紗的時候發(fā)現(xiàn)自己被綠了定欧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渔呵。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖砍鸠,靈堂內(nèi)的尸體忽然破棺而出扩氢,到底是詐尸還是另有隱情,我是刑警寧澤爷辱,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布录豺,位于F島的核電站,受9級特大地震影響饭弓,放射性物質(zhì)發(fā)生泄漏双饥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一示启、第九天 我趴在偏房一處隱蔽的房頂上張望兢哭。 院中可真熱鬧领舰,春花似錦夫嗓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锉桑,卻和暖如春排霉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背民轴。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工攻柠, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人后裸。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓瑰钮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親微驶。 傳聞我的和親對象是個殘疾皇子浪谴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容