多媒體開發(3):直播

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

本文介紹直播的實現。nginx

「直播」的特色是邊錄製邊播放。若是想完成直播的流程,通常須要有支持直播功能的服務器(也叫流媒體服務器)。有了直播服務器後,就能夠把錄製的數據推送到服務器,而後再從服務器拉取數據進行播放。git

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

nginx是一個http服務器,但經過擴展(好比加入rtmp模塊等),能夠變身爲流媒體服務器,而且支持rtmp與hls協議,也就具有了「直播」的功能。若是你對於rtmp或hls協議不瞭解,也沒有關係,只須要知道它是一個傳輸的約定就能夠了,在特定的場景再做深刻了解。shell

nginx是一個完整的程序,你只須要作一些安裝與配置的工做,就能夠弄出一個支持直播(或點播)的原型出來,甚至能夠投入使用。瀏覽器

(一)安裝nginx

以編譯nginx源碼的方式來安裝nginx,由於要讓它支持rtmp模塊,固然你也能夠經過brew install來安裝,但不是我這裏介紹的方式。bash

我列一些具體的安裝操做,你能夠按需參考:服務器

(1)nginx源碼下載

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

(2)rtmp模塊

也就是nginx-rtmp-module的源碼下載,讓它跟nginx項目在同一個目錄下面:curl

git clone https://github.com/arut/nginx-rtmp-module.git

(3)openssl模塊

openssl被rtmp使用,須要下載到它的源碼。在 https://www.openssl.org/source/ 中找到它某個版原本下載,好比我下載的是openssl-1.1.1i,下載解壓後與nginx項目在一個目錄下面。
這時,nginx、rtmp跟openssl的源碼都下載到了,以下面的目錄結構:
nginx源碼目錄結構

(4)編譯nginx並安裝

下載完源碼後,就能夠開始編譯了。注意:若是以前用brew安裝過nginx,那要先卸載:sudo brew uninstall nginx。

cd nginx-1.19.6
./configure --add-module=../nginx-rtmp-module --with-openssl=../openssl-1.1.1i --without-http_rewrite_module
make
sudo make install

最終的安裝目錄是/usr/local/nginx/sbin/,在那裏能夠看到nginx執行文件,爲了讓shell(好比我在用的bash或sh)能搜索到這個目錄,在配置文件/.bash_profile,或/.zhrc中指定搜索路徑,增長下面這句:

export PATH="${PATH}:/usr/local/nginx/sbin/"

再讓這個配置生效:

source ~/.bash_profile 或:
source ~/.zshrc

這時,能夠直接在shell中使用nginx命令了。

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

nginx -h

啓動nginx:

sudo nginx

若是有提示端口已經被佔用,那可能已經啓動了,能夠從新啓動:

sudo nginx -s reload

測試nginx:

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

mac上的hosts文件是/etc/hosts,若是須要修改能夠這樣進行:sudo vi /etc/hosts

最終能夠看到nginx的welcome:
nginx返回的welcome頁面

(二)實現直播

查看配置文件的路徑:

nginx -h

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

在配置文件中(好比最末尾),增長rtmp項:

rtmp {
	server {
		listen 1935;    	# port
		chunk_size 4096;    # data chunk size
		application rtmpdemo {
			live on;
		}
	}
}

1935爲端口,chunk_size爲塊大小。rtmpdemo是應用名稱,能夠隨意改。

注意,若是擔憂配置修改有語法上的錯誤,能夠這樣檢測:

sudo nginx -t

配置完後,重啓nginx:

sudo nginx -s reload

用ffmpeg來模擬推流:

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

其中,-re 表示按幀率來推;-f 爲推送時封裝的格式,對於rtmp都應該使用flv。1.mp4是當前目錄的一個視頻文件。

這時,服務器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來作其實很簡單,更多的,應該是對原理的理解。到此爲止,有緣再見吧,see you。

相關文章
相關標籤/搜索