FFMPEG将NSData(h264)转换为UIImage
在实现了上一篇FFMPEG将NSData(h264)转换为UIImage的功能后,我找到了reading_io.c的文件,再次阅读了FFMPEG示例代码,修改了read_buffer的实现方式,解决了read_buffer函数重复执行的问题,同时增加了解编码器及上下文内存的回收操作,解决了大量执行转换操作时内存暴增的问题。
- 新增buffer_data结构体的定义(FFMPEG示例中可以直接找到)。我在使用时,将size参数修改为 flag标记,并在执行memcpy操作时,将flag标记为1。
// FFMPEG示例
struct buffer_data {
uint8_t *ptr; // 传入bytes
int size; // 传入size
};
// 我在适配项目后的改变
struct buffer_data {
uint8_t *ptr; // 传入bytes
int flag; // 设置memcpy标记 0-未拷贝(默认) 1-已拷贝
};
- 重新定义read_buffer函数,opaque将传入buffer_data结构体,不再直接传入bytes。并在执行memcpy前检测到flag,当flag==1时,return AVERROR_EOF;
// FFMPEG示例
int read_buffer(void *opaque, uint8_t *buf, int bufsize) {
// 拷贝opaque 到buf
struct buffer_data *bd = (struct buffer_data *)opaque;
bufsize = FFMIN(bufsize, (int)bd->size);
if (!bufsize) {
return AVERROR_EOF;
}
printf("拷贝数据:%d\n", bufsize);
memcpy(buf, bd->ptr, bufsize);
bd->ptr += bufsize;
bd->size -= bufsize;
return bufsize;
}
// 我适配项目后的函数
int read_buffer(void *opaque, uint8_t *buf, int bufsize) {
// 拷贝opaque 到buf
struct buffer_data *bd = (struct buffer_data *)opaque;
if (bd->flag > 0) {
return AVERROR_EOF;
}
printf("拷贝数据:%d\n", bufsize);
memcpy(buf, bd->ptr, bufsize);
bd->flag += 1;
return bufsize;
}
- 修改编解码器及上下文的回收操作
+ (BOOL) saveImageData:(NSData *)data toPath:(NSString *)path {
// 读取io操作
AVInputFormat *input_format = av_find_input_format("h264");
// 事实上,在实际项目中,我们不需要判断input_format是否发现成功。
// 因为开发者理应清楚自己的项目是否支持该输入类型
if (!input_format) {
return NO;
}
// 申请buffer所需要的内存
unsigned char *input_buffer = (unsigned char *)av_mallocz(data.length);
// 初始化结构体buffer_data,并将NSData的信息存入到结构体中
struct buffer_data bd = { 0 };
bd.ptr = (uint8_t *)data.bytes;
bd.size = (int)data.length;
// 初始化io上下文,并将结构体bd作为opaque传入
/* 关于avio_alloc_context的参数,网络上已经存在很清晰的描述
* 我们也不需要特别分析此处的内容
*/
AVIOContext *avio_input = avio_alloc_context(input_buffer, (int)data.length, 0, &bd, &read_buffer, NULL, NULL);
// 初始化AVFormatContext,并将io赋值为pb
AVFormatContext *input_format_context = avformat_alloc_context();
input_format_context->pb = avio_input;
input_format_context->flags = AVFMT_FLAG_CUSTOM_IO;
// 打开输入上下文
int err = avformat_open_input(&input_format_context, NULL, input_format, NULL);
if (err < 0) {
goto end;
}
// 获取输入源信息
err = avformat_find_stream_info(input_format_context, NULL);
if (err < 0) {
goto end;
}
// 本次的数据源只有一帧h264数据,可以确定数据类型,因此取消了数据类型的判断操作
AVStream *stream = input_format_context->streams[0];
AVFrame *originFrame = av_frame_alloc();
BOOL isSuc = [self decodeImage:input_format_context codecContext:stream->codecpar frame:originFrame];
if (!isSuc) {
av_frame_free(&originFrame);
err = -1;
goto end;
}
// 由于我们要存储的是二进制数据,所以要用wb的方式打开文件
FILE *file = fopen([path UTF8String], "wb");
// 图片数据重新编码,并将编码数据写入文件中
isSuc = [self encodeImage:originFrame file:file];
// 处理完毕后,必须关闭文件
fclose(file);
// 释放frame
av_frame_free(&originFrame);
if (!isSuc) {
err = -1;
goto end;
}
end:
if (input_format_context) {
avformat_close_input(&input_format_context);
input_format_context = nil;
}
if (avio_input) {
av_free(avio_input->buffer);
avio_context_free(&avio_input);
avio_input = NULL;
}
return err >= 0;
}
+ (BOOL) decodeImage:(AVFormatContext *)formatContext codecContext:(AVCodecParameters *)parameters frame:(AVFrame *)frame {
int err = 0;
AVPacket *packet = NULL;
// 创建指定类型的编码器
AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
// 创建编码器上下文
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
// 拷贝参数到上下文中
err = avcodec_parameters_to_context(codecContext, parameters);
// 打开上下文获取信息
err = avcodec_open2(codecContext, codec, NULL);
if (err < 0) {
goto end;
}
// 创建数据包
packet = av_packet_alloc();
// 初始化数据包
av_init_packet(packet);
// 读取frame到包中
err = av_read_frame(formatContext, packet);
if (err < 0) {
goto end;
}
// 发送包到上下文
err = avcodec_send_packet(codecContext, packet);
if (err < 0) {
goto end;
}
// 从上下文中接收frame
err = avcodec_receive_frame(codecContext, frame);
if (err < 0) {
goto end;
}
end:
// 释放packet
if (packet) {
av_packet_free(&packet);
packet = NULL;
}
// 关闭上下文
avcodec_close(codecContext);
return YES;
}
+ (BOOL) encodeImage:(AVFrame *)frame file:(FILE *)file {
AVPacket *packet = NULL;
int err;
// 创建图片编码器
AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
// 创建上下文
AVCodecContext *codec_context = avcodec_alloc_context3(encoder);
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;
// 打开上下文
err = avcodec_open2(codec_context, encoder, NULL);
if (err < 0) {
goto end;
}
// 发送frame到上下文
err = avcodec_send_frame(codec_context, frame);
if (err < 0) {
goto end;
}
// 初始化接收packet
packet = av_packet_alloc();
av_init_packet(packet);
// 开始从上下文接收packet
err = avcodec_receive_packet(codec_context, packet);
if (err < 0) {
goto end;
}
// 数据已接收完成
// 此时可以将数据写入本地文件中,也可以直接转换为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);
end:
// 释放packet数据
if (packet) {
av_packet_free(&packet);
packet = NULL;
}
// 关闭上下文
avcodec_close(codec_context);
// 释放上下文数据
if (codec_context) {
avcodec_free_context(&codec_context);
}
return err >= 0;
}