领导5:同步视频
如何同步视频
前面全部的一段时候,我们有了一个几乎无用的电影播放器。当然,它能播放视频,也能播放音频,然则它还不克不及被称为一部电影。那么我们还要做什么呢?
PTS和DTS
荣幸的是,音频和视频流都有一些关于以多快速度和什么时候来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,若是我们只是简单的经由过程数帧 和乘以帧率的体式格式来同步视频,那么就很有可能会落空同步。于是作为一种补充,在流中的包有种叫做DTS(解码时候戳)和PTS(显示时候戳)的机制。为了 这两个参数,你须要懂得电影存放的体式格式。像MPEG等格局,应用被叫做B帧(B默示双向bidrectional)的体式格式。别的两种帧被叫做I帧和P帧 (I默示关键帧,P默示猜测帧)。I帧包含了某个特定的完全图像。P帧依附于前面的I帧和P帧并且应用斗劲或者差分的体式格式来编码。B帧与P帧有点类似,但 是它是依附于前面和后面的帧的信息的。这也就说了然为什么我们可能在调用avcodec_decode_video今后会得不到一帧图像。
所以对于一个电影,帧是如许来显示的:I B B P。如今我们须要在显示B帧之前知道P帧中的信息。是以,帧可能会遵守如许的体式格式来存储:IPBB。这就是为什么我们会有一个解码时候戳和一个显示时候戳 的原因。解码时候戳告诉我们什么时辰须要解码,显示时候戳告诉我们什么时辰须要显示。所以,在这种景象下,我们的流可所以如许的:
PTS: 1 4 2 3 DTS: 1 2 3 4 Stream: I P B B |
凡是PTS和DTS只有在流中有B帧的时辰会不合。
当我们调用av_read_frame()获得一个包的时辰,PTS和DTS的信息也会保存在包中。然则我们真正想要的PTS是我们方才解码出来的原始帧 的PTS,如许我们才干知道什么时辰来显示它。然而,我们从avcodec_decode_video()函数中获得的帧只是一个AVFrame,此中并 没有包含有效的PTS值(重视:AVFrame并没有包含时候戳信息,但当我们比及帧的时辰并不是我们想要的样子)。然而,ffmpeg从头排序包以便于 被avcodec_decode_video()函数处理惩罚的包的DTS可以老是与其返回的PTS雷同。然则,别的的一个警告是:我们也并不是总能获得这个 信息。
不消愁闷,因为有别的一种办法可以找到帖的PTS,我们可以让法度本身来从头排序包。我们保存一帧的第一个包的PTS:这将作为全部这一帧的PTS。我们 可以经由过程函数avcodec_decode_video()来策画出哪个包是一帧的第一个包。如何实现呢?任何时辰当一个包开端一帧的时 候,avcodec_decode_video()将调用一个函数来为一帧申请一个缓冲。当然,ffmpeg容许我们从头定义那个分派内存的函数。所以我 们建造了一个新的函数来保存一个包的时候戳。
当然,尽管那样,我们可能还是得不到一个正确的时候戳。我们将在后面处理惩罚这个题目。
同步
如今,知道了什么时辰来显示一个视频帧真好,然则我们如何来实际操纵呢?这里有个主意:当我们显示了一帧今后,我们策画出下一帧显示的时候。然后我们简单 的设置一个新的按时器来。你可能会想,我们搜检下一帧的PTS值而不是体系时钟来看超时是否会到。这种体式格式可以工作,然则有两种景象要处理惩罚。
起首,要知道下一个PTS是什么。如今我们能添加视频速度到我们的PTS中--太对了!然而,有些电影须要帧反复。这意味着我们反复播放当前的帧。这将导致法度显示下一帧太快了。所以我们须要策画它们。
第二,正如法度如今如许,视频和音频播放很欢欣,一点也不受同步的影响。若是一切都工作得很好的话,我们不必愁闷。然则,你的电脑并不是最好的,很多视频 文件也不是无缺的。所以,我们有三种选择:同步音频到视频,同步视频到音频,或者都同步到外部时钟(例如你的电脑时钟)。从如今开端,我们将同步视频到音 频。
写代码:获得帧的时候戳
如今让我们到代码中来做这些工作。我们将须要为我们的大布局体添加一些成员,然则我们会按照须要来做。起首,让我们看一下视频线程。记住,在这里我们获得 懂得码线程输出到队列中的包。这里我们须要的是从avcodec_decode_video函数中获得帧的时候戳。我们评论辩论的第一种体式格式是从前次处理惩罚的包 中获得DTS,这是很轻易的:
double pts;
for(;;) { if(packet_queue_get(&is->videoq, packet, 1) < 0) { // means we quit getting packets break; } pts = 0; // Decode video frame len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, packet->data, packet->size); if(packet->dts != AV_NOPTS_VALUE) { pts = packet->dts; } else { pts = 0; } pts *= av_q2d(is->video_st->time_base); |
若是我们得不到PTS就把它设置为0。
好,那是很轻易的。然则我们所说的若是包的DTS不克不及帮到我们,我们须要应用这一帧的第一个包的PTS。我们经由过程让ffmpeg应用我们本身的申请帧法度来实现。下面的是函数的格局:
int get_buffer(struct AVCodecContext *c, AVFrame *pic); void release_buffer(struct AVCodecContext *c, AVFrame *pic); |
申请函数没有告诉我们关于包的任何工作,所以我们要本身每次在获得一个包的时辰把PTS保存到一个全局变量中去。我们本身以读到它。然后,我们把值保存到AVFrame布局体难懂得的变量中去。所以一开端,这就是我们的函数:
uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;
int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) { int ret = avcodec_default_get_buffer(c, pic); uint64_t *pts = av_malloc(sizeof(uint64_t)); *pts = global_video_pkt_pts; pic->opaque = pts; return ret; } void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) { if(pic) av_freep(&pic->opaque); avcodec_default_release_buffer(c, pic); } |
函数avcodec_default_get_buffer和avcodec_default_release_buffer是ffmpeg中默认的申请缓冲的函数。函数av_freep是一个内存经管函数,它不单把内存开释并且把指针设置为NULL。
如今到了我们流打开的函数(stream_component_open),我们添加这几行来告诉ffmpeg如何去做:
codecCtx->get_buffer = our_get_buffer; codecCtx->release_buffer = our_release_buffer; |
如今我们必须添加代码来保存PTS到全局变量中,然后在须要的时辰来应用它。我们的代码如今看起来应当是如许子:
for(;;) { if(packet_queue_get(&is->videoq, packet, 1) < 0) { // means we quit getting packets break; } pts = 0;
// Save global pts to be stored in pFrame in first call global_video_pkt_pts = packet->pts; // Decode video frame len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, packet->data, packet->size); if(packet->dts == AV_NOPTS_VALUE && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) { pts = *(uint64_t *)pFrame->opaque; } else if(packet->dts != AV_NOPTS_VALUE) { pts = packet->dts; } else { pts = 0; } pts *= av_q2d(is->video_st->time_base); |
技巧提示:你可能已经重视到我们应用int64来默示PTS。这是因为PTS是以整型来保存的。这个值是一个时候戳相当于时候的怀抱,用来以流的 time_base为单位进行时候怀抱。例如,若是一个流是24帧每秒,值为42的PTS默示这一帧应当排在第42个帧的地位若是我们每秒有24帧(这里 并不完全正确)。
我们可以经由过程除以帧率来把这个值转化为秒。流中的time_base值默示1/framerate(对于固定帧率来说),所以获得了以秒为单位的PTS,我们须要乘以time_base。
写代码:应用PTS来同步
如今我们获得了PTS。我们要重视前面评论辩论到的两个同步题目。我们将定义一个函数叫做synchronize_video,它可以更新同步的PTS。这个 函数也能终极处理惩罚我们得不到PTS的景象。同时我们要知道下一帧的时候以便于正确设置刷新速度。我们可以应用内部的反该当前视频已经播放时候的时钟 video_clock来完成这个功能。我们把这些值添加到大布局体中。
typedef struct VideoState { double video_clock; /// |
下面的是函数synchronize_video,它可以很好的自我注释:
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {
double frame_delay;
if(pts != 0) {
is->video_clock = pts; } else {
pts = is->video_clock; }
frame_delay = av_q2d(is->video_st->codec->time_base);
frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); is->video_clock += frame_delay; return pts; } |
你也会重视到我们也策画了反复的帧。
如今让我们获得正确的PTS并且应用queue_picture来队列化帧,添加一个新的时候戳参数pts:
// Did we get a video frame? if(frameFinished) { pts = synchronize_video(is, pFrame, pts); if(queue_picture(is, pFrame, pts) < 0) { break; } } |
对于queue_picture来说独一改变的工作就是我们把时候戳值pts保存到VideoPicture布局体中,我们我们必须添加一个时候戳变量到布局体中并且添加一行代码:
typedef struct VideoPicture { ... double pts; } int queue_picture(VideoState *is, AVFrame *pFrame, double pts) { ... stuff ... if(vp->bmp) { ... convert picture ... vp->pts = pts; ... alert queue ... } |
如今我们的图像队列中的所有图像都有了正确的时候戳值,所以让我们看一下视频刷新函数。你会记得前次我们用80ms的刷新时候来诳骗它。那么,如今我们将会算出实际的值。
我们的策略是经由过程简单策画前一帧和如今这一帧的时候戳来猜测出下一个时候戳的时候。同时,我们须要同步视频到音频。我们将设置一个音频时候audio clock;一个内部值记录了我们正在播放的音频的地位。就像从随便率性的mp3播放器中读出来的数字一样。既然我们把视频同步到音频,视频线程应用这个值来算出是否太快还是太慢。
我们将在后面来实现这些代码;如今我们假设我们已经有一个可以给我们音频时候的函数get_audio_clock。一旦我们有了这个值,我们在音频和视 频落空同步的时辰应当做些什么呢?简单而有点笨的办法是试着用跳过正确帧或者其它的体式格式来解决。作为一种调换的手段,我们会调剂下次刷新的值;若是时候戳 太掉队于音频时候,我们加倍策画延迟。若是时候戳太领先于音频时候,我们将尽可能快的刷新。既然我们有了调剂过的时候和延迟,我们将把它和我们经由过程 frame_timer策画出来的时候进行斗劲。这个帧时候frame_timer将会统计出电影播放中所有的延时。换句话说,这个 frame_timer就是指我们什么时辰来显示下一帧。我们简单的添加新的帧按时器延时,把它和电脑的体系时候进行斗劲,然后应用那个值来调剂下一次刷 新。这可能有点难以懂得,所以请卖力研究代码:
void video_refresh_timer(void *userdata) {
VideoState *is = (VideoState *)userdata; VideoPicture *vp; double actual_delay, delay, sync_threshold, ref_clock, diff;
if(is->video_st) { if(is->pictq_size == 0) { schedule_refresh(is, 1); } else { vp = &is->pictq[is->pictq_rindex];
delay = vp->pts - is->frame_last_pts; if(delay <= 0 || delay >= 1.0) {
delay = is->frame_last_delay; }
is->frame_last_delay = delay; is->frame_last_pts = vp->pts;
ref_clock = get_audio_clock(is); diff = vp->pts - ref_clock;
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD; if(fabs(diff) < AV_NOSYNC_THRESHOLD) { if(diff <= -sync_threshold) { delay = 0; } else if(diff >= sync_threshold) { delay = 2 * delay; } } is->frame_timer += delay;
actual_delay = is->frame_timer - (av_gettime() / 1000000.0); if(actual_delay < 0.010) {
actual_delay = 0.010; } schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
video_display(is);
if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) { is->pictq_rindex = 0; } SDL_LockMutex(is->pictq_mutex); is->pictq_size--; SDL_CondSignal(is->pictq_cond); SDL_UnlockMutex(is->pictq_mutex); } } else { schedule_refresh(is, 100); } } |
我们在这里做了很多搜检:起首,我们包管如今的时候戳和上一个时候戳之间的处以delay是有意义的。若是不是的话,我们就猜测着用前次的延迟。接着,我 们有一个同步阈值,因为在同步的时辰工作并不老是那么完美的。在ffplay中应用0.01作为它的值。我们也包管阈值不会比时候戳之间的间隔短。最后, 我们把最小的刷新值设置为10毫秒。
(这句不知道应当放在哪里)事实上这里我们应当跳过这一帧,然则我们不想为此而懊恼。 |
我们给大布局体添加了很多的变量,所以不要忘怀搜检一下代码。同时也不要忘怀在函数streame_component_open中初始化帧时候frame_timer和前面的帧延迟frame delay:
is->frame_timer = (double)av_gettime() / 1000000.0; is->frame_last_delay = 40e-3; |
同步:声音时钟
如今让我们看一下如何来获得声音时钟。我们可以在声音解码函数audio_decode_frame中更新时钟时候。如今,请记住我们并不是每次调用这个 函数的时辰都在处理惩罚新的包,所以有我们要在两个处所更新时钟。第一个处所是我们获得新的包的时辰:我们简单的设置声音时钟为这个包的时候戳。然后,若是一 个包里有很多帧,我们经由过程样本数和采样率来策画,所以当我们获得包的时辰:
if(pkt->pts != AV_NOPTS_VALUE) { is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts; } |
然后当我们处理惩罚这个包的时辰:
pts = is->audio_clock; *pts_ptr = pts; n = 2 * is->audio_st->codec->channels; is->audio_clock += (double)data_size / (double)(n * is->audio_st->codec->sample_rate); |
一点细节:姑且函数被改成包含pts_ptr,所以要包管你已经改了那些。这时的pts_ptr是一个用来通知audio_callback函数当前声音包的时候戳的指针。这将鄙人次用来同步声音和视频。
如今我们可以最后来实现我们的get_audio_clock函数。它并不像获得is->audio_clock值那样简单。重视我们会在每次处理惩罚 它的时辰设置声音时候戳,然则若是你看了audio_callback函数,它花费了时候来把数据从声音包中移到我们的输出缓冲区中。这意味着我们声音时 钟中记录的时候比实际的要早太多。所以我们必必要搜检一下我们还有几许没有写入。下面是完全的代码:
double get_audio_clock(VideoState *is) { double pts; int hw_buf_size, bytes_per_sec, n;
pts = is->audio_clock; hw_buf_size = is->audio_buf_size - is->audio_buf_index; bytes_per_sec = 0; n = is->audio_st->codec->channels * 2; if(is->audio_st) { bytes_per_sec = is->audio_st->codec->sample_rate * n; } if(bytes_per_sec) { pts -= (double)hw_buf_size / bytes_per_sec; } return pts; } |
你应当知道为什么这个函数可以正常工作了;)
这就是了!让我们编译它:
gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
最后,你可以应用我们本身的电影播放器来看电影了。下次我们将看一下声音同步,然后接下来的领导我们会评论辩论查询。
领导6:同步音频
同步音频
如今我们已经有了一个斗劲像样的播放器。所以让我们看一下还有哪些零散的器材没处理惩罚。前次,我们掩盖了一点同步题目,也就是同步音频到视频而不是其它的同 步体式格式。我们将采取和视频一样的体式格式:做一个内部视频时钟来记录视频线程播放了多久,然后同步音频到上方去。后面我们也来看一下如何推而广之把音频和视频 都同步到外部时钟。
生成一个视频时钟
如今我们要生成一个类似于前次我们的声音时钟的视频时钟:一个给出当前视频播放时候的内部值。开端,你可能会想这和应用上一帧的时候戳来更新按时器一样简 单。然则,不要忘了视频帧之间的时候间隔是很长的,以毫秒为计量的。解决办法是跟踪别的一个值:我们在设置上一帧时候戳的时辰的时候值。于是当前视频时候 值就是PTS_of_last_frame + (current_time - time_elapsed_since_PTS_value_was_set)。这种解决体式格式与我们在函数get_audio_clock中的体式格式很类 似。
地点在我们的大布局体中,我们将放上一个双精度浮点变量video_current_pts和一个64位宽整型变量video_current_pts_time。时钟更新将被放在video_refresh_timer函数中。
void video_refresh_timer(void *userdata) {
if(is->video_st) { if(is->pictq_size == 0) { schedule_refresh(is, 1); } else { vp = &is->pictq[is->pictq_rindex];
is->video_current_pts = vp->pts; is->video_current_pts_time = av_gettime(); |
不要忘怀在stream_component_open函数中初始化它:
is->video_current_pts_time = av_gettime(); |
如今我们须要一种获得信息的体式格式:
double get_video_clock(VideoState *is) { double delta;
delta = (av_gettime() - is->video_current_pts_time) / 1000000.0; return is->video_current_pts + delta; } |
提取时钟
然则为什么要强迫应用视频时钟呢?我们更改视频同步代码乃至于音频和视频不会试着去彼此同步。想像一下我们让它像ffplay一样有一个号令行参数。所以 让我们抽象一样这件工作:我们将做一个新的封装函数get_master_clock,用来检测av_sync_type变量然后决意调用 get_audio_clock还是get_video_clock或者其它的想应用的获得时钟的函数。我们甚至可以应用电脑时钟,这个函数我们叫做 get_external_clock:
enum { AV_SYNC_AUDIO_MASTER, AV_SYNC_VIDEO_MASTER, AV_SYNC_EXTERNAL_MASTER, };
#define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER
double get_master_clock(VideoState *is) { if(is->av_sync_type == AV_SYNC_VIDEO_MASTER) { return get_video_clock(is); } else if(is->av_sync_type == AV_SYNC_AUDIO_MASTER) { return get_audio_clock(is); } else { return get_external_clock(is); } } main() { ... is->av_sync_type = DEFAULT_AV_SYNC_TYPE; ... } |
同步音频
如今是最难的项目组:同步音频到视频时钟。我们的策略是测量声音的地位,把它与视频时候斗劲然后算出我们须要批改几许的样本数,也就是说:我们是否须要经由过程丢弃样本的体式格式来加快播放还是须要经由过程插值样本的体式格式来放慢播放?
我们将在每次处理惩罚声音样本的时辰运行一个synchronize_audio的函数来正确的紧缩或者扩大声音样本。然而,我们不想在每次发明有误差的时辰 都进行同步,因为如许会使同步音频多于视频包。所以我们为函数synchronize_audio设置一个最小连气儿值来限制须要同步的时刻,如许我们就不 会老是在调剂了。当然,就像前次那样,“落空同步”意味着声音时钟和视频时钟的差别大于我们的阈值。
所以我们将应用一个分数系数,叫c,所以如今可以说我们获得了N个落空同步的声音样本。落空同步的数量可能会有很多变更,所以我们要策画一下落空同步的长 度的均值。例如,第一次调用的时辰,显示出来我们落空同步的长度为40ms,下次变为50ms等等。然则我们不会应用一个简单的均值,因为间隔如今比来的 值比靠前的值要首要的多。所以我们将应用一个分数体系,叫c,然后用如许的公式来策画差别:diff_sum = new_diff + diff_sum*c。当我们筹办好去找均匀差别的时辰,我们用简单的策画体式格式:avg_diff = diff_sum * (1-c)。
重视:为什么会在这里?这个公式看来很神奇!嗯,它根蒂根基上是一个应用等比级数的加权均匀值。我不知道这是否有名字(我甚至查过维基百科!),然则若是想要更多的信息,这里是一个申明http://www.dranger.com/ffmpeg/weightedmean.html或者在http://www.dranger.com/ffmpeg/weightedmean.txt里。 |
下面是我们的函数:
int synchronize_audio(VideoState *is, short *samples, int samples_size, double pts) { int n; double ref_clock;
n = 2 * is->audio_st->codec->channels;
if(is->av_sync_type != AV_SYNC_AUDIO_MASTER) { double diff, avg_diff; int wanted_size, min_size, max_size, nb_samples;
ref_clock = get_master_clock(is); diff = get_audio_clock(is) - ref_clock;
if(diff < AV_NOSYNC_THRESHOLD) { // accumulate the diffs is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum; if(is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) { is->audio_diff_avg_count++; } else { avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
} } else {
is->audio_diff_avg_count = 0; is->audio_diff_cum = 0; } } return samples_size; } |
如今我们已经做得很好;我们已经近似的知道如何用视频或者其它的时钟来调剂音频了。所以让我们来策画一下要在添加和砍掉几许样本,并且如安在“Shrinking/expanding buffer code”项目组来写上代码:
if(fabs(avg_diff) >= is->audio_diff_threshold) { wanted_size = samples_size + ((int)(diff * is->audio_st->codec->sample_rate) * n); min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100); max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100); if(wanted_size < min_size) { wanted_size = min_size; } else if (wanted_size > max_size) { wanted_size = max_size; } |
记住audio_length * (sample_rate * # of channels * 2)就是audio_length秒时候的声音的样本数。所以,我们想要的样本数就是我们按照声音偏移添加或者削减后的声音样本数。我们也可以设置一个局限来限制我们一次进行批改的长度,因为若是我们改变的太多,用户会听到逆耳的声音。
批改样本数
如今我们要真正的批改一下声音。你可能会重视到我们的同步函数synchronize_audio返回了一个样本数,这可以告诉我们有几许个字节被送到流 中。所以我们只要调剂样本数为wanted_size就可以了。这会让样本更小一些。然则若是我们想让它变大,我们不克不及只是让样本大小变大,因为在缓冲区 中没有多余的数据!所以我们必须添加上去。然则我们如何来添加呢?最笨的办法就是试着来推算声音,所以让我们用已有的数据在缓冲的末尾添加上最后的样本。
if(wanted_size < samples_size) {
samples_size = wanted_size; } else if(wanted_size > samples_size) { uint8_t *samples_end, *q; int nb;
nb = (samples_size - wanted_size); samples_end = (uint8_t *)samples + samples_size - n; q = samples_end + n; while(nb > 0) { memcpy(q, samples_end, n); q += n; nb -= n; } samples_size = wanted_size; } |
如今我们经由过程这个函数返回的是样本数。我们如今要做的是应用它:
void audio_callback(void *userdata, Uint8 *stream, int len) {
VideoState *is = (VideoState *)userdata; int len1, audio_size; double pts;
while(len > 0) { if(is->audio_buf_index >= is->audio_buf_size) {
audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts); if(audio_size < 0) {
is->audio_buf_size = 1024; memset(is->audio_buf, 0, is->audio_buf_size); } else { audio_size = synchronize_audio(is, (int16_t *)is->audio_buf, audio_size, pts); is->audio_buf_size = audio_size; |
我们要做的是把函数synchronize_audio插入进去。(同时,包管在初始化上方变量的时辰搜检一下代码,这些我没有赘述)。
停止之前的最后一件工作:我们须要添加一个if语句来包管我们不会在视频为主时钟的时辰也来同步视频。
if(is->av_sync_type != AV_SYNC_VIDEO_MASTER) { ref_clock = get_master_clock(is); diff = vp->pts - ref_clock;
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD; if(fabs(diff) < AV_NOSYNC_THRESHOLD) { if(diff <= -sync_threshold) { delay = 0; } else if(diff >= sync_threshold) { delay = 2 * delay; } } } |
添加后就可以了。要包管全部法度中我没有赘述的变量都被初始化过了。然后编译它:
gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
然后你就可以运行它了。
下次我们要做的是让你可以让电影快退和快进。
领导7:快进快退
处理惩罚快进快退号令
如今我们来为我们的播放器参加一些快进和快退的功能,因为若是你不克不及全局搜刮一部电影是很让人憎恶的。同时,这将告诉你av_seek_frame函数是多么轻易应用。
我们将在电影播放中应用左标的目标键和右标的目标键来默示向后和向前一小段,应用向上和向下键来默示向前和向后一大段。这里一小段是10秒,一大段是60秒。所以 我们须要设置我们的主轮回来捕获键盘事务。然而当我们捕获到键盘事务后我们不克不及直接调用av_seek_frame函数。我们要首要的解码线程 decode_thread的轮回中做这些。所以,我们要添加一些变量到大布局体中,用来包含新的跳转地位和一些跳转标记:
int seek_req; int seek_flags; int64_t seek_pos; |
如今让我们在主轮回中捕获按键:
for(;;) { double incr, pos;
SDL_WaitEvent(&event); switch(event.type) { case SDL_KEYDOWN: switch(event.key.keysym.sym) { case SDLK_LEFT: incr = -10.0; goto do_seek; case SDLK_RIGHT: incr = 10.0; goto do_seek; case SDLK_UP: incr = 60.0; goto do_seek; case SDLK_DOWN: incr = -60.0; goto do_seek; do_seek: if(global_video_state) { pos = get_master_clock(global_video_state); pos += incr; stream_seek(global_video_state, (int64_t)(pos * AV_TIME_BASE), incr); } break; default: break; } break; |
为了检测按键,我们先查了一下是否有SDL_KEYDOWN事务。然后我们应用event.key.keysym.sym来断定哪个按键被按下。一旦我们 知道了如何来跳转,我们就来策画新的时候,办法为把增长的时候值加到从函数get_master_clock中获得的时候值上。然后我们调用 stream_seek函数来设置seek_pos等变量。我们把新的时候转换成为avcodec中的内部时候戳单位。在流中调用那个时候戳将应用帧而不 是用秒来策画,公式为seconds = frames * time_base(fps)。默认的avcodec值为1,000,000fps(所以2秒的内部时候戳为2,000,000)。在后面我们来看一下为 什么要把这个值进行一下转换。
这就是我们的stream_seek函数。请重视我们设置了一个标记为撤退猬缩办事:
void stream_seek(VideoState *is, int64_t pos, int rel) {
if(!is->seek_req) { is->seek_pos = pos; is->seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0; is->seek_req = 1; } } |
如今让我们看一下若是在decode_thread中实现跳转。你会重视到我们已经在源文件中标识表记标帜了一个叫做“seek stuff goes here”的项目组。如今我们将把代码写在这里。
跳转是环绕着av_seek_frame函数的。这个函数用到了一个格局高低文,一个流,一个时候戳和一组标识表记标帜来作为它的参数。这个函数将会跳转到你所给 的时候戳的地位。时候戳的单位是你传递给函数的流的时基time_base。然而,你并不是必必要传给它一个流(流可以用-1来庖代)。若是你如许做了, 时基time_base将会是avcodec中的内部时候戳单位,或者是1000000fps。这就是为什么我们在设置seek_pos的时辰会把地位乘 以AV_TIME_BASER的原因。
然则,若是给av_seek_frame函数的stream参数传递传-1,你有时会在播放某些文件的时辰碰到题目(斗劲少见),所以我们会取文件中的第一个流并且把它传递到av_seek_frame函数。不要忘怀我们也要把时候戳timestamp的单位进行转化。
if(is->seek_req) { int stream_index= -1; int64_t seek_target = is->seek_pos;
if (is->videoStream >= 0) stream_index = is->videoStream; else if(is->audioStream >= 0) stream_index = is->audioStream;
if(stream_index>=0){ seek_target= av_rescale_q(seek_target, AV_TIME_BASE_Q, pFormatCtx->streams[stream_index]->time_base); } if(av_seek_frame(is->pFormatCtx, stream_index, seek_target, is->seek_flags) < 0) { fprintf(stderr, "%s: error while seeking\n", is->pFormatCtx->filename); } else {
|
这里av_rescale_q(a,b,c)是用来把时候戳从一个时基调剂到别的一个时基时辰用的函数。它根蒂根基的动作是策画a*b/c,然则这个函数还是 必须的,因为直接策画会有溢出的景象产生。AV_TIME_BASE_Q是AV_TIME_BASE作为分母后的版本。它们是很不雷同 的:AV_TIME_BASE * time_in_seconds = avcodec_timestamp而AV_TIME_BASE_Q * avcodec_timestamp = time_in_seconds(重视AV_TIME_BASE_Q实际上是一个AVRational对象,所以你必须应用avcodec中特定的q函数 来处理惩罚它)。
清空我们的缓冲
我们已经正确设定了跳转地位,然则我们还没有停止。记住我们有一个堆放了很多包的队列。既然我们跳到了不合的地位,我们必须把队列中的内容清空不然电影是不会跳转的。不仅如此,avcodec也有它本身的内部缓冲,也须要每次被清空。
要实现这个,我们须要起首写一个函数来清空我们的包队列。然后我们须要一种号令声音和视频线程来清空avcodec内部缓冲的办法。我们可以在清空队列后把特定的包放入到队列中,然后当它们检测到特定的包的时辰,它们就会把本身的内部缓冲清空。
让我们开端写清空函数。其实很简单的,所以我直接把代码写鄙人面:
static void packet_queue_flush(PacketQueue *q) { AVPacketList *pkt, *pkt1;
SDL_LockMutex(q->mutex); for(pkt = q->first_pkt; pkt != NULL; pkt = pkt1) { pkt1 = pkt->next; av_free_packet(&pkt->pkt); av_freep(&pkt); } q->last_pkt = NULL; q->first_pkt = NULL; q->nb_packets = 0; q->size = 0; SDL_UnlockMutex(q->mutex); } |
既然队列已经清空了,我们放入“清空包”。然则开端我们要定义和创建这个包:
AVPacket flush_pkt;
main() { ... av_init_packet(&flush_pkt); flush_pkt.data = "FLUSH"; ... } |
如今我们把这个包放到队列中:
} else { if(is->audioStream >= 0) { packet_queue_flush(&is->audioq); packet_queue_put(&is->audioq, &flush_pkt); } if(is->videoStream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); } } is->seek_req = 0; } |
(这些代码片段是接着前面decode_thread中的代码片段的)我们也须要批改packet_queue_put函数才不至于直接简单复制了这个包:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1; if(pkt != &flush_pkt && av_dup_packet(pkt) < 0) { return -1; } |
然后在声音线程和视频线程中,我们在packet_queue_get后立即调用函数avcodec_flush_buffers:
if(packet_queue_get(&is->audioq, pkt, 1) < 0) { return -1; } if(packet->data == flush_pkt.data) { avcodec_flush_buffers(is->audio_st->codec); continue; } |
上方的代码片段与视频线程中的一样,只要把“audio”换成“video”。
就如许,让我们编译我们的播放器:
gcc -o tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
试一下!我们几乎已经都做完了;下次我们只要做一点小的批改就好了,那就是检测ffmpeg供给的小的软件缩放采样。
领导8:软件缩放
软件缩放库libswscale
迩来ffmpeg添加了新的接口:libswscale来处理惩罚图像缩放。
然则在前面我们应用img_convert来把RGB转换成YUV12,我们如今应用新的接口。新接口加倍标准和快速,并且我信赖里面有了MMX优化代码。换句话说,它是做缩放更好的体式格式。
我们将用来缩放的根蒂根基函数是sws_scale。但一开端,我们必须建树一个SwsContext的概念。这将让我们进行想要的转换,然后把它传递给 sws_scale函数。类似于在SQL中的筹办阶段或者是在Python中编译的规矩表达式regexp。要筹办这个高低文,我们应用 sws_getContext函数,它须要我们源的宽度和高度,我们想要的宽度和高度,源的格局和想要转换成的格局,同时还有一些其它的参数和标记。然后 我们像应用img_convert一样来应用sws_scale函数,独一不合的是我们传递给的是SwsContext:
#include <ffmpeg/swscale.h> // include the header!
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
static struct SwsContext *img_convert_ctx; ...
if(vp->bmp) {
SDL_LockYUVOverlay(vp->bmp);
dst_pix_fmt = PIX_FMT_YUV420P;
pict.data[0] = vp->bmp->pixels[0]; pict.data[1] = vp->bmp->pixels[2]; pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0]; pict.linesize[1] = vp->bmp->pitches[2]; pict.linesize[2] = vp->bmp->pitches[1];
// Convert the image into YUV format that SDL uses if(img_convert_ctx == NULL) { int w = is->video_st->codec->width; int h = is->video_st->codec->height; img_convert_ctx = sws_getContext(w, h, is->video_st->codec->pix_fmt, w, h, dst_pix_fmt, SWS_BICUBIC, NULL, NULL, NULL); if(img_convert_ctx == NULL) { fprintf(stderr, "Cannot initialize the conversion context!\n"); exit(1); } } sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, is->video_st->codec->height, pict.data, pict.linesize); |
我们把新的缩放器放到了合适的地位。欲望这会让你知道libswscale能做什么。
就如许,我们做完了!编译我们的播放器:
gcc -o tutorial08 tutorial08.c -lavutil -lavformat -lavcodec -lz -lm `sdl-config --cflags --libs` |
享受我们用C写的少于1000行的电影播放器吧。
当然,还有很多工作要做。
如今还要做什么?
我们已经有了一个可以工作的播放器,然则它必然还不敷好。我们做了很多,然则还有很多要添加的机能:
·错误处理惩罚。我们代码中的错误处理惩罚是无穷的,多处理惩罚一些会更好。
·暂停。我们不克不及暂停电影,这是一个很有效的功能。我们可以在大布局体中应用一个内部暂停变量,当用户暂停的时辰就设置它。然后我们的音频,视频和解码线 程检测到它后就不再输出任何器材。我们也应用av_read_play来支撑收集。这很轻易申明,然则你却不克不及明显的策画出,所以把这个作为一个家庭作 业,若是你想测验测验的话。提示,可以参考ffplay.c。
·支撑视频硬件特点。一个参考的例子,请参考Frame Grabbing在Martin的旧的领导中的相干项目组。http://www.inb.uni-luebeck.de/~boehme/libavcodec_.html
·按字节跳转。若是你可以遵守字节而不是秒的体式格式来策画出跳转地位,那么对于像VOB文件一样的有不连气儿时候戳的视频文件来说,定位会加倍正确。
·丢弃帧。若是视频掉队的太多,我们该当把下一帧丢弃掉而不是设置一个短的刷新时候。
·支撑收集。如今的电影播放器还不克不及播放收集流媒体。
·支撑像YUV文件一样的原始视频流。若是我们的播放器支撑的话,因为我们不克不及猜测出时基和大小,我们应当参加一些参数来进行响应的设置。
·全屏。
·多种参数,例如:不合图像格局;参考ffplay.c中的号令开关。
·其它工作,例如:在布局体中的音频缓冲区应当对齐。
若是你想懂得关于ffmpeg更多的工作,我们已经包含了此中的一项目组。下一步应当进修的就是如何来编码多媒体。一个好的入手点是在ffmpeg中的output_example.c文件。我可认为它写别的一个领导,然则我没有足够的时候来做。
好,我欲望这个领导是有益和有趣的。若是你有任何建议,题目,抱怨和讴歌等,请留言