多媒體開發(3):直播

之前介紹瞭如何錄製音視頻,以及相關的多媒體的概念。對於已經錄製的多媒體進行“就地”播放(參考前文),就是回放,除了“回放”這個流程,還有一個流程也會經常遇到,那就是“直播”。

本文介紹直播的實現。

“直播”的特點是邊錄製邊播放。如果想完成直播的流程,需要有支持直播功能的服務器(也叫流媒體服務器)。有了直播服務器後,就可以把錄製的數據推送到服務器,然後再從服務器拉取數據進行播放。

那麼怎麼實現這個有直播功能的服務器呢,在這裏,小程介紹具備這個功能的服務器程序:nginx。

nginx是一個http服務器,但通過擴展(比如加入rtmp模塊等),可以變身爲流媒體服務器,並且支持rtmp與hls協議,也就具備了“直播”的功能。如果讀者對於rtmp或hls協議不瞭解,也沒有關係,只需要知道它是一個傳輸的約定就可以了,在特定的場景再作深入瞭解。

nginx是一個完整的程序,讀者只需要做一些安裝與配置的工作,就可以弄出一個支持直播(或點播)的原型出來,甚至可以投入使用。

(一)安裝nginx

以編譯nginx源碼的方式來安裝nginx,因爲要讓它支持rtmp模塊。

nginx源碼的目錄結構是這樣的:
nginx源碼目錄結構

小程列一些具體的安裝操作,讀者可以按需參考:

nginx×××
到nginx官網下載最新版本的nginx源碼,官網的地址:
http://nginx.org/en/download.html

rtmp模塊,也就是nginx-rtmp-module的×××,讓它跟nginx項目在同一個目錄下面:
git clone https://github.com/arut/nginx-rtmp-module.git

openssl模塊的×××(這個被rtmp使用,其實也可以不用,但小程這裏還是加上了),同樣與nginx項目在一個目錄下面:
git clone https://github.com/openssl/openssl.git

編譯nginx並安裝
下載完源碼後,就可以開始編譯了:
(注意:如果之前用brew安裝過nginx,那要先卸載:sudo brew uninstall nginx,因爲要支持rtmp,須通過源碼來編譯安裝。)
cd nginx-1.11.13/
./configure --add-module=../nginx-rtmp-module --with-openssl=../openssl
make
sudo make install

最終的安裝目錄應該是/usr/local/nginx/sbin/,
在~/.bash_profile中指定搜索路徑:
export PATH="${PATH}:/usr/local/nginx/sbin/"

安裝好nginx後,可以嘗試使用它:

查看nginx配置文件路徑等信息:
nginx -h

啓動nginx:
sudo nginx
如果有提示端口已經被佔用,那可能已經啓動了,可以重新啓動:
sudo nginx -s reload

測試nginx:
curl 127.0.0.1
或者瀏覽器訪問localhost:8080,
能看到welcome信息即表示安裝成功而且已經運行,佔用8080端口。

最終可以看到nginx的welcome:
nginx安裝成功提示

(二)實現點播

因爲實現點播的功能會更簡單一點,所以小程先介紹使用nginx來實現點播的操作,讀者也可以直接跳至“直播”的內容來閱讀。

做到點播(回放),只需要改下配置,讓nginx關聯一個本地的文件即可。

具體操作是這樣的:

查看配置文件的路徑:
nginx -h

配置文件爲/usr/local/nginx/conf/nginx.conf,
也可以通過nginx -c來指定一個新的配置文件。

在配置文件中,增加rtmp項:
rtmp {
server {
listen 1935; # port
chunk_size 4096; # data chunk size
application playback {
play /opt/show; # target file path
}
}
}
1935爲端口,chunk_size爲塊大小。
playback爲應用的名稱,可以任意改。
play爲關鍵字,不能改。
play對應的值爲本地文件所在目錄,把要播放的文件放在這個目錄內。

配置完畢後,重啓nginx:
sudo nginx -s reload

測試,使用ffplay來播放nginx服務器的點播(對於ffplay的使用讀者可以參考小程之前的介紹):
ffplay rtmp://localhost/playback/Movie-1.mp4

Movie-1.mp4是放在目錄/opt/show/下面的文件。

按測試的示例,可以想到,如果把localhost換成某個ip就可以對不同機子上面的文件進行點播。

(三)實現直播

實現直播,也很簡單,同樣修改一下配置,就可以做到。

具體的操作是這樣的:

在配置文件中,增加rtmp項:
rtmp {
server {
listen 1935; # port
chunk_size 4096; # data chunk size
application rtmpdemo {
live on;
}
}
}
同樣,1935爲端口,chunk_size爲塊大小。
rtmpdemo是應用名稱,可以隨意改。

重啓nginx:
sudo nginx -s reload

用ffmpeg來模擬推流:
sudo ffmpeg -re -i Movie-1.mp4 -vcodec copy -f flv rtmp://localhost/rtmpdemo/test1
-re 表示按幀率來推;
-f 爲推送時封裝的格式,對於rtmp都應該使用flv。

這時,服務器nginx已經有多媒體流了,客戶端拉流播放:
ffplay "rtmp://localhost/rtmpdemo/test1 live=1"

上面的演示,是把一個本地的文件推到了nginx,實際的直播場景中,是邊錄製邊推流,讀者可以結合小程以前介紹的錄製視頻的辦法,來做到錄製。

至此,已經把“使用nginx來實現直播”的主體操作介紹完了,但這畢竟只是一個原型,直播的難點分落在服務器與客戶端,比如服務器如何高性能低延遲,客戶端如何實時(與協議選擇、服務器分佈也有關)並處理好聲畫質量的問題,等等。

以上介紹了通過nginx實現直播的流程,其中一個環節是通過ffmpeg的命令來推流的,那如果想寫代碼來實現,可以怎麼做呢?

這裏涉及到FFmpeg的調用,而它的使用應該有更多的前提,比如FFmpeg的編譯、引用、調用等等,如果讀者想在瞭解這些前置環節之後再作深入瞭解也是可以的,那就不必閱讀下面的內容。但是,爲了保持內容的完整性,小程還是加上這部分內容。


使用ffmpeg命令來推流,控制度不夠高,現在以代碼的方式來實現,可靈活控制。

最終的效果是這樣的(一邊推流到服務器,一邊從服務器拉流播放):
推流與播放的效果

演示推流的代碼

#include <stdio.h>
#include "ffmpeg/include/libavformat/avformat.h"
#include "ffmpeg/include/libavcodec/avcodec.h"

void publishstream() {
    const char* srcfile = "t.mp4";
    const char* streamseverurl = "rtmp://localhost/rtmpdemo/test1";
    av_register_all();
    avformat_network_init();
    av_log_set_level(AV_LOG_DEBUG);
    int status = 0;
    AVFormatContext* formatcontext = avformat_alloc_context();
    status = avformat_open_input(&formatcontext, srcfile, NULL, NULL);
    if (status >= 0) {
        status = avformat_find_stream_info(formatcontext, NULL);
        if (status >= 0) {
            int videoindex = -1;
            for (int i = 0; i < formatcontext->nb_streams; i ++) {
                if (formatcontext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                    videoindex = i;
                    break;
                }
            }
            if (videoindex >= 0) {
                AVFormatContext* outformatcontext;
                avformat_alloc_output_context2(&outformatcontext, NULL, "flv", streamseverurl);
                if (outformatcontext) {
                    status = -1;
                    for (int i = 0; i < formatcontext->nb_streams; i ++) {
                        AVStream* onestream = formatcontext->streams[i];
                        AVStream* newstream = avformat_new_stream(outformatcontext, onestream->codec->codec);
                        status = newstream ? 0 : -1;
                        if (status == 0) {
                            status = avcodec_copy_context(newstream->codec, onestream->codec);
                            if (status >= 0) {
                                newstream->codec->codec_tag = 0;
                                if (outformatcontext->oformat->flags & AVFMT_GLOBALHEADER) {
                                    newstream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
                                }
                            }
                        }
                    }
                    if (status >= 0) {
                        AVOutputFormat* outformat = outformatcontext->oformat;
                        av_usleep(5*1000*1000); // 故意等一下再開始推流,讓拉流的客戶端有時間啓動,以拿到視頻的pps/sps
                        if (!(outformat->flags & AVFMT_NOFILE)) {
                            av_dump_format(outformatcontext, 0, streamseverurl, 1);
                            status = avio_open(&outformatcontext->pb, streamseverurl, AVIO_FLAG_WRITE);
                            if (status >= 0) {
                                status = avformat_write_header(outformatcontext, NULL);
                                if (status >= 0) {
                                    AVPacket packet;
                                    int videoframeidx = 0;
                                    int64_t starttime = av_gettime();
                                    while (1) {
                                        status = av_read_frame(formatcontext, &packet);
                                        if (status < 0) {
                                            break;
                                        }
                                        if (packet.pts == AV_NOPTS_VALUE) {
                                            av_log(NULL, AV_LOG_DEBUG, "set pakcet.pts\n");
                                            AVRational video_time_base = formatcontext->streams[videoindex]->time_base;
                                            int64_t frameduration = (double)AV_TIME_BASE / av_q2d(formatcontext->streams[videoindex]->r_frame_rate);
                                            packet.pts = (double)(videoframeidx * frameduration) / (double)(av_q2d(video_time_base) * AV_TIME_BASE);
                                            packet.dts = packet.pts;
                                            packet.duration = (double)frameduration / (double)(av_q2d(video_time_base) * AV_TIME_BASE);
                                        }
                                        if (packet.stream_index == videoindex) {
                                            AVRational video_time_base = formatcontext->streams[videoindex]->time_base;
                                            AVRational time_base_q = {1, AV_TIME_BASE};
                                            int64_t cur_pts = av_rescale_q(packet.dts, video_time_base, time_base_q);
                                            int64_t curtime = av_gettime() - starttime;
                                            av_log(NULL, AV_LOG_DEBUG, "on video frame curpts=%lld curtime=%lld\n", cur_pts, curtime);
                                            if (cur_pts > curtime) {
                                                av_usleep(cur_pts - curtime);
                                            }
                                        }
                                        AVStream* instream = formatcontext->streams[packet.stream_index];
                                        AVStream* outstream = outformatcontext->streams[packet.stream_index];
                                        packet.pts = av_rescale_q_rnd(packet.pts, instream->time_base, outstream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
                                        packet.dts = av_rescale_q_rnd(packet.dts, instream->time_base, outstream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
                                        packet.duration = av_rescale_q(packet.duration, instream->time_base, outstream->time_base);
                                        packet.pos = -1;
                                        if (packet.stream_index == videoindex) {
                                            videoframeidx ++;
                                        }
                                        status = av_interleaved_write_frame(outformatcontext, &packet);
                                        if (status < 0) {
                                            break;
                                        }
                                    }
                                    av_write_trailer(outformatcontext);
                                }
                                avio_close(outformatcontext->pb);
                            }
                        }
                    }
                    avformat_free_context(outformatcontext);
                }
            }
        }
        avformat_close_input(&formatcontext);
    }
    avformat_free_context(formatcontext);
}

int main(int argc, char *argv[])
{
    publishstream();
    return 0;
}

這裏以本地的視頻文件作爲內容,模擬了直播推流(推到nginx),功能上相當於直接調用ffmpeg命令:

sudo ffmpeg -re -i Movie-1.mp4 -vcodec copy -f flv rtmp://localhost/rtmpdemo/test1

當然也可以邊錄製邊推送,也可以在不同的電腦或手機上,拉流播放。

直播開始後,這裏的流媒體服務器並沒有給中途拉流的客戶端發送視頻解碼所必須的參數(pps/sps),所以在測試的時候,要保證拉流端能拿到第一幀數據,比如演示代碼中故意sleep幾秒後纔開始推流,讓拉流端有時間開啓並拿到推上去的所有數據(包括關鍵參數)。


總結一下,本文介紹了使用nginx來實現多媒體的直播。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章