快进和快退
av_seek_frame
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它包括了领先的音/视频编码库libavcodec等。
libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构
和读取音视频帧等功能;
libavcodec:用于各种类型声音/图像编解码;
libavutil:包含一些公共的工具函数;
libswscale:用于视频场景比例缩放、色彩映射转换;
libpostproc:用于后期效果处理;
ffmpeg:该项目提供的一个工具,可用于格式转换、解码或电视卡即时编码等;
ffsever:一个 HTTP 多媒体即时广播串流服务器;
ffplay:是一个简单的播放器,使用ffmpeg 库解析和解码,通过SDL显示;
FFmpeg是相当强大的多媒体编解码框架,在深入分析其源代码之前必须要有基本的多媒体基础知识,否则其源代码会非常晦涩难懂。本文将从介绍一些基本的多媒体只是,主要是为研读ffmpeg源代码做准备,比如一些编解码部分,只有真正了解了多媒体处理的基本流程,研读ffmpeg源代码才能事半功倍。
下面分析一下多媒体中最基本最核心的视频解码过程,平常我们从网上下载一部电影或者一首歌曲,那么相应的多媒体播放器为我们做好了一切工作,我们只用欣赏就ok了。目前几乎所有的主流多媒体播放器都是基于开源多媒体框架ffmpeg来做的,可见ffmpeg的强大。下面是对一个媒体文件进行解码的主要流程:
1. 解复用(Demux)
当我们打开一个多媒体文件之后,第一步就是解复用,称之为Demux。为什么需要这一步,这一步究竟是做什么的?我们知道在一个多媒体文件中,既包括音频也包括视频,而且音频和视频都是分开进行压缩的,因为音频和视频的压缩算法不一样,既然压缩算法不一样,那么肯定解码也不一样,所以需要对音频和视频分别进行解码。虽然音频和视频是分开进行压缩的,但是为了传输过程的方便,将压缩过的音频和视频捆绑在一起进行传输。所以我们解码的第一步就是将这些绑在一起的音频和视频流分开来,也就是传说中的解复用,所以一句话,解复用这一步就是将文件中捆绑在一起的音频流和视频流分开来以方便后面分别对它们进行解码,下面是Demux之后的效果。
2. 解码(Decode)
这一步不用多说,一个多媒体文件肯定是经过某种或几种格式的压缩的,也就是通常所说的视频和音频编码,编码是为了减少数据量,否则的话对我们的存储设备是一个挑战,如果是流媒体的话对网络带宽也是一个几乎不可能完成的任务。所以我们必须对媒体信息进行尽可能的压缩。
3. FFmpeg中解码流程对应的API函数
了解了上面的一个媒体文件从打开到解码的流程,就可以很轻松的阅读ffmpeg代码,ffmpeg的框架也基本是按照这个流程来的,但不是每个流程对应一个API,下面这副图是我分析ffmpeg并根据自己的理解得到的ffmpeg解码流程对应的API,我想这幅图应该对理解ffmpeg和编解码有一些帮助。
Ffmpeg中Demux这一步是通过avformat_open_input()这个api来做的,这个api读出文件的头部信息,并做demux,在此之后我们就可以读取媒体文件中的音频和视频流,然后通过av_read_frame()从音频和视频流中读取出基本数据流packet,然后将packet送到avcodec_decode_video2()和相对应的api进行解码。
av_seek_frame 函数用到了一个格式上下文,一个流,一个时间戳和一组标记来作为它的参数。这个函数将会跳转到你所给 的时间戳的位置。时间戳的单位是你传递给函数的流的时基 time_base。然而,你并不是必需要传给它一个流(流可以用-1 来代替)。如果你这样做了, 时基time_base 将会是 avcodec 中的内部时间戳单位,或者是 1000000fps。这就是为什么我们在设置 seek_pos的时候会把位置乘 以 AV_TIME_BASER 的原因。但是,如果给 av_seek_frame 函数的 stream 参数传递传-1,你有时会在播放某些文件的时候遇到问题(比较少见),所以我们会取文件中的第一个流并且把它传递到 av_seek_frame 函数。不要忘记我们也要把时间戳 timestamp 的单位进行转化。
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags) { int ret; AVStream *st; ff_read_frame_flush(s); if(flags & AVSEEK_FLAG_BYTE) return av_seek_frame_byte(s, stream_index, timestamp, flags); if(stream_index < 0){ stream_index= av_find_default_stream_index(s); if(stream_index < 0) return -1; st= s->streams[stream_index]; /* timestamp for default must be expressed in AV_TIME_BASE units */ timestamp = av_rescale(timestamp, st->time_base.den, AV_TIME_BASE * (int64_t)st->time_base.num); } /* first, we try the format specific seek */ if (s->iformat->read_seek) ret = s->iformat->read_seek(s, stream_index, timestamp, flags); else ret = -1; if (ret >= 0) { return 0; } if(s->iformat->read_timestamp) return av_seek_frame_binary(s, stream_index, timestamp, flags); else return av_seek_frame_generic(s, stream_index, timestamp, flags); }
utils.c和avformat.h
static int av_seek_frame_byte(AVFormatContext *s, int stream_index, int64_t pos, int flags){ int64_t pos_min, pos_max; #if 0 AVStream *st; if (stream_index < 0) return -1; st= s->streams[stream_index]; #endif pos_min = s->data_offset; pos_max = url_fsize(s->pb) - 1; if (pos < pos_min) pos= pos_min; else if(pos > pos_max) pos= pos_max; url_fseek(s->pb, pos, SEEK_SET); #if 0 av_update_cur_dts(s, st, ts); #endif return 0; }
int av_seek_frame_binary(AVFormatContext *s, int stream_index, int64_t target_ts, int flags){ AVInputFormat *avif= s->iformat; int64_t av_uninit(pos_min), av_uninit(pos_max), pos, pos_limit; int64_t ts_min, ts_max, ts; int index; int64_t ret; AVStream *st; if (stream_index < 0) return -1; #ifdef DEBUG_SEEK av_log(s, AV_LOG_DEBUG, "read_seek: %d %"PRId64"\n", stream_index, target_ts); #endif ts_max= ts_min= AV_NOPTS_VALUE; pos_limit= -1; //gcc falsely says it may be uninitialized st= s->streams[stream_index]; if(st->index_entries){ AVIndexEntry *e; index= av_index_search_timestamp(st, target_ts, flags | AVSEEK_FLAG_BACKWARD); //FIXME whole func must be checked for non-keyframe entries in index case, especially read_timestamp() index= FFMAX(index, 0); e= &st->index_entries[index]; if(e->timestamp <= target_ts || e->pos == e->min_distance){ pos_min= e->pos; ts_min= e->timestamp; #ifdef DEBUG_SEEK av_log(s, AV_LOG_DEBUG, "using cached pos_min=0x%"PRIx64" dts_min=%"PRId64"\n", pos_min,ts_min); #endif }else{ assert(index==0); } index= av_index_search_timestamp(st, target_ts, flags & ~AVSEEK_FLAG_BACKWARD); assert(index < st->nb_index_entries); if(index >= 0){ e= &st->index_entries[index]; assert(e->timestamp >= target_ts); pos_max= e->pos; ts_max= e->timestamp; pos_limit= pos_max - e->min_distance; #ifdef DEBUG_SEEK av_log(s, AV_LOG_DEBUG, "using cached pos_max=0x%"PRIx64" pos_limit=0x%"PRIx64" dts_max=%"PRId64"\n", pos_max,pos_limit, ts_max); #endif } } pos= av_gen_search(s, stream_index, target_ts, pos_min, pos_max, pos_limit, ts_min, ts_max, flags, &ts, avif->read_timestamp); if(pos<0) return -1; /* do the seek */ if ((ret = url_fseek(s->pb, pos, SEEK_SET)) < 0) return ret; av_update_cur_dts(s, st, ts); return 0; }
static int av_seek_frame_generic(AVFormatContext *s, int stream_index, int64_t timestamp, int flags) { int index; int64_t ret; AVStream *st; AVIndexEntry *ie; st = s->streams[stream_index]; index = av_index_search_timestamp(st, timestamp, flags); if(index < 0 && st->nb_index_entries && timestamp < st->index_entries[0].timestamp) return -1; if(index < 0 || index==st->nb_index_entries-1){ int i; AVPacket pkt; if(st->nb_index_entries){ assert(st->index_entries); ie= &st->index_entries[st->nb_index_entries-1]; if ((ret = url_fseek(s->pb, ie->pos, SEEK_SET)) < 0) return ret; av_update_cur_dts(s, st, ie->timestamp); }else{ if ((ret = url_fseek(s->pb, s->data_offset, SEEK_SET)) < 0) return ret; } for(i=0;; i++) { int ret; do{ ret = av_read_frame(s, &pkt); }while(ret == AVERROR(EAGAIN)); if(ret<0) break; av_free_packet(&pkt); if(stream_index == pkt.stream_index){ if((pkt.flags & AV_PKT_FLAG_KEY) && pkt.dts > timestamp) break; } } index = av_index_search_timestamp(st, timestamp, flags); } if (index < 0) return -1; ff_read_frame_flush(s); if (s->iformat->read_seek){ if(s->iformat->read_seek(s, stream_index, timestamp, flags) >= 0) return 0; } ie = &st->index_entries[index]; if ((ret = url_fseek(s->pb, ie->pos, SEEK_SET)) < 0) return ret; av_update_cur_dts(s, st, ie->timestamp); return 0; }
int av_index_search_timestamp(AVStream *st, int64_t wanted_timestamp, int flags) { AVIndexEntry *entries= st->index_entries; int nb_entries= st->nb_index_entries; int a, b, m; int64_t timestamp; a = - 1; b = nb_entries; //optimize appending index entries at the end if(b && entries[b-1].timestamp < wanted_timestamp) a= b-1; while (b - a > 1) { m = (a + b) >> 1; timestamp = entries[m].timestamp; if(timestamp >= wanted_timestamp) b = m; if(timestamp <= wanted_timestamp) a = m; } m= (flags & AVSEEK_FLAG_BACKWARD) ? a : b; if(!(flags & AVSEEK_FLAG_ANY)){ while(m>=0 && m<nb_entries && !(entries[m].flags & AVINDEX_KEYFRAME)){ m += (flags & AVSEEK_FLAG_BACKWARD) ? -1 : 1; } } if(m == nb_entries) return -1; return m; }
aviobuf.c和avio.h
int64_t url_fseek(ByteIOContext *s, int64_t offset, int whence) { int64_t offset1; int64_t pos; int force = whence & AVSEEK_FORCE; whence &= ~AVSEEK_FORCE; if(!s) return AVERROR(EINVAL); pos = s->pos - (s->write_flag ? 0 : (s->buf_end - s->buffer)); if (whence != SEEK_CUR && whence != SEEK_SET) return AVERROR(EINVAL); if (whence == SEEK_CUR) { offset1 = pos + (s->buf_ptr - s->buffer); if (offset == 0) return offset1; offset += offset1; } offset1 = offset - pos; if (!s->must_flush && offset1 >= 0 && offset1 <= (s->buf_end - s->buffer)) { /* can do the seek inside the buffer */ s->buf_ptr = s->buffer + offset1; } else if ((s->is_streamed || offset1 <= s->buf_end + SHORT_SEEK_THRESHOLD - s->buffer) && !s->write_flag && offset1 >= 0 && (whence != SEEK_END || force)) { while(s->pos < offset && !s->eof_reached) fill_buffer(s); if (s->eof_reached) return AVERROR_EOF; s->buf_ptr = s->buf_end + offset - s->pos; } else { int64_t res; #if CONFIG_MUXERS || CONFIG_NETWORK if (s->write_flag) { flush_buffer(s); s->must_flush = 1; } #endif /* CONFIG_MUXERS || CONFIG_NETWORK */ if (!s->seek) return AVERROR(EPIPE); if ((res = s->seek(s->opaque, offset, SEEK_SET)) < 0) return res; if (!s->write_flag) s->buf_end = s->buffer; s->buf_ptr = s->buffer; s->pos = offset; } s->eof_reached = 0; return offset; }
通过opencv里的VideoCapture的函数set(CV_CAP_PROP_POS_FRAMES,nextFrameNumber),定位到具体的帧号,但最终读取的并不是对应帧的图像.
问题出现的原因:
Opencv底层是通过ffmpeg读取视频.其中定位主要用av_seek_frame()来定位frame的位置.
int av_seek_frame(AVFormatContext *s,int stream_index,int64_t timestamp,int flags)其中最后一个参数有
AVSEEK_FLAG_BACKWARD = 1 // seek backward
AVSEEK_FLAG_BYTE = 2 // seeking based on position in bytes
AVSEEK_FLAG_ANY = 4 // seek to any frame,even non key-frames.
ffmpeg默认的是选取关键帧,opencv里面这个函数的参数flag是0.
因而,进行定位时,若下一帧不是关键帧,进行读取时会出跳跃现象.
将参数改为AVSEEK_FLAG_ANY,虽然可以解决跳跃现象,读取任何帧图像.
但是将会出现花屏现象,因为帧图像解码是需要利用关键帧的图像进行帧间的解码,
若读取帧图像时,其对应关键帧没有被读取解码,将只会对该帧进行帧内解码得到花屏图像.
如何才能解决跳跃现象,但不产生花屏图像?
解决思路:读取下一帧号最相近且前面的关键帧,然后一帧帧的读取视频,直到读到下一帧的帧号为止.
将Opencv2.3.1里面的cap_ffmpeg_impl.cpp里面bool CvCapture_FFMPEG::setProperty( int property_id, double value )函数改成如下实现方式,即可达到准确定位的效果.