码农终结者

浅析 requestAnimationFrame

2017/03/02 · JavaScript
· 1 评论 ·
requestAnimationFrame

原稿出处: Taobao前端团队(FED)-
腾渊   

金沙澳门官网 1

信任现在多数人在 JavaScript 中绘制动画已经在应用
requestAnimationFrame 了,关于 requestAnimationFrame
的各类就不多说了,关于这么些 API 的资料,详见
http://www.w3.org/TR/animation-timing/,https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame。

假设大家把时钟往前拨到引入 requestAnimationFrame 从前,要是在 JavaScript
中要落到实处动画效果,如何做呢?无外乎使用 set提姆eout 或
setInterval。那么难点就来了:

  • 哪些规定科学的时间距离(浏览器、机器硬件的质量各差距)?
  • 微秒的不精确性怎么解决?
  • 怎么样防止过度渲染(渲染频率太高、tab 不可知等等)?

开发者可以用比比皆是方法来减轻那些题材的病症,但是彻底解决,那么些、基本、很难。

到头来,难点的源于在于时机。对于前端开发者来说,set提姆eout 和
setInterval 提供的是一个等长的定时器循环(timer
loop),可是对于浏览器内查对渲染函数的响应以及什么日期可以发起下一个动画帧的火候,是一心不打听的。对于浏览器内核来讲,它亦可领会发起下一个渲染帧的确切时机,然而对于其余setTimeout 和 setInterval
传入的回调函数执行,都是比量齐观的,它很难了解哪个回调函数是用于动画渲染的,因而,优化的时机分外难以驾驭。悖论就在于,写
JavaScript
的人询问一帧动画片在哪行代码早先,哪行代码截至,却不打听应该几时起初,应该曾几何时甘休,而在根本引擎来说,事情却恰恰相反,所以两岸很难完美包容,直到
requestAnimationFrame 出现。

自身很欣赏 requestAnimationFrame 那一个名字,因为起得越发直白 – request
animation frame,对于那几个 API 最好的演讲就是名字本身了。那样一个
API,你传入的 API 不是用来渲染一帧卡通,你上街都不佳意思跟人通告。

鉴于自家是个喜欢读书代码的人,为了突显自己好学的态势,特意读了下 Chrome
的代码去驾驭它是怎么落实 requestAnimationFrame 的(代码基于 Android
4.4):

JavaScript

int
Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback>
callback) { if (!m_scriptedAnimationController) {
m_scriptedAnimationController =
ScriptedAnimationController::create(this); // We need to make sure that
we don’t start up the animation controller on a background tab, for
example. if (!page()) m_scriptedAnimationController->suspend(); }
return m_scriptedAnimationController->registerCallback(callback); }

1
2
3
4
5
6
7
8
9
10
11
int Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback> callback)
{
  if (!m_scriptedAnimationController) {
    m_scriptedAnimationController = ScriptedAnimationController::create(this);
    // We need to make sure that we don’t start up the animation controller on a background tab, for example.
      if (!page())
        m_scriptedAnimationController->suspend();
  }
 
  return m_scriptedAnimationController->registerCallback(callback);
}

细心看看就认为底层完结意外地差不多,生成一个 ScriptedAnimationController
的实例,然后注册那几个 callback。那我们就看看 ScriptAnimationController
里面做了些什么:

JavaScript

void ScriptedAnimationController::serviceScriptedAnimations(double
monotonicTimeNow) { if (!m_callbacks.size() || m_suspendCount) return;
double highResNowMs = 1000.0 *
m_document->loader()->timing()->monotonicTimeToZeroBasedDocumentTime(monotonicTimeNow);
double legacyHighResNowMs = 1000.0 *
m_document->loader()->timing()->monotonicTimeToPseudoWallTime(monotonicTimeNow);
// First, generate a list of callbacks to consider. Callbacks registered
from this point // on are considered only for the “next” frame, not this
one. CallbackList callbacks(m_callbacks); // Invoking callbacks may
detach elements from our document, which clears the document’s //
reference to us, so take a defensive reference.
RefPtr<ScriptedAnimationController> protector(this); for (size_t
i = 0; i < callbacks.size(); ++i) { RequestAnimationFrameCallback*
callback = callbacks[i].get(); if (!callback->m_firedOrCancelled)
{ callback->m_firedOrCancelled = true;
InspectorInstrumentationCookie cookie =
InspectorInstrumentation::willFireAnimationFrame(m_document,
callback->m_id); if (callback->m_useLegacyTimeBase)
callback->handleEvent(legacyHighResNowMs); else
callback->handleEvent(highResNowMs);
InspectorInstrumentation::didFireAnimationFrame(cookie); } } // Remove
any callbacks we fired from the list of pending callbacks. for (size_t
i = 0; i < m_callbacks.size();) { if
(m_callbacks[i]->m_firedOrCancelled) m_callbacks.remove(i); else
++i; } if (m_callbacks.size()) scheduleAnimation(); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void ScriptedAnimationController::serviceScriptedAnimations(double monotonicTimeNow)
{
  if (!m_callbacks.size() || m_suspendCount)
    return;
 
    double highResNowMs = 1000.0 * m_document->loader()->timing()->monotonicTimeToZeroBasedDocumentTime(monotonicTimeNow);
    double legacyHighResNowMs = 1000.0 * m_document->loader()->timing()->monotonicTimeToPseudoWallTime(monotonicTimeNow);
 
    // First, generate a list of callbacks to consider.  Callbacks registered from this point
    // on are considered only for the "next" frame, not this one.
    CallbackList callbacks(m_callbacks);
 
    // Invoking callbacks may detach elements from our document, which clears the document’s
    // reference to us, so take a defensive reference.
    RefPtr<ScriptedAnimationController> protector(this);
 
    for (size_t i = 0; i < callbacks.size(); ++i) {
        RequestAnimationFrameCallback* callback = callbacks[i].get();
      if (!callback->m_firedOrCancelled) {
        callback->m_firedOrCancelled = true;
        InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireAnimationFrame(m_document, callback->m_id);
        if (callback->m_useLegacyTimeBase)
          callback->handleEvent(legacyHighResNowMs);
        else
          callback->handleEvent(highResNowMs);
        InspectorInstrumentation::didFireAnimationFrame(cookie);
      }
    }
 
    // Remove any callbacks we fired from the list of pending callbacks.
    for (size_t i = 0; i < m_callbacks.size();) {
      if (m_callbacks[i]->m_firedOrCancelled)
        m_callbacks.remove(i);
      else
        ++i;
    }
 
    if (m_callbacks.size())
      scheduleAnimation();
}

这一个函数自然就是进行回调函数的地点了。那么动画是哪些被触发的吧?大家须要火速地看一串函数(一个从下往上的
call stack):

JavaScript

void PageWidgetDelegate::animate(Page* page, double
monotonicFrameBeginTime) { FrameView* view = mainFrameView(page); if
(!view) return;
view->serviceScriptedAnimations(monotonicFrameBeginTime); }

1
2
3
4
5
6
7
void PageWidgetDelegate::animate(Page* page, double monotonicFrameBeginTime)
{
  FrameView* view = mainFrameView(page);
  if (!view)
    return;
  view->serviceScriptedAnimations(monotonicFrameBeginTime);
}

JavaScript

void WebViewImpl::animate(double monotonicFrameBeginTime) {
TRACE_EVENT0(“webkit”, “WebViewImpl::animate”); if
(!monotonicFrameBeginTime) monotonicFrameBeginTime =
monotonicallyIncreasingTime(); // Create synthetic wheel events as
necessary for fling. if (m_gestureAnimation) { if
(m_gestureAnimation->animate(monotonicFrameBeginTime))
scheduleAnimation(); else { m_gestureAnimation.clear(); if
(m_layerTreeView) m_layerTreeView->didStopFlinging();
PlatformGestureEvent endScrollEvent(PlatformEvent::GestureScrollEnd,
m_positionOnFlingStart, m_globalPositionOnFlingStart, 0, 0, 0, false,
false, false, false);
mainFrameImpl()->frame()->eventHandler()->handleGestureScrollEnd(endScrollEvent);
} } if (!m_page) return; PageWidgetDelegate::animate(m_page.get(),
monotonicFrameBeginTime); if (m_continuousPaintingEnabled) {
ContinuousPainter::setNeedsDisplayRecursive(m_rootGraphicsLayer,
m_pageOverlays.get()); m_client->scheduleAnimation(); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void WebViewImpl::animate(double monotonicFrameBeginTime)
{
  TRACE_EVENT0("webkit", "WebViewImpl::animate");
 
  if (!monotonicFrameBeginTime)
      monotonicFrameBeginTime = monotonicallyIncreasingTime();
 
  // Create synthetic wheel events as necessary for fling.
  if (m_gestureAnimation) {
    if (m_gestureAnimation->animate(monotonicFrameBeginTime))
      scheduleAnimation();
    else {
      m_gestureAnimation.clear();
      if (m_layerTreeView)
        m_layerTreeView->didStopFlinging();
 
      PlatformGestureEvent endScrollEvent(PlatformEvent::GestureScrollEnd,
          m_positionOnFlingStart, m_globalPositionOnFlingStart, 0, 0, 0,
          false, false, false, false);
 
      mainFrameImpl()->frame()->eventHandler()->handleGestureScrollEnd(endScrollEvent);
    }
  }
 
  if (!m_page)
    return;
 
  PageWidgetDelegate::animate(m_page.get(), monotonicFrameBeginTime);
 
  if (m_continuousPaintingEnabled) {
    ContinuousPainter::setNeedsDisplayRecursive(m_rootGraphicsLayer, m_pageOverlays.get());
    m_client->scheduleAnimation();
  }
}

JavaScript

void RenderWidget::AnimateIfNeeded() { if
(!animation_update_pending_) return; // Target 60FPS if vsync is on.
Go as fast as we can if vsync is off. base::TimeDelta animationInterval
= IsRenderingVSynced() ? base::TimeDelta::FromMilliseconds(16) :
base::TimeDelta(); base::Time now = base::Time::Now(); //
animation_floor_time_ is the earliest time that we should animate
when // using the dead reckoning software scheduler. If we’re using
swapbuffers // complete callbacks to rate limit, we can ignore this
floor. if (now >= animation_floor_time_ ||
num_swapbuffers_complete_pending_ > 0) {
TRACE_EVENT0(“renderer”, “RenderWidget::AnimateIfNeeded”)
animation_floor_time_ = now + animationInterval; // Set a timer to
call us back after animationInterval before // running animation
callbacks so that if a callback requests another // we’ll be sure to run
it at the proper time. animation_timer_.Stop();
animation_timer_.Start(FROM_HERE, animationInterval, this,
&RenderWidget::AnimationCallback); animation_update_pending_ = false;
if (is_accelerated_compositing_active_ && compositor_) {
compositor_->Animate(base::TimeTicks::Now()); } else { double
frame_begin_time = (base::TimeTicks::Now() –
base::TimeTicks()).InSecondsF();
webwidget_->animate(frame_begin_time); } return; }
TRACE_EVENT0(“renderer”, “EarlyOut_AnimatedTooRecently”); if
(!animation_timer_.IsRunning()) { // This code uses base::Time::Now()
to calculate the floor and next fire // time because javascript’s Date
object uses base::Time::Now(). The // message loop uses base::TimeTicks,
which on windows can have a // different granularity than base::Time. //
The upshot of all this is that this function might be called before //
base::Time::Now() has advanced past the animation_floor_time_. To //
avoid exposing this delay to javascript, we keep posting delayed //
tasks until base::Time::Now() has advanced far enough. base::TimeDelta
delay = animation_floor_time_ – now;
animation_timer_.Start(FROM_HERE, delay, this,
&RenderWidget::AnimationCallback); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void RenderWidget::AnimateIfNeeded() {
  if (!animation_update_pending_)
    return;
 
  // Target 60FPS if vsync is on. Go as fast as we can if vsync is off.
  base::TimeDelta animationInterval = IsRenderingVSynced() ? base::TimeDelta::FromMilliseconds(16) : base::TimeDelta();
 
  base::Time now = base::Time::Now();
 
  // animation_floor_time_ is the earliest time that we should animate when
  // using the dead reckoning software scheduler. If we’re using swapbuffers
  // complete callbacks to rate limit, we can ignore this floor.
  if (now >= animation_floor_time_ || num_swapbuffers_complete_pending_ > 0) {
    TRACE_EVENT0("renderer", "RenderWidget::AnimateIfNeeded")
    animation_floor_time_ = now + animationInterval;
    // Set a timer to call us back after animationInterval before
    // running animation callbacks so that if a callback requests another
    // we’ll be sure to run it at the proper time.
    animation_timer_.Stop();
    animation_timer_.Start(FROM_HERE, animationInterval, this, &RenderWidget::AnimationCallback);
    animation_update_pending_ = false;
    if (is_accelerated_compositing_active_ && compositor_) {
      compositor_->Animate(base::TimeTicks::Now());
    } else {
      double frame_begin_time = (base::TimeTicks::Now() – base::TimeTicks()).InSecondsF();
      webwidget_->animate(frame_begin_time);
    }
    return;
  }
  TRACE_EVENT0("renderer", "EarlyOut_AnimatedTooRecently");
  if (!animation_timer_.IsRunning()) {
    // This code uses base::Time::Now() to calculate the floor and next fire
    // time because javascript’s Date object uses base::Time::Now().  The
    // message loop uses base::TimeTicks, which on windows can have a
    // different granularity than base::Time.
    // The upshot of all this is that this function might be called before
    // base::Time::Now() has advanced past the animation_floor_time_.  To
    // avoid exposing this delay to javascript, we keep posting delayed
    // tasks until base::Time::Now() has advanced far enough.
    base::TimeDelta delay = animation_floor_time_ – now;
    animation_timer_.Start(FROM_HERE, delay, this, &RenderWidget::AnimationCallback);
  }
}

专门表达:RenderWidget 是在 ./content/renderer/render_widget.cc
中(content::RenderWidget)而非在 ./core/rendering/RenderWidget.cpp
中。作者最早读 RenderWidget.cpp 还因为里面没有此外关于 animation
的代码而可疑了很久。

总的来看此间实在 requestAnimationFrame 的贯彻原理就很显明了:

  • 注册回调函数
  • 浏览器更新时触发 animate
  • animate 会触发所有注册过的 callback

这边的干活机制得以知道为所有权的更换,把触发帧更新的流年所有权交给浏览器内核,与浏览器的革新保持同步。那样做既可避防止浏览器更新与动画帧更新的不一起,又足以授予浏览器足够大的优化空间。
在往上的调用入口就这几个了,很多函数(RenderWidget::didInvalidateRect,RenderWidget::CompleteInit等)会触发动画检查,从而必要三回动画帧的更新。

此地一张图表明 requestAnimationFrame
的已毕机制(来自官方):
金沙澳门官网 2

题图: By Kai Oberhäuser

1 赞 1 收藏 1
评论

金沙澳门官网 3

前言

正文首要参考w3c资料,从最底层已毕原理的角度介绍了requestAnimationFrame、cancelAnimationFrame,给出了相关的以身作则代码以及我对落到实处原理的明白和座谈。


正文中校对第5篇文章的太阳系模型举办修改,加入一些卡通效果。其它还会参预突显帧速率的代码。 

学科一:视频截图(Tutorial 01: Making Screencaps)

本文介绍

浏览器中卡通有两种完成方式:通过注明元素完成(如SVG中的

要素)松阳吉剧本已毕。

可以由此set提姆eout和setInterval方法来在剧本中落到实处动画,可是那样效果兴许不够流畅,且会占用额外的资源。可参考《Html5
Canvas要旨技术》中的论述:

它们有如下的风味:

1、即便向其传递飞秒为单位的参数,它们也不可以达标ms的准头。那是因为javascript是单线程的,可能会生出堵塞。

2、没有对调用动画的循环机制举行优化。

3、没有考虑到绘制动画的最佳时机,只是一味地以某个差不多的事件间隔来调用循环。

实在,使用setInterval或set提姆eout来贯彻主循环,根本错误就在于它们抽象等级不符合要求。我们想让浏览器执行的是一套可以控制各个细节的api,落成如“最优帧速率”、“采纳绘制下一帧的最佳时机”等职能。不过如若选拔它们来说,那么些现实的细节就亟须由开发者自己来形成。

requestAnimationFrame不须求使用者指定循环间隔时间,浏览器会基于当前页面是不是可知、CPU的载重情状等来自行决定最佳的帧速率,从而更客观地应用CPU。


     
加盟动画效果最简单的章程是响应WM_TIMER音信,在其音信处理函数中改变部分参数值,比如每过多少阿秒就旋转一定的角度,并且重绘场景。

首先我们必要明白摄像文件的一部分基本概念,视频文件本身被称作容器,例如avi或者是quicktime,容器的类型确定

名词表明

Frame Rate

了文本的音讯。然后,容器里装的事物叫流(stream),平日包罗视频流和音频流(“流”的意味其实就是“随着时间推移

动画帧请求回调函数列表

每个Document都有一个动画帧请求回调函数列表,该列表可以用作是由<
handle,
callback>元组组成的集合。其中handle是一个整数,唯一地标识了元组在列表中的地方;callback是一个无重返值的、形参为一个时间值的函数(该时间值为由浏览器传入的从1970年11月1日到当下所通过的阿秒数)。
刚开首该列表为空。

Document

Dom模型中定义的Document节点。

Active document

浏览器上下文browsingContext中的Document被指定为active document。

browsingContext

浏览器上下文。

浏览器上下文是显示document对象给用户的条件。
浏览器中的1个tab或一个窗口包括一个超级浏览器上下文,假若该页面有iframe,则iframe中也会有自己的浏览器上下文,称为嵌套的浏览器上下文。

DOM模型

详尽我的知情DOM。

document对象

当html文档加载成功后,浏览器会成立一个document对象。它对应于Document节点,达成了HTML的Document接口。
通过该目标可获得全套html文档的音讯,从而对HTML页面中的所有因素举行走访和操作。

HTML的Document接口

该接口对DOM定义的Document接口举办了增加,定义了 HTML 专用的品质和章程。

详见The Document
object

页面可知

当页面被最小化或者被切换成后台标签页时,页面为不可知,浏览器会触发一个
visibilitychange事件,并设置document.hidden属性为true;切换来浮现状态时,页面为可知,也一样触发一个
visibilitychange事件,设置document.hidden属性为false。

详见Page
Visibility、Page
Visibility(页面可知性)
API介绍、微拓展

队列

浏览器让一个单线程共用来实施javascrip和革新用户界面。那几个线程平时被称为“浏览器UI线程”。
浏览器UI线程的办事按照一个简便的队列系统,义务会被封存到行列中直到进度空闲。一旦空闲,队列中的下一个任务就被再度提取出来并运行。那些职分仍旧是运行javascript代码,要么执行UI更新,包罗重绘和重排。

API接口

Window对象定义了以下三个接口:

partial interface Window {

long requestAnimationFrame(FrameRequestCallback callback);

void cancelAnimationFrame(long handle);

};


Frame rate is nothing but the number of frames that can be rendered per
second. The higher this rate, the smoother the animation. In order to
calculate the frame rate we retrieve the system time (using the Windows
multimedia API function timeGetTime()) before the rendering is
performed and after the buffer is swapped. The difference between the
two values is the elapsed time to render one frame. Thus we can
calculate the frame rate for a given application.

的一段连接的多少元素”)。流中的多少元素叫做“帧”。每个流由分歧的编解码器来编码,编解码器定义了多少如何编码

requestAnimationFrame

requestAnimationFrame方法用于文告浏览珍惜采样动画。

当requestAnimationFrame(callback)被调用时不会执行callback,而是会将元组<
handle,callback>插入到动画帧请求回调函数列表末尾(其中元组的callback就是传播requestAnimationFrame的回调函数),并且再次来到handle值,该值为浏览器定义的、大于0的整数,唯一标识了该回调函数在列表中地点。

每个回调函数都有一个布尔标识cancelled,该标识初始值为false,并且对外不可知。

在前面的“处理模型”
中我们会看到,浏览器在实践“采样所有动画”的任务时会遍历动画帧请求回调函数列表,判断每个元组的callback的cancelled,借使为false,则举行callback。

1,大家供给调用timeGetTime()函数,因此在stdafx.h中加入:

(COded)息争码(DECoded),所以称为编解码器(CODEC)。编解码器的例证有Divx和mp4。包(Packets),是从流中

cancelAnimationFrame

cancelAnimationFrame 方法用于撤销在此从前计划的一个动画帧更新的请求。

当调用cancelAnimationFrame(handle)时,浏览器会设置该handle指向的回调函数的cancelled为true。

不论是该回调函数是或不是在动画帧请求回调函数列表中,它的cancelled都会被安装为true。

万一该handle没有对准任何回调函数,则调用cancelAnimationFrame
不会发出任何业务。

#include <mmsystem.h>        // for MM timers (you’ll need WINMM.LIB)

读取的,通过解码器解包,得到原始的帧,大家就足以对那一个数量进行广播等的拍卖。对于我们来说,每个包包括完整的帧,

处理模型

当页面可知并且动画帧请求回调函数列表不为空时,浏览器会定期地加入一个“采样所有动画”的任务到UI线程的行列中。

此间使用伪代码来注脚“采样所有动画”职务的执行步骤:

var list = {};

var browsingContexts = 浏览器一级上下文及其属下的浏览器上下文;

for (var browsingContext in browsingContexts) {

var time = 从1970年六月1日到当前所经过的微秒数;

var d = browsingContext的active document; 
//即当前浏览器上下文中的Document节点

//如果该active document可见

if (d.hidden !== true) {

//拷贝active document的动画帧请求回调函数列表到list中,并清空该列表

var doclist = d的动画帧请求回调函数列表

doclist.appendTo(list);

clear(doclist);

}

//遍历动画帧请求回调函数列表的元组中的回调函数

for (var callback in list) {

if (callback.cancelled !== true) {

try {

//每个browsingContext都有一个应和的WindowProxy对象,WindowProxy对象会将callback指向active
document关联的window对象。

//传入时间值time

callback.call(window, time);

}

//忽略相当

catch (e) {

}

}

}

}

并且Link—>Object/library modules中加入winmm.lib

要么多少个音频帧。

已解决的难点

为啥在callback内部推行cancelAnimationFrame无法撤消动画?

题材讲述

如上面的代码会一向执行a:

var id = null;

function a(time) {

console.log(“animation”);

window.cancelAnimationFrame(id); //不起功用

id = window.requestAnimationFrame(a);

}

a();

由来分析

大家来分析下那段代码是什么样履行的:

1、执行a

(1)执行“a();”,执行函数a;

(2)执行“console.log(“animation”);”,打印“animation”;

(3)执行“window.cancelAnimationFrame(id);”,因为id为null,浏览器在动画帧请求回调函数列表中找不到对应的callback,所以不发生其余事情;

(4)执行“id = window.requestAnimationFrame(a);”,浏览器会将一个元组<
handle,
a>插入到Document的动画帧请求回调函数列表末尾,将id赋值为该元组的handle值;

2、a执行落成后,执行首个“采样所有动画”的天职

如果当前页面从来可知,因为动画帧请求回调函数列表不为空,所以浏览器会定期地进入一个“采样所有动画”的职分到线程队列中。

a执行达成后的率先个“采样所有动画”的任务执行时会进行以下步骤:

(1)拷贝Document的动画帧请求回调函数列表到list变量中,清空Document的动画帧请求回调函数列表;

(2)遍历list的列表,列表有1个元组,该元组的callback为a;

(3)判断a的cancelled,为默许值false,所以执行a;

(4)执行“console.log(“animation”);”,打印“animation”;

(5)执行“window.cancelAnimationFrame(id);”,此时id指向当前元组的a(即眼前正在执行的a),浏览器将

当前元组

的a的cancelled设为true。

(6)执行“id = window.requestAnimationFrame(a);”,浏览器会将

新的元组< handle, a>

插入到Document的动画帧请求回调函数列表末尾(新元组的a的cancelled为默许值false),将id赋值为该元组的handle值。

3、执行下一个“采样所有动画”的职责

随即一个“采样所有动画”的任务履行时,会判定动画帧请求回调函数列表的元组的a的cancelled,因为该元组为新插入的元组,所以值为默许值false,因而会连续执行a。

如此类推,浏览器会一向循环执行a。

解决方案

有上边三个方案:

1、执行requestAnimationFrame之后再进行cancelAnimationFrame。

上边代码只会实施三回a:

var id = null;

function a(time) {

console.log(“animation”);

id = window.requestAnimationFrame(a);

window.cancelAnimationFrame(id);

}

a();

2、在callback外部执行cancelAnimationFrame。 上边代码只会执行一遍a:

function a(time) {

console.log(“animation”);

id = window.requestAnimationFrame(a);

}

a();

window.cancelAnimationFrame(id);

因为执行“window.cancelAnimationFrame(id);”时,id指向了新插入到动画帧请求回调函数列表中的元组的a,所以
“采样所有动画”义务判断元组的a的cancelled时,该值为true,从而不再执行a。

注意事项

1、在处理模型
中大家早已观察,在遍历执行拷贝的动画帧请求回调函数列表中的回调函数之前,Document的动画帧请求回调函数列表已经被清空了。由此即使要再三执行回调函数,需求在回调函数中再一次调用requestAnimationFrame将涵盖回调函数的元组加入到Document的动画帧请求回调函数列表中,从而浏览器才会再一次定期进入“采样所有动画”的职分(当页面可知并且动画帧请求回调函数列表不为空时,浏览器才会进入该义务),执行回调函数。

譬如下边代码只进行1次animate函数:

var id = null;

function animate(time) {

console.log(“animation”);

}

window.requestAnimationFrame(animate);

上面代码会一贯执行animate函数:

var id = null;

function animate(time) {

console.log(“animation”);

window.requestAnimationFrame(animate);

}

animate();

2、尽管在执行回调函数或者Document的动画帧请求回调函数列表被清空从前反复调用requestAnimationFrame插入同一个回调函数,那么列表中会有多个元组指向该回调函数(它们的handle不一致,但callback都为该回调函数),“采集所有动画”职分会举办多次该回调函数。

2,为了总计绘制用时,在CCY457OpenGLView.h中加入如下变量:

在低档的水平,处理音视频流是很是不难的:

例如上面的代码在推行“id1 = window.requestAnimationFrame(animate);”和“id2

window.requestAnimationFrame(animate);”时会将四个元组(handle分别为id1、id2,回调函数callback都为animate)插入到Document的动画帧请求回调函数列表末尾。
因为“采样所有动画”职责会遍历执行动画帧请求回调函数列表的各种回调函数,所以在“采样所有动画”义务中会执行五回animate。

//上面代码会打印四遍”animation”

var id1 = null,

id2 = null;

function animate(time) {

console.log(“animation”);

}

id1 = window.requestAnimationFrame(animate);

id2 = window.requestAnimationFrame(animate); 
//id1和id2值差距,指向列表中区其他元组,这些元组中的callback都为同一个animate

包容性方法

上面为《HTML5 Canvas
主题技术》给出的匹配主流浏览器的requestNextAnimationFrame
和cancelNextRequestAnimationFrame方法,我们可径直拿去用:

window.requestNextAnimationFrame = (function () {

var originalWebkitRequestAnimationFrame = undefined,

wrapper = undefined,

callback = undefined,

geckoVersion = 0,

userAgent = navigator.userAgent,

index = 0,

self = this;

// Workaround for Chrome 10 bug where Chrome

// does not pass the time to the animation function

if (window.webkitRequestAnimationFrame) {

// Define the wrapper

wrapper = function (time) {

if (time === undefined) {

time = +new Date();

}

self.callback(time);

};

// Make the switch

originalWebkitRequestAnimationFrame =
window.webkitRequestAnimationFrame;

window.webkitRequestAnimationFrame = function (callback, element) {

self.callback = callback;

// Browser calls the wrapper and wrapper calls the callback

originalWebkitRequestAnimationFrame(wrapper, element);

}

}

// Workaround for Gecko 2.0, which has a bug in

// mozRequestAnimationFrame() that restricts animations

// to 30-40 fps.

if (window.mozRequestAnimationFrame) {

// Check the Gecko version. Gecko is used by browsers

// other than Firefox. Gecko 2.0 corresponds to

// Firefox 4.0.

index = userAgent.indexOf(‘rv:’);

if (userAgent.indexOf(‘Gecko’) != -1) {

geckoVersion = userAgent.substr(index + 3, 3);

if (geckoVersion === ‘2.0’) {

// Forces the return statement to fall through

// to the setTimeout() function.

window.mozRequestAnimationFrame = undefined;

}

}

}

return  window.requestAnimationFrame ||

window.webkitRequestAnimationFrame ||

window.mozRequestAnimationFrame ||

window.oRequestAnimationFrame ||

window.msRequestAnimationFrame ||

function (callback, element) {

var start,

finish;

window.setTimeout(function () {

start = +new Date();

callback(start);

finish = +new Date();

self.timeout = 1000 / 60 – (finish – start);

}, self.timeout);

};

}());

window.cancelNextRequestAnimationFrame =
window.cancelRequestAnimationFrame

|| window.webkitCancelAnimationFrame

|| window.webkitCancelRequestAnimationFrame

|| window.mozCancelRequestAnimationFrame

|| window.oCancelRequestAnimationFrame

|| window.msCancelRequestAnimationFrame

|| clearTimeout;


参考资料

Timing control for script-based
animations

Browsing
contexts

The Document
object

《HTML5 Canvas主旨技术》

理解DOM

Page
Visibility

Page Visibility(页面可知性)
API介绍、微拓展

HOW BROWSERS WORK: BEHIND THE SCENES OF MODERN WEB
BROWSERS

    //For elapsed timing calculations
    DWORD m_StartTime, m_ElapsedTime, m_previousElapsedTime;    
    CString m_WindowTitle;    //Window Title
    int DayOfYear;
    int HourOfDay;

从video.avi中得到视频流

并在构造函数中展开初阶化:

从视频流中解包获得帧

CCY457OpenGLView::CCY457OpenGLView()
{
    DayOfYear = 1;
    HourOfDay = 1;
}

假若帧不完整,重复第2步

3,为了总结帧速率,修改OnCreate函数,在里面获得窗口标题,从题目中去掉”Untitled”字样,并启动定时器;

对帧进行有关操作

4,同样为了总计帧速率,修改OnDraw函数如下,在里面用glPushMatrix 和
glPopMatrix将RenderScene函数包裹起来,从而确保动画会正确运行。在SwapBuffers调用后大家调用PostRenderScene来显示帧速率音信到窗口标题。

重复第2步

void CCY457OpenGLView::OnDraw(CDC* pDC)
{
    CCY457OpenGLDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    // Get the system time, in milliseconds.
    m_ElapsedTime = ::timeGetTime(); // get current time
    if ( ElapsedTimeinMSSinceLastRender() < 30 )
        return
    // Clear out the color & depth buffers
    ::glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    glPushMatrix();
        RenderScene();
    glPopMatrix();
    // Tell OpenGL to flush its pipeline
    ::glFinish();
    // Now Swap the buffers
    ::SwapBuffers( m_pDC->GetSafeHdc() );
    //Perform Post Display Processing
    // Only update the title every 15 redraws (this is about
    // every 1/2 second)
    PostRenderScene();
    // the very last thing we do is to save
    // the elapsed time, this is used with the
    // next elapsed time to calculate the
    // elapsed time since a render and the frame rate
    m_previousElapsedTime = m_ElapsedTime;
}

用ffmpeg来处理多媒体就像是上边的手续那么不难,尽管你的第4步可能很复杂。所以在本教程,大家先开辟一个视频,

4,在CCY457OpenGLView类中加入下述成员函数,用来显示帧速率音信到窗口标题

读取视频流,得到帧,然后第4步是把帧数据存储为PPM文件。

//////////////////////////////////////////////////////////////////////////////
// PostRenderScene
// perform post display processing
// The default PostRenderScene places the framerate in the
// view’s title. Replace this with your own title if you like.
void CCY457OpenGLView::PostRenderScene( void )
{
    // Only update the title every 15 redraws (this is about
    // every 1/2 second)
    static int updateFrame = 15;
    if (16 > ++updateFrame )
        return;
    updateFrame = 0;
    char string[256];
    _snprintf( string, 200, “%s ( %d Frames/sec )”,
        (const char*)m_WindowTitle, FramesPerSecond() );
    GetParentFrame()->SetWindowText( string );
}
//////////////////////////////////////////////////////////////////////////////
// FramesPerSecond
// fetch frame rate calculations
int CCY457OpenGLView::FramesPerSecond( void )
{
    double eTime = ElapsedTimeinMSSinceLastRender();
    if ( 0 == (int)eTime )
        return 0;
    return (int)(1000/(int)eTime);
}
DWORD ElapsedTimeinMSSinceLastStartup()
{
    return(m_ElapsedTime – m_StartTime);
}
DWORD ElapsedTimeinMSSinceLastRender()
{
    return(m_ElapsedTime – m_previousElapsedTime);
}

打开文件

5,在OnTimer函数中,通过增加变量DayOfYear 和
HourOfDay的值来控制地球和月亮的职位,并且调用InvalidateRect来刷新界面。

咱俩先来看一下怎么打开一个视频文件,首先把头文件蕴涵进来

void CCY457OpenGLView::OnTimer(UINT nIDEvent) 
{
    if(DayOfYear < 365)
        DayOfYear++;
    else
        DayOfYear = 1;
    if(HourOfDay < 365)
        HourOfDay++;
    else
        HourOfDay = 1;
    InvalidateRect(NULL, FALSE);    
    CView::OnTimer(nIDEvent);
}

#include #include #include

6,在RenderScene中投入绘制代码:

void CCY457OpenGLView::RenderScene ()
{//绘制函数
    glTranslatef(0.0f,0.0f,-5.0f);
    //Draw the Sun
    glutWireSphere(1.0f,20,20);
    //Rotate the Planet in its orbit
    glRotatef((GLfloat) (360.0*DayOfYear)/365.0, 0.0f, 1.0f, 0.0f);
    glTranslatef(4.0f,0.0f,0.0f);
    glPushMatrix();
        //Rotate the Planet in its orbit
        glRotatef((GLfloat)(360*HourOfDay)/24.0, 0.0f,1.0f,0.0f);
        //Draw the Planet
        glutWireSphere(0.2f,20,20);
    glPopMatrix();
    glRotatef((GLfloat) (360.0*12.5*DayOfYear)/365.0, 0.0f, 1.0f, 0.0f);
    glTranslatef(0.5f,0.0f,0.0f);
    //Draw the Moon
    glutWireSphere(0.01f,20,20);
}

int main(int argc, char *argv[]){

av_register_all();

av_register_all只须求调用五遍,他会登记所有可用的文件格式和编解码库,当文件被打开时他俩将自动匹配相应的编

解码库。假设你愿意,能够只登记个其余文件格式和编解码库。

今天的确要开辟一个文书了:

AVFormatContext *pFormatCtx;

if(av_open_input_file(&pFormatCtx,argv[1],NULL,0,NULL)!=0)

return -1;

从传播的率先个参数得到文件路径,那个函数会读取文件头音信,并把新闻保存在pFormatCtx结构体当中。那么些函数后

面三个参数分别是:指定文件格式、缓存大小和格式化选项,当我们设置为NULL或0时,libavformat会自动落成那个干活儿。

那个函数仅仅是取得了头新闻,接下去大家要赢得流音讯:

if(av_find_steam_info(pFormatCtx)<0)

return -1

本条函数填充了pFormatCtx->streams流新闻,可以经过dump_format把音信打印出来:dump_format(pFormatCtx,
0, argv[1], 0);

pFromatCtx->streams只是大小为pFormateCtx->nb_streams的一文山会海的点,我们要从中得到视频流:int
i;

1·35

AVCodecContext *pCodecCtx;

// Find the first video streamvideoStream=-1;

for(i=0; inb_streams; i++)

if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO)
{videoStream=i;

break;

}

if(videoStream==-1)

return -1; // Didn’t find a video stream

// Get a pointer to the codec context for the video stream

pCodecCtx=pFormatCtx->streams[videoStream]->codec;

pCodecCtx包罗了那么些流在用的编解码的享有新闻,但大家仍急需经过她取得一定的解码器然后打开他。

AVCodec *pCodec;

pCodec=avcodec_find_decoder(pCodecCtx->codec_id);

if(pCodec==NULL) {

fprintf(stderr, “Unsupported codec!\n”);

return -1; // Codec not found

}

// Open codec

if(avcodec_open(pCodecCtx, pCodec)<0)

return -1; // Could not open codec

储存数据

现行大家须求一个地点来存储一帧:

AVFrame *pFrame;

pFrame=avcodec_alloc_frame();

俺们布署存储的PPM文件,其储存的数码是24位RGB,大家需求把得到的一帧从本地格式转换为RGB,ffmpeg可以帮

大家做到那些工作。在许多工程里,大家都梦想把原始帧转换来特定格式。现在就让我们来形成那么些工作吗。

AVFrame *pFrameRGB;

pFrameRGB=avcodec_alloc_frame();

if(pFrameRGB==NULL)

return -1;

即使分红了帧空间,大家如故要求空间来存放转换时的raw数据,大家用avpicture_get_size来赢得须求的空中,然后

手动分配。

uint8_t *buffer;

int numBytes;

numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);buffer=(uint8_t
*)av_malloc(numBytes*sizeof(uint8_t));

av_malloc是ffmpeg简单包装的一个分红函数,目的在于保管内存地址的对齐等,它不会维护内存泄漏、二次释放或任何malloc难点。

如今,大家运用avpicture_fill来波及新分配的缓冲区的帧。AVPicture结构体是AVFrame结构体的一个子集,起始的AVFrame是和AVPicture相同的。

// Assign appropriate parts of buffer to image planes in pFrameRGB

// Note that pFrameRGB is an AVFrame, but AVFrame is a superset of
AVPicture

2·35

avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);

下一步大家准备读取流了!

读取数据

大家要做的是通过包来读取整个视频流,然后解码到帧当中,一但一帧完结了,将转移并保留它(那里跟教程的接口

调用有不平等的地点)。

int frameFinished;

AVPacket packet;

i=0;

while(av_read_frame(pFormatCtx, &packet)>=0) {

// Is this a packet from the video stream?

if(packet.stream_index==videoStream) {

// Decode video frame

int result;

avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished, &packet);

// Did we get a video frame?

if(frameFinished) {

// Convert the image from its native format to RGB

img_convert_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width,

pCodecCtx->height, PIX_FMT_RGB24, SWS_BICUBIC,NULL, NULL,NULL);

result = sws_scale(img_convert_ctx, (const uint8_t*
const*)pFrame->data, pFrame->linesize,

0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);

printf(“get result is %d~~~~~\n”,result);

// Save the frame to disk

printf(“i is %d \n”,i);

if(++i<=5)

SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);

}

}

// Free the packet that was allocated by av_read_frame

av_free_packet(&packet);

}

现今内需做的政工就是写SaveFrame函数来保存数据到PPM文件。void
SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {

FILE *pFile;

char szFilename[32];

int y;

printf(“start sws_scale\n”);

// Open file

sprintf(szFilename, “frame%d.ppm”, iFrame);pFile=fopen(szFilename,
“wb”);if(pFile==NULL){

printf(“pFile is null”);

return;

3·35

}

// Write header

fprintf(pFile, “P6\n%d %d\n255\n”, width, height);

// Write pixel data

for(y=0; y

fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3,
pFile);

// Close file

fclose(pFile);

}

我们做了一些标准文件打开,然后写RGB数据,一回写一行文件,PPM文件就是简约地把RGB新闻保存为一长串。头

部记录着宽和高,和RGB的最大尺寸。

现行归来main函数,读完摄像流后,大家须要自由全部:// Free the RGB image

av_free(buffer);

av_free(pFrameRGB);

// Free the YUV frame

av_free(pFrame);

// Close the codec

avcodec_close(pCodecCtx);

// Close the video file

av_close_input_file(pFormatCtx);

return 0;

那些就是全部代码来,现在你要求编译和运作

gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lswscale -lz

得到tutorial01,执行以下语句可取得同级目录下的5个PPM文件

./tutorial01 hello.mp4

学科二:输出到显示器(Tutorial 02: Outputting to the Screen)SDL与摄像

咱们运用SDL来把视频输出到显示器。SDL也就是Simple Direct
Layer,它是多媒体里一个充裕棒的跨平台库,在很多品种

中都有利用到。可以从官方网站得到库文件和血脉相通文档,在里面看到汉语的介绍文档。其实也可以运用apt-get来设置库和

对应的头文件,如:sudo apt-get install libsdl1.2-dev

SDL提供了累累把图画画到显示屏上的点子,而且有特意为视频播放到显示屏的组件,叫做YUV层,YUV(技术上叫YCbCr)是一种像RGB格式一样的仓储
原始图片的章程,粗略地说,Y是亮度分量,U和V是颜色分量(它比RGB复杂,因为有的颜

色新闻或者会被舍弃,2个Y样本可能只有1个U样本和1个V样
本)。SDL的YUV层放置一组YUV数据并将它们突显出来,

它接济4种YUV格式,但突显YV12最快,另一种YUV格式YUV420P与YV12一
样,除非U和V阵列沟通了。420的意趣

是其二次采样比例为4:2:0,基本的情趣是4个亮度分量对应1个颜色分量,所以颜色分量是四等分的。那是节约带宽的一

4·35

种很好的方法,基于人类对与这种转移不灵敏。“P”的意味是该格式是“planar”,一句话来说就是YUV分别在单独的数组中。ffmpeg可以把图像转换为YUV420P,现在比比皆是视频流格式已经是它了,或者很简单就能转换成那种格式。

那就是说,现在我们的安插是把课程1的SaveFrame函数替换掉,换成在屏幕中突显大家的视频,可是,首先必要驾驭怎

么使用SDL库,第一步是富含头文件和初步化SDL。

#include

#include

if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){

fprintf(stderr, “Could not initialize SDL – %s\n”, SDL_GetError());

exit(1);

}

SDL_Init本质上是告诉库大家须求动用什么效益。SDL_GetError是一个手工除错函数。

创制呈现画面

近年来内需在屏幕某个区域上放上一些东西,SDL里突显图像的区域叫做华为平板:SDL_Surface
*screen;

screen = SDL_SetVideoMode(pCodecCtx->width,
pCodecCtx->height,0,0);if(!screen){

fprintf(stderr, “SDL: could not set video mode – exiting\n”);

exit(1);

}

那就创设了一个给定长和宽的显示器,下一个参数是屏幕的水彩深浅–0表示使用当前显示屏的颜料深浅。

前天我们在屏幕创立了一个YUV overlay,可以把录像放进去了。

SDL_Overlay *bmp;

bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
SDL_YV12_OVERLAY, screen);就像是以前说的那么,用YV12来突显图像。

播音图像

那一个曾经够用简单,现在固然播放图像就好了。让我们来看一下是怎么样处理完了后的帧的。大家可以解脱从前处理RGB帧的章程,用播放代码代替以前的SaveFrame函数,为了播放图像,必要创制AVPicture结构体和装置其指针和初步化YUV

overlay。

if(frameFinished){SDL_LockYUVOverlay(bmp);AVPicture pict;

pict.data[0] = bmp->pixels[0];pict.data[1] =
bmp->pixels[2];pict.data[2] = bmp->pixels[1];

pict.linesize[0] = bmp->pitches[0];

pict.linesize[1] = bmp->pitches[2];

pict.linesize[2] = bmp->pitches[1];

// Convert the image into YUV format that SDL uses

img_convert_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height, pCodecCtx->pix_fmt,

pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P,
SWS_BICUBIC,NULL, NULL,NULL);

sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data,

pFrame->linesize, 0, pCodecCtx->height, pict.data,
pict.linesize);5·35

SDL_UnlockYUVOverlay(bmp);

}

首先要把图层锁住,因为我们要往上面写东西,那是一个幸免未来发现难题的好习惯。就好像前边所突显那样,AVPicture结构体有一个多少指针指向一个有多个元素的数码指针,因为大家处理的YUV420P唯有三通道,所以只要设置三组数据。

任何格式可能有第四组数据来存储alpha值或者其他东西。linesize就如它名字,在YUV层中lineszie与pitches相同(pitches是在SDL里用来表示指定行数据小幅的值),所以把pict的linesize指向须求的半空中地址,这样当大家向pict里面写东西时,

实际是写进了overlay里面,那里已经分配好了要求的空间。相似地,可以平昔从overlay里得到linesize的信息,转换格

式为YUV420P,之后的动作就像是在此之前一样。

绘图图像

但大家照例需要告诉SDL显示已经放进去的多少,要传播一个标志电影地点、宽度、高度、缩放比例的矩形参数。那

样SDL就足以用显卡做飞快缩放。

SDL_Rect rect;

rect.x = 0;

rect.y = 0;

rect.w = pCodecCtx->width;

rect.h = pCodecCtx->height;SDL_DisplayYUVOverlay(bmp, &rect);

后天,影片初始广播了。

让咱们来看望SDL的另一个特色,事件系统,SDL被装置为但你点击,鼠标经过或者给它一个信号的时候,它会时有发生

一个轩然大波,程序通过检查那些事件来拍卖相关的用户输入,程序也足以向SDL事件系统发送事件,当用SDL来编排多任务程

序的时候特意有用,我们将会在教程4里面领略。在这些顺序中,我们会处理完包后轮换事件(将拍卖SDL_QUIT以便于程

序结束)。

SDL_码农终结者。Event event;

av_free_packet(&packet);

SDL_PollEvent(&event);

switch(event.type) {

case SDL_QUIT:

SDL_Quit();

exit(0);

break;

default:

break;

}

让大家去掉旧的代码初始编译,首先实施:sdl-config –cflags –libs

再起来编译代码:gcc -o tutorial02 tutorial02.c -lavutil -lavformat
-lavcodec -lswscale -lSDL -lz –lm

学科三:播放声音(Tutorial 03: Playing Sound)音频

近来我们想播放音乐。SDL同样提供出口声音的格局,SDL_Open奥迪(Audi)o()函数用来开辟音频设备,它用SDL_奥迪(Audi)oSpec作为结构体,包蕴了装有大家须要的节奏信息。

在浮现如何树立那一个东西事先,首先分析一下总计机是何许处理音频的。数码音频由一长串采样流组成。每个样本值

意味着声音波形的一个数值。声音依照一个一定的采样率被记录着,一言以蔽之就采样率是以多快的进程来播音每个采样,也即

是每分钟记录多少个采样点。例如采样率为22050和44100功用常用来电台和CD。此外,大多音频不止一个通路来代表立

体声或者环绕,例如,如若采样是立体声的,会同时存入两大路采样信号。当大家从电影里获取数据时,不知道能够获取多

少路的采样信号,不会给大家有的采样,也就是说它不会把立体声分开处理。

6·35

SDL播放音频的法门是这么的:你要安装好点子相关的选项,采样率(在SDL结构体里面叫做频率“freq”),通道数和

任何参数,还设置了一个回调函数和用户数量。当起始广播音频,SDL会不停地调用回调函数来必要它把声音缓冲数据填充

进一个一定数量的字节流里面。当把那几个信息写到SDL_奥迪(Audi)oSpec结构体里面后,调用SDL_Open奥迪o(),它会开启声音设

备和重返另一个奥迪oSpec结构体给我们。这么些结构体是大家实在利用的,因为大家不可能担保我必要怎么着就拿走什么样。

设置音乐

先记住下面这一个,因为咱们还一直不有关音频流的相干新闻!回到我们事先写的代码,看看是怎么找到摄像流,同样也可

以用同一的法门找到音频流。

// Find the first video streamvideoStream=-1;

audioStream=-1;

for(i=0; inb_streams; i++) {

if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO
&& videoStream < 0) {

videoStream=i;

}

if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO
&& audioStream < 0) {

audioStream=i;

}

}

if(videoStream==-1)

return -1; // Didn’t find a video stream

if(audioStream==-1)

return -1;

如今可以从AVCodecContext获得所有大家想要的东西,就像处理摄像流那样:

AVCodecContext *aCodecCtx;

aCodecCtx=pFormatCtx->streams[audioStream]->codec;

那个编解码内容是树立音频所急需的全体内容:

// Set audio settings from codec infowanted_spec.freq =
aCodecCtx->sample_rate;wanted_spec.format =
AUDIO_S16SYS;wanted_spec.channels =
aCodecCtx->channels;wanted_spec.silence = 0;

wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;wanted_spec.callback =
audio_callback;wanted_spec.userdata =
aCodecCtx;if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {

fprintf(stderr, “SDL_OpenAudio: %s\n”, SDL_GetError());

return -1;

}

先来推广一下:

freq:采样率,似乎往日解释的那么。

format:这么些会告知SDL,大家会给它怎么着格式。“S16SYS”中的“S”是有记号的情致,16的情致是各种样本是16

位,“SYS”表示字节顺序根据近日系统的顺序。这一个格式是从avcodec_decode_audio2得到以来设置到点子输入中。channels:声音的大道数.

silence:那是用来代表静音的值。因为声音是有标志的,所以静音的值一般为0。

7·35

samples:那些值是节奏缓存,它让大家设置当SDL请求越多音频数据时大家应当给它多大的数码。其值为512到8192中间为佳,ffmpeg用的值是1024

callback:那是回调函数,那几个前面大家会详细谈论。

userdata:SDL会回调一个回调函数运行的参数。大家将让回调函数得到任何编解码的上下文;你将会在后边知道原因。

末段,大家使用SDL_OpenAudio来打开音频。

若是你还记得前边的学科,大家照样需求打开音响编解码器本身,那是众所周知的。

AVCodec *aCodec;

Codec = avcodec_find_decoder(aCodecCtx->codec_id);if(!aCodec) {

fprintf(stderr, “Unsupported codec!\n”);

return -1;

}

avcodec_open(aCodecCtx, aCodec);

队列

现今备选上马把拍子音信从流里面拿出来。不过大家用那些音信来干什么?大家打算持续地从电影文件之中取出包,但

同时SDL在调用回调函数!解决办法是建立部分大局结构体,使得到的节拍包有地点存放,同时鸣响回调函数可以从这些地

方拿到数码!所以接下去要做的事情就是创办一个包的系列。在ffmpeg中提供了一个结构体来接济大家:AVPacketList,实际

上只是一个包的链表。上边就是队列结构体:

typedef struct PacketQueue {AVPacketList *first_pkt, *last_pkt;int
nb_packets;

int size;

SDL_mutex *mutex;

SDL_cond *cond;

} PacketQueue;

首先,大家应该提议nb_packets是与size不均等的,size代表从packet->size中获得的字节数。你会小心到结构体中有

互斥量mutex和一个条件变量cond。那是因为SDL是在一个独门的线程中做音频处理的。假设没有正确地锁定这一个队列,

就可能搞乱数据。大家将看到那么些队列是什么运转的。每个程序员都应该通晓怎么开创一个队列,但大家会蕴藏那些以至于

你可以学习到SDL的函数。

第一编写一个函数来开头化队列:

void packet_queue_init(PacketQueue *q) {

memset(q, 0, sizeof(PacketQueue));

q->mutex = SDL_CreateMutex();

q->cond = SDL_CreateCond();

}

下一场编写别的一个函数来把东西放到队列当中:

int packet_queue_put(PacketQueue *q, AVPacket *pkt) {AVPacketList
*pkt1;

if(av_dup_packet(pkt) < 0) {

return -1;

}

pkt1 = av_malloc(sizeof(AVPacketList));

if (!pkt1)

return -1;

pkt1->pkt = *pkt;

8·35

pkt1->next = NULL;

SDL_LockMutex(q->mutex);

if (!q->last_pkt)

q->first_pkt = pkt1;

else

q->last_pkt->next = pkt1;

q->last_pkt = pkt1;q->nb_packets++;

q->size += pkt1->pkt.size;SDL_CondSignal(q->cond);

SDL_UnlockMutex(q->mutex);

return 0;

}

SDL_LockMutex()用来锁住队列里的互斥量,那样就足以往队列之中加东西了,然后SDL_CondSignal()会通过标准变量发

送一个信号给接受函数(若是它在守候的话)来告诉它现在早已有数量了,然后解锁互斥量。

上边是对应的收纳函数。注意SDL_CondWait()是如何根据须要让函数阻塞block的(例如向来等到行列中有多少)。int
quit = 0;

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int
block){AVPacketList *pkt1;

int ret;

SDL_LockMutex(q->mutex);

for(;;) {

if(quit) {

ret = -1;

break;

}

pkt1 = q->first_pkt;

if (pkt1) {

q->first_pkt = pkt1->next;

if (!q->first_pkt)

q->last_pkt = NULL;

q->nb_packets–;

q->size -= pkt1->pkt.size;*pkt = pkt1->pkt;av_free(pkt1);

ret = 1;

break;

} else if (!block) {

ret = 0;

break;

} else {

SDL_CondWait(q->cond, q->mutex);

}

9·35

}

SDL_UnlockMutex(q->mutex);

return ret;

}

就好像你见到的那样,我们已经用一个但是循环包装了那个函数以便用阻塞的法子来博取数码。用SDL_CondWait()来防止

极端循环。基本上,所有的CondWait都在伺机SDL_CondSignal()
(或者SDL_CondBroadcast())发来的信号然后继续。可是,虽

然看起来是排斥的,若是一向维持着那个锁,put函数将无法往队列之中甩掉何东西!但是,SDL_CondWait()同样为我们解

锁互斥量,然后当我们获得信号后再次锁上它。

意料之外处境

你同一令人瞩目到有一个大局变量quit,用它来保管还尚未安装程序退出的信号(SDL会自动处理类似于TERM等的信号)。

否则,那些线程会永远运行下去,除非用kill
-9来收场它。ffmpeg同样提供了一个回调函数用来检测是还是不是须求退出一些被阻

塞的函数:那么些函数叫做url_set_interrupt_cb。

int decode_interrupt_cb(void) {

return quit;

}

…main() {…

url_set_interrupt_cb(decode_interrupt_cb);

SDL_PollEvent(&event);

switch(event.type) {

case SDL_QUIT:

quit = 1;

填充包

剩下来的事务就唯有建立队列了:

PacketQueue audioq;main() {

avcodec_open(aCodecCtx, aCodec);

packet_queue_init(&audioq);

SDL_PauseAudio(0);

SDL_Pause奥迪(Audi)o()最终启动了音频设备。没有数据的时候它是广播静音。

今昔,已经确立起队列,并且一度做好了填充数据包的准备。下边就进来读包的循环了:

while(av_read_frame(pFormatCtx, &packet)>=0) {

// Is this a packet from the video stream?

if(packet.stream_index==videoStream) {

// Decode video frame….

}

} else if(packet.stream_index==audioStream) {

packet_queue_put(&audioq, &packet);

10·35

} else {

av_free_packet(&packet);

}

要注意的是,把包放进队列之后没有自由它。大家将会在解码之后才会去放活那么些包。

取包

现在写audio_callback函数来读取队列之中的包,回调函数必须是以下的方式void
callback(void *userdata, Uint8 *stream,

int
len),用户数据就是给SDL的指针,stream就是就是即将写音频数据的缓冲区,还有len是缓冲区的大大小小。以下是代码:

void audio_callback(void *userdata, Uint8 *stream, int len) {

AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;

int len1, audio_size;

static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) /
2];static unsigned int audio_buf_size = 0;

static unsigned int audio_buf_index = 0;

while(len > 0) {

if(audio_buf_index >= audio_buf_size) {

/* We have already sent all our data; get more */

audio_size = audio_decode_frame(aCodecCtx, audio_buf,
sizeof(audio_buf));if(audio_size < 0) {

/* If error, output silence */

audio_buf_size = 1024;

memset(audio_buf, 0, audio_buf_size);

} else {

audio_buf_size = audio_size;

}

audio_buf_index = 0;

}

len1 = audio_buf_size – audio_buf_index;

if(len1 > len)

len1 = len;

memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);len -=
len1;

stream += len1;

audio_buf_index += len1;

}

}

以此简单的循环会从另一个函数来读取数据,叫做audio_decode_frame(),把多少存储在一个中档缓冲中,企图将字节

转变为流,当大家多少不够的时候提须求大家,当数码塞满时帮大家保留数据以使大家未来再用。那些点子缓冲的大小是ffmpeg给大家的音频帧最大值的1.5倍,以给大家一个很好的缓冲。

最后,举办音频解码,得到实在的节奏数据,audio_decode_frame:

int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t
*audio_buf, int buf_size) {

static AVPacket pkt;

static uint8_t *audio_pkt_data = NULL;static int audio_pkt_size =
0;

11·35

int len1, data_size;

for(;;) {

while(audio_pkt_size > 0) {

data_size = buf_size;

len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf,
&data_size, audio_pkt_data, audio_pkt_size);if(len1 < 0) {/* if
error, skip frame */

audio_pkt_size = 0;

break;

}

audio_pkt_data += len1;

audio_pkt_size -= len1;

if(data_size <= 0) {/* No data yet, get more frames */

continue;

}

/* We have data, return it and come back for more later */

return data_size;

}

if(pkt.data)

av_free_packet(&pkt);

if(quit) return -1;

if(packet_queue_get(&audioq, &pkt, 1) < 0) {

return -1;

}

audio_pkt_data = pkt.data;audio_pkt_size = pkt.size;

}

}

实际上任何流程开首朝向截至,当调用packet_queue_get()。大家把包从队列之中拿出来和保存其新闻。然后,一但得

到一个包就调用avcodec_decode_audio2(),他的意义似乎姐妹函数avcodec_decode_video(),唯一的区分是:一个包里富含

连绵不断一个帧,所以可能要频繁调用来解码包中所有的数目。同时记住对audio_buf强制转换,因为SDL给出的是8位缓冲指

针而ffmpeg给出的数据是16位的整型指针。同时要注意len1和data_size的距离,len1表示我们解码使用的数额在包中的

大小,data_size是事实上重返的原始声音数据的分寸。

当得到一些数据后,重回来探望是或不是需求从队列里得到更多数据仍然判断是不是已形成。假使在进程中有过多多少要处

理就保存它以过后才使用。倘诺我们完结了一个包,我们最后会自由这几个包。

就是这么!我们选用重点循环从文件得到音频并送到行列中,然后被audio_callback读取,最终把数量送给SDL,于是SDL相当于大家的声卡。编译命令如下:

gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale
-lSDL -lz -lm

视频即便依然那么快,但音频播放正常。为何吧?因为音频音讯中有采样率,大家尽量快地填写数据到声卡缓冲

中,可是动静设备会根据原先指定的采样率来进行播报。

大家大概已经准备好来开头联名音频和视频了,但首先要求一些主次的团社团。用队列的形式来社团和广播音频在一个

单身的线程中工作得很好:它使程序尤其易于控制和模块化。在初步同步音频和视频从前,必要让代码更易于处理。

12·35

课程四:成立线程(Tutorial 04: Spawning Threads)概要

上四回大家利用SDL的函数来达成扶助音频播放的功能。每当SDL须求音频时它会启动一个线程来调用大家提供的回调

函数。现在我们对摄像展开同样的处理。那样会使程序越发模块化和跟不难协调工作,尤其是当大家想往代码里面加入一起

效果。那么要从何地初始吧?

首先大家注意到主函数处理太多东西了:它运行着事件循环、读取包和处理视频解码。所以我们将把那个事物分成多少个部分:创设一个线程来担负解包;那一个包会参与到行列之中,然后由有关的视频或者音频线程来读取这么些包。音频线程从前曾经依据大家的想法建立好了;由于须要协调来播放视

频,因而创制视频线程会有点复杂。大家会把真的播放

视频的代码放在主线程。不是仅仅在每一次循环时突显视

频,而是把摄像播放整合到事件循环中。现在的想法是

解码摄像,把结果保存到另一个连串中,然后创设一个

平常事件(FF_REFRESH_EVENT)参与到事件系统中,接着

事件不断检测这一个事件。他将会在这一个队列之中播

耷拉一帧。那里有一个图来解释究竟暴发了什么工作;

重大目标是由此采纳SDL_Delay线程的事件驱动来

操纵视频的运动,可以决定下一帧摄像应该在什么时间

在显示器上显示。当大家在下一个科目中添加摄像的基础代谢

时光控制代码,就可以使视频速度播放正常了。

简化代码

俺们同样会清理一些代码。大家有所有这一个视频和韵律编解码器的音讯,将会加盟队列和缓冲和具备其余的东西。所

有那几个事物都是为了一个逻辑单元,也就是视频。所以创设一个大社团体来装载这么些新闻,把它称作VideoState。

typedef struct VideoState {

AVFormatContext *pFormatCtx;

int

AVStreamPacketQueueuint8_tunsigned intunsigned intAVPacketuint8_t

int

AVStreamPacketQueue

VideoPicture

int

SDL_mutex

SDL_cond

SDL_Thread

SDL_Thread

videoStream, audioStream;

*audio_st;

audioq;

audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];

audio_buf_size;

audio_buf_index;

audio_pkt;

*audio_pkt_data;

audio_pkt_size;

*video_st;

videoq;

pictq[VIDEO_PICTURE_QUEUE_SIZE];

pictq_size, pictq_rindex, pictq_windex;

*pictq_mutex;

*pictq_cond;

*parse_tid;

*video_tid;

char

filename[1024];

13·35

int quit;

} VideoState;

让大家来看一下见到了哪些。首先,看到中央音信:视频和音频流的格式和参数,和对应的AVStream对象。然后看到

我们把以下音频缓冲移动到这些结构体里面。那一个点子的关于信息(音频缓冲、缓冲大小等)都在相邻。大家早就给摄像添

加了另一个队列,也为解码的帧(保存为overlay)准备了缓冲(会用来作为队列,不须要一个鲜艳的队列)。VideoPicture是我们创设的(会在后来看看其中有啥事物)。同样令人瞩目到结构体还分配指针额外成立的线程,退出标志和视频的文本名。

近日归来主函数,看看怎么样修改代码,首先设置VideoState结构体:int main(int
argc, char *argv[]) {

SDL_Event event;VideoState *is;

is = av_mallocz(sizeof(VideoState));

av_mallocz()函数会申请空间而且开端化为全0。

然后要开首化为摄像缓冲准备的锁(pictq)。因为如果事件驱动调用视频函数,摄像函数会从pictq抽出预解码帧。同

时,视频解码器会把新闻放进去,大家不亮堂相当动作会头阵生。希望您认识到那是一个经文的竞争条件。所以要在开首任

何线程前为其分配空间。同时把文件名放到VideoState当中。

pstrcpy(is->filename, sizeof(is->filename), argv[1]);

is->pictq_mutex = SDL_CreateMutex();

is->pictq_cond = SDL_CreateCond();

pstrcpy(已过期)是ffmpeg中的一个函数,其对strncpy作了一些外加的检测;

率先个线程

让我们启动我们的线程使办事落实吧:

schedule_refresh(is, 40);

is->parse_tid = SDL_CreateThread(decode_thread,
is);if(!is->parse_tid) {

av_free(is);

return -1;

}

schedule_refresh是一个就要定义的函数。它的动作是报告系统在某个特定的阿秒数后弹出FF_REFRESH_EVENT事件。

那将会反过来调用事件队列里的视频刷新函数。不过现在,让我们解析一下SDL_CreateThread()。

SDL_CreateThread()做的业务是那般的,它生成一个新线程能完全访问原本进度中的内存,启动大家给的线程。它一律

会运效率户定义数据的函数。在这种情景下,调用decode_thread()并与VideoState结构体连接。上半部分的函数没什么新东

西;它的行事就是打开文件和找到视频流和音频流的目录。唯一分歧的地点是把格式内容保留到大结构体中。当找到流后,

调用另一个将要定义的函数stream_component_open()。那是一个貌似的诀其余措施,自从大家设置过多相似的视频和节奏

解码的代码,我们透过编制这几个函数来重用它们。

stream_component_open()函数的功用是找到解码器,设置音频参数,保存主要音讯到大结构体中,然后启动音频和视

频线程。大家还会在此地设置有些任何参数,例如指定编码器而不是自动检测等等,下边就是代码:

int stream_component_open(VideoState *is, int stream_index)
{AVFormatContext *pFormatCtx = is->pFormatCtx;AVCodecContext
*codecCtx;

AVCodec *codec;

SDL_AudioSpec wanted_spec, spec;

if(stream_index < 0 || stream_index >=
pFormatCtx->nb_streams) {

return -1;

}

14·35

// Get a pointer to the codec context for the video stream

codecCtx = pFormatCtx->streams[stream_index]->codec;

if(codecCtx->codec_type == CODEC_TYPE_AUDIO) {// Set audio
settings from codec infowanted_spec.freq = codecCtx->sample_rate;

/* …. */

wanted_spec.callback = audio_callback;

wanted_spec.userdata = is;

if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {

fprintf(stderr, “SDL_OpenAudio: %s\n”, SDL_GetError());

return -1;

}

}

codec = avcodec_find_decoder(codecCtx->codec_id);

if(!codec || (avcodec_open(codecCtx, codec) < 0)) {

fprintf(stderr, “Unsupported codec!\n”);

return -1;

}

switch(codecCtx->codec_type) {

case CODEC_TYPE_AUDIO:

is->audioStream = stream_index;

is->audio_st =
pFormatCtx->streams[stream_index];is->audio_buf_size = 0;

is->audio_buf_index = 0;

memset(&is->audio_pkt, 0,
sizeof(is->audio_pkt));packet_queue_init(&is->audioq);SDL_PauseAudio(0);

break;

case CODEC_TYPE_VIDEO:

is->videoStream = stream_index;

is->video_st =
pFormatCtx->streams[stream_index];packet_queue_init(&is->videoq);

is->video_tid = SDL_CreateThread(video_thread, is);break;

default:

break;

}

}

那跟在此之前写的代码大约一致,只可是现在是概括音频和视频。注意到建立了大结构体来作为音频回调的用户数据来代

替了aCodecCtx。同样保留流到audio_st和video_st。像建立音频队列一样,也增多了摄像队列。重假如运作录像和音频线

程。似乎如下:

SDL_PauseAudio(0);

15·35

break;

/* …… */

is->video_tid = SDL_CreateThread(video_thread, is);

还记得以前SDL_PauseAudio()的作用,还有SDL_CreateThread()跟往日的用法一样。大家会回到video_thread()函数。在

那以前,让大家回来decode_thread()函数的下半部分。基本上就是一个循环来读取包和把它放到相应的队列中:

for(;;) {

if(is->quit) {

break;

}

// seek stuff goes here

if(is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size >
MAX_VIDEOQ_SIZE) {

SDL_Delay(10);

continue;

}

if(av_read_frame(is->pFormatCtx, packet) < 0) {

if(url_ferror(&pFormatCtx->pb) == 0) {

SDL_Delay(100); /* no error; wait for user input */

continue;

} else {

break;

}

}

// Is this a packet from the video stream?if(packet->stream_index ==
is->videoStream) {

packet_queue_put(&is->videoq, packet);

} else if(packet->stream_index == is->audioStream) {

packet_queue_put(&is->audioq, packet);

} else {

av_free_packet(packet);

}

}

那边没有新的事物,除了音频和视频队列定义了一个最大值,还有我们投入了检测读取错误的函数。格式内容之中有

一个叫做pb的ByteIOContext结构体。ByteIOContext是一个封存所有低级文件新闻的结构体。url_ferror检测结构体在读取

文本时出现的一点错误。

透过for循环,大家拭目以待程序截至或者文告我们曾经收尾。那个代码指点我们怎样推送事件,一些我们随后用来突显视

频的东西。

while(!is->quit) {

SDL_Delay(100);

}

fail:

if(1){

SDL_Event event;

event.type = FF_QUIT_EVENT;event.user.data1 =
is;SDL_PushEvent(&event);

}

16·35

return 0;

大家经过SDL定义的一个宏来获取用户事件的值。第二个用户事件应该分配给SDL_USEREVENT,下一个分红给

SDL_USEREVENT + 1,如此类推。FF_QUIT_EVENT在SDL_USEREVENT +
2中定义。如果大家欣赏,大家一致可以传递用户事件,

此间把大家的指针传递给了一个大结构体。最终调用SDL_Push伊夫nt()。在循环分流中,大家只是把SDL_QUIT_EVENT部分放

进入。我们还会看出事件循环的越多细节;现在,只是保障当推送FF_QUIT_EVENT时,会得到它和quit值变为1。

获取帧:视频线程

预备好解码后,开启视频线程。这些线程从视频队列之中读取包,把视频解码为帧,然后调用queue_picture函数来把

帧放进picture队列:

int video_thread(void *arg) {VideoState *is = (VideoState
*)arg;AVPacket pkt1, *packet = &pkt1;int len1, frameFinished;

AVFrame *pFrame;

pFrame = avcodec_alloc_frame();for(;;) {

if(packet_queue_get(&is->videoq, packet, 1) < 0) {// means we
quit getting packets

break;

}

// Decode video frame

len1 = avcodec_decode_video(is->video_st->codec, pFrame,
&frameFinished,

packet->data, packet->size);

// Did we get a video frame?

if(frameFinished) {

if(queue_picture(is, pFrame) < 0) {

break;

}

}

av_free_packet(packet);

}

av_free(pFrame);

return 0;

}

超过一半函数在那点上相应是一般的。已经把avcodec_decode_video函数移动到这边,只是交替了有的参数;例如,大

结构体里面有AVStream,所以从这里得到编解码器。持续地从视频队列之中取包,知道某人告诉大家该截至或者蒙受错误。

帧排队

一头来看看picture队列里面用来囤积解码帧的函数pFrame。由于picture队列是SDL
overlay(大致是为了视频显示尽

金沙澳门官网 ,量少的揣测),要求把转换帧存储在picture队列里面的多少是我们转变的:

typedef struct VideoPicture {

SDL_Overlay *bmp;

int width, height; /* source height & width */int allocated;

} VideoPicture;

大结构体有缓冲来囤积他们。不过,要求自己分配SDL_Overlay(注意到allocated标志用来标示是或不是曾经分配了内存)。

17·35

运用这么些队列要求八个指针:写索引和读索引。同样记录着缓冲里面其实有些许图片。为了写队列,首回要等待

缓冲清空以管教有空间存储VideoPicture。然后检测我们是否为写索引申请了overlay。要是没有,大家要求提请一些上空。

一旦窗口的尺寸改变了,同样须求重新申请缓冲。然则,为了幸免锁难点,不会在此处申请(我还不太确定为何,但应当

幸免在差异线程调用SDL overlay函数)。

int queue_picture(VideoState *is, AVFrame *pFrame) {VideoPicture
*vp;

int dst_pix_fmt;

AVPicture pict;

/* wait until we have space for a new pic
*/SDL_LockMutex(is->pictq_mutex);

while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
!is->quit) {

SDL_CondWait(is->pictq_cond, is->pictq_mutex);

}

SDL_UnlockMutex(is->pictq_mutex);

if(is->quit)

return -1;

// windex is set to 0 initially

vp = &is->pictq[is->pictq_windex];

/* allocate or resize the buffer! */

if(!vp->bmp || vp->width != is->video_st->codec->width
|| vp->height != is->video_st->codec->height) {

SDL_Event event;

vp->allocated = 0;

/* we have to do it in the main thread */event.type =
FF_ALLOC_EVENT;event.user.data1 = is;SDL_PushEvent(&event);

/* wait until we have a picture allocated */

SDL_LockMutex(is->pictq_mutex);

while(!vp->allocated && !is->quit) {

SDL_CondWait(is->pictq_cond, is->pictq_mutex);

}

SDL_UnlockMutex(is->pictq_mutex);

if(is->quit) {

return -1;

}

}

当大家想脱离时,退出机制就如以前看来的那样处理。已经定义了FF_ALLOC_EVENT为SDL_USEREVENT。推送事件然

后等候条件变量分配函数运行。

让我们来看看我们是怎么转移事件循环的:

for(;;) {

SDL_WaitEvent(&event);

switch(event.type) {

18·35

/* … */

case FF_ALLOC_EVENT:

alloc_picture(event.user.data1);

break;

记住event.user.data1就是大结构体。那早就够用不难了。让我们来探望alloc_picture()函数:

void alloc_picture(void *userdata) {VideoState *is = (VideoState
*)userdata;VideoPicture *vp;

vp = &is->pictq[is->pictq_windex];if(vp->bmp) {

// we already have one make another, bigger/smaller

SDL_FreeYUVOverlay(vp->bmp);

}

// Allocate a place to put our YUV image on that screen

vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
is->video_st->codec->height,

SDL_YV12_OVERLAY, screen);

vp->width = is->video_st->codec->width;

vp->height = is->video_st->codec->height;

SDL_LockMutex(is->pictq_mutex);

vp->allocated = 1;

SDL_CondSignal(is->pictq_cond);

SDL_UnlockMutex(is->pictq_mutex);

}

你应有专注到大家早就把SDL_CreateYUVOverlay移动到此地。此代码现在理应相比较好领悟了。记住我们把宽度和低度

保存到VideoPicture里面,因为出于某些原因不想更改视频的尺码。

好了,大家缓解了具有东西,现在YUV
overlay已经分配好内存,准备接受图片了。回到queue_picture来看看把帧复

制到overlay当中,你应有记得那有的内容的:

int queue_picture(VideoState *is, AVFrame *pFrame) {

/* Allocate a frame if we need it… */

/* … */

/* We have a place to put our picture on the queue */

if(vp->bmp) {

SDL_LockYUVOverlay(vp->bmp);

dst_pix_fmt = PIX_FMT_YUV420P;

/* point pict at the queue */

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];

19·35

// Convert the image into YUV format that SDL uses

img_convert(&pict, dst_pix_fmt, (AVPicture *)pFrame,
is->video_st->codec->pix_fmt,

is->video_st->codec->width,
is->video_st->codec->height);

SDL_UnlockYUVOverlay(vp->bmp);

/* now we inform our display thread that we have a pic ready
*/if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {

is->pictq_windex = 0;

}

SDL_LockMutex(is->pictq_mutex);

is->pictq_size++;

SDL_UnlockMutex(is->pictq_mutex);

}

return 0;

}

这有些的根本作用就是从前所用的容易地把帧填充到YUV
overlay。最终把值加到行列当中。队列的干活是不停添加直

到满,和里面有何样就读取什么。因而有着东西都依照is->pictq_size那么些值,须要锁住它。所以现在做事是增添写指针(有

需求的话翻转它),然后锁住队列扩充其尺寸。现在读索引知道队列之中有越来越多的音信,如若队列满了,写索引会知道的。

广播视频

那就是摄像线程!现在曾经包裹起有着松散的线程,除了那个,还记得调用schedule_refresh()函数吗?让大家来看看它

实质上做了哪些工作:

/* schedule a video refresh in ‘delay’ ms */

static void schedule_refresh(VideoState *is, int delay) {

SDL_AddTimer(delay, sdl_refresh_timer_cb, is);

}

SDL_Add提姆er()是一个SDL函数,在一个特定的阿秒数里它差不多地回调了用户指定函数(可挑选指引部分用户数据)。

用那几个函数来安插视频的换代,每一回调用那一个函数,它会设定一个年华,然后会触发一个事变,然后主函数会调用函数来从picture队列里拉出一帧然后出示它!

可是首先,让我们来触发事件。它会发送:

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque)
{SDL_Event event;

event.type = FF_REFRESH_EVENT;

event.user.data1 = opaque;

SDL_PushEvent(&event);

return 0; /* 0 means stop timer */

}

此间就是一般的轩然大波推送。FF_REFRESH_EVENT在此地的概念是SDL_USEREVENT +
1。有一个地点需求专注的是当大家

再次回到0时,SDL会甘休计时器,回调将不再起作用。

前几天推送FF_REFRESH_EVENT,大家需要在事变循环中拍卖它:for(;;) {

SDL_WaitEvent(&event);switch(event.type) {

/* … */

case FF_REFRESH_EVENT:

video_refresh_timer(event.user.data1);

20·35

break;

下一场调用这些函数,将会把多少从picture队列里面拉出去:

void video_refresh_timer(void *userdata) {VideoState *is =
(VideoState *)userdata;VideoPicture *vp;

if(is->video_st) {

if(is->pictq_size == 0) {

schedule_refresh(is, 1);

} else {

vp = &is->pictq[is->pictq_rindex];

/* Timing code goes here */schedule_refresh(is,
80);video_display(is); /* show the picture! */

/* update queue for next picture! */

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);

}

}

近来,那么些函数就卓殊简单明知道:它会从队列之中拉出数据,设置下一帧播放时间,调用vidoe_display来使视频显

示到显示器中,队列计数值加1,然后减小它的尺寸。你会小心到大家从没对vp做任何动作,那里解析为啥:在事后,我

们会动用访问时序信息来一块视频和韵律。看看那些“那里的时序代码”的地点,大家会找到大家应当以多快的快慢来播音

摄像的下一帧,然后把值传给schedule_refresh()函数。现在只是设了一个固定值80。技术上,你可以揣摸和检验这么些值,

下一场重编你想看的具有电影,然则:1、过一段时间它会变,2、那是很笨的措施。之后我们会回来那一个地点。

大家已经大约已毕了;还余下最终一样东西要做:播放视频!那里就是视频播放的函数:

void video_display(VideoState *is) {SDL_Rect rect;

VideoPicture *vp;

AVPicture pict;

float aspect_ratio;int w, h, x, y;

int i;

vp = &is->pictq[is->pictq_rindex];

if(vp->bmp) {

if(is->video_st->codec->sample_aspect_ratio.num == 0) {

aspect_ratio = 0;

} else {

21·35

aspect_ratio =
av_q2d(is->video_st->codec->sample_aspect_ratio)
*is->video_st->codec->width /
is->video_st->codec->height;

}

if(aspect_ratio <= 0.0) {

aspect_ratio = (float)is->video_st->codec->width /
(float)is->video_st->codec->height;

}

h = screen->h;

w = ((int)rint(h * aspect_ratio)) & -3;if(w > screen->w) {

w = screen->w;

h = ((int)rint(w / aspect_ratio)) & -3;

}

x = (screen->w – w) / 2;

y = (screen->h – h) / 2;

rect.x = x;

rect.y = y;

rect.w = w;

rect.h = h;SDL_DisplayYUVOverlay(vp->bmp, &rect);

}

}

是因为屏幕尺寸可能为其余尺寸(大家设置为640×480,用户可以重新设置尺寸),大家要动态提出要求多大的一个矩

形区域。所以首先要指定视频的长宽比,也就是宽除以高的值。一些编解码器会有一个奇样本长宽比,也就是一个像素或者

一个样书的宽高比。由于编解码的长宽值是比照像平素测算的,所以实际上的宽高比等于样本宽高比某些编解码器的宽高比为0,表示每个像素的宽高比为1×1。然后把摄像缩放到尽可能大的尺寸。那里的&
-3意味与-3做与运算,实际上是让他们4字节对齐。然后我们把电影居中,然后调用SDL_DisplayYUVOverlay()。

那么结果什么?做完了啊?如故要重写音频代码来接纳新的VideoStruct,但那只是零星的改动,你可以参见示例代码。

最后索要做的事情是改变ffmpeg内部的退出回调函数,变为自己的淡出回调函数。

VideoState *global_video_state;

int decode_interrupt_cb(void) {

return (global_video_state && global_video_state->quit);

}

在主函数里面安装global_video_state这几个大结构体。

那就是了!让大家来编译它:

sdl-config –cflags –libs

gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lswscale
-lSDL -lz -lm

享受你的未共同电影吧!下一节大家会使视频播放器真正地工作起来。

课程五:同步视频(Tutorial 05: Synching Video)摄像怎样联合

那在那个日子里,大家早已弄好了一个大抵没什么用的视频播放器。它能播放视频,也能播放音频,但它不是普通

意思上说的播放器。接下来大家应当怎么办?

PTS和DTS

22·35

幸运地,音频或视频流都有局地音信告诉我们,它接济以多快的快慢去播放:音频流采样率,视频流帧率值。但是,

若是单单地经过帧数乘以帧率来一头视频,可能会使音频失步。作为替代,流里面的包可能会有解码时间戳(DTS)和浮现

光阴戳(PTS)。要搞懂这三个值,你须要领会视频存储的点子。某些格式,例如MPEG,使用叫做B帧的法门(B表示双向“bidirectional”)。其余三种帧叫做“I”帧和“P”帧(“I”表示关
键”intra”,“P”表示推测“predicted”)。I帧保存一幅完整的图像,P帧信赖于前方的I帧和P帧,并且选用相比较或者差分的方法来编码。B帧与P帧类似,但依靠于前方和前面帧音信!这就

诠释了为何当我们调用avcodec_decode_video后恐怕没有赢得完全的一帧。

倘诺有一部影视,其帧排列为:I B B
P。现在我们在广播B帧从前要精晓P帧的消息。因为这一个缘故,帧的蕴藏顺序可

能是这么的:I P B
B。那就是干什么大家会有一个解码时间戳和体现时间戳。解码时间戳告诉大家如何时候需求解码什么,

突显时间戳告诉大家怎么时候必要出示怎么。所以,在这几个案例中,流可能是如此的:

PTS: 1 4 2 3

DTS: 1 2 3 4

Stream: I P B B

//突显顺序//解码顺序//存储顺序

日常只有当突显B帧的时候PTS和DTS才会差别等。

当咱们从av_read_frame()得到一个包,包里会包蕴PTS和DTS音讯。但真正想要的是PTS是刚刚解码出来的原始帧的PTS,那样我们才会通晓应该在什么样时候显得它。然则avcodec_decode_video()给我们的帧包蕴的AVFrame没有包涵有用的PTS新闻(警告:AVFrame包蕴PTS值,但当获得帧的时候并不一而再咱们要求的)。而且,ffmpeg重新排序包以便于被avcodec_decode_video()函数处理的包的DTS总是与其重返的PTS相同。不过,另一个警戒:并不是总能获得那一个音信。

毫无操心,因为有其余一种艺术可以找到帧的PTS,可以让程序自己来排序包。保存一帧首个包里面得到的PTS:那

不怕一切帧的PTS。所以当流不给我们提供DTS的时候,就拔取那些保存了的PTS。可以透过avcodec_decode_video()来告诉

大家尤其是一帧里头的率先个包。怎样完成?每当一个包先导一帧的时候,avcodec_decode_video()会调用一个函数来为一

帧申请缓冲。当然,ffmpeg允许我们再一次定义相当分配内存的函数。所以大家会创建一个新的函数来保存一个包的pts。

当然,即便可能依旧得不到真正的pts。我们会在背后处理它。同步

现在,已经精通哪天显得一个视频帧,但要怎么样贯彻?那里有一个呼吁:当播放完一帧后,找出下一帧应当在什

么时候播放。然后简单地设置一个定时器来重新刷新视频。可能你会想,检查PTS的值来而不是用系统时钟来设置延时时间。

这种格局可以,但有多少个难题亟待缓解。

第一第三个难点是要知道下一个PTS是怎么着。现在,你也许会想可以把视频速率添加到PTS中,那几个主张不错。但是,

稍微电影要求帧重复。那就象征重复播放当下帧。那会使程序突显下一帧太快。所以须求总结它们。

其次个难点是前几日视频和旋律各自播放,一点不受同步影响。如果所有工作都好的话大家不要担心。但您的计算机可能

不太好,或者很多视频文件也
不太好。所以现在有两种选拔:音频同步视频,视频一起音频,或者是视频和节奏同步到一

个外表时钟(例如你的计算机)。从现在起,大家拔取视频一起音频的章程。

编程:得到帧的年月戳

明天编制代码来形成那个事物。我们会增多越来越多成员进我们的大结构体中,但我们会在急需的时候才做这一个事情。首

先来探视视频线程。记住,就是在此间大家获得从解码线程放进队列里的包。须要做的事体是从avcodec_decode_video解

出的帧里拿到PTS。大家谈谈的第一种方法是从上次拍卖的包中得到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);

23·35

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);

get函数不会告诉大家别的有关包的音讯,所以每当获得一个包时,必要把其PTS存放到一个全局变量里面,然后get

函数就可以读取到了。然后可以把值存放到AVFrame结构体不透明变量中。那是一个用户定义的变量,所以可以任意使用

它。首先,那里是我们的函数达成代码:

uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;

/* These are called whenever we allocate a frame buffer. We use this to
store the global_pts in* a frame at the time it is allocated. */

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是ffmepg默许用来分配缓冲的函数。av_freep是一个内存

管住函数,它不光释放指针指向的内存,还会把指针设置为NULL。接下来过来打开流的函数
(stream_component_open),

咱俩添加这几行来报告ffmpeg咋做:

codecCtx->get_buffer = our_get_buffer;

codecCtx->release_buffer = our_release_buffer;

近期加上代码以高达PTS保存到全局变量的目标,那么就可以在须要时选拔这几个曾经储存了的PTS。代码如同这么:

for(;;) {

if(packet_queue_get(&is->videoq, packet, 1) < 0) {

// means we quit getting packets

break;

}

pts = 0;

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) {

// Save global pts to be stored in pFrame in first call

24·35

pts = packet->dts;

} else {

pts = 0;

}

pts *= av_q2d(is->video_st->time_base);

技巧笔记:你可能注意到我们用int64来装载PTS。因为PTS以整形的款型来存放在。这些时间戳是度量流为主时间单元

的年月长短的。例如,假若流每分钟有24帧,那么PTS为42时表示一旦每帧的时间是24分之一的话,现在应当播放到42帧了(肯定未必是真实的)。

可以透过除以帧率而把PTS转换为秒数。time_base的值其实就是1/帧率(对于固定帧率来说),所以可以用PTS乘time_base来得到时间。

编程:使用PTS来同步

俺们取得了PTS。现在来化解此前所说的八个协同的难题。定义一个名为synchronize_video的函数来更新同步PTS。那

个函数同样会处理当得不到PTS值的境况。同时须要注意什么时候须求播放下一帧以设置刷新率。可以利用一个展现摄像已

经播放了多久的中间值video_clock来形成那一个工作。把这些值放在大结构体中。

typedef struct VideoState {

double video_clock; ///<=”” pre=””>

这里是synchronize_video函数,他有很好的笺注:

double synchronize_video(VideoState *is, AVFrame *src_frame, double
pts) {

double frame_delay;

if(pts != 0) {

is->video_clock = pts; /* if we have pts, set video clock to it */

} else {

pts = is->video_clock; /* if we aren’t given a pts, set it to the
clock */

}

/* update the video clock */

frame_delay = av_q2d(is->video_st->codec->time_base);

/* if we are repeating a frame, adjust clock accordingly
*/frame_delay += src_frame->repeat_pict * (frame_delay *
0.5);is->video_clock += frame_delay;

return 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结构体中。所以要把pts值添加到结构体中并扩展一行

代码:

typedef struct VideoPicture {

double pts;

25·35

}

int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {

… stuff …

if(vp->bmp) {

… convert picture …vp->pts = pts;

… alert queue …

}现在有着图像队列之中的图像都有了正确的岁月戳了,就让大家看看录像刷新函数吧。你恐怕还记得从前用固定值80ms

来欺骗它。现在要算出正确的值。

俺们的国策是透过不难总括前一帧和现行那帧的年月戳的差。同时须要视频一起到点子。将安装音频时钟:一个里面

值记录正在播放音频的岗位。就好像从随机mp5播放器中读出来数字同样。由于我们要求视频一起到点子,所以视频线程会

选择那些值来测算出播放视频是快了依然慢了。

咱俩会在未来落成这一个代码;现在假设已经有一个得以给我们音频时钟的函数get_audio_clock。即使我们有了那个值,

在录像和节奏失步的时候理应怎么办?简单而笨的法子是试着用跳过正确帧或者其他艺术来化解。除了那种笨办法,大家会

去判断和调整下次刷新的小运值。即使PTS太落后于音频时间,大家加陪计算延迟。假如PTS太领先于音频时间,应尽量加

快刷新时间。现在有了刷新时间或者是延时,我们会和处理器时钟计算出的frame_timer做比较。那么些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; /* the pts from last time
*/if(delay <= 0 || delay >= 1.0) {

delay = is->frame_last_delay; /* if incorrect delay, use previous
one */

}

/* save for next time */is->frame_last_delay =
delay;is->frame_last_pts = vp->pts;

/* update delay to sync to audio */ref_clock =
get_audio_clock(is);diff = vp->pts – ref_clock;

/* Skip or repeat the frame. Take delay into account FFPlay still
doesn’t “know if this is the best guess.” */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;

}

26·35

}

is->frame_timer += delay;

/* computer the REAL delay */

actual_delay = is->frame_timer – (av_gettime() /
1000000.0);if(actual_delay < 0.010) {

actual_delay = 0.010; /* Really it should skip the picture instead */

}

schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));

/* show the picture! */

video_display(is);

/* update queue for next picture! */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);

}

}

那里大家做了成百上千检测:首先,确保现在的日子戳和上一个小时戳之间的延时是可行的。假若不是的话大家猜忌着使

用上次的延时。接着,有限援救大家有一个同步阀值,因为一块的时候并不总是无微不至的。ffplay用的值是0.01。大家也准保阀值

不会比时间戳之间的距离短。最终,把最小的刷新值设置为10微秒,但我们不会去理会。

往大结构体里面加了一大串值,所以不要忘记去检查代码。同样地,不要遗忘在stream_component_open里开头化frame

time和previous frame delay。

is->frame_timer = (double)av_gettime() / 1000000.0;

is->frame_last_delay = 40e-3;

联手:音频时钟

今日是时候来贯彻音频时钟了。可以在audio_decode_frame函数里面更新时间,也就是做音频解码的地点。现在挥之不去

调用那几个函数的时候并不再三再四处理新包,所以要在五个地方更新时钟。一个是得到新包的地点:不难地把包的PTS赋值给audio
clock。然后一旦一个包有多个帧,通过测算采样数和采样每秒的乘积来获取音频播放的岁月。所以只要得到包:

/* if update, update the audio clock w/pts */

if(pkt->pts != AV_NOPTS_VALUE) {

is->audio_clock =
av_q2d(is->audio_st->time_base)*pkt->pts;

}

和假使我们处理这些包:

/* Keep audio_clock up-to-date */

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);

27·35

局地细节:临时函数改变为涵盖pts_ptr,所以确保您转移了它。pts_ptr是一个用来通告audio_callback函数当前节奏

包的时刻戳的指针。这几个会在下次用来一同音频和摄像。

近期可以兑现get_audio_clock函数了。那不是简约地拿到is->audio_clock值。注意每一趟处理它的时候设置PTS,当假设

你看看audio_callback函数,它开销了是未来把数据从声音包活动到输出缓冲区中。那意味着在audio
clock中著录的光阴可

能会比实际的要早很多,所以必要检讨还剩余多少要写入。下边是总体的代码:

double get_audio_clock(VideoState *is) {

double pts;

int hw_buf_size, bytes_per_sec, n;

pts = is->audio_clock; /* maintained in the audio thread
*/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 -lswscale
-lSDL -lz -lm`

终极,你可以用你自己的摄像播放器来看摄像了。下一节大家来探视音频同步,然后再下一节商量查询。

学科六:音频同步(Tutorial 06: Synching 奥迪(Audi)o)同步音频

现行我们早已弄了一个比较像样的播放器了,让大家看看还有啥样零散的事物必要做的。上两遍,我们演示了几许一同

的难点,就是一块摄像到点子而不是使用其余方法。大家将应用视频一样的做法:做一个之中摄像时钟来记录摄像线程播放

了多长时间,然后共同到点子上去。之后大家会创建把视频和节奏同步到表面时钟。

扭转摄像时钟

今昔我们想像音频时钟那样生成音频时钟:一个付给当前摄像播放时间的里边值。首先,你恐怕会想那和采纳上一帧

日子戳来更新定时器一样不难。不过,别忘记当我们用微秒来计量时间的话时间帧可能会很长。解决办法是跟踪此外一个值,

我们在安装上一帧日子戳的时候的日子值。那么当前摄像时间值就是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时初叶化代码:28·35

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_MASTERdouble
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)。

在意:为啥会在那里?那些公式看来很神奇!它基本剩是一个使用等比级数的加权平均值。想要越来越多的消息请点击

以下三个网址:

29·35

以下就是我们的函数:

/* Add or subtract samples to get a better sync, return new audio
buffer size */

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);

/* Shrinking/expanding buffer code…. */

}

} else {

/* difference is TOO big; reset diff stuff */

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每秒时间的样本数。由此,大家须求的样本数是自己

们更具声音的偏移添加或者裁减后的响声样本数。大家一样可以安装一个限量来界定五遍开展创新的长短,因为考订太多,

用户会听到难听的声响。

30·35

考订样本数

现今大家要实在地考订音频。你或许注意到synchronize_audio函数重返一个样书大小。所以只须要调动样本数为wanted_size就足以了。那样可以使样本值小一些。可是借使想把它变大,我们无法只是让样本的大小变大,因为缓冲里面

尚无愈多的多寡。所以大家无法不添加它。可是应该什么添加?最笨的方法是揣度声音,所以让大家用已有的数据在缓冲的末

尾添加上最终的样书。

if(wanted_size < samples_size) {

/* remove samples */

samples_size = wanted_size;

} else if(wanted_size > samples_size) {

uint8_t *samples_end, *q;

int nb;

/* add samples by copying final samples */

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) {

/* We have already sent all our data; get more */

audio_size = audio_decode_frame(is, is->audio_buf,
sizeof(is->audio_buf), &pts);if(audio_size < 0) {

/* If error, output silence */

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;

/* Skip or repeat the frame. Take delay into account FFPlay still
doesn’t “know if this is the best guess.” */sync_threshold = (delay
> AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;

31·35

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 -lswscale
-lSDL -lz -lm

下一场您可以运行它。

下次大家要做的是让您可以让影片快退和快进。

教程七:跳转(Tutorial 07: Seeking)处理seek命令

当今要往播放器里面添加查找作用,因为一个播放器不可能倒带还确确实实蛮烦人。再加上那能够显得一下av_seek_frame是什么样行使的。大家打算安装方向键的左和右的功效是快退和快进10秒,上和下的成效是快退快进60秒。所以大家要求设

置我们的主循环来捕获键值。但是,当大家赢得键值时大家不可以直接调用av_seek_frame。大家必须在解码主进程decode_thread来处理。所以我们会向大结构体里面添加跳转地点和一些跳转标识:

int

intint64_t

seek_req;

seek_flags;

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);

}

32·35

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,000fps)。大家在末端切磋为啥要更换那个值。那里就是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,那是贯彻跳转的地点。你会注意到已经申明了一个区域“这里完毕跳转”。现在要把代码填

到那里。跳转是围绕“av_seek_frame”函数的。那么些函数用到一个格式内容,一个流,一个时光戳和一组标记来作为它的

参数。这一个函数会跳转到你给它的流年戳地方。时间戳的单位是你传递给函数的流的time_base。不过,你不是必要求传送

一个流进去(可以流传-1代表)。即使您那样做了,time_base将会利用其中时间戳单位,或者1000000fps。就是干吗在

设置seek_pos的时候把岗位乘于AV_TIME_BASE的原因。

而是,假使传递了-1给av_seek_frame,播放某些文件或者会并发难点(几率较少),所以要把首个流传递给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 {

/* handle packet queues… more later… */

av_rescale_q(a,b,c)函数是用来把timestamp的火候调整到另一个时机。其中央动作是a8b/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内部缓存的

方法。可以经过在清理后放入一个非同常常包的办法来贯彻它,当他们检测到那些奇异的包后,他们就会清理他们的缓存。

33·35

让我们初阶编制清理缓存的函数。它相比较简单,所以我只是把它展现出来:

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) {

34·35

avcodec_flush_buffers(is->audio_st->codec);

continue;

}

下面的代码片段与视频线程中的一样,只要把”audio”替换为”video”。

就是那般了!让我们来编译播放器吧:

gcc -o tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lswscale
-lSDL -lz -lm

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图