FFMPEG将NSData(h264)转换为UIImage

文章内容已更新关于FFMPEG将NSData(h264)转换为UIImage的更新

涉及内容

  1. ffmpeg从内存读取数据
  2. ffmpeg将h264转换为mjpeg
  3. 保存AVFormat数据

为什么要实现Data(h264)到UIImage的转换

FFMPEG是一个非常强大的多媒体开发工具。然而,多数情况下,移动端开发者并不怎么需要他。一般来说,通用的音视频及图片格式,系统自带的SDK已经足够我们使用了。逼迫我们不得不想到这个家伙的,都是一些比较特殊的格式,比如娇弱的avi,高傲的rtsp,我见犹怜的flv之类的……
关于FFMPEG的使用,一般都是打开一个文件,或者一个流媒体的url,这些在网上存在了各种成熟的解决方案,就不再赘述了。
近一年来,我一直在开发并维护着一款行车记录仪的APP,APP通过连接行车记录仪内置的WIFI,发送特定的指令获取行车记录仪的录像文件,展示封面图或播放视频文件。
就在今天,我遇到了这个特殊的问题:新的行车记录仪,在发送录像文件的封面图时,直接将一帧h264的视频流发送过来,记录仪厂商提供的SDK将这一帧数据保存在NSData中。UIImage不能解析h264的视频帧,所以无法展示给用户,这就需要我们先将h264视频帧转码为可以识别的格式,这时FFMPEG就进入了我的选项中。

遇到的问题

  1. 正如上面所说,FFMPEG的使用,一般都是打开一个文件,或者是一个流媒体的url,似乎还没有直接将NSData作为数据源的案例。当然,这在理论上一定是可行的,因为不论打开一个文件,还是一个流媒体的url,本质上都是在读取数据,只要找到了读取数据的方法,问题也就迎刃而解了。
  2. 作为首次使用FFMPEG的萌新,立刻开始了面向百度的编程。一番操作猛如虎,回眸数据0-5,我先后搜索了NSData转换AVPacket,NSData写入AVFrame,AVFormatContext从NSData加载数据,h264转码,FFMPEG如何解析NSData……
    最后无一例外,没有任何一种方案和问题沾边。

解决方式

首先上个厕所放松一下心情,然后泡上一杯绿茶,双手捧着滚烫的杯子,靠在椅子上缓缓思量人生的过往。从音视频播放,到音视频转码,从FFMPEG想到IJKPlayer,从哔哩哔哩,想到流媒体应用技术。
当我打开CSDN,开始漫无目的浏览博客的时候,在收藏中看到了雷神。尽管雷神已离开我们四年之久,但作为“流媒体大神”,“音视频领域的佼佼者”,我想他或许已经写下了解决这些问题的思路。
点开雷神的主页,翻开博客列表,耐下心来一篇一篇阅读起来。一下午的时间随着杯中的绿茶悄然流逝,就在我眼睛泛化的时候,我终于发现了这篇ffmpeg 从内存中读取数据(或将数据输出到内存)。哈哈,从内存中读取数据,NSData不就是在内存中的数据吗?踏破铁鞋无觅处,得来全不费功夫,我不由精神大振!
通读整篇文章,再比较一下更早的一篇,果然,雷博士已将解决方案详细的阐述清楚了!

上代码

首先附上雷神的代码:

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是一个读取数据的回调函数(如下是雷神书写的内容)
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 回调函数中的fread操作,更换为从NSData中拷贝数据,那问题就完全解决了。顺利读取到NSData数据,其余的转码操作只需要copy即可。
下面是我修改的代码:为了头文件引入和书写的方便,我选择使用Object-C实现转码方法
声明:我本次使用的FFMPEG版本为4.2,与雷神所用的不同,各位看官使用时请选择使用自己版本的方法。此版本已经废弃了av_register_all等注册编码器的操作,所以不需要执行注册操作,如果你使用的是未废弃注册方法的版本,请一定提前执行注册函数,否则此方法将无法执行。

// 声明读取函数,在此函数中将数据拷贝到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可以传入任意类型的数据,所以这里的执行方法时不唯一的
    // 只要能够将需要的数据拷贝到buffer中即可
    // 如雷神的代码,就是将数据从文件中读取到buffer
    memcpy(buf, opaque, bufsize);
    return bufsize;
}
/**解析h264帧数据,并将解码后的数据保存到指定文件中
 * @param data h264视频帧数据
 * @param path 解码后图片数据保存的文件地址
 * @return 解码结果 YES-解码并保存成功 NO-解码或保存失败
 */
+ (BOOL) saveImageData:(NSData *)data toPath:(NSString *)path {
    // 初始化输入格式,我们已经分析过数据为h264视频帧,所以直接选择h264输入格式
    AVInputFormat *input_format = av_find_input_format("h264");
    if (!input_format) {
        NSLog(@"in_fmt 初始化失败");
        return NO;
    }
    // 申请io_buffer,用来读取数据,io_buffer的空间和data的大小相等
    unsigned char *input_buffer = (unsigned char *)av_mallocz(data.length);
    // 初始化io上下文,准备读取数据
    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;
    }
    // 创建输入上下文
    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;
    // 打开数据源,此时将会执行read_buffer函数
    int err = avformat_open_input(&input_format_context, NULL, input_format, NULL);
    if (err < 0) {
        NSLog(@"打开数据源失败");
        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(@"发现数据源信息失败");
        avformat_close_input(&input_format_context);
        avio_context_free(&avio_input);
        avformat_free_context(input_format_context);
        return NO;
    }
    // 我们的数据流可以确定只有一帧,所以不需要循环读取
    // 你在读取时,如果不确定只有一帧,则需要循环查看,
    // 可以通过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();
        // 这里是解码函数,从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;
        }
        // 由于我们要存储的是二进制数据,所以要用wb的方式打开文件
        FILE *file = fopen([path UTF8String], "wb");
        // 图片数据重新编码,并将编码数据写入文件中
        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 数据源上下文
  * @param parameters 音视频处理上下文的参数。由于AVStream的参数codec已标记为废
  *  弃,所以选择此参数
  * @param frame 解析后frame的值放置在此参数中
  * @return BOOL
  */
+ (BOOL) decodeImage:(AVFormatContext *)formatContext codecContext:(AVCodecParameters *)parameters frame:(AVFrame *)frame {
    int err = 0;
    // 创建指定类型的解码器
    AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
    if (!codec) {
        NSLog(@"avcodec_find_decoder fail");
        return NO;
    }
    // 创建解码器上下文
    AVCodecContext *codecContext = avcodec_alloc_context3(codec);
    if (!codecContext) {
        NSLog(@"解码器上下文初始化失败");
        return NO;
    }
    // 拷贝参数到上下文中
    err = avcodec_parameters_to_context(codecContext, parameters);
    if (err < 0) {
        NSLog(@"解码器上下文添加参数失败");
        return NO;
    }
    // 打开上下文获取信息
    err = avcodec_open2(codecContext, codec, NULL);
    if (err < 0) {
        NSLog(@"avcodec_open2 fail: %d", err);
    }
    // 创建数据包
    AVPacket *packet = av_packet_alloc();
    if (!packet) {
        NSLog(@"packet 生成失败");
        return NO;
    }
    // 初始化数据包
    av_init_packet(packet);
    // 读取frame到包中
    err = av_read_frame(formatContext, packet);
    if (err < 0) {
        NSLog(@"读取frame失败");
        return NO;
    }
    // 发送包到上下文
    err = avcodec_send_packet(codecContext, packet);
    if (err < 0 && err != AVERROR_EOF) {
        NSLog(@"发送 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转码,并将数据保存到指定文件中
 * @param frame 已编码成功的AVFrame
 * @param file 用来执行写操作的文件管理对象
 * @return BOOL
 */
+ (BOOL) encodeImage:(AVFrame *)frame file:(FILE *)file {
    // 创建图片编码器
    AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
    if (!encoder) {
        NSLog(@"图片编码器设置失败");
        return NO;
    }
    // 创建上下文
    AVCodecContext *codec_context = avcodec_alloc_context3(encoder);
    if (!codec_context) {
        NSLog(@"图片编码上下文设置失败");
        avcodec_free_context(&codec_context);
        return NO;
    }
    // 设置上下文参数
    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(@"参数错误,图片解码器打开失败");
        avcodec_close(codec_context);
        return NO;
    }
    // 发送frame到上下文
    err = avcodec_send_frame(codec_context, frame);
    if (err < 0) {
        NSLog(@"重编码发送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失败");
    }
    // 数据已接收完成
    // 此时可以将数据写入本地文件中,也可以直接转换为NSData数据使用
    // 生成的NSData可直接用于创建UIImage
    // 为了保证现有项目的逻辑架构不再发生变化,我是将packet->data直接写入本地文件
    /*
     NSLog(@"重编码结果:%d", packet->size);
     uint8_t *data = packet->data;
     NSData *imageData = [NSData dataWithBytes:(const void *)data length:packet->size];
     NSLog(@"转码后数据:%@", imageData);
     UIImage *image = [UIImage imageWithData:imageData];
     NSLog(@"转码后图片:%@", image);
     */
    // 写入数据
    fwrite(packet->data, packet->size, 1, file);
    // 刷流
    fflush(file);
    // 释放packet数据
    if (packet) {
        av_packet_free(&packet);
    }
    // 关闭上下文
    avcodec_close(codec_context);
    // 释放上下文数据
    if (codec_context) {
        avcodec_free_context(&codec_context);
    }
    return YES;
}

你可能感兴趣的:(FFMPEG将NSData(h264)转换为UIImage)