FFMPEG將NSData(h264)轉換為UIImage

文章內(nèi)容已更新關于FFMPEG將NSData(h264)轉換為UIImage的更新

涉及內(nèi)容

  1. ffmpeg從內(nèi)存讀取數(shù)據(jù)
  2. ffmpeg將h264轉換為mjpeg
  3. 保存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就進入了我的選項中桶良。

遇到的問題

  1. 正如上面所說座舍,F(xiàn)FMPEG的使用,一般都是打開一個文件陨帆,或者是一個流媒體的url曲秉,似乎還沒有直接將NSData作為數(shù)據(jù)源的案例。當然疲牵,這在理論上一定是可行的承二,因為不論打開一個文件,還是一個流媒體的url纲爸,本質上都是在讀取數(shù)據(jù)亥鸠,只要找到了讀取數(shù)據(jù)的方法,問題也就迎刃而解了识啦。
  2. 作為首次使用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;
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沉桌,一起剝皮案震驚了整個濱河市谢鹊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌留凭,老刑警劉巖佃扼,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔼夜,居然都是意外死亡松嘶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門挎扰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翠订,“玉大人巢音,你說我怎么就攤上這事【〕” “怎么了官撼?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長似谁。 經(jīng)常有香客問我傲绣,道長,這世上最難降的妖魔是什么巩踏? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任秃诵,我火速辦了婚禮,結果婚禮上塞琼,老公的妹妹穿的比我還像新娘菠净。我一直安慰自己,他們只是感情好彪杉,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布毅往。 她就那樣靜靜地躺著,像睡著了一般派近。 火紅的嫁衣襯著肌膚如雪攀唯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天渴丸,我揣著相機與錄音侯嘀,去河邊找鬼。 笑死谱轨,一個胖子當著我的面吹牛戒幔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碟嘴,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼囊卜!你這毒婦竟也來了娜扇?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤栅组,失蹤者是張志新(化名)和其女友劉穎雀瓢,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玉掸,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡刃麸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了司浪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泊业。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡把沼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吁伺,到底是詐尸還是另有隱情饮睬,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布篮奄,位于F島的核電站捆愁,受9級特大地震影響,放射性物質發(fā)生泄漏窟却。R本人自食惡果不足惜昼丑,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夸赫。 院中可真熱鬧菩帝,春花似錦、人聲如沸憔足。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滓彰。三九已至控妻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揭绑,已是汗流浹背弓候。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留他匪,地道東北人菇存。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像邦蜜,于是被迫代替她去往敵國和親依鸥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345