<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; Graphic</title>
	<atom:link href="https://blog.gmem.cc/category/work/graphic/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 12:46:48 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>H.264学习笔记</title>
		<link>https://blog.gmem.cc/h264-study-note</link>
		<comments>https://blog.gmem.cc/h264-study-note#comments</comments>
		<pubDate>Sat, 16 Sep 2017 07:02:53 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16164</guid>
		<description><![CDATA[<p>简介 MPEG MPEG是动态图像专家组（Moving Picture Experts Group）的简称，它可以指： 一个成立于1988年的，研究视频和音频编码标准的组织 一系列音视频编码标准，包括MPEG-1、MPEG-2、MPEG-3、MPEG-4、MPEG-7以及正在制定中的MPEG-21 MPEG-1 MPEG发布的第一个视频和音频有损压缩标准，它采用了块方式的运动补偿、离散余弦变换（DCT）、量化等技术，并为1.2Mbps传输速率进行了优化。其主要是为光盘类介质制定，随后被作为VCD（352×240，1.15mbps比特率）的核心技术。 MPEG-1标准由五个部分组成，其中第二部分、第三部分规定了视频、音频编码标准。 MPEG-1音频编码标准分为三代，逐代提升了压缩比。其中最著名的第三代协议被称为MPEG-1 Layer 3，简称MP3。 一个MPEG-1视频序列，包含多个图像群组（Group Of Pictures，GOP），每个GOP包含多个帧，每个帧包含多个slice。GOP由两个I帧之间的帧构成。 帧是MPEG-1的一个重要基本元素，一个帧就是一个完整的显示图像。帧的种类有四种： I帧（Intra Frame）即帧内帧，也叫关键帧。这类帧能够被独立的解码，可以看做是基线Profile的JPEG图像 P帧（Predicted Frame）即预测帧，也叫向前预测帧（ Forward-predicted Frames）。P帧利用视频中的时域冗余（ <a class="read-more" href="https://blog.gmem.cc/h264-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/h264-study-note">H.264学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">MPEG</span></div>
<p>MPEG是动态图像专家组（Moving Picture Experts Group）的简称，它可以指：</p>
<ol>
<li>一个成立于1988年的，研究视频和音频编码标准的组织</li>
<li>一系列音视频编码标准，包括MPEG-1、MPEG-2、MPEG-3、MPEG-4、MPEG-7以及正在制定中的MPEG-21</li>
</ol>
<div class="blog_h3"><span class="graybg">MPEG-1</span></div>
<p>MPEG发布的第一个视频和音频有损压缩标准，它采用了块方式的运动补偿、离散余弦变换（DCT）、量化等技术，并为1.2Mbps传输速率进行了优化。其主要是为光盘类介质制定，随后被作为VCD（352×240，1.15mbps比特率）的核心技术。</p>
<p>MPEG-1标准由五个部分组成，其中第二部分、第三部分规定了视频、音频编码标准。</p>
<p>MPEG-1音频编码标准分为三代，逐代提升了压缩比。其中最著名的第三代协议被称为<span style="background-color: #c0c0c0;">MPEG-1 Layer 3，简称MP3</span>。</p>
<p>一个MPEG-1视频序列，<span style="background-color: #c0c0c0;">包含多个图像群组（Group Of Pictures，GOP），每个GOP包含多个帧，每个帧包含多个slice</span>。<span style="background-color: #c0c0c0;">GOP由两个I帧之间的帧构成</span>。</p>
<p>帧是MPEG-1的一个重要基本元素，<span style="background-color: #c0c0c0;">一个帧就是一个完整的显示图像</span>。帧的种类有四种：</p>
<ol>
<li>I帧（Intra Frame）即帧内帧，也叫关键帧。这类帧能够被独立的解码，可以看做是基线Profile的JPEG图像</li>
<li>P帧（Predicted Frame）即预测帧，也叫向前预测帧（ Forward-predicted Frames）。P帧利用视频中的时域冗余（ Temporal Redundancy）来提高压缩比。P帧仅仅存储相对于它前面的那一帧的图像的差异（基于运动补偿和运动估计算法）部分</li>
<li>B帧（Bidirectional Frame）即双向预测帧，也叫向后预测帧（Backwards-predicted Frames）。B帧类似于P帧，但是它同时可以基于前一帧、后一帧进行预测。由于B帧可能依赖于“未来”的帧来解码，它会引入额外的编解码延迟</li>
<li>D帧（Direct Frame）即指示帧。仅仅由DC转换系数编码而成，因此其质量较低。D帧不会被I/P/B帧引用，仅仅在快速预览时有用。D帧实际应用的很少，后续标准也没有包含它</li>
</ol>
<div class="blog_h3"><span class="graybg">MPEG-2</span></div>
<p>通常用来为广播信号提供视频和音频编码，包括卫星电视、有线电视等。MPEG-2经过少量修改后，成为DVD产品的核心技术。</p>
<p>MPEG-2的视频部分，提供了对隔行扫描（广泛应用在广播电视领域，在CRT类显示器上，比相同帧率的逐行扫描更加不会引起视觉闪烁）视频显示模式的支持。</p>
<p>高级音频编码（Advanced Audio Coding，AAC）在MPEG-4发布前，作为MPEG-2的附加内容发布。</p>
<p>MPEG-2定义了两种复合信息流：传送流（TS）和节目流（PS：Program Stream）。TS流与PS流的区别在于TS流的包结构是固定长度的，而PS流的包结构是可变长度的。 PS包与TS包在结构上的这种差异，导致了它们对传输误码具有不同的抵抗能力，因而应用的环境也有所不同。TS码流由于采用了固定长度的包结构，当传输误码破坏了某一个TS包的同步信息时，接收机可在固定的位置检测它后面包中的同步信息，从而恢复同步，避免了信息丢失。而PS包由于长度是变化的，一旦某一PS包的同步信息丢失，接收机无法确定下一包的同步位置，就会造成失步，导致严重的信息丢失。因此，在信道环境较为恶劣，传输误码较高时，一般采用TS码流；而在信道环境较好，传输误码较低时，一般采用PS码流。由于TS码流具有较强的抵抗传输误码的能力，因此目前在传输媒体中进行传输的MPEG-2码流基本上都采用了TS码流。</p>
<div class="blog_h3"><span class="graybg">MPEG-3</span></div>
<p>本来的目标是为HDTV提供20-40Mbps视频压缩技术。在标准制定的过程中，委员会很快发现MPEG-2技术足以获取类似的效果，因此将其合并到MPEG-2，成为MPEG-2的延伸。</p>
<div class="blog_h3"><span class="graybg">MPEG-4</span></div>
<p>主要用途在于网上流、光盘、语音发送，以及电视广播。 MPEG-4吸收了MPEG-1、MPEG-2以及其它相关标准的很多特性。</p>
<p>MPEG-4仍然在进化之中，其关键组成部分是：</p>
<ol>
<li>第二部分：定义了一个编码器标准，DivX、Xvid都是该标准的实现</li>
<li>第十部分：即MPEG-4 AVC（高级视频编码， Advanced Video Coding），也称H.264。开源编码器x264、Quick Time7以及蓝光都遵循此标准</li>
</ol>
<div class="blog_h2"><span class="graybg">H.264</span></div>
<p>目前H.264已经成为高精度视频录制、压缩和发布的最常用格式之一。它不是单个标准，而是由<span style="background-color: #c0c0c0;">多个配置（Profile）构成的标准家族</span>。每个编码器至少需要支持一种H.264配置。</p>
<p>H.264能够在低带宽情况下提供优质视频，同等视频质量下，它仅仅需要MPEG-2/H.263/MPEG-4 Part2的一半甚至更少的带宽。</p>
<div class="blog_h1"><span class="graybg">格式与质量</span></div>
<p>视频编码是压缩、解压缩数字视频信号的处理过程。数字视频是真实世界中视觉影像的基于<span style="background-color: #c0c0c0;">空间、时间的采样</span>。</p>
<p>通常情况下，在<span style="background-color: #c0c0c0;">某一特定时刻对整个场景采样，形成帧（Frame）</span>，或者，对场景进行<span style="background-color: #c0c0c0;">隔行采样，所谓场（Field）</span>。采样总是按照一定的时间间隔进行，例如每1/25秒一次采样，这样连续的采样就形成了动态的视频信号，每秒钟采样的次数叫做<span style="background-color: #c0c0c0;">帧率（Frame Rate）</span>。为了表示彩色的场景，通常需要三个分量（Component）或者一系列的采样。</p>
<p>程序需要度量场景还原的精度，这样才能评估自身的性能。评估逻辑比较困难，原因是场景的质量很大程度上要考虑人的视觉心理特征，不同的人的视觉心理特征是不同的。</p>
<div class="blog_h2"><span class="graybg">自然的视觉场景</span></div>
<p>真实世界中的视觉场景，通常由多个物体构成。这些物体有各自的形状、深度（景深）、纹理、照度。自然视觉场景的颜色、亮度呈现出平滑变化的特征。</p>
<p>对自然视觉场景进行数字化处理时，程序需要关注两个维度：</p>
<ol>
<li>空间特征：单个场景内部纹理的变化特征、物体的数量和形状、颜色</li>
<li>时间特征：物体移动、明度变化、镜头/视点的切换</li>
</ol>
<div class="blog_h2"><span class="graybg">捕获</span></div>
<p>自然视觉场景在空间、时间上都是连续的。要以数字化的方式呈现这种场景，需要：</p>
<ol>
<li>空间采样：通常在场景的图像平面上设立矩形网格（Grid），采集离散的点（分辨率，帧大小），这些点分布在Grid的交叉处</li>
<li>时间采样：按照一定的间隔对帧或者帧的分量进行采样（帧率）。对于低质量的视频通信来说了，采样率通常在10-20FPS之间，这种级别的FPS下急速移动的物体不容易平滑显示；25-30FPS是典型的电视电影采样率；高达50-60FPS的时间采样下移动物体会非常平滑，代价是过高的数据率（Data Rate）</li>
</ol>
<p>时空采样的示意图如下：</p>
<p><img class="aligncenter size-large wp-image-16298" src="https://blog.gmem.cc/wp-content/uploads/2017/09/st-sample-1024x670.png" alt="st-sample" width="710" height="464" /></p>
<p>每个时空采样点 —— 叫做图像元素（Picture Element）或者像素（Pixel）——采用1-N个数字来表示。这些数字包含了亮度（Brightness）/照度（Luminance）、颜色信息。注意，从视频采集设备（如CCD）直接获得的采样阵列是模拟视频电信号。经过处理后才能变成像素表示的数字信号。</p>
<div class="blog_h3"><span class="graybg">亮度/照度</span></div>
<p>这两个概念是对同一事物的不同表述。</p>
<p>照度是一个客观性的概念，即以特定角度射向指定区域的光照强度。单位是坎德拉（Candela）/平方厘米（cd/cm2）。通过调整，不同显示器可以达到相同的光照强度。</p>
<p>亮度则是一个主观性的和光照相关的概念。显示器可以调整亮度，但是不好度量，只能自己感觉。</p>
<div class="blog_h3"><span class="graybg">帧和场</span></div>
<p>视频信号采样可以由：</p>
<ol>
<li>一系列连续的完整的帧构成，所谓逐行采样（Progressive Sampling） </li>
<li>一系列交错的场构成，所谓隔行采样（Interlaced Sampling）</li>
</ol>
<p>隔行采样时，通常在每个时间采样间隔中，两个场（分别由奇数行、偶数行构成）都进行采样。奇数行构成的场叫做Top Field，偶数行构成的场叫做Bottom Field。</p>
<p>隔行采样的优势是，提供两倍的帧率。根据人类的视觉停留的特点，可以避免产生画面抖动。</p>
<div class="blog_h2"><span class="graybg">色彩空间</span></div>
<p>相关文章：<a href="/image-processing-faq#color-space">图像处理知识集锦</a></p>
<p>大部分数字视频程序依赖于显示彩色图像，因此，需要一种机制来捕获、呈现颜色信息。单色图像仅仅需要一个数字来表示像素点的亮度/明度。彩色图像则需要至少三个数字来表示一个像素。</p>
<p>所谓色彩空间，就是用来描述<span style="background-color: #c0c0c0;">亮度/照度</span>（Brightness, Luminance or Luma）、<span style="background-color: #c0c0c0;">颜色</span>信息的方法。</p>
<div class="blog_h3"><span class="graybg">RGB</span></div>
<p>这种色彩空间中，一个采样点利用三个数字表示，分别代表红色、绿色、蓝色的<span style="background-color: #c0c0c0;">相对</span>比例。由于RGB是三原色，因而它们的组合可以形成任何颜色。RGB的各分量取值越大，则亮度越高。</p>
<div class="blog_h3"><span class="graybg">YCrCb</span></div>
<p>RGB色彩空间中，颜色信息、亮度信息是融合在一起的。然而，人类视觉系统（HVS）的特点是，对于亮度比颜色更加敏感。为了节省空间，可以把亮度信息分离出来，然后以较低的分辨率存储颜色信息，较高的分辨率存储亮度信息。</p>
<p>YCrCb（也叫YUV）就是一种分离亮度的色彩空间。其中Y是明度分量，Y根据根据RGB的权重计算出来：</p>
<p style="padding-left: 60px;"><em>Y = kr R + kgG + kbB        k*是颜色分量的权重因子</em></p>
<p>颜色信息则可以利用色差（Chrominance/Chroma）分量表示，每个色差分量即RGB与Y的差值：</p>
<p style="padding-left: 60px;"><em>Cr = R − Y</em><br /><em>Cb = B − Y</em><br /><em>Cg = G − Y</em></p>
<p>由于Cr+Cb+Cg求和是常量，因此，实际上仅仅需要记录两个色差信息就足够了。在YCrCb空间中，仅仅明度、红色差、蓝色差信号被传输。</p>
<p>YCrCb可以使用较低分辨率来描述Cr、Cb，由于HVS的特点，这样做图像质量不会受到太大影响。</p>
<div class="blog_h1"><span class="graybg">视频编码原理</span></div>
<p>视频编码的目的是实现视频压缩，这样视频信号更加容易存储和传输。没有压缩过的原始视频需要很高的比特率，对于标清视频来说，大概256pbps。</p>
<p>压缩总是要和解压缩配对使用，因此视频编码器通常包含压缩、解压缩两套算法。</p>
<p>某些类型的数据包含统计冗余（Statistical Redundancy），可以被无损的压缩/解压缩。不幸的是，要实现无损的图像、视频压缩，则压缩比会很低，因而在这些领域常常使用有损压缩。</p>
<p>视频的有损压缩原则是基于主观冗余（Subjective Redundancy），即在不太影响观察者的主观感受的前提下，信息可以被删除。大部分视频编码器同时关注空间、时间上的冗余：</p>
<ol>
<li>在时域上，相邻的场景总是有很大的相似性，特别是在高帧率的情况下</li>
<li>在空域上，类似于静态图片的压缩算法</li>
</ol>
<p>H.264和其它流行视频压缩算法——例如MPEG-2、MPEG-4、H.263——共享了一系列通用的特性。例如预测、基于块的运动补偿（Motion Compensation）。</p>
<div class="blog_h2"><span class="graybg">一般流程</span></div>
<p>编码器的一般工作流程如下图所示：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2017/09/encoder-procss.png"><img class="aligncenter size-full wp-image-16306" src="https://blog.gmem.cc/wp-content/uploads/2017/09/encoder-procss.png" alt="encoder-procss" width="100%" /></a></p>
<p>&nbsp;</p>
<p>编码器使用一个模型（Model）来表示视频源，一个搞笑的编码算法允许解码器尽可能保真的还原视频流。理想情况下，编码后的视频应该占据尽可能少的比特，同时尽可能的保真，当然这两个目标通常是冲突的。</p>
<p>编码器主要包括三个功能单元：预测模型、空闲模型、熵（Entropy，信息论中的熵是信息量的度量，熵越高包含的信息量越大）编码器：</p>
<ol>
<li>预测模型的输入时原始视频序列。预测模型利用邻近的视频帧/图像采样之间的相似性，来降低信息冗余。典型的做法是构造当前帧/视频数据块的预测（Prediction）。对于H.264来说，预测可以是：
<ol>
<li>帧内预测（Intra Prediction）：通过根据当前帧内的邻近的图像采样进行空间推断（Spatial Extrapolation），构造出预测</li>
<li>帧间预测（Inter Prediction）或者叫运动补偿预测（Motion Compensated Prediction）：通过补偿不同帧之间的差异构造出预测</li>
</ol>
</li>
<li>预测模型的输出是帧残余（Residual Frame）—— 从当前帧中减去预测，附加上说明帧间/帧内预测如何进行的模型参数</li>
<li>残余帧输入到空间模型，后者利用残余帧中的采样之间的相似性，降低空间冗余。H.264的做法是对残余帧进行转换并对结果进行量化。转换后的残余帧变为量化转换系数（Quantized Transform Coefficients）表示——量化移除了采样中不重要的数据以实现对残余帧的进一步压缩</li>
<li>预测模型的参数：帧内预测模式、帧间预测模式、运动向量（Motion Vectors），以及空间模型的参数，一起被熵编码器进一步压缩，移除统计学冗余数据。例如，反复出现的向量、系数被替换为简短的二进制代码。熵编码器产生容易传输的比特流或者文件</li>
<li>压缩完成后的视频序列，包括编码后的预测参数、编码后的残余系数，外加头信息</li>
</ol>
<div class="blog_h2"><span class="graybg">预测模型</span></div>
<p>预测模型处理的对象是当前帧/场中的一系列图像采样，其目标是减少数据冗余，其手段是构建一个预测，并将其中当前数据中减去。预测可能从先前已经编码好的帧中推导，此所谓时域预测；预测也可能从当前帧/场中已经编码好的图像采样中推导，此所谓空域预测。</p>
<p>预测模型的输出是<span style="background-color: #c0c0c0;">一系列残余/差异样本</span>。预测处理越精确，则残余样板中包含的Energy（信息量）越少。</p>
<div class="blog_h2"><span class="graybg">时域预测</span></div>
<p>被预测的帧的产生依赖于参考帧（Reference Frames），参考帧可以是过去或者未来的帧。帧预测的精度通常可以通过运动补偿——补偿当前帧和参考帧中由于物体移动产生的差异——的方式提高。</p>
<div class="blog_h3"><span class="graybg">简单预测</span></div>
<p>最简单的时域预测，是使用前一个帧（预测器，Predictor）来预测当前帧，<span style="background-color: #c0c0c0;">从当前帧中减去预测帧，直接得到帧残余</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2017/09/simple-prediction.png"><img class="size-large wp-image-16312 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/simple-prediction-1024x323.png" alt="simple-prediction" width="710" height="223" /></a></p>
<p>这种预测方法的缺点是，残余的信息量很大。这些残余很大程度上都是因为物体运动导致的，因而更好的时域预测算法能够通过自动补偿，减少不必要的信息量。</p>
<div class="blog_h3"><span class="graybg">运动导致的差异</span></div>
<p>帧之间的差异，主要原因包括：<span style="background-color: #c0c0c0;">物体运动、未覆盖（Uncovered）的区域、光照变化</span>。</p>
<p>物体运动的类型包括：</p>
<ol>
<li>死板的平移，例如汽车运动</li>
<li>变形运动，例如人说话时脸部的运动</li>
<li>镜头运动，例如平移、倾斜、缩放、旋转</li>
</ol>
<p>未覆盖区域的类型包括：</p>
<ol>
<li>由于物体移动而显露出来的背景区域</li>
</ol>
<p>除了未覆盖区域、光照变化之外的其他帧间差异，都属于<span style="background-color: #c0c0c0;">帧间像素移动</span>。估算每个像素在帧间的移动轨迹（Trajectory）是可能的，<span style="background-color: #c0c0c0;">像素移动轨迹构成的场被称为光流（Optical Flow）</span>：</p>
<p><img class="size-full wp-image-16314 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/optical-flow.png" alt="optical-flow" width="636" height="434" /></p>
<p>上图是前面前帧、后帧之间的光流的示意图。</p>
<p>如果得到了精确的光流场，那么就可以构造当前帧的绝大部分像素的精确预测，只需要将参考帧中的每一个像素沿着它的光流向量（Optical Flow Vector）移动即可。然而，精确的光流场需要大量的计算资源才能获得。</p>
<div class="blog_h3"><span class="graybg">基于块的运动估算和补偿</span></div>
<p>实践中经常使用的一种运动补偿方是，针对块（当前帧中一个矩形区域）进行运动补偿，这种方法避免了逐像素光流计算的资源消耗。基于块的运动补偿的流程如下（针对当前帧中每一个MxN大小的采样块）：</p>
<ol>
<li>搜索过去或者未来的参考帧中的一个相似的MxN采样块。具体的做法可能是，将当前帧的MxN块和搜索区域中所有可能的MxN块进行比较，从中选取最匹配的块。一个流行的判断“匹配”的准则是，将两个块进行相减得到残余，残余的Energy越低匹配度越高。寻找<span style="background-color: #c0c0c0;">最佳</span>匹配的过程被称为移动估算（Motion Estimation）</li>
<li>最佳匹配的块被作为当前MxN块的预测器（Predictor），预测器和当前块求差后，形成一个MxN的残余块 —— 运动补偿（Motion Compensation）</li>
<li>编码后的残余块，外加预测器和当前块之间的位置偏移（运动向量，Motion Vector），被一起发送</li>
</ol>
<p>解码器利用运动向量重新定位预测器区域，解码残余块，将预测器 + 残余块即可还原当前块。</p>
<p>基于块的运动补偿之所以流行，有如下几个原因：</p>
<ol>
<li>计算资源的消耗相对较小，而且算法比较直观</li>
<li>帧本身都是矩形的，和块运动补偿很适配</li>
<li>基于块的图像转换算法（例如离散余弦变换，Discrete Cosine Transform，DCT）和块运动补偿很适配</li>
<li>对于很多视频序列来说，块运动补偿能提供高效的时域模型（Temporal Model）</li>
</ol>
<p>但是这种运动补偿也有缺陷：</p>
<ol>
<li>真实物体很少具有能匹配矩形区域的边界</li>
<li>物体的帧间移动距离，常常不是整数个像素</li>
<li>很多类型的对象运动很难通过基于块的方式补偿 —— 例如变形、旋转，以及类似云或者烟雾那样复杂的运动</li>
</ol>
<p>尽管如此，当前所有视频编码标准均将基于块的运动补偿作为时域预测模型的基础。</p>
<div class="blog_h3"><span class="graybg">宏块的运动补偿预测</span></div>
<p>宏块（Macroblock）是帧中16x16大小的区域，它是包括MPEG-1、MPEG-2、MPEG-4 Visual、H.261、H.262、H,264在内的很多视频编码标准的运动补偿预测的基本单元。</p>
<p>在常见的YUV 4:2:0图像编码格式中，一个宏块由：</p>
<ol>
<li>256个照度采样构成，这些采样组成4个8x8的采样块</li>
<li>64个红色色差采样构成，这些采样组成1个8x8的采样块</li>
<li>64个蓝色色差采样构成，这些采样组成1个8x8的采样块</li>
</ol>
<p>即一共6个采样块，示意图如下：</p>
<p><img class="size-full wp-image-16320 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/macroblock-420.png" alt="macroblock-420" width="100%" /></p>
<p>宏块的运动估计，主要是寻找参考帧中和当前宏块匹配的16x16采样区域。参考帧是先前就编码好的一个帧，在时间维上，参考帧可以在过去或者未来。参考帧中以当前宏块为中心的区域被搜索，寻找最佳匹配。</p>
<p>最佳匹配的照度、色差采样，被从当前宏块中减去，这样就产生了一个残余宏块。<span style="background-color: #c0c0c0;">残余宏块</span>与标示了最佳匹配区域和当前宏块的相对位移的<span style="background-color: #c0c0c0;">移动向量</span>一起编码并传输。</p>
<p>在上述基本的运动估计、运动补偿的基础上，有很多变体的算法：</p>
<ol>
<li>如果使用了未来的帧作为参考帧，则未来的帧必须在当前帧之前编码，也就是帧的编码必须是乱序的</li>
<li>当参考帧和当前帧的差异非常大时，不使用运动补偿可能更加高效，编码器可能选择使用帧内预测</li>
<li>视频中的移动物体很少能恰恰匹配16x16的边缘，因此使用可变大小的块往往更加高效</li>
<li>物体移动的距离可能不是整像素，例如物体可能在水平方向移动3.83像素的距离。因此一个好的预测算法会在搜索最佳匹配之前在参考帧中，在次像素级别进行插值</li>
</ol>
<div class="blog_h3"><span class="graybg">宏块的尺寸</span></div>
<p>宏块的尺寸越小，则残余帧的Energy越低，预测越精准。但是相应的，计算复杂度越高。</p>
<p>为此，一个折衷的方式是：对于扁平、均匀的区域选择大的宏块尺寸；对于高度细节、复杂的移动区域选择小的宏块尺寸。</p>
<div class="blog_h3"><span class="graybg">次像素运动补偿</span></div>
<p>某些情况下，从参考帧的插值后（非整数像素）的采样位置进行预测可能获得更佳的效果。例如下图中，参考帧区域中像素被插值到半像素级别，这样匹配位置的精度可以提高一倍，通过搜索插值采样，可能获得更好的匹配。</p>
<p><img class="size-large wp-image-16328 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/half-pixel-interp-1024x481.png" alt="half-pixel-interp" width="710" height="333" /></p>
<p>通常来说，更加细粒度的插值可以提供更好的运动补偿效果，得到更低Engery的残余，代价是更高的复杂性。 但是这种效果的提升不是线性的，插值越精细，效果进一步的提升越小。</p>
<div class="blog_h2"><span class="graybg">空间预测</span></div>
<p>对当前块的空间预测，是基于当前帧中其它先前编码过的采样进行的。假设帧中的块以光栅扫描（Raster-scan） 顺序逐个编码，则所有左上方向的块都可以用于当前块的帧内预测。由于左上方向的块已经编码并存放到输出流，解码器很自然的可以用它们进行预测的重建。</p>
<p>帧内预测的具体算法有很多，H.264使用的是空间外推法（Spatial Extrapolation）。一个/多个预测由当前块上侧或左侧的外推采样构成。通常最靠近的采样最可能和当前块中的采用具有相关性，因而仅仅沿着上侧/左侧边缘的那些像素才会用来创建预测块。一旦预测块被创建，会被用来产生残余块，具体方式和帧间预测类似。</p>
<div class="blog_h2"><span class="graybg">图像模型</span></div>
<p>自然的视频帧是一系列采样构成的Grid，这种图片的原始格式很难被压缩，因为邻近的采样具有高相关性。下图左侧是某个自然视频帧的2D自相关（Autocorrelation）函数的曲面，高度表现了图片与其空间偏移之后的副本的相关性，底面的两个维度表示了空间偏移的方向。缓和的坡度提示了邻近样本的高度相关性。</p>
<p><img class="alignnone size-full wp-image-16332" src="https://blog.gmem.cc/wp-content/uploads/2017/09/autocorrelation.png" alt="autocorrelation" width="100%" /></p>
<p>而经过运动补偿的残余图像的自相关性函数如上图右侧所示，可以看到随着空间偏移的增大，相关性急剧的降低。这提示了邻近采样的若相关性。有效的运动补偿/帧间预测降低了残余图像的本地相关性，让其比原始的视频帧更加容易被压缩。</p>
<p>图像模型的功能是，进一步的对残余图像进行去相关（Decorrelate），让它能够更有效的被熵编码器所压缩。图像模型通常有三个处理阶段：</p>
<ol>
<li>转换（Transformation）：对图片进行去相关、让数据更加紧凑（Compact）</li>
<li>量化（Quantization）：降低转换后数据的精度</li>
<li>重排（Reordering）：对数据进行重新排序，让关键数值（Significant Values）分组在一起</li>
</ol>
<div class="blog_h3"><span class="graybg">预测性图像编码</span></div>
<p>运动补偿是预测性编码的一个例子，编码器基于过去/未来的某个帧创建当前帧中某个区域的预测，然后把预测从当前区域中减去，得到一个残余。如果预测成功的话，残余的Energy会比原始区域小，需要更少的比特来表示。</p>
<p>预测性编码是早期的视频编码器的基础，也是H.264的帧内编码的重要组件。空间预测需要基于先前传输的、当前帧已经编码好的区域的样本，这种预测方法有时被称为差分脉冲编码调制（Differential Pulse Code Modulation）。</p>
<p>下面的公式示意像素X的编码过程，我们假设帧基于光栅顺序处理。A、B、C是对于编解码器都可用的参考（相邻）像素，这些像素应该在X之前被编解码：</p>
<p style="padding-left: 60px;"><em>P(X) = (2A + B + C)/4</em><br /><em>R(X) = X − P(X)</em></p>
<p> 解码器根据A、B、C可以重新构造出预测，然后<em>X = R(X) + P(X)</em>即解码出像素X。四个像素的位置关系如下图：</p>
<p><img class="size-full wp-image-16338 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/spatil-prediction.png" alt="spatil-prediction" width="550" height="339" /></p>
<p>如果编码过程是有损的，也就是说残余被量化过，那么解码得到的A'/B'/C'并不和A/B/C完全一致，而依据A'/B'/C'推导出来的X'则会与X有更多的误差。这会导致编码器和解码器之间的累积性的误差，或者叫漂移（Drift）。为了避免这种漂移，编码器可以使用解码后的参考像素来构建残余，即：</p>
<p style="padding-left: 60px;"><em>P(X) = (2A’ + B'+ C‘</em><em>) / 4</em></p>
<p>这样编码器、解码器使用相同的P(X)，也就避免了漂移。</p>
<p>上面介绍的预测性编码算法的效率取决于P(X)的精度，如果P(X)和X很接近，则残余的Engery很低，预测效率就高。但是，很难为复杂图像的所有区域选取一个通用的预测器。为了得到高性能，通常需要基于图像的本地统计信息进行自适应性的预测器选取。例如，对于图片中的扁平纹理、强竖直纹理、强水平纹理区域选取不同的预测器。由于你需要使用额外的bit来告知解码器使用了哪些预测器，因此要注意预测性能和流量消耗之间的权衡。</p>
<div class="blog_h3"><span class="graybg">转换</span></div>
<p>图片或者视频编码器的转换阶段的意图是，将图片或者运动补偿残余数据转换到转换域（Transform Domain）。选择转换算法取决于一系列准则：</p>
<ol>
<li>转换域中的数据应该是：
<ol>
<li>去相关的，也就是说，这些数据应该分离到最小相关性的分量中</li>
<li>紧凑的，大部分的Energy应该集中到数据的一小部分数值中</li>
</ol>
</li>
<li>转换必须是可逆的</li>
<li>转换对计算资源的需求必须是可容忍的，包括内存、CPU</li>
</ol>
<p>流行的图像/视频转换转发基本上分为两大类：</p>
<ol>
<li>基于块的：包括KLT、SVD、DCT。这些算法以NxN的采样为操作单元，其优点是内存用量小，适合压缩基于块的运动补偿残余。这类算法的缺点是块效应（Blockiness）明显</li>
<li>基于图像的：在整个图像/帧上，或者大块的区域（所谓Tile）上操作。包括离散小波变换（Discrete Wavelet Transform，DWT）。这类算法对于静态图像的压缩处理由于上一类，但是需要更高的内存</li>
</ol>
<div class="blog_h3"><span class="graybg">量化</span></div>
<p>量化器（Quantizer）将信号值范围X映射到一个较小的值范围Y。主要有两类量化器：</p>
<ol>
<li>标量量化器：将输入信号中的一个采样映射为一个量化的输出值</li>
<li>向量量化器：将输入信号中的一组采样映射为一组量化值</li>
</ol>
<div class="blog_h3"><span class="graybg">重排和零编码</span></div>
<p>对于一个基于转换的图像/视频编码器，量化器的输出是一个稀疏的数组。其中包含少量的非零系数，以及大量的零值系数。</p>
<p>重排阶段的工作就是把非零系数排列在一起，然后标识出这些系数在数组中的索引，实现压缩。</p>
<div class="blog_h2"><span class="graybg">熵编码器</span></div>
<p>熵编码器把一系列表示视频序列的元素转换成适合传输和存储的压缩比特流。输入符号包括量化后的转换系数、整/次像素级别的移动向量、标记性编码、宏块头、图像头等。</p>
<p>在信息论中，熵编码属于无损压缩，且压缩不受媒介的特质影响。熵编码器可以把定长的输入符号替换为相应的可变长度的代号（C<span style="color: #222222;">odeword），从而实现压缩。代号的长度和出现几率的负对数正相关，因而大部分公共符号具有最短的代号。</span></p>
<div class="blog_h1"><span class="graybg">H.264简介</span></div>
<div class="blog_h2"><span class="graybg">H.264是什么</span></div>
<p>从不同的视角看，H.264可以有不同的含义：</p>
<ol>
<li>它是一个工业标准，定义了一种压缩视频格式</li>
<li>一种流行的视频格式</li>
<li>一套用于视频压缩的工具 </li>
</ol>
<p>编码是视频类应用的基础技术，因为原始视频格式太大，难以传输或者存储。对视频编码进行标准化，可以让不同厂商开发的编码器、解码器、媒体存储能够方便的互操作。</p>
<p>典型的H.264应用，例如远程视频监控，视频从摄像头采集出来后被编码为H.264比特流，通过网络传输。终端应用解码比特流并获得原始视频：</p>
<p><img class="alignnone size-full wp-image-16343" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-process.png" alt="h264-process" width="100%" /></p>
<p>H.264标准首次发布于2003年，之后经历了数次修订和更新。它基于早先的视频编码标准的设计理念，进一步提高了压缩视频的质量，在压缩、传输、存储方面有更大的灵活性。</p>
<p>H.264描述了一组用于压缩的工具/方法，规定了基于这些工具编码的视频如何呈现和解码。视频编码器可以选择一个工具，应用一些约束，然后处理视频流。H.264兼容的解码器必须能够使用工具组的<span style="background-color: #c0c0c0;">某个子集 —— 所谓配置（Profile）</span>。</p>
<div class="blog_h2"><span class="graybg">H.264如何工作</span></div>
<p>通过预测、转换、编码等处理过程，H.264编码器生成一个H.264比特流。解码器则进行逆向处理——解码、反向转换、重构——以生成原始（Raw）视频序列。</p>
<p>每个视频帧/场都需要被编码器处理，帧/场被编码后，可能被放到已编码图像缓冲中（Coded Picture Buffer，CPB）。在编码后续帧时，编码器可以使用CPB。类似的，解码器在解码出一个帧后，将其放到已解码图像缓冲中（Decded Picture Buffer，DPB），在解码后续帧时可以使用DPB。</p>
<p><img class="alignnone  wp-image-16347" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-codec-high-lv-view.png" alt="h264-codec-high-lv-view" width="908" height="269" /></p>
<div class="blog_h3"><span class="graybg">编解码流程总览</span></div>
<p>H.264的数据处理单元是16x16大小的宏块（Macroblock） 。</p>
<p>在编码器中，预测宏块从当前宏块中减去，得到一个残余宏块。残余宏块被转换、量化并编码。在此同时，量化后的数据被重新扫描、反向转换并加上预测宏块，得到一个编码后的帧版本，然后存储起来用于后续的预测：</p>
<p><img class="alignnone  wp-image-16351" src="https://blog.gmem.cc/wp-content/uploads/2017/09/typical-h264-encoder.png" alt="typical-h264-encoder" width="911" height="372" /></p>
<p>在解码器中，宏块被解码、重新扫描、反向转换，得到一个编码过的残余宏块。解码器生成预测宏块后加上残余宏块，产生解码后的宏块：</p>
<p><img class="alignnone  wp-image-16353" src="https://blog.gmem.cc/wp-content/uploads/2017/09/typical-h264-decoder.png" alt="typical-h264-decoder" width="908" height="367" /></p>
<div class="blog_h3"><span class="graybg">编码流程</span></div>
<p>预测阶段，包括帧间预测和帧内预测。H.264支持的预测方法很灵活，从而实现更精确的预测。帧内预测使用16x16或者4x4的块大小，从当前宏块的四周进行预测。帧间预测的块大小可以在16x16 - 4x4之间自由变动，参考帧可以来自过去或者未来。</p>
<p>预测阶段产生的残余采样，使用4x4或者8x8的整数变换（Integer Transform）——离散余弦变换的近似形式——进行转换，转换的输出是一组系数。<span style="background-color: #c0c0c0;">系数的每个成员是一种标准化基本图式（Standard Basis Patterns）的权重值</span>。通过系数可以重新创建出残余采样：</p>
<p><img class="size-full wp-image-16359 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/forward-transform.png" alt="forward-transform" width="883" height="617" /></p>
<p>转换的结果进一步被量化，也就是，每个系数除以一个整数。量化后的转换系数精度降低：</p>
<p><img class="size-full wp-image-16360 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2017/09/quantization.png" alt="quantization" width="845" height="695" />视频编码最终产生的是一系列需要编码组成压缩比特流的数值，这些数值包括：</p>
<ol>
<li>量化后的转换系数</li>
<li>供解码器重建预测的信息</li>
<li>压缩数据结构相关信息</li>
<li>和完整视频序列有关的信息 </li>
</ol>
<p>这些数值和参数，以及语法元素（Syntax Elements），被可变长度编码/算术编码算法转换为二进制代码。</p>
<div class="blog_h3"><span class="graybg">解码流程</span></div>
<p>首先要进行的是对二进制比特流进行解码，解码语法元素并抽取上节所述的数值和参数。</p>
<p>然后是重扫描，每个系数乘以一个整数以近似的还原其原始值：</p>
<p><img class="aligncenter size-full wp-image-16362" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rescaling.png" alt="rescaling" width="897" height="295" /></p>
<p>重扫描后的图式权重系数，加上标准化基础图式，经过反向离散余弦变换/整数变换可以重新创建出采样的残余数据：</p>
<p><img class="aligncenter size-full wp-image-16364" src="https://blog.gmem.cc/wp-content/uploads/2017/09/inverse-transform.png" alt="inverse-transform" width="795" height="607" /></p>
<p>得到采样残余后，解码器使用和编码器一样的预测，加上残余即得到原始图像。</p>
<div class="blog_h2"><span class="graybg">H.264语法</span></div>
<p>H.264规范清晰的定义了一套格式，或者叫语法。用于呈现压缩视频及其相关信息。这套语法的总体结构图如下：</p>
<p><img class="aligncenter  wp-image-16368" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-syntax.png" alt="h264-syntax" width="902" height="924" /></p>
<p>在最顶层， 一个H.264序列由一系列的包（Packet），或者叫网络抽象层单元（Network Abstraction Layer Unit，NALU）构成。NAL可以包含解码器需要用到的关键参数集，这些参数集指示解码器如何正确的解码帧（Frame）或切片（Slice）。所谓切片，是指被分解后的帧的一部分，帧可以仅仅包含一个切片</p>
<p>在下一层，切片由一系列编码过的宏块组成。每个宏块对应帧中16x16大小的块。</p>
<p>在最底层，宏块包含描述自己如何被编码的信息 —— 编码的具体方法、预测信息、残余采样等。</p>
<div class="blog_h1"><span class="graybg">H.264语法</span></div>
<p>所谓H.264视频，是一种遵循特定规范——H.264/AVC语法——的视频序列。 此语法是H.264规范的一部分，它以语法元素的形式精确的描述了H.264视频序列结构的不同层面。</p>
<p>此语法是层次性的，它描述了最顶层的视频序列，以及下层的帧/场、切片，直到底层的宏块。控制参数可以：</p>
<ol>
<li>以独立的语法区段存储，例如参数集（Parameter Sets）</li>
<li>嵌入为其它区段（宏块层）的一部分</li>
</ol>
<div class="blog_h2"><span class="graybg">概要</span></div>
<p>H.264语法的层次性组织如下图所示：</p>
<p><img class="aligncenter size-large wp-image-16372" src="https://blog.gmem.cc/wp-content/uploads/2017/09/syntax-overview-966x1024.png" alt="syntax-overview" width="710" height="752" />说明如下：</p>
<ol>
<li>网络抽象层：由一系列的NAL单元组成：
<ol>
<li>SPS、PPS是特殊的NAL单元，作为解码器特定通用控制参数变更的信号</li>
<li>编码后的视频数据对应视频编码层（Video Coding Layer）NAL单元，也被称为切片（Slice）。每个访问单元（Access Unit），即编码后的帧/场，可以由1-N个切片构成</li>
</ol>
</li>
<li>切片层：每个切片包括切片头、切片数据两部分。切片数据是一系列编码后的宏块，外加可能的跳过提示符。跳过提示符用于指示特定的宏块位置没有数据</li>
<li>宏块层：每个编码后的宏块包括如下语法元素：
<ol>
<li>MB类型：
<ol>
<li>I：帧内编码</li>
<li>P：基于一个参考帧进行帧间编码</li>
<li>B：基于1-2个参考帧进行帧间编码</li>
</ol>
</li>
<li>预测信息：I宏块的预测模式，P/B宏块的参考帧和移动向量</li>
<li>编码块图式（Coded Block Pattern CBP）：提示哪些明度块、色差块包含非零残余系数</li>
<li>量化参数（Quantization Parameter QP）：仅仅CBP非零的宏块具有此元素</li>
<li>残余数据：仅仅CBP非零的宏块具有此元素</li>
</ol>
</li>
</ol>
<p>编码后的视频序列总是以即时解码器刷新（Instantaneous Decoder Refresh，IDR）访问单元开始，其包括若干个IDR切片。IDR切片是一种特殊的帧内编码切片。IDR访问单元后面跟着很多普通的访问单元序列。当一个新的视频序列到达时，需要提前再次发送IDR切片。此外传输结束时也发送IDR切片。</p>
<div class="blog_h3"><span class="graybg">语法区段列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 18%; text-align: center;">区段</td>
<td style="width: 20%; text-align: center;">包含区段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NAL unit</td>
<td>RBSP</td>
<td>
<p>网络抽象层单元，包含原始字节序列载荷（Raw Byte Sequence Payload，RBSP）。RBSP是包含了H.264语法元素的字节序列。H.264元素的长度是以位计算的可变长度，因此RBSP的总长度不一定是整数字节。因此RBSP尾部会补零（Trailing Bits）确保匹配整数字节</p>
<p>一个RBSP语法元素可以在单独的包中发送。某些语法端包含子段</p>
</td>
</tr>
<tr>
<td>SPS</td>
<td>Scaling List<br />VUI Parameters<br />Trailing bits</td>
<td>
<p>此区段是RBSP</p>
<p>序列参数集（Sequence Parameter Set），对于视频序列通用的参数</p>
</td>
</tr>
<tr>
<td>Scaling List </td>
<td> </td>
<td>编码器提供的用于反向量化处理的缩放矩阵 </td>
</tr>
<tr>
<td>SPS Extension</td>
<td>Trailing bits</td>
<td>此区段是RBSP。包含用于阿尔法混合（Alpha Blending，混合多个透明图片）的辅助图片信息</td>
</tr>
<tr>
<td>SEI</td>
<td>SEI Message<br />Trailing bits</td>
<td>
<p>此区段是RBSP</p>
<p>辅助增强信息（Supplement Enhancement Information），SEI消息容器</p>
</td>
</tr>
<tr>
<td>SEI Message </td>
<td>SEI payload</td>
<td>此区段是RBSPSEI消息可以用于辅助解码或显示，但是不影响解码帧的构建 </td>
</tr>
<tr>
<td>AUD</td>
<td>Trailing bits</td>
<td>
<p>此可选区段是RBSP</p>
<p>访问单元定界符（Access Unit Delimiter），可选的定界符，用于指示下一个编码图片的切片类型</p>
</td>
</tr>
<tr>
<td>End of Sequence</td>
<td> </td>
<td>
<p>此可选区段是RBSP。指示下一个切片是IDR </p>
</td>
</tr>
<tr>
<td>End of Stream</td>
<td> </td>
<td>此可选区段是RBSP。指示视频流的结束</td>
</tr>
<tr>
<td>Filler Data</td>
<td>Trailing bits </td>
<td>此可选区段是RBSP。填充字节序列</td>
</tr>
<tr>
<td>Slice layer</td>
<td>Slice header<br />Slice data<br />Trailing bits</td>
<td>此区段是RBSP。编码后的切片，分为几个类别：
<ol>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="font-size: 13px; line-height: 22px;">Slice layer without partitioning，不适用分区的切片</span></span></li>
<li><span style="color: #333333; font-family: Ubuntu, 'Times New Roman', 'Bitstream Charter', Times, serif;"><span style="font-size: 13px; line-height: 22px;">Slice data partition A layer，分区切片的分区A</span></span></li>
<li>Slice data partition B layer，分区切片的分区B</li>
<li>Slice data partition C layer，分区切片的分区C</li>
</ol>
</td>
</tr>
<tr>
<td>Slice header</td>
<td>RPLR<br />PWT<br />DRPR</td>
<td>对于切片的通用参数 </td>
</tr>
<tr>
<td>RPLR</td>
<td> </td>
<td>引用图片列表重排（Reference Picture List Reordering） ，一系列用于修改默认引用图片列表顺序的命令</td>
</tr>
<tr>
<td>PWT</td>
<td> </td>
<td>预测权重表格（Prediction Weight Table） ，明度、色差权重偏移量，用于影响运动补偿预测的效果</td>
</tr>
<tr>
<td>DRPR</td>
<td> </td>
<td>解码后引用图片标记（Decoded Reference Picture Marking），一系列用于标记引用图片为长期引用的命令</td>
</tr>
<tr>
<td>Slice data</td>
<td>MB layer</td>
<td>包含一系列编码后的宏块</td>
</tr>
<tr>
<td>MB layer</td>
<td>MB prediction<br />Sub-MB prediction<br />Residual data</td>
<td>PCM头、宏块头、 预测、转换系数</td>
</tr>
<tr>
<td>MB prediction</td>
<td> </td>
<td>帧内预测模式，或者引用索引+移动向量</td>
</tr>
<tr>
<td>Sub-MB prediction </td>
<td> </td>
<td>引用索引+移动向量</td>
</tr>
<tr>
<td>Residual data </td>
<td>RB CAVLC<br />RB CABAC</td>
<td>包含一系列残余块，具体内容取决于CBP</td>
</tr>
<tr>
<td>RB CAVLC </td>
<td> </td>
<td>基于CAVLC编码的转换系数块 </td>
</tr>
<tr>
<td>RB CABAC</td>
<td> </td>
<td>基于CABAC编码的转换系数块</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">帧/场/图片</span></div>
<p>H.264将帧定义为一组明度采样数组，外加两组对应的色差采样数组。场分为Top field、Bottom field，共同构成帧，两个场可以同时扫描或者交错扫描。术语图片（Picture）作为帧/场的通称。</p>
<p>帧/场被解码，形成解码后图片并被放置到解码后图片缓冲（Decoded Picture Buffer，DPB）。此缓冲中的图片可以用于：</p>
<ol>
<li>支持后续的帧间预测</li>
<li>输出到显示组件</li>
</ol>
<p>区分以下三个顺序很重要：</p>
<ol>
<li>解码顺序：图片从比特流中被解码的顺序</li>
<li>显示顺序：图片输出到显示组件的顺序</li>
<li>参考顺序：图片如何被排列以供其它图片进行帧间预测</li>
</ol>
<div class="blog_h3"><span class="graybg">解码顺序</span></div>
<p>帧和帧之间可能存在引用（时域预测）关系，因此它们的解码顺序必须是确定的。</p>
<p>解码序（Decoding Order）确定了编码后的帧/场的解码顺序，由切片头参数frame_num确定。一般情况下，当前帧的frame_num为先前参考帧的frame_num+1。</p>
<div class="blog_h3"><span class="graybg">显示顺序</span></div>
<p>显示序（Display Order）即帧/场的播放顺序。由参数图像顺序计数器（POC，Picture Order Count）确定，POC参数包括TopFieldOrderCount、BottomFieldOrderCount。这两个参数也来自切片头，获取方法有三种：</p>
<ol>
<li>类型0：在每个切片头中，都包含了POC的最低有效位（Least Significant Bits），这种方式提供了灵活性但是占据更多字节。Type0示意如下图，箭头（从被参考帧发起）表示帧引用关系：<img class="aligncenter  wp-image-16451" src="https://blog.gmem.cc/wp-content/uploads/2017/09/doc-type0.png" alt="doc-type0" width="100%" /></li>
<li>类型1：在SPS中设立一个循环的POC计数器，POC依据此计数器循环计数，除非切片头使用Delta Offset</li>
<li>类型2：直接从frame_num获得，解码序和显示序一致</li>
</ol>
<div class="blog_h3"><span class="graybg">参考顺序</span></div>
<p>图片编码后，如果允许被其它图片参考，则进入已解码图片缓冲（Decoded Picture Buffer，DPB），并被标记为以下两种之一：</p>
<ol>
<li>短期参考图片，以frame_num或者POC进行索引。把这类图片从DPB移除的方法有：
<ol>
<li>通过比特流中明确的命令移除</li>
<li>如果启用了DPB自动处理模式，并且DPB已满，自动移除最旧的图片</li>
</ol>
</li>
<li>长期参考图片，以LongTermPicNum进行索引，此数字基于图片被标记为长期参考帧时设置的参数LongTermFrameIdx推导。这类图片需要通过比特流中明确的命令移除</li>
</ol>
<p>短期参考图片后续可以被赋予LongTermFrameIdx，导致它变为长期参考图片。</p>
<div class="blog_h3"><span class="graybg">默认参考图像列表顺序</span></div>
<p>参考图像列表（Reference Picture List） 是存放参考图片引用的列表。对于P切片来说，使用单个列表list0；对于B切片来说，使用两个列表list0、list1。</p>
<p>在每个列表中，短期参考图片排在前面，短期参考图片的排列规则：</p>
<ol>
<li>如果当前切片是P，依赖于解码序</li>
<li>如果当前切片是B，依赖于显示序</li>
</ol>
<p>长期参考图片排在短期参考图片后面，且按照LongTermPicNum升序排列。</p>
<p>列表元素的排序细节很重要，因为要引用列表中前面的项需要的比特数更少。因此默认排序规则让“接近”当前图像的参考图像排在列表前面，这些参考图像中存在最佳预测匹配的几率更大：</p>
<ol>
<li>P切片的list0：默认顺序是PicNum的降序，frame_num对MaxFrameNum取模得到PicNum</li>
<li>B切片的list0：默认顺序是：
<ol>
<li>如果参考图片的POC比当前图像早，则按POC降序</li>
<li>如果参考图片的POC比当前图片晚，则按POC升序</li>
</ol>
</li>
<li>B切片的list1：默认顺序是：
<ol>
<li>如果参考图片的POC比当前图像早，则按POC升序</li>
<li>如果参考图片的POC比当前图片晚，则按POC降序</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">NAL单元</span></div>
<p>编码后的H.264数据以NAL单元这种数据包在网络中发送。每个NAL单元包含1字节的NALU头，后面跟着包含控制参数或者视频数据的比特流。</p>
<p>NALU头包含信息：</p>
<ol>
<li>NALU的类型<br />
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 70px; text-align: center;">值</td>
<td style="text-align: center;">NALU类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>未使用</td>
<td> </td>
</tr>
<tr>
<td>1</td>
<td>Coded slice, non-IDR</td>
<td>典型的切片</td>
</tr>
<tr>
<td>2</td>
<td>Coded slice data partition A</td>
<td>数据分区切片，分区A</td>
</tr>
<tr>
<td>3</td>
<td>Coded slice data partition B</td>
<td>激活数据分区切片，分区B</td>
</tr>
<tr>
<td>4</td>
<td>Coded slice data partition C</td>
<td>数据分区切片，分区C</td>
</tr>
<tr>
<td>5</td>
<td>Coded slice, IDR</td>
<td>作为视频序列起点</td>
</tr>
<tr>
<td>6</td>
<td>SEI</td>
<td>补充增强信息</td>
</tr>
<tr>
<td>7</td>
<td>SPS</td>
<td>序列参数集，每序列一个</td>
</tr>
<tr>
<td>8</td>
<td>PPS</td>
<td>图像参数集</td>
</tr>
<tr>
<td>9</td>
<td>Access unit delimiter</td>
<td>提示下一个编码图片的切片类型</td>
</tr>
<tr>
<td>10</td>
<td>End of sequence</td>
<td>提示下一个NALU是IDR</td>
</tr>
<tr>
<td>11</td>
<td>End of stream</td>
<td>提示视频序列结束</td>
</tr>
<tr>
<td>12</td>
<td>Filler</td>
<td>填充字节</td>
</tr>
<tr>
<td>13-23</td>
<td>保留</td>
<td> </td>
</tr>
<tr>
<td>24-31</td>
<td>不保留，RTP打包用到</td>
<td> </td>
</tr>
</tbody>
</table>
</li>
<li>NALU的重要程度</li>
</ol>
<div class="blog_h3"><span class="graybg">NALU头结构</span></div>
<p>NALU头固定为1字节长，其结构示意图如下：</p>
<p style="padding-left: 60px;"><span class="monospace">+---------------+</span><br /><span class="monospace">|0|1|2|3|4|5|6|7|</span><br /><span class="monospace">+-+-+-+-+-+-+-+-+</span><br /><span class="monospace">|F|NRI| Type    |</span><br /><span class="monospace">+---------------+</span></p>
<p>其中：</p>
<ol>
<li>forbidden_zero_bit，第1位，必须为0</li>
<li>nal_ref_idc，第2-3位，重要程度。值越大越重要，当解码器过载时可以考虑把值为0的NALU丢弃。在RTP中使用，NRI还指示了传输的相对优先级</li>
<li>nal_unit_type，最后5位。类型6/9/10/11/12对应的NRI应该为00，类型7/8对应第NRI应该为11</li>
</ol>
<div class="blog_h2"><span class="graybg">参数集</span></div>
<p>参数集是携带了解码参数的NALU，这些参数对于后续若干切片是公用的，独立于切片发送参数集可以提高效率。 这些参数对于正确解码非常重要，在不可靠信道上传输视频流时，参数集可能丢失，可以考虑用更高的QoS发送参数集。</p>
<p>序列参数集（SPS）包含对整个视频序列有效的参数，例如Profile和Level、帧尺寸、某些解码器约束（例如参考帧最大数量）。SPS示例：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="width: 12%; text-align: center;">取值</td>
<td style="width: 8%; text-align: center;">符号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>profile_idc</td>
<td>1000010    </td>
<td>66</td>
<td>使用Profile Baseline</td>
</tr>
<tr>
<td>constrained_set0_flag</td>
<td>0</td>
<td>0</td>
<td>比特流可能不遵从Baseline的所有约束</td>
</tr>
<tr>
<td>constrained_set1_flag</td>
<td>0</td>
<td>0</td>
<td>比特流可能不遵从Main的所有约束</td>
</tr>
<tr>
<td>constrained_set2_flag</td>
<td>0</td>
<td>0</td>
<td>比特流可能不遵从Extended的所有约束</td>
</tr>
<tr>
<td>reserved_zero_4bits</td>
<td>0</td>
<td>0</td>
<td>保留的4bit</td>
</tr>
<tr>
<td>level_idc</td>
<td>11110</td>
<td>30 </td>
<td>级别3</td>
</tr>
<tr>
<td>seq_parameter_set_id</td>
<td>1</td>
<td>0 </td>
<td>SPS标识符</td>
</tr>
<tr>
<td>log2_max_frame_num_minus4</td>
<td>1</td>
<td>0 </td>
<td>frame_num不大于16 </td>
</tr>
<tr>
<td>pic_order_cnt_type</td>
<td>1 </td>
<td>0 </td>
<td>默认POC </td>
</tr>
<tr>
<td>log2_max_pic_order_cnt_lsb_minus4</td>
<td>1</td>
<td>0 </td>
<td>POC的LSB不大于16 </td>
</tr>
<tr>
<td>num_ref_frames</td>
<td>1011 </td>
<td>10 </td>
<td>最多10个参考帧 </td>
</tr>
<tr>
<td>gaps_in_frame_num_value_allowed_flag</td>
<td>0</td>
<td>0 </td>
<td>frame_num中没有gap</td>
</tr>
<tr>
<td>pic_width_in_mbs_minus1</td>
<td>1011</td>
<td>10</td>
<td>11宏块宽 = QCIF</td>
</tr>
<tr>
<td>pic_height_in_map_units_minus1</td>
<td>1001</td>
<td>8</td>
<td>9宏块高 = QCIF </td>
</tr>
<tr>
<td>frame_mbs_only_flag</td>
<td>1</td>
<td>1</td>
<td>没有场切片或者场宏块</td>
</tr>
<tr>
<td>direct_8_×_8_inference_flag</td>
<td>1</td>
<td>1 </td>
<td>指定B宏块的移动向量如何得出 </td>
</tr>
<tr>
<td>frame_cropping_flag</td>
<td>0 </td>
<td>0</td>
<td>帧没有被裁剪</td>
</tr>
<tr>
<td>vui_parameters_present_flag</td>
<td>0</td>
<td>0</td>
<td>VUI参数不存在 </td>
</tr>
</tbody>
</table>
<p>图像参数集（PPS）包含应用到一部分帧的参数，例如熵编码类型、活动参考图片数量、初始化参数。PPS继承特定SPS的参数。PPS示例：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="width: 12%; text-align: center;">取值</td>
<td style="width: 8%; text-align: center;">符号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 35%;">pic_parameter_set_id</td>
<td style="width: 12%;">1 </td>
<td style="width: 8%;">0</td>
<td>PPS标识符</td>
</tr>
<tr>
<td>seq_parameter_set_id</td>
<td>1</td>
<td>0</td>
<td>继承自的SPS</td>
</tr>
<tr>
<td>entropy_coding_mode_flag</td>
<td>0</td>
<td>0</td>
<td>基于CAVLC进行熵编码</td>
</tr>
<tr>
<td>pic_order_present_flag</td>
<td>0</td>
<td>0</td>
<td>POC未设置</td>
</tr>
<tr>
<td>num_slice_groups_minus1</td>
<td>1</td>
<td>0</td>
<td>一个Slice组</td>
</tr>
<tr>
<td>num_ref_idx_l0_active_minus1</td>
<td>1010</td>
<td>9</td>
<td>第一个列表中有10个参考图像</td>
</tr>
<tr>
<td>num_ref_idx_l1_active_minus1</td>
<td>1010</td>
<td>9</td>
<td>第二个列表中有10个参考图像</td>
</tr>
<tr>
<td>weighted_pred_flag</td>
<td>0</td>
<td>0</td>
<td>没有使用权重预测</td>
</tr>
<tr>
<td>weighted_bipred_idc</td>
<td>0</td>
<td>0</td>
<td>没有使用双向权重预测</td>
</tr>
<tr>
<td>pic_init_qp_minus26</td>
<td>1</td>
<td>0</td>
<td>初始明度量化参数为26</td>
</tr>
<tr>
<td>pic_init_qs_minus26</td>
<td>1</td>
<td>0</td>
<td>初始SI/SP 量化参数为26</td>
</tr>
<tr>
<td>chroma_qp_index_offset</td>
<td>1</td>
<td>0</td>
<td>色差量化参数没有设置</td>
</tr>
<tr>
<td>deblocking_filter_control_present_flag</td>
<td>0</td>
<td>0</td>
<td>使用默认过滤器参数</td>
</tr>
<tr>
<td>constrained_intra_pred_flag</td>
<td>0</td>
<td>0</td>
<td>帧内预测不受限</td>
</tr>
<tr>
<td>redundant_pic_cnt_present_flag</td>
<td>0</td>
<td>0</td>
<td>没有使用荣誉图像计数参数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">参数集的激活 </span></div>
<p>直到切片头引用PPS之前，PPS没有被激活，也就是编码器不使用它。一旦应用，则一直有效，直到另一个PPS被激活。</p>
<p>SPS仅仅在引用它PPS激活时，才被激活。单一的SPS之后对整个流有效，而流以IDR切片开始，因而通常由IDR激活SPS。</p>
<div class="blog_h2"><span class="graybg">切片层</span></div>
<p>每个编码后的帧/场都由1-N个切片组成。切片以切片头开始，后面跟着1-N个宏块，宏块的数量可以不固定。</p>
<p>切片大小的选择方式有：</p>
<ol>
<ol>
<ol>
<li>每个帧一个切片，很多H.264编码器选择这种方式</li>
<li>每个帧分为N个切片，每个切片分为M个宏块。切片的比特数量随着运动量的变大而便多</li>
<li>每个帧分为N个切片，每个切片包含的宏块数量不一定。这种方式可以让切片的比特数大致一致，用于固定长度的网络包</li>
</ol>
</ol>
</ol>
<div class="blog_h3"><span class="graybg">切片类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">切片类型</td>
<td style="width: 20%; text-align: center;">内部宏块类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>I（包括IDR）</td>
<td>仅I</td>
<td>仅帧内预测</td>
</tr>
<tr>
<td>P</td>
<td>I或P</td>
<td>帧内预测、每个宏块分区基于一个参考帧预测</td>
</tr>
<tr>
<td>B</td>
<td>I、P或B</td>
<td>帧内预测、每个宏块分区基于1-2个参考帧预测</td>
</tr>
<tr>
<td>SP</td>
<td>P或I</td>
<td>用于切换到不同的流</td>
</tr>
<tr>
<td>SI</td>
<td>SI</td>
<td>用于切换到不同的流</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">切片头</span></div>
<p>切片头携带了对于所有宏块通用的信息，例如：</p>
<ol>
<li>切片类型，限制了宏块可能的类型</li>
<li>帧编号（Frame Number），此切片所属的帧</li>
</ol>
<p>一个示例切片头如下（IDR/Intra，Frame0）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="width: 12%; text-align: center;">取值</td>
<td style="width: 8%; text-align: center;">符号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 35%;">first_mb_in_slice</td>
<td style="width: 12%;">1</td>
<td style="width: 8%;">0</td>
<td>第一个宏块位于位置0 —— 当前切片的左上角</td>
</tr>
<tr>
<td>slice_type</td>
<td>1000</td>
<td>7</td>
<td>这是一个I切片</td>
</tr>
<tr>
<td>pic_parameter_set_id</td>
<td>1</td>
<td>0</td>
<td>使用PPS 0</td>
</tr>
<tr>
<td>frame_num</td>
<td>0</td>
<td>0</td>
<td>此切片属于帧 0</td>
</tr>
<tr>
<td>idr_pic_id</td>
<td>1</td>
<td>0</td>
<td>仅出现在IDR切片，IRD #0</td>
</tr>
<tr>
<td>pic_order_cnt_lsb</td>
<td>0</td>
<td>0</td>
<td>POC = 0</td>
</tr>
<tr>
<td>no_output_of_prior_pics_flag</td>
<td>0</td>
<td>0</td>
<td>未使用</td>
</tr>
<tr>
<td>long_term_reference_flag</td>
<td>0</td>
<td>0</td>
<td>未使用长期参考帧</td>
</tr>
<tr>
<td>slice_qp_delta</td>
<td>1000</td>
<td>4</td>
<td>量化参数偏移量 = initial QP + 4 = 30</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">切片数据</span></div>
<p>切片数据为若干宏块的集合。有一种没有数据的宏块 —— Skip Macroblock，在很多编码序列中会出现。和熵编码器有关。</p>
<div class="blog_h2"><span class="graybg">宏块层 </span></div>
<p>宏块层中包含解码一个宏块所需要的所有语法元素：￼</p>
<p><img class="aligncenter size-full wp-image-16386" src="https://blog.gmem.cc/wp-content/uploads/2017/09/macroblock-syntax.png" alt="macroblock-syntax" width="445" height="1002" /></p>
<p>其中：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">语法元素</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>mb_type</td>
<td>指示当前宏块的类型，可以是I、SI、P、B，并且可以包含额外的信息说明宏块如何编码和预测</td>
</tr>
<tr>
<td>transform_size_8_×_8_flag</td>
<td>仅仅出现在High Profile中，此元素可以出现在两个位置之一，这取决于宏块的类型。此元素不会出现在16x16的帧内预测宏块</td>
</tr>
<tr>
<td>mb_pred</td>
<td>除了8x8分区大小的P/B宏块之外，指示帧内或者帧间预测类型</td>
</tr>
<tr>
<td>sub_mb_pred</td>
<td>8x8分区大小的P/B宏块，指示帧内或者帧间预测类型</td>
</tr>
<tr>
<td>coded_block_pattern</td>
<td>除了16x16帧内预测块，取值范围0-47之间</td>
</tr>
<tr>
<td>delta_qp</td>
<td>指示量化参数的变化</td>
</tr>
<tr>
<td>residual_data</td>
<td>残余块</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">配置和级别</span></div>
<p>前文提到过，H.264支持一组工具 —— 处理编解码的算法和过程。这些工具中有些很基础，任何编解码器实现都需要用到，例如4x4的变换算法。另外一些工具是可选的，例如CABAC/CAVLC熵编码器。</p>
<p>通过按需选择标准中定义的工具，编码器的实现可以非常的灵活，编码器可以仅仅使用工具的某些子集。</p>
<div class="blog_h2"><span class="graybg">Profile</span></div>
<p>H.264配置（Profile）规范了工具子集的定义。任何H.264比特流必须遵从Profile规范，使用子集中部分或者全部工具实现编码。一个Profile兼容的解码器，必须能够解码使用子集中任何工具编码的H.264比特流。</p>
<p>当前最广泛使用的Profile是Main，其包含的工具很好的在压缩性能和计算复杂度之间进行权衡。Constrained Baseline这个Profile是Main的一个子集，在低复杂度、低延迟的应用程序中非常流行，例如移动视频电话。</p>
<div class="blog_h3"><span class="graybg">Main及其子集</span></div>
<p>Extended、Main、Baseline、Constrained Baseline之间的关系如下图：</p>
<p><img class="aligncenter  wp-image-16388" src="https://blog.gmem.cc/wp-content/uploads/2017/09/main-profiles.png" alt="main-profiles" width="851" height="764" /></p>
<p>说明如下：</p>
<ol>
<ol>
<ol>
<li>Baseline设计用于低复杂度、低延迟应用程序，例如移动视频电话。它支持：
<ol>
<li>I帧、P帧</li>
<li>允许帧内预测、基于单个参考帧的运动补偿</li>
<li>使用基本的4x4整数变换</li>
<li>使用CAVLC熵编码</li>
<li>支持FMO、ASO、冗余切片，这些技术用于提高传输效率</li>
</ol>
</li>
<li>上面提到的最后三个工具并不流行，很多实现不能完整支持。排除了这三者的Profile就是Constrained Baseline</li>
<li>Extended是Baseline的超集，添加了某些提高网络流传输效率的工具</li>
<li>Main是Constrained Baseline的超级，支持CABAC编码和B帧</li>
</ol>
</ol>
</ol>
<div class="blog_h3"><span class="graybg">High</span></div>
<p>高配置分为4个级别，添加了一些编码工具，满足高质量应用程序（高分辨率、扩展比特深度、高色彩深度）的需要。High是Main的超集，添加特性：</p>
<ol>
<ol>
<ol>
<li>8x8变换</li>
<li>8x8帧间预测，这提高了编码性能，特别是高空间分辨率情况下</li>
<li>支持频率相关的量化器权重</li>
<li>为Cr、Cb分开设置量化器参数</li>
<li>支持单色差视频（4:0:0格式）</li>
</ol>
</ol>
</ol>
<p>不同级别High和Main的关系如下图：</p>
<p><img class="aligncenter  wp-image-16389" src="https://blog.gmem.cc/wp-content/uploads/2017/09/high-profiles.png" alt="high-profiles" width="895" height="555" /></p>
<div class="blog_h2"><span class="graybg">Level</span></div>
<p>级别规定了帧尺寸、处理速度（每秒能够解码的帧或者块数量）、工作内存的最大需求量。Profile + Level共同构成了对解码器的约束条件。</p>
<p>照度分辨率、帧率和级别的关系如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">分辨率</td>
<td style="text-align: center;">最大帧率</td>
<td style="text-align: center;">级别</td>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">QCIF (176x144)</td>
<td>15</td>
<td>1, 1b</td>
</tr>
<tr>
<td>30</td>
<td>1.1</td>
</tr>
<tr>
<td rowspan="2">CIF (352x288)</td>
<td>15</td>
<td>1.2</td>
</tr>
<tr>
<td>30</td>
<td>1.3, 2</td>
</tr>
<tr>
<td>525 SD (720x480)</td>
<td>30</td>
<td>3</td>
</tr>
<tr>
<td>625 SD (720x576)</td>
<td>25</td>
<td>3</td>
</tr>
<tr>
<td>720p HD (1280x720)</td>
<td>30</td>
<td>3.1</td>
</tr>
<tr>
<td rowspan="2">1080p HD (1920x1080)</td>
<td>30</td>
<td>4, 4.1</td>
</tr>
<tr>
<td>60</td>
<td>4.2</td>
</tr>
<tr>
<td>4Kx2K (4096x2048)</td>
<td>30</td>
<td>5.1</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">H.264的传输</span></div>
<div class="blog_h2"><span class="graybg">传输支持工具</span></div>
<p>鉴于大部分H.264应用程序都牵涉到传输、存储比特流。H.264标准引入了一系列工具和特性，让传输过程更加高效和健壮。</p>
<p>需要注意：大部分商业编解码器没有支持这些特性。</p>
<div class="blog_h3"><span class="graybg">冗余切片</span></div>
<p>如果包含某个帧部分或者全部的重复信息，一个切片可以被标记为冗余的。解码器通常基于非冗余切片重构帧，如果非冗余切片损坏或者丢失，则使用冗余切片。</p>
<p>冗余切片增强了健壮性，代价是更高的比特率。</p>
<div class="blog_h3"><span class="graybg">任意切片顺序</span></div>
<p>任意切片顺序（Arbitrary Slice Order，ASO）允许帧中的切片以任意（非光栅序）的解码顺序排列。可以用于辅助解码错误的隐藏。</p>
<div class="blog_h3"><span class="graybg">切片组/灵活宏块排序</span></div>
<p>灵活宏块排序（Flexible Macroblock Ordering，FMO）让帧中的宏块被分配到一个或者多个切片组（Slice Groups）中。每个切片组包含1-N个切片。在切片组内部，宏块以光栅序编码，但是这些宏块在帧中的位置不一定相邻。宏块和切片组的对应关系由宏块分配映射（Macroblock Allocation Map）指定。</p>
<p>FMO可以增加容错性，因为每个切片组可以独立的解码。如果在使用交错排序的情况下，一个切片或者切片组丢失，其影响可以利用空间插值屏蔽掉。</p>
<div class="blog_h3"><span class="graybg">SP/SI切片</span></div>
<p>SP和SI切片的用途是：</p>
<ol>
<ol>
<ol>
<li>允许高效的在不同视频流之间切换</li>
<li>允许解码器进行高效的随机访问</li>
</ol>
</ol>
</ol>
<p>例如，同一视频源使用不同码率在网络中传输，解码器可以在正常情况下使用高码率，并且在网络拥塞的时候切换到低码率。</p>
<div class="blog_h3"><span class="graybg">数据分区切片</span></div>
<p>该特性将切片分为三个区：NAL头</p>
<ol>
<ol>
<ol>
<li>A分区：包含切片头、每个宏块的头</li>
<li>B分区：包含帧内预测的残余数据、SI切片宏块</li>
<li>C分区：包含帧间预测的残余数据、SP切片宏块</li>
</ol>
</ol>
</ol>
<p>每个分区都是独立的NAL单元。A、B、C分区的容错度依次增高，传输时可以应用不同的QoS。</p>
<div class="blog_h2"><span class="graybg">RBSP/NALU/Packet封装</span></div>
<p>这三者的逐层封装关系如下图：</p>
<p><img class="aligncenter size-full wp-image-16394" src="https://blog.gmem.cc/wp-content/uploads/2017/09/h264-encap.png" alt="h264-encap" width="868" height="672" /></p>
<p>说明：</p>
<ol>
<ol>
<ol>
<li>H.264语法元素被封装为RBSP，后者被封装为NALU。由于H.264语法元素是可变长度的，为了将RBSP字节对齐，尾部可能需要补零</li>
<li>RBSP封装进NALU的方式如下：
<ol>
<li>添加一字节的NALU头</li>
<li>按序添加模拟预防（Emulation Prevention）字节。为了防止起始码（Start Code）出现在NALU内部，每当出现和起始码前缀一致的3字节图式时，就插入一个模拟预防字节（二进制00000011）。解码器能够检测到模拟预防字节，进而知道其相邻的字节不是起始码</li>
</ol>
</li>
<li>NALU可以作为传输协议的载荷。每个NALU都前缀一个起始码，起始码为三字节的特殊序列。解码器依赖起始码来判断NALU的边界 </li>
</ol>
</ol>
</ol>
<div class="blog_h2"><span class="graybg">RTP传输</span></div>
<p>相关文章：<a href="/realtime-communication-protocols">实时通信协议族</a>。</p>
<p>H.264对传输协议没有任何规定，常用的传输协议是RTP。RTP是一种常见的打包协议，一般在UDP基础上运行。RTP Payload Formats为很多标准的音视频编码格式定义了标准。</p>
<p>本节主要讨论<a href="https://tools.ietf.org/html/rfc6184">RFC 6184</a>：RTP Payload Format for H.264 Video。</p>
<div class="blog_h3"><span class="graybg">RTP载荷结构</span></div>
<p>用于H.264传输时，RTP支持三种载荷结构。接收方可以根据载荷的首字节来识别载荷结构。这个直接也作为RTP载荷头，某些情况下还作为载荷的组成部分（第一字节）。</p>
<p>此首字节的格式和NALU头格式一致。其中NALU类型字段指明了载荷结构是哪一种：</p>
<ol>
<li>单NALU包：载荷中仅仅包含单个NALU，NALU类型取值范围在1-23之间</li>
<li>聚合包：载荷中包含多个NALU。这种载荷结构具有4种子类型：
<ol>
<li>STAP-A：单一时间聚合包，类型A。NALU类型为24</li>
<li>STAP-B：单一时间聚合包，类型B。NALU类型为25</li>
<li>MTAP16：多时间聚合包，使用16bit偏移。NALU类型为26</li>
<li>MTAP24：多时间聚合包，使用24bit偏移。NALU类型为27</li>
</ol>
</li>
<li>片断单元：其中仅仅包含NALU的一部分，这种方式允许NALU拆分到多个RTP包中传输。这种载荷结构具有2种子类型：
<ol>
<li>FU-A。NALU类型为28</li>
<li>FU-B。NALU类型为29</li>
</ol>
</li>
</ol>
<p>此首字节的NRI字段，00表示可丢弃，这个语义和H.264规范是一致的，解码器不关心任何非零NRI的具体值。RFC6184对非零值的含义进行了延伸，用于表示传输相对优先级。MANE可以对高优先级的包进行更强的保护，防止丢包，11是最高优先级。</p>
<div class="blog_h3"><span class="graybg">打包模式</span></div>
<p>对NALU的打包模式有三种：</p>
<ol>
<li>单NALU模式，用于遵从H.241规范的系统</li>
<li>非交错模式，用于可能不遵循H.241规定的系统。在此模式下，NALU的按解码序传输</li>
<li>交错模式，用于不需要非常低的端对端延迟低系统。此模式允许NALU不按解码序传输</li>
</ol>
<p>打包模式可以从SDP的packetization-mode字段获得：</p>
<ol>
<li>0或者没有该字段对应单NALU模式</li>
<li>1对应非交错模式</li>
<li>2对应交错模式</li>
</ol>
<p>打包模式和载荷结构类型的兼容性表格如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">载荷类型</td>
<td style="text-align: center;">单NALU模式</td>
<td style="text-align: center;">非交错模式</td>
<td style="text-align: center;">交错模式</td>
</tr>
</thead>
<tbody>
<tr>
<td>reserved</td>
<td>忽略</td>
<td>忽略</td>
<td>忽略</td>
</tr>
<tr>
<td>单NALU</td>
<td>是</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>STAP-A</td>
<td>否</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>STAP-B</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>MTAP16</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>MTAP24</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>FU-A</td>
<td>否</td>
<td>是</td>
<td>是</td>
</tr>
<tr>
<td>FU-B</td>
<td>否</td>
<td>否</td>
<td>是</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">单NALU包</span></div>
<p>此类型的载荷仅仅包含一个NALU，并且，<span style="background-color: #c0c0c0;">NALU头即为RTP载荷头</span>： </p>
<p><img class="aligncenter size-full wp-image-16408" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rpt-single-nalu.png" alt="rpt-single-nalu" width="767" height="256" /></p>
<p>NALU由NALU头、NALU载荷组成，<span style="background-color: #c0c0c0;">前缀的开始码（00 00 00 01或者00 00 01）被清除</span>后再打包。</p>
<div class="blog_h3"><span class="graybg">聚合包</span> </div>
<p>引入聚合包的原因是不同网络的MTU不同：</p>
<ol>
<li>有线IP网络的MTU主要受限于以太网MTU，大概1500字节左右</li>
<li>IP/非IP无线网络的MTU首先的传输单元较小，大概254字节或者更少 </li>
</ol>
<p>由于RTP通常基于UDP，UDP包的尺寸则受限于MTU。因此聚合包能够最高效率的使用MTU。</p>
<p>聚合包分为两大类，分别为单时间（STAP）、多时间（MTAP）聚合包。前者的NALU时间只有一个值。后者包含低NALU可能对应不同的时间。聚合包中的每个NALU都基于聚合单元打包：</p>
<p><img class="aligncenter size-full wp-image-16409" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-aggr-pkg.png" alt="rtp-aggr-pkg" width="766" height="252" /></p>
<p>STAP和MTAP共享以下打包规则：</p>
<ol>
<li>RTP时间戳必须设置为包内所有NALU的最早的那个NALU-time</li>
<li>NALU类型必须正确设置</li>
<li>如果所有NALU的F位均为0，F位必须清零</li>
<li>NRI必须设置为所有NALU中NRI的最大值</li>
</ol>
<p>STAP-A载荷结构如下：</p>
<p><img class="aligncenter size-full wp-image-16412" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-stap.png" alt="rtp-stap" width="762" height="250" />STAP-B载荷结构如下：</p>
<p><img class="aligncenter size-full wp-image-16414" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-mtap.png" alt="rtp-mtap" width="766" height="253" />两者的主要区别是STAP-B有一个DON字段，它以传输序指定了第一个NALU的位置，后续NALU的DON = (第一个NALU的DON + 1) %65536。</p>
<p>STAP-A包含1-N个聚合单元，聚合单元的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16415" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-stau.png" alt="rtp-stau" width="767" height="253" /></p>
<p>STAP-A、STAP-B的聚合单元的头部是16bit的网络序无符号整数，指示后续NALU的长度（字节）。聚合单元在RTP包内是字节对齐的。</p>
<p>MTAP载荷由一个16bit网络序无符号整数的解码序号基数（Decoding Order Number Base）和1-N个聚合单元组成。DONB的值为当前包中以NALU解码序计第一个NALU的DON值。</p>
<p>MTAP的聚合单元MTAP16的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16417" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-mtap16.png" alt="rtp-mtap16" width="768" height="253" /></p>
<p>MTAP24的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16418" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-mtap24.png" alt="rtp-mtap24" width="779" height="268" /></p>
<p>两种聚合单元都有如下字段：</p>
<ol>
<li>16bit的NALU大小</li>
<li>8bit的解码序号差异（Decoding Order Number Difference，DOND）</li>
<li>Nbit的时间戳偏移，对于MTAP16 N = 16，对于MTAP24 N = 24</li>
</ol>
<div class="blog_h3"><span class="graybg">分段包</span></div>
<p>当NALU长度超过MTU后，可以使用分段包，让一个NALU分散在多个RTP包中。FU有两个子类，其中FU-A的结构如下：</p>
<p><img class="aligncenter size-full wp-image-16420" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-fu.png" alt="rtp-fu" width="765" height="250" /></p>
<p>FU-B必须配合交错打包模式使用。其结构如下：</p>
<p><img class="aligncenter size-full wp-image-16421" src="https://blog.gmem.cc/wp-content/uploads/2017/09/rtp-fu-b.png" alt="rtp-fu-b" width="770" height="251" /></p>
<p>FU indicator结构与NALU头一致，NALU类型取值28或29。</p>
<p>FU header结构如下：</p>
<p style="padding-left: 60px;"><span class="monospace">+---------------+</span><br /><span class="monospace">|0|1|2|3|4|5|6|7|</span><br /><span class="monospace">+-+-+-+-+-+-+-+-+</span><br /><span class="monospace">|S|E|R| Type    |</span><br /><span class="monospace">+---------------+</span></p>
<p>其中：</p>
<ol>
<li>S：1bit，如果设置此位，表示此FU是第一个NALU分片</li>
<li>E：1bit，如果设置此位，表示此FU是最后一个NALU分片</li>
<li>R：1bit，取值0</li>
<li>Type：NALU载荷类型</li>
</ol>
<div class="blog_h1"><span class="graybg">X264</span></div>
<p>x264是VideoLAN开源的H.264编码器，特性包括：</p>
<ol>
<li>8x8和4x4自适应空域变换</li>
<li>自适应B帧置入</li>
<li>B帧作为参考帧</li>
<li>任意帧顺序</li>
<li>支持CAVLC、CABAC熵编码</li>
<li>自定义量化矩阵</li>
<li>帧内预测：16x16, 8x8, 4x4等任意宏块大小</li>
<li>帧间预测：所有分区大小，从16x16到4x4</li>
<li>帧间双向预测：分区大小支持从16x16到8x8</li>
<li>交错扫描—— 宏块级帧场自适应（Macro-block Adaptive Field Frame，MBAFF）</li>
<li>多参考帧</li>
<li>速率控制：常量量化器、常量质量、单步/多步ABR、可选VBV</li>
<li>场景切换（Scenecut）检测</li>
<li>B帧中的空域/时域直接模式，自适应模式选择</li>
<li>使用多个CPU并行编码</li>
<li>预测性无损模式</li>
</ol>
<div class="blog_h2"><span class="graybg">构建</span></div>
<pre class="crayon-plain-tag">git clone http://git.videolan.org/git/x264.git
cd x264
./configure --enable-debug --enable-static --enable-shared --prefix=/home/alex/CPP/lib/x264
make &amp;&amp; make install &amp;&amp; make clean</pre>
<div class="blog_h2"><span class="graybg">HelloWorld</span></div>
<p>下面是一个编码QCIF尺寸的YUV序列的示例。CMake项目配置：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(x264 C)

set(X264_HOME /home/alex/CPP/lib/x264)
include_directories(${X264_HOME}/include)

set(SRC_ENCODER encoder.c)
add_executable(encoder ${SRC_ENCODER})
target_link_libraries(
        encoder
        ${X264_HOME}/lib/libx264.so
)</pre>
<p> 编码器源码：</p>
<pre class="crayon-plain-tag">#include &lt;stdint.h&gt;
#include &lt;stdio.h&gt;
#include &lt;x264.h&gt;

int main( int argc, char **argv ) {
    int width = 176, height = 144;    //QCIF
    x264_param_t param;
    x264_picture_t pic;
    x264_picture_t pic_out;
    x264_t *h;
    int i_frame = 0;
    int i_frame_size;
    x264_nal_t *nal;
    int i_nal;

    /* 应用编码器预设 */
    if ( x264_param_default_preset( &amp;param, "fast", "zerolatency" ) &lt; 0 ) goto fail;

    /* 在预设的基础上定制 */
    param.i_csp = X264_CSP_I420;      // 色彩空间：yuv 4:2:0 planar（三个数组）
    param.i_width = width;            // 帧尺寸
    param.i_height = height;
    param.b_vfr_input = 0;            // 可变帧率的输入：否
    param.b_repeat_headers = 1;       // 在每个I帧之前插入SPS/PPS
    param.b_annexb = 1;               // 在NALU之前插入4字节起始码

    /* 设置Profile，x264仅仅提供了Baseline选项，但是由于不支持某些特性，编码实际使用的是Constrained Baseline */
    if ( x264_param_apply_profile( &amp;param, "baseline" ) &lt; 0 ) goto fail;
    if ( x264_picture_alloc( &amp;pic, param.i_csp, param.i_width, param.i_height ) &lt; 0 ) goto fail;
#undef fail
#define fail fail2
    // 创建句柄
    h = x264_encoder_open( &amp;param );
    if ( !h ) goto fail;
#undef fail
#define fail fail3

    int luma_size = width * height;    // 单帧包含的明度元素个数
    int chroma_size = luma_size / 4;   // 单帧包括的色差元素个数
    /* 循环编码所有帧  */
    FILE *in = fopen( "/home/alex/CPP/projects/clion/x264/qcif.yuv", "r" );
    FILE *out = fopen( "/home/alex/CPP/projects/clion/x264/qcif.h264", "w" );
    for ( ;; i_frame++ ) {
        /* 读取输入帧 */
        if ( fread( pic.img.plane[ 0 ], 1, luma_size, in ) != luma_size )
            break;
        if ( fread( pic.img.plane[ 1 ], 1, chroma_size, in ) != chroma_size )
            break;
        if ( fread( pic.img.plane[ 2 ], 1, chroma_size, in ) != chroma_size )
            break;

        pic.i_pts = i_frame;  // PTS，展现时间戳
        // 编码当前帧，nal是编码后的第一个NALU的指针，i_nal是NALU个数。返回编码后NALU的总字节数
        i_frame_size = x264_encoder_encode( h, &amp;nal, &amp;i_nal, &amp;pic, &amp;pic_out );
        if ( i_frame_size &lt; 0 ) goto fail;
            // 将NALU的载荷写入到输出流
        else if ( !fwrite( nal-&gt;p_payload, i_frame_size, 1, out ))goto fail;
    }
    /* 写出延迟帧 */
    while ( x264_encoder_delayed_frames( h )) {
        i_frame_size = x264_encoder_encode( h, &amp;nal, &amp;i_nal, NULL, &amp;pic_out );
        if ( i_frame_size &lt; 0 ) goto fail;
        else if ( i_frame_size ) if ( !fwrite( nal-&gt;p_payload, i_frame_size, 1, out )) goto fail;
    }
    // 关闭编码器
    x264_encoder_close( h );
    // 清理内存
    x264_picture_clean( &amp;pic );
    return 0;

#undef fail
    fail3:
    x264_encoder_close( h );
    fail2:
    x264_picture_clean( &amp;pic );
    fail:
    return -1;
}</pre>
<p>编码后原始的5.7MB的YUV序列产生了88.3KB的H.264 NALU序列，缩小了接近80倍。 以HEX打开输出文件：</p>
<p style="padding-left: 60px;"><span class="monospace">0000 0001 6742 c00b db0b 13a1 0000 0300</span><br /><span class="monospace">0100 0003 0032 8f14 2ae0 0000 0001 68ca</span><br /><span class="monospace">83cb 2000 0001 0605 ffff 66dc 45e9 bde6</span><br /><span class="monospace">d948 b796 2cd8 20d9 23ee ef78 3236 3420 ...</span></p>
<p>可以看到：</p>
<ol>
<li>NALU前缀了4字节的起始码HEX：0000 0001</li>
<li>第1个NALU头为BIN：01100111，此单元是一个SPS</li>
<li>第2个NALU是PPS</li>
<li>第3个NALU是一个典型切片</li>
</ol>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/h264-study-note">H.264学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/h264-study-note/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>基于Kurento搭建WebRTC服务器</title>
		<link>https://blog.gmem.cc/webrtc-server-basedon-kurento</link>
		<comments>https://blog.gmem.cc/webrtc-server-basedon-kurento#comments</comments>
		<pubDate>Thu, 31 Aug 2017 09:08:42 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Work]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[视频监控]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15701</guid>
		<description><![CDATA[<p>基础 Kurento是一个WebRTC媒体服务器，同时提供了一系列的客户端API，可以简化供浏览器、移动平台使用的视频类应用程序的开发。Kurento支持： 群组通信（group communications） 媒体流的转码（transcoding）、录制（recording）、广播（broadcasting）、路由（routing） 高级媒体处理特性，包括：机器视觉（CV）、视频索引、增强现实（AR）、语音分析 Kurento的模块化架构使其与第三方媒体处理算法 —— 语音识别、人脸识别 —— 很容易集成。 架构 和大部分多媒体通信技术一样，Kurento应用的整体架构包含两个层（layer）或者叫平面（plane）： 信号平面（Signaling Plane）：负责通信的管理，例如媒体协商、QoS、呼叫建立、身份验证等 媒体平面（Media Plane）：负责媒体传输、编解码等 典型Kurento应用的整体架构如下图： 分层视角 按分层的方式来划分，Kurento应用可以分为三层（类似于典型的Web应用）： 展现层 —— <a class="read-more" href="https://blog.gmem.cc/webrtc-server-basedon-kurento">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/webrtc-server-basedon-kurento">基于Kurento搭建WebRTC服务器</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">基础</span></div>
<p>Kurento是一个WebRTC媒体服务器，同时提供了一系列的客户端API，可以简化供浏览器、移动平台使用的视频类应用程序的开发。Kurento支持：</p>
<ol>
<li>群组通信（group communications）</li>
<li>媒体流的转码（transcoding）、录制（recording）、广播（broadcasting）、路由（routing）</li>
<li>高级媒体处理特性，包括：机器视觉（CV）、视频索引、增强现实（AR）、语音分析</li>
</ol>
<p>Kurento的模块化架构使其与第三方媒体处理算法 —— 语音识别、人脸识别 —— 很容易集成。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>和大部分多媒体通信技术一样，Kurento应用的整体架构包含两个层（layer）或者叫平面（plane）：</p>
<ol>
<li>信号平面（Signaling Plane）：负责通信的管理，例如媒体协商、QoS、呼叫建立、身份验证等</li>
<li>媒体平面（Media Plane）：负责媒体传输、编解码等</li>
</ol>
<p>典型Kurento应用的整体架构如下图：</p>
<p><img class="aligncenter size-full wp-image-15744" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurentoapp-architecture.png" alt="kurentoapp-architecture" width="670" height="495" /></p>
<div class="blog_h3"><span class="graybg">分层视角</span></div>
<p>按分层的方式来划分，Kurento应用可以分为三层（类似于典型的Web应用）：</p>
<ol>
<li>展现层 —— 浏览器、移动应用、其它媒体源等应用客户端：
<ol>
<li> 基于任意协议和应用逻辑层通信，发起信号处理</li>
<li> 基于RTP/HTTP/WebRTC协议和KMS通信：
<ol>
<li>通过KMS的输入端点，传输媒体流到KMS</li>
<li>通过KMS的输出端点，从KMS获得媒体流</li>
</ol>
</li>
</ol>
</li>
<li>应用逻辑层——应用服务器负责信号平面：
<ol>
<li>基于WebSocket/HTTP/REST/SIP等方式和应用客户端通信，进行信号处理</li>
<li>内嵌Kurento Client，基于Kurento Protocol与KMS通信，管理媒体元素/媒体管线</li>
</ol>
</li>
<li>服务层——KMS负责媒体平面，可以对输入流进行各种处理，并产生输出流</li>
</ol>
<div class="blog_h3"><span class="graybg">层之间的交互</span></div>
<p>媒体协商（信号处理）阶段：</p>
<ol>
<li>客户端首先向应服务器请求某种媒体特性（例如请求一个九画面视频监控流、请求发布自己的SDP）。这块WebRTC没有规定，可以基于任何协议（HTTP/WS/SIP）实现</li>
<li>应用服务器接收到请求后，执行特定的服务器端逻辑，例如AAA（认证授权审计）、<span style="color: #404040;">CDR生成等</span></li>
<li>应用服务器处理请求，并命令KMS实例化适当的媒体元素、构建媒体流（例如从多个RTSP源混合出九画面）</li>
<li>媒体流构建完毕后，KMS应答应用服务器，后者应答客户端，告知其如何获取媒体服务</li>
</ol>
<p>媒体交换阶段：</p>
<ol>
<li>客户端利用协商阶段收集的信息，向KMS发起请求（例如向目标端口发起UDP请求，获取九画面视频监控流）</li>
</ol>
<p>下图是交互的序列示意，注意先后顺序：</p>
<p><img class="aligncenter size-full wp-image-15749" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurentoapp-generic_interactions.png" alt="kurentoapp-generic_interactions" width="670" height="439" /></p>
<div class="blog_h3"><span class="graybg">WebRTC应用的例子</span></div>
<p style="text-align: left;">Kurento允许基于WebRTC建立浏览器和KMS之间的实时多媒体会话：</p>
<ol>
<li>客户端基于SDP来发布自己的媒体特性，请求发送给应用服务器</li>
<li>应用服务器根据SDP来创建合适的WebRTC端点，并请求KMS生成一个响应SDP</li>
<li>应用服务器获得响应SDP后，将其返回给客户端</li>
<li>由于双方都知道对方的SDP了，客户端和KMS可以进行媒体交换了</li>
</ol>
<p>下图是交互的序列示意：</p>
<p style="text-align: left;"><img class="aligncenter size-full wp-image-15761" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurento-webrtc-session.png" alt="kurento-webrtc-session" width="670" height="439" /></p>
<p style="text-align: left;">Kurento也可以作为一个媒体代理，让浏览器之间建立直接的媒体交换。交互序列仍然如上图，仅仅是KMS返回的SDP不同</p>
<div class="blog_h2"><span class="graybg">媒体服务器</span></div>
<p>WebRTC让浏览器能够进行实时的点对点通信（在没有服务器的情况下）。但是要想实现群组通信、媒体流录制、媒体广播、转码等高级特性，没有媒体服务器是很难实现的。</p>
<p>Kurento的核心是一个媒体服务器（Kurento Media Server，KMS），负责媒体的传输、处理、加载、录制，主要基于 GStreamer实现。此媒体服务器的特性包括：</p>
<ol>
<li>网络流协议处理，包括HTTP、RTP、WebRTC</li>
<li>支持媒体混合（mixing）、路由和分发的群组通信（MCU、SFU功能）</li>
<li>对机器视觉和增强现实过滤器的一般性支持</li>
<li>媒体存储支持，支持对WebM、MP4进行录像操作，可以播放任何GStreamer支持的视频格式</li>
<li>对于GStreamer支持的编码格式，可以进行任意的转码，例如VP8, H.264, H.263, AMR, OPUS, Speex, G.711</li>
</ol>
<div class="blog_h2"><span class="graybg">模块</span></div>
<p>KMS基于模块化的设计，模块主要分为三类：</p>
<ol>
<li>核心（kms-core）</li>
<li>媒体元素（kms-elements）</li>
<li>过滤器（kms-filters）</li>
<li>其它增强KMS的模块，例如kms-crowddetector, kms-pointerdetector, kms-chroma, kms-platedetector</li>
</ol>
<p>KMS允许用户扩展自己的模块。</p>
<div class="blog_h2"><span class="graybg">协议</span></div>
<p>Kurento Protocol是一个网络协议，通过WebSocket暴露KMS的特性。</p>
<p>Kurento API是对上述协议的OO封装，通过此API能够创建媒体元素和管线。Kurento提供了API的Java、JavaScript绑定。</p>
<div class="blog_h2"><span class="graybg">客户端</span></div>
<p>Kurento提供了Java、JavaScript（包括浏览器和Node.js）的客户端库，通过这些库你可以控制媒体服务器。对于其它编程语言，可以使用 Kurento Protocol协议（基于WebSocket/JSON-RPC）。</p>
<p>Kurento客户端API基于所谓媒体元素（Media Element）的概念。一个每天元素持有一种特定的媒体特性。例如：</p>
<ol>
<li>媒体元素WebRtcEndpoint的特性是，接收WebRTC媒体流</li>
<li>媒体元素RecorderEndpoint的特性是，将接收到的媒体流录制到文件系统</li>
<li>媒体元素FaceOverlayFilter则能够检测人脸，在其上方显示一个特定的图像</li>
</ol>
<p>开箱即用的媒体元素如下图：</p>
<p><img class="aligncenter size-full wp-image-15714" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurento-basic-toolbox.png" alt="kurento-basic-toolbox" width="500" height="324" /></p>
<p>从开发者角度来说，操控媒体元素就好像搭积木。 你只需要按照期望的拓扑结构把它们连接起来就可以了。一系列连接起来的媒体元素称为媒体管线（Media Pipeline）。只有一个管线内部的媒体元素才能相互通信</p>
<p>当创建管道时，开发者需要明确希望使用到的特性，以及媒体连接（connectivity） —— 产生媒体的元素和消费媒体的元素之间的连接：</p>
<pre class="crayon-plain-tag">sourceMediaElement.connect(sinkMediaElement);
// 例如：客户端接收WebRTC流并录制到媒体服务器的文件系统
webRtcEndpoint.connect(recorderEndpoint);</pre>
<div class="blog_h3"><span class="graybg">Web客户端</span></div>
<p>为了简化浏览器客户端的WebRTC流处理，Kurento提供了工具WebRtcPeer，你仍然可以使用WebRTC的标准API，以及连接到WebRtcEndpoint。</p>
<div class="blog_h1"><span class="graybg">安装配置</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>你可以在64位Ubuntu 14.04 LTS上安装KMS：</p>
<pre class="crayon-plain-tag">docker create -it -h kurento --name kurento --network local --dns 172.21.0.1 --ip 172.21.0.6 docker.gmem.cc/ubuntu:14.04 bash

# 在上述容器中执行
echo "deb http://ubuntu.kurento.org trusty kms6" | sudo tee /etc/apt/sources.list.d/kurento.list
wget -O - http://ubuntu.kurento.org/kurento.gpg.key | sudo apt-key add -
sudo apt-get update
# 执行下面的命令安装KMS，可能需要手工选择依赖冲突处理方案
aptitude install kurento-media-server-6.0
# 选择降级gcc-4.8-base、libstdc++6的那个方案</pre>
<p>要启动或者停止KMS服务，执行下面的命令：</p>
<pre class="crayon-plain-tag"># 启动服务
sudo service kurento-media-server-6.0 start
# 停止服务
sudo service kurento-media-server-6.0 stop</pre>
<div class="blog_h3"><span class="graybg">兼容性</span></div>
<p>Trickle ICE是对ICE的扩展，它允许ICE代理（KMS、客户端）增量的收发candidates而不是交换完整的candidate列表。</p>
<p>由于使用了Trickle ICE协议， 目前的6.0版本的KMS和5.1-版本不兼容，你需要卸载老版本后重新安装：</p>
<pre class="crayon-plain-tag">sudo apt-get remove kurento-media-server
sudo apt-get purge kurento-media-server
sudo apt-get autoremove</pre>
<p>注意sources.list文件和sources.list.d下的文件中，对kurento的引用也要删除。</p>
<div class="blog_h3"><span class="graybg">容器化</span></div>
<p>在Ubuntu:14.04容器中安装后，关闭容器，提交为镜像：</p>
<pre class="crayon-plain-tag">docker commit kurento docker.gmem.cc/kurento:base</pre>
<p>新建一个Docker项目：</p>
<pre class="crayon-plain-tag">FROM docker.gmem.cc/kurento:base

ADD /fs /
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"] </pre>
<p><a id="entrypoint"></a>入口点脚本：</p>
<pre class="crayon-plain-tag">#!/usr/bin/env bash

sighdl() {
    echo
    echo -e "\033[44mKilling sub process $pid \033[0m"
    kill -TERM $pid
    echo -e "\033[44mStopping KMS \033[0m"
    service kurento-media-server-6.0 stop
    echo -e "\033[44mCleaning up log files \033[0m"
    rm -rf /var/log/kurento-media-server/*
}
trap  sighdl HUP INT PIPE QUIT TERM

service kurento-media-server-6.0 start

sleep 10

kmspid=`ps -A | grep kurento-media | xargs |cut -d" " -f1`
pushd /var/log/kurento-media-server &gt; /dev/null
logfile=`find . -name "*pid$kmspid.log" | head -n 1`

# 持续输出当前日志的内容，确保容器不退出
tail -f $logfile &amp;
pid=$!
# 捕获到信号的时候，下面的命令退出 —— 等待被中断
wait $pid

# 信号处理完毕后，执行下面的命令，如果tail这个子进程已经终止，则wait会立即退出
# 如果子进程正在处理TERM信号，则等待其处理完毕后，wait退出
# 如果没有这个double wait，则子进程有可能成为僵尸，因为没有父进程实际完成wait系统调用
wait $pid</pre>
<p>构建新镜像：<pre class="crayon-plain-tag">docker build --force-rm -t docker.gmem.cc/kurento .</pre> </p>
<p>创建基于新镜像容器： </p>
<pre class="crayon-plain-tag">docker create --name kurento -h kurento --dns 172.21.0.1 --network local --ip 172.21.0.6  --expose 8888 \
              -p 8888:8888 docker.gmem.cc/kurento</pre>
<p>启动容器：</p>
<pre class="crayon-plain-tag">docker start -i kurento</pre>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>要自己构建Kurento，可以参考本节的操作步骤。本节记录的操作步骤是在Ubuntu 14.04 TLS上执行的。</p>
<div class="blog_h3"><span class="graybg">构建OpenCV</span></div>
<p>kms-filters依赖于此库：</p>
<pre class="crayon-plain-tag">pushd /home/alex/CPP/lib  &gt; /dev/null
mkdir opencv
pushd opencv &gt; /dev/null
wget https://codeload.github.com/opencv/opencv/zip/2.4.13.3 -O 2.4.13.zip
unzip -o -d . 2.4.13.zip 
mv opencv-2.4.13.3 2.4.13
mkdir build
pushd build &gt; /dev/null
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/opencv/2.4.13 ..
make &amp;&amp; make install
popd &amp;&amp; popd</pre>
<div class="blog_h3"><span class="graybg">构建kurento组件</span></div>
<pre class="crayon-plain-tag"># Kurento的组件和源码安装在此
export KURENTO_HOME=/home/alex/CPP/lib/kurento
# 构建Kurento组件的通用CMake选项
export CMAKE_OPTS="-DCMAKE_INSTALL_PREFIX:STRING=$KURENTO_HOME -DCMAKE_MODULE_PATH:STRING=$KURENTO_HOME/share/cmake-2.8/Modules"
export BOOST_ROOT=/home/alex/CPP/lib/boost/1.65.1

pushd /home/alex/CPP/lib/kurento/src &gt; /dev/null


# 构建kms-cmake-utils
git clone https://github.com/Kurento/kms-cmake-utils.git
pushd kms-cmake-utils  &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
cmake $CMAKE_OPTS ..
make &amp;&amp; make install
popd &amp;&amp; popd


# 构建kurento-module-creator
git clone https://github.com/Kurento/kurento-module-creator.git
pushd kurento-module-creator &gt; /dev/null
mvn package
# CMake模块统一存放处
cp target/classes/FindKurentoModuleCreator.cmake $KURENTO_HOME/share/cmake-2.8/Modules/
mkdir $KURENTO_HOME/kurento-module-creator
cp target/kurento-module-creator-jar-with-dependencies.jar $KURENTO_HOME/kurento-module-creator/
cp scripts/kurento-module-creator $KURENTO_HOME/kurento-module-creator/
export PATH=$PATH:$KURENTO_HOME/kurento-module-creator
popd


# 构建kms-jsonrpc
git clone https://github.com/Kurento/jsoncpp.git
pushd jsoncpp  &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
# 需要修改$KURENTO_HOME/src/jsoncpp/src/lib_json/CMakeLists.txt
# 添加目标属性 SET_TARGET_PROPERTIES(jsoncpp_lib_static PROPERTIES COMPILE_FLAGS "-fPIC")
# 否则kms-jsonrpc的构建会报错 ...can not be used when making a shared object; recompile with -fPIC
cmake $CMAKE_OPTS ..
make &amp;&amp; make install
popd &amp;&amp; popd

git clone https://github.com/Kurento/kms-jsonrpc.git
pushd kms-jsonrpc &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
# 下一步会报错 package 'kmsjsoncpp&gt;=0.6.0' not found  
# 实际上我们刚刚构建好kmsjsoncpp，其Package config位于$KURENTO_HOME/lib/pkgconfig目录下
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$KURENTO_HOME/lib/pkgconfig
cmake $CMAKE_OPTS ..
export LIBRARY_PATH=$LIBRARY_PATH:$KURENTO_HOME/lib
# 报错 fatal error: json/json.h: No such file or directory
# 经过检查，使用的jsoncpp头文件路径是/home/alex/CPP/lib/kurento/include/kmsjsoncpp
# 而实际路径是/home/alex/CPP/lib/kurento/include
# 这是jsoncpp项目的pkgconfig模板错误导致，
# 手工修改$KURENTO_HOME/lib/pkgconfig/kmsjsoncpp.pc最后一行为Cflags: -I${includedir}
make &amp;&amp; make install
popd &amp;&amp; popd


# 构建kms-core
# 后续可能需要调试
export CMAKE_OPTS="$CMAKE_OPTS -DCMAKE_BUILD_TYPE:STRING=Debug"
sudo apt install libvpx-dev
# Kurento使用自己打包的gstreamer
echo "deb http://ubuntu.kurento.org trusty kms6" | sudo tee /etc/apt/sources.list.d/kurento.list
wget -O - http://ubuntu.kurento.org/kurento.gpg.key | sudo apt-key add -
sudo apt-get update
sudo apt install libgstreamer1.5-dev libgstreamer-plugins-base1.5-dev

git clone https://github.com/Kurento/kms-core.git
pushd kms-core &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
cmake $CMAKE_OPTS ..
# 构建时又找不到BOOST头文件
export CPATH=$CPATH:/home/alex/CPP/lib/boost/1.65.1/include
make &amp;&amp; make install
popd &amp;&amp; popd


# 构建kms-elements
export CMAKE_OPTS="$CMAKE_OPTS -DKURENTO_MODULES_DIR:STRING=$KURENTO_HOME/share/kurento/modules"
sudo apt-get install libusrsctp*
git clone https://github.com/Kurento/openwebrtc-gst-plugins.git
pushd openwebrtc-gst-plugins &gt; /dev/null
./autogen.sh
./configure --prefix=$KURENTO_HOME
make &amp;&amp; make install
popd

sudo apt-get install libnice-dev

git clone https://github.com/Kurento/kms-elements.git
pushd kms-elements &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
cmake $CMAKE_OPTS ..
export CPATH=$CPATH:$KURENTO_HOME/include/gstreamer-1.5
make &amp;&amp; make install
popd &amp;&amp; popd


# 构建kms-filters
git clone https://github.com/Kurento/kms-filters.git
pushd kms-filters &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/home/alex/CPP/lib/opencv/2.4.13/lib/pkgconfig
export LIBRARY_PATH=$LIBRARY_PATH:/home/alex/CPP/lib/opencv/2.4.13/lib
export CPATH=$CPATH:/home/alex/CPP/lib/opencv/2.4.13/include
# 修改CMake配置/home/alex/CPP/lib/kurento/src/kms-filters/CMakeLists.txt
# 第29-30行，去掉 -Wall -Werror 
cmake $CMAKE_OPTS ..
make &amp;&amp; make install
popd &amp;&amp; popd


# 构建 kurento-media-server
git clone https://github.com/Kurento/kurento-media-server.git
pushd kurento-media-server &gt; /dev/null
mkdir build
pushd build &gt; /dev/null
cmake $CMAKE_OPTS ..
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/alex/CPP/lib/kurento/lib/x86_64-linux-gnu
make &amp;&amp; make install
popd &amp;&amp; popd</pre>
<p>运行，要运行自己构建的KMS，参考如下脚本：</p>
<pre class="crayon-plain-tag">#!/bin/bash
 cd /home/alex/CPP/lib/kurento/bin
 export CLIB_HOME=/home/alex/CPP/lib
 export KURENTO_HOME=$CLIB_HOME/kurento
 export LD_LIBRARY_PATH=$CLIB_HOME/boost/1.65.1/lib:$KURENTO_HOME/lib:$KURENTO_HOME/lib/x86_64-linux-gnu
./kurento-media-server -f $KURENTO_HOME/etc/kurento/kurento.conf.json -c $KURENTO_HOME/etc/kurento/modules/kurento</pre>
<div class="blog_h2"><span class="graybg">配置</span></div>
<div class="blog_h3"><span class="graybg">配置文件</span></div>
<p>KMS的主配置文件位于/etc/kurento/kurento.conf.json，内容如下：</p>
<pre class="crayon-plain-tag">{
  "mediaServer" : {
    "resources": {
        // 当请求创建一个对象时，如果资源用量达到下面的阈值，抛出异常
        "exceptionLimit": "0.8",
        // 如果没有任何存活对象，且资源用量达到下面的阈值，则重启服务器
        "killLimit": "0.7",
        // 垃圾回收器活动间隔（秒）
        "garbageCollectorPeriod": 240
    },
    "net" : {
      // WS用于Kurento Protocol
      "websocket": {
         // 普通WS端口
         "port": 8888,
         // WSS端口、数字证书信息
         "secure": {
            "port": 8433,
            "certificate": "defaultCertificate.pem",
            "password": ""
          },
          "registrar": {
            "address": "ws://localhost:9090",
            "localAddress": "localhost"
          },
        // URL路径
        "path": "kurento",
        "threads": 10
      }
    }
  }
}</pre>
<p>此外还有以下配置文件：</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>媒体元素的一般性参数：/etc/kurento/modules/kurento/MediaElement.conf.json</td>
</tr>
<tr>
<td>
<p>SDP端点（WebRtcEndpoint、RtpEndpoint）的音视频参数</p>
<p>/etc/kurento/modules/kurento/SdpEndpoint.conf.json</p>
</td>
</tr>
<tr>
<td>WebRtcEndpoint专有参数：/etc/kurento/modules/kurento/WebRtcEndpoint.conf.json</td>
</tr>
<tr>
<td>HttpEndpoint专有参数：/etc/kurento/modules/kurento/HttpEndpoint.conf.json</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">STUN支持</span></div>
<p>如果KMS位于NAT设备后面，你需要使用STUN或者TURN以便实现NAT穿透。大部分情况下STUN足够，在对称NAT的情况下才需要使用TURN。</p>
<p>要启用STUN支持，修改配置文件： </p>
<pre class="crayon-plain-tag">; 解除注释：
stunServerAddress=&lt;serverAddress&gt;
stunServerPort=&lt;serverPort&gt;
; 公网有很多免费的STUN服务：
; 173.194.66.127:19302
; 173.194.71.127:19302
; 74.125.200.127:19302
; 74.125.204.127:19302
; 173.194.72.127:19302
; 74.125.23.127:3478
; 77.72.174.163:3478
; 77.72.174.165:3478
; 77.72.174.167:3478
; 77.72.174.161:3478
; 208.97.25.20:3478
; 62.71.2.168:3478
; 212.227.67.194:3478
; 212.227.67.195:3478
; 107.23.150.92:3478
; 77.72.169.155:3478
; 77.72.169.156:3478
; 77.72.169.164:3478
; 77.72.169.166:3478
; 77.72.174.162:3478
; 77.72.174.164:3478
; 77.72.174.166:3478
; 77.72.174.160:3478
; 54.172.47.69:3478</pre>
<div class="blog_h3"><span class="graybg">TURN支持</span></div>
<p>要启用TURN支持，解除注释：</p>
<pre class="crayon-plain-tag">turnURL=user:password@address:port</pre>
<p>一个开源的TURN实现是<a href="https://github.com/coturn/coturn">coturn</a></p>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p>KMS的日志默认存放在/var/log/kurento-media-server/目录下：</p>
<ol>
<li>media-server_&lt;timestamp&gt;.&lt;log_number&gt;.&lt;kms_pid&gt;.log为本次运行的KMS日志</li>
<li>media-server_error.log为第三方错误日志</li>
<li>logs子目录存放历史日志</li>
</ol>
<div class="blog_h1"><span class="graybg"><span class="graybg">Kurento API</span></span></div>
<p>Kurento提供了Java/JavaScript的API，对于其它编程语言，目前需要通过WebSocket/JSON-RPC使用Kurento Protocol。 </p>
<p>本章仅仅进行概念上的阐述，如果需要了解针对具体语言的API，请参阅官方文档：</p>
<ol>
<li><a href="http://doc-kurento.readthedocs.io/en/stable/_static/langdoc/javadoc/index.html">kurento-client-java</a>：Java客户端</li>
<li><a href="http://doc-kurento.readthedocs.io/en/stable/_static/langdoc/jsdoc/kurento-client-js/index.html">kurento-client-js</a>：JavaScript客户端</li>
<li><a href="http://doc-kurento.readthedocs.io/en/stable/_static/langdoc/jsdoc/kurento-utils-js/index.html">kurento-utils-js</a>：用于简化WebRTC应用开发的JavaScript工具</li>
</ol>
<div class="blog_h2"><span class="graybg">整体结构</span></div>
<p>Kurento的主要类型的类图如下，可以看到MediaObject是所有类型的根，并且实现了组合模式：</p>
<p><img class="aligncenter size-full wp-image-15773" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurento-classes.png" alt="kurento-classes" width="521" height="285" /></p>
<div class="blog_h2"><span class="graybg">媒体元素/管线</span></div>
<p>媒体元素和媒体管线是最核心的API。</p>
<div class="blog_h3"><span class="graybg">媒体元素</span></div>
<p>MediaElement是媒体流中，执行特定动作的功能单元。它让媒体特性对于应用开发者表现为自包含的黑盒，这些开发者不需要了解底层细节。</p>
<p>MediaElement可以通过mediaSrcs从其它媒体元素接收媒体，或者通过mediaSinks将媒体发送给其它媒体元素。</p>
<p>根据功能的不同，MediaElement可以分为：</p>
<ol>
<li>输入媒体元素：支持接收媒体，并将媒体注入到管线中。这类媒体元素有多种，实现从文件、网络、摄像头等来源读取媒体流</li>
<li>过滤器：能够转换、分析媒体流，实现混合、AR之类的功能</li>
<li>HubPort：Hub负责管理管线中的多个媒体流。每个Hub有多个HubPort，这些HubPort连接其它媒体元素</li>
<li>输出媒体元素：支持输出媒体，将媒体流带出管线。实现录像、在屏幕上播放、发送到网络等功能</li>
</ol>
<p>MediaElement常常由Endpoint实现，后者可能同时作为输入、输出元素。</p>
<div class="blog_h3"><span class="graybg">媒体管线</span></div>
<p>MediaPipeline是MediaElement构成的链条。链条可以有多个作为入口点的输入元素。由一个元素生成的输出流（SRC）可能输入到1-N个元素的输入流（SINK）：</p>
<p><img class="aligncenter size-full wp-image-15776" src="https://blog.gmem.cc/wp-content/uploads/2017/08/pipeline-src-sink.png" alt="pipeline-src-sink" width="400" height="141" /></p>
<div class="blog_h2"><span class="graybg">端点</span></div>
<p>端点是MediaElement的一种实现，能够输入、输出媒体流。端点类层次的类图如下：</p>
<p><img class="aligncenter size-full wp-image-15779" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurento-endpoints.png" alt="kurento-endpoints" width="490" height="428" /></p>
<p>这些端点的功能简述如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">端点</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>WebRtcEndpoint</td>
<td>输入输出端点（能够接受外部输入、也能够输出到外部），实现WebRTC协议</td>
</tr>
<tr>
<td>RtpEndpoint</td>
<td>输入输出端点，基于SDP进行媒体协商，基于RTP进行流发送</td>
</tr>
<tr>
<td>HttpPostEndpoint</td>
<td>输入端点，支持类似于HTTP文件上传那样的POST请求</td>
</tr>
<tr>
<td>PlayerEndpoint</td>
<td>输入端点，支持从文件系统、HTTP URL、RTSP URL接收内容，并将其注入到媒体管线中</td>
</tr>
<tr>
<td>RecorderEndpoint </td>
<td> 输出端点，以可靠的方式存储媒体内容到文件系统。用法示例：<br />
<pre class="crayon-plain-tag">recorder = new RecorderEndpoint.Builder(pipeline, "录像存储路径").build();
webrtcEndpoint.connect(recorder);</pre>
</td>
</tr>
</tbody>
</table>
<p>/home/alex/CPP/lib/kurento/src/kurento-media-server</p>
<p>关于端点，要注意：</p>
<ol>
<li>这些<span style="background-color: #c0c0c0;">端点都是在KMS中运行的</span>！尽管你会通过Java/Node的客户端，在应用服务器上操控端点，但是实质上都是基于Kurento协议向KMS发起远程调用</li>
<li>端点可能具有SRC、SINK端子，分别用于发送媒体流到其它端点、接受其它端点的发来的媒体流。SRC、SINK是媒体管线内部概念</li>
<li>端点可能对外部系统具有接收、发送媒体流的功能（但不叫SRC/SINK），例如WebRtcEndpoint。接收到的媒体流可以通过SRC发送给其它端点，其它端点发送到SINK的媒体流可以转发到外部系统</li>
<li>端点自己的SRC可以连接到自己的SINK</li>
</ol>
<div class="blog_h3"><span class="graybg">WebRtcEndpoint</span></div>
<p>代表一个运行在KMS中的WebRTC端点，是这类端点的控制接口。WebRTC端点可以和浏览器中的WebRTC客户端交互。例如<a href="#loopback">环回视频流</a>的那个实例，其媒体流向图如下：</p>
<p><img class="aligncenter size-full wp-image-15967" src="https://blog.gmem.cc/wp-content/uploads/2017/08/kurento-loopback.png" alt="kurento-loopback" width="600" height="319" /></p>
<p>说明如下：</p>
<ol>
<li>摄像头出来视频流，一方面在本地浏览器上渲染</li>
<li>另外一方面，发送给KMS中的WebRTCEndpoint端点</li>
<li>上一步的媒体流，到达SRC端子，进而发给自己的SINK端子（环回）</li>
<li>SINK端子的媒体流发回给浏览器</li>
<li>浏览器在另外一个video元素中渲染视频流</li>
</ol>
<p>WebRTC端点是P2P的WebRTC通信的一端，<span style="background-color: #c0c0c0;">另一端可以是使用RTCPeerConnection接口的浏览器、Native的WebRTC应用程序、甚至是另一个KMS服务器</span>。</p>
<p>为了建立WebRTC通信，两端必须进行SDP协商，其中一方作为邀请者（Offerer）另外一方作为应答者（Offeree），WebRTC端点可以作为两种角色之一。</p>
<p>当作为邀请者时：</p>
<ol>
<li>KMS客户端调用generateOffer()方法后，KMS生成一个SDP offer，此Offer返回给KMS客户端（应用服务器），再被转发给浏览器</li>
<li>浏览器处理上述Offer，并产生一个应答，应答传递给KMS客户端</li>
<li>后者调用processAnswer()导致应答转发给KMS</li>
</ol>
<p>当作为应答者时：</p>
<ol>
<li>浏览器生成一个SDP offer，发送到KMS客户端</li>
<li>KMS客户端调用processOffer()，SDP被转发给KMS，KMS生成应答，发送给KMS客户端</li>
<li>KMS客户端把应答转发给浏览器处理</li>
</ol>
<p>SDP独立于ICE候选发送。Kurento使用优化了的ICE收发机制 ——  Trickle ICE。两端分别、独立的执行收集ICE候选：</p>
<ol>
<li>浏览器中候选会自动收集，你可以使用onicecandidate回调接收通知。此事件常常比SDP处理发生的更快</li>
<li>KMS必须依赖于客户端调用gatherCandidates()，并在此调用之前注册IceCandidateFound的监听器</li>
</ol>
<p>KMS、浏览器每收集到一个ICE候选，就（以KMS客户端也就是应用服务器）为中介，发送给对方。接收到对方的ICE候选后，双方就会开始尝试建立双向连接。</p>
<p>需要注意WebRTC信号处理的异步性，假设你希望录制WebRTC端点的视频，在媒体流实际发送之前就执行录制是没有意义的。要感知WebRTC端点的状态，你需要监听端点的事件：</p>
<ol>
<li>IceComponentStateChange，在WebRTC点对点连接性发生变化后立即发布。这个事件仅仅能用于检测底层的连接性，处于CONNECTED 状态不意味着媒体流就已经开始传输。连接性状态包括（RFC5245定义了它们之间的状态转换图）：
<ol>
<li>DISCONNECTED  没有任何被调度的活动</li>
<li>GATHERING 开始收集本地（KMS服务器）的ICE候选</li>
<li>/home/alex/CPP/lib/kurento/src/kurento-media-serverCONNECTING  尝试创建连接，这在接收到对方的ICE候选后触发</li>
<li>CONNECTED  至少一个有效的ICE候选对出现，导致双向连接成功</li>
<li>READY  ICE结束，候选对选择完成</li>
<li>FAILED  连接性检查已经完毕，但是媒体流连接没有建立</li>
</ol>
</li>
<li>IceCandidateFound，一旦新的ICE候选可用即触发，这些候选必须被发送给对方</li>
<li>IceGatheringDone，所有ICE候选都被收集完毕后触发</li>
<li>NewCandidatePairSelected，当新的ICE候选对（本地、远程）可用时触发，当媒体会话已经进行后，此事件仍然可以触发 —— 一个更高优先级的ICE候选对被发现时</li>
<li>DataChannelOpen，数据通道打开时</li>
<li>DataChannelClose，数据通道关闭后</li>
</ol>
<p>流控制、拥塞管理是WebRTC最重要的一项功能。WebRTC连接总是以一个较低的带宽开始，慢慢的加大到最大可用带宽。WebRTC 端点如果服务多个外部连接，那么它们将共享一个码流质量，这意味着一个新的外部连接接入后，现有连接的码流质量会下降（因为要从较低带宽开始）。</p>
<p>默认的带宽范围取值在100kbps-500kbps之间，可以单独设置SRC/SINK、音频/视频的带宽范围：</p>
<ol>
<li>setMin/MaxVideoRecvBandwidth() 设置接收视频带宽</li>
<li>setMin/MaxAudioRecvBandwidth() 设置接收音频带宽</li>
<li>setMin/MaxVideoSendBandwidth() 设置发送视频带宽</li>
</ol>
<p>带宽最大值在SDP中有声明。</p>
<p>WebRTC可以提供数据通道，并且支持可靠/不可靠、有序/无序的数据传输。要支持数据通道，必须在创建WebRtcEndpoint时显式说明，默认是不允许创建数据通道的</p>
<div class="blog_h3"><span class="graybg">PlayerEndpoint</span></div>
<p>此端点从可Seek/不可Seek的媒体源中获取媒体流，并将流注入到KMS中。支持的URL格式：</p>
<ol>
<li>挂载到本地文件系统的文件：file:///path/to/file</li>
<li>提供RTSP协议的摄像头：rtsp://、rtsp://username:password@ip:port...</li>
<li>Web服务器：http(s):///path/to/file、http(s)://username:password@/path/to/file</li>
</ol>
<p>此端点支持以下操作：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">操作</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>play</td>
<td>开始播放媒体流，可以在pause后调用，恢复播放</td>
</tr>
<tr>
<td>stop</td>
<td>停止播放媒体流</td>
</tr>
<tr>
<td>pause</td>
<td>暂停播放媒体流</td>
</tr>
<tr>
<td>setPosition/getPosition</td>
<td>如果媒体源支持，可以用来执行seek操作</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">过滤器</span></div>
<p>这类媒体元素负责媒体的处理、机器视觉、AR等功能。 这些媒体元素的功能简述如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">过滤器</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ZBarFilter</td>
<td>检测二维码（QR）、条形码，一旦检测成功，就发布一个CodeFoundEvent事件。客户端可以侦听此事件并执行相应的操作</td>
</tr>
<tr>
<td>FaceOverlayFilter</td>
<td>检测人脸，叠加一个可配置的图像。用法示例：<br />
<pre class="crayon-plain-tag">FaceOverlayFilter filter = new FaceOverlayFilter.Builder(pipeline).build();
filter.setOverlayedImage("图片URL", -0.35F, -1.2F, 1.6F, 1.6F);</pre>
</td>
</tr>
<tr>
<td>GStreamerFilter</td>
<td>允许你在Kurento中使用GStreamer过滤器</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Hubs</span></div>
<p>这类媒体对象能够管理多个媒体流。这些媒体对象的功能简述如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">Hub</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Composite</td>
<td>
<p>能够混合多个输入音频流</p>
<p>能够合并多个输入视频流，构成多画面</p>
</td>
</tr>
<tr>
<td>DispatcherOneToMany</td>
<td>把一个输入HubPort分发给所有输出HubPort</td>
</tr>
<tr>
<td>Dispatcher</td>
<td>运行在任意输入-输出HubPort对值之间路由</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Kurento Utils JS</span></div>
<p>Utils JS用于简化浏览器端WebRTC应用的开发。</p>
<div class="blog_h3"><span class="graybg">安装</span></div>
<p>执行下面的命令安装：</p>
<pre class="crayon-plain-tag"># 基于NPM
npm install kurento-utils
# 基于Bower
bower install kurento-utils</pre>
<p>或者到<a href="http://builds.kurento.org/release/6.6.2/js/kurento-utils.min.js">这里下载</a>压缩后的JS文件。</p>
<div class="blog_h3"><span class="graybg">创建连接</span></div>
<p>WebRtcPeer对RTCPeerConnection进行了包装。连接可以是单向的（进行发送或者接收），也可以是双向的（同时发送接收）。</p>
<p>下面的例子示意了如何基于Utils JS创建一个RTCPeerConnection，并与其它Peer进行会话协商：</p>
<pre class="crayon-plain-tag">// 信号处理通道，由你自己决定如何实现，它能够让客户端知道可以和谁通信、如何通信
// 典型的做法是，所有客户端公开一个自己的名字，同时以一条WebSocket连接到服务器
// 客户端通过名字发起通信请求，服务器负责中介会话协商
var signalingChannel = createSignalingChannel(peerName);

// 用于显示远程视频的元素
var videoInput = document.getElementById( 'videoInput' );
// 用于显示本地视频的元素
var videoOutput = document.getElementById( 'videoOutput' );
// getUserMedia约束条件
var constraints = {
    audio: true,
    video: {
        width: 640,
        framerate: 15
    }
};

var options = {
    localVideo: videoInput,
    remoteVideo: videoOutput,
    onicecandidate: function( candidate ){
        // 把本地candidate发送给Peer，基于Trickle ICE，也就是说，一旦发现一个候选，就立即发送
        // 不等待所有候选收集成功，这样效率更高。此回调可能被调用多次
        signalingChannel.sendCandidate(candidate );
    },
    mediaConstraints: constraints
};

// 创建一个连接。注意，在双方都需要创建连接，创建的时机，就是服务器确认了两者要进行通信之后
var webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, function ( error ) {
    // 处理失败
    if ( error ) return onError( error );
    // 生成本地的SDP Offer
    this.generateOffer( onOffer );
} );

// 当收到Peer的candidate后，添加。下面的代码应该在信号处理的回调中调用
webRtcPeer.addIceCandidate(candidate);

// 当本地SDP Offer生成后，调用此回调
function onOffer( error, sdpOffer ) {
    if ( error ) return onError( error );
    // 发送SDP给Peer，Peer应该给出SDP应答，然后本地调用sdpAnswer回调
    signalingChannel.sendOffer( sdpOffer, sdpAnswer );
    function onAnswer( sdpAnswer ) {
        webRtcPeer.processAnswer( sdpAnswer );
    };
}</pre>
<p>简述一下上例对应的业务流程：</p>
<ol>
<li>通信发起方A，根据接受方B的标识符，向服务器发送WS请求 —— 我要和B通信</li>
<li>服务器通过WS推送信息给B，A想和你通信，你愿意吗？</li>
<li>如果B愿意，服务器通过WS推送消息给A、B，你们可以通信了</li>
<li>A、B分别创建连接对象（WebRtcPeer）</li>
<li>WebRtcPeer会自动收集Candidate，你应该通过WS把Candidate发回服务器，服务器再中转给Peer</li>
<li>一单A、B都收集到Candidate，它们就有可能进行点对点通信了（如果是局域网内）</li>
<li>A发起（Offer）一个会话描述（SDP），B接收到后，给出Answer</li>
<li>根据双方的SDP，建立媒体流交换</li>
</ol>
<div class="blog_h3"><span class="graybg">使用数据通道</span></div>
<p>数据通道允许你通过活动WebRTC连接传递二进制、文本数据。WebRtcPeer对数据通道的使用也提供了封装，将dataChannels选项设置为true即可使用：</p>
<pre class="crayon-plain-tag">var options = {
    localVideo: videoInput,
    remoteVideo: videoOutput,
    // 启用数据通道
    dataChannels: true,
    // 下面这个配置是可选的，允许你执行一些声明周期回调
    dataChannelConfig: {
        id: getChannelName(),
        onmessage: onMessage,
        onopen: onOpen,
        onclose: onClosed,
        onbufferedamountlow: onbufferedamountlow,
        onerror: onerror
    },
    onicecandidate: onIceCandidate
}

webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, onWebRtcPeerCreated );</pre>
<p>一旦webRtcPeer对象被创建，你就可以调用下面的方法，通过数据通道发送信息：</p>
<pre class="crayon-plain-tag">// 发送的数据类型取决于应用
webRtcPeer.send('Hello there');</pre>
<p>数据通道的生命周期受限于其依赖的连接，<pre class="crayon-plain-tag">webRtcPeer.dispose()</pre>被调用后数据通道也被关闭和释放。 </p>
<div class="blog_h1"><span class="graybg"><span class="graybg">Kurento模块</span></span></div>
<p>Kurento是一个可拔插的框架，它的每个插件称为模块。模块分为三类。</p>
<div class="blog_h2"><span class="graybg">主模块</span></div>
<p>这类模块安装了KMS就可以使用，包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">模块</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>kms-core</td>
<td>KMS的核心功能，基于C编写</td>
</tr>
<tr>
<td>kms-elements</td>
<td>实现媒体元素，例如WebRtcEndpoint、WebRtcEndpoint</td>
</tr>
<tr>
<td>kms-filters</td>
<td>实现过滤器，例如FaceOverlayFilter, ZBarFilter, GStreamerFilter</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">内置模块</span></div>
<p>这些模块用于增强KMS的基本功能，没有随KMS安装，包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">模块</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>kms-pointerdetector</td>
<td>
<p>一个过滤器，基于颜色追踪在视频流中检测点（pointers），执行下面的命令安装：</p>
<pre class="crayon-plain-tag">sudo apt-get install kms-pointerdetector-6.0</pre>
</td>
</tr>
<tr>
<td>kms-chroma</td>
<td>
<p>一个过滤器，在一个层上让指定的色彩范围变得透明，这样下面层的图像就会显示出来。执行下面的命令安装：
<pre class="crayon-plain-tag">sudo apt-get install kms-chroma-6.0</pre>
</td>
</tr>
<tr>
<td>kms-crowddetector</td>
<td>
<p>过滤器，能够检测人群聚集。执行下面的命令安装：
<pre class="crayon-plain-tag">sudo apt-get install kms-crowddetector-6.0</pre>
</td>
</tr>
<tr>
<td>kms-platedetector</td>
<td>
<p>过滤器，能够实现车牌检测。执行下面的命令安装：
<pre class="crayon-plain-tag">sudo apt-get install kms-platedetector-6.0</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">自定义模块</span></div>
<p>你可以根据需要<a href="http://doc-kurento.readthedocs.io/en/stable/mastering/develop_kurento_modules.html">自己扩展KMS模块</a>。
<div class="blog_h1"><span class="graybg"><span class="graybg">实例</span></span></div>
<div class="blog_h2"><span class="graybg"><a id="loopback"></a>HelloWorld</span></div>
<p>这是一个环回视频流的例子 —— 视频流发送给自己，需要一台客户端即可测试。通信流程如下：</p>
<ol>
<li>页面加载时，客户端自动创建一个到服务器的wss连接：<br />
<pre class="crayon-plain-tag">var ws = new WebSocket('wss://' + location.host + '/helloworld'); </pre></p>
<p>信号处理依赖此wss连接进行，信号格式为JSON，其id字段表示消息的类型。</p>
</li>
<li>用户点击页面上的开始按钮，执行下面的逻辑：<br />
<pre class="crayon-plain-tag">var options = {
   // 显示本地流的元素
   localVideo : videoInput,
   // 显示远程流的元素
   remoteVideo : videoOutput,
   // 当候选通信地址可用时，执行的回调
   onicecandidate : onIceCandidate
}
// 连接对象创建后执行的回调
function( err ){
    if ( err ) console.error( err );
    // 生成SDP，成功后执行回调
    webRtcPeer.generateOffer( function( error, offerSdp ) {
        ws.send( JSON.stringify( {
            id : 'start',
            sdpOffer : offerSdp
        } ) );
    });
}
// 创建具有收、发能力的连接对象
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, callback ); </pre></p>
<p>也就是说，作为通信发起方：</p>
<ol>
<li>创建一个连接对象WebRtcPeerSendrecv，此对象创建后，本地流立即就显示在localVideo这个video标签中</li>
<li>创建完毕后，即生成SDP，其内容如下（主要是发起方允许的连接方式、支持的媒体特性）：<br />
<pre class="crayon-plain-tag">v=0
# 第一个数字是会话标识，第二个数字是会话版本。后续三个参数和会话协商无关：网络类型Internet，地址类型IPv4，产生SDP的机器的地址
o=- 6324724567974172241 2 IN IP4 127.0.0.1
# 会话的名称，不常用
s=-
# 会话起止时间，都为0表示不限制时间
t=0 0
# BUNDLE分组将多个媒体行关联起来，在WebRTC中用于在同一RTP会话中传递多个媒体流
a=group:BUNDLE audio video
# 在PeerConnection声明周期中，赋予WebRTC媒体流唯一标识
a=msid-semantic: WMS g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t

###  音频行  ### 
# m表示这是一个媒体行，audio表示这是音频，后面是协议，最后的长串数字为媒体格式说明
m=audio 38968 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
# c表示这是一个连接行，表示收发数据通过什么IP进行。但是由于WebRTC强制使用ICE，因此这一行没什么用
c=IN IP4 192.168.56.1
# 明确说明用于RTCP的地址和端口
a=rtcp:51004 IN IP4 192.168.56.1
# 下面若干行都是ICE候选，ICE是用于NAT穿透的协议
#           标识       1RTP/2RTCP 优先级  通信地址和端口
a=candidate:2999745851 1 udp 2122260223 192.168.56.1 38968 typ host generation 0
a=candidate:364622241 1 udp 2122194687 10.255.0.1 49487 typ host generation 0
a=candidate:1051995033 1 udp 2122129151 172.18.0.1 52714 typ host generation 0
a=candidate:410389623 1 udp 2122063615 172.21.0.1 54819 typ host generation 0
a=candidate:2199032595 1 udp 2121998079 192.168.1.89 47718 typ host generation 0
a=candidate:627415207 1 udp 2121932543 192.168.0.89 52455 typ host generation 0
a=candidate:2999745851 2 udp 2122260222 192.168.56.1 51004 typ host generation 0
a=candidate:364622241 2 udp 2122194686 10.255.0.1 59954 typ host generation 0
a=candidate:1051995033 2 udp 2122129150 172.18.0.1 41985 typ host generation 0
a=candidate:410389623 2 udp 2122063614 172.21.0.1 59234 typ host generation 0
a=candidate:2199032595 2 udp 2121998078 192.168.1.89 58222 typ host generation 0
a=candidate:627415207 2 udp 2121932542 192.168.0.89 36590 typ host generation 0
# 下面两行是ICE参数
a=ice-ufrag:Oyu3vwR19M1nxsx4
a=ice-pwd:8RbNWdv799Hz7aXWj2DMIPGH
# 下面两行是DTLS参数
# DTLS-SRTP协商时使用的证书的指纹信息
a=fingerprint:sha-256 58:BC:1A:0B:22:10:95:7B:C9:98:4A:D5:34:E9:44:85:FF:9D:A4:7B:07:39:36:FE:90:59:E0:14:3D:B9:21:6C
a=setup:actpass
# 用在BUNDLE中的标识符
a=mid:audio
# 定义RTP扩展头
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
# 同时支持接收、发送
a=sendrecv
# 支持RTCP多路复用
a=rtcp-mux
# 解码器参数
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10; useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:126 telephone-event/8000
a=maxptime:60
# SSRC参数
a=ssrc:2978616353 cname:GrA29DQMxaUfd99u
a=ssrc:2978616353 msid:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t 97776675-4490-4b74-a849-bbd46a722c89
a=ssrc:2978616353 mslabel:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
a=ssrc:2978616353 label:97776675-4490-4b74-a849-bbd46a722c89
m=video 46497 UDP/TLS/RTP/SAVPF 100 101 116 117 96 97 98
c=IN IP4 192.168.56.1
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:2999745851 1 udp 2122260223 192.168.56.1 46497 typ host generation 0
a=candidate:364622241 1 udp 2122194687 10.255.0.1 34284 typ host generation 0
a=ice-ufrag:Oyu3vwR19M1nxsx4
a=ice-pwd:8RbNWdv799Hz7aXWj2DMIPGH
a=fingerprint:sha-256 58:BC:1A:0B:22:10:95:7B:C9:98:4A:D5:34:E9:44:85:FF:9D:A4:7B:07:39:36:FE:90:59:E0:14:3D:B9:21:6C
a=setup:actpass

### 视频行 ### 
a=mid:video
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4 urn:3gpp:video-orientation
a=sendrecv
a=rtcp-mux
# 支持的视频编码
a=rtpmap:100 VP8/90000
# 如果客户端是Firefox、Chrome 61 —— 支持H264
a=rtpmap:120 VP8/90000
a=rtpmap:126 H264/90000
a=rtpmap:97 H264/90000
# 则出现以上三行
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtpmap:101 VP9/90000
a=rtcp-fb:101 ccm fir
a=rtcp-fb:101 nack
a=rtcp-fb:101 nack pli
a=rtcp-fb:101 goog-remb
a=rtcp-fb:101 transport-cc
a=rtpmap:116 red/90000
a=rtpmap:117 ulpfec/90000
a=rtpmap:96 rtx/90000
a=fmtp:96 apt=100
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=101
a=rtpmap:98 rtx/90000
a=fmtp:98 apt=116
a=ssrc-group:FID 3977515695 1979665708
a=ssrc:3977515695 cname:GrA29DQMxaUfd99u
a=ssrc:3977515695 msid:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t 153f4d5f-ba5b-4772-8700-aff4474d8652
a=ssrc:3977515695 mslabel:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
a=ssrc:3977515695 label:153f4d5f-ba5b-4772-8700-aff4474d8652
a=ssrc:1979665708 cname:GrA29DQMxaUfd99u
a=ssrc:1979665708 msid:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t 153f4d5f-ba5b-4772-8700-aff4474d8652
a=ssrc:1979665708 mslabel:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
a=ssrc:1979665708 label:153f4d5f-ba5b-4772-8700-aff4474d8652</pre>
</li>
<li>上述SDP以消息类型start发送给服务器</li>
</ol>
</li>
<li>服务器接收到start消息后，执行以下逻辑：<br />
<pre class="crayon-plain-tag">// 创建媒体管线
MediaPipeline pipeline = kurento.createMediaPipeline();
// 在管线中添加一个WebRTC端点
WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
// 连接WebRTC端点到自己
webRtcEndpoint.connect(webRtcEndpoint);

// 创建一个用户会话（UserSession不属于KMS 客户端API的组成部分）
UserSession user = new UserSession();
// 连接到的管线
user.setMediaPipeline(pipeline);
// 连接到的端点，注意此端点的输入、输出是同一个流
user.setWebRtcEndpoint(webRtcEndpoint);
// 以WebSocket会话标识时别用户https://localhost:8443/#
users.put(session.getId(), user);

// 处理SDP
String sdpOffer = jsonMessage.get("sdpOffer").getAsString();
// 由端点来处理SDP，生成应答
String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer);
// 以消息类型startResponse将应答SDP通过WebSocketSession发回给客户端
JsonObject response = new JsonObject();response.addProperty("id", "startResponse");
response.addProperty("sdpAnswer", sdpAnswer);
synchronized (session) {
    session.sendMessage(new TextMessage(response.toString()));
}

// 一旦收集到服务器的ICE候选信息，即以消息类型iceCandidate发送给客户端
webRtcEndpoint.addIceCandidateFoundListener(new EventListener&lt;IceCandidateFoundEvent&gt;() {
  @Override
  public void onEvent(IceCandidateFoundEvent event) {
    JsonObject response = new JsonObject();
    response.addProperty("id", "iceCandidate");
    response.add("candidate", JsonUtils.toJsonObject(event.getCandidate()));
    try {
      synchronized (session) {
        session.sendMessage(new TextMessage(response.toString()));
      }
    } catch (IOException e) {
      log.error(e.getMessage());
    }
  }
});
// 为某个端点收集服务器的ICE候选信息
webRtcEndpoint.gatherCandidates();</pre></p>
<p>服务器生成的SDP应答内容如下：</p>
<pre class="crayon-plain-tag">v=0
o=- 3713658153 3713658153 IN IP4 0.0.0.0
s=Kurento Media Server
c=IN IP4 0.0.0.0
t=0 0
a=msid-semantic: WMS kGkOSxP0iFTu9aRzm53BNz0fROtBq1HxLFje
a=group:BUNDLE audio video
m=audio 1 UDP/TLS/RTP/SAVPF 111 0
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=mid:audio
a=rtcp:9 IN IP4 0.0.0.0
a=rtpmap:111 opus/48000/2
a=rtpmap:0 PCMU/8000
a=setup:active
a=sendrecv
a=rtcp-mux
a=fmtp:111 minptime=10; useinbandfec=1
a=maxptime:60
a=ssrc:1475810019 cname:user35735626@host-c1cf1e49
a=ice-ufrag:/Jml
a=ice-pwd:RCpQ+o7Ybof5B5mxYDGM17
a=fingerprint:sha-256 B4:72:A8:44:90:3D:CF:1B:8E:30:93:09:AC:66:BF:05:60:D7:0B:C3:C3:AA:28:7D:44:46:8E:55:17:61:4F:43
m=video 1 UDP/TLS/RTP/SAVPF 100
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=mid:video
a=rtcp:9 IN IP4 0.0.0.0
# 使用VP8作为视频编码格式
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtcp-fb:100 goog-remb
a=setup:active
a=sendrecv
a=rtcp-mux
a=ssrc:101029323 cname:user35735626@host-c1cf1e49
a=ice-ufrag:/Jml
a=ice-pwd:RCpQ+o7Ybof5B5mxYDGM17
a=fingerprint:sha-256 B4:72:A8:44:90:3D:CF:1B:8E:30:93:09:AC:66:BF:05:60:D7:0B:C3:C3:AA:28:7D:44:46:8E:55:17:61:4F:43</pre>
</li>
<li>客户端接收到startResponse消息后，调用下面的方法处理SDP应答：<br />
<pre class="crayon-plain-tag">webRtcPeer.processAnswer(message.sdpAnswer, function(error) {
    if (error) console.error(error);
});</pre>
</li>
<li>关于Ice Candidate的处理上面没有提及，这会异步的进行：
<ol>
<li>客户端连接创建后，就会自动收集ICE候选，一旦收集到，就调用如下回调：<br />
<pre class="crayon-plain-tag">function onIceCandidate(candidate) {
    ws.send(JSON.stringify({
        id : 'onIceCandidate',
        candidate : candidate
    }));
}</pre>
<p>候选的内容如下：</p>
<pre class="crayon-plain-tag">{
    // 此候选的通信地址
    "candidate":"candidate:2999745851 1 udp 2122260223 192.168.56.1 36777 typ host generation 0 ufrag waE0gMnNFX3ug+yW",
    // 此候选关联的媒体流的标识（identification-tag）
    "sdpMid":"audio",
    // 此候选关联SDP中媒体描述的索引
    "sdpMLineIndex":0
}</pre>
<p>也就是说，以消息类型onIceCandidate发送给服务器</p>
</li>
<li>服务器接收到onIceCandidate消息后，将其保存到用户对象中：<br />
<pre class="crayon-plain-tag">UserSession user = users.get(session.getId());
IceCandidate candidate = new IceCandidate(
    jsonCandidate.get("candidate").getAsString(),
    jsonCandidate.get("sdpMid").getAsString(),
    jsonCandidate.get("sdpMLineIndex").getAsInt()
);
user.addCandidate(candidate);</pre>
</li>
<li>
<p>随着客户端候选的收集，onIceCandidate消息会被发送很多次，后续的sdpMid可能是video，sdpMLineIndex可能是1</p>
</li>
<li>服务器端在创建端点后，也同样会自动收集ICE候选信息，并以iceCandidate消息发送给客户端。候选的内容如下：<br />
<pre class="crayon-plain-tag">{
    "candidate": "candidate:5 1 TCP 1019216383 172.21.0.6 9 typ host tcptype active",
    "sdpMid":"video",
    "sdpMLineIndex":1
}</pre>
</li>
<li>客户端做如下处理：<br />
<pre class="crayon-plain-tag">webRtcPeer.addIceCandidate(parsedMessage.candidate, function(error) {
    if (error)  console.error(error);
});</pre>
</li>
<li>
<p>随着服务器端候选的收集， iceCandidate消息也会被发送多次</p>
</li>
</ol>
</li>
<li>随着候选信息的收集，webRtcPeer有了足够的信息，它会在remoteView元素中渲染远程媒体流</li>
<li>当用户点击停止按钮后，调用<pre class="crayon-plain-tag">webRtcPeer.dispose()</pre>并发送一个stop类型的消息</li>
<li>服务器收到stop消息后，清理用户数据：<br />
<pre class="crayon-plain-tag">UserSession user = users.remove(session.getId());
user.release();</pre></p>
<ol>
<li>释放用户数据的时候，会调用<pre class="crayon-plain-tag">mediaPipeline.release()</pre>释放媒体管线</li>
</ol>
</li>
<li>页面卸载时，客户端自动关闭wss连接：<br />
<pre class="crayon-plain-tag">ws.close();</pre>
</li>
</ol>
<p>在这个HelloWorld例子中，媒体流不是简单的由客户端发给自己，而是由服务器中转。也就是说，通信的Peer是服务器、客户端。</p>
<div class="blog_h2"><span class="graybg">FaceOverlay</span></div>
<p>可以在上例的Loopback媒体管线上插入一个FaceOverlayFilter，在检测到人脸时，附加一个帽子图片到人脸上方：</p>
<pre class="crayon-plain-tag">UserSession user = new UserSession();
MediaPipeline pipeline = kurento.createMediaPipeline();
WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
user.setWebRtcEndpoint(webRtcEndpoint);

// 注意媒体管线在KMS中运行
FaceOverlayFilter faceOverlayFilter = new FaceOverlayFilter.Builder(pipeline).build();
faceOverlayFilter.setOverlayedImage(“https://172.21.0.1:8443/img/mario-wings.png", -0.35F, -1.2F, 1.6F, 1.6F);
// 连接WebRTC端点的SRC（输出）到FaceOverlayFilter的SINK（输入）
webRtcEndpoint.connect(faceOverlayFilter);
// 连接FaceOverlayFilter的SRC（输出）到WebRTC的SINK（输入）
faceOverlayFilter.connect(webRtcEndpoint);</pre>
<div class="blog_h2"><span class="graybg">一对多广播</span></div>
<div class="blog_h3"><span class="graybg">发布者客户端</span></div>
<p>首先初始化连接：</p>
<pre class="crayon-plain-tag">// 仅仅需要发送数据，不需要接收
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
    {
        localVideo : video,
        onicecandidate : function(){ /* 发送本地ICE候选信息给服务器 */ }
    },
    function( err ){
        webRtcPeer.generateOffer(function( err, offerSdp ){
            /* 发送SDP，消息类型presenter */
        });
    }
)</pre>
<p>服务器接收到presenter消息后，会发送一个presenterResponse消息过来。如果服务器同意当前客户端作为发布者，则发布者调用：</p>
<pre class="crayon-plain-tag">webRtcPeer.processAnswer(message.sdpAnswer);
// 否则关闭连接</pre>
<p>服务器发来的ICE候选消息的处理，和前面的例子一样。 </p>
<div class="blog_h3"><span class="graybg">查看者客户端</span></div>
<p>首先也是初始化连接：</p>
<pre class="crayon-plain-tag">webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly( options, function( err ){
    this.generateOffer(function(){
        /* 发送SDP，消息类型viewer */
    });
});</pre>
<p>viewerResponse、 服务器发来的ICE候选消息的处理，和发布者一样。</p>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<p>当服务器接收到发布者发来的presenter消息时，执行：</p>
<ol>
<li>记录一个发布者的会话对象，本质上是基于WS客户端标识对发布者进行时别</li>
<li>创建媒体管线：<br />
<pre class="crayon-plain-tag">pipeline = kurento.createMediaPipeline();
// 设置发布者的端点对象
presenterUserSession.setWebRtcEndpoint(new WebRtcEndpoint.Builder(pipeline).build());
// 当服务器的ICE候选准备好之后，发送给发布者客户端：
presenterUserSession.getWebRtcEndpoint().addIceCandidateFoundListener( e-&gt; {
    // 作为iceCandidate事件发送
});

// 处理发布者的SDP
String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
String sdpAnswer = presenterWebRtc.processOffer(sdpOffer);
// 然后以presenterResponse消息发送SDP应答给发布者

// 最后，为发布者收集ICE候选信息
presenterWebRtc.gatherCandidates();</pre>
</li>
<li>当接收到发布者的ICE候选后，把这些信息记录到代表发布者的会话对象中：<br />
<pre class="crayon-plain-tag">presenterUserSession.addCandidate(cand); // 处理方式和HelloWorld那个例子相同 </pre>
</li>
</ol>
<p>到目前为止，尚未发生任何媒体流的传输工作。因为没有人查看者。</p>
<p>当有查看者接入后，服务器首先收到一个viewer信息，并执行：</p>
<ol>
<li>如果当前没有发布者，返回viewerResponse消息，其response属性为rejected</li>
<li>如果当前有发布者，则为其创建UserSession对象、WebRtcEndpoint端点，并发此端点加入到之前创建的管线中：<br />
<pre class="crayon-plain-tag">UserSession viewer = new UserSession(webSocketSession);
WebRtcEndpoint nextWebRtc = new WebRtcEndpoint.Builder(pipeline).build();
viewer.setWebRtcEndpoint(nextWebRtc);
viewer.getWebRtcEndpoint().addIceCandidateFoundListener( e-&gt; {
    // 作为iceCandidate事件发送
});

// 重要：将发布者的SRC连接到查看者的SINK
presenterUserSession.getWebRtcEndpoint().connect(nextWebRtc);

// 处理查看者的SDP
String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
// SDP应答总是调用请求者的端点对象获得
String sdpAnswer = nextWebRtc.processOffer(sdpOffer);
// 然后以viewerResponse消息发送SDP应答给发布者

// 最后，为观看者收集ICE候选信息
nextWebRtc.gatherCandidates();</pre>
</li>
</ol>
<p>当由更多的查看者连接进来后，发布者端点的SRC将连接到更多的SINK，呈现出星状结构。从ICE候选信息来看，貌似媒体流都是从服务器中转的。</p>
<div class="blog_h2"><span class="graybg">一对一视频电话</span></div>
<p>这个在实现上没有特别的地方，参与通话双方的WebRTC端点，需要配置为首尾相连。</p>
<p>此外，业务逻辑部分需要实现拒接之类的功能。</p>
<div class="blog_h2"><span class="graybg">多对多视频会议</span></div>
<p>相当于每个参与者都进行一对多广播。在实现时，往往会抽象出会议房间（Group）的概念，房间内的每个人都需要对其它人进行广播。</p>
<p>每个参与者都需要创建一个发送端点，N-1个接收端点，一共N个video元素。</p>
<p>此外，一旦有新人加入、旧人退出，就需要通知房间的所有参与者，进行客户端资源清理、UI更新。</p>
<div class="blog_h1"><span class="graybg"><a id="vs"></a>视频监控</span></div>
<p>这类应用场景中，媒体流的来源主要有两类：</p>
<ol>
<li>基于ONVIF框架协议，视频流基于RTSP/RTP传输</li>
<li>由设备SDK提供，SDK可能提供标准格式的码流、视频帧，或者解码后的原始图像</li>
</ol>
<p>视频监控的主要需求包括：</p>
<ol>
<li>实时监控，特别是多画面实时监控</li>
<li>录像回放</li>
<li>视频分析，例如移动侦测、模式识别</li>
</ol>
<div class="blog_h2"><span class="graybg">封装</span></div>
<p>为了简化开发，我们对Kurento、信号处理进行了组件化封装。</p>
<div class="blog_h3"><span class="graybg">MediaSession</span></div>
<p>代表一个WebRTC客户端与Kurento的媒体会话：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;


import org.kurento.client.Endpoint;
import org.kurento.client.IceCandidate;
import org.kurento.client.MediaPipeline;
import org.kurento.client.WebRtcEndpoint;

import java.security.Principal;
import java.util.ArrayList;
import java.util.List;

public class MediaSession {

    private String id;

    private Principal principal;

    private MediaPipeline pipeline;

    private WebRtcEndpoint endpoint;

    private List&lt;IceCandidate&gt; candidatesPending;

    public MediaSession( String id ) {
        this.id = id;
        candidatesPending = new ArrayList&lt;&gt;();
    }

    public MediaPipeline getPipeline() {
        return pipeline;
    }

    public void setPipeline( MediaPipeline pipeline ) {
        this.pipeline = pipeline;
    }

    public Endpoint getEndpoint() {
        return endpoint;
    }

    public synchronized void setEndpoint( WebRtcEndpoint endpoint ) {
        this.endpoint = endpoint;
        // ICE可能在端点创建之前就送达
        if ( candidatesPending != null ) {
            candidatesPending.forEach( cp -&gt; {
                endpoint.addIceCandidate( cp );
            } );
            candidatesPending = null;
        }
    }

    @Override
    public String toString() {
        return String.format( "id = %s  ep = %s  pp = %s", getId(), getEndpoint(), getPipeline() );
    }

    public String getId() {
        return id;
    }

    public Principal getPrincipal() {
        return principal;
    }

    public void setPrincipal( Principal principal ) {
        this.principal = principal;
    }

    public synchronized void addIceCandidate( IceCandidate candidate ) {
        // ICE可能在端点创建之前就送达
        if ( endpoint == null ) {
            candidatesPending.add( candidate );
        } else {
            endpoint.addIceCandidate( candidate );
        }
    }
}</pre>
<div class="blog_h3"><span class="graybg">VideoSurveillanceApp</span></div>
<p>Spring Boot应用程序，信号处理以STOMP作为子协议：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.KurentoClient;
import org.kurento.client.KurentoClientBuilder;
import org.kurento.client.MediaPipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import sun.security.acl.PrincipalImpl;

import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SpringBootApplication
@EnableWebSocketMessageBroker
public class VideoSurveillanceApp extends AbstractWebSocketMessageBrokerConfigurer {

    private static final Logger LOGGER = LoggerFactory.getLogger( VideoSurveillanceApp.class );

    private Map&lt;String, MediaSession&gt; sessions = new ConcurrentHashMap&lt;&gt;();

    public void registerStompEndpoints( StompEndpointRegistry registry ) {
        // 信号处理在 /signal下进行
        registry.addEndpoint( "/signal" );
    }

    @Override
    public void configureMessageBroker( MessageBrokerRegistry registry ) {
        registry.setApplicationDestinationPrefixes( "/app" );
    }

    @Override
    public void configureClientInboundChannel( ChannelRegistration registration ) {
        registration.setInterceptors( new ChannelInterceptorAdapter() {
            @Override
            public Message&lt;?&gt; preSend( Message&lt;?&gt; message, MessageChannel channel ) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor( message, StompHeaderAccessor.class );
                String simpSessionId = (String) accessor.getHeader( "simpSessionId" );
                MediaSession session = getMideaSession( simpSessionId );
                if ( StompCommand.CONNECT.equals( accessor.getCommand() ) ) {
                    // 设置当前用户身份
                    String login = accessor.getNativeHeader( "login" ).get( 0 );
                    Principal principal = new PrincipalImpl( login );
                    accessor.setUser( principal );
                    session.setPrincipal( principal );
                    LOGGER.info( "User {} connected with session id {}", login, simpSessionId );
                }
                // 每次处理消息之前，设置session头，便于消息处理方法注入之
                accessor.setHeader( "session", session );
                return message;
            }
        } );
    }

    private MediaSession getMideaSession( String simpSessionId ) {
        if ( sessions.containsKey( simpSessionId ) ) {
            return sessions.get( simpSessionId );
        } else {
            MediaSession session = new MediaSession( simpSessionId );
            sessions.put( simpSessionId, session );
            return session;
        }
    }

    @Bean
    public KurentoClient kurentoClient() {
        return new KurentoClientBuilder().setKmsWsUri( "ws://172.21.0.6:8888/kurento" ).connect();
    }

    public static void main( String[] args ) {
        new SpringApplication( VideoSurveillanceApp.class ).run( args );
    }
}</pre>
<div class="blog_h3"><span class="graybg">KurentoService</span></div>
<p>封装一些模板代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.KurentoClient;
import org.kurento.client.MediaPipeline;
import org.kurento.client.WebRtcEndpoint;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

import javax.inject.Inject;

@Service
public class KurentoService {

    @Inject
    private KurentoClient client;

    @Inject
    private SimpMessagingTemplate template;

    /**
     * 初始化一个媒体管线
     *
     * @return
     */
    public MediaPipeline createMediaPipeline() {
        return client.createMediaPipeline();
    }

    /**
     * 在媒体管线上创建一个与WebRTC浏览器客户端通信的端点
     *
     * @param pipeline 管线
     * @param sdpoffer 浏览器发送来的SDP邀请
     * @param user     浏览器的身份
     * @return 运行在KMS中的WebRTC端点
     */
    public WebRtcEndpoint createWebRtcEndpoint( MediaPipeline pipeline, String sdpoffer, String user, String namespace ) {
        WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder( pipeline ).build();
        // 处理SDP
        String sdpAnswer = webRtcEndpoint.processOffer( sdpoffer );
        template.convertAndSendToUser( user, namespace + "/sdpanswer", sdpAnswer );
        // 处理ICE候选
        webRtcEndpoint.addIceCandidateFoundListener( event -&gt; {
            String dest = namespace + "/icecandidate";
            template.convertAndSendToUser( user, dest, event.getCandidate() );
        } );
        webRtcEndpoint.gatherCandidates();
        return webRtcEndpoint;
    }
} </pre>
<div class="blog_h3"><span class="graybg">StompClient </span></div>
<p>对stomp.js进行简单的封装：</p>
<ol>
<li>每个客户端在一个名字空间内操作</li>
<li>订阅总是针对/user前缀进行</li>
<li>发送总是针对/app前缀进行 </li>
</ol>
<p>代码如下：</p>
<pre class="crayon-plain-tag">class StompClient {
    /**
     * 选项：
     * url，WebSocket连接地址
     * namespace，不包含/app、/user的目的地前缀
     * login，用户名
     * passcode，密码
     */
    constructor( options ) {
        this.namespace = options.namespace || {};
        this.pending = [];
        this.stomp = Stomp.over( new WebSocket( options.url ) );
        this.stomp.heartbeat.outgoing = 20000;
        this.stomp.connect( options.login, options.passcode, ( frame ) =&gt; {
            this.connected = true;
            this.processPending();
        } );
    }

    processPending() {
        if ( this.connected ) {
            let pending = this.pending;
            this.pending = [];
            pending.forEach( callback =&gt; callback() );
        }
    }

    recv( destination, callback ) {
        this.pending.push( () =&gt; {
            this.stomp.subscribe( '/user' + this.namespace + destination, ( frame ) =&gt; {
                callback( this.decode( frame.body, frame.headers[ 'content-type' ] ), frame );
            } );
        } );
        this.processPending();
    }

    encode( obj ) {
        return JSON.stringify( obj );
    }

    decode( str, mimeType ) {
        // 自动分析MIME类型，进行适当的解析
        if ( mimeType.startsWith( 'application/json;' ) ) {
            return JSON.parse( str );
        }
        else {
            return str;
        }
    }

    send( destination, object ) {
        this.pending.push( () =&gt; {
            this.stomp.send( '/app' + this.namespace + destination, {
                "content-type": "application/json;charset=UTF-8"
            }, this.encode( object ) );
        } );
        this.processPending();
    }

    disconnect() {
        this.stomp.disconnect();
    }
}</pre>
<div class="blog_h3"><span class="graybg">WebRTCEndpoint</span></div>
<p>对Kurento Utils的WebRtcPeer进行封装。 </p>
<p>WebRTCEndpoint的STOMP消息目的地格式：  前缀 + 名字空间 + 消息类型。消息类型包括：</p>
<ol>
<li>sdpoffer，表示浏览器客户端发起SDP邀请</li>
<li>sdpanswer，表示KMS客户端发给浏览器的SDP应答</li>
<li>icecandidate，双方交换ICE候选</li>
<li>stop，客户端请求停止会话</li>
<li>其它消息类型</li>
</ol>
<p>代码如下：</p>
<pre class="crayon-plain-tag">class WebRTCEndpoint {
    constructor( mode, options ) {
        /**
         * 选项：
         * remoteVideo，显示远程视频流的元素
         */
        options = options || {};
        let stomp = new StompClient( {
            url: options.url,
            namespace: options.namespace,
            login: options.login
        } );

        let webRtcPeerType;
        switch ( mode ) {
            case WebRTCEndpoint.MODE_SEND:
                webRtcPeerType = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
                break;
            case WebRTCEndpoint.MODE_RECV:
                webRtcPeerType = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
                break;
            case WebRTCEndpoint.MODE_SEND_RECV:
                webRtcPeerType = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv;
                break;
        }
        stomp.recv( '/icecandidate', candidate =&gt; {
            this.peer.addIceCandidate( candidate );
        } );
        stomp.recv( '/sdpanswer', answer =&gt; {
            this.peer.processAnswer( answer );
        } );
        options.onicecandidate = candidate =&gt; {
            stomp.send( '/icecandidate', candidate );
        }
        this.peer = webRtcPeerType( options, err =&gt; {
            this.peer.generateOffer( ( error, sdpOffer ) =&gt; {
                stomp.send( '/sdpoffer', sdpOffer );
            } );
        } );
        this.stomp = stomp;
    }

    dispose() {
        this.stomp.send( '/stop', "bye" );
        this.stomp.disconnect();
        this.peer &amp;&amp; this.peer.dispose();
    }
}
WebRTCEndpoint.MODE_SEND = 0;
WebRTCEndpoint.MODE_RECV = 1;
WebRTCEndpoint.MODE_SEND_RECV = 2; </pre>
<div class="blog_h2"><span class="graybg">RTSP接入</span></div>
<p>以下情况下可以考虑RTSP接入：</p>
<ol>
<li>IP摄像头或者NVR直接提供流RTSP协议服务器</li>
<li>通过SDK获取码流，手工创建RTSP协议服务器</li>
</ol>
<p>对于第二种方式，还可以考虑利用Kurento的RTPEndpint，直接通过RTP协议发送媒体流到KMS。</p>
<div class="blog_h3"><span class="graybg">媒体互操作性</span></div>
<p>IP摄像头常常会提供某种基于流的接入方式：</p>
<ol>
<li>RTSP/H.264：这类摄像头通常用在视频监控领域。它们通过RTSP协议来建立RTP媒体会话 —— 信号处理基于RTSP进行而媒体流直接通过RTP传输。不同的摄像头厂商支持的RTP profile可能不同，<a href="https://www.ietf.org/rfc/rfc3551.txt">AVP</a>（用于音视频会议的RTP profile，最小化控制。RTP Profile for Audio and Video Conferences<br /> with Minimal Control）是一种常用的profile。视频编码方式也有不同的选择，典型的是 H.264</li>
<li>HTTP/MJPEG：这类摄像头基于HTTP协议进行信号处理和媒体传输，视频流被编码为JPEG的序列。这类摄像头的硬件比较简单，资源（包括电量）消耗少但是视频质量差</li>
</ol>
<p>要实现WebRTC到IP摄像头的媒体互操作性，两者的码流格式必须兼容，这种码流转换的工作是由某种WebRTC网关负责的（例如Kurento）。此网关需要完成：</p>
<ol>
<li>和摄像头交互，也就是网关需要理解RTSP/RTP或者HTTP</li>
<li>解码从摄像头取得的码流，例如H264或者MJPEG</li>
<li>将码流重新编为浏览器支持的格式，例如VP8是WebRTC最广泛支持的编码</li>
<li>通过WebRTC协议把码流发送给客户端</li>
</ol>
<p>此工作流示意如下图：</p>
<p><img class="aligncenter size-large wp-image-15974" src="https://blog.gmem.cc/wp-content/uploads/2017/08/rtsp-gw-1024x366.png" alt="rtsp-gw" width="710" height="253" /></p>
<div class="blog_h3"><span class="graybg">关于H.264</span></div>
<p>在Chrome中WebRTC使用的视频编码格式一直是VP8/VP9，直到Chrome 50才支持H264。你可以使用标记enable-webrtc-h264-with-openh264-ffmpeg打开H264支持（最新的Chrome 61此标记默认是打开的）。</p>
<p>H264被微软Edge的ORTC、Firefox、移动设备、遗留视频系统支持。移动设备大部分支持H264硬件解码，这意味着播放视频不会过于消耗电池，这一点很关键。</p>
<p>目前的情况并不乐观，主要是不同系统对于H.264的支持程度不同，它们可能支持不兼容的Profile，因而存在互操作性问题。</p>
<div class="blog_h3"><span class="graybg"><a id="interoperability"></a>通信互操作性</span></div>
<p>WebRTC协议栈使用SAVPF这一RTP profile， 其含义是针对基于RTCP的反馈的扩展安全RTP profile（Extended Secure RTP Profile for Real-time Transport Control Protocol Based Feedback），SAVPF主要包括两个RTP profile：</p>
<ol>
<li>SAVP：AVP的基础上包含安全特性</li>
<li>AVPF：用于及时的向媒体流的发送者反馈信息</li>
</ol>
<p>SAVPF的意义在于，提供安全RTP通信的基础上支持反馈。WebRTC客户端会向WebRTC网关发送反馈信息（在RTCP包中），通知网关可能影响到媒体质量的网络状况。</p>
<p>大多数IP摄像头仅仅支持AVP，这意味着，网关无法把WebRTC的反馈传递给IP摄像头。网关必须自己管理好反馈信息，或者用行话说，网关必须终结（terminate）RTCP反馈。</p>
<p>这一点很重要，如果网关没有正确处理反馈，WebRTC客户端可能出现严重的QoS问题，通常是视频画面卡死。卡死的具体原因是：</p>
<ol>
<li>PLI（画面丢失提示，Picture Loss Indication）反馈：如果此反馈没有被网关正确处理，只要出现丢包，画面可能随机的卡死。这和VP8编码器的工作机制有关。VP8允许长时间没有关键帧生成（以分钟计），当PLI出现后网关应该立即生成新的关键帧，否则直到下一次关键帧（周期性的）到达，客户端都无法解码。某些网关的解决方式是，频繁的生成关键帧，这种做法的劣势是大量消耗带宽，导致视频质量差</li>
<li>REMB（接收者估算的最大比特率， Receiver Estimated Maximum Bitrate）反馈：如果网关没有处理此反馈，且没有任何拥塞控制机制，则网关就不可能指示VP8编码器降低比特率。这样随着接入的客户端便多，网络带宽不够用后，视频质量变差</li>
</ol>
<div class="blog_h3"><span class="graybg">Kurento中接入RTSP</span></div>
<p>将Kurento作为WebRTC网关时，上述互操作性问题已经被解决，你需要了解以下三点：</p>
<ol>
<li>PlayerEndpoint这个端点支持从各种各样的源读取视频流，这些源可以是RTSP/RTP、HTTP/MJPEG。这意味着PlayerEndpoint有能力从IP摄像头读取码流</li>
<li>WebRtcEndpoint这个端点支持完整的WebRTC协议栈，能够正确处理RTCP反馈：
<ol>
<li>每当PLI包被收到，WebRtcEndpoint会命令VP8编码器立即生成一个新的关键帧</li>
<li>内置了拥塞控制，且响应REMB包。必要时命令VP8编码器降低比特率</li>
</ol>
</li>
<li>不可知媒体特性：当两个不兼容的媒体元素连接在一起时，Kurento会自动进行编码格式转换。也就是说H.264/MJPEG到VP8的转码会自动发生，不需要开发人员干预</li>
</ol>
<p>RTSP到WebRTC的媒体管线示意如下：</p>
<p><img class="aligncenter size-large wp-image-15978" src="https://blog.gmem.cc/wp-content/uploads/2017/08/rtsp-kurento-1024x366.png" alt="rtsp-kurento" width="710" height="253" /></p>
<div class="blog_h3"><span class="graybg">单画面接入代码</span></div>
<p>客户端代码：</p>
<pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;WebRTC Video Surveillance - RTSP Preview&lt;/title&gt;
    &lt;script src="js/stomp.js"&gt;&lt;/script&gt;
    &lt;script src="js/stomp-wrapper.js"&gt;&lt;/script&gt;
    &lt;script src="js/adapter.js"&gt;&lt;/script&gt;
    &lt;script src="js/kurento-utils.js"&gt;&lt;/script&gt;
    &lt;script src="js/webrtc-endpoint.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body onunload="endpoint.dispose();"&gt;
&lt;div&gt;
    &lt;video id="remoteVideo" autoplay width="427px" height="240px"&gt;&lt;/video&gt;
&lt;/div&gt;
&lt;script&gt;
    let endpoint = new WebRTCEndpoint( WebRTCEndpoint.MODE_RECV, {
        url: 'ws://172.21.0.1:9090/signal',
        namespace: '/rtsp/preview',
        login: new Date().getTime(),
        remoteVideo: document.getElementById( 'remoteVideo' )
    } );
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>服务器代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

import javax.inject.Inject;
import java.security.Principal;

@Controller
@MessageMapping( RtspPreviewController.NAMESPACE )
public class RtspPreviewController {

    private static final Logger LOGGER = LoggerFactory.getLogger( RtspPreviewController.class );

    public static final String NAMESPACE = "/rtsp/preview";

    @Inject
    private KurentoService kurento;

    @MessageMapping( "/icecandidate" )
    public void onIceCandidate( IceCandidate candidate, @Header MediaSession session ) {
        WebRtcEndpoint endpoint = (WebRtcEndpoint) session.getEndpoint();
        session.addIceCandidate( candidate );
        return;
    }

    @MessageMapping( "/stop" )
    public void onStop( @Header MediaSession session ) {
        session.getEndpoint().release();
        session.getPipeline().release();
    }

    @MessageMapping( "/sdpoffer" )
    public void onSdpOffer( String sdpoffer, Principal principal, @Header MediaSession session ) {

        MediaPipeline pipeline = kurento.createMediaPipeline();
        session.setPipeline( pipeline );

        PlayerEndpoint.Builder peb = new PlayerEndpoint.Builder( pipeline, "rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream" );
        PlayerEndpoint playerEndpoint = peb.build();
        playerEndpoint.addMediaFlowInStateChangeListener( e -&gt; {
            LOGGER.info( "RTSP input flow state changed, media type: {}, media state: {}", e.getMediaType(), e.getState() );
        } );

        WebRtcEndpoint webRtcEndpoint = kurento.createWebRtcEndpoint( pipeline, sdpoffer, principal.getName(), NAMESPACE );
        session.setEndpoint( webRtcEndpoint );

        playerEndpoint.connect( webRtcEndpoint );
        playerEndpoint.play();
    }
}</pre>
<div class="blog_h3"><span class="graybg">多画面接入代码</span></div>
<p>没有什么本质区别，只有一些技术上的细节需要处理：</p>
<pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;WebRTC Video Surveillance - RTSP Preview&lt;/title&gt;
    &lt;script src="js/stomp.js"&gt;&lt;/script&gt;
    &lt;script src="js/stomp-wrapper.js"&gt;&lt;/script&gt;
    &lt;script src="js/adapter.js"&gt;&lt;/script&gt;
    &lt;script src="js/kurento-utils.js"&gt;&lt;/script&gt;
    &lt;script src="js/webrtc-endpoint.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        body {
            background-color: #02686c;
        }

        .nvbar {
            font-family: sans-serif;
            height: 30px;
            display: flex;
        }

        .title {
            font-size: 18px;
            margin-left: 12px;
            font-weight: bold;
            color: rgba(255, 255, 255, .8);
            text-shadow: 2px 2px 1px rgba(0, 0, 0, .8);
            line-height: 20px;
            vertical-align: text-bottom;
        }

        .subtitle {
            font-size: 14px;
            font-weight: bold;
            font-style: italic;
            margin-left: 24px;
            color: rgba(255, 255, 255, .5);
            line-height: 20px;
            vertical-align: text-bottom;
        }

        #videos {
            display: flex;
            flex-wrap: wrap;
        }

        #videos video {
            margin: 3px;
        }

        video {
            border-radius: 5px;
            border: 6px solid rgba(0, 0, 0, .5);
        }

        video:hover {
            border: 6px solid rgba(163, 163, 163, 0.5);
            cursor: pointer;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body onunload="dispose();"&gt;
&lt;div class="nvbar"&gt;
    &lt;div class="title"&gt;基于WebRTC+Kurento的视频监控示例&lt;/div&gt;
    &lt;div class="subtitle"&gt;http://172.21.0.1:9090/rtsp-preview.html&lt;/div&gt;
&lt;/div&gt;
&lt;div id="videos"&gt;&lt;/div&gt;

&lt;script&gt;
    let endpoints = [];
    let videos = document.getElementById( 'videos' );
    for ( let ch = 1; ch &lt;= 21; ch++ ) {
        let video = document.createElement( 'video' );
        video.autoplay = true;
        video.width = 352 / 2;
        video.height = 288 / 2;
        videos.appendChild( video );
        let endpoint = new WebRTCEndpoint( WebRTCEndpoint.MODE_RECV, {
            url: 'ws://172.21.0.1:9090/signal',
            namespace: '/rtsp/preview/ch1',
            // 因为执行速度太高，之前的new Date().getTime()两次调用会重复
            login: Math.random(),
            remoteVideo: video
        } );
        endpoints.push( endpoint );

    }
    function dispose() {
        endpoints.forEach( e =&gt; e.dispose() );
    }
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>服务器器端，每个视频通道使用一个名字空间（STOMP目的地中缀）： </p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;

import javax.inject.Inject;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Controller
@MessageMapping( RtspPreviewController.NSP )
public class RtspPreviewController {

    private static final Logger LOGGER = LoggerFactory.getLogger( RtspPreviewController.class );

    public static final String NSP = "/rtsp/preview";

    @Inject
    private KurentoService kurento;

    private Map&lt;String, MediaPipeline&gt; mediaPipelines = new ConcurrentHashMap&lt;&gt;();

    @MessageMapping( "/{ch}/icecandidate" )
    public void onIceCandidate( @DestinationVariable String ch, IceCandidate candidate, @Header MediaSession session ) {
        WebRtcEndpoint endpoint = (WebRtcEndpoint) session.getEndpoint();
        session.addIceCandidate( candidate );
        return;
    }

    @MessageMapping( "/{ch}/stop" )
    public void onStop( @DestinationVariable String ch, @Header MediaSession session ) {
        Endpoint endpoint = session.getEndpoint();
        // 获取连接到当前端点SINK的那些连接，注意，PlayerEndpoint会创建三个连接过来，分别用于AUDIO、VEDIO、DATA
        endpoint.getSourceConnections().forEach( data -&gt; {
            MediaElement source = data.getSource();
            MediaElement sink = data.getSink();
            source.disconnect( sink );
        } );
        endpoint.release();

    }

    @MessageMapping( "/{ch}/sdpoffer" )
    public void onSdpOffer( @DestinationVariable String ch, String sdpoffer, Principal principal, @Header MediaSession session ) {
        // 媒体管线现在不是当前会话独占了，而是每个通道一个
        PlayerEndpoint playerEndpoint = getPlayerEndpoint( ch );
        MediaPipeline pipeline = playerEndpoint.getMediaPipeline();
        session.setPipeline( pipeline );

        WebRtcEndpoint webRtcEndpoint = kurento.createWebRtcEndpoint( pipeline, sdpoffer, principal.getName(), NSP + '/' + ch );
        session.setEndpoint( webRtcEndpoint );

        playerEndpoint.connect( webRtcEndpoint );
    }

    private synchronized PlayerEndpoint getPlayerEndpoint( String ch ) {
        MediaPipeline pipeline = getMediaPipline( ch );
        if ( pipeline == null ) {
            pipeline = kurento.createMediaPipeline();
            mediaPipelines.put( ch, pipeline );
            PlayerEndpoint.Builder peb = new PlayerEndpoint.Builder( pipeline, getRtspUrlFor( ch ) );
            PlayerEndpoint playerEndpoint = peb.build();
            playerEndpoint.addMediaFlowInStateChangeListener( e -&gt; {
                LOGGER.info( "RTSP input flow state changed, media type: {}, media state: {}", e.getMediaType(), e.getState() );
            } );
            playerEndpoint.play();
            return playerEndpoint;
        } else {
            PlayerEndpoint playerEndpoint = null;
            for ( MediaObject mo : pipeline.getChildren() ) {
                if ( mo instanceof PlayerEndpoint ) {
                    playerEndpoint = (PlayerEndpoint) mo;
                }
            }
            return playerEndpoint;
        }
    }

    private MediaPipeline getMediaPipline( String ch ) {
        return mediaPipelines.get( ch );
    }

    /**
     * 获取指定通道的RTSP URL
     *
     * @param ch 通道号
     * @return
     */
    private String getRtspUrlFor( String ch ) {
        return "返回此通道的RTSP地址";
    }
} </pre>
<p>即使开到21画面，客户端运行仍然非常流畅（ i7-4940MX ），完全可以满足视频监控领域的多画面需求：</p>
<p><img class="aligncenter size-large wp-image-15995" src="https://blog.gmem.cc/wp-content/uploads/2017/08/rtsp-21scr-1024x388.png" alt="rtsp-21scr" width="710" height="269" /></p>
<div class="blog_h2"><span class="graybg">强制H264</span></div>
<p>前面我们已经提到过，Chrome 61默认已经开启了H264支持，其它很多浏览器也支持H264。如果KMS不进行转码，则对服务器配置要求可以大大降低。</p>
<p>首先，为Kurento安装插件：</p>
<pre class="crayon-plain-tag">apt install openh264-gst-plugins-bad-1.5</pre>
<p>要强制KMS仅仅使用H264，可以修改KMS配置文件，注释掉VP8的支持：</p>
<pre class="crayon-plain-tag">"videoCodecs" : [
    {
      "name" : "H264/90000"
    }
]</pre>
<p>注意：</p>
<ol>
<li>一定要确保你的客户端都支持基于H264的WebRTC视频传输，才可以进行上述修改</li>
<li>进行上述修改后，如果客户端不支持H264，那么SDP应答将会不完整，缺少媒体格式说明：<br />
<pre class="crayon-plain-tag">m=video 0 UDP/TLS/RTP/SAVPF    # 后面缺少媒体格式代码</pre></p>
<p>这会导致客户端WebRTC报错：</p>
<pre class="crayon-plain-tag">Failed to parse SessionDescription. m=video 0 UDP/TLS/RTP/SAVPF Expects at least 4 fields.</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">验证</span></div>
<p>通过SDP Offer/Answer可以查看浏览器和KMS协商使用H.264作为视频编码方式。
<p>在浏览器地址栏输入<pre class="crayon-plain-tag">chrome://webrtc-internals</pre>，搜索ssrc_，会发现两个匹配项，其中一个和视频相关。可以看到mediaType为video，codecImplementationName为FFmpeg， googCodecName为H264。</p>
<div class="blog_h3"><span class="graybg">问题</span></div>
<p>在公司的环境下测试，如果使用子码流的话，Chrome 61、Firefox都可以正常多画面播放。</p>
<p>但是采用主码流的情况下，运行效果实在太差：花屏、周期性卡死：</p>
<p><img class="aligncenter size-full wp-image-16100" src="https://blog.gmem.cc/wp-content/uploads/2017/08/webrtc-chrome61-h264.png" alt="webrtc-chrome61-h264" width="100%" /></p>
<p>打开chrome://webrtc-internals可以看到帧经常无法解析（framesDecoded计数不增加）。具体原因还需要深入研究，但我估计可能的相关因素有：</p>
<ol>
<li>根据SDP，摄像头的H.264 Profile是420029，即Baseline；而Chrome支持的H.264 Profile是42e01f，即Constrained Baseline。也就是两者的Profile不兼容。这导致Kurento需要进行转码</li>
<li>如果进行SDP伪造，让Kurento相信Chrome支持420029，则完全无法播放。这意味着Chrome可能的确无法解码420029</li>
<li>转码工作依赖GST插件openh264-gst-plugins-bad-1.5完成，此插件可能存在质量问题</li>
</ol>
<p><em>9月19日更新：</em></p>
<p>被摄像头给骗了……SDP声称的H.264 Profile和它实际使用的Profile并不一致。默认情况下这款摄像头使用的H.264 Profile为Main，<a href="/research-on-html5-video-surveillance#hk-av-config">手工配置</a>之后则可以使用Baseline。</p>
<p>不过，就算改成Baseline，和Chrome/Firefox支持的Constrained Baseline仍然不兼容（注意：实际上很多编码器不使用Baseline特性针对Constrained Baseline的差集，也就是说两个Profile的编码结果很可能是兼容的）。进行SDP伪造的话，播放花屏、很快卡死。 </p>
<div class="blog_h2"><span class="graybg">服务器端多画面合成</span></div>
<p>很多情况下，监控客户端都开启重要视频通道构成的固定多画面监控。这种情况下，可以考虑在流媒体服务器端把多画面合成GRID（例如四画面、九画面），好处是：</p>
<ol>
<li>降低客户端解码压力</li>
<li>降低通信复杂度，不需要开启多个媒体连接甚至信号连接了</li>
</ol>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">术语列表</span></div>
<div class="blog_h2"><span class="graybg">WebRTC术语</span></div>
<p>更多WebRTC术语参考<a href="https://webrtcglossary.com/">webrtcglossary</a>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ICE</td>
<td>
<p>交互式连接建立（Interactive Connectivity Establishment）是WebRTC进行<a href="/network-faq#traversal">NAT穿透</a>的标准协议，由<a href="https://tools.ietf.org/html/rfc5245">IETF RFC 5245</a>定义。取决于candidate，ICE可能尝试直连、STUN、TURN —— ICE负责协调这三种底层连接机制</p>
<p>ICE通过指导连接性检测，来处理基于NATs的的媒体流连接。ICE收集所有可用的候选（candidate，可供Peer连接的地址信息）：</p>
<ol>
<li>对于STUN来说是本地IP地址、反射（reflexive）地址</li>
<li>对于TURN来说是中继地址</li>
</ol>
<p>所有收集到的candidate通过SDP发送给Peer</p>
<p>一旦WebRTC收集流自己的、Peer的所有ICE地址之后，它就开始初始化连接性测试，逐个通过candidate发送媒体流直到成功</p>
<p>使用ICE的缺点是，会引入延迟（可能高达10s），新协议Trickle ICE用于解决此问题</p>
</td>
</tr>
<tr>
<td>ICE-TCP</td>
<td>
<p>通过TCP而不是TURN来发送媒体流的机制，Chrome支持</p>
</td>
</tr>
<tr>
<td>MCU</td>
<td>
<p>多点会议单元（Multipoint Conferencing Unit）</p>
<p>jsonMessage.get("candidate")这种设备提供了在单个视频/音频会话中，连接很多参与者的能力。MCU通常都实现了Mixing架构，因而每个会话都需要消耗很多计算资源</p>
</td>
</tr>
<tr>
<td>Mixing</td>
<td>
<p>一种多点通信架构，每个参与者发送自己的媒体流到中心服务器，并从中心服务器接收混合后的单个媒体流。实现此架构的服务器称为MCU。此架构的：</p>
<ol>
<li>优势：对客户端要求低，客户端需要一个点对点连接</li>
<li>劣势：资源消耗高，因为服务器需要解码、布局、重新编码它接收到的媒体流</li>
</ol>
</td>
</tr>
<tr>
<td>SDP</td>
<td>
<p>会话描述协议（Session Description Protocol），WebRTC使用该协议来协商会话的参数，但是WebRTC不负责信号处理，因而SDP的创建和传输需要应用程序自己完成</p>
</td>
</tr>
<tr>
<td> SFU</td>
<td>
<p>选择性转发单元（Selective Forwarding Unit），有时用于描述一种视频路由设备，有时则用来描述一种路由特性</p>
<p>SFU能够接收多个媒体流，然后决定将其中的哪些流转发给哪些参与者</p>
</td>
</tr>
<tr>
<td>SIP</td>
<td>
<p>会话初始化协议（Session Initiation Protocol），一个在VoIP领域（电信行业）广泛使用的信号处理协议</p>
</td>
</tr>
<tr>
<td><a id="glossary-stun"></a>STUN</td>
<td>
<p>NAT用会话穿透工具（Session Traversal Utilities for NAT） 是WebRTC进行NAT穿透的标准方法</p>
<p>STUN的核心目的是，探测客户端的公共地址/端口：</p>
<ol>
<li>客户端发送STUN请求到服务器</li>
<li>服务器返回请求来自的公共地址信息</li>
<li>客户端通过SDP与Peer分析自己的公共地址信息</li>
</ol>
</td>
</tr>
<tr>
<td>Trickle ICE</td>
<td>
<p>对 ICE的优化。ICE的主要瓶颈是初始化连接性检测比较耗时，Trickle ICE通过并行尝试多种底层机制，以加速candidate的获取。一旦某个candidate可用客户端就可以立即进行下一步，不需要等待所有candidate</p>
</td>
</tr>
<tr>
<td>TURN</td>
<td>
<p>基于中继的NAT穿透（Traversal Using Relays around NAT）是WebRTC进行NAT穿透的标准方法</p>
<p>当STUN不可用的情况下，TURN基于TURN服务器中继所有媒体流，这可能导致昂贵的流量和CPU开销</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">如何循环播放</span></div>
<p>注册监听器，当流结束后，重新调用play()：</p>
<pre class="crayon-plain-tag">playerEndpoint.addEndOfStreamListener( e -&gt; {
    playerEndpoint.play();
} );</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/webrtc-server-basedon-kurento">基于Kurento搭建WebRTC服务器</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/webrtc-server-basedon-kurento/feed</wfw:commentRss>
		<slash:comments>31</slash:comments>
		</item>
		<item>
		<title>HTML5视频监控技术预研</title>
		<link>https://blog.gmem.cc/research-on-html5-video-surveillance</link>
		<comments>https://blog.gmem.cc/research-on-html5-video-surveillance#comments</comments>
		<pubDate>Mon, 28 Aug 2017 05:49:57 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Graphic]]></category>
		<category><![CDATA[HTML]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JSMpeg]]></category>
		<category><![CDATA[MSE]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[视频监控]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15526</guid>
		<description><![CDATA[<p>引言 安防类项目中通常都有视频监控方面的需求。视频监控客户端主要是Native应用的形式，在Web端需要利用NPAPI、ActiveX之类的插件技术实现。 但是，IE式微，Chrome也放弃了NPAPI，另一方面，监控设备硬件厂商的视频输出格式则逐渐标准化。这让基于开放、标准化接口的Web视频监控成为可能。 本文讨论以HTML5及其衍生技术为基础的B/S架构实时视频监控解决方案。主要包括两方面的内容： 视频编码、流媒体基础知识，以及相关的库、框架的介绍 介绍可以用于视频监控的HTML5特性，例如媒体标签、MSE、WebRTC，以及相关的库、框架 本文仅仅简介若干种备选的解决方案，本站其它文章进行了更加深入的探讨： H.264学习笔记 实时通信协议族 基于Kurento搭建WebRTC服务器 基于Broadway的HTML5视频监控 音视频编码 音频、视频的编码（Codec，压缩）算法有很多，不同浏览器对音视频的编码算法的支持有差异。H264这样的监控设备常用的视频编码格式，主流浏览器都有某种程度的支持。 常见的音频编码算法包括： MP3, Vorbis, AAC；常见的视频编码算法包括： H.264, HEVC, VP8, VP9。 编码后的音频、视频通常被封装在一个比特流容器格式（container）中，这些格式中常见的有： <a class="read-more" href="https://blog.gmem.cc/research-on-html5-video-surveillance">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/research-on-html5-video-surveillance">HTML5视频监控技术预研</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">引言</span></div>
<p>安防类项目中通常都有视频监控方面的需求。视频监控客户端主要是Native应用的形式，在Web端需要利用NPAPI、ActiveX之类的插件技术实现。</p>
<p>但是，IE式微，Chrome也放弃了NPAPI，另一方面，监控设备硬件厂商的视频输出格式则逐渐标准化。这让基于开放、标准化接口的Web视频监控成为可能。</p>
<p>本文讨论以HTML5及其衍生技术为基础的B/S架构实时视频监控解决方案。主要包括两方面的内容：</p>
<ol>
<li>视频编码、流媒体基础知识，以及相关的库、框架的介绍</li>
<li>介绍可以用于视频监控的HTML5特性，例如媒体标签、MSE、WebRTC，以及相关的库、框架</li>
</ol>
<p>本文仅仅简介若干种备选的解决方案，本站其它文章进行了更加深入的探讨：</p>
<ol>
<li><a href="/h264-study-note">H.264学习笔记</a></li>
<li><a href="/realtime-communication-protocols">实时通信协议族</a></li>
<li><a href="/webrtc-server-basedon-kurento">基于Kurento搭建WebRTC服务器</a></li>
<li><a href="/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a></li>
</ol>
<div class="blog_h1"><span class="graybg">音视频编码</span></div>
<p>音频、视频的编码（Codec，压缩）算法有很多，不同浏览器对音视频的编码算法的支持有<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility">差异</a>。H264这样的监控设备常用的视频编码格式，主流浏览器都有某种程度的支持。</p>
<p>常见的音频编码算法包括： MP3, Vorbis, AAC；常见的视频编码算法包括： H.264, HEVC, VP8, VP9。</p>
<p>编码后的音频、视频通常被封装在一个比特流容器格式（container）中，这些格式中常见的有： MP4, FLV, WebM,  ASF, ISMA等。</p>
<div class="blog_h2"><span class="graybg">JSMpeg</span></div>
<p>视频解码工作通常由浏览器本身负责，配合video实现视频播放。</p>
<p>现代浏览器的JS引擎性能较好，因此出现了纯粹由JS实现的解码器<a href="https://github.com/phoboslab/jsmpeg">JSMpeg</a>，它能够解码视频格式MPEG1、音频格式MP2。支持通过Ajax加载静态视频文件，支持低延迟（小于50ms）的流式播放（通过WebSocket）。JSMpeg包括以下组件：</p>
<ol>
<li>MPEG-TS分流器（demuxer）。muxer负责把视频、音频、字幕打包成一种容器格式，demuxer则作相反的工作</li>
<li>MPEG1视频解码器</li>
<li>MP2音频解码器</li>
<li>WebGL渲染器、Canvas2D渲染器</li>
<li>WebAudio音频输出组件</li>
</ol>
<p>JSMpeg的优势在于兼容性好，几乎所有现代浏览器都能运行JSMpeg。</p>
<div class="blog_h3"><span class="graybg">性能</span></div>
<p>JSMpeg不能使用硬件加速。在iPhone 5S这样的设备上，JSMpeg能够处理720p@30fps视频。</p>
<p>比起现代解码器，MPEG1压缩率较低，因而需要更大的带宽。720p的视频大概占用250KB/s的带宽。</p>
<div class="blog_h3"><span class="graybg">示例</span></div>
<p>下面我们尝试利用ffmpeg编码本地摄像头视频，并通过JSMpeg播放。</p>
<p>创建一个NPM项目，安装依赖：</p>
<pre class="crayon-plain-tag">npm install jsmpeg --save
npm install ws --save</pre>
<p>JSMpeg提供了一个中继器，能够把基于HTTP的MPEG-TS流转换后通过WebSocket发送给客户端。此脚本需要<a href="https://github.com/phoboslab/jsmpeg/blob/master/websocket-relay.js">到Github下载</a>。 下面的命令启动一个中继器：</p>
<pre class="crayon-plain-tag">node ./app/websocket-relay.js 12345 8800 8801
# Listening for incomming MPEG-TS Stream on http://127.0.0.1:8800/&lt;secret&gt;
# Awaiting WebSocket connections on ws://127.0.0.1:8801/
# 实际上在所有网络接口上监听，并非仅仅loopback</pre>
<p>下面的命令捕获本地摄像头（Linux），并编码为MPEG1格式，然后发送到中继器：</p>
<pre class="crayon-plain-tag"># 从摄像头/dev/video0以480的分辨率捕获原始视频流
ffmpeg -s 640x480 -f video4linux2 -i /dev/video0 \
       # 输出为原始MPEG-1视频（JSMpeg可用），帧率30fps，比特率800kbps
       -f mpegts -codec:v mpeg1video -b 800k -r 30 http://127.0.0.1:8800/12345
# 在我的机器上，上述ffmpeg私有内存占用18MB</pre>
<p>上述命令执行后，中继器控制台上打印：</p>
<pre class="crayon-plain-tag">Stream Connected: ::ffff:127.0.0.1:42399</pre>
<p>客户端代码：</p>
<pre class="crayon-plain-tag">var player = new JSMpeg.Player( 'ws://127.0.0.1:8801/', {
    canvas: document.getElementById( 'canvas' ),
    autoplay: true
} ); </pre>
<div class="blog_h2"><span class="graybg">Broadway</span></div>
<p><a href="https://github.com/mbebenita/Broadway">Broadway</a>是一个基于JavaScript的H.264解码器，其源码来自于Android的H.264解码器，利用Emscripten转译成了JavaScript，之后利用Google的Closure编译器优化，并针对WebGL进一步优化。</p>
<p>注意：Broadway仅仅支持Baseline这个H.264 Profile。</p>
<p><a href="https://github.com/131/h264-live-player">h264-live-player</a>是基于Broadway实现的播放器，允许通过WebSocket来传输NAL单元（原始H.264帧），并在画布上渲染。我们运行一下它的示例应用：</p>
<pre class="crayon-plain-tag">git clone https://github.com/131/h264-live-player.git
cd h264-live-player
npm install</pre>
<p>因为我的机器是Linux，所以修改h264-live-player/lib/ffmpeg.js， 把ffpmeg的参数改为：</p>
<pre class="crayon-plain-tag">var args = [
    "-f", "video4linux2",
    "-i",  "/dev/video0" ,
    "-framerate", this.options.fps,
    "-video_size", this.options.width + 'x' + this.options.height,
    '-pix_fmt',  'yuv420p',
    '-c:v',  'libx264',
    '-b:v', '600k',
    '-bufsize', '600k',
    '-vprofile', 'baseline',
    '-tune', 'zerolatency',
    '-f' ,'rawvideo',
    '-'
];</pre>
<p>然后运行<pre class="crayon-plain-tag">node server-ffmpeg</pre>，打开http://127.0.0.1:8080/，可以看到自己摄像头传来的H.264码流，效果还不错。</p>
<div class="blog_h2"><span class="graybg">服务器端技术</span></div>
<div class="blog_h3"><span class="graybg">ffpmeg</span></div>
<p>老牌的编解码库，支持很多的音频、视频格式的编解码，支持多种容器格式，支持多种流协议。关于ffpmeg的详细介绍参见<a href="/linux-command-faq#ffmpeg">Linux命令知识集锦</a>。</p>
<p>ffpmeg除了提供开发套件之外，还有一个同名的命令行工具，直接使用它就可以完成很多编解码、流转换的工作。</p>
<p>类似的库是libav，ffpmeg和它的功能非常相似，特性更多一些。</p>
<div class="blog_h3"><span class="graybg">x264</span></div>
<p>官网自称是最好的H.264编码器。特性包括：</p>
<ol>
<li>提供一流的性能、压缩比。特别是性能方面，可以在普通PC上并行编码4路或者更多的1080P流</li>
<li>提供最好的视频质量，具有最高级的心理视觉优化</li>
<li>支持多种不同应用程序所需要的特性，例如电视广播、蓝光低延迟视频应用、Web视频</li>
</ol>
<div class="blog_h1"><span class="graybg">流媒体技术</span></div>
<p>有了上面介绍的HTML5标签、合理编码的视频格式，就可以实现简单的监控录像回放了。但是，要进行实时监控画面预览则没有这么简单，必须依赖流媒体技术实现。</p>
<div class="blog_h2"><span class="graybg">流媒体</span></div>
<p>所谓多媒体（Multimedia）是指多种内容形式 —— 文本、音频、视频、图片、动画等的组合。</p>
<p>所谓流媒体，就是指源源不断的由提供者产生，并持续的被终端用户接收、展示的多媒体，就像水流一样。现实世界中的媒体，有些天生就是流式的，例如电视、广播，另外一些则不是，例如书籍、CD。</p>
<p>流媒体技术（从传递媒体角度来看）可以作为文件下载的替代品。</p>
<p><span style="background-color: #c0c0c0;">流媒体技术关注的是如何传递媒体，而不是如何编码媒体</span>，具体的实现就是各种流媒体协议。封装后的媒体比特流（容器格式）由流媒体服务器递送到流媒体客户端。流媒体协议可能对底层容器格式、编码格式有要求，也可能没有任何要求。</p>
<div class="blog_h2"><span class="graybg">直播</span></div>
<p>直播流（Live streaming）和静态文件播放的关键差异：</p>
<ol>
<li>点播的目标文件通常位于服务器上，具有一定的播放时长、文件大小。浏览器可以使用渐进式下载，一边下载一边播放</li>
<li>直播不存在播放起点、终点。它表现为一种流的形式，源源不断的从视频采集源通过服务器，传递到客户端</li>
<li>直播流通常是自适应的（adaptive），其码率随着客户端可用带宽的变化，可能变大、变小，以尽可能消除延迟</li>
</ol>
<p>流媒体技术不但可以用于监控画面预览，也可以改善录像播放的用户体验，比起简单的静态文件回放，流式回放具有以下优势：</p>
<ol>
<li>延迟相对较低，播放能够尽快开始</li>
<li>自适应流可以避免卡顿</li>
</ol>
<div class="blog_h2"><span class="graybg">流协议</span></div>
<p>主流的用于承载视频流的流媒体协议包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 18%; text-align: center;">协议</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>HLS</td>
<td>
<p>HTTP实时流（HTTP Live Streaming），由苹果开发，基于HTTP协议</p>
<p>HLS的工作原理是，把整个流划分成一个个较小的文件，客户端在建立流媒体会话后，基于HTTP协议下载流片段并播放。客户端可以从多个服务器（源）下载流。</p>
<p>在建立会话时，客户端需要下载extended M3U (m3u8) 播放列表文件，其中包含了MPEG-2 TS（Transport Stream）容器格式的视频的列表。在播放完列表中的文件后，需要再次下载<span style="color: #444444;">m3u8，如此循环</span></p>
<p>此协议在移动平台上支持较好，目前的Android、iOS版本都支持</p>
<p>此协议的重要缺点是高延迟（5s以上通常），要做到低延迟会导致频繁的缓冲（下载新片段）并对服务器造成压力，不适合视频监控</p>
<p>播放HLS流的HTML代码片段：</p>
<pre class="crayon-plain-tag">&lt;video src="http://movie.m3u8" height="329" width="480"&gt;&lt;/video&gt;</pre>
</td>
</tr>
<tr>
<td>RTMP</td>
<td>
<p>实时消息协议（Real Time Messaging Protocol），由Macromedia（Adobe）开发。此协议实时性很好，需要Flash插件才能在客户端使用，但是Adobe已经打算在不久的将来放弃对Flash的支持了
<p>有一个开源项目<a href="https://github.com/Bilibili/flv.js">HTML5 FLV Player</a>，它支持在没有Flash插件的情况下，播放Flash的视频格式FLV。此项目依赖于<a href="https://w3c.github.io/media-source/">MSE</a>，支持以下特性：</p>
<ol>
<li>支持H.264 + AAC/MP3编码的FLV容器格式的播放</li>
<li>分段（segmented）视频播放</li>
<li>基于HTTP的FLV低延迟实时流播放</li>
<li>兼容主流浏览器</li>
<li>资源占用低，可以使用客户端的硬件加速</li>
</ol>
</td>
</tr>
<tr>
<td>RTSP</td>
<td>
<p>实时流协议（Real Time Streaming Protocol），由RealNetworks等公司开发。此协议负责控制通信端点（Endpoint）之间的媒体会话（media sessions） —— 例如播放、暂停、录制。通常需要结合：实时传输协议（Real-time Transport Protocol）、实时控制协议（Real-time Control Protocol）来实现视频流本身的传递</p>
<p>大部分浏览器没有对RTSP提供原生的支持</p>
<p>RTSP 2.0版本目前正在开发中，和旧版本不兼容</p>
</td>
</tr>
<tr>
<td>MPEG-DASH</td>
<td>
<p>基于HTTP的动态自适应流（Dynamic Adaptive Streaming over HTTP），它类似于HLS，也是把流切分为很小的片段。DASH为支持为每个片段提供多种码率的版本，以满足不同客户带宽</p>
<p>协议的客户端根据自己的可用带宽，选择尽可能高（避免卡顿、重新缓冲）的码率进行播放，并根据网络状况实时调整码率</p>
<p>DASH不限制编码方式，你可以使用H.265, H.264, VP9等视频编码算法</p>
<p>Chrome 24+、Firefox 32+、Chrome for Android、IE 10+支持此格式</p>
<p>类似于HLS的高延迟问题也存在</p>
</td>
</tr>
<tr>
<td>WebRTC</td>
<td>
<p>WebRTC是一整套API，为浏览器、移动应用提供实时通信（RealTime Communications）能力。它包含了流媒体协议的功能，但是不是以协议的方式暴露给开发者的</p>
<p>WebRTC支持Chrome 23+、Firefox 22+、Chrome for Android，提供Java / Objective-C绑定</p>
<p>WebRTC主要有三个职责：</p>
<ol>
<li>捕获客户端音视频，对应接口MediaStream（也就是getUserMedia）</li>
<li>音视频传输，对应接口RTCPeerConnection</li>
<li>任意数据传输，对应接口RTCDataChannel</li>
</ol>
<p>WebRTC内置了点对点的支持，也就是说流不一定需要经过服务器中转</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">服务器端技术</span></div>
<p>视频监控通常都是CS模式（而非P2P），在服务器端，你需要部署流媒体服务。</p>
<div class="blog_h3"><span class="graybg">GStreamer</span></div>
<p><a href="https://gstreamer.freedesktop.org/">这是</a>一个开源的跨平台多媒体框架。通过它你可以构建各种各样的媒体处理组件，包括流媒体组件。通过插件机制，GStreamer支持上百种编码格式，包括MPEG-1, MPEG-2, MPEG-4, H.261, H.263, H.264, RealVideo, MP3, WMV, FLV</p>
<p><a href="https://www.kurento.org/">Kurento</a>、<a href="http://www.flumotion.net/features/">Flumotion</a>是基于GStreamer构建的流媒体服务器软件。</p>
<div class="blog_h3"><span class="graybg">Live555</span></div>
<p><a href="http://www.live555.com/">Live555</a>是流媒体服务开发的基础库，支持 RTP/RTCP/RTSP/SIP等协议，适合在硬件资源受限的情况下使用（例如嵌入式设备）。</p>
<p>基于Live555的软件包括：</p>
<ol>
<li>Live555媒体服务器，完整的RTSP服务器</li>
<li>openRTSP，一个命令行程序，支持提供RTSP流、接收RTSP流、把RTSP流中的媒体录像到磁盘</li>
<li>playSIP，可以进行VoIP通话</li>
<li>liveCaster，支持组播的MP3流媒体服务</li>
</ol>
<div class="blog_h3"><span class="graybg">其它</span></div>
<p>流媒体服务实现有很多，它们中的一些在最初针对特定的流协议，大部分都走向多元化。例如，Red5是一个RTMP流媒体服务器，Wowza是一个综合的流媒体服务器，支持WebRTC的流媒体服务在后面的章节介绍。</p>
<div class="blog_h1"><span class="graybg">HTML5媒体标签</span></div>
<p>HTML5支持<pre class="crayon-plain-tag">&lt;audio&gt;</pre>和<pre class="crayon-plain-tag">&lt;video&gt;</pre>标签（两者都对应了HTMLMediaElement的子类型）以实现视频、音频的播放。</p>
<div class="blog_h2"><span class="graybg">&lt;audio&gt;</span></div>
<p>此标签用于在浏览器中创建一个纯音频播放器。播放静态文件的示例：</p>
<pre class="crayon-plain-tag">&lt;audio controls preload="auto"&gt;
    &lt;source src="song.mp3" type="audio/mpeg"&gt;
    &lt;!-- 备选格式，如果浏览器不支持mp3 --&gt;
    &lt;source src="song.ogg" type="audio/ogg"&gt;
    &lt;!-- 如果浏览器不支持audio标签，显示下面的连接 --&gt;
    &lt;a href="audiofile.mp3"&gt;download audio&lt;/a&gt;
&lt;/audio&gt;</pre>
<div class="blog_h2"><span class="graybg">&lt;video&gt;</span></div>
<p>此标签用于在浏览器中创建一个视频播放器。播放静态文件的示例：</p>
<pre class="crayon-plain-tag">&lt;!-- poster指定预览图，autoplay自动播放，muted静音 --&gt;
&lt;video controls width="640" height="480" poster="movie.png" autoplay muted&gt;
  &lt;source src="movie.mp4" type="video/mp4"&gt;
  &lt;!-- 备选格式，如果浏览器不支持mp4 --&gt;
  &lt;source src="movie.webm" type="video/webm"&gt;
  &lt;!-- 可以附带字幕 --&gt;
  &lt;track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"&gt;
  &lt;!-- 如果浏览器不支持video标签，显示下面的连接 --&gt;
  &lt;a href="videofile.mp4"&gt;download video&lt;/a&gt;
&lt;/video&gt;</pre>
<div class="blog_h2"><span class="graybg">&lt;canvas&gt;</span></div>
<p>在画布中，你可以进行任意的图形绘制，当然可以去逐帧渲染视频内容。</p>
<div class="blog_h2"><span class="graybg">编程方式创建</span></div>
<p>音频、视频播放器标签也可以利用JavaScript编程式的创建，示例代码：</p>
<pre class="crayon-plain-tag">var video = document.createElement( 'video' );
if ( video.canPlayType( 'video/mp4' ) ) {
    video.setAttribute( 'src', 'movie.mp4' );
}
else if ( video.canPlayType( 'video/webm' ) ) {
    video.setAttribute( 'src', 'movie.webm' );
}
video.width = 640;
video.height = 480; </pre>
<div class="blog_h1"><span class="graybg">MSE</span></div>
<p>媒体源扩展（Media Source Extensions，MSE）是一个W3C草案，<a href="http://caniuse.com/#feat=mediasource">桌面浏览器对MSE的支持较好</a>。MSE扩展流video/audio元素的能力，允许你<span style="background-color: #c0c0c0;">通过JavaScript来生成（例如从服务器抓取）媒体流供video/audio元素播放</span>。使用MSE你可以：</p>
<ol>
<li>通过JavaScript来构建媒体流，不管媒体是如何捕获的</li>
<li>处理自适应码流、广告插入、时间平移（time-shifting，回看）、视频编辑等应用场景</li>
<li>最小化JavaScript中处理媒体解析的代码</li>
</ol>
<p>MSE定义支持的（你生成的）<a href="https://www.w3.org/TR/media-source/">媒体格式</a>，只有符合要求的容器格式、编码格式才能被MSE处理。通常容器格式是<span style="color: #24292e;">ISO BMFF（MP4），也就是说你需要生成MP4的片断，然后Feed给MSE进行播放。</span></p>
<p>MediaSource对象作为video/audio元素的媒体来源，它可以具有多个SourceBuffer对象。应用程序把数据片段（segment）附加到SourceBuffer中，并可以根据系统性能对数据片段的质量进行适配。SourceBuffer中包含多个track buffer —— 分别对应音频、视频、文本等可播放数据。这些数据被音频、视频解码器解码，然后在屏幕上显示、在扬声器中播放：</p>
<p> <img class="aligncenter size-large wp-image-15564" src="https://blog.gmem.cc/wp-content/uploads/2017/08/pipeline_model.png" alt="pipeline_model" width="710" height="516" /></p>
<p>要把MediaSource提供给video/audio播放，调用：</p>
<pre class="crayon-plain-tag">video.src = URL.createObjectURL(mediaSource);</pre>
<div class="blog_h2"><span class="graybg">基于MSE的框架</span></div>
<div class="blog_h3"><span class="graybg">wfs</span></div>
<p><a href="https://github.com/ChihChengYang/wfs.js">wfs</a>是一个播放原始H.264帧的HTML5播放器，它的工作方式是把H.264 NAL单元封装为 ISO BMFF（MP4）片，然后Feed给MSE处理。</p>
<div class="blog_h3"><span class="graybg">flv.js</span></div>
<p><a href="https://github.com/Bilibili/flv.js">flv.js</a>是一个HTML5 Flash视频播放器，基于纯JS，不需要Flash插件的支持。此播放器将FLV流转换为ISO BMFF（MP4）片断，然后把MP4片断提供给video元素使用。</p>
<p>flv.js支持Chrome 43+, FireFox 42+, Edge 15.15048+以上版本的直播流 。</p>
<div class="blog_h3"><span class="graybg">Streamedian</span></div>
<p><a href="https://github.com/Streamedian/html5_rtsp_player/wiki/HTML5-RTSP-Player">Streamedian</a>是一个HTML5的RTSP播放器。实现了RTSP客户端功能，你可以利用此框架直接播放RTSP直播流。此播放器把RTP协议下的H264/AAC在转换为ISO BMFF供video元素使用。Streamedian支持Chrome 23+, FireFox 42+, Edge 13+，以及Android 5.0+。不支持iOS和IE。</p>
<p>在服务器端，你需要安装Streamedian提供的代理（此代理收费），此代理将RTSP转换为WebSocket。Streamedian处理视频流的流程如下：<img class="aligncenter size-large wp-image-15609" src="https://blog.gmem.cc/wp-content/uploads/2017/08/streamedian-1024x391.png" alt="streamedian" width="710" height="271" /></p>
<div class="blog_h1"><span class="graybg">WebRTC</span></div>
<p>WebRTC是一整套API，其中一部分供Web开发者使用，另外一部分属于要求浏览器厂商实现的接口规范。WebRTC解决诸如客户端流媒体发送、点对点通信、视频编码等问题。<a href="http://iswebrtcreadyyet.com/legacy.html">桌面浏览器对WebRTC的支持较好</a>，WebRTC也很容易和Native应用集成。</p>
<p>使用MSE时，你需要自己构建视频流。使用WebRTC时则可以直接捕获客户端视频流。</p>
<p>使用WebRTC时，大部分情况下流量不需要依赖于服务器中转，服务器的作用主要是：</p>
<ol>
<li>在信号处理时，转发客户端的数据</li>
<li>配合实现NAT/防火墙穿透</li>
<li>在点对点通信失败时，作为中继器使用</li>
</ol>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p><img class="aligncenter size-full wp-image-15576" src="https://blog.gmem.cc/wp-content/uploads/2017/08/webrtcArchitecture.png" alt="webrtcarchitecture" width="100%" /></p>
<div class="blog_h2"><span class="graybg">流捕获</span></div>
<div class="blog_h3"><span class="graybg">捕获视频</span></div>
<p>主要是捕获客户端摄像头、麦克风。在视频监控领域用处不大，这里大概了解一下。流捕获通过navigator.getUserMedia调用实现： </p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript"&gt;
    navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.getUserMedia;
    var success = function ( stream ) {
        var video = document.getElementById( 'camrea' );
        // 把MediaStream对象转换为Blob URL，提供给video播放
        video.src = URL.createObjectURL( stream );
        video.play();
    }
    var error = function ( err ) {
        console.log( err )
    }
    // 调用成功后，得到MediaStream对象
    navigator.getUserMedia( { video: true, audio: true }, success, error );
&lt;/script&gt;
&lt;video id="camrea" width="640" height="480"/&gt;</pre>
<p>三个调用参数分别是：</p>
<ol>
<li><a href="http://io13webrtc.appspot.com/#22">约束条件</a>，你可以指定媒体类型、分辨率、帧率 </li>
<li>成功后的回调，你可以在回调中解析出URL提供给video元素播放</li>
<li>失败后的回调</li>
</ol>
<div class="blog_h3"><span class="graybg">捕获音频</span></div>
<p>捕获音频类似：</p>
<pre class="crayon-plain-tag">navigator.getUserMedia( { audio: true }, function ( stream ) {
    var audioContext = new AudioContext();

    // 从捕获的音频流创建一个媒体源管理
    var streamSource = audioContext.createMediaStreamSource( stream );

    // 把媒体源连接到目标（默认是扬声器）
    streamSource.connect( audioContext.destination );
}, error );</pre>
<div class="blog_h3"><span class="graybg">MediaStream</span></div>
<p>MediaStream对象提供以下方法：</p>
<ol>
<li>getAudioTracks()，音轨列表</li>
<li>getVideoTracks()，视轨列表</li>
</ol>
<p>每个音轨、视轨都有个label属性，对应其设备名称。</p>
<div class="blog_h3"><span class="graybg">Camera.js</span></div>
<p><a href="https://github.com/idevelop/camera.js">Camera.js</a>是对getUserMedia的简单封装，简化了API并提供了跨浏览器支持：</p>
<pre class="crayon-plain-tag">camera.init( {
    width: 640,
    height: 480,
    fps: 30, // 帧率
    mirror: false,  // 是否显示为镜像
    targetCanvas: document.getElementById( 'webcam' ), // 默认null，如果设置了则在画布中渲染

    onFrame: function ( canvas ) {
        // 每当新的帧被捕获，调用此回调
    },

    onSuccess: function () {
        // 流成功获取后
    },

    onError: function ( error ) {
        // 如果初始化失败
    },

    onNotSupported: function () {
        // 当浏览器不支持camera.js时
    }
} );
// 暂停
camera.pause();
// 恢复
camera.start();</pre>
<p><a href="https://idevelop.ro/predator-vision/">掠食者视觉</a>是基于Camera实现的一个好玩的例子（移动侦测）。</p>
<div class="blog_h2"><span class="graybg">信号处理</span></div>
<p>在端点之间（Peer）发送流之前，需要进行通信协调、发送控制消息，即所谓信号处理（Signaling），信号处理牵涉到三类信息：</p>
<ol>
<li>会话控制信息：初始化、关闭通信，报告错误</li>
<li>网络配置：对于其它端点来说，本机的IP和端口是什么</li>
<li>媒体特性：本机能够处理什么音视频编码、多高的分辨率。本机发送什么样的音视频编码</li>
</ol>
<p>WebRTC没有对信号处理规定太多，我们可以通过Ajax/WebSocket通信，以SIP、Jingle、ISUP等协议完成信号处理。点对点连接设立后，流的传输并不需要服务器介入。信号处理的示意图如下：</p>
<p><img class="aligncenter wp-image-15587 size-full" src="https://blog.gmem.cc/wp-content/uploads/2017/08/jsep.png" alt="jsep" width="100%" /></p>
<div class="blog_h3"><span class="graybg">示例代码</span></div>
<p>下面的代表片段包含了一个视频电话的信号处理过程：</p>
<pre class="crayon-plain-tag">// 信号处理通道，底层传输方式和协议自定义
var signalingChannel = createSignalingChannel();
var conn;

// 信号通过此回调送达本地，可能分多次送达
signalingChannel.onmessage = function ( evt ) {
    if ( !conn ) start( false );

    var signal = JSON.parse( evt.data );
    // 会话描述协议（Session Description Protocol），用于交换媒体配置信息（分辨率、编解码能力）
    if ( signal.sdp )
    // 设置Peer的RTCSessionDescription
        conn.setRemoteDescription( new RTCSessionDescription( signal.sdp ) );
    else
    // 添加Peer的Candidate信息
        conn.addIceCandidate( new RTCIceCandidate( signal.candidate ) );
};

// 调用此方法启动WebRTC，获取本地流并显示，侦听连接上的事件并处理
function start( isCaller ) {
    conn = new RTCPeerConnection( { /**/ } );

    // 把地址/端口信息发送给其它Peer。所谓Candidate就是基于ICE框架获得的本机可用地址/端口
    conn.onicecandidate = function ( evt ) {
        signalingChannel.send( JSON.stringify( { "candidate": evt.candidate } ) );
    };

    // 当远程流到达后，在remoteView元素中显示
    conn.onaddstream = function ( evt ) {
        remoteView.src = URL.createObjectURL( evt.stream );
    };

    // 获得本地流
    navigator.getUserMedia( { "audio": true, "video": true }, function ( stream ) {
        // 在remoteView元素中显示
        localView.src = URL.createObjectURL( stream );
        // 添加本地流，Peer将接收到onaddstream事件
        conn.addStream( stream );


        if ( isCaller )
        // 获得本地的RTCSessionDescription
            conn.createOffer( gotDescription );
        else
        // 针对Peer的RTCSessionDescription生成兼容的本地SDP
            conn.createAnswer( conn.remoteDescription, gotDescription );

        function gotDescription( desc ) {
            // 设置自己的RTCSessionDescription
            conn.setLocalDescription( desc );
            // 把自己的RTCSessionDescription发送给Peer
            signalingChannel.send( JSON.stringify( { "sdp": desc } ) );
        }
    } );
}

// 通信发起方调用：
start( true );</pre>
<div class="blog_h2"><span class="graybg">流转发</span></div>
<p>主要牵涉到的接口是RTCPeerConnection，上面的例子中已经包含了此接口的用法。WebRTC在底层做很多复杂的工作，这些工作对于JavaScript来说是透明的： </p>
<ol>
<li>执行解码</li>
<li>屏蔽丢包的影响</li>
<li>点对点通信：WebRTC引入流交互式连接建立（Interactive Connectivity Establishment，ICE）框架。ICE负责建立点对点链路的建立：
<ol>
<li>首先尝试直接</li>
<li>不行的话尝试STUN（Session Traversal Utilities for NAT）协议。此协议通过一个简单的保活机制确保NAT端口映射在会话期间有效</li>
<li>仍然不行尝试TURN（Traversal Using Relays around NAT）协议。此协议依赖于部署在公网上的中继服务器。只要端点可以访问TURN服务器就可以建立连接</li>
</ol>
</li>
<li>通信安全</li>
<li>带宽适配</li>
<li>噪声抑制</li>
<li>动态抖动缓冲（dynamic jitter buffering），抖动是由于网络状况的变化，缓冲用于收集、存储数据，定期发送</li>
</ol>
<div class="blog_h2"><span class="graybg">任意数据交换</span></div>
<p>通过RTCDataChannel完成，允许点对点之间任意的数据交换。RTCPeerConnection连接创建后，不但可以传输音视频流，还可以打开多个信道（RTCDataChannel）进行任意数据的交换。RTCDataChanel的特点是：</p>
<ol>
<li>类似于WebSocket的API</li>
<li>支持带优先级的多通道</li>
<li>超低延迟，因为不需要通过服务器中转</li>
<li>支持可靠/不可靠传输语义。支持SCTP、DTLS、UDP几种传输协议</li>
<li>内置安全传输（DTLS）</li>
<li>内置拥塞控制</li>
</ol>
<p>使用RTCDataChannel可以很好的支持游戏、远程桌面、实时文本聊天、文件传输、去中心化网络等业务场景。</p>
<div class="blog_h2"><span class="graybg">adapter.js</span></div>
<p><a href="https://github.com/webrtc/adapter">WebRTC adapter</a>是一个垫片库，使用它开发WebRTC应用时，不需要考虑不同浏览器厂商的<a href="https://webrtc.org/web-apis/interop/">API前缀差异</a>。</p>
<div class="blog_h2"><span class="graybg">WebRTC示例</span></div>
<p>本节列出一些WebRTC的代码示例，这些例子都使用adapter.js。</p>
<div class="blog_h3"><span class="graybg">限定分辨率</span></div>
<pre class="crayon-plain-tag">// 指定分辨率
// adapter.js 支持Promise
navigator.mediaDevices.getUserMedia( { video: { width: { exact: 640 }, height: { exact: 480 } } } ).then( stream =&gt; {
    let video = document.createElement( 'video' );
    document.body.appendChild( video );
    video.srcObject = stream;
    video.play();
} ).catch( err =&gt; console.log( err ) );</pre>
<div class="blog_h3"><span class="graybg">在画布中截图</span></div>
<pre class="crayon-plain-tag">// video为video元素
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);</pre>
<div class="blog_h2"><span class="graybg">WebRTC框架</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">框架</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://peerjs.com/">PeerJS </a></td>
<td>
<p>简化WebRTC的点对点通信、视频、音频调用</p>
<p>提供云端的PeerServer，你也可以自己搭建服务器</p>
</td>
</tr>
<tr>
<td><a href="https://github.com/peer5/sharefest">Sharefest</a></td>
<td>基于Web的P2P文件共享</td>
</tr>
<tr>
<td><a href="https://github.com/webRTC-io/webRTC.io">webRTC.io</a></td>
<td>
<p>WebRTC的一个抽象层，同时提供了客户端、服务器端Node.js组件。服务器端组件抽象了STUN</p>
<p>类似的框架还有<a href="https://github.com/andyet/SimpleWebRTC">SimpleWebRTC</a>、<a href="https://github.com/priologic/easyrtc">easyrtc</a></p>
</td>
</tr>
<tr>
<td><a href="https://www.openwebrtc.org/">OpenWebRTC</a></td>
<td>
<p>允许你构建能够和遵循WebRTC标准的浏览器进行通信的Native应用程序，支持Java绑定</p>
</td>
</tr>
<tr>
<td><a href="https://nextrtc.org/">NextRTC</a></td>
<td>
<p>基于Java实现的WebRTC信号处理服务器</p>
</td>
</tr>
<tr>
<td><a href="https://github.com/meetecho/janus-gateway">Janus</a></td>
<td>
<p>这是一个WebRTC网关，纯服务器端组件，目前仅仅支持Linux环境下安装</p>
<p>Janus本身实现了到浏览器的WebRTC连接机制，支持以JSON格式交换数据，支持在服务器端应用逻辑 - 浏览器之间中继RTP/RTCP和消息。特殊化的功能有服务器端插件完成</p>
<p>官网地址：<a href="https://janus.conf.meetecho.com/index.html">https://janus.conf.meetecho.com</a></p>
</td>
</tr>
<tr>
<td><a href="https://www.kurento.org">Kurento</a></td>
<td>
<p>这是一个开源的WebRTC媒体服务器</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">备选方案一：从RTSP开始</span></div>
<p>我们首先尝试的方案是直接使用RTSP源，原因是海康、大华主流厂商的较新的IP摄像头均支持暴露标准化的RTSP流。</p>
<div class="blog_h2"><span class="graybg">尝试播放</span></div>
<p>使用VLC播放器，打开网络串流：rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream，视频源为公司门口的海康摄像头的主码流（main，子码流为sub）。</p>
<p>发现可以正常播放，说明视频格式应该是标准的。VLC菜单 Tool ⇨ Codec Info查看，编码格式为H264。</p>
<p>浏览器无法直接使用RTSP协议，因此，需要有服务器端来处理视频源的RTSP，将其转换为：</p>
<ol>
<li>通过WebSocket发送的视频片断，由客户端的：
<ol>
<li>JSMpeg/Broadway直接解码，渲染到画布</li>
<li>或者，构造MP4片断Feed给MSE播放</li>
</ol>
</li>
<li>或者，通过WebRTC网关，转换后提供给客户端的WebRTC代码处理</li>
<li>或者，使用浏览器插件机制，例如Chrome的NaCl</li>
</ol>
<div class="blog_h2"><span class="graybg">实现方式一：MSE</span></div>
<p>Streamedian的服务器端需要授权，我们选用了另外一个实现。</p>
<p><span style="color: #24292e;"><a href="https://github.com/veyesys/h5stream">H5S</a>是一个基于live555实现的开源的HTML5 RTSP网关，支持将RTSP/H264流输入转换为HTML5 MSE支持的H264，客户端基于MSE。</span></p>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<p>尝试在容器中运行H5S：</p>
<pre class="crayon-plain-tag">docker create --name ubuntu-16.04 -h ubuntu-16 --network local --dns 172.21.0.1 --ip 172.21.0.6 -it docker.gmem.cc/ubuntu:16.04 bash
docker start ubuntu-16.04
docker exec -it ubuntu-16.04 bash

apt update &amp;&amp; apt install wget
wget https://raw.githubusercontent.com/veyesys/release/master/h5stream/H5S-r1.0.1128.16-Ubuntu-16.04-64bit.tar.gz
tar xzf H5S-r1.0.1128.16-Ubuntu-16.04-64bit.tar.gz &amp;&amp; mv H5S-r1.0.1128.16-Ubuntu-16.04-64bit h5s-1.0

cd h5s-1.0
export LD_LIBRARY_PATH=`pwd`/lib/:$LD_LIBRARY_PATH
# 指定两次密码，可能H5S存在bug，不这样报身份验证失败
./h5ss rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream admin 12345</pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<p>使用H5S自带的基于MSE的客户端代码 + Chrome 49，播放后发现画面静止。控制它查看发现解码错误。打开chrome://media-internals/，发现错误Media segment did not begin with key frame. Support for such segments will be available in a future version。看样子是提供给SourceBuffer的数据不是以关键帧开始导致，未来版本的Chrome可能取消此限制。</p>
<p>换成Chrome 50，可以正常播放，但是流畅度较差，播放一段时间后出现卡死的情况。</p>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>H5S实现不完善，在不修改源码的情况下，服务器端只能接入一路视频输入。客户端也存在不流畅、卡死的问题，不适合生产环境。</p>
<div class="blog_h2"><span class="graybg">实现方式二：JSMpeg</span></div>
<div class="blog_h3"><span class="graybg">转码进程</span></div>
<p>在上文中我们已经成功尝试了利用JSMpege + WebSocket的方式，在网页中显示摄像头捕获的视频。ffmpeg转换RTSP也是非常简单的：</p>
<pre class="crayon-plain-tag">ffmpeg -i rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream -s 427x240 -f mpegts -vcodec mpeg1video -b 800k -r 30 http://127.0.0.1:8800/12345</pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<p>可以使用JSMpeg自带的简单Node.js服务器测试：</p>
<pre class="crayon-plain-tag">node ./app/websocket-relay.js 12345 8800 8801 </pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<p>下面是客户端代码，默认JSMpeg会基于WebGL渲染，但是我的机器最多开到8画面，开9画面时出现警告：</p>
<p>Too many active WebGL contexts. Oldest context will be lost，且第一画面丢失，简单的通融方法是，第9画面使用Canvas2D渲染：</p>
<pre class="crayon-plain-tag">new JSMpeg.Player( 'ws://127.0.0.1:8801/', {
    canvas: document.getElementById( 'canvas9' ),
    autoplay: true,
    // 浏览器对WebGL context的数量有限制
    disableGl: true
} ); </pre>
<p>渲染截图：</p>
<p><img class="aligncenter size-large wp-image-15669" src="https://blog.gmem.cc/wp-content/uploads/2017/08/jsmpeg-s9-1024x621.png" alt="jsmpeg-s9" width="100%" /></p>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>这种方式客户端解码压力较大，同时开9画面的352x288视频，我的机器上CPU占用率大概到40%左右，画面变化较为剧烈的时候会出现卡顿现象。</p>
<div class="blog_h2"><span class="graybg">实现方式三：Broadway</span></div>
<p>与JSMpeg类似，Broadway也是JavaScript解码工具。关键之处是，Broadway支持的视频编码是H.264，意味着可能免去消耗服务器资源的视频重编码。</p>
<p><a id="hk-av-config"></a>最初的尝试并不顺利，根据IP摄像头的RTSP Describe应答（SDP），我们推断其H.264 Profile为Baseline，但是不转码的情况下Broadway根本无法播放。后来查看ffmpeg的日志输出，发现其实际上使用的Profile是Main。进一步尝试，发现摄像头是可以配置为Baseline的：</p>
<p><img class="aligncenter size-full wp-image-16196" src="https://blog.gmem.cc/wp-content/uploads/2017/08/hk-config.png" alt="hk-config" width="707" height="465" /></p>
<p>只需要把编码复杂度设置为低，H.264的Profile就从Main变为Baseline。</p>
<p>设置完毕后，仍然基于h264-live-player的Demo进行测试，使用如下命令行抽取原始H.264帧：</p>
<pre class="crayon-plain-tag">ffmpeg -i rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream -c:v copy -f rawvideo  -</pre>
<p>即可免转码的进行实时视频预览了。 </p>
<p>此实现方式更多细节信息请参考<a href="/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a>。</p>
<div class="blog_h2"><span class="graybg">实现方式四：NaCl</span></div>
<p>Chrome放弃NPAPI之后，插件开发需要使用PPAPI /NaCl。目前能找到的实现有<a href="https://www.videoexpertsgroup.com/vxg-chrome-plugin/">VXG Chrome Plugin</a>，这是一个商业产品，需要授权。除了RTSP之外，还支持RTMP、HLS等协议。</p>
<p>插件方案的缺点是，需要安装，而且仅仅针对单种浏览器。优势则是灵活性高，理论上性能可以做的很好。</p>
<div class="blog_h2"><span class="graybg">实现方式五：WebRTC</span></div>
<p>WebRTC相关的框架非常多，经过简单的比较，我们决定从Kurento入手。主要原因是：</p>
<ol>
<li>容易扩展的模块化设计</li>
<li>提供Java客户端、JS客户端</li>
<li>可以在服务器端合成多画面，这样可以减轻客户端解码压力，特别是那些低配置的客户端</li>
<li>内置对RTSP协议的支持</li>
</ol>
<p><a href="/webrtc-server-basedon-kurento#vs">基于Kurento搭建WebRTC服务器</a>一文详细讨论了这种实现方式。</p>
<div class="blog_h1"><span class="graybg">备选方案二：从设备SDK开始</span></div>
<p>这里的设备，主要包括：网络硬盘录像机（NVR）、视频服务器、IP摄像头。为了便于二次开发，硬件厂商都为这些设备配置的相应的SDK套件。这些SDK通常都提供了：实时码流预览、录像文件回放、播放控制（如：暂停、单帧前进、单帧后退）、获取码流基本信息、播放截图等功能。</p>
<p>我们的基本目标是，通过SDK得到标准化的码流，例如H264格式。具体如何操作，得看厂商的SDK，但是思路基本是：</p>
<ol>
<li>如果SDK直接支持获取标准格式的流，例如RTSP，那么备选方案一就可以直接用上</li>
<li>如果SDK支持获取标准编码的视频帧，例如H264，那我们只需要将其包装为合适的容器格式，再通过RTSP/HTTP的方式发送出去</li>
<li>如果SDK支持获取解码后的原始图像数据，例如RGB、YV12，我们可以基于H264再次编码，然后按第2步方式处理。这种方式对服务器性能要求比较高，CPU压力较大，PC机处理不了多少个通道</li>
<li>如果都不支持，只提供了封装好的播放控件 —— 这个就比较悲催了，不过通过OS底层API，例如Windows的GDI应该也是可以实现，否则那些屏幕录像软件怎么做的呢？</li>
</ol>
<div class="blog_h2"><span class="graybg">海康SDK</span></div>
<p>根据Linux版本的海康设备网络编程指南的描述，我们应该可以：</p>
<ol>
<li>调用NET_DVR_Init进行SDK初始化</li>
<li>调用NET_DVR_Login登陆到目标设备</li>
<li>调用NET_DVR_RealPlay进行播放，此时返回一个实时播放句柄
<ol>
<li>如果设备支持RTSP协议取流：针对上述句柄调用NET_DVR_SetStandardDataCallBack，可以设置一个标准的数据回调函数，此回调会接受到标准码流，这对应上面的第1种思路</li>
<li>如果设备不支持RTSP协议取流：针对上述句柄调用NET_DVR_SetRealDataCallBack，然后通过PlayM4播放库中的PlayM4_SetDecCallBack回调得到<a href="/image-processing-faq#yv12">yv12</a>格式的原始图像。这对应上面的第3种思路</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">示例代码</span></div>
<p>cmake构建配置：</p>
<pre class="crayon-plain-tag">cmake_minimum_required(VERSION 3.6)
project(hikvision)

include_directories(/home/alex/CPP/lib/hcnedsdk/include)

set(SOURCE_FILES getstream.cpp)
add_executable(getstream ${SOURCE_FILES})
target_link_libraries(getstream /home/alex/CPP/lib/hcnedsdk/lib/libhcnetsdk.so)</pre>
<p> C++代码：</p>
<pre class="crayon-plain-tag">#include &lt;HCNetSDK.h&gt;
#include &lt;stdio.h&gt;
#include &lt;cstring&gt;
#include &lt;unistd.h&gt;

// RTSP协议取流
void CALLBACK cbStdData( LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, DWORD dwUser ) {
    switch ( dwDataType ) {
        case NET_DVR_SYSHEAD:        // 系统头数据，回调的第一个包是40字节的文件头
            break;
        case NET_DVR_STREAMDATA:     // 基于私有协议时：视频流数据（包括复合流和音视频分开的视频流数据）
            break;
        case NET_DVR_STD_VIDEODATA:  // 基于标准协议时：标准视频流数据（RTP包）
            break;
        case NET_DVR_STD_AUDIODATA:  // 基于标准协议时：标准音频流数据
            break;
        case NET_DVR_SDP:            // SDP信息(RTSP传输时有效)
            break;
        case NET_DVR_PRIVATE_DATA:   // 私有数据,包括智能信息叠加等
            break;
    }
}

int main() {
    // SDK初始化
    BOOL result = NET_DVR_Init();
    if ( !result ) return 1;

    // 同步登陆
    NET_DVR_USER_LOGIN_INFO struLoginInfo = { 0 };
    struLoginInfo.bUseAsynLogin = 0;
    strcpy( struLoginInfo.sDeviceAddress, "192.168.0.196" );
    struLoginInfo.wPort = 8000;
    strcpy( struLoginInfo.sUserName, "admin" );
    strcpy( struLoginInfo.sPassword, "12345" );
    NET_DVR_DEVICEINFO_V40 struDevInfo = { 0 };
    LPNET_DVR_DEVICEINFO_V30 lpDevInfo30;
    long lUserID = NET_DVR_Login_V40( &amp;struLoginInfo, &amp;struDevInfo );
    if ( lUserID &lt; 0 ) {
        printf( "登陆失败，错误码 %d\n", NET_DVR_GetLastError());
        NET_DVR_Cleanup();
        return 1;
    } else {
        lpDevInfo30 = &amp;struDevInfo.struDeviceV30;
        printf( "成功登陆到设备：%s\n", lpDevInfo30-&gt;sSerialNumber );
        printf( "SDK字符串编码方式（1 GB2312，2 GBK，3 BIG5，6 UTF-8）：%d\n", struDevInfo.byCharEncodeType );
        printf( "设备类型（31 高清网络摄像机）：%d\n", lpDevInfo30-&gt;wDevType );
        printf( "模拟通道起始号：%d，模拟通道个数%d，数字通道起始号：%d，数字通道个数%d\n", lpDevInfo30-&gt;byStartChan, lpDevInfo30-&gt;byChanNum,
                lpDevInfo30-&gt;byStartDChan, lpDevInfo30-&gt;byIPChanNum + lpDevInfo30-&gt;byHighDChanNum &lt;&lt; 8 );
        printf( "主码流是否支持RTSP方式：%s，子码流是否支持RTSP方式：%s\n", lpDevInfo30-&gt;byMainProto &gt; 0 ? "是" : "否",
                lpDevInfo30-&gt;bySubProto &gt; 0 ? "是" : "否" );
    }

    // 启动预览
    NET_DVR_PREVIEWINFO struPrevInfo = { 0 };
    struPrevInfo.hPlayWnd = NULL;    // Linux 64 位系统不支持软解码功能
    struPrevInfo.lChannel = 1;       // 预览通道号
    struPrevInfo.dwStreamType = 0;   // 0-主码流， 1-子码流， 2-码流 3， 3-码流 4，以此类推
    struPrevInfo.dwLinkMode = 0;     // 0- TCP 方式， 1- UDP 方式， 2- 组播方式， 3- RTP 方式， 4-RTP/RTSP， 5-RSTP/HTTP
    struPrevInfo.bBlocked = 1;       // 0- 非阻塞取流， 1- 阻塞取流
    struPrevInfo.byProtoType = 1;    // 应用层取流协议使用RTSP
    LONG lRealHandle = NET_DVR_RealPlay_V40( lUserID, &amp;struPrevInfo, NULL, NULL );
    if ( lRealHandle == -1 ) {
        printf( "启动预览失败，错误码 %d\n", NET_DVR_GetLastError());
        NET_DVR_Logout( lUserID );
        NET_DVR_Cleanup();
        return 1;
    }

    if ( lpDevInfo30-&gt;byMainProto ) {
        printf( "设置获取标准码流的回调\n" );
        // 仅支持对 支持RTSP协议取流的设备的 标准码流回调
        NET_DVR_SetStandardDataCallBack( lRealHandle, cbStdData, NULL );
    }

    sleep( 120 );
    // 停止预览
    NET_DVR_StopRealPlay( lRealHandle );
    // 登出
    NET_DVR_Logout( lUserID );
    // SDK清理
    NET_DVR_Cleanup();
    return 0;
}</pre>
<p>运行脚本：</p>
<pre class="crayon-plain-tag">export HKLIB_HOME=/home/alex/CPP/lib/hcnedsdk/lib
export LD_LIBRARY_PATH=$HKLIB_HOME:$HKLIB_HOME/HCNetSDKCom
./getstream </pre>
<p>此程序运行后，会自动获取到基于RTSP协议的媒体流，回调函数会反复被调用：</p>
<ol>
<li>第一次调用为40字节的头，不太清楚有什么用</li>
<li>第二次调用传递了SDP</li>
<li>后续调用传递标准音视频数据，其内容是RTP封包</li>
</ol>
<div class="blog_h1"><span class="graybg">总结</span></div>
<p>基于HTM5的视频监控，媒体流从采集设备到浏览器，主要路径如下图所示：</p>
<p><img class="aligncenter size-full wp-image-15824" src="https://blog.gmem.cc/wp-content/uploads/2017/08/h5vs-dataflow.png" alt="h5vs-dataflow" width="100%" /></p>
<p>对上图的说明如下：</p>
<ol>
<li>在设备层，需要以某种方式获得码流，以流协议的方式发送出去。最常用的方式是RTSP/RTP。流的可能获取路径为：
<ol>
<li>设备直接暴露RTSP协议端点，并且发送标准码流</li>
<li>设备SDK允许获取标准码流，需要自己以RTSP协议发送</li>
<li>设备SDK允许获得解码后的逐帧，需要直接编码为H264，然后以RTSP发送</li>
</ol>
</li>
<li>流媒体层通常需要引入专门的流媒体服务器，这类服务器能够在内部进行各种流协议的转换，可以解除客户端对特定流协议的依赖</li>
<li>客户端和服务器端的传输方式，可以有TCP、HTTP、P2P（WebRTC）、WebSocket等多种。其中
<ol>
<li>直接的TCP协议浏览器是不支持的，这意味着RTSP/RTMP等协议，在浏览器端必须要有插件才可以使用</li>
<li>WebSocket通常配合JSMpeg或者MSE使用，由程序向JSMpeg/MSE不断Feed视频帧</li>
</ol>
</li>
<li>客户端解码展示的技术主要有三类：
<ol>
<li>浏览器内置的解码能力，主要通过video标签，MSE属于此类</li>
<li>JavaScript软解码，主要是JSMpeg、Broadway</li>
<li>插件机制，例如Chrome的NaCl</li>
</ol>
</li>
</ol>
<p>能够免于引入流媒体层的方案，需要：设备能直接暴露标准码流的RTSP端点，并且安装浏览器插件。缺点也很明显，一个是设备的访问密码暴露给了客户端，第二个是目前没有成熟、开源的插件可用。我相信主要原因是合理技术方向不在于此，没人愿意去开发。</p>
<p>直接使用设备层的RTSP端点，可能存在兼容性问题。一个是它发送的码流是否标准化，第二个是市场上有多少设备没有暴露RTSP端点。</p>
<p>客户端方面，JSMpeg是兼容性较好的方案，WebRTC/MSE都有部分平台不支持（但是桌面级的浏览器大部分支持）。JSMpeg的缺点是：</p>
<ol>
<li>如果基于WebGL渲染，受限于浏览器WebGL上下文最大数量，多画面可能无法渲染。某些流媒体服务器支持在服务器端合成多画面Grid，可以规避此缺点</li>
<li>如果基于Canvas2D渲染，画质较差（我的机器上还有莫名其妙的斜线）</li>
<li>对码流格式要求严格，仅仅支持MPEG-TS，此格式压缩比差，网络带宽占用大</li>
<li>性能相对较差，尽管使用了MPEG-TS这种简单的视频格式，基于JavaScript解码渲染仍然使客户端压力较大。我的机器（i7-4940MX / Quadro K5100M / Ubuntu 14.04 LTS）上会出现卡顿情况</li>
</ol>
<p>和JSMpeg类似的库是Broadway，后者能够进行Baseline的H.264解码。如果设备支持Baseline H.264输出，使用Broadway可以很好的解决服务器端转码导致的资源消耗问题。</p>
<div class="blog_h1"><span class="graybg">附录</span></div>
<div class="blog_h2"><span class="graybg">参考资料</span></div>
<ol>
<li><a href="https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery">Audio and Video Delivery</a></li>
<li><a href="https://www.w3.org/TR/media-source/">W3C Recommendation - Media Source Extensions™</a></li>
<li><a href="https://webrtc.org">WebRTC Project Home</a></li>
<li><a href="https://imququ.com/post/html5-live-player-3.html">HTML5 视频直播（三）</a></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/research-on-html5-video-surveillance">HTML5视频监控技术预研</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/research-on-html5-video-surveillance/feed</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Blender知识集锦</title>
		<link>https://blog.gmem.cc/blender-faq</link>
		<comments>https://blog.gmem.cc/blender-faq#comments</comments>
		<pubDate>Wed, 18 Jan 2017 07:16:45 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14561</guid>
		<description><![CDATA[<p>常用快捷键 快捷键 说明 右键 单选元素 Shift   右键 追加元素（物体、点、线或者面）到选区 Ctrl I 反选元素 A 全选/全不选 B   左键 以矩形框追加元素到选区 C  中键 以笔刷修剪选区滚动鼠标滚轮，可以修改笔刷大小 Ctrl <a class="read-more" href="https://blog.gmem.cc/blender-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/blender-faq">Blender知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">常用快捷键</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">快捷键</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>右键</td>
<td>单选元素</td>
</tr>
<tr>
<td>Shift   右键</td>
<td>追加元素（物体、点、线或者面）到选区</td>
</tr>
<tr>
<td>Ctrl I</td>
<td>反选元素</td>
</tr>
<tr>
<td>A</td>
<td>全选/全不选</td>
</tr>
<tr>
<td>B   左键</td>
<td>以矩形框追加元素到选区</td>
</tr>
<tr>
<td>C  中键</td>
<td>以笔刷修剪选区<br />滚动鼠标滚轮，可以修改笔刷大小</td>
</tr>
<tr>
<td>Ctrl  左键拖动</td>
<td>以套索追加元素到选区</td>
</tr>
<tr>
<td>Ctrl  左键点击</td>
<td>以当前选中的元素为基准，快速添加新的元素</td>
</tr>
<tr>
<td>Ctrl   Shift   左键</td>
<td>以套索修剪选区</td>
</tr>
<tr>
<td>Alt   右键</td>
<td>
<p>选取一圈子的面，按住Alt，点击右键选取一个面，再次点击右键选取一个面，顺着这两个面方向的一圈子面都被选中</p>
<p>选取边的时候，按Alt 右键，选择循环边；按Ctrl Alt 右键，选择平行边</p>
</td>
</tr>
<tr>
<td>Ctrl   右键</td>
<td>
<p>选取一个范围的面，按住Ctrl，点击右键选取一个面，再次点击右键选取一个面，则两个面之间的所有面都被选中</p>
<p>该快捷键可以和Shift 右键联合使用</p>
</td>
</tr>
<tr>
<td>Ctrl 小键盘 + / -</td>
<td>选择更多/更少：首先选择一个元素，然后按此快捷键，可以选择周围的元素</td>
</tr>
<tr>
<td>Ctrl Shift 小键盘 + / -</td>
<td>选择更多/更少：首先选择两个元素E1、E2，然后按此快捷键，可以继续选择下一个元素E3，使得E3与E2的位置关系、E2与E1的位置关系相同</td>
</tr>
<tr>
<td>Ctrl  Tab</td>
<td>子模式切换：在编辑模式（Edit Mode）下，切换选取的目标：点、线、面</td>
</tr>
<tr>
<td>Ctrl   +</td>
<td>增加选区</td>
</tr>
<tr>
<td>Ctrl   -</td>
<td>缩小选区</td>
</tr>
<tr>
<td>L</td>
<td>
<p>鼠标悬停在元素上，按L可以选中所有与之关联（属于同一物体）的元素</p>
<p>点选元素后，按Ctrl L也可以</p>
</td>
</tr>
<tr>
<td>中键</td>
<td>旋转视图</td>
</tr>
<tr>
<td>Shift 中键</td>
<td>平移视图</td>
</tr>
<tr>
<td>Ctrl 中键</td>
<td>缩放视图，或者中键滚轮也可以</td>
</tr>
<tr>
<td>小键盘0</td>
<td>进入/退出摄像机视角</td>
</tr>
<tr>
<td>Ctrl + 小键盘0</td>
<td>进入/退出活动摄像机视角</td>
</tr>
<tr>
<td>小键盘5</td>
<td>透视/正交视角切换</td>
</tr>
<tr>
<td>小键盘1</td>
<td>从正前方观察。Ctrl 小键盘1则从正后方</td>
</tr>
<tr>
<td>小键盘3</td>
<td>从正右方观察。Ctrl 小键盘3则从正左方</td>
</tr>
<tr>
<td>小键盘7</td>
<td>从正上方观察。Ctrl 小键盘7则从正下方</td>
</tr>
<tr>
<td>Ctrl Alt Q</td>
<td>切换上/前/右/透视四画面</td>
</tr>
<tr>
<td>Home</td>
<td>显示所有物体</td>
</tr>
<tr>
<td>小键盘.</td>
<td>放大、居中显示选中物体</td>
</tr>
<tr>
<td>小键盘/</td>
<td>放大、居中显示选中物体，并且隔离其它未选中物体</td>
</tr>
<tr>
<td>左键</td>
<td>移动3D游标</td>
</tr>
<tr>
<td>Shift C</td>
<td>重置视图，3D游标回到原点</td>
</tr>
<tr>
<td>Z</td>
<td>切换以线框/实体（纯色）来显示物体</td>
</tr>
<tr>
<td>Alt Z</td>
<td>切换以纹理/实体（纯色）来显示物体</td>
</tr>
<tr>
<td>Shift Z</td>
<td>切换以渲染结果/实体（纯色）来显示物体</td>
</tr>
<tr>
<td>Ctrl R</td>
<td>
<p>编辑模式下，悬停在两条平行线框之间：</p>
<ol>
<li>滚动滚轮，可以增加新的分割线</li>
<li>然后左键，确认添加分割线</li>
<li>然后可以移动分割线位置，或者右键让分割线居中</li>
</ol>
<p>可以用于曲线的细分</p>
</td>
</tr>
<tr>
<td>K</td>
<td>切割</td>
</tr>
<tr>
<td>Y</td>
<td>分离选中的面，后续用右键移动被分离的面</td>
</tr>
<tr>
<td>E</td>
<td>抬升。Alt E 调出选项菜单</td>
</tr>
<tr>
<td>Ctrl B</td>
<td>选中一个元素，制造倒角。移动鼠标改变倒角高度，滚动滚轮增加分段数</td>
</tr>
<tr>
<td>V</td>
<td>撕开选中的边</td>
</tr>
<tr>
<td>Alt V</td>
<td>撕开选中的边并填充，注意3D游标的位置</td>
</tr>
<tr>
<td>F</td>
<td>选中两个顶点，然后连接为线，或者选中三个以上顶点，连接为面</td>
</tr>
<tr>
<td>P</td>
<td>将选区独立为新的物体</td>
</tr>
<tr>
<td>G G</td>
<td>滑移：受限制的移动线或面</td>
</tr>
<tr>
<td>X</td>
<td>删除元素</td>
</tr>
<tr>
<td>I</td>
<td>
<p>在选中的面上新增一个内插的面：</p>
<ol>
<li>滚动滚轮，可以改变内插面的大小</li>
<li>按住Ctrl并移动鼠标，可以凸起、凹下此内插面</li>
</ol>
</td>
</tr>
<tr>
<td>W</td>
<td>特殊菜单项，细分、平滑等功能在此</td>
</tr>
<tr>
<td>Ctrl  V / E / F</td>
<td>编辑模式下与点、线、面有关的命令</td>
</tr>
<tr>
<td>M</td>
<td>放置元素到层</td>
</tr>
<tr>
<td>Shift R</td>
<td>重复上一次动作，例如上一次动作为细分，则按此快捷键可进一步细分</td>
</tr>
<tr>
<td>Alt  S</td>
<td>法向缩放</td>
</tr>
<tr>
<td>Alt R</td>
<td>旋绕（Spin）工具</td>
</tr>
<tr>
<td>Alt D</td>
<td>在点选择模式下，添加新的点</td>
</tr>
<tr>
<td>U</td>
<td>UV映射</td>
</tr>
<tr>
<td>Shift H</td>
<td>隐藏未选择部分</td>
</tr>
<tr>
<td>Alt H</td>
<td>显示隐藏部分</td>
</tr>
<tr>
<td colspan="2"><strong><em>物体模式下的快捷键</em></strong></td>
</tr>
<tr>
<td>Shift A</td>
<td>添加物体</td>
</tr>
<tr>
<td>G</td>
<td>
<p>移动物体。GZ仅仅在Z轴方向移动，GY、GX类似。G5移动5</p>
<p>G后，按Shift X，表示在Y、Z方向移动（排除X方向）</p>
</td>
</tr>
<tr>
<td>S</td>
<td>
<p>缩放物体。SZ仅仅在Z轴方向缩放，SY、SX类似。S2放大二倍</p>
<p>SX0，可以多个面的法线一律平行于X轴</p>
</td>
</tr>
<tr>
<td>R</td>
<td>旋转物体。RZ仅仅在Z轴方向旋转，RY、RX类似</td>
</tr>
<tr>
<td>Ctrl Alt Shift C</td>
<td>设置物体的质心（Origin）</td>
</tr>
<tr>
<td>Shift D</td>
<td>复制物体</td>
</tr>
<tr>
<td>Alt D</td>
<td>复制链接（Linked）物体</td>
</tr>
<tr>
<td>Shift S</td>
<td>
<p>吸附（Snap）操作，例如：</p>
<ol>
<li>Cursor to Center，把3D游标移动到世界坐标系的中心点</li>
<li>Selection to  Cursor，可以把选中对象的圆心与3D游标重合</li>
</ol>
</td>
</tr>
<tr>
<td>Ctrl J</td>
<td>把多个物体合并为一个</td>
</tr>
<tr>
<td>Ctrl G</td>
<td>物体分组</td>
</tr>
<tr>
<td>Shift G</td>
<td>选择物体所在的分组</td>
</tr>
<tr>
<td>Ctrl A</td>
<td>应用。具有重置位置、缩放、旋转值的作用，N面板中的数值会重置</td>
</tr>
<tr>
<td>Alt C</td>
<td>将曲线、文本、表面转换未Mesh，或者反向转换</td>
</tr>
<tr>
<td>O</td>
<td>比例化编辑</td>
</tr>
<tr>
<td colspan="2"><strong><em>动画快捷键</em></strong></td>
</tr>
<tr>
<td>Alt A</td>
<td>播放/暂停</td>
</tr>
<tr>
<td>Shift Alt A</td>
<td>倒放的播放/暂停</td>
</tr>
<tr>
<td>Left  Right</td>
<td>前进或者后退一帧</td>
</tr>
<tr>
<td>Shift Left<br />Shfit Right</td>
<td>转到时间线开始/结束处</td>
</tr>
<tr>
<td>Shift Up<br />Shift Down</td>
<td>跳跃10帧</td>
</tr>
<tr>
<td colspan="2"><em><strong>Blender界面快捷键</strong></em></td>
</tr>
<tr>
<td>T</td>
<td>显示/关闭左侧面板</td>
</tr>
<tr>
<td>N</td>
<td>显示/关闭右侧面板</td>
</tr>
<tr>
<td>A</td>
<td>鼠标悬停，可以展开/折叠面板中的字段集</td>
</tr>
<tr>
<td>Shift + Space</td>
<td>扩大编辑器面板</td>
</tr>
<tr>
<td colspan="2"><strong><em>圆盘菜单快捷键</em></strong></td>
</tr>
<tr>
<td>Tab</td>
<td>切换物体交互模式</td>
</tr>
<tr>
<td>Z</td>
<td>切换物体渲染方式</td>
</tr>
<tr>
<td>Q</td>
<td>切换查看角度</td>
</tr>
<tr>
<td>.</td>
<td>切换旋转、缩放时使用的轴心点（Pivot Point）</td>
</tr>
<tr>
<td>Ctrl Shift Tab</td>
<td>切换吸附（Snap）点</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">常用操作</span></div>
<div class="blog_h2"><span class="graybg">基本操作</span></div>
<div class="blog_h3"><span class="graybg">如何隐藏网格线</span></div>
<p>N打开面板，在Display控件中，把Lines设置为0</p>
<div class="blog_h3"><span class="graybg">如何关闭拆分出的编辑器</span></div>
<p>左键按住其源面板由上角的grabber（小三角），稍微拖动一下即可。</p>
<div class="blog_h3"><span class="graybg">比例化编辑</span></div>
<p>按O键启用。其效果是：以选中的元素为基准，按照特定的衰减函数，对物体进行整体变换。离选中元素越近的元素，受变换的影响越大。选择的轴心点也会影响到变换的效果</p>
<div class="blog_h3"><span class="graybg">倒角操作</span></div>
<p>操作步骤：</p>
<ol>
<li>以边选择模式，选中一个边，按Ctrl B执行倒角</li>
<li>移动鼠标，可以修改倒角面的大小</li>
<li>滚动滚轮，可以对倒角面进行细分，产生圆角效果</li>
<li>按F6，可以设置属性，Profile用于控制形成凸面还是凹面（曲率）</li>
</ol>
<p>注意：F6的设置会被记住，下一次倒角其参数被自动使用</p>
<div class="blog_h3"><span class="graybg">法向缩放</span></div>
<p>即Tools面板中Transform字段集中的Shrink/Fatten功能，快捷键Alt S。所谓法向缩放，就是让各面分别沿着其法线的方向进行放大或者缩小，在设计建筑时可以用到。操作步骤：</p>
<ol>
<li>Alt 右键，选取需要缩放的循环面</li>
<li>按E挤出，立即回车，不进行实质上的挤出操作</li>
<li>Alt S激活法向缩放，在缩放期间，可以按S启用Even Thickness</li>
</ol>
<p>效果图如下：<a href="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-shrink-fatten.png"><img class="aligncenter size-full wp-image-14593" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-shrink-fatten.png" alt="blender-shrink-fatten" width="450" height="274" /></a></p>
<p>顶部的凸台就是利用法线缩放生成的。 </p>
<div class="blog_h3"><span class="graybg">挤出操作</span></div>
<p>选中多个面并进行挤出时，可以按Alt E调出选项：</p>
<ol>
<li>Region，默认的挤出方式，所有面统一方向</li>
<li>Individual faces，各面沿着自己的法线方向挤出</li>
</ol>
<p>注意：E键挤出后，用<span style="background-color: #c0c0c0;">右键取消，实际上相当于0距离挤出</span>，也就是新的面已经存在。要取消，应该通过Ctrl Z。</p>
<div class="blog_h3"><span class="graybg">旋绕操作</span></div>
<p>即Tools面板Add字段集中的Spin按钮，快捷键Alt R。一般在编辑模式的正交视图下、侧面进行操作。可以让目标Mesh以3D游标为中心，旋转一定的角度。下图是平面绕旋转100度的效果示意：</p>
<p><img class="aligncenter size-full wp-image-14598" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-spin.png" alt="blender-spin" width="498" height="394" /></p>
<div class="blog_h3"><span class="graybg">刀片与选择</span></div>
<p>即Tools面板Add字段集中的刀片（切割）、Select（选择）按钮，快捷键分别为K、Shift K。</p>
<p>刀片工具，用于在Mesh表面<span style="background-color: #c0c0c0;">切分出更多的面</span>出来，操作完毕后，按Enter确认。</p>
<p>选择工具，要求首先选择一系列的面，然后Shift K绘制一个路径（直线），Enter确认。路径经过的地方，会被<span style="background-color: #c0c0c0;">切分出新的面</span>。</p>
<div class="blog_h3"><span class="graybg">切割操作</span></div>
<p>即Tools面板Add字段中的切割（Bisect）按钮，可以用一个平面来割掉Mesh的一部分。操作步骤：</p>
<ol>
<li>进入编辑模式，按A全选目标Mesh</li>
<li>点击Bisect按钮，单击一点作为平面起点，然后拖拽，到终点处再次单击，定义一个切割平面</li>
<li>按F6，Fill表示是否对截面进行填充，Clear Inner/Outer指示切除平面内侧还是外侧的哪一部分</li>
</ol>
<div class="blog_h3"><span class="graybg">交叉操作</span></div>
<p>可以在编辑模式下，根据两个几何图像的并、交、差集，确定Mesh的最终形态。例如，用立方体对球体进行打洞的例子：</p>
<ol>
<li>创建一个UV球，进入编辑模式，取消选择</li>
<li>创建一个Cube，缩放，使其穿过球体。此时Cube处于选中状态</li>
<li>按Ctrl F，选择Intersect（Boolean），按F6
<ol>
<li>Boolean取值Difference，表示保留未选中元素中、不属于选中元素的部分，即打洞</li>
<li>Boolean取值Intersect，表示保留两者共同的部分</li>
<li>Boolean取值Union，表示保留两者的并集</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">桥接边循环</span></div>
<p>可以连接多个游离平面的循环边，创建多个面。操作步骤：</p>
<ol>
<li>在编辑模式下，为一个Mesh创建多个游离的平面</li>
<li>选中所有平面，按快捷键W，选择Bridge Edge Loops，按F6
<ol>
<li>Connect Loops，可以选择开放/闭合最后一个、第一个面</li>
<li>Number of Cuts，对创建的新面进行细分</li>
</ol>
</li>
</ol>
<p>注意游离平面的顺序、旋转角度很重要。桥接也可以用于其它二维几何图形。</p>
<p>LoopTools的Bridge也可以实现类似的功能。</p>
<div class="blog_h3"><span class="graybg">增加厚度</span></div>
<p>选中若干面，按Ctrl F，选择Solidify，然后F6，可以增加厚度（立体化）。</p>
<div class="blog_h3"><span class="graybg">自动焊接</span></div>
<p>编辑模式，点击<img class="aligncenter size-full wp-image-14573 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-ico-3.png" alt="blender-ico-3" width="17" height="17" />启用吸附，右侧Snap Element选择顶点。在你移动顶点时，会自动吸附到临近的顶点，合而为一。</p>
<div class="blog_h3"><span class="graybg">管道的绘制</span></div>
<ol>
<li>添加Curve ⇨ Path，进入编辑模式。选中一个点，按E可以凸起</li>
<li>右侧曲线面板，设置Active Spline的Order为2可以绘制直角管道</li>
<li>右侧曲线面板，设置Shape为3D，Fill为Full。Bevel的Depth、Resolution可以用来绘制圆管</li>
<li>完毕后，Alt + C转换为Mesh</li>
</ol>
<div class="blog_h2"><span class="graybg">BSurfaces</span></div>
<p>使用此插件，可以方便制作拓扑曲面。</p>
<div class="blog_h3"><span class="graybg">线条转换</span></div>
<p>操作步骤：</p>
<ol>
<li>作为参考的物体，可以在右侧大纲中，点击<img class="aligncenter size-full wp-image-14572 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-ico-1.png" alt="blender-ico-1" width="16" height="16" />，禁止在Viewport中选中</li>
<li>添加一个Mesh，例如平面，进入编辑模式，删除其所有顶点。即将制作的曲面将以此Mesh为容器</li>
<li>左侧选择GP面板（Grease Pencil），Data Source选择Object，Stroke Placement选择Surface。表示手绘线体置于参考物体表面</li>
<li>按住D，在参考物体表面绘制线条，切换视角你会注意到，线条沿着物体表面延伸</li>
<li>左侧选择Tools面板，找到Bsurfaces一段，点击Add Surface，F6修改参数</li>
<li>工具栏点击<img class="aligncenter size-full wp-image-14573 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-ico-3.png" alt="blender-ico-3" width="17" height="17" />，其后的下拉框选择<img class="aligncenter size-full wp-image-14574 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-ico-2.png" alt="blender-ico-2" width="35" height="20" />，点选<img class="aligncenter size-full wp-image-14575 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-ico-4.png" alt="blender-ico-4" width="45" height="22" />。这些设置表示转换（移动/缩放/旋转）时吸附到参考物体表面</li>
<li>A全选所有元素，按一下G，不做任何变动，G仅仅是触发一下转换</li>
</ol>
<div class="blog_h3"><span class="graybg">平面吸附</span></div>
<p>类似的，你可以在执行上节第6步的情况下，转换细分过的平面，让它吸附到参考物体的表面。</p>
<div class="blog_h2"><span class="graybg">LoopTools</span></div>
<p>该插件需要启用，在编辑模式下，按W键可以看到LoopTools的菜单项</p>
<div class="blog_h3"><span class="graybg">Bridge</span></div>
<p>可以连接两对游离的循环边，生成新的面。功能类似于Bridge Edge Loops。</p>
<div class="blog_h3"><span class="graybg">Circle</span></div>
<p>可以让选中的面几何的外边缘圆化。</p>
<div class="blog_h3"><span class="graybg">Curve</span></div>
<p>选取几个采样点，基于这些点曲线化一系列相邻的边。 示例：</p>
<p><img class="aligncenter size-full wp-image-14613" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-looptools-curve.png" alt="blender-looptools-curve" width="508" height="482" /></p>
<p>图中上半部分，呈现为桔黄色的三个点，被选中作为采样点。然后按W键选择Loop Tools ⇨ Curve，即可得到下半部分的效果。</p>
<div class="blog_h3"><span class="graybg">Relax</span></div>
<p>具有平缓曲线的效果，以上图下半部分为例，选中曲线上的所有点，然后W键选择Loop Tools  ⇨ Relax，Shift R重复，即可逐步的让曲线平缓。</p>
<div class="blog_h3"><span class="graybg">GStretch</span></div>
<p>吸附倒GP线：使用蜡笔绘制一条曲线，然后选中曲线上所有点，自动连接曲线点、蜡笔Mesh化后的点，形成多个面。</p>
<div class="blog_h3"><span class="graybg">Loft</span></div>
<p>放样，可以把两个或者是多个二维曲线符合成三维物体。比如下面这个效果是利用3个在Z方向圆心重合的Circle进行Loft得到的：</p>
<p><img class="aligncenter size-full wp-image-14616" src="https://blog.gmem.cc/wp-content/uploads/2017/01/blender-looptools-loft.png" alt="blender-looptools-loft" width="314" height="449" /></p>
<div class="blog_h3"><span class="graybg">Space</span></div>
<p>等距：可以让一条曲线上的点分布的更加均匀。</p>
<div class="blog_h2"><span class="graybg">UV展开</span></div>
<p>简单操作步骤：</p>
<ol>
<li>进入编辑模式</li>
<li> 按U键，打开UV映射菜单
<ol>
<li>Unwrap，直接展开，基于缝合边设定进行展开</li>
<li>Smart UV project，智能UV映射</li>
</ol>
</li>
<li>选择智能UV映射，F6打开属性菜单
<ol>
<li>Island Margin，孤岛边距，可以设置各个展开面的边距。避免“贴图出血”</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">人工缝合</span></div>
<ol>
<li>切换到边选择模式</li>
<li>选定缝合边，所谓缝合边，是指从它们裁剪开物体的边</li>
<li>Ctrl + E，标记缝合边（Mark Seam）</li>
<li>A全选，U选择Unwrap</li>
</ol>
<p>盒子十字展开技巧：侧面四个边、顶面3个边作为缝合边。</p>
<p>圆柱展开技巧：上下底，外加任意一条竖边作为缝合边。通常选择正交视图的前/侧视图的正前方的那个边</p>
<div class="blog_h3"><span class="graybg">UV和选区同步</span></div>
<p>点击按钮<img class="aligncenter size-full wp-image-14626 inlineBlock" src="https://blog.gmem.cc/wp-content/uploads/2017/01/uvsync.png" alt="uvsync" width="22" height="23" />，则编辑模式下，选区和UV保持同步</p>
<div class="blog_h3"><span class="graybg">孤岛选择</span></div>
<p>在面选择模式下，Ctrl + L可以选择同一UV孤岛中的所有面。做复杂UV展开时可以Shift + H 隐藏未选择部分 。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/blender-faq">Blender知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/blender-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于MinGW的海康视频监控开发</title>
		<link>https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw</link>
		<comments>https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw#comments</comments>
		<pubDate>Thu, 15 May 2014 03:38:33 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Graphic]]></category>
		<category><![CDATA[MinGW]]></category>
		<category><![CDATA[Multimedia]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=3535</guid>
		<description><![CDATA[<p>工程配置 项 说明  集成开发环境 Eclipse 4.3.2 + MinGW工具链（TDM-GCC 4.8） 工程配置 工程类型：C++ Project宏定义：_WIN32、UNICODE头文件路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\include依赖库：HCNetSDK库路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\dllLinker flags：-mwindows 基于SDK直接解码的直播样例 [crayon-69d4887be84d1167244251/]</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw">基于MinGW的海康视频监控开发</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">工程配置</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">项</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>集成开发环境</td>
<td>Eclipse 4.3.2 + MinGW工具链（TDM-GCC 4.8）</td>
</tr>
<tr>
<td>工程配置</td>
<td>工程类型：C++ Project<br />宏定义：_WIN32、UNICODE<br />头文件路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\include<br />依赖库：HCNetSDK<br />库路径：D:\CPP\tools\CH-HCNetSDK\win32-4.3.0.6\dll<br />Linker flags：-mwindows</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基于SDK直接解码的直播样例</span></div>
<pre class="crayon-plain-tag">/*
 * SDKDirectPlayBack.cpp
 *
 *  Created on: May 14, 2014
 *      Author: WangZhen
 */
#include &lt;stdio.h&gt;
#include &lt;windows.h&gt;
#include "HCNetSDK.h"
#include &lt;time.h&gt;

class RealPlayInfo
{
    public:
        LONG lUserID;
        LONG lRealPlayHandle;
        RealPlayInfo() :
                lUserID( -1 ), lRealPlayHandle( -1 )
        {
        }
        ;
        bool isSucceed()
        {
            return this-&gt;lUserID &gt;= 0;
        }
};
void CALLBACK g_ExceptionCallBack( DWORD dwType, LONG lUserID, LONG lHandle, void* pUser )
{
    switch ( dwType )
    {
        case EXCEPTION_RECONNECT :
            printf( "Reconnecting...%d\n", time( NULL ) );
            break;
        default :
            break;
    }
}

RealPlayInfo* StartRealPlay( HWND hWnd, char* ip, int port = 8000, int chnl = 1, char* user = "admin", char* pswd = "12345" )
{
    RealPlayInfo* info = new RealPlayInfo;
    NET_DVR_DEVICEINFO_V30 struDeviceInfo;
    info-&gt;lUserID = NET_DVR_Login_V30( ip, 8000, user, pswd, &amp;struDeviceInfo ); //登录
    if ( info-&gt;lUserID &lt; 0 )
    {
        printf( "Failed to logged on to %s, Error Code: %d\n", ip, NET_DVR_GetLastError() );
        return info;
    }
    else
    {
        printf( "Logon successful.\n" );
    }
    NET_DVR_SetExceptionCallBack_V30( 0, NULL, g_ExceptionCallBack, NULL );
    //启动预览并设置回调数据流
    NET_DVR_PREVIEWINFO struPlayInfo = { 0 };
    //使用SDK直接解码
    struPlayInfo.hPlayWnd = hWnd;
    //预览通道号
    struPlayInfo.lChannel = chnl;
    //0主码流，1子码流
    struPlayInfo.dwStreamType = 0;
    //0 TCP，1 UDP，2 多播，3 RTP，4 RTP/RTSP，5 RSTP/HTTP
    struPlayInfo.dwLinkMode = 0;
    //0- 非阻塞取流，1- 阻塞取流
    struPlayInfo.bBlocked = 1;
    info-&gt;lRealPlayHandle = NET_DVR_RealPlay_V40( info-&gt;lUserID, &amp;struPlayInfo, NULL, NULL );
    if ( info-&gt;lRealPlayHandle &lt; 0 )
    {
        printf( "Failed to start RealPlay.\n" );
        NET_DVR_Logout( info-&gt;lUserID );
        return info;
    }
    else
    {
        printf( "RealPlay started successfully.\n" );
    }
    return info;
}
void StopRealPlay( RealPlayInfo* info )
{
    if ( !info-&gt;isSucceed() ) return;
    printf( "Stopping RealPlay.\n" );
    //停止预览
    NET_DVR_StopRealPlay( info-&gt;lRealPlayHandle );
    //注销用户
    NET_DVR_Logout( info-&gt;lUserID );
}

LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ); //窗口过程声明

int main()
{
    HINSTANCE hInstance = GetModuleHandle( NULL );
    static TCHAR szAppName[] = TEXT( "HikSDKDirectPlayBack" );
    HWND hWnd;
    MSG msg;
    WNDCLASS wndclass;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION );
    wndclass.hCursor = LoadCursor( NULL, IDC_ARROW );
    wndclass.hbrBackground = ( HBRUSH ) GetStockObject( BLACK_BRUSH );
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szAppName;
    RegisterClass( &amp;wndclass );
    int w = 640;
    int h = 480;
    hWnd = CreateWindow(
            szAppName,
            TEXT("MinGW海康SDK直播实例"),
            WS_OVERLAPPEDWINDOW,
            (GetSystemMetrics(SM_CXSCREEN) - w)/2,
            (GetSystemMetrics(SM_CYSCREEN) - h)/2,
            w,
            h,
            NULL,
            NULL,
            hInstance,
            NULL
    );
    printf( "Main window created.\n" );
    printf( "Prepare to render main window.\n" );
    ShowWindow( hWnd, 10 );
    UpdateWindow( hWnd );
    while ( GetMessage( &amp;msg, NULL, 0, 0 ) )
    {
        TranslateMessage( &amp;msg );
        DispatchMessage( &amp;msg );
    }
    return msg.wParam;
}
//窗口过程定义
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam )
{
    HDC hdc;
    PAINTSTRUCT ps;
    RECT rect;
    static RealPlayInfo* info;
    switch ( message )
    {
        case WM_CREATE :
            NET_DVR_Init();
            NET_DVR_SetConnectTime( 2000, 1 );
            NET_DVR_SetReconnect( 10000, true );
            info = StartRealPlay( hwnd, "192.168.0.196" );
            return 0;
        case WM_DESTROY :
            PostQuitMessage( 0 );
            StopRealPlay( info );
            NET_DVR_Cleanup();
            return 0;
    }
    return DefWindowProc( hwnd, message, wParam, IParam );
}</pre> 
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw">基于MinGW的海康视频监控开发</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/hikvision-video-monitoring-development-with-mingw/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>人像处理技巧</title>
		<link>https://blog.gmem.cc/facial-image-processing</link>
		<comments>https://blog.gmem.cc/facial-image-processing#comments</comments>
		<pubDate>Sun, 27 May 2012 08:36:35 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>
		<category><![CDATA[Photoshop]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=5614</guid>
		<description><![CDATA[<p>美白技巧 利用混合模式美白 原理：使用滤色混合模式，可以使底层图像变亮，让人的皮肤看起来更白 步骤： 使用套索勾画出皮肤轮廓的选区，羽化10像素左右 CTRL+J将选取复制到新图层 设置新图层的混合模式为滤色，不透明度为45% 为新图层添加图层蒙板 点击图层蒙板，前景色设置为白色，然后使用不透明度50%的橡皮擦修改边缘</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/facial-image-processing">人像处理技巧</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">美白技巧</span></div>
<div class="blog_h3"><span class="graybg">利用混合模式美白</span></div>
<p>原理：使用滤色混合模式，可以使底层图像变亮，让人的皮肤看起来更白</p>
<p>步骤：</p>
<ol>
<li>使用套索勾画出皮肤轮廓的选区，羽化10像素左右</li>
<li>CTRL+J将选取复制到新图层</li>
<li>设置新图层的混合模式为滤色，不透明度为45%</li>
<li>为新图层添加图层蒙板</li>
<li>点击图层蒙板，前景色设置为白色，然后使用不透明度50%的橡皮擦修改边缘</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/facial-image-processing">人像处理技巧</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/facial-image-processing/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>图像处理知识集锦</title>
		<link>https://blog.gmem.cc/image-processing-faq</link>
		<comments>https://blog.gmem.cc/image-processing-faq#comments</comments>
		<pubDate>Wed, 02 Mar 2011 06:51:53 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15800</guid>
		<description><![CDATA[<p>基本概念 色彩空间 也叫做色彩模型、色彩系统，基于色素的基本颜色（例如发光的三原色RGB）进行组合（例如相加）能够得到的所有颜色的集合，定义了一个颜色空间。 颜色空间也被指代用于简化对其的描述的抽象数学模型，这些模型把色彩定义为元组（通常包括3或者4元素），你可以把每个颜色看做N维（元组元素个数）空间中的一个点。 色彩空间帮助理解设备的色彩处理能力，例如摄像头能看到什么颜色、显示器能渲染什么颜色、打印机能打印什么颜色。 常用色彩空间： 类别 空间 说明 RGB 基于红、绿、蓝三种原色的叠加描述颜色。每个颜色的浓度在0-255之间，一共可以表示24位（16,777,216种）颜色 RGB是最常用的色彩模型，原因是它适合计算机处理，而且和人类的视觉系统类似。它包含两种实现： sRGB，即标准RGB。由HP、微软于77年开发，在WWW中广泛使用 Adobe RGB，比sRGB更加光的色彩空间。包含CMYK色域（gamut） HSL/HSV 是基于RGB色彩空间（通常sRGB）的转换： HSV ，基于色相、饱和度、亮度来描述颜色，也叫HSB HSL，基于色相、饱和度、照度（lightness）来描述颜色 CMYK 主要用于打印领域，基于青色、品红、黄色、黑色的相减来描述颜色 <a class="read-more" href="https://blog.gmem.cc/image-processing-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/image-processing-faq">图像处理知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">基本概念</span></div>
<div class="blog_h3"><span class="graybg"><a id="color-space"></a>色彩空间</span></div>
<p>也叫做色彩模型、色彩系统，基于色素的基本颜色（例如发光的三原色RGB）进行组合（例如相加）能够得到的所有颜色的集合，定义了一个颜色空间。</p>
<p>颜色空间也被指代用于简化对其的描述的抽象数学模型，这些模型把色彩定义为元组（通常包括3或者4元素），你可以把每个颜色看做N维（元组元素个数）空间中的一个点。</p>
<p>色彩空间帮助理解设备的色彩处理能力，例如摄像头能看到什么颜色、显示器能渲染什么颜色、打印机能打印什么颜色。</p>
<p>常用色彩空间：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 12%; text-align: center;">类别</td>
<td style="width: 10%; text-align: center;">空间</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>RGB</td>
<td colspan="2">
<p>基于红、绿、蓝三种原色的叠加描述颜色。每个颜色的浓度在0-255之间，一共可以表示24位（<span style="color: #4d4d4d;">16,777,216种）颜色</span></p>
<p>RGB是最常用的色彩模型，原因是它适合计算机处理，而且和人类的视觉系统类似。它包含两种实现：</p>
<ol>
<li>sRGB，即标准RGB。由HP、微软于77年开发，在WWW中广泛使用</li>
<li>Adobe RGB，比sRGB更加光的色彩空间。包含CMYK色域（gamut）</li>
</ol>
</td>
</tr>
<tr>
<td>HSL/HSV</td>
<td colspan="2">
<p>是基于RGB色彩空间（通常sRGB）的转换：</p>
<ol>
<li>HSV ，基于色相、饱和度、亮度来描述颜色，也叫HSB</li>
<li>HSL，基于色相、饱和度、照度（lightness）来描述颜色</li>
</ol>
</td>
</tr>
<tr>
<td>CMYK</td>
<td colspan="2">
<p>主要用于打印领域，基于青色、品红、黄色、黑色的相减来描述颜色</p>
</td>
</tr>
<tr>
<td rowspan="2">明度+色度</td>
<td colspan="2"> 基于亮度（luma）和色度（chroma）两个维度来描述颜色。这类模型的优势是亮度信息和色彩信息分离，可以很好的兼容彩色、黑白信号</td>
</tr>
<tr>
<td>
<p>YUV</p>
<p>也叫</p>
<p>YCbCr</p>
</td>
<td>
<p>通常用于视频处理管线中，在编码时，利用人类视觉的特点 —— 对亮度比色度更加敏感——减少色度分量（component）的空间占用而不失真</p>
<p>YUV三个字母中，Y表示亮度分量，U、V表示色度分量 —— 红色、蓝色两种颜色的浓度。当Y'为0.5时，UV平面的颜色映射如下图：</p>
<p><img class="aligncenter size-full wp-image-15810" src="https://blog.gmem.cc/wp-content/uploads/2011/03/yuv-uv-plane.png" alt="yuv-uv-plane" width="300" height="300" /></p>
<p>从RGB转换为YUV可以基于以下公式：</p>
<p style="padding-left: 30px;">Y = (0.257 * R) + (0.504 * G) + (0.098 * B) + 16<br />Cr = V = (0.439 * R) - (0.368 * G) - (0.071 * B) + 128<br />Cb = U = -( 0.148 * R) - (0.291 * G) + (0.439 * B) + 128</p>
<p>从YUV转换为RGB可以基于以下公式：</p>
<p style="padding-left: 30px;">B = 1.164(Y - 16) + 2.018(U - 128)<br />G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)<br />R = 1.164(Y - 16) + 1.596(V - 128)</p>
<p>YUV主要有两类存储格式：</p>
<ol>
<li>Packed格式：每个像素的Y单独保存，但是几个相邻点的U/V分量，使用同一个值表示，这几个合并表示的点称为宏像素（macro-pixel）：
<ol>
<li>4:4:4采样：U、V采样不减少，YUV各占一个字节</li>
<li>4:2:2采样：U、V采样减半，例如第一个像素仅仅采样U，第二个仅仅采样V</li>
<li>4:2:0采样：隔行采样U、V，第一行仅仅采样U，第二行仅仅采样V</li>
<li>4:1:1采样：每隔四个点采样一次U、V分量</li>
</ol>
</li>
<li>Planar：将Y、U、V使用三个独立的数组表示
<ol>
<li><a id="yv12"></a><a href="https://www.fourcc.org/pixel-format/yuv-yv12/">YV12</a>，很多MPEG软件编码器选择此格式，Y正常采样，U/V在水平和垂直方向各行/列采样</li>
</ol>
</li>
</ol>
</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/image-processing-faq">图像处理知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/image-processing-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Photoshop知识集锦</title>
		<link>https://blog.gmem.cc/photoshop-faq</link>
		<comments>https://blog.gmem.cc/photoshop-faq#comments</comments>
		<pubDate>Mon, 07 Mar 2005 03:09:37 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Graphic]]></category>
		<category><![CDATA[Photoshop]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=3787</guid>
		<description><![CDATA[<p>常用快捷键 分类 快捷键   说明 选区操作 Ctrl + Enter 路径转换为选区，可以用钢笔勾画出闭合路径，再使用此快捷键  Ctrl + Mouse1 点击图层、通道面板上的图层缩略图，可以把当前图层上的图像载入选区 Ctrl + H 隐藏选区  图像操作 Ctrl + Alt + <a class="read-more" href="https://blog.gmem.cc/photoshop-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/photoshop-faq">Photoshop知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">常用快捷键</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 120px; text-align: center;">分类</td>
<td style="width: 170px; text-align: center;">快捷键 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;" rowspan="3">选区操作</td>
<td>Ctrl + Enter</td>
<td>路径转换为选区，可以用钢笔勾画出闭合路径，再使用此快捷键 </td>
</tr>
<tr>
<td>Ctrl + Mouse1</td>
<td>点击图层、通道面板上的图层缩略图，可以把当前图层上的图像载入选区</td>
</tr>
<tr>
<td>Ctrl + H</td>
<td>隐藏选区</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="2"> 图像操作</td>
<td>Ctrl + Alt + I</td>
<td>修改图像大小</td>
</tr>
<tr>
<td>Ctrl + Alt + C</td>
<td>修改画布大小</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="9">图层操控</td>
<td>Ctrl + J</td>
<td>复制图层</td>
</tr>
<tr>
<td>Ctrl + [</td>
<td>向下移动图层</td>
</tr>
<tr>
<td>Ctrl + Shift + [</td>
<td>移动图层到最底层</td>
</tr>
<tr>
<td>Ctrl + ]</td>
<td>向上移动图层</td>
</tr>
<tr>
<td>Ctrl + Shift + ]</td>
<td>移动图层到最顶层</td>
</tr>
<tr>
<td>数字1-10</td>
<td>调整图层的不透明度</td>
</tr>
<tr>
<td>Shift + Mouse1</td>
<td>选中图层后，按住拖动，可以跨文件复制图层</td>
</tr>
<tr>
<td>Alt + Delete</td>
<td>使用前景色填充</td>
</tr>
<tr>
<td>Ctrl + Delete</td>
<td>使用背景色填充</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="6">栅格图层调整</td>
<td>Ctrl + I</td>
<td>反相</td>
</tr>
<tr>
<td>Ctrl + L</td>
<td>色阶调整</td>
</tr>
<tr>
<td>Ctrl + U</td>
<td>色相、饱和度、明度调整</td>
</tr>
<tr>
<td>Ctrl + B</td>
<td>色彩平衡</td>
</tr>
<tr>
<td>Ctrl + Shift + Alt + B</td>
<td>黑白</td>
</tr>
<tr>
<td>Ctrl + Shift + U</td>
<td>去掉颜色</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="8">文本操作</td>
<td>ctrl + shift + K</td>
<td>全部转为大写</td>
</tr>
<tr>
<td>Ctrl+Shift + ,</td>
<td>选中文本，可以减小文本尺寸</td>
</tr>
<tr>
<td>Ctrl+Shift + .</td>
<td>选中文本，可以加大文本尺寸</td>
</tr>
<tr>
<td>Alt + Left</td>
<td>光标置于文本，减小文字间距</td>
</tr>
<tr>
<td>Alt + Right</td>
<td>光标置于文本，加大文字间距</td>
</tr>
<tr>
<td>Alt + Up</td>
<td>光标选中多行文本，减小行距</td>
</tr>
<tr>
<td>Alt + Down</td>
<td>光标选中多行文本，加大行距</td>
</tr>
<tr>
<td>Shift + Alt + Up</td>
<td>移动文本基线</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="3">蒙版操作</td>
<td>Alt + Mouse1</td>
<td>点击蒙版，可以显示当前图层的蒙版信息</td>
</tr>
<tr>
<td>Shift + Mouse1</td>
<td>点击蒙版，可以禁用或启用蒙版</td>
</tr>
<tr>
<td>Ctrl + I</td>
<td>选中蒙版，可以反相</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="4">调板与工具</td>
<td>X</td>
<td>切换前背景色</td>
</tr>
<tr>
<td>Tab</td>
<td>隐藏/显示所有调板</td>
</tr>
<tr>
<td>Ctrl+Backspace</td>
<td>重置对话框参数</td>
</tr>
<tr>
<td>Shift + ?</td>
<td>
<p>在一个类别的工具里面切换：</p>
<p>V 移动<br />M 矩形选框、椭圆选框<br />L 套索、多边形套索、磁性套索<br />W 快速选择、魔棒<br />C 裁剪、透视裁剪、切片、切片选择<br />I 吸管、取色器、标尺<br />J 污点修复、修复、修补、内容感知移动、红眼<br />B 画笔、铅笔<br />S 仿制图章、图案图章<br />Y 画笔历史记录<br />E 橡皮、背景橡皮<br />G 油漆桶、渐变<br />O 减淡、加深、海绵<br />P 钢笔、自由钢笔<br />T 文字、直排文字<br />A 路径选择、直接选择<br />U 椭圆、圆角矩形、多边形、直线、自定义多边形<br />H 抓手<br />R 旋转</p>
</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="7">整体缩放与平移</td>
<td>Alt + Wheel</td>
<td>缩放图像</td>
</tr>
<tr>
<td>Wheel</td>
<td>上下平移图像</td>
</tr>
<tr>
<td>Ctrl + Wheel</td>
<td>上下平移图像（快速）</td>
</tr>
<tr>
<td>Shfit + Wheel</td>
<td>左右平移图像</td>
</tr>
<tr>
<td>Ctrl + Shfit + Wheel</td>
<td>左右平移图像（快速）</td>
</tr>
<tr>
<td>Ctrl + Space + Mouse1</td>
<td>左右滑动鼠标可以缩小、放大图像</td>
</tr>
<tr>
<td>H + Mouse1</td>
<td>使图像大小临时适合屏幕</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="8">文件操作</td>
<td>Ctrl + W</td>
<td>关闭</td>
</tr>
<tr>
<td>Ctrl + Alt + W</td>
<td>全部关闭</td>
</tr>
<tr>
<td>Ctrl + S</td>
<td>保存当前图片或者PSD</td>
</tr>
<tr>
<td>Ctrl + Shift + S</td>
<td>另存为支持的格式</td>
</tr>
<tr>
<td>Ctrl + Shift + Alt + S</td>
<td>存储为网页适用的格式，可以进行切片</td>
</tr>
<tr>
<td>Ctrl + Z</td>
<td>撤销上一步操作，再次点击还原上一步操作</td>
</tr>
<tr>
<td>Ctrl + Alt + Z</td>
<td>撤销一步</td>
</tr>
<tr>
<td>Ctrl + Shift + Z</td>
<td>重做一步</td>
</tr>
<tr>
<td style="text-align: center;" rowspan="2">画笔 </td>
<td>Ctrl + Alt + Mouse2</td>
<td>左右滑动调整画笔的大小 </td>
</tr>
<tr>
<td>Shift + Mouse1</td>
<td>画直线</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">常用技巧</span></div>
<div class="blog_h3"><span class="graybg">形状绘制</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 200px; text-align: center;"> 目标</td>
<td style="text-align: center;">操作步骤 </td>
</tr>
</thead>
<tbody>
<tr>
<td>以单击点位中心绘制</td>
<td>选择椭圆、矩形工具，按住Alt，并拖动</td>
</tr>
<tr>
<td>绘制正方形、圆形</td>
<td>选择椭圆、矩形工具，按住Shfit，并拖动</td>
</tr>
</tbody>
</table>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/photoshop-faq">Photoshop知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/photoshop-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
