您的位置:首页 > 移动开发 > Android开发

使用ffmepg实现手机直播功能(Android)

2016-08-30 16:21 639 查看
客户端的话最主要就是使用ffmpeg。

接下来要讲的就是从ffmpeg的编译开始,到编码,以及推流,到解码等过程。

ffmpeg的编译

懂英文看这里就行:ubuntu下的编译指南

ffmpeg的编译需要linux环境,我这里使用的是虚拟机(vmware+ubuntu),软件的话大家自己网上下载就行,这里需要注意的就是vmTools的安装,可参考戳这里,如果你不安装vmtools的话就不能直接拖动文件到虚拟机,就很麻烦。总之按照上面的百度经验的方法安装了之后就能直接拖动文件到虚拟机,总之就很方便。

接下来进入正文,首先下载ffmpeg的压缩包,这是ffmpeg的官网,你直接百度ffmpeg也行,进入官网之后直接下载就行(中间最明显的那个Download直接按下去,不要犹豫)。

下载之后进入,蛋疼的编译。。。把压缩包拖到ubuntu中,放在哪个目录中随便,我的话是在桌面建了一个文件夹sqq。然后把压缩包放在sqq下面。

1、打开终端(ctrl+alt+t)或者上级sqq文件夹,右键open in terminal

2、右键的同学直接就tar jxvf ffmpeg-x.x.x.tar.bz2 ,快捷键打开的同学就cd到sqq目录,注意这里解压的命令,可参考:linux下解压命令大全

上面的工作就完成了解压,其实因为是yoga的ubuntu所以解压可以直接右键 extract here,哈哈哈哈。。。。不要打我!既然用了linux系统就多用一下命令行,没毛病。

解压之后,大家就可以去看看雷神的博客了:最简单的基于FFmpeg的移动端例子:Android HelloWorld

大家可能在其他地方百度到一些在linux下编译ffmpeg的博客,但是很多都不是针对android的,直接编译的在android上无法使用,因为android是arm的cpu,而我们一般的电脑是x86之类的,所以需要交叉编译,也就需要在linux下载用ndk。具体的操作就去看雷神的博客。

1、这里你可能会遇到no working c compiler 的问题,去看下解压出来的文件夹下的config.log,一般是因为ndk的路径写错了,(遇到这个问题,可能有人会去叫你配置环境变量之类的,我告诉你不需要,只要把路径写对就行)。

2、这里简单介绍几个指令的意思

–enable-shared 这是 configure 常用的一个参数,表示启用动态库版本。

如果你要编译一个库的源代码,可以把它编译成静态库,也可以把它编译成动态库。如果你想编译成静态库,就用 –enable-shared参数;如果你想编译成静态库,就用–enable-static参数。动态库是运行时加载,静态库就相当于写在自己的代码中。

–prefix=/usr/local/ffmpeg 指定安装路径

不指定prefix,则可执行文件默认放在/usr /local/bin,库文件默认放在/usr/local/lib,配置文件默认放在/usr/local/etc。其它的资源文件放在/usr /local/share。你要卸载这个程序,要么在原来的make目录下用一次make uninstall(前提是make文件指定过uninstall),要么去上述目录里面把相关的文件一个个手工删掉。

指定prefix,直接删掉一个文件夹就够了。

–disable-yasm 编译FFMPEG时不加的话会出现 ffmpeg yasm not found, use –disable-yasm for a crippled build的错误,是因为 FFMPEG为了提高编译速度,使用了汇编指令,如果系统中没有yasm指令的话,就会出现上述的问题。所以就直接disable就可以了

正常的按照上面的步骤走下来应该就完成了编译工作。

这个时候你会很高兴的去把生成的文件从虚拟机中复制出来,很可能会出现复制不出来的问题,我的方法是直接压缩然后就可以复制了。很简单。

客户端技术要点

完成了编译之后,不能说你已经进入了音视频技术界,但是绝对可以吹逼说自己会linux,是不是很开心。不逼逼了,进入正题讲一讲,怎么使用ffmpeg做客户端的直播。

其实在讲下面的内容之前,大家最好是先有一点音视频编码的基础,这里我就不详细说了,不要问我为什么,我tmd也是个菜逼。

像我们一般做android应用,牵扯到音视频开发,无非就是调用一下系统的api,用的最多的可能是MediaRecorder,底层一点录制音频用AudioRecord,播放用AudioTrack,录制视频用Camera。用MediaRecorder的话其实就是硬编码,用硬编码其实速度应该是最快的,但是牵扯到适配的问题(不同的厂商封装的格式之类的会有区别),所以我这里还是选择使用软编码,也就是自己做编码工作,编码其实就是个压缩的过程,去掉一些不影响质量又不需要的数据,比如声音,人耳能听到的也就20hz~20khz之间,其他的就都是多余数据可以去掉。

不扯了进入正题,我这里也就按照我自己的实现流程去讲好了。不管你理解不理解。

首先android使用Camera采集原始视频数据,android摄像头采集的数据是NV21格式的,需要先转换成YUV420p格式随后使用ffmpeg编码成h264.

其次android使用AudioRecord采集音频数据,采集的音频数据是pcm格式的,可以直接使用ffmpeg编码成aac的。

(上面不懂aac、h264之类的先去补一下音视频知识,不然就不要继续看了,没意义的,兄弟!)

在很多地方看到,在完成了上面的两个步骤之后说还需要做音视频的同步,我这里音视频编码的时候使用的时间戳都是使用的系统的时间,倒是没有出现播放的时候不同步的情况,但是出现了加速的问题,单独视频或者音频倒是对的,说明还是需要做同步,我的项目中暂时还没有做,大家凑活着看下先。

c代码如下

#include <stdio.h>
#include <time.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
//#include "libswresample/swresample.h"
#include "libavutil/audio_fifo.h"
#include "libavutil/log.h"
#include "com_example_sqqfinalrecord_FfmpegHelper.h"
#include <jni.h>
#include <android/log.h>

#define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, "sqqlog", format, ##__VA_ARGS__)
#define LOGI(format, ...)  __android_log_print(ANDROID_LOG_INFO,  "sqqlog", format, ##__VA_ARGS__)

AVBitStreamFilterContext* faacbsfc = NULL;
AVFormatContext *ofmt_ctx /*,*fmt_ctx*/;
AVCodec* pCodec,*pCodec_a;
AVCodecContext* pCodecCtx,*pCodecCtx_a;
AVStream* video_st,*audio_st;
AVPacket enc_pkt,enc_pkt_a;
AVFrame *pFrameYUV,*pFrame;
//AVAudioFifo *fifo;

//int output_frame_size;
char *filedir;
int width = 600;
int height = 800;
int framecnt = 0;
int framecnt_a = 0;
int nb_samples = 0;
int yuv_width;
int yuv_height;
int y_length;
int uv_length;
int64_t start_time;

int init_video(){
//编码器的初始化
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!pCodec){
LOGE("Can not find video encoder!\n");
return -1;
}
pCodecCtx = avcodec_alloc_context3(pCodec);
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
pCodecCtx->width = width;
pCodecCtx->height = height;
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 30;
pCodecCtx->bit_rate = 800000;
pCodecCtx->gop_size = 30;
/* Some formats want stream headers to be separate. */
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
//Optional Param
pCodecCtx->max_b_frames = 3;
// Set H264 preset and tune
AVDictionary *param = 0;
av_dict_set(¶m, "preset", "ultrafast", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);

if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0){
LOGE("Failed to open video encoder!\n");
return -1;
}

//Add a new stream to output,should be called by the user before avformat_write_header() for muxing
video_st = avformat_new_stream(ofmt_ctx, pCodec);
if (video_st == NULL){
return -1;
}
video_st->time_base.num = 1;
video_st->time_base.den = 30;
video_st->codec = pCodecCtx;

return 0;
}

int init_audio(){
pCodec_a = avcodec_find_encoder(AV_CODEC_ID_AAC);
if(!pCodec_a){
LOGE("Can not find audio encoder!\n");
return -1;
}
pCodecCtx_a = avcodec_alloc_context3(pCodec_a);

pCodecCtx_a->channels = 2;

//pCodecCtx_a->channel_layout = av_get_default_channel_layout(2);
pCodecCtx_a->channel_layout = av_get_default_channel_layout(
pCodecCtx_a->channels);

pCodecCtx_a->sample_rate = 44100;//44100 8000
//pCodecCtx_a->sample_fmt = pCodec_a->sample_fmts[0];
pCodecCtx_a->sample_fmt = AV_SAMPLE_FMT_S16;
pCodecCtx_a->bit_rate = 64000;
pCodecCtx_a->time_base.num = 1;
pCodecCtx_a->time_base.den = pCodecCtx_a->sample_rate;
pCodecCtx_a->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
/* Some formats want stream headers to be separate. */
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
pCodecCtx_a->flags |= CODEC_FLAG_GLOBAL_HEADER;

if(avcodec_open2(pCodecCtx_a,pCodec_a,NULL)<0){
LOGE("Failed to open audio encoder!\n");
return -1;
}

audio_st = avformat_new_stream(ofmt_ctx,pCodec_a);
if(audio_st == NULL){
return -1;
}
audio_st->time_base.num = 1;
audio_st->time_base.den = pCodecCtx_a->sample_rate;
audio_st->codec = pCodecCtx_a;

//fifo = av_audio_fifo_alloc(pCodecCtx_a->sample_fmt,pCodecCtx_a->channels,1);

//output_frame_size = pCodecCtx_a->frame_size;
return 0;
}

/*
* Class:     com_example_sqqfinalrecord_FfmpegHelper
* Method:    init
* Signature: ([B)I
*/
JNIEXPORT jint JNICALL Java_com_example_sqqfinalrecord_FfmpegHelper_init
(JNIEnv *env, jclass cls, jbyteArray filename /*,jbyteArray path*/){

//filedir = (char*)(*env)->GetByteArrayElements(env, filename, 0);
//const char* out_path = "rtmp://10.0.3.114:1935/live/demo";
const char* out_path = "rtmp://10.0.6.114:1935/live/demo";

yuv_width=width;
yuv_height=height;
y_length=width*height;
uv_length=width*height/4;

av_register_all();
faacbsfc =  av_bitstream_filter_init("aac_adtstoasc");

//初始化输出格式上下文
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path/*filedir*/);
if(init_video()!=0){
return -1;
}

if(init_audio()!=0){
return -1;
}
//Open output URL,set before avformat_write_header() for muxing
if (avio_open(&ofmt_ctx->pb, /*filedir*/out_path, AVIO_FLAG_READ_WRITE) < 0){
LOGE("Failed to open output file!\n");
return -1;
}

//Write File Header
avformat_write_header(ofmt_ctx, NULL);

start_time = av_gettime();

return 0;
}

/*
* Class:     com_example_sqqfinalrecord_FfmpegHelper
* Method:    start
* Signature: ([B)I
*/
JNIEXPORT jint JNICALL Java_com_example_sqqfinalrecord_FfmpegHelper_start
(JNIEnv *env, jclass cls, jbyteArray yuv){
//传递进来yuv数据
int ret;
int enc_got_frame=0;
int i=0;

pFrameYUV = av_frame_alloc();
uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

jbyte* in= (jbyte*)(*env)->GetByteArrayElements(env,yuv,0);
memcpy(pFrameYUV->data[0],in,y_length);
(*env)->ReleaseByteArrayElements(env,yuv,in,0);
for(i=0;i<uv_length;i++)
{
*(pFrameYUV->data[2]+i)=*(in+y_length+i*2);
*(pFrameYUV->data[1]+i)=*(in+y_length+i*2+1);
}
/*int y_size = pCodecCtx->width * pCodecCtx->height;

//pFrameYUV->pts = count;
pFrameYUV->data[0] = in;  //y
pFrameYUV->data[1] = in+ y_size;      // U
pFrameYUV->data[2] = in+ y_size*5/4;  // V*/

pFrameYUV->format = AV_PIX_FMT_YUV420P;
pFrameYUV->width = yuv_width;
pFrameYUV->height = yuv_height;

enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);

ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);
av_frame_free(&pFrameYUV);

if (enc_got_frame == 1){
LOGI("Succeed to encode video frame: %5d\tsize:%5d\n", framecnt, enc_pkt.size);
framecnt++;
enc_pkt.stream_index = video_st->index;

//Write PTS
AVRational time_base=ofmt_ctx->streams[0]->time_base;

//表示一秒30帧
AVRational r_framerate1 = {30, 1 };
AVRational time_base_q = AV_TIME_BASE_Q;

//Duration between 2 frames (us)两帧之间的时间间隔,这里的单位是微秒
int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳

//Parameters
int64_t timett = av_gettime();
int64_t now_time = timett - start_time;
enc_pkt.pts = av_rescale_q(now_time, time_base_q, time_base);;
enc_pkt.dts=enc_pkt.pts;
enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base);

enc_pkt.pos = -1;

ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
av_free_packet(&enc_pkt);

}

return 0;
}

/*
* Class:     com_example_sqqfinalrecord_FfmpegHelper
* Method:    startAudio
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_com_example_sqqfinalrecord_FfmpegHelper_startAudio
(JNIEnv *env, jclass cls, jbyteArray au_data,jint datasize){

//传递进来pcm数据
int ret;
int enc_got_frame=0;
int i=0;

pFrame = av_frame_alloc();
pFrame->nb_samples = pCodecCtx_a->frame_size;
pFrame->format = pCodecCtx_a->sample_fmt;
pFrame->channel_layout = pCodecCtx_a->channel_layout;
pFrame->sample_rate = pCodecCtx_a->sample_rate;

int size = av_samples_get_buffer_size(NULL,pCodecCtx_a->channels,
pCodecCtx_a->frame_size,pCodecCtx_a->sample_fmt,1);

uint8_t *frame_buf = (uint8_t *)av_malloc(size*4);
avcodec_fill_audio_frame(pFrame,pCodecCtx_a->channels,pCodecCtx_a->sample_fmt,(const uint8_t *)frame_buf,size,1);

jbyte* in= (jbyte*)(*env)->GetByteArrayElements(env,au_data,0);
if(memcpy(frame_buf,in,datasize)<=0){
LOGE("Failed to read raw data!");
return -1;
}
pFrame->data[0] = frame_buf;
(*env)->ReleaseByteArrayElements(env,au_data,in,0);

enc_pkt_a.data = NULL;
enc_pkt_a.size = 0;
av_init_packet(&enc_pkt_a);
ret = avcodec_encode_audio2(pCodecCtx_a,&enc_pkt_a,pFrame, &enc_got_frame);
av_frame_free(&pFrame);

if (enc_got_frame == 1){
LOGI("Succeed to encode audio frame: %5d\tsize:%5d\t bufsize:%5d\n ", framecnt_a, enc_pkt_a.size,size);
framecnt_a++;
enc_pkt_a.stream_index = audio_st->index;
av_bitstream_filter_filter(faacbsfc, pCodecCtx_a, NULL, &enc_pkt_a.data, &enc_pkt_a.size, enc_pkt_a.data, enc_pkt_a.size, 0);

//Write PTS
AVRational time_base=ofmt_ctx->streams[audio_st->index]->time_base;

//表示一秒30帧
AVRational r_framerate1 = {pCodecCtx_a->sample_rate, 1 };
AVRational time_base_q = AV_TIME_BASE_Q;

//Duration between 2 frames (us)两帧之间的时间间隔,这里的单位是微秒
int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳

/*      enc_pkt_a.pts = av_rescale_q(nb_samples*calc_duration, time_base_q, time_base);
enc_pkt_a.dts=enc_pkt_a.pts;
enc_pkt_a.duration = av_rescale_q(calc_duration, time_base_q, time_base);*/

//Parameters
int64_t timett = av_gettime();
int64_t now_time = timett - start_time;
enc_pkt_a.pts = av_rescale_q(now_time, time_base_q, time_base);;
enc_pkt_a.dts=enc_pkt_a.pts;
enc_pkt_a.duration = av_rescale_q(calc_duration, time_base_q, time_base);

enc_pkt_a.pos = -1;
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt_a);
av_free_packet(&enc_pkt_a);
}

return 0;
}

int flush_encoder(){
int ret;
int got_frame;
AVPacket enc_pkt;
if (!(ofmt_ctx->streams[0]->codec->codec->capabilities &
CODEC_CAP_DELAY))
return 0;
while (1) {
enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);
ret = avcodec_encode_video2(ofmt_ctx->streams[0]->codec, &enc_pkt,
NULL, &got_frame);
if (ret < 0)
break;
if (!got_frame){
ret = 0;
break;
}
LOGI("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size);

//Write PTS
AVRational time_base = ofmt_ctx->streams[0]->time_base;//{ 1, 1000 };
AVRational r_framerate1 = { 60, 2 };
AVRational time_base_q = { 1, AV_TIME_BASE };
//Duration between 2 frames (us)
int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳
//Parameters
enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);
enc_pkt.dts = enc_pkt.pts;
enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base);

//转换PTS/DTS(Convert PTS/DTS)
enc_pkt.pos = -1;
framecnt++;
ofmt_ctx->duration = enc_pkt.duration * framecnt;

/* mux encoded frame */
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
if (ret < 0)
break;
}
}

int flush_encoder_a(){
int ret;
int got_frame;
AVPacket enc_pkt_a;
if (!(ofmt_ctx->streams[audio_st->index]->codec->codec->capabilities &
CODEC_CAP_DELAY))
return 0;

while(1){

enc_pkt_a.data = NULL;
enc_pkt_a.size = 0;
av_init_packet(&enc_pkt_a);
ret = avcodec_encode_audio2(ofmt_ctx->streams[audio_st->index]->codec,&enc_pkt_a,NULL,&got_frame);
av_frame_free(NULL);
if(ret<0){
break;
}

if(!got_frame){
ret = 0;
break;
}
LOGE("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt_a.size);

av_bitstream_filter_filter(faacbsfc, ofmt_ctx->streams[audio_st->index]->codec, NULL, &enc_pkt_a.data, &enc_pkt_a.size, enc_pkt_a.data, enc_pkt_a.size, 0);

//Write PTS
AVRational time_base=ofmt_ctx->streams[audio_st->index]->time_base;

//表示一秒30帧
AVRational r_framerate1 = {pCodecCtx_a->sample_rate, 1 };
AVRational time_base_q = AV_TIME_BASE_Q;

//Duration between 2 frames (us)两帧之间的时间间隔,这里的单位是微秒
int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1));  //内部时间戳

//Parameters
int64_t timett = av_gettime();
int64_t now_time = timett - start_time;
enc_pkt_a.pts = av_rescale_q(now_time, time_base_q, time_base);;
enc_pkt_a.dts=enc_pkt_a.pts;
enc_pkt_a.duration = av_rescale_q(calc_duration, time_base_q, time_base);
/*      enc_pkt_a.pts = av_rescale_q(nb_samples*calc_duration, time_base_q, time_base);
enc_pkt_a.dts=enc_pkt_a.pts;
enc_pkt_a.duration = av_rescale_q(calc_duration, time_base_q, time_base);*/

enc_pkt_a.pos = -1;

framecnt_a++;
ofmt_ctx->duration = enc_pkt_a.duration * framecnt_a;

ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt_a);
if (ret < 0)
break;
}
return 1;
}

/*
* Class:     com_example_sqqfinalrecord_FfmpegHelper
* Method:    flush
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_com_example_sqqfinalrecord_FfmpegHelper_flush
(JNIEnv *env, jclass cls){
flush_encoder();
flush_encoder_a();

//Write file trailer
av_write_trailer(ofmt_ctx);
return 0;
}

/*
* Class:     com_example_sqqfinalrecord_FfmpegHelper
* Method:    close
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_com_example_sqqfinalrecord_FfmpegHelper_close
(JNIEnv *env, jclass cls){
if (video_st)
avcodec_close(video_st->codec);
if (audio_st)
avcodec_close(audio_st->codec);

avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
return 0;
}


android的代码就不贴了,可以去我的github上看,地址:https://github.com/shanquanqiang/SqqFinalRecord

推流的话ffmpeg上面的代码中也有了,不再过多介绍。

具体的就去看源码就行。

android客户端可优化之处

上面采集音频部分使用的AudioRecord,可以做进一步优化,直接在jni中就可以采集音频数据,就不用jni和java代码传数据了,使用的是OpenSl ES,大家可以搜一下,这个我还在做。。。可参考这里直接做:Android 音频 OpenSL ES 录音

流媒体服务器的搭建

这个没什么好讲的,直接下载,地址:nginx-rtmp-win32,具体其他的细节看这里,还是比较简单的

推流端的写法

优化: 利用ffmpeg打造播放器直播观看公网rtmp1s延时极致优化

参考

我做的时候参考的一些文章(除了雷神的,这个太有名了大家都知道),分享给大家

Android音频开发(2):如何采集一帧音频

ffmpeg综合应用示例(三)——安卓手机摄像头编码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 直播