码农终结者

浅析 requestAnimationFrame

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

原稿出处: 天猫商城前端团队(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
中要落到实处动画效果,如何做呢?无外乎使用 setTimeout 或
setInterval。那么难题就来了:

  • 怎么着规定科学的日子间隔(浏览器、机器硬件的品质各不同)?
  • 微秒的不精确性怎么解决?
  • 哪些避免过度渲染(渲染频率太高、tab 不可知等等)?

开发者可以用多如牛毛艺术来减轻这么些标题标病症,可是彻底解决,那些、基本、很难。

算是,难点的来源于在于时机。对于前端开发者来说,set提姆eout 和
setInterval 提供的是一个等长的定时器循环(timer
loop),可是对于浏览器内核查渲染函数的响应以及哪天可以发起下一个动画帧的空子,是一点一滴不打听的。对于浏览器内核来讲,它亦可明白发起下一个渲染帧的适合机会,可是对于其他set提姆eout 和 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

正文将官对第5篇文章的太阳系模型进行修改,加入一些动画效果。其余还会参预突显帧速率的代码。 

前言

正文首要参考w3c资料,从底层完成原理的角度介绍了requestAnimationFrame、cancelAnimationFrame,给出了连带的以身作则代码以及本人对落到实处原理的知情和探讨。


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

     
参与动画效果最不难的点子是响应WM_TIMER信息,在其音讯处理函数中改变一些参数值,比如每过多少微秒就旋转一定的角度,并且重绘场景。

本文介绍

浏览器中卡通有三种完结格局:通过表明元素完成(如SVG中的

要素)松阳高腔本达成。

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

它们有如下的表征:

1、纵然向其传递阿秒为单位的参数,它们也不可能达标ms的准确性。这是因为javascript是单线程的,可能会生出堵塞。

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

3、没有考虑到绘制动画的最佳时机,只是平素地以某个大致的事件间隔来调用循环。

骨子里,使用setInterval或set提姆eout来兑现主循环,根本错误就在于它们抽象等级不符合要求。大家想让浏览器执行的是一套可以决定种种细节的api,完结如“最优帧速率”、“采用绘制下一帧的最佳时机”等效果。可是如若选拔它们来说,那么些实际的细节就非得由开发者自己来成功。

requestAnimationFrame不须要使用者指定循环间隔时间,浏览器会基于当前页面是或不是可知、CPU的载荷境况等源于行决定最佳的帧速率,从而更合理地运用CPU。


先是我们须求精通摄像文件的部分基本概念,视频文件本身被称作容器,例如avi或者是quicktime,容器的花色确定

Frame Rate

名词表达

了文件的新闻。然后,容器里装的东西叫流(stream),平日包蕴视频流和音频流(“流”的意趣其实就是“随着时间推移

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.

动画帧请求回调函数列表

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

};


的一段连接的多少元素”)。流中的多寡元素叫做“帧”。每个流由不一致的编解码器来编码,编解码器定义了数据怎么样编码

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

requestAnimationFrame

requestAnimationFrame方法用于通知浏览尊敬采样动画。

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

各样回调函数都有一个布尔标识cancelled,该标识开首值为false,并且对外不可知。

在后头的“处理模型”
中我们会面到,浏览器在推行“采样所有动画”的任务时会遍历动画帧请求回调函数列表,判断每个元组的callback的cancelled,若是为false,则执行callback。

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

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

cancelAnimationFrame

cancelAnimationFrame 方法用于取消之前布署的一个动画帧更新的请求。

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

任凭该回调函数是还是不是在动画帧请求回调函数列表中,它的cancelled都会被装置为true。

即使该handle没有针对任何回调函数,则调用cancelAnimationFrame
不会暴发任何工作。

读取的,通过解码器解包,获得原始的帧,我们就可以对那些多少进行播放等的处理。对于大家来说,每个包包蕴完整的帧,

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

拍卖模型

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

此地使用伪代码来证实“采样所有动画”职务的进行步骤:

var list = {};

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

for (var browsingContext in browsingContexts) {

var time = 从1970年3月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) {

}

}

}

}

依然多少个音频帧。

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

已解决的题材

缘何在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都为该回调函数),“采集所有动画”职责会举行很多次该回调函数。

在低级的档次,处理音视频流是卓殊简单的:

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

比如说上边的代码在实施“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

从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里突显图像的区域叫做GALAXY Tab: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_奥迪oSpec作为结构体,包蕴了颇具大家必要的旋律新闻。

在显示什么建立那么些事物事先,首先分析一下电脑是何许处理音频的。数码音频由一长串采样流组成。每个样本值

表示声音波形的一个数值。声音按照一个一定的采样率被记录着,简单的话就采样率是以多快的进程来播放每个采样,也即

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

体声或者环绕,例如,即便采样是立体声的,会同时存入两大路采样信号。当大家从影视里获取数据时,不晓得可以博得多

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

6·35

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

其余参数,还安装了一个回调函数和用户数据。当开头播放音频,SDL会持续地调用回调函数来要求它把声音缓冲数据填充

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

备和重回另一个奥迪(Audi)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_Open奥迪(Audi)o来开辟音频。

如果你还记得后边的科目,大家照旧须要开拓音响编解码器本身,那是尽人皆知的。

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奥迪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

相关文章

发表评论

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

*
*
Website