文章內(nèi)容已更新關于FFMPEG將NSData(h264)轉換為UIImage的更新
涉及內(nèi)容
- ffmpeg從內(nèi)存讀取數(shù)據(jù)
- ffmpeg將h264轉換為mjpeg
- 保存AVFormat數(shù)據(jù)
為什么要實現(xiàn)Data(h264)到UIImage的轉換
FFMPEG是一個非常強大的多媒體開發(fā)工具翘悉。然而往衷,多數(shù)情況下线得,移動端開發(fā)者并不怎么需要他。一般來說怪蔑,通用的音視頻及圖片格式,系統(tǒng)自帶的SDK已經(jīng)足夠我們使用了。逼迫我們不得不想到這個家伙的衩藤,都是一些比較特殊的格式谦屑,比如嬌弱的avi,高傲的rtsp,我見猶憐的flv之類的……
關于FFMPEG的使用驳糯,一般都是打開一個文件,或者一個流媒體的url氢橙,這些在網(wǎng)上存在了各種成熟的解決方案酝枢,就不再贅述了。
近一年來悍手,我一直在開發(fā)并維護著一款行車記錄儀的APP帘睦,APP通過連接行車記錄儀內(nèi)置的WIFI袍患,發(fā)送特定的指令獲取行車記錄儀的錄像文件,展示封面圖或播放視頻文件官脓。
就在今天协怒,我遇到了這個特殊的問題:新的行車記錄儀,在發(fā)送錄像文件的封面圖時卑笨,直接將一幀h264的視頻流發(fā)送過來孕暇,記錄儀廠商提供的SDK將這一幀數(shù)據(jù)保存在NSData中。UIImage不能解析h264的視頻幀赤兴,所以無法展示給用戶妖滔,這就需要我們先將h264視頻幀轉碼為可以識別的格式,這時FFMPEG就進入了我的選項中桶良。
遇到的問題
- 正如上面所說座舍,F(xiàn)FMPEG的使用,一般都是打開一個文件陨帆,或者是一個流媒體的url曲秉,似乎還沒有直接將NSData作為數(shù)據(jù)源的案例。當然疲牵,這在理論上一定是可行的承二,因為不論打開一個文件,還是一個流媒體的url纲爸,本質上都是在讀取數(shù)據(jù)亥鸠,只要找到了讀取數(shù)據(jù)的方法,問題也就迎刃而解了识啦。
- 作為首次使用FFMPEG的萌新负蚊,立刻開始了面向百度的編程。一番操作猛如虎颓哮,回眸數(shù)據(jù)0-5家妆,我先后搜索了NSData轉換AVPacket,NSData寫入AVFrame冕茅,AVFormatContext從NSData加載數(shù)據(jù)揩徊,h264轉碼,F(xiàn)FMPEG如何解析NSData……
最后無一例外嵌赠,沒有任何一種方案和問題沾邊塑荒。
解決方式
首先上個廁所放松一下心情,然后泡上一杯綠茶姜挺,雙手捧著滾燙的杯子齿税,靠在椅子上緩緩思量人生的過往。從音視頻播放炊豪,到音視頻轉碼凌箕,從FFMPEG想到IJKPlayer拧篮,從嗶哩嗶哩,想到流媒體應用技術牵舱。
當我打開CSDN串绩,開始漫無目的瀏覽博客的時候,在收藏中看到了雷神芜壁。盡管雷神已離開我們四年之久礁凡,但作為“流媒體大神”,“音視頻領域的佼佼者”慧妄,我想他或許已經(jīng)寫下了解決這些問題的思路顷牌。
點開雷神的主頁,翻開博客列表塞淹,耐下心來一篇一篇閱讀起來窟蓝。一下午的時間隨著杯中的綠茶悄然流逝,就在我眼睛泛化的時候饱普,我終于發(fā)現(xiàn)了這篇ffmpeg 從內(nèi)存中讀取數(shù)據(jù)(或將數(shù)據(jù)輸出到內(nèi)存)运挫。哈哈,從內(nèi)存中讀取數(shù)據(jù)套耕,NSData不就是在內(nèi)存中的數(shù)據(jù)嗎滑臊?踏破鐵鞋無覓處,得來全不費功夫箍铲,我不由精神大振!
通讀整篇文章鬓椭,再比較一下更早的一篇颠猴,果然,雷博士已將解決方案詳細的闡述清楚了小染!
上代碼
首先附上雷神的代碼:
AVFormatContext *ic = NULL;
ic = avformat_alloc_context();
unsigned char * iobuffer=(unsigned char *)av_malloc(32768);
AVIOContext *avio =avio_alloc_context(iobuffer, 32768,0,NULL,fill_iobuffer,NULL,NULL);
ic->pb=avio;
err = avformat_open_input(&ic, "nothing", NULL, NULL);
// fill_iobuffer是一個讀取數(shù)據(jù)的回調(diào)函數(shù)(如下是雷神書寫的內(nèi)容)
FILE *fp_open;
int fill_iobuffer(void * opaque,uint8_t *buf, int buf_size){
if(!feof(fp_open)){
int true_size=fread(buf,1,buf_size,fp_open);
return true_size;
}else{
return -1;
}
}
int main(){
...
fp_open=fopen("test.h264","rb+");
AVFormatContext *ic = NULL;
ic = avformat_alloc_context();
unsigned char * iobuffer=(unsigned char *)av_malloc(32768);
AVIOContext *avio =avio_alloc_context(iobuffer, 32768,0,NULL,fill_iobuffer,NULL,NULL);
ic->pb=avio;
err = avformat_open_input(&ic, "nothing", NULL, NULL);
...//解碼
}
看過雷神的代碼翘瓮,是不是感覺豁然開朗!我們只需要把 fill_iobuffer 回調(diào)函數(shù)中的fread操作裤翩,更換為從NSData中拷貝數(shù)據(jù)资盅,那問題就完全解決了。順利讀取到NSData數(shù)據(jù)踊赠,其余的轉碼操作只需要copy即可呵扛。
下面是我修改的代碼:為了頭文件引入和書寫的方便,我選擇使用Object-C實現(xiàn)轉碼方法
聲明:我本次使用的FFMPEG版本為4.2筐带,與雷神所用的不同今穿,各位看官使用時請選擇使用自己版本的方法。此版本已經(jīng)廢棄了av_register_all等注冊編碼器的操作伦籍,所以不需要執(zhí)行注冊操作蓝晒,如果你使用的是未廢棄注冊方法的版本腮出,請一定提前執(zhí)行注冊函數(shù),否則此方法將無法執(zhí)行芝薇。
// 聲明讀取函數(shù)胚嘲,在此函數(shù)中將數(shù)據(jù)拷貝到buffer中
int read_buffer(void *opaque, uint8_t *buf, int bufsize) {
// opaque及buf,bufsize都 是從avio_alloc_context中傳入進來的
if (opaque == NULL) {
return -1;
}
// 如果opaque不為空,則拷貝opaque 到buf中
// 由于opaque可以傳入任意類型的數(shù)據(jù)洛二,所以這里的執(zhí)行方法時不唯一的
// 只要能夠將需要的數(shù)據(jù)拷貝到buffer中即可
// 如雷神的代碼馋劈,就是將數(shù)據(jù)從文件中讀取到buffer
memcpy(buf, opaque, bufsize);
return bufsize;
}
/**解析h264幀數(shù)據(jù),并將解碼后的數(shù)據(jù)保存到指定文件中
* @param data h264視頻幀數(shù)據(jù)
* @param path 解碼后圖片數(shù)據(jù)保存的文件地址
* @return 解碼結果 YES-解碼并保存成功 NO-解碼或保存失敗
*/
+ (BOOL) saveImageData:(NSData *)data toPath:(NSString *)path {
// 初始化輸入格式灭红,我們已經(jīng)分析過數(shù)據(jù)為h264視頻幀侣滩,所以直接選擇h264輸入格式
AVInputFormat *input_format = av_find_input_format("h264");
if (!input_format) {
NSLog(@"in_fmt 初始化失敗");
return NO;
}
// 申請io_buffer,用來讀取數(shù)據(jù)变擒,io_buffer的空間和data的大小相等
unsigned char *input_buffer = (unsigned char *)av_mallocz(data.length);
// 初始化io上下文君珠,準備讀取數(shù)據(jù)
AVIOContext *avio_input = avio_alloc_context(input_buffer, (int)data.length, 0, (void *)data.bytes, read_buffer, NULL, NULL);
if (!avio_input) {
NSLog(@"io 寫入失敗");
return NO;
}
// 創(chuàng)建輸入上下文
AVFormatContext *input_format_context = avformat_alloc_context();
if (!input_format_context) {
NSLog(@"ifmt_ctx 初始化失敗");
avio_context_free(&avio_input);
return NO;
}
// 將io上下文寫入format
input_format_context->pb = avio_input;
input_format_context->flags = AVFMT_FLAG_CUSTOM_IO;
// 打開數(shù)據(jù)源,此時將會執(zhí)行read_buffer函數(shù)
int err = avformat_open_input(&input_format_context, NULL, input_format, NULL);
if (err < 0) {
NSLog(@"打開數(shù)據(jù)源失敗");
avformat_close_input(&input_format_context);
avio_context_free(&avio_input);
avformat_free_context(input_format_context);
return NO;
}
// 獲取視頻信息
err = avformat_find_stream_info(input_format_context, NULL);
if (err < 0) {
NSLog(@"發(fā)現(xiàn)數(shù)據(jù)源信息失敗");
avformat_close_input(&input_format_context);
avio_context_free(&avio_input);
avformat_free_context(input_format_context);
return NO;
}
// 我們的數(shù)據(jù)流可以確定只有一幀娇斑,所以不需要循環(huán)讀取
// 你在讀取時策添,如果不確定只有一幀,則需要循環(huán)查看毫缆,
// 可以通過input_format_context->nb_streams控制終點
AVStream *stream = input_format_context->streams[0];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
NSLog(@"確定stream為視頻幀");
AVFrame *originFrame = av_frame_alloc();
// 這里是解碼函數(shù)唯竹,從AVStream中獲取AVFrame
BOOL isSuc = [self decodeImage:input_format_context codecContext:stream->codecpar frame:originFrame];
if (!isSuc) {
NSLog(@"解碼失敗");
avformat_close_input(&input_format_context);
avio_context_free(&avio_input);
avformat_free_context(input_format_context);
return NO;
}
// 由于我們要存儲的是二進制數(shù)據(jù),所以要用wb的方式打開文件
FILE *file = fopen([path UTF8String], "wb");
// 圖片數(shù)據(jù)重新編碼苦丁,并將編碼數(shù)據(jù)寫入文件中
isSuc = [self encodeImage:originFrame file:file];
// 處理完畢后浸颓,必須關閉文件
fclose(file);
if (!isSuc) {
NSLog(@"重編碼失敗");
avformat_close_input(&input_format_context);
avio_context_free(&avio_input);
avformat_free_context(input_format_context);
return NO;
}
}
NSLog(@"初始化已全部成功");
avformat_close_input(&input_format_context);
avio_context_free(&avio_input);
avformat_free_context(input_format_context);
return NO;
}
/**從上下文中獲取AVFrame
* @param formatContext 數(shù)據(jù)源上下文
* @param parameters 音視頻處理上下文的參數(shù)。由于AVStream的參數(shù)codec已標記為廢
* 棄旺拉,所以選擇此參數(shù)
* @param frame 解析后frame的值放置在此參數(shù)中
* @return BOOL
*/
+ (BOOL) decodeImage:(AVFormatContext *)formatContext codecContext:(AVCodecParameters *)parameters frame:(AVFrame *)frame {
int err = 0;
// 創(chuàng)建指定類型的解碼器
AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
if (!codec) {
NSLog(@"avcodec_find_decoder fail");
return NO;
}
// 創(chuàng)建解碼器上下文
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
NSLog(@"解碼器上下文初始化失敗");
return NO;
}
// 拷貝參數(shù)到上下文中
err = avcodec_parameters_to_context(codecContext, parameters);
if (err < 0) {
NSLog(@"解碼器上下文添加參數(shù)失敗");
return NO;
}
// 打開上下文獲取信息
err = avcodec_open2(codecContext, codec, NULL);
if (err < 0) {
NSLog(@"avcodec_open2 fail: %d", err);
}
// 創(chuàng)建數(shù)據(jù)包
AVPacket *packet = av_packet_alloc();
if (!packet) {
NSLog(@"packet 生成失敗");
return NO;
}
// 初始化數(shù)據(jù)包
av_init_packet(packet);
// 讀取frame到包中
err = av_read_frame(formatContext, packet);
if (err < 0) {
NSLog(@"讀取frame失敗");
return NO;
}
// 發(fā)送包到上下文
err = avcodec_send_packet(codecContext, packet);
if (err < 0 && err != AVERROR_EOF) {
NSLog(@"發(fā)送 packet 失敗: %d", err);
return NO;
}
// 從上下文中接收frame
err = avcodec_receive_frame(codecContext, frame);
if (err < 0) {
NSLog(@"frame 接收失敗");
return NO;
}
NSLog(@"AVFrame width=%d,height=%d", frame->width, frame->height);
return YES;
}
/** 將AVFrame轉碼产上,并將數(shù)據(jù)保存到指定文件中
* @param frame 已編碼成功的AVFrame
* @param file 用來執(zhí)行寫操作的文件管理對象
* @return BOOL
*/
+ (BOOL) encodeImage:(AVFrame *)frame file:(FILE *)file {
// 創(chuàng)建圖片編碼器
AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
if (!encoder) {
NSLog(@"圖片編碼器設置失敗");
return NO;
}
// 創(chuàng)建上下文
AVCodecContext *codec_context = avcodec_alloc_context3(encoder);
if (!codec_context) {
NSLog(@"圖片編碼上下文設置失敗");
avcodec_free_context(&codec_context);
return NO;
}
// 設置上下文參數(shù)
codec_context->width = frame->width;
codec_context->height = frame->height;
codec_context->time_base.num = 1;
codec_context->time_base.den = 1000;
codec_context->pix_fmt = AV_PIX_FMT_YUVJ420P;
codec_context->codec_id = AV_CODEC_ID_MJPEG;
codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
// 打開上下文
int err = avcodec_open2(codec_context, encoder, NULL);
if (err < 0) {
NSLog(@"參數(shù)錯誤,圖片解碼器打開失敗");
avcodec_close(codec_context);
return NO;
}
// 發(fā)送frame到上下文
err = avcodec_send_frame(codec_context, frame);
if (err < 0) {
NSLog(@"重編碼發(fā)送frame失敹旯贰:%d", err);
avcodec_close(codec_context);
return NO;
}
// 初始化接收packet
AVPacket *packet = av_packet_alloc();
av_init_packet(packet);
if (!packet) {
NSLog(@"重編碼接收packet初始化失敗");
return NO;
}
// 開始從上下文接收packet
err = avcodec_receive_packet(codec_context, packet);
if (err < 0) {
NSLog(@"重編碼接收packet失敗");
}
// 數(shù)據(jù)已接收完成
// 此時可以將數(shù)據(jù)寫入本地文件中晋涣,也可以直接轉換為NSData數(shù)據(jù)使用
// 生成的NSData可直接用于創(chuàng)建UIImage
// 為了保證現(xiàn)有項目的邏輯架構不再發(fā)生變化,我是將packet->data直接寫入本地文件
/*
NSLog(@"重編碼結果:%d", packet->size);
uint8_t *data = packet->data;
NSData *imageData = [NSData dataWithBytes:(const void *)data length:packet->size];
NSLog(@"轉碼后數(shù)據(jù):%@", imageData);
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"轉碼后圖片:%@", image);
*/
// 寫入數(shù)據(jù)
fwrite(packet->data, packet->size, 1, file);
// 刷流
fflush(file);
// 釋放packet數(shù)據(jù)
if (packet) {
av_packet_free(&packet);
}
// 關閉上下文
avcodec_close(codec_context);
// 釋放上下文數(shù)據(jù)
if (codec_context) {
avcodec_free_context(&codec_context);
}
return YES;
}