android平臺下基於ffmpeg對相機採集的NV21資料編碼為MP4視訊檔案
音視訊實踐學習
- android全平臺編譯ffmpeg以及x264與fdk-aac實踐
- ubuntu下使用nginx和nginx-rtmp-module配置直播推流伺服器
- android全平臺編譯ffmpeg合併為單個庫實踐
- android-studio使用cmake編譯ffmpeg實踐
- android全平臺下基於ffmpeg解碼MP4視訊檔案為YUV檔案
- android全平臺編譯ffmpeg支援命令列實踐
- android全平臺編譯ffmpeg視訊推流實踐
- android平臺下音訊編碼之編譯LAME庫轉碼PCM為MP3
- ubuntu平臺下編譯vlc-android視訊播放器實踐
- 圖解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的區別
- 圖解RGB565、RGB555、RGB16、RGB24、RGB32、ARGB32等格式的區別
- YUV420P、YUV420SP、NV12、NV21和RGB互相轉換並存儲為JPEG以及PNG圖片
- android全平臺編譯libyuv庫實現YUV和RGB的轉換
- android平臺下基於ffmpeg對相機採集的NV21資料編碼為MP4視訊檔案
概述
在音視訊開發中幾乎都要涉及兩個非常重要的環節:編碼
和解碼
,今天要記錄的就是其中的編碼環節,筆者這裡不打算引入OpenGLES
視訊編碼
這塊,因為android相機預設採集的原始資料基本都是NV21格式的
,並且是橫向的,因此我們通過libyuv庫
先轉換為I420(即YUV420P)格式
,然後旋轉270度
,最終進行編碼到視訊檔案中去。
環境配置
作業系統:ubuntu 16.05
ndk版本:android-ndk-r16b
ffmpeg版本:ffmpeg-3.3.8版本
ffmpeg的編譯不是本文的內容,可以參考之前的部落格android全平臺編譯ffmpeg合併為單個庫實踐
下面給出最新的流程圖:
關鍵函式說明
//註冊FFmpeg所有編解碼器。
av_register_all()
//初始化輸出碼流的AVFormatContext。
avformat_alloc_output_context2()
//開啟輸出檔案。
avio_open()
//建立輸出碼流的AVStream。
av_new_stream()
//查詢編碼器。
avcodec_find_encoder()
//分配編碼器上下文引數。
avcodec_alloc_context3()
//開啟編碼器。
avcodec_open2()
//寫檔案頭(對於某些沒有檔案頭的封裝格式,不需要此函式。比如說MPEG2TS)。
avformat_write_header()
//傳送AVFrame資料給編碼器
avcodec_send_frame()
//獲取編碼為AVPacket
avcodec_receive_packet()
//將編碼後的視訊碼流寫入檔案。
av_interleaved_write_frame()
//用於輸出編碼器中剩餘的AVPacket
flush_encoder()
//寫檔案尾(對於某些沒有檔案頭的封裝格式,不需要此函式。比如說MPEG2TS)。
av_write_trailer()
工程實踐
基於之前的專案工程,新建子工程ffmpeg-camera-encode
關於CMakeLists.txt以及build.gradle配置,這裡不再贅述
開始編碼
編寫java層
的NativeEncoder類
,用來將相機採集到的資料回傳到native層處理
。
package com.onzhou.ffmpeg.encode;
public class NativeEncoder {
static {
System.loadLibrary("native-encode");
}
public native void encodeMP4Start(String mp4Path, int width, int height);
public native void encodeMP4Stop();
public native void onPreviewFrame(byte[] yuvData, int width, int height);
}
定義native層的類
:
VideoEncoder *videoEncoder = NULL;
/**
* 編碼開始
* @param env
* @param obj
* @param jmp4Path
* @param width
* @param height
*/
void encodeMP4Start(JNIEnv *env, jobject obj, jstring jmp4Path, jint width, jint height) {
const char *mp4Path = env->GetStringUTFChars(jmp4Path, NULL);
if (videoEncoder == NULL) {
videoEncoder = new MP4Encoder();
}
videoEncoder->InitEncoder(mp4Path, width, height);
videoEncoder->EncodeStart();
env->ReleaseStringUTFChars(jmp4Path, mp4Path);
}
/**
* 編碼結束
* @param env
* @param obj
* @param jmp4Path
* @param width
* @param height
*/
void encodeMP4Stop(JNIEnv *env, jobject obj) {
if (NULL != videoEncoder) {
videoEncoder->EncodeStop();
videoEncoder = NULL;
}
}
/**
* 處理相機回撥的預覽資料
* @param env
* @param obj
* @param yuvArray
* @param width
* @param height
*/
void onPreviewFrame(JNIEnv *env, jobject obj, jbyteArray yuvArray, jint width,
jint height) {
if (NULL != videoEncoder && videoEncoder->isTransform()) {
jbyte *yuv420Buffer = env->GetByteArrayElements(yuvArray, 0);
videoEncoder->EncodeBuffer((unsigned char *) yuv420Buffer);
env->ReleaseByteArrayElements(yuvArray, yuv420Buffer, 0);
}
}
考慮到後續可能會有不同的編碼器,這裡定義了一個視訊編碼的基類:
class VideoEncoder {
protected:
bool transform = false;
public:
virtual void InitEncoder(const char *mp4Path, int width, int height) = 0;
virtual void EncodeStart() = 0;
virtual void EncodeBuffer(unsigned char *nv21Buffer) = 0;
virtual void EncodeStop() = 0;
bool isTransform();
};
接下來就是具體的MP4的編碼器實現類了
:encode_mp4.cpp
#include <libyuv.h>
#include "logger.h"
#include "encode_mp4.h"
void MP4Encoder::InitEncoder(const char *mp4Path, int width, int height) {
this->mp4Path = mp4Path;
this->width = width;
this->height = height;
}
int MP4Encoder::EncodeFrame(AVCodecContext *pCodecCtx, AVFrame *pFrame, AVPacket *avPacket) {
int ret = avcodec_send_frame(pCodecCtx, pFrame);
if (ret < 0) {
//failed to send frame for encoding
return -1;
}
while (!ret) {
ret = avcodec_receive_packet(pCodecCtx, avPacket);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
//error during encoding
return -1;
}
printf("Write frame %d, size=%d\n", avPacket->pts, avPacket->size);
avPacket->stream_index = pStream->index;
av_packet_rescale_ts(avPacket, pCodecCtx->time_base, pStream->time_base);
avPacket->pos = -1;
av_interleaved_write_frame(pFormatCtx, avPacket);
av_packet_unref(avPacket);
}
return 0;
}
void MP4Encoder::EncodeStart() {
//1. 註冊所有元件
av_register_all();
//2. 初始化輸出碼流的AVFormatContext
avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, this->mp4Path);
fmt = pFormatCtx->oformat;
//3. 開啟待輸出的視訊檔案
if (avio_open(&pFormatCtx->pb, this->mp4Path, AVIO_FLAG_READ_WRITE)) {
LOGE("open output file failed");
return;
}
//4. 初始化視訊碼流
pStream = avformat_new_stream(pFormatCtx, NULL);
if (pStream == NULL) {
LOGE("allocating output stream failed");
return;
}
//5. 尋找編碼器並開啟編碼器
pCodec = avcodec_find_encoder(fmt->video_codec);
if (!pCodec) {
LOGE("could not find encoder");
return;
}
//6. 分配編碼器並設定引數
pCodecCtx = avcodec_alloc_context3(pCodec);
pCodecCtx->codec_id = fmt->video_codec;
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
pCodecCtx->width = height;
pCodecCtx->height = width;
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;
pCodecCtx->bit_rate = 400000;
pCodecCtx->gop_size = 12;
//將AVCodecContext的成員複製到AVCodecParameters結構體
avcodec_parameters_from_context(pStream->codecpar, pCodecCtx);
av_stream_set_r_frame_rate(pStream, {1, 25});
//7. 開啟編碼器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
LOGE("open encoder fail!");
return;
}
//輸出格式資訊
av_dump_format(pFormatCtx, 0, this->mp4Path, 1);
//初始化幀
pFrame = av_frame_alloc();
pFrame->width = pCodecCtx->width;
pFrame->height = pCodecCtx->height;
pFrame->format = pCodecCtx->pix_fmt;
int bufferSize = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height, 1);
pFrameBuffer = (uint8_t *) av_malloc(bufferSize);
av_image_fill_arrays(pFrame->data, pFrame->linesize, pFrameBuffer, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, 1);
//8. 寫檔案頭
avformat_write_header(pFormatCtx, NULL);
//建立已編碼幀
av_new_packet(&avPacket, bufferSize * 3);
//標記正在轉換
this->transform = true;
}
void MP4Encoder::EncodeBuffer(unsigned char *nv21Buffer) {
uint8_t *i420_y = pFrameBuffer;
uint8_t *i420_u = pFrameBuffer + width * height;
uint8_t *i420_v = pFrameBuffer + width * height * 5 / 4;
//NV21轉I420
libyuv::ConvertToI420(nv21Buffer, width * height, i420_y, height, i420_u, height / 2, i420_v,
height / 2, 0, 0, width, height, width, height, libyuv::kRotate270,
libyuv::FOURCC_NV21);
pFrame->data[0] = i420_y;
pFrame->data[1] = i420_u;
pFrame->data[2] = i420_v;
//AVFrame PTS
pFrame->pts = index++;
//編碼資料
EncodeFrame(pCodecCtx, pFrame, &avPacket);
}
void MP4Encoder::EncodeStop() {
//標記轉換結束
this->transform = false;
int result = EncodeFrame(pCodecCtx, NULL, &avPacket);
if (result >= 0) {
//封裝檔案尾
av_write_trailer(pFormatCtx);
//釋放記憶體
if (pCodecCtx != NULL) {
avcodec_close(pCodecCtx);
avcodec_free_context(&pCodecCtx);
pCodecCtx = NULL;
}
if (pFrame != NULL) {
av_free(pFrame);
pFrame = NULL;
}
if (pFrameBuffer != NULL) {
av_free(pFrameBuffer);
pFrameBuffer = NULL;
}
if (pFormatCtx != NULL) {
avio_close(pFormatCtx->pb);
avformat_free_context(pFormatCtx);
pFormatCtx = NULL;
}
}
}
因為相機的資料都是實時的通過onPreviewFrame
傳遞到native層來
,如果我們想要編碼MP4視訊檔案
,這個過程需要先初始化好相關的編碼器和上下文環境
,然後編碼資料
,最後寫檔案尾結束,因此筆者這裡使用了一個transform欄位來標記是否需要編碼資料
。
注意:
上述的EncodeStart函式中第6個步驟
:這裡將寬和高互換的原因在於,android的前置相機採集的資料是橫向的,需要旋轉270度才能正常顯示
。
pCodecCtx->width = height;
pCodecCtx->height = width;
上述的EncodeBuffer函式開頭部分
:因為android相機預設採集的資料是NV21格式的
,並且是橫向的,因此我們需要轉換成I420(就是YUV420P)的格式,並且旋轉270度,旋轉後的寬和高要互換
,當然了,不進行這一步也是可以的,不過你採集的前置攝像頭資料
,最終顯示的就是橫向顯示的視訊檔案
。
uint8_t *i420_y = pFrameBuffer;
uint8_t *i420_u = pFrameBuffer + width * height;
uint8_t *i420_v = pFrameBuffer + width * height * 5 / 4;
//NV21轉I420
libyuv::ConvertToI420(nv21Buffer, width * height, i420_y, height, i420_u, height / 2, i420_v,
height / 2, 0, 0, width, height, width, height, libyuv::kRotate270,
libyuv::FOURCC_NV21);
所以我們在應用程式中開啟相機之後,通過onPreviewFrame
拿到相機預覽資料之後,回傳給native層處理
,通過手動點選按鈕決定開始編碼
和停止編碼
。
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
this.mPreviewSize = camera.getParameters().getPreviewSize();
if (mNativeFrame != null) {
mNativeFrame.onPreviewFrame(data, mPreviewSize.width, mPreviewSize.height);
}
}
將編碼後的檔案同步下來,即可看到相關資訊,因為我們還沒有編碼音訊,所以音訊不可用。
專案地址:ffmpeg-camera-encode
https://github.com/byhook/ffmpeg4android
參考:
https://blog.csdn.net/leixiaohua1020/article/details/25430425
https://blog.csdn.net/luotuo44/article/details/54981809
http://www.cnblogs.com/yongdaimi/p/9804699.html