使用ffmepg實現手機直播功能(Android)
客戶端的話最主要就是使用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目錄,注意這裡解壓的命令,可參考:
上面的工作就完成了解壓,其實因為是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客戶端可優化之處
上面採集音訊部分使用的AudioRecord,可以做進一步優化,直接在jni中就可以採集音訊資料,就不用jni和java程式碼傳資料了,使用的是OpenSl ES,大家可以搜一下,這個我還在做。。。可參考這裡直接做:Android 音訊 OpenSL ES 錄音
流媒體伺服器的搭建
這個沒什麼好講的,直接下載,地址:nginx-rtmp-win32,具體其他的細節看這裡,還是比較簡單的