文章内容已更新关于FFMPEG将NSData(h264)转换为UIImage的更新
涉及内容
- ffmpeg从内存读取数据
- ffmpeg将h264转换为mjpeg
- 保存AVFormat数据
为什么要实现Data(h264)到UIImage的转换
FFMPEG是一个非常强大的多媒体开发工具。然而,多数情况下,移动端开发者并不怎么需要他。一般来说,通用的音视频及图片格式,系统自带的SDK已经足够我们使用了。逼迫我们不得不想到这个家伙的,都是一些比较特殊的格式,比如娇弱的avi,高傲的rtsp,我见犹怜的flv之类的……
关于FFMPEG的使用,一般都是打开一个文件,或者一个流媒体的url,这些在网上存在了各种成熟的解决方案,就不再赘述了。
近一年来,我一直在开发并维护着一款行车记录仪的APP,APP通过连接行车记录仪内置的WIFI,发送特定的指令获取行车记录仪的录像文件,展示封面图或播放视频文件。
就在今天,我遇到了这个特殊的问题:新的行车记录仪,在发送录像文件的封面图时,直接将一帧h264的视频流发送过来,记录仪厂商提供的SDK将这一帧数据保存在NSData中。UIImage不能解析h264的视频帧,所以无法展示给用户,这就需要我们先将h264视频帧转码为可以识别的格式,这时FFMPEG就进入了我的选项中。
遇到的问题
- 正如上面所说,FFMPEG的使用,一般都是打开一个文件,或者是一个流媒体的url,似乎还没有直接将NSData作为数据源的案例。当然,这在理论上一定是可行的,因为不论打开一个文件,还是一个流媒体的url,本质上都是在读取数据,只要找到了读取数据的方法,问题也就迎刃而解了。
- 作为首次使用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;
}