java實現(xiàn)直播(推拉流)和保存直播視頻

首先墨跡一下前提:
公司接了個電商的項目,然后甲方要求有類似于淘寶的直播的功能。重點是?。。〖追桨职植辉敢獬⒗镌浦辈ィv訊云直播的錢。所以問題就來了,這個直播的實現(xiàn)要免費。
然后這個項目是java+vue實現(xiàn)的。前后端分離的,至于用不用分布式啥的暫時先沒定下來,不過因為確定有直播模塊,所以這塊是先測試實現(xiàn)了的。這個帖子就是對實現(xiàn)直播這一需求的整理的記錄。

互聯(lián)網(wǎng)直播服務(wù)管理規(guī)定

2016年4月13日,百度,新浪,搜狐等20余家直播平臺共同發(fā)布《北京網(wǎng)絡(luò)直播行業(yè)自律公約》,承諾網(wǎng)絡(luò)直播房間必須標識水?。粌?nèi)容存儲時間不少于15天備查;所有主播必須實名認證;對于播出涉政、涉槍、涉毒、涉暴、涉黃內(nèi)容的主播,情節(jié)嚴重的將列入黑名單;審核人員對平臺上的直播內(nèi)容進行24小時實時監(jiān)管。
咳咳,想來想去我決定把這個放在最開始。上面的規(guī)定其實不算多:

  • 實名認證是能直播的用戶要經(jīng)過實名認證,代碼里就可以搞定。
  • 直播間房間要水印其實這個可以和點贊等做一個掛在視頻前面的“畫布”,所以我們這個項目最終決定前端搞定。
  • 至于內(nèi)容存儲時間不小于15天,因為我做了保存直播的功能,雖然還沒做定時清理,不過技術(shù)上不是難點,所以也算是完成了。
  • 最后這個涉黃,涉政,涉爆啥的人工審核我也沒辦法了,只能到時候甲方將項目上線自己去找人看著了。

如上,反正這個直播功能是理清楚了,大概做出來也是合法的了。接下來開始說技術(shù)吧(因為我也是第一次做直播,所以可能走了一些冤路,不過我覺得稍微有用的都會寫出來,畢竟前車之鑒)

直播技術(shù)的實現(xiàn)

其實這一塊沒想的那么難,但是也沒想的那么簡單。我一點點分析。

首先,要理解直播是什么:

  • 圖片可以有base64編碼,這一串碼就是代表這個圖片。
  • 視頻我們可以理解成是一張張圖片的編碼的和,展現(xiàn)出來的動圖也不過是一張張圖片連續(xù)播放的結(jié)果(我這么說并不準確,我只是為了方便理解)
  • 因為直播是一個個圖片傳輸?shù)?,在你剛開播的一秒是沒有下一秒的圖片的,所以這個視頻的編碼是要一直傳輸?shù)?,而這種一直傳輸編碼的情況,我們用一個專業(yè)術(shù)語表示:

我不知道別的語言怎么理解,但是java中有字節(jié)流,字符流等等,所以這里的流一般是音頻+視頻。所以具體是什么不深究,只要知道這里的流是用來傳輸視頻的就行。

簡單直播流程

然后這里有一張圖很好的說明了直播的流程:


直播流程

如圖,直播的流程其實主要就這么多(也可以在中間有別的操作,我說的是最簡單的操作),又因為我們決定直播的形式是服務(wù)器作為中間站。所以大概流程是直播用戶推流到服務(wù)器,然后有想要看直播的去服務(wù)器拉流。接下來一點點說:

  • 采集。其實這個很好理解,打開語音,攝像頭,采集數(shù)據(jù)。這里也說了,需要終端音視頻引擎解決。(終端是指用戶直播的設(shè)備。比如手機,pc等)

  • 前處理。這個是已經(jīng)試過的,比如我們現(xiàn)在手機拍照片自帶美顏,你以為是你攝像頭給你美顏的么?不是的,是攝像頭拍出來的是正常照片,手機拍照后對這個照片進行了處理。而這里的前處理也是類似的道理,前端調(diào)用麥克風(fēng)攝像頭,拍/錄視頻的同時對這個視頻進行一些加工(我們已經(jīng)測試完了,反正是可以美顏,磨皮等。剛剛順口問了下我們前端,說是用的h5 plus中一些自帶的api,我是前端小白,不是很理解,反正是能實現(xiàn)就對了。)

  • 編碼。這個其實就類似于圖片的base 64 一樣,視頻流傳輸之前肯定是要經(jīng)過編碼的,應(yīng)該可能也有很多種設(shè)置,反正我們調(diào)試用的H264。(關(guān)于這個編碼我在網(wǎng)上看了下,好像直接影響編碼速度解碼速度什么的,也就是影響延遲,卡頓,清晰度等。不過我們現(xiàn)在真的是只要能實現(xiàn)就行了,所以暫定h264.沒做什么多的嘗試)

  • 推流。這個其實剛剛我解釋了流是什么,推流應(yīng)該不難理解,就是把自己這個視頻直播的編碼以流的形式傳給誰。

  • 拉流。和推流對應(yīng),就是去有流的地方,把這個直播流拉倒本地。這兩個行為其實很容易理解:我把這篇圖文上傳到簡書,也就是推到簡書。你們來簡書看這篇圖文,也就是從簡書拉到你的顯示器。只不過因為直播推來的都是流而已。

  • 解碼。這個其實就不用說了吧,你當時傳輸?shù)臅r候是按照一定編碼編譯的,肯定想要復(fù)原要按照這個編碼方式解碼。

  • 渲染。屬于后期顯示問題了,就是這個視頻已經(jīng)傳到你觀看直播的畫面,不同的手機會有不同的顯示,同一個視頻有的手機看顏色發(fā)紫(因為我手機防輻射藍光膜),有的手機看顏色艷麗,有的手機看顏色昏黃,這個就是渲染吧。

如上,其實一個簡單的直播就是這么個流程。
然后注意一下,我這里說的只是一個最簡單的實現(xiàn)直播的流程。復(fù)雜的比如多人連麥啊,甚至多人視頻直播啊這種,推流是差不多的,但是拉流要眾多流合在一起展示,這個屬于復(fù)雜操作了,我也沒做,有興趣的可以自己做。

本地搭建直播平臺

其實剛剛我也說了,我們打算是流程是直播用戶把自己的直播流推給服務(wù)器,然后想要看直播的用戶來服務(wù)器拉取對應(yīng)的流。所以重點就是怎么把一臺電腦作為服務(wù)器。
其實現(xiàn)在已經(jīng)有現(xiàn)成的框架了,再次感謝共享技術(shù)的大大。然后我一點點說:
其實本地作為服務(wù)器是多多種方式的,我也是在網(wǎng)上看到的教程(最后會附上寫這個文章參考的連接)。我這里只寫我自己用的方法啦。
我這里用到的比較簡單,是ffmpeg+nginx實現(xiàn)的。
其實主要就三步:

  1. 下載nginx并啟動nginx(以后的所有推拉工作都要在nginx啟動的情況下實現(xiàn));
  2. 下載ffmpeg并設(shè)置環(huán)境變量(說實話我不知道如果沒有這個會怎么樣,不過三臺電腦我都按照要求配置了。這個應(yīng)該是推流的時候用的,不過demo中是本地推才用到這個,我感覺我和前端聯(lián)調(diào)的時候好像是沒用到這個?。?/li>
  3. 自推自拉做個demo

然后感謝之前提供網(wǎng)盤壓縮包的大佬。我這里借花獻佛貼在這里:
鏈接: https: //pan.baidu.com/s/1lN1ps0ZhCb-1A56ycNR88g
密碼: 2t88
點進這個鏈接可以下載需要的ffmpeg和nginx的壓縮包(我在網(wǎng)址的https:后面加了個空格,復(fù)制粘貼的時候注意刪除啊,不然鏈接格式發(fā)不出來)。

然后打開壓縮包里面兩個小壓縮包,一個視頻。視頻是用來做實驗的,用不用都行。


網(wǎng)盤下載來的

我解壓到d盤demo文件夾中,并把ffmpeg和nginx都解壓到當前文件夾了。


解壓后

接下來按照步驟:

  1. 啟動nginx:
    cmd打開控制臺,進入到nginx-1.7.11.3-Gryphon目錄,然后啟動。
    比如我的是放在d盤demo下,然后命令就是:

第一步,跳到d盤:

d:

第二步,cd到nginx-1.7.11.3-Gryphon目錄下:

cd demo/nginx-1.7.11.3-Gryphon

第三步,啟動nginx:

nginx.exe -c conf\nginx-win-rtmp.conf

然后正常啟動是不會顯示內(nèi)容的,但是一直在運行,這個控制臺頁面不要關(guān),就這么放著。


我控制臺啟動nginx頁面

然后可以訪問ip:80直接訪問,查看自己nginx是否正常啟動了


nginx正常啟動頁面
  1. 下載ffmpeg并設(shè)置環(huán)境變量
    第一步:設(shè)置環(huán)境變量(步驟就不重復(fù)了,這個比較初級了。)
    我直接貼我的截圖了:


    image.png
  2. 其實這個時候前臺推來的流我就已經(jīng)可以正常接收并且被同局域網(wǎng)的設(shè)備拉取了,不過如果自己做沒有這個前端配合,或者說想確定一下是不是前面都做對了,可以自己模擬一下推流,再自己模擬拉流。
    因為我這塊完全按教程做的,好像模擬拉流只能用VLC播放器,反正我是只用了這個測試。


    vlc播放器,自己去百度下載就行了

    然后這里就用到了之前下載順便帶的測試視頻了,當然你電腦里有別的視頻也可以用,新打開一個控制臺,然后下面這串命令:

ffmpeg -re -i d:/demo/orange.mp4 -vcodec libx264 -acodec aac -f flv rtmp://ip:1935/live/home

重點來了??!首先,之前我看教程,人家是直接-i orange.mp4,我復(fù)制粘貼運行一直報找不到文件,后來我改成絕對路徑就ok了。 其次這個ip+端口ip寫你自己的ip就行,端口1935的設(shè)置是在nginx的配置文件中配置的,是可以改的。要酌情使用
然后如上命令中,大概意思是把orange.mp4這個視頻以流的形式用h264編碼,flv的格式 以rtmp的形式推給ip指向的電腦的/live/home這(自我理解,錯了勿噴,歡迎指點)。
同理如果拉流也要來這個地方拉。所以可以直接復(fù)制這個路徑,直接在vlc上ctrl+v可以貼網(wǎng)址,把這個網(wǎng)址貼上就能看到你之前推的視頻了。

輸入你之前推的網(wǎng)址

如果你能看到你傳的視頻,說明你成功了!直播平臺已經(jīng)搭建完了!

視頻傳輸和直播的區(qū)別

可能看到這有人不懂了,這不就是視頻傳輸么?憑什么叫直播???這不是忽悠人么?其實不是的,這個在細節(jié)上可以看到。如果是視頻傳輸,那么接收的人只能從視頻的一開始看,而直播的區(qū)別是傳輸?shù)侥睦锞蛻?yīng)該從哪里看(不要和我抬杠延遲)、還有就是應(yīng)該是直播一結(jié)束立刻觀看直播的也會結(jié)束。
因為上面的demo我們用的視頻,所以這點可能讓很多人誤會,不過好一點的是這個視頻一分多鐘,而推流的過程不是一秒鐘一秒鐘推的,所以速度比較快,好幾秒好幾秒推的,但是我們可以稍微晚幾秒拉流,明顯可以看到每次顯示的視頻開始的內(nèi)容是不一樣的。
同樣,如果到這還不能理解或者有疑問,我建議大家不要用傳視頻的方式證明平臺搭建成功。可以直接調(diào)用本地視頻音頻,然后推流到本地,再用vlc拉流,這樣明顯看到開始直播則能拉倒,關(guān)閉直播則立刻拉不到。
做法我覺得比較麻煩,但是不難,最后我列出來的技術(shù)貼上會有教程。大家可以去看看。

保存直播視頻

最開始就說了,保存直播視頻15天是一個規(guī)范。然后15天其實可以多種方式實現(xiàn),重點是保存。
因為我推拉流都沒經(jīng)過代碼,我還以為保存會很復(fù)雜, 但是也比想的簡單的多。我就不多說心路歷程了,直接上結(jié)果(我要重復(fù)一下,實現(xiàn)的方式可能是多種多樣的,我只不過選了一種實現(xiàn)了,不代表性能是最好的或者技術(shù)最新的):

  • 導(dǎo)包(因為用到了javaCV的FFmpegFrameGrabber幀捕捉器捕捉流的音頻幀和視頻幀,所以要導(dǎo)相應(yīng)的包)
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.4.3</version>
</dependency>
  • 復(fù)制粘貼代碼
    這里因為有些不好解釋,中間遇到問題FFmpegFrameGrabber類也不說清楚,還是比較坑的,我是調(diào)了很多細微的東西才實現(xiàn)的,我先貼出來,咱們再一點點說需要注意什么。
package io.renren.modules.other.tool;

import java.io.File;
import java.io.IOException;

import org.bytedeco.javacpp.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.FrameRecorder;

public class RecordVideoThread extends Thread {

    public String streamURL;// 流地址 網(wǎng)上有自行百度
    public String filePath;// 文件路徑
    public Integer id;// 案件id
    
    public void setStreamURL(String streamURL) {
        this.streamURL = streamURL;
    }
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
    @Override
    public void run() {
        System.out.println(streamURL);
        // 獲取視頻源
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(streamURL);
        FFmpegFrameRecorder recorder = null;
        try {
            grabber.start();
            Frame frame = grabber.grabFrame();
            if (frame != null) {
                File outFile = new File(filePath);
                if (!outFile.isFile()) {
                    try {
                        outFile.createNewFile();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                // 流媒體輸出地址,分辨率(長,高),是否錄制音頻(0:不錄制/1:錄制)
                recorder = new FFmpegFrameRecorder(filePath, 1080, 1440, 1);
                recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);// 直播流格式
                recorder.setFormat("flv");// 錄制的視頻格式
                recorder.setFrameRate(25);// 幀數(shù)
                //百度翻譯的比特率,默認400000,但是我400000賊模糊,調(diào)成800000比較合適
                recorder.setVideoBitrate(800000);
                recorder.start();
                while ((frame != null)) {
                    recorder.record(frame);// 錄制
                    frame = grabber.grabFrame();// 獲取下一幀
                }
                recorder.record(frame);
                // 停止錄制
                recorder.stop();
                grabber.stop();
            }
        } catch (FrameGrabber.Exception e) {
            e.printStackTrace();
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
        } finally {
            if (null != grabber) {
                try {
                    grabber.stop();
                } catch (FrameGrabber.Exception e) {
                    e.printStackTrace();
                }
            }
            if (recorder != null) {
                try {
                    recorder.stop();
                } catch (FrameRecorder.Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        RecordVideoThread thread = new RecordVideoThread();
        thread.setFilePath("D:/testOne.flv");
        thread.setStreamURL("rtmp://ip:端口/live/home");
        thread.start();        
    }
}

如下代碼,推流的地址和要保存的文件的文件名我是寫活了的。而且因為這個保存是個長時間執(zhí)行的方法,所以我專門啟線程來實現(xiàn)的。
然后注意順序:
?。?!必須必須按照順序來:首先先啟動方法,再啟動直播,然后先結(jié)束直播,再結(jié)束方法
如果不這樣做會保存不到視頻。具體原因我也沒深究,反正我這個是一次次測試得出來的結(jié)論,并且這樣也能讓直播視頻完整的保存下來,這樣邏輯也符合,所以就這樣按照流程操作就行了。
我代碼中是存到D:/testOne.flv中,給大家看下:

保存的視頻

播放保存的視頻

其實我這個畫質(zhì)也就這樣了,把代碼中的

ecorder.setVideoBitrate(800000);

時間越大越清楚,但是同樣耗費空間也越大, 之前試過400000的時候。43s視頻才5兆不到。 然后1800000的時候不到二十兆,差別就來了,所以這個值挑一個平衡值就好。

然后說最大的一個坑:格式問題
現(xiàn)在我也不知道是我看的所有的技術(shù)貼都過時了還是推流的方式不一樣,反正好多帖子上都是mp4格式,我就傻了吧唧照著cv了,然后能保存下東西播放不了。為此還特意下載了暴風(fēng)影音和迅雷看看。。。
昨天整整調(diào)了一下午都沒弄好,今天上午在群友的建議下改成了avi還是不行,最后改成了flv才可以正常播放。
還有上面代碼中有一行:

                // 流媒體輸出地址,分辨率(長,高),是否錄制音頻(0:不錄制/1:錄制)
                recorder = new FFmpegFrameRecorder(filePath, 1080, 1440, 1);

這行代碼的1080和1440也是我自己隨手改的,這個長寬應(yīng)該是可以自定義(其實大多數(shù)屬性都可以自定義吧,只不過我沒用到的就沒那么仔細看)
其實這個我主要是卡在了視頻格式上,但是我看百度上說遇到的問題千奇百怪。反正我大概只遇到這兩個:一個是推流順序不對會保存不到文件,還有一個就是格式問題打不開。
以后如果隨著使用遇到問題了再說。
然后這次直播的實現(xiàn)差不多就這樣了。
還有一些小知識點:

路徑問題

你怎么推的怎么拉就行了,這個/live/home不是寫死的。比如我們這個項目因為每個用戶都可以直播,所以打算是/live/user_id這種形式,可以保證每個用戶的推拉流都是不同的。順便分享一下我們前端實現(xiàn)的頁面:


image.png

image.png

直播頁面,可以在這或者寫推流地址,正常應(yīng)該寫死的

看直播的頁面
端口問題

剛剛我就說了1935是nginx默認的rtmp端口。這個是可以改的,只不過默認配置是1935(不過我沒改過,有興趣的可以試試)如下圖:


配置文件的位置

配置文件中的配置
細節(jié)問題

之前我們直播的時候發(fā)現(xiàn)直播效果賊卡,各種馬賽克,中間經(jīng)過了懷疑,調(diào)試等功能,發(fā)現(xiàn)應(yīng)該是前端可以設(shè)置推流的一些配置,除了特別的情況,剩下好多畫面明亮度,清晰度啥的,都是前端調(diào)的,反正上面這種做法后端是沒啥調(diào)的能力。如果都打算這么偷懶了,就別浪費時間瞎找了,直接甩給前端ok了。

拓展問題

剛剛我也說了,這是最簡單的一種方法,也是偷懶的方法,甚至最開始的流程也是最簡流程。其實可以做的還很多。
就比如我之前說的美顏,微調(diào),清晰度啥的,后端其實是可以做的,但是那就相當于前端推流給后端,后端解析成畫面,處理,然后再編碼成流,等著別人拉。
這個過程應(yīng)該比較好理解,多了幾個步驟,肯定是性能不如現(xiàn)在。而且需要的難度比較高,雖然有現(xiàn)有的框架但是也沒現(xiàn)在一行代碼不寫方便??!
順便推薦三個后端處理的框架:

  • 大而全的Kurento

  • 務(wù)實主義的Licode

  • 小而美的Mediasoup

因為我也沒用過,所以只是給個提議,有興趣的自己去看。最后會列上關(guān)于這三個框架的詳細介紹的帖子地址。

外網(wǎng)問題

額,因為是要用在項目里,所以一直局域網(wǎng)測試肯定是不太合適。所以我是用內(nèi)網(wǎng)穿透的形式把我自己的工作電腦映射到我們的服務(wù)器上了,由此實現(xiàn)了外網(wǎng)直播,外網(wǎng)觀看。反正截止到目前為止,四五臺電腦四五個手機,4個推流5個拉流是保證延遲在5s以內(nèi)的。另外因為我是內(nèi)網(wǎng)穿透實現(xiàn)的,所以可能還有一部分時間是這么消耗的,反正是在能承受的范圍內(nèi)。
再多的就沒辦法了,起碼說明小范圍的推拉流是完全沒問題的。


如圖,手機推流給外網(wǎng),電腦外網(wǎng)拉流

補充下之前說的好多技術(shù)貼的鏈接:
使用ffmpeg將直播流保存到本地
rtmp和rtsp的區(qū)別和適用范圍
http://javadox.com/org.bytedeco/javacv/0.8/org/bytedeco/javacv/FFmpegFrameGrabber.html
本地直播平臺搭建的四種方式
通過MediaRecorder+MediaSource實現(xiàn)H5直播,以及關(guān)于WebRTC直播的問題
如何真正讓小程序,WebRTC和APP互通連麥直播
HTML5實現(xiàn)視頻直播功能思路詳解
如何打造自己的WebRTC 服務(wù)器

目前為止,我也只不過是調(diào)通了功能,實現(xiàn)了部分應(yīng)該實現(xiàn)的,多推流多拉流還有保存直播視頻,然后幾乎沒遇到什么問題(性能問題不算)。感覺就是比想的要簡單,但是也比想的要復(fù)雜,因為我做的太簡潔了。這個帖子就算是對這幾天整理的一個記錄,如果以后這塊功能在項目中實現(xiàn)了,再遇到什么問題我會追更的。
不得不承認,編寫程序的魅力就在于自己從無到有做出了一些看起來很神奇的功能。我熱愛我的職業(yè),現(xiàn)在,依然,永遠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一.創(chuàng)建接口或內(nèi)部接口 二.在Adapter 1.設(shè)置全局變量 2.添加set方法 3.在onBindViewHo...
    我叫楊毅閱讀 223評論 0 0
  • 現(xiàn)在的我比以前多了絲憂慮 為了生活 為了自己存在的狀態(tài) 當然,不是海子那樣的抑郁 也不是徐志摩為林徽因那樣的沖動 ...
    右右知我心閱讀 304評論 1 4
  • 姓名:王麗 組別:第377期六項精進努力二組組員 【日精進打卡第109天】 【知-學(xué)習(xí)】 背誦《大學(xué)》開篇5遍共1...
    天黑黑_e3af閱讀 130評論 0 0

友情鏈接更多精彩內(nèi)容