Android NDK开发之旅34--NDK-手把手带你入门直播
前言
先来了解一下视频直播的基本架构:
直播基本架构我们需要有一个主播客户端进行音视频采集,压缩,然后通过RTMP协议进行推流,推到流媒体服务器,然后其他客户端统一从流媒体服务器引流,播放。关于这里的过程的一些细节我将会在后续文章中慢慢道来。
Linux(Ubuntu系统或者虚拟机)搭建流媒体服务器
先来了解一下俄罗斯人开发的Nginx服务器,Nginx ("engine x") 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。Nginx是由Igor Sysoev为俄罗斯访问量第二的Rambler.ru站点开发的,第一个公开版本0.1.0发布于2004年10月4日。其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。
因为Nginx服务器支持RTMP协议,因此这里我们作为流媒体服务器。
Tips:实际开发可能是多台服务器,需要高并发架构支持。反向代理:外网的请求先转发到内网,然后返回的时候再转到外网。
Nginx是一种模块化的服务器,开源,我们可以自由添加删除我们自己的模块,不同的模块使用不同的端口号,互不冲突,如下图所示:
模块化的Nginx服务器一、先下载安装 nginx 和 nginx-rtmp 编译依赖工具。
sudo apt-get install build-essential libpcre3 libpcre3-dev libssl-dev
二、创建一个工作目录,并切换到工作目录。
因为后续可能还需要进行编译等操作,最好先给目录递归赋予权限:
mkdir Live
chmod +x -R Live/
三、下载 nginx 和 nginx-rtmp源码(wget是一个从网络上自动下载文件的自由工具)
wget http://nginx.org/download/nginx-1.8.1.tar.gz
wget https://github.com/arut/nginx-rtmp-module/archive/master.zip
四、解压
如果你用的是Ubuntu系统,直接通过界面操作解压即可。
安装unzip工具,解压下载的安装包
sudo apt-get install unzip
5.解压 nginx 和 nginx-rtmp安装包
tar -zxvf nginx-1.8.1.tar.gz
unzip master.zip
其中:
-zxvf分别是四个参数
x : 从 tar 包中把文件提取出来
z : 表示 tar 包是被 gzip 压缩过的,所以解压时需要用 gunzip 解压
v : 显示详细信息
f : xxx.tar.gz : 指定被处理的文件是 xxx.tar.gz
五、添加 nginx-rtmp 模板编译到 nginx
切换到 nginx-目录
cd nginx-1.8.1
./configure --with-http_ssl_module --add-module=../nginx-rtmp-module-master
六、编译、安装nginx
编译nginx源码
make
安装需要超级权限
sudo make install
Tips:make、安装编译默认就是调用configure脚本进行编译安装,因此安装路径可以在configure找到。编译过程就是先生成目标文件.o,然后进行链接得到可执行程序。安装的过程就是把一些文件复制到系统目录里面去。
七、安装nginx init 脚本
下载init脚本到/etc/init.d/nginx目录中,其中/etc/init.d目录放是Linux进程启动的时候会执行的一些脚本
sudo wget https://raw.github.com/JasonGiedymin/nginx-init-ubuntu/master/nginx -O /etc/init.d/nginx
给目录添加执行权限
sudo chmod +x /etc/init.d/nginx
刷新一下
sudo update-rc.d nginx defaults
八、启动和停止nginx 服务,生成配置文件
sudo service nginx start
这时候在浏览器输入http://127.0.0.1/,就可以看到页面,证明服务器已经成功安装好了。然后我们停止服务器,进行后续操作。
sudo service nginx stop
Nginx安装成功之后的首页
九、安装 FFmpeg
这里FFmpeg是用于做音视频的编解码的。同时我们测试直播功能的时候,也可以通过ffplay进行测试,ffplay支持RTMP协议。
cd ffmpeg-2.6.9
编译FFmpeg
./configure --disable-yasm
安装FFmpeg(这个过程比较久,耐心等待)
sudo make install
输入下面的命令测试是否安装好:
输出安装信息
ffmpeg -v
Tips:服务器内存不足需要配置虚拟内存。
十、配置 nginx-rtmp 服务器
Nginx安装在/usr/local/nginx中,其中:
- conf是配置目录
- html是网页的目录
打开 /usr/local/nginx/conf/nginx.conf
在末尾添加如下配置:
rtmp {
server {
listen 1935;
chunk_size 4096;
application live {
live on;
record off;
exec ffmpeg -i rtmp://localhost/live/$name -threads 1 -c:v libx264 -profile:v baseline -b:v 350K -s 640x360 -f flv -c:a aac -ac 1 -strict -2 -b:a 56k rtmp://localhost/live360p/$name;
}
application live360p {
live on;
record off;
}
}
}
主要是配置RTMP协议,Nginx是一种模块化的服务器,可以自由添加功能。这里主要是配置RTMP模块的一些参数,包括端口号,视频的编解码参数、格式等等。
保存上面配置文件,然后重新启动nginx服务:
sudo service nginx restart
Tips:Nginx的安装目录可以通过在configure查找“install”关键字找到。
这里也可以看到http的配置,默认是80端口,如果有冲突的话,可以修改其他端口。不同功能使用不同端口,互不冲突。
如果你使用了防火墙,请允许端口 tcp 1935。
Windows平台下流媒体服务器搭建
由于Windows平台编译Nginx源码比较麻烦,因此我们用其他人编译好的版本即可,下载地址如下:
https://github.com/illuspas/nginx-rtmp-win32
同理,我们需要配置nginx-rtmp-win32\conf\nginx.conf文件,然后点击根目录下面的nginx.exe即可打开服务器。
流媒体服务器测试
引流有两个方法测试(将来也可以通过手机客户端进行测试):
一、服务器配置测试播放器:将Flash播放器的所有文件复制到目录:/usr/local/nginx/html(Windows是www目录)/,然后修改播放地址
播放器下载地址:
链接:http://pan.baidu.com/s/1bo9ePRp 密码:627r
方法一需要把index.html的推流IP地址改为你自己的
二、用ffplay播放RTMP直播流:
在终端输入一下命令即可:
ffplay rtmp://172.17.120.44/live/test
方法二需要把命令中的推流IP地址改为你自己的,并且最好把FFmpeg添加到环境变量
推流的测试:
目前来说也是有两种方法(将来会添加Android端引流测试):
一、使用之前创建的FFmpeg Visual项目,博客地址:
http://www.jianshu.com/p/5b7c18285667
二、使用之前创建的FFmpeg Android Studio项目,相关博客地址:
http://www.jianshu.com/p/91e07b7dc8ca
http://www.jianshu.com/p/da140cffadba
其中,核心代码如下:
JNIEXPORT void JNICALL
Java_com_nan_ffmpeg_utils_FFmpegUtils_push(JNIEnv *env, jclass type, jstring input_,
jstring output_) {
const char *input = (*env)->GetStringUTFChars(env, input_, NULL);
const char *output = (*env)->GetStringUTFChars(env, output_, NULL);
LOGI("%s\n", input);
LOGI("%s\n", output);
//变量初始化
AVFormatContext *inFmtCtx = NULL, *outFmtCtx = NULL;
int ret;
//注册组件
av_register_all();
//初始化网络
avformat_network_init();
//打开输入文件
if ((ret = avformat_open_input(&inFmtCtx, input, 0, 0)) < 0) {
LOGE("无法打开文件");
goto end;
}
//获取文件信息
if ((ret = avformat_find_stream_info(inFmtCtx, 0)) < 0) {
LOGE("无法获取文件信息");
goto end;
}
//输出的封装格式上下文,使用RTMP协议推送flv封装格式的流
avformat_alloc_output_context2(&outFmtCtx, NULL, "flv", output); //RTMP
//avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", output_str);//UDP
int i = 0;
for (; i < inFmtCtx->nb_streams; i++) {
//根据输入封装格式中的AVStream流,来创建输出封装格式的AVStream流
//解码器,解码器上下文都要一致
AVStream *in_stream = inFmtCtx->streams[i];
AVStream *out_stream = avformat_new_stream(outFmtCtx, in_stream->codec->codec);
//复制解码器上下文
ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
//全局头
out_stream->codec->codec_tag = 0;
if (outFmtCtx->oformat->flags == AVFMT_GLOBALHEADER) {
out_stream->codec->flags = CODEC_FLAG_GLOBAL_HEADER;
}
}
//打开输出的AVIOContext IO流上下文
AVOutputFormat *ofmt = outFmtCtx->oformat;
if (!(ofmt->flags & AVFMT_NOFILE)) {
ret = avio_open(&outFmtCtx->pb, output, AVIO_FLAG_WRITE);
}
//先写一个头
ret = avformat_write_header(outFmtCtx, NULL);
if (ret < 0) {
LOGE("推流发生错误\n");
goto end;
}
//获取视频流的索引位置
int videoindex = -1;
for (i = 0; i < inFmtCtx->nb_streams; i++) {
if (inFmtCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoindex = i;
break;
}
}
int frame_index = 0;
int64_t start_time = av_gettime();
AVPacket pkt;
while (1) {
AVStream *in_stream, *out_stream;
//读取AVPacket
ret = av_read_frame(inFmtCtx, &pkt);
if (ret < 0)
break;
//没有封装格式的裸流(例如H.264裸流)是不包含PTS、DTS这些参数的。在发送这种数据的时候,需要自己计算并写入AVPacket的PTS,DTS,duration等参数
//PTS:Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来
//DTS:Decode Time Stamp。DTS主要是标识读入内存中的流在什么时候开始送入解码器中进行解码
if (pkt.pts == AV_NOPTS_VALUE) {
//Write PTS
AVRational time_base1 = inFmtCtx->streams[videoindex]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration =
(double) AV_TIME_BASE / av_q2d(inFmtCtx->streams[videoindex]->r_frame_rate);
//Parameters
pkt.pts = (double) (frame_index * calc_duration) /
(double) (av_q2d(time_base1) * AV_TIME_BASE);
pkt.dts = pkt.pts;
pkt.duration = (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
}
if (pkt.stream_index == videoindex) {
//FFmpeg处理数据速度很快,瞬间就能把所有的数据发送出去,流媒体服务器是接受不了
//这里采用av_usleep()函数休眠的方式来延迟发送,延时时间根据帧率与时间基准计算得到
AVRational time_base = inFmtCtx->streams[videoindex]->time_base;
AVRational time_base_q = {1, AV_TIME_BASE};
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time) {
av_usleep(pts_time - now_time);
}
}
in_stream = inFmtCtx->streams[pkt.stream_index];
out_stream = outFmtCtx->streams[pkt.stream_index];
/* copy packet */
//Convert PTS/DTS
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
//输出进度
if (pkt.stream_index == videoindex) {
LOGI("第%d帧", frame_index);
frame_index++;
}
//推送
ret = av_interleaved_write_frame(outFmtCtx, &pkt);
if (ret < 0) {
LOGE("Error muxing packet");
break;
}
av_free_packet(&pkt);
}
//输出结尾
av_write_trailer(outFmtCtx);
end:
//释放资源
avformat_free_context(inFmtCtx);
avio_close(outFmtCtx->pb);
avformat_free_context(outFmtCtx);
(*env)->ReleaseStringUTFChars(env, input_, input);
(*env)->ReleaseStringUTFChars(env, output_, output);
}
注意:
- 如果是Android Studio项目进行测试的话,记得添加网络、SDCard访问权限。
- 如果是Visual Studio项目测试,只需要把上面的JNI方法修改成main函数即可。
- 代码先不作解释。
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
公众号:Android开发进阶我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。