天天看點

Java實戰音視訊領域:JavaCV推流(MP4檔案)

作者:程式員欣宸

歡迎通路我的GitHub

  • 這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 自己的mp4檔案,如何讓更多的人遠端播放?如下圖所示:
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 這裡簡單解釋一下上圖的功能:
  1. 部署開源流媒體伺服器SRS
  2. 開發名為PushMp4的java應用,該應用會讀取本機磁盤上的Mp4檔案,讀取每一幀,推送到SRS上
  3. 每個想看視訊的人,就在自己電腦上用流媒體播放軟體(例如VLC)連接配接SRS,播放PushMp4推上來的視訊
  • 今天咱們就來完成上圖中的實戰,整個過程分為以下步驟:
  1. 環境資訊
  2. 準備MP4檔案
  3. 用docker部署SRS
  4. java應用開發和運作
  5. VLC播放

環境資訊

  • 本次實戰,我這邊涉及的環境資訊如下,供您參考
  1. 作業系統:macOS Monterey
  2. JDK:1.8.0_211
  3. JavaCV:1.5.6
  4. SRS:3

準備MP4檔案

  • 準備一個普通的MP4視訊檔案即可,我是線上下載下傳了視訊開發常用的大熊兔視訊,位址是:

    https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4

用docker部署SRS

  • SRS是著名的開源的媒體伺服器,推到這裡的流,都可以用媒體播放器線上播放,為了簡單起見,我在docker環境下一行指令完成部署
docker run -p 11935:1935 -p 1985:1985 -p 8080:8080 ossrs/srs:3           
  • 此刻SRS服務正在運作中,可以推流上去了

開發JavaCV應用

  • 接下來進入最重要的編碼階段,建立名為simple-grab-push的maven工程,pom.xml如下(那個名為javacv-tutorials的父工程其實沒有什麼作用,我這裡隻是為了友善管理多個工程的代碼而已,您可以删除這個父工程節點):
<?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目前版本 -->
        <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相關依賴,一個就夠了 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacpp.version}</version>
        </dependency>
    </dependencies>
</project>           
  • 從上述檔案可見,JavaCV的依賴隻有一個javacv-platform,挺簡潔
  • 接下來開始編碼,在編碼前,先把整個流程畫出來,這樣寫代碼就清晰多了:
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 從上圖可見流程很簡單,這裡将所有代碼寫在一個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伺服器
 * @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伺服器
     * @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("開始初始化幀抓取器");

        // 初始化幀抓取器,例如資料結構(時間戳、編碼器上下文、幀對象等),
        // 如果入參等于true,還會調用avformat_find_stream_info方法擷取流的資訊,放入AVFormatContext類型的成員變量oc中
        grabber.start(true);

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

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

        // 檔案内有幾個媒體流(一般是視訊流+音頻流)
        int streamNum = avFormatContext.nb_streams();

        // 沒有媒體流就不用繼續了
        if (streamNum<1) {
            log.error("檔案内不存在媒體流");
            return;
        }

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

        log.info("視訊幀率[{}],視訊時長[{}]秒,媒體流數量[{}]",
                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();
        // 音頻通道數量
        int audioChannels = grabber.getAudioChannels();

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

        // 執行個體化FFmpegFrameRecorder,将SRS的推送位址傳入
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(SRS_PUSH_ADDRESS,
                frameWidth,
                frameHeight,
                audioChannels);

        // 設定編碼格式
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);

        // 設定封裝格式
        recorder.setFormat("flv");

        // 一秒内的幀數
        recorder.setFrameRate(frameRate);

        // 兩個關鍵幀之間的幀數
        recorder.setGopSize(frameRate);

        // 設定音頻通道數,與視訊源的通道數相等
        recorder.setAudioChannels(grabber.getAudioChannels());

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

        // 初始化幀錄制器,例如資料結構(音頻流、視訊流指針,編碼器),
        // 調用av_guess_format方法,确定視訊輸出時的封裝方式,
        // 媒體上下文對象的記憶體配置設定,
        // 編碼器的各項參數設定
        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;

        // 假設一秒鐘15幀,那麼兩幀間隔就是(1000/15)毫秒
        int interVal = 1000/frameRate;
        // 發送完一幀後sleep的時間,不能完全等于(1000/frameRate),不然會卡頓,
        // 要更小一些,這裡取八分之一
        interVal/=8;

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

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

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

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

            // 有資料,就把資料幀加一
            if (null!=frame.data) {
                dataFrameNum++;
            }

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

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

        log.info("推送完成,視訊幀[{}],音頻幀[{}],資料幀[{}],耗時[{}]秒",
                videoFrameNum,
                audioFrameNum,
                dataFrameNum,
                (System.currentTimeMillis()-startTime)/1000);

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

    public static void main(String[] args) throws Exception {
        grabAndPush(MP4_FILE_PATH, SRS_PUSH_ADDRESS);
    }
}           
  • 上述代碼中每一行都有詳細注釋,就不多贅述了,隻有下面這四處關鍵需要注意:
  1. MP4_FILE_PATH是本地MP4檔案存放的地方,請改為自己電腦上MP4檔案存放的位置
  2. SRS_PUSH_ADDRESS是SRS服務的推流位址,請改為自己的SRS服務部署的位址
  3. grabber.start(true)方法執行的時候,内部是幀抓取器的初始化流程,會取得MP4檔案的相關資訊
  4. recorder.record(frame)方法執行的時候,會将幀推送到SRS伺服器
  • 編碼完成後運作此類,控制台日志如下所示,可見成功的取到了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]秒,媒體流數量[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],音頻通道數[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軟體,并打開
  • 如下圖紅框,點選菜單中的Open Network…,然後輸入前面代碼中寫的推流位址(我這裡是rtmp://192.168.50.43:11935/live/livestream):
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 如下圖,成功播放,而且聲音也正常:
Java實戰音視訊領域:JavaCV推流(MP4檔案)

附加知識點

  • 經過上面的實戰,我們熟悉了播放和推流的基本操作,掌握了正常資訊的擷取以及參數設定,除了代碼中的知識,還有以下幾個隐藏的知識點也值得關注
  • 設定ffmpeg日志級别的代碼是avutil.av_log_set_level(avutil.AV_LOG_ERROR),把參數改為avutil.AV_LOG_INFO後,可以在控制台看到更豐富的日志,如下圖紅色區域,裡面顯示了MP4檔案的詳細資訊,例如兩個媒體流(音頻流和視訊流):
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 第二個知識點是關于編碼器類型和編碼器ID的,如下圖,兩個媒體流(AVStream)的編碼器類型分别是0和1,兩個編碼器ID分别是27和86018,這四個數字分别代表什麼呢?
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 先看編碼器類型,用IDEA的反編譯功能打開avutil.class,如下圖,編碼器類型等于0表示視訊(VIDEO),類型等于1表示音頻(AUDIO):
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 再看編碼器ID,打開avcodec.java,看到編碼器ID為27表示H264:
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 編碼器ID值86018的十六進制是0x15002,對應的編碼器如下圖紅框:
Java實戰音視訊領域:JavaCV推流(MP4檔案)
  • 至此,JavaCV推流實戰(MP4檔案)已經全部完成,希望通過本文咱們可以一起熟悉JavaCV處理推拉流的正常操作;

歡迎關注頭條号:程式員欣宸

  • 學習路上,你不孤單,欣宸原創一路相伴...

繼續閱讀