码农终结者

浅析 requestAnimationFrame

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

初稿出处: Tmall前端团队(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 不可知等等)?

开发者可以用多如牛毛主意来减轻那个题材的病症,可是彻底解决,这一个、基本、很难。

好不简单,难点的来自在于时机。对于前端开发者来说,setTimeout 和
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中的

要素)新昌关索剧本落成。

能够由此setTimeout和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年五月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和mp4。包(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年四月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,在On提姆er函数中,通过扩展变量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_奥迪oSpec结构体里面后,调用SDL_Open奥迪(Audi)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_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 奥迪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地图