歡迎訪問我的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)程播放咐鹤?如下圖所示:
- 這里簡單解釋一下上圖的功能:
- 部署開源流媒體服務(wù)器<font color="blue">SRS</font>
- 開發(fā)名為<font color="blue">PushMp4</font>的java應(yīng)用霸旗,該應(yīng)用會讀取本機(jī)磁盤上的Mp4文件,讀取每一幀秕脓,推送到SRS上
- 每個想看視頻的人柒瓣,就在自己電腦上用流媒體播放軟件(例如VLC)連接SRS,播放PushMp4推上來的視頻
- 今天咱們就來完成上圖中的實戰(zhàn)吠架,整個過程分為以下步驟:
- 環(huán)境信息
- 準(zhǔn)備MP4文件
- 用docker部署SRS
- java應(yīng)用開發(fā)和運行
- VLC播放
環(huán)境信息
- 本次實戰(zhàn)芙贫,我這邊涉及的環(huán)境信息如下,供您參考:
- 操作系統(tǒng):macOS Monterey
- JDK:1.8.0_211
- JavaCV:1.5.6
- SRS:3
準(zhǔn)備MP4文件
- 準(zhǔn)備一個普通的MP4視頻文件即可诵肛,我是在線下載了視頻開發(fā)常用的大熊兔視頻屹培,地址是:
https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.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)鍵需要注意:
- <font color="blue">MP4_FILE_PATH</font>是本地MP4文件存放的地方舱权,請改為自己電腦上MP4文件存放的位置
- <font color="blue">SRS_PUSH_ADDRESS</font>是SRS服務(wù)的推流地址矗晃,請改為自己的SRS服務(wù)部署的地址
- <font color="blue">grabber.start(true)</font>方法執(zhí)行的時候,內(nèi)部是幀抓取器的初始化流程宴倍,會取得MP4文件的相關(guān)信息
- <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)注
- 設(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ì)信息趴樱,例如兩個媒體流(音頻流和視頻流):
- 第二個知識點是關(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ù)字分別代表什么呢逛薇?
- 先看編碼器類型捺疼,用IDEA的反編譯功能打開<font color="blue">avutil.class</font>,如下圖永罚,編碼器類型等于0表示視頻(VIDEO)啤呼,類型等于1表示音頻(AUDIO):
- 再看編碼器ID,打開<font color="blue">avcodec.java</font>呢袱,看到編碼器ID為<font color="red">27</font>表示H264:
- 編碼器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