最近在做视频解码相关的工作,有一个功能需要将整个视频解码之后放到内存里面,经测试,一分钟的视频解码需要20s,不算太理想,考虑用多线程来实现。
基本思路是获取所有关键帧信息,然后开启不同的线程来从不同的关键帧开始解码。
1,获取所有关键帧信息,获取所有的关键帧时间戳,大约会花费0.2s:
+ (NSMutableArray *)keyFramePtsWithM3u8Path:(NSString *)path{
NSMutableArray *timestamp_array = [[NSMutableArray alloc] init];
canceled = false;
AVFormatContext *qFormatCtx = NULL;
AVCodecContext *qCodeCtx = NULL;
AVCodec *qCodec = NULL;
AVPacket qPacket;
struct SwsContext *qSwsCtx;
const char *filePath = [path UTF8String];
if (filePath == nil) {
return nil;
}
av_register_all();
if (avformat_open_input(&qFormatCtx, filePath, NULL, NULL) != 0) {
return nil;
}
if (avformat_find_stream_info(qFormatCtx, NULL) < 0) {
return nil;
}
av_dump_format(qFormatCtx, 0, filePath, 0);
int videoStream = -1;
for (int i = 0;i < qFormatCtx->nb_streams;i++) {
if (qFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) {
return nil;
}
qCodeCtx = avcodec_alloc_context3(NULL);
if (avcodec_parameters_to_context(qCodeCtx, qFormatCtx->streams[videoStream]->codecpar) < 0) {
return nil;
}
qCodec = avcodec_find_decoder(qCodeCtx->codec_id);
if (qCodeCtx == NULL) {
return nil;
}
if (avcodec_open2(qCodeCtx, qCodec, NULL) < 0) {
return nil;
}
qSwsCtx = sws_getContext(qCodeCtx->width,
qCodeCtx->height,
qCodeCtx->pix_fmt,
qCodeCtx->width,
qCodeCtx->height,
AV_PIX_FMT_RGBA,
SWS_BICUBIC, NULL, NULL, NULL);
while (av_read_frame(qFormatCtx, &qPacket) >= 0) {
if (qPacket.stream_index == videoStream) {
if(canceled){
av_packet_unref(&qPacket);
break;
}
if(qPacket.flags==1){
[timestamp_array addObject:[NSString stringWithFormat:@"%lld",qPacket.pts]];
}
}
av_packet_unref(&qPacket);
}
sws_freeContext(qSwsCtx);
avcodec_close(qCodeCtx);
avformat_close_input(&qFormatCtx);
return timestamp_array;
}
2,根据关键帧信息开启多个队列(这里为每5个关键帧开启一个队列,可以适当调整):
- (void)parseVideoToImagesMultiThreadWithM3u8Path:(NSString *)path andError:(NSError *__autoreleasing *)error{
NSMutableArray *timestamps = [Decoder keyFramePtsWithM3u8Path:path andError:nil];
int i=0;
int gap = 5;//每五个关键帧开启一个队列
__block long thread_count = ceil([timestamps count]/(float)gap);
__block long complete_count = 0;
__block NSMutableDictionary *imgs = [[NSMutableDictionary alloc] init];
UInt64 t = [[NSDate date] timeIntervalSince1970]* 1000;
for(NSString *time in timestamps){
if(i%gap==0){
NSString *start = time;
NSString *end = (i+gap)<=([timestamps count]-1)?timestamps[i+gap]:@"1000000000";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[Decoder decodeWithM3u8Path:path andStart:start andEnd:end andBlock:^(unsigned char *buffer, int width, int height, NSString *timestamp) {
const size_t bufferLength = width * height * 3;
NSData *data = [NSData dataWithBytes:buffer length:bufferLength];
[imgs setObject:data forKey:timestamp];
} andError:nil];
complete_count++;
if(complete_count==thread_count){
if ([self.delegate respondsToSelector:@selector(finished:)]) {
[self.delegate finished:imgs];
}
NSLog(@"Cutter---:%fms",[[NSDate date] timeIntervalSince1970]* 1000 - t);
}
});
}
i++;
}
}
3,那么我们就需要一个有开始和结束时间戳的方法来实现区间解码:
+ (void)decodeWithM3u8Path:(NSString *)path andStart:(NSString *)start andEnd:(NSString *)end andBlock:(void (^)(unsigned char *, int, int, NSString *))block andError:(NSError *__autoreleasing *)error{
canceled = false;
AVFormatContext *qFormatCtx = NULL;
AVCodecContext *qCodeCtx = NULL;
AVCodec *qCodec = NULL;
AVPacket qPacket;
AVFrame *qFrame = NULL;
AVFrame *qFrameRGB = NULL;
struct SwsContext *qSwsCtx;
uint8_t *buffer = NULL;
int64_t start_timestamp = [start longLongValue];
int64_t end_timestamp = [end longLongValue];
const char *filePath = [path UTF8String];
if (filePath == nil) {
return;
}
av_register_all();
if (avformat_open_input(&qFormatCtx, filePath, NULL, NULL) != 0) {
return;
}
if (avformat_find_stream_info(qFormatCtx, NULL) < 0) {
return;
}
av_dump_format(qFormatCtx, 0, filePath, 0);
int videoStream = -1;
for (int i = 0;i < qFormatCtx->nb_streams;i++) {
if (qFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) {
return;
}
qCodeCtx = avcodec_alloc_context3(NULL);
if (avcodec_parameters_to_context(qCodeCtx, qFormatCtx->streams[videoStream]->codecpar) < 0) {
return;
}
qCodec = avcodec_find_decoder(qCodeCtx->codec_id);
if (qCodeCtx == NULL) {
return;
}
if (avcodec_open2(qCodeCtx, qCodec, NULL) < 0) {
return;
}
qFrame = av_frame_alloc();
qFrameRGB = av_frame_alloc();
if (qFrame == NULL || qFrameRGB == NULL) {
return;
}
int heigth = qCodeCtx->height>>2;
int width = qCodeCtx->width>>2;
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24,
width,
heigth, 1);
buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
av_image_fill_arrays(qFrameRGB->data,
qFrameRGB->linesize,
buffer,
AV_PIX_FMT_RGB24,
width,
heigth, 1);
qSwsCtx = sws_getContext(qCodeCtx->width,
qCodeCtx->height,
qCodeCtx->pix_fmt,
width,
heigth,
AV_PIX_FMT_RGB24,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
while (av_read_frame(qFormatCtx, &qPacket) >= 0) {
if (qPacket.stream_index == videoStream) {
if(canceled){
av_packet_unref(&qPacket);
break;
}
if(qPacket.pts>=start_timestamp){
if(qPacket.pts<end_timestamp){
int ret = avcodec_send_packet(qCodeCtx, &qPacket);
if (ret < 0) {
return;
}
ret = avcodec_receive_frame(qCodeCtx, qFrame);
if (ret ==0) {
sws_scale(qSwsCtx,
(uint8_t const * const *)qFrame->data,
qFrame->linesize, 0,
qCodeCtx->height,
qFrameRGB->data,
qFrameRGB->linesize);
block(buffer,width,heigth,[NSString stringWithFormat:@"%lld",qFrame->pts]);
}
}
}
}
av_packet_unref(&qPacket);
}
av_free(buffer);
av_frame_free(&qFrameRGB);
av_frame_free(&qFrame);
sws_freeContext(qSwsCtx);
avcodec_close(qCodeCtx);
avformat_close_input(&qFormatCtx);
}
这样就可以实现多线程的解码了,由于机器为双核,经测最后多线程解码的时间缩减为11s,还是很可观的。鉴于大部分视频长度都在30s左右,解码时间可以控制在五六秒的范围能,对于用户体验有不少的提升。
或者直接:av_dict_set(&opt, “threads”, “auto”, 0);
也可以实现多线程解码,当然这个是ffmpeg内部实现的。
文章首发于我的博客(yangf.vip)
