簡(jiǎn)介
前面講到了在Android平臺(tái)下使用FFmpeg進(jìn)行RTMP推流(視頻文件推流),里面主要是介紹如何解析視頻文件并進(jìn)行推流存皂,今天要給大家介紹如何在Android平臺(tái)下獲取采集的圖像软族,并進(jìn)行編碼推流画恰。同時(shí)項(xiàng)目工程也是在之前的代碼基礎(chǔ)上新增功能。源碼倉(cāng)庫(kù)地址FFmpegSample肛捍,這一節(jié)對(duì)應(yīng)的代碼版本是v1.2。大家注意不要下載錯(cuò)了版本之众。主要涉及的代碼拙毫。
建議:這套代碼和講解中,有些地方我也還沒研究透徹棺禾,但這個(gè)不影響我們要實(shí)現(xiàn)的功能缀蹄,我之前也特別糾結(jié)一些細(xì)節(jié),花了很多的時(shí)間。其實(shí)學(xué)習(xí)一門技術(shù)和框架是一個(gè)慢慢深入的過程缺前,剛開始我們先跑起來蛀醉,再深入,否則如果你還沒入門衅码,就開始糾結(jié)一些細(xì)節(jié)參數(shù)拯刁,然后又發(fā)現(xiàn)網(wǎng)上很難找到答案,那你的自信心就會(huì)受到打擊逝段,這也是我自己的體驗(yàn)垛玻,和大家分享一下。等到我們?cè)絹碓绞煜Fmpeg和一些技術(shù)奶躯,那么之前的問題都會(huì)迎刃而解
這套代碼我在4.4.2上運(yùn)行時(shí)沒問題的帚桩。所以如果有同學(xué)在5.0以上,如果涉及動(dòng)態(tài)權(quán)限問題嘹黔,大家加上即可朗儒。學(xué)習(xí)本章之前最好先看之前的文章,這里是一套連貫的教程
- RTMP服務(wù)器搭建(crtmpserver和nginx)
- 音視頻編碼相關(guān)名詞詳解
- 基于FFmpeg進(jìn)行RTMP推流(一)
- 基于FFmpeg進(jìn)行RTMP推流(二)
- Linux下FFmpeg編譯以及Android平臺(tái)下使用
- Android平臺(tái)下使用FFmpeg進(jìn)行RTMP推流(視頻文件推流)
打開攝像頭并設(shè)置參數(shù)
具體代碼查看CameraActivity.java
private Camera getCamera() {
Camera camera;
try {
//打開相機(jī)参淹,默認(rèn)為后置醉锄,可以根據(jù)攝像頭ID來指定打開前置還是后置
camera = Camera.open(1);
if (camera != null && !isPreview) {
try {
Camera.Parameters parameters = camera.getParameters();
//對(duì)拍照參數(shù)進(jìn)行設(shè)置
for (Camera.Size size : parameters.getSupportedPictureSizes()) {
LogUtils.d(size.width + " " + size.height);
}
LogUtils.d("============");
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
LogUtils.d(size.width + " " + size.height);
}
parameters.setPreviewSize(screenWidth, screenHeight); // 設(shè)置預(yù)覽照片的大小
parameters.setPreviewFpsRange(30000, 30000);
parameters.setPictureFormat(ImageFormat.NV21); // 設(shè)置圖片格式
parameters.setPictureSize(screenWidth, screenHeight); // 設(shè)置照片的大小
camera.setParameters(parameters);
//指定使用哪個(gè)SurfaceView來顯示預(yù)覽圖片
camera.setPreviewDisplay(sv.getHolder()); // 通過SurfaceView顯示取景畫面
camera.setPreviewCallback(new StreamIt()); // 設(shè)置回調(diào)的類
camera.startPreview(); // 開始預(yù)覽
//Camera.takePicture()方法進(jìn)行拍照
camera.autoFocus(null); // 自動(dòng)對(duì)焦
} catch (Exception e) {
e.printStackTrace();
}
isPreview = true;
}
} catch (Exception e) {
camera = null;
e.printStackTrace();
Toast.makeText(this, "無法獲取前置攝像頭", Toast.LENGTH_LONG);
}
return camera;
}
Camera.open(int cameraId)
這里是創(chuàng)建一個(gè)Camera對(duì)象對(duì)應(yīng)具體的硬件攝像頭,如果攝像頭已經(jīng)被其他app打開浙值,就會(huì)拋出RuntimeException異常恳不。
cameraId是camera的Id。我們可以通過getNumberOfCameras()
獲取攝像頭的數(shù)量开呐,那id的范圍就是0~(getNumberOfCameras()-1)烟勋。一般情況下傳0就直接獲取到后置攝像頭,1就獲取到前置攝像頭筐付。當(dāng)然有些設(shè)備可能有些不同卵惦。
Camera.Parameters
這個(gè)類用于存儲(chǔ)和設(shè)置攝像頭的參數(shù)信息,當(dāng)然Camera有很多默認(rèn)參數(shù)瓦戚,所以我們只需要通過camera.getParameters()
獲取該對(duì)象沮尿,然后并設(shè)置我們需要修改的屬性即可。我們看一些常見的屬性設(shè)置
-
setPreviewSize
設(shè)置預(yù)覽圖像的大小
-
setPictureSize
設(shè)置照片的大小
-
setPreviewFpsRange
設(shè)置Fps较解,幀率畜疾。但我發(fā)現(xiàn)并沒有什么卵用。每次修改后采集的頻率還是沒變印衔,擦啡捶!
-
setPictureFormat
設(shè)置采集到圖像的像素格式,Android推薦NV21奸焙。那我們就用這個(gè)瞎暑,這個(gè)參數(shù)很重要彤敛,后面編碼我們會(huì)詳細(xì)講解。
最后不要忘了調(diào)用setParameters
進(jìn)行設(shè)置了赌。否則你就白忙活了墨榄。
預(yù)覽和獲取采集圖像數(shù)據(jù)
預(yù)覽
第一個(gè)問題,用什么來承載預(yù)覽圖像揍拆。Android提供了SurfaceView和GLSurfaceView渠概。這里為了方便大家上手茶凳,我們先選擇使用SurfaceView稍微簡(jiǎn)單一點(diǎn)嫂拴,對(duì)SurfaceView大家不熟的可以查找相關(guān)資料。接下來就是使用SurfaceView
-
布局中添加SurfaceView贮喧。這里我做了一個(gè)繼承類
MySurfaceView
<com.wangheart.rtmpfile.MySurfaceView android:id="@+id/sv" android:layout_width="match_parent" android:layout_height="match_parent" />
-
獲取SurfaceHolder并設(shè)置回調(diào)
SurfaceView里有一個(gè)SurfaceHolder用來控制SurfaceView的相關(guān)操作筒狠。比如設(shè)置SurfaceView的Callback,用來監(jiān)聽SurfaceView的創(chuàng)建箱沦,變化和銷毀辩恼。這里只需要實(shí)現(xiàn)
SurfaceHolder.Callback
的接口@Override public void surfaceCreated(SurfaceHolder holder) { setStartPreview(mCamera, mHolder); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { setStartPreview(mCamera, mHolder); } @Override public void surfaceDestroyed(SurfaceHolder holder) { releaseCamera(); }
然后設(shè)置到SurfaceHolder中
mHolder.addCallback(this)
-
SurfaceView與Camera關(guān)聯(lián)
因?yàn)槲覀円v圖像預(yù)覽到SurfaceView上,那么必定有地方存在關(guān)聯(lián)谓形。這里很簡(jiǎn)單灶伊,就是調(diào)用Camera的
setPreviewDisplay
,將SurfaceView的SurfaceHolder設(shè)置進(jìn)去即可寒跳。 -
開始預(yù)覽
直接調(diào)用camera的
startPreview
開始進(jìn)行預(yù)覽聘萨。那么什么時(shí)候調(diào)用這個(gè)方法呢?- 設(shè)置一個(gè)按鈕童太,點(diǎn)擊之后我們就調(diào)用這個(gè)方法進(jìn)行預(yù)覽
- SurfaceView的創(chuàng)建回調(diào)方法中
surfaceCreated
中進(jìn)行調(diào)用米辐,因?yàn)閳D像要預(yù)覽到SurfaceView中,所以必須得SurfaceView已成功創(chuàng)建书释。
獲取采集數(shù)據(jù)
前面我們已經(jīng)知道怎么預(yù)覽圖像了翘贮。接下來就是獲取采集數(shù)據(jù)。這個(gè)也很容易就是調(diào)用Camera的setPreviewCallback
設(shè)置預(yù)覽回調(diào)爆惧。我們實(shí)現(xiàn)一下這個(gè)接口
public class StreamIt implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
long endTime = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
encodeTime = System.currentTimeMillis();
FFmpegHandle.getInstance().onFrameCallback(data);
LogUtils.w("編碼第:" + (encodeCount++) + "幀狸页,耗時(shí):" + (System.currentTimeMillis() - encodeTime));
}
});
LogUtils.d("采集第:" + (++count) + "幀,距上一幀間隔時(shí)間:"
+ (endTime - previewTime) + " " + Thread.currentThread().getName());
previewTime = endTime;
}
}
很簡(jiǎn)單扯再,這個(gè)接口就是講原始數(shù)據(jù)進(jìn)行回調(diào)肴捉。這里大家也看到了,我把采集的時(shí)間間隔和編碼消耗的時(shí)間打印出來了叔收。
編碼
前面把基礎(chǔ)的如何采集攝像頭數(shù)據(jù)講了一下齿穗,接下來就是進(jìn)行視頻數(shù)據(jù)編碼。
開啟線程編碼
因?yàn)榫幋a畢竟會(huì)比較耗時(shí)饺律,所以我們需要放到線程中處理窃页,這里我用了一個(gè)單線程池,避免每次開啟和銷毀線程產(chǎn)生的開銷。為了保證圖片按順序編碼脖卖,這里使用單線程池乒省。
ExecutorService executor = Executors.newSingleThreadExecutor();
獲取到采集的數(shù)據(jù)后就可以丟進(jìn)去進(jìn)行編碼
executor.execute(new Runnable() {
@Override
public void run() {
encodeTime = System.currentTimeMillis();
FFmpegHandle.getInstance().onFrameCallback(data);
LogUtils.w("編碼第:" + (encodeCount++) + "幀,耗時(shí):" + (System.currentTimeMillis() - encodeTime));
}
});
這里大家也看出來了調(diào)用FFmpegHandle.getInstance().onFrameCallback(data);
進(jìn)行編碼畦木。
初始化編碼相關(guān)操作
這里我們使用的是FFmpeg,所以在編碼前我們會(huì)先做一些初始化以及參數(shù)設(shè)置工作袖扛,所以我們?cè)贔FmpegHandle中增加一個(gè)native方法public native int initVideo(String url);
對(duì)應(yīng)到C++層,也就是ffmpeg_handle.cpp
AVFormatContext *ofmt_ctx;
AVStream *video_st;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVPacket enc_pkt;
AVFrame *pFrameYUV;
int count = 0;
int yuv_width;
int yuv_height;
int y_length;
int uv_length;
int width = 480;
int height = 320;
int fps = 15;
/**
* 初始化
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_initVideo(JNIEnv *env, jobject instance,
jstring url_) {
const char *out_path = env->GetStringUTFChars(url_, 0);
logd(out_path);
//計(jì)算yuv數(shù)據(jù)的長(zhǎng)度
yuv_width = width;
yuv_height = height;
y_length = width * height;
uv_length = width * height / 4;
av_register_all();
//output initialize
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path);
//output encoder initialize
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!pCodec) {
loge("Can not find encoder!\n");
return -1;
}
pCodecCtx = avcodec_alloc_context3(pCodec);
//編碼器的ID號(hào)十籍,這里為264編碼器蛆封,可以根據(jù)video_st里的codecID 參數(shù)賦值
pCodecCtx->codec_id = pCodec->id;
//像素的格式,也就是說采用什么樣的色彩空間來表明一個(gè)像素點(diǎn)
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
//編碼器編碼的數(shù)據(jù)類型
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
//編碼目標(biāo)的視頻幀大小勾栗,以像素為單位
pCodecCtx->width = width;
pCodecCtx->height = height;
pCodecCtx->framerate = (AVRational) {fps, 1};
//幀率的基本單位惨篱,我們用分?jǐn)?shù)來表示,
pCodecCtx->time_base = (AVRational) {1, fps};
//目標(biāo)的碼率围俘,即采樣的碼率砸讳;顯然,采樣碼率越大界牡,視頻大小越大
pCodecCtx->bit_rate = 400000;
//固定允許的碼率誤差簿寂,數(shù)值越大,視頻越小
// pCodecCtx->bit_rate_tolerance = 4000000;
pCodecCtx->gop_size = 50;
/* Some formats want stream headers to be separate. */
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
//H264 codec param
// pCodecCtx->me_range = 16;
//pCodecCtx->max_qdiff = 4;
pCodecCtx->qcompress = 0.6;
//最大和最小量化系數(shù)
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
//Optional Param
//兩個(gè)非B幀之間允許出現(xiàn)多少個(gè)B幀數(shù)
//設(shè)置0表示不使用B幀
//b 幀越多宿亡,圖片越小
pCodecCtx->max_b_frames = 0;
// Set H264 preset and tune
AVDictionary *param = 0;
//H.264
if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
// av_dict_set(¶m, "preset", "slow", 0);
/**
* 這個(gè)非常重要常遂,如果不設(shè)置延時(shí)非常的大
* ultrafast,superfast, veryfast, faster, fast, medium
* slow, slower, veryslow, placebo. 這是x264編碼速度的選項(xiàng)
*/
av_dict_set(¶m, "preset", "superfast", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);
}
if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0) {
loge("Failed to open 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 = fps;
// video_st->codec = pCodecCtx;
video_st->codecpar->codec_tag = 0;
avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);
//Open output URL,set before avformat_write_header() for muxing
if (avio_open(&ofmt_ctx->pb, 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);
return 0;
}
首先需要聲明一些全局的變量,方便后面編碼使用AVFormatContext她混、AVStream等烈钞。
-
進(jìn)行FFmpeg初始化
這個(gè)和之前講到的一樣av_register_all()
-
創(chuàng)建輸出格式上下文
avformat_alloc_output_context2
這些之前都講到過,就不錯(cuò)累述 -
獲取編碼器
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264)
獲取編碼器坤按。這里我們使用H264進(jìn)行視頻編碼毯欣。如果編碼器獲取失敗就沒有下文了 -
創(chuàng)建編碼器上下文
pCodecCtx = avcodec_alloc_context3(pCodec)
-
設(shè)置編碼器參數(shù)
這些參數(shù)我個(gè)參數(shù)的設(shè)置上都有加注釋,大家查看即可臭脓。同時(shí)參考音視頻編碼相關(guān)名詞詳解酗钞。這講幾個(gè)主要的參數(shù)
- pix_fmt 像素的格式這里我們使用的AV_PIX_FMT_YUV420P,也就是YUV平面格式来累,三個(gè)平面分別存放Y砚作、U、V數(shù)據(jù)嘹锁。
- codec_type 編碼器編碼的數(shù)據(jù)類型
- framerate 幀率
- time_base 幀率的基本單位
- gop_size GOP的大小
-
AVDictionary設(shè)置
-
前面講了一些常規(guī)參數(shù)的設(shè)置葫录,這里還有一些重要參數(shù)設(shè)置
if (pCodecCtx->codec_id == AV_CODEC_ID_H264) { // av_dict_set(¶m, "preset", "slow", 0); /** * 這個(gè)非常重要,如果不設(shè)置延時(shí)非常的大 * ultrafast,superfast, veryfast, faster, fast, medium * slow, slower, veryslow, placebo. 這是x264編碼速度的選項(xiàng) */ av_dict_set(¶m, "preset", "superfast", 0); av_dict_set(¶m, "tune", "zerolatency", 0); }
一定要注意
preset
這個(gè)參數(shù)的設(shè)置领猾。否則你會(huì)發(fā)現(xiàn)你編碼的延遲特別大米同,網(wǎng)上有許多朋友遇到這個(gè)問題骇扇。
-
-
使用給定的編碼器和參數(shù)初始化編碼上下文
avcodec_open2(pCodecCtx, pCodec, ¶m)
-
創(chuàng)建視頻流
video_st = avformat_new_stream(ofmt_ctx, pCodec)
這個(gè)就和之前的推文件流一樣了。創(chuàng)建并設(shè)置相關(guān)的參數(shù) -
打開輸出上下文
avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE)
-
寫入輸出頭信息
avformat_write_header(ofmt_ctx, NULL)
開始編碼
在獲取到采集的時(shí)候后我們通過線程池調(diào)用執(zhí)行了FFmpegHandle.getInstance().onFrameCallback(mData);
接下來我們重點(diǎn)看到onFrameCallback方法面粮。當(dāng)然這也是一個(gè)navive方法少孝。我們看到c++層的實(shí)現(xiàn)。
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_onFrameCallback(JNIEnv *env, jobject instance,
jbyteArray buffer_) {
// startTime = av_gettime();
jbyte *in = env->GetByteArrayElements(buffer_, NULL);
int ret = 0;
pFrameYUV = av_frame_alloc();
int picture_size = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height, 1);
uint8_t *buffers = (uint8_t *) av_malloc(picture_size);
//將buffers的地址賦給AVFrame中的圖像數(shù)據(jù)熬苍,根據(jù)像素格式判斷有幾個(gè)數(shù)據(jù)指針
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffers, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, 1);
//安卓攝像頭數(shù)據(jù)為NV21格式稍走,此處將其轉(zhuǎn)換為YUV420P格式
////N21 0~width * height是Y分量, width*height~ width*height*3/2是VU交替存儲(chǔ)
//復(fù)制Y分量的數(shù)據(jù)
memcpy(pFrameYUV->data[0], in, y_length); //Y
pFrameYUV->pts = count;
for (int i = 0; i < uv_length; i++) {
//將v數(shù)據(jù)存到第三個(gè)平面
*(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
//將U數(shù)據(jù)存到第二個(gè)平面
*(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
}
pFrameYUV->format = AV_PIX_FMT_YUV420P;
pFrameYUV->width = yuv_width;
pFrameYUV->height = yuv_height;
//例如對(duì)于H.264來說柴底。1個(gè)AVPacket的data通常對(duì)應(yīng)一個(gè)NAL
//初始化AVPacket
av_init_packet(&enc_pkt);
// __android_log_print(ANDROID_LOG_WARN, "eric", "編碼前時(shí)間:%lld",
// (long long) ((av_gettime() - startTime) / 1000));
//開始編碼YUV數(shù)據(jù)
ret = avcodec_send_frame(pCodecCtx, pFrameYUV);
if (ret != 0) {
logw("avcodec_send_frame error");
return -1;
}
//獲取編碼后的數(shù)據(jù)
ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);
// __android_log_print(ANDROID_LOG_WARN, "eric", "編碼時(shí)間:%lld",
// (long long) ((av_gettime() - startTime) / 1000));
//是否編碼前的YUV數(shù)據(jù)
av_frame_free(&pFrameYUV);
if (ret != 0 || enc_pkt.size <= 0) {
loge("avcodec_receive_packet error");
avError(ret);
return -2;
}
enc_pkt.stream_index = video_st->index;
AVRational time_base = ofmt_ctx->streams[0]->time_base;//{ 1, 1000 };
enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);
enc_pkt.dts = enc_pkt.pts;
enc_pkt.duration = (video_st->time_base.den) / ((video_st->time_base.num) * fps);
__android_log_print(ANDROID_LOG_WARN, "eric",
"index:%d,pts:%lld,dts:%lld,duration:%lld,time_base:%d,%d",
count,
(long long) enc_pkt.pts,
(long long) enc_pkt.dts,
(long long) enc_pkt.duration,
time_base.num, time_base.den);
enc_pkt.pos = -1;
// AVRational time_base_q = {1, AV_TIME_BASE};
// //計(jì)算視頻播放時(shí)間
// int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
// //計(jì)算實(shí)際視頻的播放時(shí)間
// if (count == 0) {
// startTime = av_gettime();
// }
// int64_t now_time = av_gettime() - startTime;
// __android_log_print(ANDROID_LOG_WARN, "eric", "delt time :%lld", (pts_time - now_time));
// if (pts_time > now_time) {
// //睡眠一段時(shí)間(目的是讓當(dāng)前視頻記錄的播放時(shí)間與實(shí)際時(shí)間同步)
// av_usleep((unsigned int) (pts_time - now_time));
// }
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
if (ret != 0) {
loge("av_interleaved_write_frame failed");
}
count++;
env->ReleaseByteArrayElements(buffer_, in, 0);
return 0;
}
像素格式轉(zhuǎn)換
在設(shè)置攝像頭采集的圖像格式時(shí)候我們?cè)O(shè)置的是NV21婿脸。而我們編碼需要的是AV_PIX_FMT_YUV420P。所以這需要進(jìn)行轉(zhuǎn)換似枕。我們先看下兩個(gè)像素格式的區(qū)別
-
NV21
是一個(gè)YUV 4:2:0數(shù)據(jù)盖淡,應(yīng)該說是平面和打包混合存儲(chǔ)年柠。有兩個(gè)平面凿歼,第一個(gè)平面存放Y數(shù)據(jù)第二個(gè)平面VU數(shù)據(jù)交替存儲(chǔ)
-
AV_PIX_FMT_YUV420P
那不用說是我們熟悉的YUV 4:2:0的像素?cái)?shù)據(jù),它是純平面存儲(chǔ)冗恨〈疸荆總共三個(gè)平面,分別存放掀抹,Y虐拓、U、V數(shù)據(jù)傲武。
我們還需要了解蓉驹,以為采集的數(shù)據(jù)YUV是4:2:0。所以Y:(U或V)的大小是4:1揪利。而U:V是1:1态兴。所以當(dāng)圖像寬是width,高是height時(shí)疟位,Y分量的大小就是width×heitht,而U是width×heitht/4,V也是U是width×heitht/4瞻润。
知道上面的存儲(chǔ)格式后我們就知道怎么轉(zhuǎn)換了。
首先復(fù)制Y分量的數(shù)據(jù)
memcpy(pFrameYUV->data[0], in, y_length);
然后遍歷VU數(shù)據(jù)并存放到data[1]和data[2]平面中
for (int i = 0; i < uv_length; i++) {
//將v數(shù)據(jù)存到第三個(gè)平面
*(pFrameYUV->data[2] + i) = *(in + y_length + i * 2);
//將U數(shù)據(jù)存到第二個(gè)平面
*(pFrameYUV->data[1] + i) = *(in + y_length + i * 2 + 1);
}
H264編碼
首先我們需要了解兩個(gè)數(shù)據(jù)結(jié)構(gòu)AVFrame甜刻、AVPacket
AVFrame存放的是原始數(shù)據(jù)绍撞、AVPacket存放的是編碼后的數(shù)據(jù)。所以前面格式轉(zhuǎn)換也是將數(shù)據(jù)存放到pFrameYUV中得院。
-
初始化AVPacket
av_init_packet(&enc_pkt);
-
開始編碼
ret = avcodec_send_frame(pCodecCtx, pFrameYUV);
-
接受編碼后的數(shù)據(jù)
ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);
是不是很簡(jiǎn)單傻铣,這樣編碼后的數(shù)據(jù)就存到了enc_pkt中。到這里只是完成的編碼工作祥绞,接下來還有一些參數(shù)需要設(shè)置
PTS非洲、DTS阱驾、duration
PTS是顯示時(shí)間戳,DTS解碼時(shí)間戳怪蔑,duration是當(dāng)當(dāng)前幀和下一幀的時(shí)間間隔,里覆。這個(gè)很重要,不然播放會(huì)出現(xiàn)問題缆瓣。
首先我們要知道時(shí)間基數(shù)喧枷,也就是你按什么時(shí)間單位算。
AVRational time_base = ofmt_ctx->streams[0]->time_base;
這里的值是{1,1000}弓坞,這應(yīng)該就是毫秒隧甚。知道時(shí)間基礎(chǔ),同時(shí)根據(jù)fps我們就知道每一幀的時(shí)間間隔是1000/fps渡冻。
那第n幀的pts就是n×(1000/fps)戚扳。對(duì)應(yīng)代碼
enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);
dts和pts設(shè)置成一樣,標(biāo)示解碼時(shí)間和顯示時(shí)間一致族吻。至于為什么帽借,其實(shí)我也沒太明白,如果不一致該怎么計(jì)算超歌?我們先不管砍艾,以后再研究。
duration那就容易了巍举,就是(video_st->time_base.den) / ((video_st->time_base.num) * fps)
這里的三個(gè)參數(shù)這是可能不太準(zhǔn)確脆荷,但我們先這樣,想把功能跑起來再說懊悯,不然我們糾結(jié)這些就永無止境了蜓谋。后面等我們深入了,也就會(huì)明白炭分。
輸出視頻數(shù)據(jù)
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
釋放資源
在結(jié)束編碼推流后我們也需要釋放相關(guān)的資源
if (video_st)
avcodec_close(video_st->codec);
if (ofmt_ctx) {
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
ofmt_ctx = NULL;
}
return 0;