<?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; Web</title>
	<atom:link href="https://blog.gmem.cc/category/work/web/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Sun, 12 Apr 2026 02:07:19 +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>TypeScript学习笔记</title>
		<link>https://blog.gmem.cc/typescript</link>
		<comments>https://blog.gmem.cc/typescript#comments</comments>
		<pubDate>Mon, 27 Apr 2020 13:28:42 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=39015</guid>
		<description><![CDATA[<p>安装和配置 安装 执行下面的命令安装TypeScript： [crayon-69db90ad694b2614607227/] 语言基础 数据类型 TypeScript支持类型提示，类型提示让代码自动完成更加准确： 基本类型 [crayon-69db90ad694b8108205779/] 数组和元组 [crayon-69db90ad694bb227209545/] 枚举 [crayon-69db90ad694bd122681023/] any [crayon-69db90ad694bf046174586/] void [crayon-69db90ad694c2154451837/] undefined和null  [crayon-69db90ad694c4062486895/] 默认情况下[crayon-69db90ad694c6820621754-i/]和[crayon-69db90ad694c8502459309-i/]是所有类型的子类型。 就是说你可以把 null和undefined赋值给任何类型的变量。但是如果指定了[crayon-69db90ad694ca499999215-i/]标记，则只能赋值给void或者各自的类型 <a class="read-more" href="https://blog.gmem.cc/typescript">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/typescript">TypeScript学习笔记</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">安装</span></div>
<p>执行下面的命令安装TypeScript：</p>
<pre class="crayon-plain-tag">npm install -g typescript</pre>
<div class="blog_h1"><span class="graybg">语言基础</span></div>
<div class="blog_h2"><span class="graybg">数据类型</span></div>
<p>TypeScript支持类型提示，类型提示让代码自动完成更加准确：</p>
<div class="blog_h3"><span class="graybg">基本类型</span></div>
<pre class="crayon-plain-tag">// 布尔
let isDone: boolean = false;
// 数字
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
// 字符串
let name: string = "bob";
// 模板字符串
let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;</pre>
<div class="blog_h3"><span class="graybg">数组和元组</span></div>
<pre class="crayon-plain-tag">// 数组
let list: number[] = [1, 2, 3];
// 数组，泛型语法
let list: Array&lt;number&gt; = [1, 2, 3];

// 元组
let x: [string, number];
x = ['hello', 10];</pre>
<div class="blog_h3"><span class="graybg">枚举</span></div>
<pre class="crayon-plain-tag">// 枚举
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
// 默认情况下，枚举值从0开始，可以修改
enum Color {Red = 1, Green, Blue}</pre>
<div class="blog_h3"><span class="graybg">any</span></div>
<pre class="crayon-plain-tag">// 不限定的类型
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
// 你可以假设不限定类型有任何方法，编译器不会检查
notSure.ifItExists();
// 不限定类型数组
let list: any[] = [1, true, "free"];</pre>
<div class="blog_h3"><span class="graybg">void</span></div>
<pre class="crayon-plain-tag">// Void类型
// 用于表示没有返回值的函数
function warnUser(): void {
    console.log("This is my warning message");
}
// 只能赋值undefined或null
let unusable: void = undefined;</pre>
<div class="blog_h3"><span class="graybg">undefined和null </span></div>
<pre class="crayon-plain-tag">// undefined 和 null类型
let u: undefined = undefined;
let n: null = null;</pre>
<p>默认情况下<pre class="crayon-plain-tag">null</pre>和<pre class="crayon-plain-tag">undefined</pre>是所有类型的子类型。 就是说你可以把 null和undefined赋值给任何类型的变量。但是如果指定了<pre class="crayon-plain-tag">--strictNullChecks</pre>标记，则只能赋值给void或者各自的类型 —— null赋值给null，undefined赋值给undefined。</p>
<div class="blog_h3"><span class="graybg">never </span></div>
<pre class="crayon-plain-tag">// never 永远不存在值的类型，例如：总是抛出异常或者不会有返回值的函数或者箭头函数表达式
// 总是抛出异常
function error(message: string): never {
    throw new Error(message);
}
// 编译器可以推导出返回值为never类型
function fail() {
    return error("Something failed");
}
// 永远不会返回
function infiniteLoop(): never {
    while (true) {
    }
}</pre>
<div class="blog_h3"><span class="graybg">object</span></div>
<p>表示非基本类型：即非number，string，boolean，symbol，null或undefined。</p>
<div class="blog_h3"><span class="graybg">类型断言 </span></div>
<p>尖括号语法：</p>
<pre class="crayon-plain-tag">let someValue: any = "this is a string";

let strLength: number = (&lt;string&gt;someValue).length;</pre>
<p><pre class="crayon-plain-tag">as</pre>语法：</p>
<pre class="crayon-plain-tag">let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;</pre>
<div class="blog_h2"><span class="graybg">操作符</span></div>
<div class="blog_h3"><span class="graybg">keyof</span></div>
<p>以一个对象类型为参数，产生字符串或者数字的字面值的联合，这些数字或者字符串来自对象的键：</p>
<pre class="crayon-plain-tag">type Point = { x: number; y: number };
type P = keyof Point; // "x" | "y"

// 如果目标类型具有字符串或数字的索引签名，则keyof操作符会返回对应的类型（而非字面值）的联合

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // type A = number

type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // type M = string | number
// 注意这里的M是 string | number的联合，这是因为JavaScript中对象的键，会被强制转换为字符串
// obj[0]和obj["0"]总是一样的 </pre>
<div class="blog_h3"><span class="graybg">! 非空断言</span></div>
<p>后缀的感叹号!表示非空断言操作符，其意义是：</p>
<ol>
<li><pre class="crayon-plain-tag">x!</pre> 将从 x 值对应的<span style="background-color: #c0c0c0;">类型集合中排除 null 和 undefined 的类型</span>。比如 x 可能是 string | undefined，则 x! 类型缩窄为 string</li>
<li><pre class="crayon-plain-tag">null!</pre>在类型检测器没法正确推断类型情况下，告知编译器值不可能为undefined或者null</li>
</ol>
<div class="blog_h3"><span class="graybg">!!强制转布尔</span></div>
<p>因为一个叹号区反，得到的是布尔值，再次取反相当于恢复“原值”（的布尔转型）。</p>
<div class="blog_h3"><span class="graybg">??空值合并</span></div>
<p><pre class="crayon-plain-tag">a ?? b</pre>仅仅当a为null或者undefined时，返回操作符右侧表达式即b，否则返回a。</p>
<div class="blog_h2"><span class="graybg">接口</span></div>
<p>TypeScript的核心原则是，对所有结构基于鸭子类型识别，接口用来定义对象具有哪些字段，或者定义函数的规格。</p>
<div class="blog_h3"><span class="graybg">指定属性</span></div>
<pre class="crayon-plain-tag">interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);</pre>
<div class="blog_h3"><span class="graybg">可选属性 </span></div>
<pre class="crayon-plain-tag">interface SquareConfig {
  color?: string;
  width?: number;
}</pre>
<div class="blog_h3"><span class="graybg">只读属性 </span></div>
<pre class="crayon-plain-tag">interface Point {
    readonly x: number;
    readonly y: number;
}</pre>
<div class="blog_h3"><span class="graybg">函数类型 </span></div>
<p>接口可以用来描述函数的签名：</p>
<pre class="crayon-plain-tag">interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result &gt; -1;
}</pre>
<p>接口中可以声明多个函数签名，这样的接口表示多个重载函数版本的联合：</p>
<pre class="crayon-plain-tag">interface Func1 {
    (v1: number, v2: number): number;
    (v1: string, v2: string): string;
}

const f1: Func1 = (v1, v2): any =&gt; {
    return v1 + v2;
};

console.log(f1("1", "2"));
console.log(f1(1, 2));</pre>
<div class="blog_h3"><span class="graybg">可索引类型 </span></div>
<p>可以用来指定数组的值类型、映射的键值类型：</p>
<pre class="crayon-plain-tag">interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

// 防止给索引赋值
interface ReadonlyStringArray {
    readonly [index: number]: string;
}


interface User {
  id: string;
  name: string;
  [key: string]: any;
}

let user: User = {
  id: "alex",
  name: "Alex"
  prop1: 1,
  "prop2": 2
};</pre>
<p>TypeScript允许字符串、数字作为索引。 </p>
<div class="blog_h3"><span class="graybg">类类型 </span></div>
<p>接口可以用来限制一个类必须满足的规约：</p>
<pre class="crayon-plain-tag">interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}



interface ClockInterface {
    currentTime: Date;
    // 定义方法
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}</pre>
<div class="blog_h3"><span class="graybg">接口的继承</span></div>
<pre class="crayon-plain-tag">interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}
// 支持多重继承
interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = &lt;Square&gt;{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;</pre>
<div class="blog_h3"><span class="graybg">混合类型 </span></div>
<p>接口可以描述一个函数，在限定函数签名的同时，指定函数具有的额外属性： </p>
<pre class="crayon-plain-tag">interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = &lt;Counter&gt;function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;</pre>
<div class="blog_h3"><span class="graybg">让接口继承类</span></div>
<p>当让接口继承一个类时，它会获得所有类的成员，但是不会获得这些成员的实现。</p>
<p>包括私有、保护成员，都会被继承。这种情况下，只有接口所继承的那个类的子类，才能实现该接口：</p>
<pre class="crayon-plain-tag">class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}


// 错误：“Image”类型缺少“state”属性。
class Image implements SelectableControl {
    select() { }
}</pre>
<div class="blog_h2"><span class="graybg">类</span></div>
<pre class="crayon-plain-tag">// 抽象类
abstract class Greeter {
    // 静态属性
    static cname: string = "Greeter";
    // 实例变量
    greeting: string;
    // 可选的访问限定符 private protected public
    private name: string;
    // 只读成员
    readonly age: number;
    // 构造函数
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

// 继承
class Dog extends Greeter {
    greet() {
        console.log('Woof! Woof!');
        // 调用父类的方法
        super.greet();
    }
}</pre>
<div class="blog_h2"><span class="graybg">函数 </span></div>
<div class="blog_h3"><span class="graybg">函数类型提示</span></div>
<pre class="crayon-plain-tag">//         类型提示
let myAdd: (x: number, y: number) =&gt; number =  function(x: number, y: number): number { return x + y; };</pre>
<div class="blog_h3"><span class="graybg">可选参数 </span></div>
<pre class="crayon-plain-tag">function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

let result1 = buildName("Bob");  // works correctly now
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters</pre>
<div class="blog_h3"><span class="graybg">默认参数 </span></div>
<pre class="crayon-plain-tag">function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}</pre>
<p>当调用该函数的时候，传入undefined或者没有传递，则默认参数生效。</p>
<p>在所有必须参数之后的，带有默认值的参数，都是可选参数。</p>
<div class="blog_h3"><span class="graybg">函数重载</span></div>
<p>给予不同的类型提示，即可实现函数重载。 </p>
<div class="blog_h2"><span class="graybg">泛型</span></div>
<div class="blog_h3"><span class="graybg">泛型函数</span></div>
<pre class="crayon-plain-tag">// 函数参数的泛型化
function identity&lt;T&gt;(arg: T): T {
    return arg;
}
// 泛型函数的类型提示
let myIdentity: &lt;T&gt;(arg: T) =&gt; T = identity;

// 显式指定类型参数
let output = identity&lt;string&gt;("myString");
// 自动推导类型参数
let output = identity("myString");</pre>
<div class="blog_h3"><span class="graybg">泛型接口 </span></div>
<pre class="crayon-plain-tag">// 某个函数泛型化
interface GenericIdentityFn {
    &lt;T&gt;(arg: T): T;
}
function identity&lt;T&gt;(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;


// 整个接口泛型化
interface GenericIdentityFn&lt;T&gt; {
    (arg: T): T;
}
function identity&lt;T&gt;(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn&lt;number&gt; = identity;</pre>
<div class="blog_h3"><span class="graybg">泛型类</span></div>
<pre class="crayon-plain-tag">class GenericNumber&lt;T&gt; {
    zeroValue: T;
    add: (x: T, y: T) =&gt; T;
}

let myGenericNumber = new GenericNumber&lt;number&gt;();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };</pre>
<div class="blog_h3"><span class="graybg">泛型约束 </span></div>
<pre class="crayon-plain-tag">// 规定实际类型必须继承自Lengthwise
function loggingIdentity&lt;T extends Lengthwise&gt;(arg: T): T {
    console.log(arg.length);
    return arg;
} </pre>
<div class="blog_h2"><span class="graybg">枚举</span></div>
<div class="blog_h3"><span class="graybg">字符串枚举</span></div>
<pre class="crayon-plain-tag">enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}</pre>
<div class="blog_h3"><span class="graybg">异构枚举</span></div>
<pre class="crayon-plain-tag">enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}</pre>
<div class="blog_h3"><span class="graybg">外部枚举 </span></div>
<p>用于描述已经存在的枚举类型的规格：</p>
<pre class="crayon-plain-tag">declare enum Enum {
    A = 1,
    B,
    C = 2
}</pre>
<div class="blog_h2"><span class="graybg">高级类型</span></div>
<div class="blog_h3"><span class="graybg">交叉类型</span></div>
<p>交叉类型包含所有指定的类型的特征，例如， Person &amp; Serializable &amp; Loggable同时是 Person 和 Serializable 和 Loggable。</p>
<pre class="crayon-plain-tag">function extend&lt;T, U&gt;(first: T, second: U): T &amp; U {
    let result = &lt;T &amp; U&gt;{};
    for (let id in first) {
        (&lt;any&gt;result)[id] = (&lt;any&gt;first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (&lt;any&gt;result)[id] = (&lt;any&gt;second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();</pre>
<div class="blog_h3"><span class="graybg">联合类型 </span></div>
<p>允许是指定类型集合中的任何一个： </p>
<pre class="crayon-plain-tag">interface Bird {
    fly();
    layEggs();
}
interface Fish {
    swim();
    layEggs();
}

function isFish(pet: Fish | Bird): {
    return (&lt;Fish&gt;pet).swim !== undefined;
}</pre>
<div class="blog_h3"><span class="graybg">类型别名 </span></div>
<p>可以用于任何类型：</p>
<pre class="crayon-plain-tag">type Name = string;
type NameResolver = () =&gt; string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}</pre>
<p>类型别名也可以是泛型的：</p>
<pre class="crayon-plain-tag">type Container&lt;T&gt; = { value: T };</pre>
<p>甚至可以在类型别名的属性中，引用自己：</p>
<pre class="crayon-plain-tag">type Tree&lt;T&gt; = {
    value: T;
    left: Tree&lt;T&gt;;
    right: Tree&lt;T&gt;;
}</pre>
<p>可以将字符值联合起来，形成类似于枚举的效果：</p>
<pre class="crayon-plain-tag">type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}</pre>
<p>可以为联合类型定义别名：</p>
<pre class="crayon-plain-tag">type Shape = Square | Rectangle | Circle;</pre>
<div class="blog_h2"><span class="graybg">模块 </span></div>
<p>从TypeScript 1.5开始， “内部模块”称做“命名空间”。 “外部模块”简称为“模块”。</p>
<p>TypeScript与ECMAScript 2015一样，<span style="background-color: #c0c0c0;">任何包含顶级import或者export的文件都被当成一个模块</span>。相反地，<span style="background-color: #c0c0c0;">如果一个文件不带有顶级的import或者export声明，那么它的内容被视为全局可见的</span>（因此对模块也是可见的）。</p>
<div class="blog_h3"><span class="graybg">导出 </span></div>
<p>任何声明都可以通过export关键字来导出：</p>
<pre class="crayon-plain-tag">export interface StringValidator {
    isAcceptable(s: string): boolean;
}

export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 &amp;&amp; numberRegexp.test(s);
    }
}



// 先定义再导出
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 &amp;&amp; numberRegexp.test(s);
    }
}
export { ZipCodeValidator };

// 导出时指定导出名
export { ZipCodeValidator as mainValidator };



// 重新导出其它模块中的对象
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
// 全部重新导出
export * from "./StringValidator"; // exports interface StringValidator</pre>
<div class="blog_h3"><span class="graybg">导入 </span></div>
<pre class="crayon-plain-tag">import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();


// 导入并重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();


// 将整个模块导入到一个变量
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();</pre>
<div class="blog_h3"><span class="graybg">默认导出 </span></div>
<p>每个模块可有最多一个默认导出：</p>
<pre class="crayon-plain-tag">declare let $: JQuery;
export default $;</pre>
<p>默认导出在导入时，可以使用任何名字：</p>
<pre class="crayon-plain-tag">import jq from "JQuery";

jq("button.continue").html( "Next Step..." );</pre>
<p>默认导出可以的可以是匿名函数，因为它的名字不重要：</p>
<pre class="crayon-plain-tag">const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 &amp;&amp; numberRegexp.test(s);
}</pre>
<p>你甚至可以导出一个字面值：</p>
<pre class="crayon-plain-tag">export default "123";</pre>
<div class="blog_h2"><span class="graybg">命名空间</span></div>
<p>命名空间（以前称为内部模块）可以用来组织代码。</p>
<p>命名空间可以分散在多个文件中： </p>
<pre class="crayon-plain-tag">namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}</pre>
<p>需要利用引用标签，来说明不同文件之间的依赖关系：</p>
<pre class="crayon-plain-tag">/// &lt;reference path="Validation.ts" /&gt;
namespace Validation {
    const lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}</pre>
<p>使用命名空间中定义的对象的语法：</p>
<pre class="crayon-plain-tag">let validators: { [s: string]: Validation.StringValidator; } = {};

validators["Letters only"] = new Validation.LettersOnlyValidator();</pre>
<div class="blog_h3"><span class="graybg">别名</span> </div>
<p>可以为命名空间设置别名，简化访问：</p>
<pre class="crayon-plain-tag">namespace Shapes {
    export namespace Polygons {
        export class Triangle { }
        export class Square { }
    }
}

import polygons = Shapes.Polygons;</pre>
<div class="blog_h1"><span class="graybg">JSX </span></div>
<p>JSX是一种源于React的类似于XML的语法，可以嵌入在JavaScript的源码中。</p>
<p>要在TypeScript中使用JSX，你需要：</p>
<ol>
<li>给文件一个.tsx扩展名</li>
<li>启用jsx选项，利用--jsx命令行标记或者在tsconfig.json中配置</li>
</ol>
<p>TypeScript具有三种JSX模式：preserve，react和react-native。 这些模式只在代码生成阶段起作用 - 类型检查并不受影响：</p>
<ol>
<li>preserve：生成代码中会保留JSX以供后续的转换操作（例如Babel）使用</li>
<li>react：生成React.createElement。注意React这个标识符是写死的，不能被占用</li>
<li>react-native：相当于preserve，它也保留了所有的JSX，但是输出文件的扩展名是.js</li>
</ol>
<div class="blog_h3"><span class="graybg">关于as操作符</span></div>
<p>由于尖括号风格的断言<pre class="crayon-plain-tag">var foo = &lt;foo&gt;bar;</pre>和JSX语法冲突，因而在.tsx文件中，这种风格的断言不被支持，必须使用as操作符。</p>
<div class="blog_h1"><span class="graybg">ts-node</span></div>
<p>要使用TypeScript编写Node.js程序，可以借助ts-node。ts-node是Node.js的TypeScript执行引擎，它能够JIT的将TypeScript编译为JavaScript，你不需要预先编译就可以在Node.js运行时上执行TypeScript。</p>
<p>ts-node的特性包括：</p>
<ol>
<li>为stacktrace自动生成sourcemap</li>
<li>自动解析tsconfig.json</li>
<li>自动设置匹配Node.js版本的默认值</li>
<li>类型检查</li>
<li>REPL</li>
<li>原生ESM模块加载器支持</li>
<li>支持使用第三方transpiler</li>
<li>和debugger集成</li>
</ol>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag"># Locally in your project.
npm install -D typescript
npm install -D ts-node

# Or globally with TypeScript.
npm install -g typescript
npm install -g ts-node

# Depending on configuration, you may also need these
npm install -D tslib @types/node</pre>
<div class="blog_h2"><span class="graybg">命令行</span></div>
<pre class="crayon-plain-tag"># 执行脚本
ts-node script.ts

# 启动解释器
ts-node

# 执行命令行提供的脚本
ts-node -e 'console.log("Hello, world!")'

# 执行并打印
ts-node -p -e '"Hello, world!"'</pre>
<div class="blog_h3"><span class="graybg">Shebang</span></div>
<pre class="crayon-plain-tag">#!/usr/bin/env ts-node

console.log("Hello, world!")</pre>
<div class="blog_h2"><span class="graybg">编程式使用 </span></div>
<p>可以通过下面的代码请求ts-node模块并注册它的loader，此loader会影响后续的requires。ts-node的工作原理就是挂钩到Node的模块加载API。</p>
<pre class="crayon-plain-tag">require('ts-node').register({ /* options */ })</pre>
<p>或者使用命令行参数：</p>
<pre class="crayon-plain-tag">node -r ts-node/register
node -r ts-node/register/transpile-only</pre>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>ts-node会自动查找并加载tsconfig.json文件。命令行选项<pre class="crayon-plain-tag">--skipProject</pre>可以跳过当前项目的tsconfig.json，<pre class="crayon-plain-tag">--project</pre>则用于明确指定项目配置文件的位置。 </p>
<pre class="crayon-plain-tag">{
  // This is an alias to @tsconfig/node12: https://github.com/tsconfig/bases
  "extends": "ts-node/node12/tsconfig.json",

  // 大部分配置在此
  "ts-node": {
    // 如果要跳过类型检查，去除下面这一行
    "transpileOnly": true,

    "files": true,

    "compilerOptions": {
      // 覆盖下面的编译选项
    }
  },
  "compilerOptions": {
    // typescript编译选项
  }
}</pre>
<div class="blog_h3"><span class="graybg">使用CommonJS模块加载 </span></div>
<p>TypeScript基本上都会使用import语法，但是可以在执行期间转换为CommonJS的require()或者保持ESM风格的import不变。</p>
<p>如果要转换为CommonJS风格，在package.json配置：</p>
<pre class="crayon-plain-tag">{
  // This can be omitted; commonjs is the default
  "type": "commonjs"
}</pre>
<p>在tsconfig.json中配置：</p>
<pre class="crayon-plain-tag">{
  "compilerOptions": {
    "module": "CommonJS"
  }
}</pre>
<p>如果需要为tsc，webpack等工具保留"module": "ESNext"，你可以：</p>
<pre class="crayon-plain-tag">{
  "compilerOptions": {
    "module": "ESNext"
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}</pre>
<div class="blog_h3"><span class="graybg">使用原生ESM模块加载</span></div>
<p>NodeJS的原生ESM加载器钩子还处于试验阶段，可能改变。ts-node的ESM支持会尽量保持稳定，但是它依赖于底层的Node API。</p>
<p>你需要在package.json中设置：</p>
<pre class="crayon-plain-tag">{
  "type": "module"
}</pre>
<p>在tsconfig.json中设置：</p>
<pre class="crayon-plain-tag">{
  "compilerOptions": {
    "module": "ESNext" // or ES2015, ES2020
  },
  "ts-node": {
    // Tell ts-node CLI to install the --loader automatically, explained below
    "esm": true
  }
}</pre>
<p>同时，你还需要确保--loader被传递给Node。如果使用ts-node命令行，会自动传递： </p>
<pre class="crayon-plain-tag"># 等价于上面配置文件中的"esm": true
ts-node --esm
ts-node-esm</pre>
<p>如果你直接使用Node的命令行，则需要手工传递： </p>
<pre class="crayon-plain-tag">node --loader ts-node/esm ./index.ts
# 或者
NODE_OPTIONS="--loader ts-node/esm" node ./index.ts</pre>
<div class="blog_h2"><span class="graybg">配合WebStorm </span></div>
<p>安装ts-node：</p>
<pre class="crayon-plain-tag">npm install --save-dev ts-node</pre>
<p>创建Node.js运行配置，增加Node Parameters：<pre class="crayon-plain-tag">--require ts-node/register</pre>。 </p>
<p>使用ESM时，参考上文，即增加Node Parameters：<pre class="crayon-plain-tag">--loader ts-node/esm</pre>。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/typescript">TypeScript学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/typescript/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Yarn学习笔记</title>
		<link>https://blog.gmem.cc/yarnpkg-study-note</link>
		<comments>https://blog.gmem.cc/yarnpkg-study-note#comments</comments>
		<pubDate>Mon, 10 Dec 2018 02:27:16 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=23951</guid>
		<description><![CDATA[<p>简介 Yarn是一个新的JavaScript包管理器，用于弥补npm的不足。它的优势包括： 速度快：支持并行安装依赖，离线缓存依赖 版本一致：每当安装新的模块后，Yarn都会更新锁文件yarn.lock，保证下次拉取同一代码版本 简洁的输出 安装 当前版本v1.12.3，要求NodeJS版本^4.8.0 、^5.7.0、^6.2.2 或者8.0.0以上。 你可以直接通过npm来安装Yarn： [crayon-69db90ad6a309926357511/] 或者使用操作系统的包管理机制安装： [crayon-69db90ad6a30d964037353/] 命令 Yarn的命令行工具是yarn，本章介绍常用子命令。 子命令 说明 init 初始化一个新项目 add  添加依赖： [crayon-69db90ad6a310053748295/] <a class="read-more" href="https://blog.gmem.cc/yarnpkg-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/yarnpkg-study-note">Yarn学习笔记</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>Yarn是一个新的JavaScript包管理器，用于弥补npm的不足。它的优势包括：</p>
<ol>
<li>速度快：支持并行安装依赖，离线缓存依赖</li>
<li>版本一致：每当安装新的模块后，Yarn都会更新锁文件yarn.lock，保证下次拉取同一代码版本</li>
<li>简洁的输出</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<p>当前版本v1.12.3，要求NodeJS版本^4.8.0 、^5.7.0、^6.2.2 或者8.0.0以上。</p>
<p>你可以直接通过npm来安装Yarn：</p>
<pre class="crayon-plain-tag">npm install -g yarn</pre>
<p>或者使用操作系统的包管理机制安装：</p>
<pre class="crayon-plain-tag"># Ubuntu
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update &amp;&amp; sudo apt-get install yarn</pre>
<div class="blog_h1"><span class="graybg">命令</span></div>
<p>Yarn的命令行工具是yarn，本章介绍常用子命令。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h2">init</td>
<td>初始化一个新项目</td>
</tr>
<tr>
<td class="blog_h2">add</td>
<td> 添加依赖：<br />
<pre class="crayon-plain-tag">yarn add [package]
yarn add [package]@[version]
yarn add [package]@[tag]

# 可以指定依赖类型
yarn add [package] --dev            # devDependencies
yarn add [package] --peer           # peerDependencies
yarn add [package] --optional       # optionalDependencies</pre>
</td>
</tr>
<tr>
<td class="blog_h2">upgrade</td>
<td>升级一个依赖：<br />
<pre class="crayon-plain-tag">yarn upgrade [package]
yarn upgrade [package]@[version]
yarn upgrade [package]@[tag]</pre>
</td>
</tr>
<tr>
<td class="blog_h2">remove</td>
<td>移除依赖：<pre class="crayon-plain-tag">yarn remove [package]</pre> </td>
</tr>
<tr>
<td class="blog_h2">install</td>
<td>安装所有依赖到本地：<br />
<pre class="crayon-plain-tag">yarn install
# 或者
yarn</pre>
</td>
</tr>
<tr>
<td class="blog_h2">global</td>
<td>全局性的安装一个包</td>
</tr>
<tr>
<td class="blog_h2">run</td>
<td>
<p>运行一个定义在package.json中的脚本</p>
<p>如果有个脚本定义在scripts.start:dev，则可以直接调用：</p>
<pre class="crayon-plain-tag">yarn start:dev</pre>
</td>
</tr>
<tr>
<td class="blog_h2">publish</td>
<td>发布一个包到包管理器</td>
</tr>
</tbody>
</table>
<p>&nbsp;
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/yarnpkg-study-note">Yarn学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/yarnpkg-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>基于Broadway的HTML5视频监控</title>
		<link>https://blog.gmem.cc/html5-vs-with-broadway</link>
		<comments>https://blog.gmem.cc/html5-vs-with-broadway#comments</comments>
		<pubDate>Mon, 09 Oct 2017 10:22:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Multimedia]]></category>
		<category><![CDATA[视频监控]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16500</guid>
		<description><![CDATA[<p>简介 Broadway是一个基于JavaScript的H.264解码器，支持Baseline Profile，我们在HTML5视频监控技术预研一文中介绍过这个库。如果你的监控摄像头支持Baseline的H.264码流，利用Broadway可以实现不需要重新编码的视频监控，这样服务器的负载可以大大减轻。 本文不进行理论知识的讨论，仅仅给出一个简单的实现。此实现由三个部分组成： 基于live555的C++程序，用来从视频源取RTP流，解析出NALU然后通过WebSocket推送给WebSocket服务器 基于Spring Boot的Java WebSocket服务器，接收C++程序推送来的NALU并广播给客户端 基于Broadway的HTML5视频监控客户端，为了简化开发，我们使用了Broadway的一个封装http-live-player 代码托管于GitHub：https://github.com/gmemcc/h5vs.git C++部分 这部分主要是一个RTSP客户端，功能上面已经介绍过，此客户端依赖于我以前一篇文章中的live555 RTSP客户端封装。 WebSocket客户端 [crayon-69db90ad6a680588802532/] [crayon-69db90ad6a684499449600/] 主程序 [crayon-69db90ad6a687135986536/] Java部分 这部分实现了NALU转发功能，基于Spring Boot实现。 主程序 <a class="read-more" href="https://blog.gmem.cc/html5-vs-with-broadway">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/html5-vs-with-broadway">基于Broadway的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_h2"><span class="graybg">简介</span></div>
<p>Broadway是一个基于JavaScript的H.264解码器，支持Baseline Profile，我们在<a href="/research-on-html5-video-surveillance">HTML5视频监控技术预研</a>一文中介绍过这个库。如果你的监控摄像头支持Baseline的H.264码流，利用Broadway可以实现不需要重新编码的视频监控，这样服务器的负载可以大大减轻。</p>
<p>本文不进行理论知识的讨论，仅仅给出一个简单的实现。此实现由三个部分组成：</p>
<ol>
<li>基于live555的C++程序，用来从视频源取RTP流，解析出NALU然后通过WebSocket推送给WebSocket服务器</li>
<li>基于Spring Boot的Java WebSocket服务器，接收C++程序推送来的NALU并广播给客户端</li>
<li>基于Broadway的HTML5视频监控客户端，为了简化开发，我们使用了Broadway的一个封装http-live-player</li>
</ol>
<p>代码托管于GitHub：<a href="https://github.com/gmemcc/h5vs.git">https://github.com/gmemcc/h5vs.git</a></p>
<div class="blog_h2"><span class="graybg">C++部分</span></div>
<p>这部分主要是一个RTSP客户端，功能上面已经介绍过，此客户端依赖于我以前一篇文章中的<a href="/realtime-communication-protocols#rtsp-client-wrapper">live555 RTSP客户端封装</a>。</p>
<div class="blog_h3"><span class="graybg">WebSocket客户端</span></div>
<pre class="crayon-plain-tag">//
// Created by alex on 10/9/17.
//

#ifndef LIVE5555_WEBSOCKETCLIENT_H
#define LIVE5555_WEBSOCKETCLIENT_H

#include &lt;pthread.h&gt;

#include &lt;websocketpp/config/asio_no_tls_client.hpp&gt;
#include &lt;websocketpp/client.hpp&gt;

typedef websocketpp::client&lt;websocketpp::config::asio_client&gt; WebSocketppClient;
typedef websocketpp::connection_hdl WebSocketppConnHdl;

class WebSocketClient {
private:
    char *url;
    pthread_t wsThread;
    WebSocketppClient *wsppClient;
    WebSocketppConnHdl wsppConnHdl;
public:
    WebSocketClient( char *url );

    char *getUrl() const;

    virtual void connect();

    virtual void sendBytes( unsigned char *buf, unsigned size );

    virtual void sendText( char *text );

    virtual ~WebSocketClient();

    pthread_t getWsThread() const;

    WebSocketppClient *getWsppClient();

    void setWsppConnHdl( WebSocketppConnHdl wsppConnHdl );
};


#endif //LIVE5555_WEBSOCKETCLIENT_H</pre><br />
<pre class="crayon-plain-tag">//
// Created by alex on 10/9/17.
//

#include "WebSocketClient.h"

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

#include "spdlog/spdlog.h"

static auto LOGGER = spdlog::stdout_color_st( "WebSocketClient" );

WebSocketClient::WebSocketClient( char *url ) : url( url ), wsppClient( new WebSocketppClient()) {
}

WebSocketClient::~WebSocketClient() {
    delete wsppClient;
}

static void *wsRoutine( void *arg ) {
    WebSocketClient *client = (WebSocketClient *) arg;

    WebSocketppClient *wsppClient = client-&gt;getWsppClient();
    wsppClient-&gt;clear_access_channels( websocketpp::log::alevel::frame_header );
    wsppClient-&gt;clear_access_channels( websocketpp::log::alevel::frame_payload );
    wsppClient-&gt;init_asio();

    websocketpp::lib::error_code ec;
    WebSocketppClient::connection_ptr con = wsppClient-&gt;get_connection( std::string( client-&gt;getUrl()), ec );
    wsppClient-&gt;connect( con );
    client-&gt;setWsppConnHdl( con-&gt;get_handle());
    wsppClient-&gt;run();
}

void WebSocketClient::connect() {
    pthread_create( &amp;wsThread, NULL, wsRoutine, (void *) this );
}

void WebSocketClient::sendBytes( unsigned char *buf, unsigned size ) {
    wsppClient-&gt;send( wsppConnHdl, buf, size, websocketpp::frame::opcode::BINARY );
}

void WebSocketClient::sendText( char *text ) {
    wsppClient-&gt;send( wsppConnHdl, text, strlen( text ), websocketpp::frame::opcode::TEXT );
}

char *WebSocketClient::getUrl() const {
    return url;
}

pthread_t WebSocketClient::getWsThread() const {
    return wsThread;
}

WebSocketppClient *WebSocketClient::getWsppClient() {
    return wsppClient;
};

void WebSocketClient::setWsppConnHdl( WebSocketppConnHdl wsppConnHdl ) {
    this-&gt;wsppConnHdl = wsppConnHdl;
}</pre>
<div class="blog_h3"><span class="graybg">主程序</span></div>
<pre class="crayon-plain-tag">#include &lt;iostream&gt;
#include "live5555/client.h"

#include "spdlog/spdlog.h"

#include "WebSocketClient.h"

static auto LOGGER = spdlog::stdout_color_st( "wspush" );

class VideoSink : public SinkBase {
private:
#ifdef _SAVE_H264_SEQ
    FILE *os = fopen( "./rtsp.h264", "w" );
#endif
    WebSocketClient *wsClient;
    bool firstFrameWritten;
    const char *sPropParameterSetsStr;
    unsigned char const start_code[4] = { 0x00, 0x00, 0x00, 0x01 };
public:
    VideoSink( UsageEnvironment &amp;env, unsigned int recvBufSize, WebSocketClient *wsClient ) : SinkBase( env, recvBufSize ), wsClient( wsClient ) {
        // 缓冲区前面留出起始码4字节
        recvBuf += sizeof( start_code );
    }

    virtual ~VideoSink() {
    }

    virtual void onMediaSubsessionOpened( MediaSubsession *subSession ) {
        sPropParameterSetsStr = subSession-&gt;fmtp_spropparametersets();
    }

    void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override {
        size_t scLen = sizeof( start_code );
        if ( !firstFrameWritten ) {
            // 填写起始码
            memcpy( recvBuf - scLen, start_code, scLen );
            // 防止RTSP源不送SPS/PPS
            unsigned numSPropRecords;
            SPropRecord *sPropRecords = parseSPropParameterSets( sPropParameterSetsStr, numSPropRecords );
            for ( unsigned i = 0; i &lt; numSPropRecords; ++i ) {
                unsigned int propLen = sPropRecords[ i ].sPropLength;
                size_t bufLen = propLen + scLen;
                unsigned char buf[bufLen];
                memcpy( buf, start_code, scLen );
                memcpy( buf + scLen, sPropRecords[ i ].sPropBytes, propLen );
                wsClient-&gt;sendBytes( buf, bufLen );
#ifdef _SAVE_H264_SEQ
                fwrite( buf, sizeof( unsigned char ), bufLen, os );
#endif
            }
            firstFrameWritten = true;
        }
#ifdef _SAVE_H264_SEQ
        fwrite( recvBuf - scLen, sizeof( unsigned char ), frameSize + scLen, os );
#endif
        unsigned naluHead = recvBuf[ 0 ];
        unsigned nri = naluHead &gt;&gt; 5;
        unsigned f = nri &gt;&gt; 2;
        unsigned type = naluHead &amp; 0b00011111;
        wsClient-&gt;sendBytes( recvBuf - scLen, frameSize + scLen );
        LOGGER-&gt;trace( "NALU info: nri {} type {}", nri, type );
        SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime );
    }
};

class H264RTSPClient : public RTSPClientBase {
private:
    VideoSink *videoSink;
public:
    H264RTSPClient( UsageEnvironment &amp;env, const char *rtspURL, VideoSink *videoSink ) :
        RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {}

protected:
    // 测试用的摄像头（RTSP源）仅仅有一个子会话，因此这里简化了实现：
    bool acceptSubSession( const char *mediumName, const char *codec ) override {
        return true;
    }

    MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession ) override {
        videoSink-&gt;onMediaSubsessionOpened( subSession );
        return videoSink;
    }
};

int main() {
    spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" );
    spdlog::set_level( spdlog::level::trace );

    WebSocketClient *wsClient;
    wsClient = new WebSocketClient( "ws://192.168.0.89:9090/h264src" );
    wsClient-&gt;connect();
    sleep( 3 ); // 等待WebSocket连接建立
    wsClient-&gt;sendText( "ch1" );
    TaskScheduler *scheduler = BasicTaskScheduler::createNew();
    BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler );
    VideoSink *sink = new VideoSink( *env, 1024 * 1024, wsClient );
    H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink );
    client-&gt;start();
    return 0;
}</pre>
<div class="blog_h2"><span class="graybg">Java部分</span></div>
<p>这部分实现了NALU转发功能，基于Spring Boot实现。</p>
<div class="blog_h3"><span class="graybg">主程序</span></div>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.kurento.client.KurentoClient;
import org.kurento.client.KurentoClientBuilder;
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.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.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import sun.security.acl.PrincipalImpl;

import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SpringBootApplication
@EnableWebSocket
@EnableWebSocketMessageBroker
@EnableScheduling
public class VideoSurveillanceApp extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer {

    private static final Logger LOGGER = LoggerFactory.getLogger( VideoSurveillanceApp.class );

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // WebSocket消息缓冲区大小，如果客户端发来的消息较大，需要按需调整
        container.setMaxTextMessageBufferSize( 1024 * 1024 );
        container.setMaxBinaryMessageBufferSize( 1024 * 1024 );
        return container;
    }

    @Override
    public void registerWebSocketHandlers( WebSocketHandlerRegistry registry ) {
        registry.addHandler( h264FrameSinkHandler(), "/h264sink" );
        registry.addHandler( h264FrameSrcHandler(), "/h264src" );
    }

    @Bean
    public WebSocketHandler h264FrameSrcHandler() {
        return new H264FrameSrcHandler();
    }

    @Bean
    public WebSocketHandler h264FrameSinkHandler() {
        return new H264FrameSinkHandler();
    }

    public static void main( String[] args ) {
        new SpringApplication( VideoSurveillanceApp.class ).run( args );
    }

}</pre>
<div class="blog_h3"><span class="graybg">H264FrameSrcHandler</span></div>
<p>此Bean接受C++程序的NALU推送：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class H264FrameSrcHandler extends AbstractWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( H264FrameSrcHandler.class );

    private Map&lt;String, String&gt; sessionIdToChannel = new ConcurrentHashMap&lt;&gt;();

    @Inject
    private H264FrameSinkHandler sinkHandler;

    public void afterConnectionEstablished( WebSocketSession session ) throws Exception {
        LOGGER.debug( "{} connected.", session.getRemoteAddress() );
    }

    @Override
    protected void handleBinaryMessage( WebSocketSession session, BinaryMessage message ) throws Exception {
        ByteBuffer payload = message.getPayload();
        StringBuilder hex = new StringBuilder();
        byte[] pa = payload.array();
        int len = 16;
        if ( pa.length &lt; 16 ) len = pa.length;
        for ( byte i = 0; i &lt; len; i++ ) {
            hex.append( String.format( "%02x ",Byte.toUnsignedInt( pa[i] )  ) );
        }
        LOGGER.debug( "Received binary message {} bytes: {}...", payload.array().length, hex );
        String chnl = sessionIdToChannel.get( session.getId() );
        if ( chnl != null ) sinkHandler.broadcast( chnl, payload );
    }

    @Override
    protected void handleTextMessage( WebSocketSession session, TextMessage message ) throws Exception {
        String payload = message.getPayload();
        sessionIdToChannel.put( session.getId(), payload );
        LOGGER.debug( "Received text message: {}", payload );
    }
}</pre>
<div class="blog_h3"><span class="graybg">H264FrameSinkHandler</span></div>
<p>此Bean向Web客户端广播NALU：</p>
<pre class="crayon-plain-tag">package cc.gmem.study.kurento;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.mutable.MutableInt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.inject.Inject;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class H264FrameSinkHandler extends TextWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( H264FrameSinkHandler.class );

    public static final String ACTION_INIT = "init";

    private static final String ACTION_INIT_RESP = "initresp";

    public static final String ACTION_PLAY = "play";

    public static final String ACTION_STOP = "stop";

    public static final String KEY_ACTION = "action";


    @Inject
    private ObjectMapper om;

    private Map&lt;String, List&lt;WebSocketSession&gt;&gt; chnlToSessions = new ConcurrentHashMap&lt;&gt;();

    @Override
    protected void handleTextMessage( WebSocketSession session, TextMessage message ) throws Exception {
        String client = session.getId() + '@' + session.getRemoteAddress();
        Map req = om.readValue( message.getPayload(), Map.class );
        Map resp = new LinkedHashMap();
        Object action = req.get( KEY_ACTION );
        if ( ACTION_INIT.equals( action ) ) {
            String channel = (String) req.get( "channel" );
            LOGGER.debug( "{} request to subscribe channel {}", client, channel );
            addPushTarget( channel, session );

            resp.put( KEY_ACTION, ACTION_INIT_RESP );
            resp.put( "width", 352 );
            resp.put( "height", 288 );
            session.sendMessage( new TextMessage( om.writeValueAsString( resp ) ) );
        } else if ( ACTION_PLAY.equals( action ) ) {
            LOGGER.debug( "{} request to receive nalu push", session.getRemoteAddress(), client );
            session.getAttributes().put( ACTION_PLAY, true );
        }
    }

    private synchronized void addPushTarget( String channel, WebSocketSession session ) {
        List&lt;WebSocketSession&gt; sessions = chnlToSessions.get( channel );
        if ( sessions == null ) {
            sessions = new ArrayList&lt;&gt;();
            chnlToSessions.put( channel, sessions );
        }
        sessions.add( session );
    }

    public synchronized void broadcast( String chnl, ByteBuffer payload ) {
        List&lt;WebSocketSession&gt; sessions = chnlToSessions.get( chnl );
        if ( sessions == null ) return;
        sessions.forEach( sess -&gt; {
            try {
                if ( sess.isOpen() &amp;&amp; Boolean.TRUE.equals( sess.getAttributes().get( ACTION_PLAY ) ) ) {
                    sess.sendMessage( new BinaryMessage( payload ) );
                }
            } catch ( Exception e ) {
                LOGGER.error( e.getMessage(), e );
            }
        } );
    }

    @Scheduled( fixedRate = 10000 )
    public synchronized void cleanup() {
        final MutableInt counter = new MutableInt( 0 );
        chnlToSessions.values().forEach( sessions -&gt; {
            Iterator&lt;WebSocketSession&gt; it = sessions.listIterator();
            while ( it.hasNext() ) {
                if ( !it.next().isOpen() ) {
                    it.remove();
                    counter.increment();
                }
            }
        } );
        if ( counter.intValue() &gt; 0 ) LOGGER.debug( "Remove {} invalid websocket session.", counter );
    }
}</pre>
<div class="blog_h2"><span class="graybg">Web部分</span></div>
<p>我们对http-live-player进行了简单的修改，主要是修改其通信方式以配合上述WebSocket服务器。核心代码没有变动，因此这里不张贴其代码。</p>
<div class="blog_h3"><span class="graybg">客户端代码</span></div>
<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;Broadway Video Surveillance&lt;/title&gt;
    &lt;script src="js/broadway/http-live-player.js"&gt;&lt;/script&gt;
    &lt;link rel="stylesheet" href="style.css"&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class="nvbar"&gt;
    &lt;div class="title"&gt;基于Broadway+WebSocket的视频监控示例&lt;/div&gt;
    &lt;div class="subtitle"&gt;http://192.168.0.89:9090/broadway.html&lt;/div&gt;
&lt;/div&gt;
&lt;div class="videos-wrapper"&gt;
    &lt;div id="videos"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
    var videos = document.getElementById( 'videos' );
    for ( var i = 0; i &lt; 9; i++ ) {
        var canvas = document.createElement( "canvas" );
        videos.appendChild( canvas );
        var player = new WSAvcPlayer( canvas, "webgl", 'ch1', true );
        player.connect( "ws://" + document.location.host + "/h264sink" );
    }
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<div class="blog_h3"><span class="graybg">效果截图</span></div>
<p>下面的截图是开了九画面的视频监控，使用的是子码流，在测试机器上CPU压力不大。</p>
<p><img class="aligncenter size-full wp-image-16511" src="https://blog.gmem.cc/wp-content/uploads/2017/10/html5-h264.png" alt="html5-h264" width="798" height="697" /></p>
<p>注意：如果Broadway来不及解码，http-live-player会把缓冲区中的所有NALU全部丢弃，这可能导致暂时的花屏。选择适当的帧率、码率、画幅可以尽量避免这种情况的发生。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/html5-vs-with-broadway">基于Broadway的HTML5视频监控</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/html5-vs-with-broadway/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>SockJS知识集锦</title>
		<link>https://blog.gmem.cc/sockjs-faq</link>
		<comments>https://blog.gmem.cc/sockjs-faq#comments</comments>
		<pubDate>Tue, 05 Sep 2017 06:08:03 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15858</guid>
		<description><![CDATA[<p>简介 SockJS允许应用程序使用WebSocket来进行通信，但是当WebSocket不可用时，可以使用代替的传输机制，但是保持API不变。 SockJS由以下部分组成： SockJS协议 一个JavaScript客户端 SockJS服务器端实现，例如 spring-websocket SocketJS客户端以针对/info的GET请求发起通信，服务器会返回一些基本信息，在此之后，客户端必须决定使用何种传输机制。SocketJS支持多种传输机制，包括WebSocket、HTTP Streaming、HTTP Long Polling。 从4.1开始，Spring提供SockJS的Java客户端。 客户端 JavaScript客户端 SockJS的API和WebSocket很类似： [crayon-69db90ad6aa4b596914735/] Java客户端 参考Spring对WebSocket的支持</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/sockjs-faq">SockJS知识集锦</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>
<p>SockJS允许应用程序使用WebSocket来进行通信，但是当WebSocket不可用时，可以使用代替的传输机制，但是保持API不变。</p>
<p>SockJS由以下部分组成：</p>
<ol>
<li>SockJS协议</li>
<li>一个JavaScript客户端</li>
<li>SockJS服务器端实现，例如 spring-websocket</li>
</ol>
<p>SocketJS客户端以针对/info的GET请求发起通信，服务器会返回一些基本信息，在此之后，客户端必须决定使用何种传输机制。SocketJS支持多种传输机制，包括WebSocket、HTTP Streaming、HTTP Long Polling。</p>
<p>从4.1开始，Spring提供SockJS的Java客户端。</p>
<div class="blog_h2"><span class="graybg">客户端</span></div>
<div class="blog_h3"><span class="graybg">JavaScript客户端</span></div>
<p>SockJS的API和WebSocket很类似：</p>
<pre class="crayon-plain-tag">var sock = new SockJS( 'ws://gmem.cc:8888/hello' );
// 当连接打开后的回调
sock.onopen = function () {
    console.log( 'open' );
    // 发送消息
    sock.send( 'Hello there' );
};
// 接收到消息时的回调
sock.onmessage = function ( msg ) {
    // msg.data为消息内容
    console.log( 'message', msg.data );
    // 关闭连接
    sock.close();
};
// 关闭连接时的回调
sock.onclose = function () {
    console.log( 'close' );
};</pre>
<div class="blog_h3"><span class="graybg">Java客户端</span></div>
<p>参考<a href="/ws-support-of-spring#sockjs-java-client">Spring对WebSocket的支持</a></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/sockjs-faq">SockJS知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/sockjs-faq/feed</wfw:commentRss>
		<slash:comments>0</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>Three.js学习笔记</title>
		<link>https://blog.gmem.cc/three-js-study-note</link>
		<comments>https://blog.gmem.cc/three-js-study-note#comments</comments>
		<pubDate>Sun, 01 Jan 2017 04:16:22 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WebGL]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14391</guid>
		<description><![CDATA[<p>简介 关于WebGL Web图形库（Web Graphics Library）简称WebGL，是在浏览器环境下进行3D/2D图像渲染的技术。你不需要额外的插件，就可以在HTML5的Canvas上绘制复杂的、可交互的图形。 大部分现代浏览器支持WebGL技术，IE从11开始支持，老版本的IE可以通过第三方插件支持，例如IEWebGL。 WebGL基于OpenGL ES 2.0提供3D图形接口。后者是OpenGL的一个子集，主要针对手机、PDA之类的嵌入式设备。 关于Three.js 直接使用WebGL编程难度较高，需要了解WebGL的细节、学习复杂的着色器（Shader）语言。Three.js对WebGL的底层细节进行了封装，让你更加容易的、仅利用JavaScript语言创建3D图形，你可以： 创建简单/复杂的3D几何图形 在3D场景中动画、移动对象 给对象应用纹理、材质 从3D模型软件中加载对象 Three.js基本概念 术语 说明 场景Scene 存储并跟踪所有待渲染对象的容器。场景被渲染器渲染到一个HTML5画布中 镜头Camera 定义查看场景的视角，有多种实现： <a class="read-more" href="https://blog.gmem.cc/three-js-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/three-js-study-note">Three.js学习笔记</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">关于WebGL</span></div>
<p>Web图形库（Web Graphics Library）简称WebGL，是在浏览器环境下进行3D/2D图像渲染的技术。你不需要额外的插件，就可以在HTML5的Canvas上绘制复杂的、可交互的图形。</p>
<p>大部分现代浏览器支持WebGL技术，IE从11开始支持，老版本的IE可以通过第三方插件支持，例如IEWebGL。</p>
<p>WebGL基于OpenGL ES 2.0提供3D图形接口。后者是OpenGL的一个子集，主要针对手机、PDA之类的嵌入式设备。</p>
<div class="blog_h2"><span class="graybg">关于Three.js</span></div>
<p>直接使用WebGL编程难度较高，需要了解WebGL的细节、学习复杂的着色器（Shader）语言。Three.js对WebGL的底层细节进行了封装，让你更加容易的、仅利用JavaScript语言创建3D图形，你可以：</p>
<ol>
<li>创建简单/复杂的3D几何图形</li>
<li>在3D场景中动画、移动对象</li>
<li>给对象应用纹理、材质</li>
<li>从3D模型软件中加载对象</li>
</ol>
<div class="blog_h2"><span class="graybg">Three.js基本概念</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>场景<br />Scene</td>
<td>存储并跟踪所有待渲染对象的容器。场景被渲染器渲染到一个HTML5画布中</td>
</tr>
<tr>
<td>镜头<br />Camera</td>
<td>
<p>定义查看场景的视角，有多种实现：</p>
<ol>
<li>PerspectiveCamera，基于透视投影（perspective projection）的镜头。透视投影模拟人的视觉效果（近大远小），从某个投射中心（人眼）将物体投射到单一投影面（画面）之上。是最常使用的投影模式</li>
</ol>
</td>
</tr>
<tr>
<td>视截锥<a id="ViewFrustum"></a><br />View Frustum</td>
<td>
<p>在3D计算机图形学中，视截锥是指被建模世界空间的一个区域，该区域可以出现在屏幕中，视截锥定义了概念相机的视界（Field of view，FOV）</p>
<p>使用两个平行的平面，对视野金字塔（pyramid of vision）进行截断操作，即得到视截锥。视截锥的精确形状取决于期望模拟的相机棱镜的形状，典型情况下为六面体，其中远近平面为同长宽比的矩形，如下图：</p>
<p><img class="aligncenter size-full wp-image-14439" src="https://blog.gmem.cc/wp-content/uploads/2017/01/viewfrustum-02.png" alt="viewfrustum-02" width="100%" /> </p>
<p>所谓远、近平面，是指六面体中与视觉方向正交的那两个平面。近平面即上图中标为黄色的那一面。比近平面更近、远平面更远的区域中的对象，不会被绘制。某些情况下远平面被放置到无限远处</p>
<p>视截锥选择（View frustum culling）是指从渲染过程中移除完全位于视截锥之外的对象的处理步骤</p>
</td>
</tr>
<tr>
<td>视界<br />FOV</td>
<td>
<p>在第一人称游戏中，所谓视界（ field of view, field of vision）是指某一时刻游戏世界中显示在屏幕中的（矩形）范围（extent）。视界通常用角度（angle）来描述，但是此角度可能指FOV在垂直、水平、对角线（diagonal）方向的分量</p>
<p>在一定的分辨率下，FOV会依据屏幕纵横比（aspect ratio）而变化，通常FOV在宽屏上更大</p>
<p>我们常以FOV在水平/垂直方向的角度、结合纵横比来描述FOV。它们之间的换算公式如下：</p>
<p style="padding-left: 60px;"><em>r  = w / h = tan(H/2) / tan(V/2)</em></p>
<p>其中r为屏幕纵横比，w/h为屏幕宽高度，H/V为水平、垂直方向的FOV分量</p>
<p>在Three.js中，PerspectiveCamera的fov参数为FOV的垂直分量，这意味着取值从0 ~ 180之间时变化时，如果r保持不变，则视界越大，场景中的目标显得越小</p>
</td>
</tr>
<tr>
<td>渲染器<br />Renderer</td>
<td>负责计算在指定的Camera之下，Scene长得什么样子</td>
</tr>
<tr>
<td>多边形网格<br />Polygon mesh</td>
<td>多边形网格是一系列顶点（vertex）、边线（edge）、面（face）的几何。它在3D计算机图形学中定义了一个多面体的轮廓。网格中的面通常由三角形、四边形或者其它简单的凸面多边形（ convex polygons）构成，以简化渲染的计算量。下面是一个由三角形网格构成的海豚模型示例：<a href="https://blog.gmem.cc/wp-content/uploads/2017/01/Dolphin_triangle_mesh.png"><img class="aligncenter size-full wp-image-14413" src="https://blog.gmem.cc/wp-content/uploads/2017/01/Dolphin_triangle_mesh.png" alt="dolphin_triangle_mesh" width="95%" /></a> </td>
</tr>
<tr>
<td>混合模式<br />Blend mode / <br />Mixing mode</td>
<td>
<p>图像处理中的概念。用于确定两层图像如何叠加到一起。大部分应用中默认的叠加模式就是让顶层（top layer ）直接覆盖较低的层（lower layers ）。由于每个像素的色彩都是基于数字来表示的，因此基于数学运算的大量混合模式可用</p>
<p>大部分图像处理软件，例如Photoshop、GIMP，都支持用户修改混合模式。</p>
<p>参考：<a href="https://en.wikipedia.org/wiki/Blend_modes">https://en.wikipedia.org/wiki/Blend_modes</a></p>
</td>
</tr>
<tr>
<td>粒子，精灵<br />Particles, Sprite</td>
<td>
<p>指存在于3D场景中的二维图形或者动画</p>
</td>
</tr>
<tr>
<td>右手系<br />right-handed system</td>
<td>
<p>Three.js默认使用右手坐标系，因为这是OpenGL默认的坐标系</p>
<p>所谓右手系，是指：</p>
<ol>
<li>伸出右手，伸直拇指，让它与另外四指垂直</li>
<li>弯曲中、无名、小指，让它们与食指垂直</li>
<li>以拇指指向为X轴正向、食指指向为Y轴正向、其它手指指向为Z轴正向的坐标系，即右手系</li>
</ol>
<p>图示如下：</p>
<p><img class="aligncenter size-full wp-image-14527" src="https://blog.gmem.cc/wp-content/uploads/2017/01/right-handed-system.png" alt="right-handed-system" width="220" height="165" /></p>
<p>Blender等3D建模软件，使用Z轴向上（上图右手系沿X轴正向逆时针旋转90度）的右手系。主要原因是大部分CAD软件均使用这样的坐标系</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">第一个3D场景</span></div>
<div class="blog_h2"><span class="graybg">渲染并查看3D对象</span></div>
<p>在本节，我们创建以下几个对象：</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>Plane</td>
<td>平面，二维的矩形，作为“地面”在场景的中央展示</td>
</tr>
<tr>
<td>Cube</td>
<td>三维盒子，展示为红色</td>
</tr>
<tr>
<td>Sphere</td>
<td>三维球体，展示为蓝色</td>
</tr>
<tr>
<td>Camera</td>
<td>镜头，决定你看到的场景是什么样子</td>
</tr>
<tr>
<td>Axes</td>
<td>X/Y/Z轴，辅助的调试工具，方便查看对象在哪里渲染</td>
</tr>
</tbody>
</table>
<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;Three.js Study&lt;/title&gt;
    &lt;script src="https://code.jquery.com/jquery-1.12.4.js"&gt;&lt;/script&gt;
    &lt;script src="three.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        body {
            margin: 0;
            overflow: hidden;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id="WebGL"&gt;&lt;/div&gt;
&lt;script type="text/javascript"&gt;
    $( function () {
        // 场景
        var scene = new THREE.Scene();

        var aspect = window.innerWidth / window.innerHeight;
        /**
         * 定义一个透视镜头，参数：
         * fov FOV垂直分量，镜头到视截锥近平面上下边之间的夹角
         * aspect 视截锥的纵横比
         * near 近平面离镜头多远
         * far 远平面离镜头多远
         */
        var camera = new THREE.PerspectiveCamera( 45, aspect, 0.1, 1000 );

        // WebGL渲染器，使用显卡来渲染场景。尽管还存在其它渲染器实现，但是处于性能、特性的考虑，不推荐使用
        var renderer = new THREE.WebGLRenderer();
        // 设置背景色，第二参数为透明度
        renderer.setClearColor( 0xEEEEEE, 1 );
        // 设置场景的大小
        renderer.setSize( window.innerWidth, window.innerHeight );

        // 创建一个调试用途的坐标轴，X轴红色、Y轴绿色、Z轴蓝色。20表示轴线长度
        var axes = new THREE.AxisHelper( 20 );
        // 把对象添加到场景中
        scene.add( axes );

        // 创建一个平面几何图形，宽60高20，在宽、高方向上分段数为1（不切分）
        var planeGeometry = new THREE.PlaneGeometry( 60, 20, 1, 1 );
        // 材质，定义颜色、透明度、反光效果之类的属性
        // MeshBasicMaterial是一种简单材质，它不受光线影响，使用纯色或者网格（wireframe）渲染几何图形
        var planeMaterial = new THREE.MeshBasicMaterial( { color: 0xcccccc } );
        // Mesh表示一类基于三角形网格（triangular polygon mesh）的对象
        var plane = new THREE.Mesh( planeGeometry, planeMaterial );
        // 默认的，平面的对称中心位于原点，width与X轴平行，height与Y轴平行
        // 在X轴方向逆时针（从原点往X轴正向看）旋转90度
        plane.rotation.x = -0.5 * Math.PI; // 圆周长2PI，PI代表180度，
        // 在X轴方向偏移15
        plane.position.x = 15;
        plane.position.y = 0;
        plane.position.z = 0;
        scene.add( plane );

        // 类似的，创建一个立方体，类似的，其对称中心也是默认位于原点
        var cubeGeometry = new THREE.CubeGeometry( 4, 4, 4 );
        // wireframe表示绘制网格线
        var cubeMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } );
        var cube = new THREE.Mesh( cubeGeometry, cubeMaterial );
        cube.position.x = -4;
        cube.position.y = 3;
        cube.position.z = 0;
        scene.add( cube );

        // 绘制一个球体
        var sphereGeometry = new THREE.SphereGeometry( 4, 20, 20 );
        var sphereMaterial = new THREE.MeshBasicMaterial( { color: 0x7777ff, wireframe: true } );
        var sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
        sphere.position.x = 20;
        sphere.position.y = 4;
        sphere.position.z = 2;
        scene.add( sphere );

        // 移动镜头
        camera.position.x = -30;
        camera.position.y = 40;
        camera.position.z = 30;
        // 将镜头指向场景的中心
        camera.lookAt( scene.position );
        
        // 渲染
        $( "#WebGL" ).append( renderer.domElement );
        renderer.render( scene, camera );
    } );
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
<p>渲染效果：<img class="aligncenter size-full wp-image-14416" src="https://blog.gmem.cc/wp-content/uploads/2017/01/first-threejs-scene-01.png" alt="first-threejs-scene-01" width="100%" /></p>
<div class="blog_h2"><span class="graybg">添加材质/光影效果</span></div>
<p>本节我们改进一下上面的例子，修改材质，并添加光线、阴影效果。</p>
<p>首先，为场景添加一个光源：</p>
<pre class="crayon-plain-tag">// 聚光灯效果的白色光源
var spotLight = new THREE.SpotLight( 0xffffff );
// 设置聚光灯的位置
spotLight.position.set( -40, 60, -10 );
scene.add( spotLight );</pre>
<p>添加这段代码后，渲染效果不会有任何改变。原因我们已经在前面的代码注释中提到过， MeshBasicMaterial这种材质不会对光线作出反应。我们替换一下材质：</p>
<pre class="crayon-plain-tag">var planeMaterial = new THREE.MeshLambertMaterial( { color: 0xcccccc } );
// ...
var cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000 } );
// ... 
var sphereMaterial = new THREE.MeshLambertMaterial( { color: 0x7777ff} );</pre>
<p>除了MeshLambertMaterial之外，MeshPhongMaterial也会对光源作出反应。</p>
<p>现在刷新一下页面，可以看到如下渲染效果：<img class="aligncenter size-full wp-image-14419" src="https://blog.gmem.cc/wp-content/uploads/2017/01/first-threejs-scene-02.png" alt="first-threejs-scene-02" width="100%" /></p>
<p>比上一幅截图好看多了，但是还有点不自然，因为没有阴影效果。</p>
<p>由于渲染阴影比较消耗资源，因此默认情况下Three.js关闭了阴影。要启用阴影其实很简单：</p>
<pre class="crayon-plain-tag">// 启用阴影效果
renderer.shadowMapEnabled = true;</pre>
<p>此外，你还需要定义什么对象产生（cast）阴影，什么对象接收阴影：</p>
<pre class="crayon-plain-tag">// 阴影由平面接收
plane.receiveShadow = true;
// ...
cube.castShadow = true;
// ...
sphere.castShadow = true;


// 设置产生阴影的光源
spotLight.castShadow = true;
// 提高阴影质量
spotLight.shadowMapWidth = spotLight.shadowMapHeight = 1024 * 4;</pre>
<div class="blog_h2"><span class="graybg">添加动画效果</span></div>
<p>要想为场景添加动画效果，我们需要找到定期重渲染场景的方法。setInterval()这种定时器是不适合的，因为它与渲染行为不是同步的，会导致严重性能问题。</p>
<p><pre class="crayon-plain-tag">requestAnimationFrame()</pre> 是现代浏览器支持的、避免两setInterval()缺点的函数。你可以为它提供一个回调，此回调会定期（间隔由浏览器定义）的被调用。在回调中你可以指定任何渲染逻辑，浏览器负责尽可能平滑、高效的绘制。示例代码：</p>
<pre class="crayon-plain-tag">function renderScene() {
    requestAnimationFrame( renderScene );
    renderer.render( scene, camera );
}</pre>
<p>上面的函数把自己传递给requestAnimationFrame，从而导致函数的逻辑被反复调用，从而可以产生动画效果。</p>
<div class="blog_h3"><span class="graybg">FPS统计</span></div>
<p>为了显示动画帧率信息，我们引入一个助手库<a href="https://github.com/mrdoob/stats.js/">stats.js</a>：</p>
<pre class="crayon-plain-tag">&lt;script src="stat.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript"&gt;
    var stats = new Stats();
    stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom
    document.body.appendChild( stats.dom );
    function renderScene() {
        // 开始统计
        stats.begin();
        // 这里编写被监控的代码
        stats.end();
        requestAnimationFrame( animate );
    }
    requestAnimationFrame( renderScene );
&lt;/script&gt;</pre>
<div class="blog_h3"><span class="graybg">添加动画</span></div>
<p>下面我们为立方体添加一个翻滚效果，为球体添加一个弹跳效果：</p>
<pre class="crayon-plain-tag">var step = 0;

function animate() {
    stats.begin();
    cube.rotation.x += 0.02;
    cube.rotation.y += 0.02;
    cube.rotation.z += 0.02;
    step += 0.04; // 定义弹跳速度
    sphere.position.x = 20 + ( 10 * (Math.cos( step )));
    sphere.position.y = 2 + ( 10 * Math.abs( Math.sin( step ) ));
    renderer.render( scene, camera );  // 反复渲染
    stats.end();
    requestAnimationFrame( animate );
}

requestAnimationFrame( animate );</pre>
<p>刷新浏览器，可以查看动画效果，注意左上角的帧率窗口。</p>
<div class="blog_h1"><span class="graybg">使用基本组件</span></div>
<p>上一章的学习中，我们创建了由几个对象构成的简单场景，并制作了简单的动画效果。现在我们来更深入的了解一下构成Three.js场景的组件。 </p>
<div class="blog_h2"><span class="graybg">场景的内容物</span></div>
<p>之前我们调用<pre class="crayon-plain-tag">new THREE.Scene()</pre> 创建了一个场景。场景是一个容器，它内部可以包含三类东西：</p>
<ol>
<li>镜头（Camera）：决定了查看场景的角度和方式。在渲染场景的时候镜头可以自动创建，但是你也可以手工指定其参数</li>
<li>光源（Lights）：影响材质的渲染效果、阴影</li>
<li>物体（Objects）：场景中渲染的主要东西。包括各种几何形状、导入的模型</li>
</ol>
<div class="blog_h2"><span class="graybg">场景的基本API</span></div>
<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>children</td>
<td>所有对象组成的数组</td>
</tr>
<tr>
<td>getChildByName(name)</td>
<td>根据名称来查找对象</td>
</tr>
<tr>
<td>remove(obj)</td>
<td>从场景中移除一个对象</td>
</tr>
<tr>
<td>traverse(callback)</td>
<td>指定一个回调，针对场景中所有对象调用之</td>
</tr>
<tr>
<td>fog</td>
<td>添加烟雾效果，这样越远的物体显示越模糊：<br />
<pre class="crayon-plain-tag">// 白色雾，从near=0.015开始出现，far=100表示雾变浓厚的速率
scene.fog = new THREE.Fog( 0xffffff, 0.015, 100 );
// 指定颜色、浓度
scene.fog = new THREE.FogExp2( 0xffffff, 0.01 );</pre>
</td>
</tr>
<tr>
<td>overrideMaterial</td>
<td>覆盖场景中所有物体的材质设置：<br />
<pre class="crayon-plain-tag">scene.overrideMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Geometry/Mesh的基本API</span></div>
<p>Three.js提供了大量开箱即用的Geometry， Geometry用于定义物体的形状，材质则定义其外观。</p>
<p>在大部分3D图形库中，Geometry基本上都是三维空间中一系列点、以及连接这些点的面的集合。以立方体为例：</p>
<ol>
<li>每个立方体包含8个角，这些角可以由三维空间中的一个点来确定。这些点称为顶点（vertices）</li>
<li>每个立方体包含6个面，这些面的每个角都对应一个顶点。这些面称为face</li>
</ol>
<p>当使用Three.js自带的Geometry时你不需要逐个定义所有点、面。例如对于Cube，你只需要定义长宽高即可，Three.js会利用你提供的这些信息创建所有必须的点、面。</p>
<p>Three.js允许自定义点、面，然后构成一个几何图形。下面是手工构建Cube的例子：</p>
<pre class="crayon-plain-tag">// 所有顶点
var vertices = [
    new THREE.Vector3( 1, 3, 1 ),
    new THREE.Vector3( 1, 3, -1 ),
    new THREE.Vector3( 1, -1, 1 ),
    new THREE.Vector3( 1, -1, -1 ),
    new THREE.Vector3( -1, 3, -1 ),
    new THREE.Vector3( -1, 3, 1 ),
    new THREE.Vector3( -1, -1, -1 ),
    new THREE.Vector3( -1, -1, 1 )
];
// 所有三角形的面，数字为从0开始的顶点序号
var faces = [
    new THREE.Face3( 0, 2, 1 ),
    new THREE.Face3( 2, 3, 1 ),
    new THREE.Face3( 4, 6, 5 ),
    new THREE.Face3( 6, 7, 5 ),
    new THREE.Face3( 4, 5, 1 ),
    new THREE.Face3( 5, 0, 1 ),
    new THREE.Face3( 7, 6, 2 ),
    new THREE.Face3( 6, 3, 2 ),
    new THREE.Face3( 5, 7, 0 ),
    new THREE.Face3( 7, 2, 0 ),
    new THREE.Face3( 1, 3, 4 ),
    new THREE.Face3( 3, 6, 4 ),
];
var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
// 在重新设置顶点数组后，提示需要更新。这是因为Three.js默认假设Mesh的Geometry的形状在生命周期内保持不变
geom.verticesNeedUpdate = true;
// 根据顶点重新计算面法线
geom.computeFaceNormals();</pre>
<p>在以前版本的Three.js中，允许使用四边形来定义面。四边形在建模时比较受欢迎，原因是很容易被增强、平滑。三角形在渲染、游戏引擎中比较受欢迎，原因是比较简单。 </p>
<p>有了Geometry后，加上材质就可以构成简单的3D物体——Mesh了：</p>
<pre class="crayon-plain-tag">// 材质数组
var materials = [
    new THREE.MeshLambertMaterial( { opacity: 0.6, color: 0x44ff44, transparent: true } ),
    new THREE.MeshBasicMaterial( { color: 0x666666, wireframe: true } )
];
// Mesh组
var mesh = THREE.SceneUtils.createMultiMaterialObject( geom, materials );
scene.add( mesh );</pre>
<p>Three.js允许给Geometry应用多个材质，上例中的Cube既有颜色填充，也显示了线条，这是两种材质的混合效果。从实现角度来说，<span style="background-color: #c0c0c0;">Three.js创建了两个THREE.Mesh实例</span>，每个材质对应一个实例，这<span style="background-color: #c0c0c0;">两个实例被放置到一个组里面</span>。添加组到场景的方式，与添加Mesh一致。</p>
<p>我们可以调用组的forEach，对其中所有Mesh进行操作：</p>
<pre class="crayon-plain-tag">mesh.children.forEach( function ( e ) {
    e.castShadow = true
} );</pre>
<div class="blog_h3"><span class="graybg">Geometry的基本API</span></div>
<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>vertices</td>
<td>构成此Geometry的顶点坐标数组</td>
</tr>
<tr>
<td>faces</td>
<td>构成Geometry的三角形面数组</td>
</tr>
<tr>
<td>verticesNeedUpdate</td>
<td>修改顶点数组后，提示Three.js需要更新顶点</td>
</tr>
<tr>
<td>computeFaceNormals()</td>
<td>重新根据顶点来计算面</td>
</tr>
<tr>
<td>clone()</td>
<td>克隆一个Geometry</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Mesh的基本API</span></div>
<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>position.x|y|z<br />position.set(x,y,z)</td>
<td>此物体相对于父对象的位置，大部分物体的父对象是THREE.Scene对象，对于组中的Mesh，其父对象是组。示例代码：<br />
<pre class="crayon-plain-tag">// 方法一：
cube.position.x=10;
cube.position.y=3;
cube.position.z=1;
// 方法二：
cube.position.set(10,3,1);
// 方法三：
cube.postion=new THREE.Vector3(10,3,1)</pre>
</td>
</tr>
<tr>
<td>rotation.x|y|z</td>
<td>让物体围绕自己的轴（而不是场景的）旋转一定角度。与position类似，具有三种设置方法</td>
</tr>
<tr>
<td>scale.x|y|z</td>
<td>让物体在其轴方向缩放。与position类似，具有三种设置方法</td>
</tr>
<tr>
<td>translateX(amount)</td>
<td rowspan="3">
<p>将物体沿着X/Y/Z轴方向移动</p>
<p>这些方法指定的是相对位移，而position指定的是绝对值</p>
</td>
</tr>
<tr>
<td>translateY(amount)</td>
</tr>
<tr>
<td>translateZ(amount)</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">使用镜头</span></div>
<p>Three.js支持两种类型的镜头：正交（orthographic）镜头、透视（perspective）镜头。到目前为止我们还没有使用过正交镜头。</p>
<p>正交镜头的特点是，物品的渲染尺寸与它距离镜头的远近无关。也就是说在场景中移动一个物体，其大小不会变化。正交镜头适合2D游戏。</p>
<p>透视镜头则是模拟人眼的视觉特点，距离远的物体显得更小。透视镜头通常更适合3D渲染。</p>
<div class="blog_h3"><span class="graybg">THREE.PerspectiveCamera的API</span></div>
<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>fov</td>
<td>
<p>视界，从镜头可以看到的场景的部分。其值为镜头到近平面上下边的夹角</p>
<p>人眼的FOV接近180度，某些鸟类的FOV打到360度。但是计算机屏幕做不到覆盖视野，通常3D游戏的FOV取值在60-90度之间</p>
<p>较好的默认值为45</p>
</td>
</tr>
<tr>
<td>aspect</td>
<td>渲染区域的纵横比。较好的默认值为window.innerWidth/window.innerHeight</td>
</tr>
<tr>
<td>near</td>
<td>近平面离镜头的距离。较好的默认值为0.1</td>
</tr>
<tr>
<td>far</td>
<td>远平面离镜头的距离。较好的默认值为1000</td>
</tr>
</tbody>
</table>
<p>关于这些参数的形象化描述，请参考<a href="#ViewFrustum">术语视截锥</a>中的截图。</p>
<div class="blog_h3"><span class="graybg">THREE.OrthographicCamera的API</span></div>
<p>正交镜头不关心FOV、纵横比这些概念。其构造函数实际上是指定了一个Cube，落在其中的物体会被渲染：</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>left</td>
<td>相机截锥左平面位置，如果你将其设置为-100，则位置在其更左边的物体将不可见</td>
</tr>
<tr>
<td>right</td>
<td>相机截锥右平面位置</td>
</tr>
<tr>
<td>top</td>
<td>相机截锥上平面位置</td>
</tr>
<tr>
<td>bottom</td>
<td>相机截锥下平面位置</td>
</tr>
<tr>
<td>near</td>
<td>近平面的位置</td>
</tr>
<tr>
<td>far</td>
<td>远平面的位置</td>
</tr>
</tbody>
</table>
<p>关于这些参数的形象化描述，参考下图：</p>
<div class="blog_h2"><img class="aligncenter size-full wp-image-14445" src="https://blog.gmem.cc/wp-content/uploads/2017/01/orthographic-camera.png" alt="orthographic-camera" width="100%" /><span class="graybg">镜头聚焦</span></div>
<p>创建镜头后，还需要将其移动、然后对准物体积聚的场景中心位置，才能确保物体的渲染。移动镜头，通过设置其position属性来实现：</p>
<pre class="crayon-plain-tag">camera.position.x = 120;
camera.position.y = 60;
camera.position.z = 180;</pre>
<p>聚焦，则是调用下面的方法实现：</p>
<pre class="crayon-plain-tag">camera.lookAt( new THREE.Vector3( x, 10, 0 ) );</pre>
<div class="blog_h2"><span class="graybg">HUD</span></div>
<p>所谓HUD（head-up display，平视显示），是指在屏幕（挡风玻璃）上显示一些辅助信息（例如飞机、汽车的仪表信息），避免驾驶员低头分散注意力。HUD的特点是其显示内容的大小、位置与镜头无关。</p>
<p>要实现HUD效果，可以同时渲染两套场景，其中HUD场景使用OrthographicCamera镜头：</p>
<pre class="crayon-plain-tag">var scene = new THREE.Scene();
var sceneOrtho = new THREE.Scene();

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 250);
// 正交镜头的近平面的大小，和浏览器窗口大小一致
var cameraOrtho = new THREE.OrthographicCamera(0, window.innerWidth, window.innerHeight, 0, -10, 10);

var webGLRenderer = new THREE.WebGLRenderer();

webGLRenderer.render(scene, camera);
// 防止在下一次render时，自动清屏
webGLRenderer.autoClear = false;
webGLRenderer.render(sceneOrtho, cameraOrtho);</pre>
<div class="blog_h1"><span class="graybg">使用光源</span></div>
<p>在Three.js中光源很重要。不设置光源你就看不到被渲染的物体。Three.js内置了多种光源以满足特定场景的需要。</p>
<div class="blog_h2"><span class="graybg">光源分类</span></div>
<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>AmbientLight</td>
<td>环境光，其颜色均匀的应用到场景及其所有对象上</td>
</tr>
<tr>
<td>PointLight</td>
<td>3D空间中的一个点光源，向所有方向发出光线</td>
</tr>
<tr>
<td>SpotLight</td>
<td>产生圆锥形光柱的聚光灯，台灯、天花板射灯通常都属于这类光源</td>
</tr>
<tr>
<td>DirectionalLight</td>
<td>也就无限光，光线是平行的。典型的例子是日光</td>
</tr>
<tr>
<td>HemisphereLight</td>
<td>特殊光源，用于创建户外自然的光线效果，此光源模拟物体表面反光效果、微弱发光的天空</td>
</tr>
<tr>
<td>AreaLight</td>
<td>面光源，指定一个发光的区域</td>
</tr>
<tr>
<td>LensFlare</td>
<td>不是光源，用于给光源添加镜头光晕效果</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基本光源</span></div>
<div class="blog_h3"><span class="graybg">AmbientLight</span></div>
<p>这种光源为场景添加全局的环境光。这种光没有特定的方向，不会产生阴影。通常不会把AmbientLight作为唯一的光源，而是和SpotLight、DirectionalLight等光源结合使用，从而达到<span style="background-color: #c0c0c0;">柔化阴影、添加全局色调</span>的效果。</p>
<p>指定颜色时要相对保守，例如#0c0c0c。设置太亮的颜色会导致整个画面过度饱和，什么都看不清：</p>
<pre class="crayon-plain-tag">var ambiColor = "#0c0c0c";
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);</pre>
<div class="blog_h3"><span class="graybg">PointLight</span></div>
<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>color</td>
<td>光线的颜色</td>
</tr>
<tr>
<td>intensity</td>
<td>光线的强度，默认1，浮点数</td>
</tr>
<tr>
<td>distance</td>
<td>光线能照耀的距离</td>
</tr>
<tr>
<td>position</td>
<td>光源的位置</td>
</tr>
<tr>
<td>visible</td>
<td>设置为true则打开光源</td>
</tr>
</tbody>
</table>
<p>示例代码：</p>
<pre class="crayon-plain-tag">var pointColor = "#ccffcc";
var pointLight = new THREE.PointLight( pointColor );
pointLight.distance = 100;
scene.add( pointLight );
// 设置强度
pointLight.intensity = 2.4;</pre>
<div class="blog_h3"><span class="graybg">SpotLight</span></div>
<p>这种光源的使用场景最多，特别是在你需要阴影效果的时候。PointLight的所有属性对于SpotLight可用，前者还包括以下属性：</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>castShadow</td>
<td>
<p>此光源是否可以导致物体产生阴影</p>
<p>注意：目标物体需要设置receiveShadow</p>
</td>
</tr>
<tr>
<td>shadowCameraNear</td>
<td>从距离光源多远的地方开始创建阴影</td>
</tr>
<tr>
<td>shadowCameraFar</td>
<td>到距离光源多远的地方不再创建阴影</td>
</tr>
<tr>
<td>shadowCameraFov</td>
<td>阴影的FOV</td>
</tr>
<tr>
<td>target</td>
<td>此光源指向的目标。光线从光源照向该目标：<br />
<pre class="crayon-plain-tag">var targetObject = new THREE.Object3D();
scene.add(targetObject);
// 聚光灯将跟踪三维空间中的一个点
light.target = targetObject;</pre>
</td>
</tr>
<tr>
<td>shadowBias</td>
<td>设置阴影的位置偏移</td>
</tr>
<tr>
<td>angle</td>
<td>光锥的夹角，默认Math.PI/3</td>
</tr>
<tr>
<td>exponent</td>
<td>衰减指数，即随着与光源距离的增加，光线衰减的速度</td>
</tr>
<tr>
<td>onlyShadow</td>
<td>如果设置为true，仅仅产生阴影，而不照亮物体</td>
</tr>
<tr>
<td>shadowCameraVisible</td>
<td>如果设置为true，你将看到光源如何、从何处产生阴影（显示截锥）。用于调试目的</td>
</tr>
<tr>
<td>shadowDarkness</td>
<td>阴影的暗度，默认0.5。一旦场景被创建此参数即不可修改</td>
</tr>
<tr>
<td>shadowMapWidth<br />shadowMapHeight</td>
<td>
<p>有多少像素用于创建阴影，如果阴影出现锯齿效果，可以增加此参数。一旦场景被创建此参数即不可修改</p>
<p>另一种减轻阴影锯齿的方法是，让阴影相机截锥尽可能小</p>
</td>
</tr>
</tbody>
</table>
<p>示例代码：</p>
<pre class="crayon-plain-tag">var spotLight = new THREE.SpotLight(pointColor);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
spotLight.shadowCameraNear = 2;
spotLight.shadowCameraFar = 200;
spotLight.shadowCameraFov = 30;
spotLight.target = plane;   // 跟踪目标
spotLight.distance = 0;
spotLight.angle = 0.4;

scene.add(spotLight);</pre>
<p>光锥的宽、高可以基于以下代码求出：</p>
<pre class="crayon-plain-tag">var coneLength = light.distance ? light.distance : 10000;
var coneWidth = coneLength * Math.tan( light.angle * 0.5 ) * 2;</pre>
<div class="blog_h3"><span class="graybg">DirectionalLight</span></div>
<p>用于模拟遥远的，类似太阳那样的光源。该光源与SpotLight的主要区别是，它不会随着距离而变暗，所有被照耀的地方获得相同的光照强度。</p>
<p>DirectionalLight具有大部分SpotLight的属性。示例代码：</p>
<pre class="crayon-plain-tag">var directionalLight = new THREE.DirectionalLight( pointColor );
directionalLight.position.set( -40, 60, -10 );
directionalLight.castShadow = true;
directionalLight.shadowCameraNear = 2;
directionalLight.shadowCameraFar = 200;
directionalLight.shadowCameraLeft = -50;
directionalLight.shadowCameraRight = 50;
directionalLight.shadowCameraTop = 50;
directionalLight.shadowCameraBottom = -50;

directionalLight.distance = 0;
directionalLight.intensity = 0.5;
directionalLight.shadowMapHeight = 1024;
directionalLight.shadowMapWidth = 1024;

scene.add( directionalLight );</pre>
<div class="blog_h2"><span class="graybg">高级光源</span></div>
<div class="blog_h3"><span class="graybg">HemisphereLight</span></div>
<p>模拟穹顶（半球）的微弱发光效果，让户外场景更加逼真。使用DirectionalLight + AmbientLight可以在某种程度上来模拟户外光线，但是不够真实，因为无法体现大气层的散射效果、地面或物体的反射效果。常用属性：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>color</td>
<td>天空散射的光线颜色</td>
</tr>
<tr>
<td>groundColor</td>
<td>地面散射的光线颜色</td>
</tr>
<tr>
<td>intensity</td>
<td>光线强度</td>
</tr>
</tbody>
</table>
<p>示例代码：</p>
<pre class="crayon-plain-tag">// 三个参数分别对应天空颜色、地面颜色、强度
var hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6);
hemiLight.position.set(0, 500, 0);
scene.add(hemiLight);</pre>
<div class="blog_h3"><span class="graybg">AreaLight</span></div>
<p>用于定义一个发光的矩形区域，该光源属于Three.js扩展。</p>
<p>THREE.WebGLRenderer这个渲染器不能和AreaLight一起使用，原因是THREE.AreaLight是一种复杂的光源，与WebGLRenderer一起使用会导致严重的性能问题。</p>
<p>渲染器THREE.WebGLDeferredRenderer使用不同的途径来渲染场景，它将渲染拆分为几个步骤。它能够处理复杂的光源或者数量众多的光源。</p>
<div class="blog_h1"><span class="graybg">使用材质</span></div>
<p>通过前面章节的学习，我们已经知道材质 + Geometry可以构成Mesh——可以添加到3D场景中的物体。</p>
<p>Geometry就好像是骨架，材质则类似于皮肤，它定义了Geometry的外观——是否有金属质感、是否透明、是否显示为线框（wireframe）。</p>
<div class="blog_h2"><span class="graybg">材质分类</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">材质</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>MeshBasicMaterial</td>
<td>基本的材质，显示为简单的颜色或者显示为线框。不考虑光线的影响</td>
</tr>
<tr>
<td>MeshDepthMaterial</td>
<td>使用简单的颜色，但是颜色深度和距离相机的远近有关</td>
</tr>
<tr>
<td>MeshNormalMaterial</td>
<td>基于面Geometry的法线（normals）数组来给面着色</td>
</tr>
<tr>
<td>MeshFacematerial</td>
<td>容器，允许为Geometry的每一个面指定一个材质</td>
</tr>
<tr>
<td>MeshLambertMaterial</td>
<td>考虑光线的影响，哑光材质</td>
</tr>
<tr>
<td>MeshPhongMaterial</td>
<td>考虑光线的影响，光泽材质</td>
</tr>
<tr>
<td>ShaderMaterial</td>
<td>允许使用自己的着色器来控制顶点如何被放置、像素如何被着色</td>
</tr>
<tr>
<td>LineBasicMaterial</td>
<td>用于THREE.Line对象，创建彩色线条</td>
</tr>
<tr>
<td>LineDashMaterial</td>
<td>用于THREE.Line对象，创建虚线条</td>
</tr>
<tr>
<td>RawShaderMaterial</td>
<td>仅和THREE.BufferedGeometry联用，优化静态Geometry（顶点、面不变）的渲染</td>
</tr>
<tr>
<td>SpriteCanvasMaterial</td>
<td rowspan="3">在针对单独的点进行渲染时用到</td>
</tr>
<tr>
<td>SpriteMaterial</td>
</tr>
<tr>
<td>PointCloudMaterial</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">公共属性</span></div>
<p>作为所有材质的基类，THREE.Material提供了以下属性：</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 colspan="2"><em><strong>基本属性</strong></em><br />最常用的属性，用于控制对象的透明度、是否可见、如何被引用（基于ID还是名称）</td>
</tr>
<tr>
<td>id</td>
<td>当你创建一个材质的时候自动分配，作为材质实例的标识，从0开始自动计数</td>
</tr>
<tr>
<td>uuid</td>
<td>全局唯一标识，内部使用</td>
</tr>
<tr>
<td>name</td>
<td>给材质分配一个名称，调试用</td>
</tr>
<tr>
<td>opacity</td>
<td>
<p>设定材质的透明度，和transparent联用，范围0～1</p>
</td>
</tr>
<tr>
<td>transparent</td>
<td>
<p>如果设置为true，则Three.js考虑opacity的设置。对于具有Alpha通道的纹理，该属性也需要设置为true</p>
</td>
</tr>
<tr>
<td>overdraw</td>
<td>使用THREE.CanvasRenderer渲染器时多边形会比预期的绘制的大一些。如果使用该渲染器时你法线Gaps可设置为true </td>
</tr>
<tr>
<td>visible</td>
<td>材质是否可见，如果设置为false则物体看不见</td>
</tr>
<tr>
<td>side</td>
<td>
<p>材质应用到目标Geomotry的哪一面。默认THREE.Frontside表示应用在外面，可选值THREE.BackSide应用在里面、THREE.DoubleSide应用到两面</p>
<p>对于不封闭空间的Geomotry，例如平面，此属性重要</p>
</td>
</tr>
<tr>
<td>needsUpdate</td>
<td>改变材质的某些属性后，你可以设置该属性为true，这样Three.js就会丢弃缓存，重新渲染材质</td>
</tr>
<tr>
<td colspan="2"><em><strong>混合（Blending）属性</strong></em><br />定义对象如何与其背景混合，或者说我们渲染的颜色如何与其背后的颜色交互</td>
</tr>
<tr>
<td>blending </td>
<td>决定材质如何与背景混合，默认值THREE.NormalBlending，表示仅仅显示顶层颜色</td>
</tr>
<tr>
<td>blendsrc</td>
<td>定义物体（源）如何混合到背景（目标）中，默认THREE.SrcAlphaFactor表示基于物体的Alpha通道进行混合</td>
</tr>
<tr>
<td>blenddst</td>
<td>定义在混合时，背景（目标）如何渲染，默认THREE.OneMinusSrcAlphaFactor表示基于物体的Alpha通道进行混合</td>
</tr>
<tr>
<td> blendequation</td>
<td>定义blendsrc、blenddst如何使用，默认将它们相加（AddEquation）</td>
</tr>
<tr>
<td colspan="2"><strong><em>高级属性</em></strong><br />控制低级别的WebGL上下文如何渲染对象，大部分情况下不需要使用</td>
</tr>
<tr>
<td>depthTest</td>
<td>
<p>如果关闭depthTest，意味着同时关闭reading/testing/writing</p>
<p>到底什么是深度测试（depthTest）呢？假设由两个完全一样的形状，位于你的正前方。真实世界中，你仅能看到里你近的那一个。但是在3D渲染过程中：</p>
<ol>
<li>如果远的物体先被绘制，那么没有问题，效果和真实世界一致</li>
<li>如果近的物体先被绘制，远物体后被绘制，就会有问题，远物体可以被看见</li>
</ol>
<p>所谓深度测试，是现代GPU中内置的一个工具，能够让渲染输出总是符合预期，而不管对象的输出先后顺序。具体实现机制是：当绘制一个像素时，会查看此像素位置原先的depth（即离相机的远近）值，<span style="background-color: #c0c0c0;">如果新的像素depth值较小，则执行绘制</span>，否则保留原来的值</p>
<p>由于深度测试的实现机制，和透明度（混合）在一起工作时可能出现问题，有时候需要禁用</p>
</td>
</tr>
</tbody>
</table>
<p>本章跳过了所有和纹理（textures）、映射（maps）、动画 有关的属性。</p>
<div class="blog_h2"><span class="graybg">简单Mesh材质</span></div>
<p>你可以把属性组成一个对象，作为构造函数的入参：</p>
<pre class="crayon-plain-tag">var material = new THREE.MeshBasicMaterial( {
    color: 0xff0000,
    name: 'material-1',
    opacity: 0.5,
    transparency: true
} );</pre>
<p>或者逐个的设置属性：</p>
<pre class="crayon-plain-tag">var material = new THREE.MeshBasicMaterial();
material.color = new THREE.Color( 0xff0000 );  // 这种方式必须提供Color对象
material.name = 'material-1';
material.opacity = 0.5;
material.transparency = true;</pre>
<div class="blog_h3"><span class="graybg">THREE.MeshBasicMaterial</span></div>
<p>该材质不考虑场景中的光源，目标物体被渲染成简单的、扁平（Flat）的形状。可选的，你可以显示物体的线框（Wireframe），线框由所有面的边构成。</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>color</td>
<td>材质的颜色</td>
</tr>
<tr>
<td>wireframe</td>
<td>是否显示线框。显示线框对于调试有帮助</td>
</tr>
<tr>
<td>Wireframelinewidth</td>
<td>线框的线条宽度</td>
</tr>
<tr>
<td>shading</td>
<td>
<p>定义如何着色，可选值THREE.SmoothShading、THREE.NoShading、THREE.FlatShadin</p>
<p>默认值THREE.SmoothShading，导致渲染平滑的渲染——例如平滑过渡颜色</p>
</td>
</tr>
<tr>
<td>vertexColors</td>
<td>可以定义各个顶点的颜色，默认值THREE.NoColors。你可以设置为THREE.VertexColors，这样渲染器会考虑Geometry.colors属性</td>
</tr>
<tr>
<td>fog</td>
<td>该材质是否被全局迷雾效果影响</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.MeshDepthMaterial</span></div>
<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>wireframe</td>
<td>是否显示线框</td>
</tr>
<tr>
<td>wireframeLineWidth</td>
<td>线框的宽度</td>
</tr>
</tbody>
</table>
<p>设置相机的near、far属性，可以决定使用此材质的物体的淡出速度 。如果far - near很大，则物体的淡出速度非常慢。</p>
<div class="blog_h3"><span class="graybg">THREE.MeshNormalMaterial</span></div>
<p>每个面显示特定的颜色，其颜色取决于该面的法线（垂直于面的向量）。当物体旋转时，其固定角度的颜色保持不变。</p>
<p>法线在Three.js中被大量使用，它被用来确定光线反射效果、帮助映射纹理到3D模型，并且为如何照亮、shade、染色（color）一个表面上的像素点。</p>
<p>为了查看法线的方向，我们可以使用THREE.ArrowHelper：</p>
<pre class="crayon-plain-tag">//遍历球体的所有面
for ( var f = 0, fl = sphere.geometry.faces.length; f &lt; fl; f++ ) {
    var face = sphere.geometry.faces[ f ];
    // 计算面的中心点：把面的3个顶点依次加到三维向量中，然后除以3
    var centroid = new THREE.Vector3( 0, 0, 0 );
    centroid.add( sphere.geometry.vertices[ face.a ] );
    centroid.add( sphere.geometry.vertices[ face.b ] );
    centroid.add( sphere.geometry.vertices[ face.c ] );
    centroid.divideScalar( 3 );
    // 创建一个箭头助手
    var arrow = new THREE.ArrowHelper(
        face.normal,  // 法线矢量（箭头方向）
        centroid,   // 中心点 （箭头起点）
        2, // 长度
        0x3333FF, // 颜色
        0.5,  //箭头长度
        0.5 //箭头宽度
    );
    sphere.add( arrow );
}</pre>
<p>该材质的额外属性包括：wireframe、wireframeLineWidth、shading。 使用FlatShading、SmoothShading的效果分别如下图：</p>
<p><img class="aligncenter size-full wp-image-14459" src="https://blog.gmem.cc/wp-content/uploads/2017/01/shading-diff.png" alt="shading-diff" width="582" height="297" /></p>
<div class="blog_h3"><span class="graybg">THREE.MeshFaceMaterial</span></div>
<p>这不是一个单独的材质，而是一个容器。使用它，你可以为每个面指定材质。例如，对于具有12个面（Three.js仅支持三角形面）的Cube，你可以指定具有12个元素的MeshFaceMaterial：</p>
<pre class="crayon-plain-tag">var mats = [];
mats.push(new THREE.MeshBasicMaterial({color: 0x009e60}));
mats.push(new THREE.MeshBasicMaterial({color: 0x009e60}));
mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffd500}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffd500}));
mats.push(new THREE.MeshBasicMaterial({color: 0xff5800}));
mats.push(new THREE.MeshBasicMaterial({color: 0xff5800}));
mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A}));
mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffffff}));
mats.push(new THREE.MeshBasicMaterial({color: 0xffffff}));

var faceMaterial = new THREE.MeshFaceMaterial(mats);

var cubeGeom = new THREE.BoxGeometry( 2.9, 2.9, 2.9 );
var cube = new THREE.Mesh( cubeGeom, faceMaterial );</pre>
<p>你可以设置<pre class="crayon-plain-tag">geometry.faces[*].materialIndex</pre>  来指名某个面使用MeshFaceMaterial中的哪个元素来渲染。</p>
<div class="blog_h2"><span class="graybg">联合多个材质</span></div>
<p>像MeshDepthMaterial这样的材质，不能设置颜色或者纹理，基本不能单独使用。</p>
<p>Three.js允许联合使用多个材质，以产生新的特效。<span style="background-color: #c0c0c0;">材质联合也使混合（blending）有意义</span>。示例：</p>
<pre class="crayon-plain-tag">var cubeMaterial = new THREE.MeshDepthMaterial();
var colorMaterial = new THREE.MeshBasicMaterial( {
    // 绿色材质
    color: 0x00ff00,
    // 允许透明度
    transparent: true,
    // 决定如何与背景（即使用了MeshDepthMaterial的那个内部盒子）进行交互
    // MultiplyBlending将前景、背景色进行乘积运算（正片叠底）
    blending: THREE.MultiplyBlending
} );
// 创建两个物体构成的组
var cube = new THREE.SceneUtils.createMultiMaterialObject( cubeGeometry, [ colorMaterial, cubeMaterial ] );
// 避免两个相同大小的重叠物体产生闪烁
cube.children[ 1 ].scale.set( 0.99, 0.99, 0.99 );</pre>
<div class="blog_h2"><span class="graybg">高级材质</span></div>
<div class="blog_h3"><span class="graybg">THREE.MeshLambertMaterial </span></div>
<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>ambient</td>
<td>材质的阴影色，与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合（正片叠底），默认白色</td>
</tr>
<tr>
<td>emissive</td>
<td>材质发出的光线的颜色，注意这不会让材质成为光源，只是一个不会被其它光源影响的颜色而已，默认黑色 </td>
</tr>
<tr>
<td>wrapAround</td>
<td>
<p>设置为true则启用半环境光（half-lambert lighting）技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域，设置为true可以柔化阴影、更均匀的分散（distribute）光线</p>
</td>
</tr>
<tr>
<td>wrapRGB</td>
<td>当wrapAround设置为true时，使用一个THREE.Vector3来控制光线减弱（drop off）的速度，可以用来微调物体的色泽</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.MeshPhongMaterial</span></div>
<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>ambient</td>
<td>材质的阴影色，与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合（正片叠底），默认白色</td>
</tr>
<tr>
<td>emissive</td>
<td>材质发出的光线的颜色，注意这不会让材质成为光源，只是一个不会被其它光源影响的颜色而已，默认黑色</td>
</tr>
<tr>
<td>specular</td>
<td>
<p>材质的高光色，即反光的颜色。如果将其设置：</p>
<ol>
<li>和color属性相同，可以得到金属质感（metallic-looking）的材质</li>
<li>为灰色，可以得到塑料质感（plastic-looking）的材质</li>
</ol>
</td>
</tr>
<tr>
<td>shininess</td>
<td>高光色的亮度，默认30</td>
</tr>
<tr>
<td>metal</td>
<td>设置为true，则Three.js更改算法，让材质更加像金属</td>
</tr>
<tr>
<td>wrapAround</td>
<td>
<p>设置为true则启用半环境光（half-lambert lighting）技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域，设置为true可以柔化阴影、更均匀的分散（distribute）光线</p>
</td>
</tr>
<tr>
<td>wrapRGB</td>
<td>当wrapAround设置为true时，使用一个THREE.Vector3来控制光线减弱（drop off）的速度，可以用来微调物体的色泽</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.ShaderMaterial</span></div>
<p>基于这种材质，可以应用自己开发的着色器。通过定制着色器，你可以精确的定义物体如何被渲染，或者修改Threee.js的默认渲染行为。</p>
<p>ShaderMaterial支持wireframe、Wireframelinewidth、linewidth、shading、vertexColors、fog以及以下额外属性：</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>fragmentShader</td>
<td>
<p>使用的片断着色器程序的名称</p>
<p>片断着色器，也叫像素着色器（pixel shader）。用于定义顶点之间每个点如何渲染</p>
</td>
</tr>
<tr>
<td>vertexShader</td>
<td>
<p>使用的顶点着色器程序的名称</p>
<p>顶点着色器，可以操控顶点的属性（例如改变顶点位置）</p>
<p>如果你像让多边形为全红色，可以基于此着色器，指定所有顶点为红色（此颜色信息会传递给片断着色器）。反之，如果你想在顶点之间产生渐变效果，则需要基于片断着色器</p>
<p>顶点着色器位于图形管线（graphic pipeline）的早期，在模型坐标转换、多边形修剪（clipping）之前，此时实际渲染工作并为开始</p>
</td>
</tr>
<tr>
<td>uniforms</td>
<td>用于向着色器程序发送信息，相同的信息被传递给每个vertex、fragment</td>
</tr>
<tr>
<td>defines </td>
<td>转换为#define代码片断，设置一些全局变量供着色器程序使用</td>
</tr>
<tr>
<td>attributes</td>
<td>用于传递位置性的、法线相关的信息。如果使用该属性，必须为每个顶点提供</td>
</tr>
<tr>
<td>lights</td>
<td>是否把光照数据传入着色器，默认false</td>
</tr>
</tbody>
</table>
<p>对于前面已经讨论过的其它材质，Three.js已经提供了它们的片断着色器、顶点着色器。</p>
<div class="blog_h3"><span class="graybg">GSGL</span></div>
<p>着色器不是基于JavaScript语言编写的，它的专用语言是GSGL，即OpenGL ES着色器语言的WebGL支持。这种语言的语法风格类似于C语言。</p>
<div class="blog_h3"><span class="graybg">示例一：动画材质</span></div>
<p>在本节，我们编写：</p>
<ol>
<li>一个简单顶点着色器。该着色器能够修改Cube顶点的坐标值</li>
<li>多个借用自<a href="http://glslsandbox.com/">glslsandbox</a>代码的片断着色器，创建具有动画效果的材质</li>
</ol>
<p>顶点着色器代码：</p>
<pre class="crayon-plain-tag">&lt;script id="vertex-shader" type="x-shader/x-vertex"&gt;
    // 外部传入的时间
    uniform float time;
    varying vec2 vUv;


    void main()
    {
        // 计算变换后的位置
        vec3 posChanged = position;
        posChanged.x = posChanged.x*(abs(sin(time*1.0)));
        posChanged.y = posChanged.y*(abs(cos(time*1.0)));
        posChanged.z = posChanged.z*(abs(sin(time*1.0)));
        gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);
    }

&lt;/script&gt;</pre>
<p>为了JavaScript与着色器之间的通信，我们使用所谓uniforms。上面的例子中定义了一个uniform，传递外部的时间，根据此时间来变换顶点的位置。</p>
<p><pre class="crayon-plain-tag">gl_Position</pre> 是一个特殊变量，用于将顶点位置信息传回JavaScript。</p>
<p>其中一个片断着色器代码：</p>
<pre class="crayon-plain-tag">&lt;script id="fragment-shader-6" type="x-shader/x-fragment"&gt;


    uniform float time;
    uniform vec2 resolution;


    void main( void )
    {
        vec2 uPos = ( gl_FragCoord.xy / resolution.xy );

        uPos.x -= 1.0;
        uPos.y -= 0.5;

        vec3 color = vec3(0.0);
        float vertColor = 2.0;
        for( float i = 0.0; i &lt; 15.0; ++i )
        {
        float t = time * (0.9);

        uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1;
        float fTemp = abs(1.0 / uPos.y / 100.0);
        vertColor += fTemp;
        color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 );
        }

        vec4 color_final = vec4(color, 1.0);
        // 把颜色传递回JavaScript
        gl_FragColor = color_final;
    }

&lt;/script&gt; </pre>
<p>材质的创建，可以基于以下助手函数：</p>
<pre class="crayon-plain-tag">function createMaterial( vertexShader, fragmentShader ) {
    // 从HTML标签中读取着色器源码vertexShader、fragmentShader为脚本标签的ID
    var vertShader = document.getElementById( vertexShader ).innerHTML;
    var fragShader = document.getElementById( fragmentShader ).innerHTML;

    var attributes = {};
    // 向着色器传递变量
    var uniforms = {
        time: { type: 'f', value: 0.2 },
        scale: { type: 'f', value: 0.2 },
        alpha: { type: 'f', value: 0.6 },
        resolution: { type: "v2", value: new THREE.Vector2() }
    };

    uniforms.resolution.value.x = window.innerWidth;
    uniforms.resolution.value.y = window.innerHeight;

    // 创建一个ShaderMaterial材质
    var meshMaterial = new THREE.ShaderMaterial( {
        uniforms: uniforms,
        attributes: attributes,
        vertexShader: vertShader,
        fragmentShader: fragShader,
        transparent: true

    } );

    return meshMaterial;
}</pre>
<p>渲染循环中，我们需要改变uniform，从而导致着色器绘制结果发生变化，进而产生动画效果：</p>
<pre class="crayon-plain-tag">function render() {
    // 递增time
    cube.material.materials.forEach( function ( e ) {
        e.uniforms.time.value += 0.01;
    } );

    requestAnimationFrame( render );
    renderer.render( scene, camera );
}</pre>
<div class="blog_h2"><span class="graybg">线形几何图形的材质</span></div>
<p>有两类仅仅支持用在线条（THREE.Line）的材质。线条这种特殊的Geometry仅仅具有顶点，而没有面。</p>
<div class="blog_h3"><span class="graybg">THREE.LineBasicMaterial</span></div>
<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>color</td>
<td>线条的颜色</td>
</tr>
<tr>
<td>linewidth</td>
<td>线条的宽度</td>
</tr>
<tr>
<td>vertexColors</td>
<td>设置各个顶点的颜色为THREE.VertexColors类型。覆盖color属性</td>
</tr>
<tr>
<td>fog</td>
<td>是否受到全局迷雾的影响</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.LineDashedMaterial</span></div>
<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>scale</td>
<td>虚线条、线条间隔的缩放比例</td>
</tr>
<tr>
<td>dashSize</td>
<td>虚线条的大小</td>
</tr>
<tr>
<td>gapSize</td>
<td>线条间隔的大小</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用几何图形</span></div>
<p>Three.js内置了大量的Geometry，可以开箱即用。 本章介绍其中的二维、三维Geometry，线条类不再介绍。</p>
<div class="blog_h2"><span class="graybg">二维几何图形</span></div>
<p>二维图形的初始摆放位置是X-Y平面，但是很多情况下需要需要将它们（特别是PlaneGeometry）放置到“地面”上，也就是X-Z平面上。此时可以让它绕着X轴逆时针旋转90度：</p>
<pre class="crayon-plain-tag">mesh.rotation.x =- Math.PI/2; </pre>
<div class="blog_h3"><span class="graybg">THREE.PlaneGeometry</span></div>
<p>外观上是一个矩形。示例：</p>
<pre class="crayon-plain-tag">new THREE.PlaneGeometry(width, height, widthSegments, heightSegments);</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td> width</td>
<td style="text-align: center;">Y</td>
<td>矩形的宽度</td>
</tr>
<tr>
<td> height</td>
<td style="text-align: center;">Y </td>
<td>矩形的高度 </td>
</tr>
<tr>
<td> widthSegments</td>
<td style="text-align: center;">N </td>
<td>宽方向上分段的数量，默认1 </td>
</tr>
<tr>
<td> heightSegments</td>
<td style="text-align: center;">N </td>
<td>高方向上分段的数量，默认1 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.CircleGeometry</span></div>
<p>外观上是一个圆形或者扇形。示例：</p>
<pre class="crayon-plain-tag">// 半径为3的圆
new THREE.CircleGeometry(3, 12);
// 半径为3的半圆
new THREE.CircleGeometry(3, 12, 0, Math.PI);</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>圆的半径，默认50</td>
</tr>
<tr>
<td>segments</td>
<td style="text-align: center;">N</td>
<td>分段数，定义了构成圆的面的数量，最小值3，默认值8。更大的面数意味着更平滑的边缘</td>
</tr>
<tr>
<td>thetaStart</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制起始扇边，默认0，支持范围0 ～ 2 * PI</td>
</tr>
<tr>
<td>thetaLength</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制终止扇边，默认 2 * PI，支持范围0 ～ 2 * PI</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.RingGeometry</span></div>
<p>外观上是一个圆环或者扇环。示例：</p>
<pre class="crayon-plain-tag">Var ring = new THREE.RingGeometry();</pre>
<p>可用属性： </p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>innerRadius</td>
<td style="text-align: center;">N</td>
<td>内半径，默认0</td>
</tr>
<tr>
<td>outerRadius</td>
<td style="text-align: center;">N</td>
<td>外半径，默认50</td>
</tr>
<tr>
<td>thetaSegments</td>
<td style="text-align: center;">N</td>
<td>分段数，定义了构成环的面的数量。影响圆弧的平滑度</td>
</tr>
<tr>
<td>phiSegments</td>
<td style="text-align: center;">N</td>
<td>不影响圆环的平滑度，但是可以增加其构成面的数量</td>
</tr>
<tr>
<td>thetaStart</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制起始扇边，默认0，支持范围0 ～ 2 * PI</td>
</tr>
<tr>
<td>thetaLength</td>
<td style="text-align: center;">N</td>
<td>从什么角度绘制终止扇边，默认 2 * PI，支持范围0 ～ 2 * PI</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.ShapeGeometry</span></div>
<p>该形状允许你创建自定义的二维图形，其操作方式类似于SVG/Canvas中的画布。 示例：</p>
<pre class="crayon-plain-tag">var shape = new THREE.Shape();

// 移动画笔到指定的点
shape.moveTo( 10, 10 );

// 向Y方向画30像素的线段
shape.lineTo( 10, 40 );

// 绘制贝塞尔曲线
shape.bezierCurveTo( 15, 25, 25, 25, 30, 40 );

// 绘制拟合曲线
shape.splineThru( [
    new THREE.Vector2( 32, 30 ),
    new THREE.Vector2( 28, 20 ),
    new THREE.Vector2( 30, 10 ),
] );

// 绘制二次方曲线
shape.quadraticCurveTo( 20, 15, 10, 10 );

// 添加一个路径到形状中，挖洞
var hole1 = new THREE.Path();
hole1.absellipse( 16, 24, 2, 3, 0, Math.PI * 2, true );
shape.holes.push( hole1 );
// 再挖一个洞
var hole2 = new THREE.Path();
hole2.absellipse( 23, 24, 2, 3, 0, Math.PI * 2, true );
shape.holes.push( hole2 );
// 再一个挖洞
var hole3 = new THREE.Path();
hole3.absarc( 20, 16, 2, 0, Math.PI, true );
shape.holes.push( hole3 );

// 基于上述形状创建Geometry对象
new THREE.ShapeGeometry( shape );</pre>
<p>运行结果示意图：</p>
<p><img class="aligncenter size-full wp-image-14474" src="https://blog.gmem.cc/wp-content/uploads/2017/01/ShapeGeometry.png" alt="shapegeometry" width="406" height="431" />ShapeGeometry支持以下属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>shapes</td>
<td style="text-align: center;">Y</td>
<td>构成此Geometry的一个或者多个THREE.Shape对象，可以传入数组</td>
</tr>
<tr>
<td>options</td>
<td style="text-align: center;">N</td>
<td>
<p>应用到所有THREE.Shape的选项：</p>
<ol>
<li>curveSegments，决定了曲线的平滑程度，默认12</li>
<li>material，使用MeshFaceMaterial时，指定该形状使用的materialIndex</li>
<li>UVGenerator，当为材质指定纹理时，指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.Shape</span></div>
<p>该类型是THREE.ShapeGeometry的最重要的部分，允许你创建自定义的形状。它提供以下方法和属性：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td><strong>moveTo(x,y)  </strong>移动画笔到指定的位置</td>
</tr>
<tr>
<td><strong>lineTo(x,y)</strong>  从当前位置向(x,y)绘制直线</td>
</tr>
<tr>
<td>
<p><strong>quadraticCurveTo(aCPx, aCPy, x, y)</strong><br /><strong>bezierCurveTo(aCPx1, aCPy1, aCPx2, aCPy2, x, y)</strong></p>
<p>你可以使用两种方式绘制曲线：二次曲线、贝塞尔曲线。两种方式的不同之处在于如何指定曲线的曲率（curvature）。下图显示这两种曲线的差别：</p>
<p><img class="aligncenter size-full wp-image-14478" src="https://blog.gmem.cc/wp-content/uploads/2017/01/curve.png" alt="curve" width="430" height="154" /></p>
<p>除了曲线的两个端点以外，对于：</p>
<ol>
<li>二次曲线，你需要提供额外的一个点(aCPx, aCPy)，这个点决定了曲线的曲率</li>
<li>三次曲线（贝塞尔曲线），你需要提供额外的两个点(aCPx1, aCPy1, aCPx2, aCPy2)</li>
</ol>
<p>注意：起点都是画笔当前位置，不需要在参数中指定</p>
</td>
</tr>
<tr>
<td><strong>splineThru(pts)</strong><br />在一系列点之间绘制流线型（拟合）的曲线，参数必须是THREE.Vector2对象的数组</td>
</tr>
<tr>
<td><strong>arc(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise)</strong><br />绘制一个圆圈或者圆弧。(aX, aY)指定离开当前画笔位置的偏移量，aRadius表示半径，(aStartAngle, aEndAngle)表示起始、终止角度，aClockwise为布尔值，表示是否顺时针绘制</td>
</tr>
<tr>
<td><strong>absArc(aX, aY, aRadius, aStartAngle, aEndAngle,AClockwise)</strong><br />在绝对位置上绘制圆弧</td>
</tr>
<tr>
<td><strong>ellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise)</strong><br />绘制椭圆或者部分椭圆</td>
</tr>
<tr>
<td><strong>absellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise)</strong><br />在绝对位置上绘制椭圆或者部分椭圆</td>
</tr>
<tr>
<td><strong>fromPoints(vectors)</strong><br />根据THREE.Vector2或者THREE.Vector3数组绘制路径</td>
</tr>
<tr>
<td><strong>holes</strong><br />THREE.Shape对象的数组，表示在当前形状上挖去的洞</td>
</tr>
<tr>
<td><strong>makeGeometry(options)<br /></strong>基于此形状生成一个THREE.ShapeGeometry对象<strong><br /></strong></td>
</tr>
<tr>
<td><strong>createPointsGeometry(divisions)<br /></strong>把形状转换为一系列采样点的数组，divisions指定点的数量。你可以基于这些点生成一个线条对象：<br />
<pre class="crayon-plain-tag">new THREE.Line( 
    shape.createPointsGeometry(10), new
    THREE.LineBasicMaterial( { color: 0xff3333, linewidth: 2 } ) 
);</pre>
</td>
</tr>
<tr>
<td><strong>createSpacedPointsGeometry(divisions)</strong> <br />与上面类似，但是生成一个Path对象</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">三维几何图形</span></div>
<div class="blog_h3"><span class="graybg">THREE.BoxGeometry</span></div>
<p>这是一个非常简单的三维图形，具有长宽高的盒子：</p>
<pre class="crayon-plain-tag">new THREE.BoxGeometry(10,10,10);</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>width</td>
<td style="text-align: center;">Y</td>
<td>宽度，沿着X轴</td>
</tr>
<tr>
<td>height</td>
<td style="text-align: center;">Y</td>
<td>高度，沿着Y轴</td>
</tr>
<tr>
<td>depth</td>
<td style="text-align: center;">Y</td>
<td>长度，沿着Z轴</td>
</tr>
<tr>
<td>widthSegments</td>
<td style="text-align: center;">N</td>
<td rowspan="3">在三个方向上的分段数</td>
</tr>
<tr>
<td>heightSegments</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>depthSegments</td>
<td style="text-align: center;">N</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.SphereGeometry</span></div>
<p>基于此类型，你可以绘制三维球体、不完整球体：</p>
<p><img class="aligncenter size-full wp-image-14482" src="https://blog.gmem.cc/wp-content/uploads/2017/01/sphere.png" alt="sphere" width="676" height="430" />可以看到，你可以截取球体经度、纬度方向的任意片断。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">new THREE.SphereGeometry(radius,widthSegments,heightSegments,phiStart,phiLength,thetaStart,thetaLength) </pre>
<p>该类型提供以下属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>球体的半径，默认50</td>
</tr>
<tr>
<td>widthSegments</td>
<td style="text-align: center;">N</td>
<td>垂直方向的分段数，默认8</td>
</tr>
<tr>
<td>heightSegments</td>
<td style="text-align: center;">N</td>
<td>水平方向的分段数，默认8</td>
</tr>
<tr>
<td>phiStart</td>
<td style="text-align: center;">N</td>
<td>在经度方向上，绘制球体的起点，向东绘制。范围0 ~ 2*PI</td>
</tr>
<tr>
<td>phiLength</td>
<td style="text-align: center;">N</td>
<td>在经度方向上，绘制的长度。范围0 ~ 2*PI</td>
</tr>
<tr>
<td>thetaStart</td>
<td style="text-align: center;">N</td>
<td>在纬度方向上，绘制球体的起点，向南绘制。范围0 ~ 2*PI</td>
</tr>
<tr>
<td>thetaLength</td>
<td style="text-align: center;">N</td>
<td>在纬度方向上，绘制的长度。范围0 ~ 2*PI</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.CylinderGeometry</span></div>
<p>可以绘制圆柱、圆筒、圆锥或者截锥。代码示例：</p>
<pre class="crayon-plain-tag">new THREE.CylinderGeometry(radiusTop,radiusBottom,height,radialSegments,heightSegments,openEnded)</pre>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radiusTop</td>
<td style="text-align: center;">N</td>
<td>上半径</td>
</tr>
<tr>
<td>radiusBottom</td>
<td style="text-align: center;">N</td>
<td>下半截</td>
</tr>
<tr>
<td>height</td>
<td style="text-align: center;">N</td>
<td>高度</td>
</tr>
<tr>
<td>radialSegments</td>
<td style="text-align: center;">N</td>
<td>在上下底方向上的分段数，决定光滑度</td>
</tr>
<tr>
<td>heightSegments</td>
<td style="text-align: center;">N</td>
<td>在高度方向上的分段数</td>
</tr>
<tr>
<td>openEnded</td>
<td style="text-align: center;">N</td>
<td>是否上下底开放，默认false</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">THREE.TorusGeometry</span></div>
<p>类似于甜甜圈的圆环面。代码示例：</p>
<pre class="crayon-plain-tag">new THREE.TorusGeometry(radius, tube, radialSegments,tubularSegments,arc)</pre>
<p> 可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>外半径</td>
</tr>
<tr>
<td>tube</td>
<td style="text-align: center;">N</td>
<td>甜甜圈管道的半径</td>
</tr>
<tr>
<td>radialSegments</td>
<td style="text-align: center;">N</td>
<td rowspan="2">分段数</td>
</tr>
<tr>
<td>tubularSegments</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>arc</td>
<td style="text-align: center;">N</td>
<td>弧度，决定是不是绘制完整的甜甜圈，最大值2 * PI</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">高级图形及二进制操作</span></div>
<div class="blog_h2"><span class="graybg">THREE.ConvexGeometry</span></div>
<p>创建基于若干点的最小化凸面体。该形状不是Three.js核心库的组成部分。</p>
<div class="blog_h2"><span class="graybg">THREE.LatheGeometry</span></div>
<p>允许你基于一个光滑曲线来创建形状。此曲线由一系列的点（Knots）指定，通常是拟合曲线。曲线围绕对象的中心Z轴转动，可以产生类似于花瓶、钟之类的形状。示例：</p>
<p><img class="aligncenter size-full wp-image-14484" src="https://blog.gmem.cc/wp-content/uploads/2017/01/lathe.png" alt="lathe" width="566" height="560" /></p>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>points</td>
<td style="text-align: center;">Y</td>
<td>绘制曲线的基准点</td>
</tr>
<tr>
<td>segments</td>
<td style="text-align: center;">N</td>
<td>分段数，数字越大则形状越光滑</td>
</tr>
<tr>
<td>phiStart</td>
<td style="text-align: center;">N</td>
<td>开始弧度</td>
</tr>
<tr>
<td>phiLength</td>
<td style="text-align: center;">N</td>
<td>绘制弧长</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.ExtrudeGeometry</span></div>
<p>可以把2D图形凸起、抬高为3D图形。比如我们可以把上面章节中的ShapeGeometry抬高：</p>
<pre class="crayon-plain-tag">var options = {
    amount: 10,
    bevelThickness: 2,
    bevelSize: 1,
    bevelSegments: 3,
    bevelEnabled: true,
    curveSegments: 12,
    steps: 1
};
new THREE.ExtrudeGeometry( drawShape(), options );</pre>
<p> 运行效果图如下：</p>
<p><img class="aligncenter size-full wp-image-14485" src="https://blog.gmem.cc/wp-content/uploads/2017/01/extrude-geometry.png" alt="extrude-geometry" width="95%" /></p>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>shapes</td>
<td style="text-align: center;">Y</td>
<td>基于其进行凸起、抬高的THREE.Shape或者THREE.Shape数组</td>
</tr>
<tr>
<td>amount</td>
<td style="text-align: center;">N</td>
<td>抬高的高度，默认100</td>
</tr>
<tr>
<td>bevelThickness</td>
<td style="text-align: center;">N</td>
<td>
<p>在形状前面、后面，以及抬起的哪个侧面之间，创建一个斜坡</p>
<p>此厚度，即为圆滑斜坡给侧面“增加”的厚度的1/2</p>
</td>
</tr>
<tr>
<td>bevelSize</td>
<td style="text-align: center;">N</td>
<td>斜坡的高度，此高度导致从前/后面看形状，其面积变大</td>
</tr>
<tr>
<td>bevelSegments</td>
<td style="text-align: center;">N</td>
<td>斜坡分段数，让斜坡光滑</td>
</tr>
<tr>
<td>bevelEnabled</td>
<td style="text-align: center;">N</td>
<td>是否启用斜坡，默认启用</td>
</tr>
<tr>
<td>curveSegments</td>
<td style="text-align: center;">N</td>
<td>让曲线光滑</td>
</tr>
<tr>
<td>steps</td>
<td style="text-align: center;">N</td>
<td>凸起生成的面的分段数，默认1</td>
</tr>
<tr>
<td>extrudePath</td>
<td style="text-align: center;">N</td>
<td>沿着什么路径执行凸起，默认沿着Z轴，可以指定任意的路径</td>
</tr>
<tr>
<td>material</td>
<td style="text-align: center;">N</td>
<td>用作前后面的材质的索引，如果希望前后面使用不同材质可以调用THREE.SceneUtils.createMultiMaterialObject()</td>
</tr>
<tr>
<td>extrudeMaterial</td>
<td style="text-align: center;">N</td>
<td>凸起面和斜坡使用的材质的索引</td>
</tr>
<tr>
<td>uvGenerator</td>
<td style="text-align: center;">N</td>
<td>UVGenerator，当为材质指定纹理时，指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.TubeGeometry</span></div>
<p>与ExtrudeGeometry类似，这个类也是用于“凸起”的，只是它凸起的目标是3D的拟合曲线，而非2D图形。可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>path</td>
<td style="text-align: center;">Y</td>
<td>凸起的目标，一个THREE.SplineCurve3对象</td>
</tr>
<tr>
<td>segments</td>
<td style="text-align: center;">N</td>
<td>分段数，路径越长，该值应该越大，默认64</td>
</tr>
<tr>
<td>radius</td>
<td style="text-align: center;">N</td>
<td>管道的半径，默认1</td>
</tr>
<tr>
<td>radiusSegments</td>
<td style="text-align: center;">N</td>
<td>管道截面分段数，默认8</td>
</tr>
<tr>
<td>closed</td>
<td style="text-align: center;">N</td>
<td>是否闭合管道，默认false</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">THREE.ParametricGeometry</span></div>
<p>基于一个函数来生成几何图形。此函数有两个入参u、v，其返回值是一个三维向量，此向量作为几何图形的顶点，本质上是二维平面到三维空间的映射。</p>
<p>可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>function</td>
<td style="text-align: center;">Y</td>
<td>生成器函数，其返回值作为结果Geometry的顶点</td>
</tr>
<tr>
<td>slices</td>
<td style="text-align: center;">Y</td>
<td>u值被划分为多少子值，u的取值范围是0～1</td>
</tr>
<tr>
<td>stacks</td>
<td style="text-align: center;">Y</td>
<td>v值被划分为多少子值，v的取值范围是0～1</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre class="crayon-plain-tag">var radialWave = function ( u, v ) {
    var r = 50;

    var x = Math.sin( u ) * r;
    var z = Math.sin( v / 2 ) * 2 * r;
    var y = (Math.sin( u * 4 * Math.PI ) + Math.cos( v * 2 * Math.PI )) * 2.8;

    return new THREE.Vector3( x, y, z );
};

var mesh = createMesh( new THREE.ParametricGeometry( radialWave, 120, 120, false ) );</pre>
<p>运行效果图：</p>
<p><img class="aligncenter size-full wp-image-14498" src="https://blog.gmem.cc/wp-content/uploads/2017/01/radialWave.png" alt="radialwave" width="400" height="294" /> </p>
<div class="blog_h2"><span class="graybg">THREE.TextGeometry</span></div>
<p>该类型用于创建凸起、抬升的3D文本。可用属性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">属性</td>
<td style="width: 28px; text-align: center;">必</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>size</td>
<td style="text-align: center;">N</td>
<td>文本的尺寸，默认100</td>
</tr>
<tr>
<td>height</td>
<td style="text-align: center;">N</td>
<td>凸起的高度</td>
</tr>
<tr>
<td>weight</td>
<td style="text-align: center;">N</td>
<td>粗体设置，可选值bold、normal</td>
</tr>
<tr>
<td>font</td>
<td style="text-align: center;">N</td>
<td>字体名称，默认helvetiker</td>
</tr>
<tr>
<td>style</td>
<td style="text-align: center;">N</td>
<td>字体样式，可选值normal、italic</td>
</tr>
<tr>
<td>bevelThickness</td>
<td style="text-align: center;">N</td>
<td rowspan="4">斜坡设置，默认不启用斜坡</td>
</tr>
<tr>
<td>bevelSize</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>bevelSegments</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>bevelEnabled</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>curveSegments</td>
<td style="text-align: center;">N</td>
<td>让曲线光滑</td>
</tr>
<tr>
<td>steps</td>
<td style="text-align: center;">N</td>
<td rowspan="5">参考ExtrudeGeometry</td>
</tr>
<tr>
<td>extrudePath</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>material</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>extrudeMaterial</td>
<td style="text-align: center;">N</td>
</tr>
<tr>
<td>uvGenerator</td>
<td style="text-align: center;">N</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">二进制操作</span></div>
<p>你可以把Three.js的标准几何图形联合起来，形成复杂的新图形，这种技术叫做CSG（Constructive Solid Geometry，构造实体几何）。</p>
<p>为了支持CSG，我们需要使用到Three.js扩展<a href="https://github.com/skalnik/ThreeBSP">ThreeBSP</a>。该库提供了以下函数：</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>intersect</td>
<td>基于两个既有Geometry的空间交叉部分（intersection）来生成新的Geometry</td>
</tr>
<tr>
<td>union</td>
<td>联合两个既有Geometry的空间，生成新的Geometry </td>
</tr>
<tr>
<td>subtract</td>
<td>从一个Geometry中挖去与另外一个Geometry重叠的部分，形成新的Geometry</td>
</tr>
</tbody>
</table>
<p>注意：这三个函数都基于Mesh的绝对位置执行计算。因此，如果你对组（或者应用多重材质）进行操作，可能得到意外的结果。</p>
<p>下面的代码示例了如何对两个球体进行二进制操作：</p>
<pre class="crayon-plain-tag">// 创建BSP对象
var sphere1BSP = new ThreeBSP( sphere1 );
var sphere2BSP = new ThreeBSP( sphere2 );

var resultBSP;

switch ( controls.actionSphere ) {
    case "subtract":
        resultBSP = sphere1BSP.subtract( sphere2BSP );
        break;
    case "intersect":
        resultBSP = sphere1BSP.intersect( sphere2BSP );
        break;
    case "union":
        resultBSP = sphere1BSP.union( sphere2BSP );
        break;
    case "none": // noop;
}
// 转换为Mesh并添加到场景
result = resultBSP.toMesh();
result.geometry.computeFaceNormals();
result.geometry.computeVertexNormals();
scene.add(result);</pre>
<div class="blog_h1"><span class="graybg">粒子和点云</span></div>
<p>在前面的章节中，我们以及了解了Three.js的大部分重要组件：场景、镜头、灯光、图形、材质。本章主要研究一个重要的，但是迄今为止尚未提及的重要概念——粒子。</p>
<p>粒子（particles）某些时候也称为精灵（sprites），是场景中的小物体。这些物体很容易被大量的创建，以模拟雨、雪、烟以及其它多种有趣的特效。</p>
<p>需要注意，在较近版本的Three.js中，与粒子相关的物体的类型名称从THREE.ParticleSystem变为THREE.PointCloud。粒子本身的类型名称从THREE.Particle变为THREE.Sprite。</p>
<div class="blog_h2"><span class="graybg">理解粒子</span></div>
<p>粒子是<span style="background-color: #c0c0c0;">2D的平面，该平面总是正向面对镜头</span>。下面的代码创建了100个粒子：</p>
<pre class="crayon-plain-tag">// 粒子的材质
var material = new THREE.SpriteMaterial();
for ( var x = -5; x &lt; 5; x++ ) {
    for ( var y = -5; y &lt; 5; y++ ) {
        // 创建粒子
        var sprite = new THREE.Sprite( material );
        // 设置其位置
        sprite.position.set( x * 10, y * 10, 0 );
        // 添加到场景
        scene.add( sprite );
    }
}</pre>
<p>当你不指定任何属性的时候，粒子被渲染为白色二维小方块。 所以，上面的代码会在场景中展示10 x 10的小方块阵列。</p>
<p>粒子接受的材质类型只有：THREE.SpriteCanvasMaterial、THREE.SpriteMaterial。</p>
<p>与Three.Mesh类似，THREE.Sprite也继承自THREE.Object3D。这意味着THREE.Mesh的很多属性/方法对于粒子也是可用的，你可以使用scale属性对其缩放、使用position让其移动。</p>
<div class="blog_h2"><span class="graybg">理解点云</span></div>
<p>虽然创建并移动粒子很简单，但是如果操控的粒子数量很大，你很快就会遇到性能问题。为此，Three.js提供了THREE.PointCloud用来统一处理大量的粒子。基于PointCloud的、与上面等效的代码如下：</p>
<pre class="crayon-plain-tag">var geom = new THREE.Geometry();
// 点云材质
var material = new THREE.PointCloudMaterial( {
    size: 4,
    vertexColors: true, color: 0xffffff
} );
for ( var x = -5; x &lt; 5; x++ ) {
    for ( var y = -5; y &lt; 5; y++ ) {
        // 每个粒子是三维空间中的一个点
        var particle = new THREE.Vector3( x * 10, y * 10, 0 );
        geom.vertices.push( particle );
        geom.colors.push( new THREE.Color( Math.random() * 0x00ffff ) );
    }
}
// 点的集合，点云
var cloud = new THREE.PointCloud( geom, material );
scene.add( cloud );</pre>
<p>要创建点云，需要两个参数：</p>
<ol>
<li>材质，使用颜色或者纹理来装饰粒子</li>
<li>Geometry， 指定所有粒子的位置</li>
</ol>
<p>下面再举一个例子：创建15000个随机亮度的绿色粒子构成的点云：</p>
<pre class="crayon-plain-tag">var geom = new THREE.Geometry();
var material = new THREE.PointCloudMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    vertexColors: vertexColors,
    sizeAttenuation: sizeAttenuation,
    color: color
} );


var range = 500;
for ( var i = 0; i &lt; 15000; i++ ) {
    // 位置随机的粒子
    var particle = new THREE.Vector3( 
        Math.random() * range - range / 2, 
        Math.random() * range - range / 2, 
        Math.random() * range - range / 2 
    );
    geom.vertices.push( particle );
    var color = new THREE.Color( 0x00ff00 );
    // 以色相、饱和度、亮度的方式设置颜色。随机亮度的绿色
    color.setHSL( color.getHSL().h, color.getHSL().s, Math.random() * color.getHSL().l );
    // 顶点颜色数组
    geom.colors.push( color );

}</pre>
<p>当自动旋转点云时，你可以看到粒子满天飞舞的效果。</p>
<div class="blog_h3"><span class="graybg">THREE.PointCloudMaterial</span></div>
<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>color</td>
<td>点云（粒子系统）中所有粒子的颜色，如果vertexColors设置为true，并且设置了Geometry的colors属性，则该属性被覆盖</td>
</tr>
<tr>
<td>map</td>
<td>指定该属性，你可以为粒子设置纹理。使用该属性，你可以让粒子看起来更像真实世界中的粒子，例如雪花 </td>
</tr>
<tr>
<td>size</td>
<td>粒子的尺寸，默认1</td>
</tr>
<tr>
<td>sizeAnnutation</td>
<td>如果false，则所有粒子的大小一样。否则，其尺寸取决于粒子距离镜头的远近</td>
</tr>
<tr>
<td>vertexColors</td>
<td>默认情况下，点云中所有粒子的颜色一致，设置该属性为THREE.VertexColors则Geometry的colors属性被用来指定粒子的颜色。默认值THREE.NoColors </td>
</tr>
<tr>
<td>opacity</td>
<td>与transparent联用，设置粒子的透明度</td>
</tr>
<tr>
<td>transparent</td>
<td>默认false，如果设置为true，允许粒子具有透明度 </td>
</tr>
<tr>
<td>blending</td>
<td>渲染粒子时使用的混合模式 </td>
</tr>
<tr>
<td>fog </td>
<td>默认true，粒子是否被全局迷雾影响</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基于HTML5画布来装饰粒子</span></div>
<p>你可以使用三种方式来基于HTML画布装饰（Style）粒子：</p>
<ol>
<li>如果使用THREE.CanvasRenderer，你可以直接通过THREE.SpriteCanvasMaterial引用HTML5画布对象</li>
<li>如果使用THREE.WebGLRenderer，你需要一些额外的步骤来使用HTML5画布</li>
</ol>
<div class="blog_h3"><span class="graybg">使用THREE.CanvasRenderer</span></div>
<p>在使用该渲染器时，你可以使用THREE.SpriteCanvasMaterial，直接把画布的输出作为粒子的纹理使用。SpriteCanvasMaterial这个材质是专门为CanvasRenderer准备的，支持以下属性：</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>color</td>
<td>粒子的颜色，依据混合模式的设置，该颜色会和画布中图片进行混合</td>
</tr>
<tr>
<td>program</td>
<td>一个函数，以画布上下文作为入参。在粒子渲染时该函数被调用，函数的输出被绘制为粒子</td>
</tr>
<tr>
<td>opacity</td>
<td>粒子的透明度</td>
</tr>
<tr>
<td>transparent</td>
<td>是否允许粒子透明</td>
</tr>
<tr>
<td>blending</td>
<td>使用的混合模式</td>
</tr>
<tr>
<td>rotation</td>
<td>用于旋转画布的内容</td>
</tr>
</tbody>
</table>
<p>示例：</p>
<pre class="crayon-plain-tag">var canvasRenderer = new THREE.CanvasRenderer();
// ...

// 抽取纹理的程序
var getTexture = function ( ctx ) {

    // the body
    ctx.translate( -81, -84 );

    ctx.fillStyle = "orange";
    ctx.beginPath();
    // ...
    ctx.fill();

};

// 粒子材质
var material = new THREE.SpriteCanvasMaterial( {
        program: getTexture,
        color: 0xffffff
    }
);
// 旋转
material.rotation = Math.PI;
// 创建粒子
var range = 500;
for ( var i = 0; i &lt; 1500; i++ ) {
    var sprite = new THREE.Sprite( material );
    sprite.position.set( /* random */ );
    sprite.scale.set( 0.1, 0.1, 0.1 );
    scene.add( sprite );
}</pre>
<div class="blog_h3"><span class="graybg">使用WebGLRenderer</span></div>
<p>使用该渲染器时， 你需要手工在内存中创建画布对象，完成2D图形绘制，并返回一个纹理对象：</p>
<pre class="crayon-plain-tag">var getTexture = function () {
    var canvas = document.createElement( 'canvas' );
    canvas.width = 32;
    canvas.height = 32;

    var ctx = canvas.getContext( '2d' );
    // ...

    // 返回一个纹理对象
    var texture = new THREE.Texture( canvas );
    texture.needsUpdate = true;
    return texture;
};

var geom = new THREE.Geometry();


var material = new THREE.PointCloudMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    // 指定使用的THREE.Texture对象
    map: getTexture(),
    sizeAttenuation: sizeAttenuation,
    color: color
} );


var range = 500;
for ( var i = 0; i &lt; 5000; i++ ) {
    var particle = new THREE.Vector3( /* random */ );
    geom.vertices.push( particle );
}

cloud = new THREE.PointCloud( geom, material );</pre>
<div class="blog_h3"><span class="graybg">径向渐变的例子</span></div>
<p>下面的代码演示了如何使用Canvas绘制一个径向渐变的光球：</p>
<pre class="crayon-plain-tag">var canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;

var context = canvas.getContext('2d');
var gradient = context.createRadialGradient(
    canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2
);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
gradient.addColorStop(0.4, 'rgba(0,0,64,1)');
gradient.addColorStop(1, 'rgba(0,0,0,1)');

context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);

var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;</pre>
<p>可以使用此光球来装饰粒子，产生荧光那样的效果。 </p>
<div class="blog_h2"><span class="graybg">使用纹理装饰粒子</span></div>
<p>上一个粒子中，我们已经使用了纹理，纹理的图像从画布中抓取。</p>
<p>实际上，我们可以把任何图片作为纹理使用：</p>
<pre class="crayon-plain-tag">var texture = THREE.ImageUtils.loadTexture( "../assets/textures/particles/raindrop-3.png" );</pre>
<p>注意：作为纹理的图片，大小必须是2的N次方，必须是正方形。</p>
<div class="blog_h3"><span class="graybg">下雨的例子</span></div>
<p>下面使用该纹理模拟下雨效果：</p>
<pre class="crayon-plain-tag">var geom = new THREE.Geometry();

var material = new THREE.ParticleBasicMaterial( {
    size: size,
    transparent: transparent,
    opacity: opacity,
    map: texture,
    // 设置混合模式为相加，意味着雨滴图片中黑色背景部分（000000）不会被绘制——背景色 + 0 仍然是背景色
    // 使用透明背景的纹理是不支持的
    blending: THREE.AdditiveBlending,
    sizeAttenuation: sizeAttenuation,
    color: color
} );


var range = 40;
for ( var i = 0; i &lt; 1500; i++ ) {
    var particle = new THREE.Vector3(
        Math.random() * range - range / 2,
        Math.random() * range * 1.5,
        Math.random() * range - range / 2 
    );
    // 设置随机的速度属性，备用
    particle.velocityY = 0.1 + Math.random() / 5;
    particle.velocityX = (Math.random() - 0.5) / 3;
    geom.vertices.push( particle );
}

cloud = new THREE.ParticleSystem( geom, material );
cloud.sortParticles = true;

scene.add( cloud );


function render() {
    scene.children.forEach( function ( child ) {
        if ( child instanceof THREE.PointCloud ) {
            var vertices = child.geometry.vertices;
            // 在渲染循环中遍历处理所有粒子，根据速度设置其位置
            vertices.forEach( function ( v ) {
                v.y = v.y - (v.velocityY);
                v.x = v.x - (v.velocityX);
                // 如果粒子超出显示范围，则重置其位置
                if ( v.y &lt;= 0 ) v.y = 60;
                if ( v.x &lt;= -20 || v.x &gt;= 20 ) v.velocityX = v.velocityX * -1;
            } );
        }
    } );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h3"><span class="graybg">下雪的例子</span></div>
<p>我们改进一下上面的例子，模拟更真实的下雪效果：</p>
<ol>
<li>建立多个点云，来模拟不同大小的雪花</li>
<li>在Z轴方向改变雪花的位置，模拟三维空间中雪花的飘舞</li>
</ol>
<p>代码如下：</p>
<pre class="crayon-plain-tag">function createPointCloud( name, texture, size, transparent, opacity, sizeAttenuation, color ) {
    var geom = new THREE.Geometry();

    var color = new THREE.Color( color );
    // 随机的改变亮度
    color.setHSL( color.getHSL().h, color.getHSL().s, (Math.random()) * color.getHSL().l );

    var material = new THREE.PointCloudMaterial( {
        size: size,
        transparent: transparent,
        opacity: opacity,
        map: texture,
        blending: THREE.AdditiveBlending,
        // 设置为false，表示对象不影响WebGL的depth buffer，这样不同的粒子系统就不会相互干扰
        depthWrite: false,
        sizeAttenuation: sizeAttenuation,
        color: color
    } );

    var range = 40;
    for ( var i = 0; i &lt; 50; i++ ) {
        var particle = new THREE.Vector3(
            Math.random() * range - range / 2,
            Math.random() * range * 1.5,
            Math.random() * range - range / 2 );
        // 雪花在三个轴的方向上都具有速度
        particle.velocityY = 0.1 + Math.random() / 5;
        particle.velocityX = (Math.random() - 0.5) / 3;
        particle.velocityZ = (Math.random() - 0.5) / 3;
        geom.vertices.push( particle );
    }

    var system = new THREE.PointCloud( geom, material );
    system.name = name;
    system.sortParticles = true;
    return system;
}
// 创建多个粒子系统
function createPointClouds( size, transparent, opacity, sizeAttenuation, color ) {

    var texture1 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake1.png" );
    var texture2 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake2.png" );
    var texture3 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake3.png" );
    var texture4 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake5.png" );

    scene.add( createPointCloud( "system1", texture1, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system2", texture2, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system3", texture3, size, transparent, opacity, sizeAttenuation, color ) );
    scene.add( createPointCloud( "system4", texture4, size, transparent, opacity, sizeAttenuation, color ) );
}

createPointClouds( controls.size, controls.transparent, controls.opacity, controls.sizeAttenuation, controls.color );

function render() {

    scene.children.forEach( function ( child ) {
        if ( child instanceof THREE.PointCloud ) {
            var vertices = child.geometry.vertices;
            vertices.forEach( function ( v ) {
                // 模拟三维飘落效果
                v.y = v.y - (v.velocityY);
                v.x = v.x - (v.velocityX);
                v.z = v.z - (v.velocityZ);

                if ( v.y &lt;= 0 ) v.y = 60;
                if ( v.x &lt;= -20 || v.x &gt;= 20 ) v.velocityX = v.velocityX * -1;
                if ( v.z &lt;= -20 || v.z &gt;= 20 ) v.velocityZ = v.velocityZ * -1;
            } );
        }
    } );

    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h2"><span class="graybg">Sprite Map</span></div>
<p>我们可以把多个Sprite放在一个图片中，然后通过偏移量来加载、使用。就像CSS Sprite那样：</p>
<pre class="crayon-plain-tag">var spriteMaterial = new THREE.SpriteMaterial({
        opacity: opacity,
        color: color,
        transparent: transparent,
        map: getTexture() // 这个纹理中包含五个横向排列的小图片，我们只需要其中一个
    }
);
// 纹理图片中，X轴（u）\Y轴（v）方向的偏移量
// 如果spriteNumber取2，表示需要第三个小图片，u-offset = 0.2 * 2 = 0.4，即u偏移为0.4
// 注意u、v的最大值都是1，表示整个图片的大小，因此每个图片的大小是0.2
spriteMaterial.map.offset = new THREE.Vector2(0.2 * spriteNumber, 0);
// 如果不设置repeat，那么3、4、5几个小图都称为Sprite的一部分。而我们只需要第三个图片
// 1/5表示在u方向仅需要1/5长度，恰好是一个小图的大小
spriteMaterial.map.repeat = new THREE.Vector2(1 / 5, 1);
spriteMaterial.depthTest = false;

spriteMaterial.blending = THREE.AdditiveBlending;

var sprite = new THREE.Sprite(spriteMaterial);</pre>
<div class="blog_h2"><span class="graybg">从高级形状创建点云</span></div>
<p>点云基于你所提供的Geometry的顶点来渲染粒子。这意味着我们可以向它传递前面所学过的任何几何图形。</p>
<div class="blog_h1"><span class="graybg">创建、加载高级Mesh和Geometry</span></div>
<p>前面的章节我们了解到，可以通过ThreeBSP这个插件来创建复合的Mesh。本章我们将学习另外两种创建高级Geometry/Mesh的机制：</p>
<ol>
<li>分组、合并：Three.js支持内置的分组/合并机制，允许基于现存的对象来创建Mesh/Geometry</li>
<li>加载模型：Three.js支持从多种外部格式来加载Mesh/Geometry</li>
</ol>
<div class="blog_h2"><span class="graybg">分组多个Mesh</span></div>
<p>这个机制我们已经使用过，当为Geometry应用多个材质时，实际上Three.js就创建了组。</p>
<p>创建组非常容易，任何Mesh都可以包含子元素，你可以通过<pre class="crayon-plain-tag">add()</pre> 方法随时添加子元素：</p>
<pre class="crayon-plain-tag">sphere = createMesh(new THREE.SphereGeometry(5, 10, 10));
cube = createMesh(new THREE.BoxGeometry(6, 6, 6));

// 任何3D对象可以作为组的容器，Object3D是Mesh、Scene的超类
group = new THREE.Object3D();   // 最近版本的Three.js引入THREE.Group，专门用作组容器
// 向容器中添加其它Mesh
group.add(sphere);
group.add(cube);

scene.add(group); </pre>
<p>当你针对组中的父对象进行移动、缩放、旋转等操作时，所有子对象将会被应用相同的操作。需要强调的是旋转操作，执行旋转的时候，是整个组围绕组的中心进行旋转，而不是每个元素绕着各自的中心旋转。</p>
<p>使用组时，你依然可以对单个元素进行移动、缩放、旋转等操作。但是需要注意，这些操作都是相对于父对象进行的。</p>
<div class="blog_h2"><span class="graybg">合并多个Geometry</span></div>
<p>大部分情况下，使用分组可以让你方便的操控大量的Mesh。但是性能问题也可能出现，因为使用分组时，每个对象依然需要被单独的处理、渲染。</p>
<p>使用<pre class="crayon-plain-tag">THREE.Geometry.merge()</pre> 你可以合并多个几何图形，然后创建单个Mesh：</p>
<pre class="crayon-plain-tag">var geometry = new THREE.Geometry();
for ( var i = 0; i &lt; controls.numberOfObjects; i++ ) {
    var cubeMesh = createCube();
    cubeMesh.updateMatrix();
    // 提供被合并geometry的转换矩阵，确保geometry被正确的置位、旋转
    geometry.merge( cubeMesh.geometry, cubeMesh.matrix );
}
scene.add( new THREE.Mesh( geometry, cubeMaterial ) );</pre>
<div class="blog_h2"><span class="graybg">加载外部模型</span></div>
<p>使用编程方式来模拟真实世界中复杂的形状是困难的，Three.js允许加载3D建模软件设计的Geometry/Mesh。</p>
<div class="blog_h3"><span class="graybg">加载器</span></div>
<p>加载外部模型，是通过Three.js加载器（Loader）实现的。加载器把文本/二进制的模型文件转化为Three.js对象结构。</p>
<p>每个加载器理解某种特定的文件格式。</p>
<div class="blog_h3"><span class="graybg">支持的格式</span></div>
<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>JSON</td>
<td>
<p>Three.js自定义的、基于JSON的格式。可以声明式的定义一个Geometry或者Scene</p>
<p>利用该格式，你可以方便的重用复杂的Geometry或Scene</p>
</td>
</tr>
<tr>
<td>OBJ / MTL</td>
<td>
<p>OBJ是Wavefront开发的一种简单3D格式，此格式被广泛的支持，用于定义Geometry</p>
<p>MTL用于配合OBJ，它指定OBJ使用的材质</p>
<p>Three.js提供了OBJExporter.js，使用它可以把Three.js模型导出为OBJ格式</p>
</td>
</tr>
<tr>
<td>Collada</td>
<td>基于XML的格式，被大量3D应用程序、渲染引擎支持</td>
</tr>
<tr>
<td>STL</td>
<td>
<p>STereoLithography的简写，在快速原型领域被广泛使用。3D打印模型通常使用该格式定义</p>
<p>Three.js提供了STLExporter.js，使用它可以把Three.js模型导出为STL格式</p>
</td>
</tr>
<tr>
<td>CTM</td>
<td>openCTM定义的格式，以紧凑的格式存储基于三角形的Mesh</td>
</tr>
<tr>
<td>VTK</td>
<td>Visualization Toolkit定义的格式，用于声明顶点和面。此格式有二进制/ASCII两种变体，Three.js仅支持ASCII变体</td>
</tr>
<tr>
<td>AWD</td>
<td>3D场景的二进制格式，主要被away3d引擎使用，Three.js不支持AWD压缩格式</td>
</tr>
<tr>
<td>Assimp</td>
<td>开放资产导入库（Open asset import library）是导入多种3D模型的标准方式。使用该Loader你可以导入多种多样的3D模型格式</td>
</tr>
<tr>
<td>VRML</td>
<td>
<p>虚拟现实建模语言（Virtual Reality Modeling Language）是一种基于文本的格式，现已经被X3D格式取代</p>
<p>尽管Three.js不直接支持X3D，但是后者很容易被转换为其它格式</p>
</td>
</tr>
<tr>
<td>Babylon</td>
<td>
<p>游戏引擎Babylon的私有格式</p>
</td>
</tr>
<tr>
<td>PLY</td>
<td>
<p>常用于存储来自3D扫描仪的信息</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">保存/加载JSON格式</span></div>
<p>使用Three.js的JSON格式，你可以保存/加载一个Mesh或者整个场景。</p>
<div class="blog_h3"><span class="graybg">保存/加载Mesh</span></div>
<pre class="crayon-plain-tag">knot = createMesh(new THREE.TorusKnotGeometry());
scene.add(knot);

// 保存
var result = knot.toJSON();
localStorage.setItem("json", JSON.stringify(result));

// 加载
var json = localStorage.getItem("json");
if (json) {
    var loadedGeometry = JSON.parse(json);
    var loader = new THREE.ObjectLoader();
    // 将JSON解析为Mesh
    loadedMesh = loader.parse(loadedGeometry);
    loadedMesh.position.x -= 50;
    scene.add(loadedMesh);
}</pre>
<div class="blog_h3"><span class="graybg">保存/加载场景</span></div>
<p>首先引入必要的脚本：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/SceneLoader.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="../libs/SceneExporter.js"&gt;&lt;/script&gt;</pre>
<p>代码示例：</p>
<pre class="crayon-plain-tag">var exporter = new THREE.SceneExporter();
var sceneJson = JSON.stringify(exporter.parse(scene));
localStorage.setItem('scene', sceneJson);


var json = (localStorage.getItem('scene'));
var sceneLoader = new THREE.SceneLoader();
// 最后一个参数 . 定义了相对URL，加载纹理时需要
sceneLoader.parse(JSON.parse(json), function(e) {
    scene = e.scene;
}, '.');</pre>
<div class="blog_h2"><span class="graybg">与Blender一起使用</span></div>
<p>市场上有大量3D建模软件，用来设计复杂的Mesh。开源领域比较流行的是Blender。</p>
<p>Three.js提供了针对Blender、Maya、3D Studio Max等流行软件的Exporter，可以把基于这些软件设计的模型直接导出为Three.js的JSON格式。 </p>
<p>Exporter并非Three.js支持Blender的唯一途径，因为Three.js本身理解多种3D格式，而Blender也支持保存为这些格式。</p>
<div class="blog_h3"><span class="graybg">安装Blender加载项</span></div>
<ol>
<li>复制/home/alex/JavaScript/three.js/utils/exporters/blender/addons目录到/home/alex/Applications/blender/2.78/scripts/addons</li>
<li>打开Blender，点击菜单栏File ⇨ User Preferences，选择Addons选项卡，搜索Three.js，勾选以启用：<a href="https://blog.gmem.cc/wp-content/uploads/2017/01/Blender-User-Preferences_001.png"><img class="aligncenter size-full wp-image-14519" src="https://blog.gmem.cc/wp-content/uploads/2017/01/Blender-User-Preferences_001.png" alt="blender-user-preferences_001" width="95%" /></a></li>
<li>点击File ⇨ Export，在弹出的菜单中应该可以看到Three.js项</li>
</ol>
<div class="blog_h3"><span class="graybg">从Blender导出</span></div>
<p>点击File ⇨ Open，你可以打开既有模型文件并编辑。点击File ⇨ Export ⇨ Three.js(json)，选择目标路径即可导出。</p>
<p>在导出对话框中，可以修改设置：</p>
<p><img class="aligncenter size-full wp-image-14522" src="https://blog.gmem.cc/wp-content/uploads/2017/01/BlenderExportSettings.png" alt="blenderexportsettings" width="369" height="98" /></p>
<p>这样，导出的JSON会包含材质的声明，并且模型使用的纹理自动导出为图片。</p>
<div class="blog_h3"><span class="graybg">导入到Three.js场景</span></div>
<pre class="crayon-plain-tag">var loader = new THREE.JSONLoader();
// 设置纹理的加载路径，JSON中仅仅包含纹理图片文件的名称，不包括目录前缀
loader.setTexturePath( './assets/' ); // 注意结尾的 /
// 异步的加载操作，回调函数提供两个入参：加载的Geometry、加载到的材质的数组
loader.load( './assets/chair.json', function ( geometry, materials ) {
    // 回调参数是THREE.Geometry，THREE.Material[]
    var material = new THREE.MultiMaterial( materials );
    var mesh = new THREE.Mesh( geometry, material );
    // 放大以便看清
    mesh.scale.x = 15;
    mesh.scale.y = 15;
    mesh.scale.z = 15;
    scene.add( mesh );

} ); </pre>
<div class="blog_h2"><span class="graybg">加载OBJ/MTL格式</span></div>
<p>此格式被Blender原生支持、Three.js也提供了相应的加载器。</p>
<p>首先引入必要的脚本：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/OBJLoader.js"&gt;&lt;/script&gt;
&lt;!-- 下面两个用于加载MTL --&gt;
&lt;script type="text/javascript" src="../libs/MTLLoader.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="../libs/OBJMTLLoader.js"&gt;&lt;/script&gt;</pre>
<div class="blog_h3"><span class="graybg">仅加载OBJ</span></div>
<pre class="crayon-plain-tag">var loader = new THREE.OBJLoader();
loader.load( '../assets/models/pinecone.obj', function ( loadedMesh ) {
    // 回调参数是一个THREE.Object3D对象
    var material = new THREE.MeshLambertMaterial( { color: 0x5C3A21 } );
    loadedMesh.children.forEach( function ( child ) {
        child.material = material;
        child.geometry.computeFaceNormals();
        child.geometry.computeVertexNormals();
    } );

    scene.add( loadedMesh );
} );</pre>
<p>一个好的实践是，在回调中打印加载对象的结构。通常，加载的Geometry/Mesh表现为层次化的Group。理解此Group的结构，以便正确的应用材质，并执行额外的处理步骤。</p>
<p>此外，注意查看顶点的位置信息，然后估算是否需要进行缩放、如何放置镜头。</p>
<p>对Geometry调用computeFaceNormals、computeVertexNormals，以确保材质被正确的渲染。</p>
<div class="blog_h3"><span class="graybg">同时加载MTL</span></div>
<p>如果你需要通过OBJ/MTL来加载模型，首先检查MTL的内容，确保它以相对路径来引用纹理图片。</p>
<p>下面的例子加载一个蝴蝶模型，需要注意，某些时候需要对材质进行微调：</p>
<pre class="crayon-plain-tag">var loader = new THREE.OBJMTLLoader();

loader.load( '../assets/models/butterfly.obj', '../assets/models/butterfly.mtl', function ( object ) {
    // 回调参数是一个THREE.Group对象

    var wing2 = object.children[ 5 ].children[ 0 ];
    var wing1 = object.children[ 4 ].children[ 0 ];

    // 模型源文件中，蝴蝶翅膀的透明度设置有误导致看不见，这里手工调整一下材质
    wing1.material.opacity = 0.6;
    wing1.material.transparent = true;
    // 禁用深度测试，避免渲染错误（不指定下面的代码来运行示例，可以看到翅膀中部分像素不停抖动）
    wing1.material.depthTest = false;
    // 默认情况下，Three.js仅仅会渲染一个面
    wing1.material.side = THREE.DoubleSide;

    wing2.material.opacity = 0.6;
    wing2.material.depthTest = false;
    wing2.material.transparent = true;
    wing2.material.side = THREE.DoubleSide;

    object.scale.set( 140, 140, 140 );
    mesh = object;
    scene.add( mesh );

    object.rotation.x = 0.2;
    object.rotation.y = -1.3;
} );</pre>
<div class="blog_h2"><span class="graybg">加载Collada格式</span></div>
<p>此格式的默认扩展名为.dae，也被广泛的使用。此格式用来定义场景、模型，甚至是动画。一个Collada模型同时包含了Geometry、材质的定义。</p>
<p>不意外的，要加载Collada格式同样需要引入Loader脚本：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/ColladaLoader.js"&gt;&lt;/script&gt;</pre>
<p>下面的代码，从Collada模型中导入一个卡车模型：</p>
<pre class="crayon-plain-tag">var loader = new THREE.ColladaLoader();

var mesh;
loader.load( "../assets/models/dae/Truck_dae.dae", function ( result ) {
    // 从模型场景中克隆出一个对象
    mesh = result.scene.children[ 0 ].children[ 0 ].clone();
    mesh.scale.set( 4, 4, 4 );
    // 添加到当前场景中
    scene.add( mesh );
} );</pre>
<p>需要注意的是，Collada加载器回调参数是如下结构：</p>
<pre class="crayon-plain-tag">var result = {
    scene: scene,  // 场景对象，THREE.Scene，包括所有模型对象，都在其中
    morphs: morphs,
    skins: skins,
    animations: animData,
    dae: {}
};</pre>
<p>本章仅关心scene属性中的对象。需要注意，纹理可能基于WebGL不支持的格式（例如.tga），你可能需要将其转换为.png格式，并编辑Collada文件。</p>
<div class="blog_h1"><span class="graybg">动画和镜头控制</span></div>
<div class="blog_h2"><span class="graybg">基本动画</span></div>
<p>我们之前的动画，都是基于渲染循环来实现——通知Three.js尽快的重新渲染。实现代码都是如下的模式：</p>
<pre class="crayon-plain-tag">render();
function render() {
    // 在此，可以修改模型属性
    /* ... */
    // 执行渲染
    renderer.render( scene, camera );
    // 调度下依次渲染
    requestAnimationFrame( render );
}</pre>
<p>我们只需要手工触发一次render()调用，之后它就会被定期（通常是每秒60次）递归调用了。 </p>
<p>基于这种方式，我们可以修改模型的各种属性——otation,scale, position, material, vertices, faces从而产生简单的动画效果。</p>
<div class="blog_h2"><span class="graybg">选择对象</span></div>
<p>尽管用鼠标选择对象和动画没有直接关系，但是为了深入理解镜头和动画，我们需要用到这一功能。</p>
<p>Three.js没有直接提供“点击”功能，但是我们可以基于THREE.Projector、THREE.Raycaster来判断鼠标当前对应到哪个物体：</p>
<pre class="crayon-plain-tag">document.addEventListener( 'mousedown', onDocumentMouseDown, false );

var projector = new THREE.Projector();

function onDocumentMouseDown( event ) {
    // 基于鼠标当前位置，创建一个3D向量
    var x = ( event.clientX / window.innerWidth ) * 2 - 1;
    var y = -( event.clientY / window.innerHeight ) * 2 + 1;
    var z = 0.5;
    var vector = new THREE.Vector3( x, y, z );
    // 把鼠标当前位置转换为Three.js场景中的坐标 —— 把2D屏幕坐标unproject为3D世界坐标
    vector = vector.unproject( camera );
    // 从相机所在位置发出一条射线，射到鼠标位置
    var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
    // 检查此射线穿过哪些物体
    var intersects = raycaster.intersectObjects( [ sphere, cylinder, cube ] );

    if ( intersects.length &gt; 0 ) {

        console.log( intersects[ 0 ] );

        intersects[ 0 ].object.material.transparent = true;
        intersects[ 0 ].object.material.opacity = 0.1;
    }
}</pre>
<div class="blog_h2"><span class="graybg">基于Tween.js的动画</span></div>
<p><a href="https://github.com/tweenjs/tween.js/blob/master/docs/user_guide.md">Tween.js</a>是一个简单的JS库，可以基于给定的初值、终值自动计算所有中间值。这个中间值计算过程一般叫做tweening。示例代码：</p>
<pre class="crayon-plain-tag">var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween( coords )
    .to( { x: 100, y: 100 }, 1000 ) // 在1秒内完成变换
    .onUpdate( function () {  // 每当值变化时，执行的回调
        console.log( this.x, this.y );
    } )
    .start();

requestAnimationFrame( animate );
// requestAnimationFrame会自动把一个高精度的、从DOM加载到当前流逝的时间传递给回调
function animate( time ) {
    requestAnimationFrame( animate );
    TWEEN.update( time );
}</pre>
<p>我们可以创建一个改变物体位置的循环动画：</p>
<pre class="crayon-plain-tag">var posSrc = { pos: 1 };
// 创建两个Tween并链接，形成循环动画
var tween = new TWEEN.Tween( posSrc ).to( { pos: 0 }, 5000 );
tween.easing( TWEEN.Easing.Sinusoidal.InOut );

var tweenBack = new TWEEN.Tween( posSrc ).to( { pos: 1 }, 5000 );
tweenBack.easing( TWEEN.Easing.Sinusoidal.InOut );

tween.chain( tweenBack );
tweenBack.chain( tween );
// 当值变化时，改变物体的顶点位置
var onUpdate = function () {
    var count = 0;
    var pos = this.pos;

    loadedGeometry.vertices.forEach( function ( e ) {
        var newY = ((e.y + 3.22544) * pos) - 3.22544;
        pointCloud.geometry.vertices[ count++ ].set( e.x, newY, e.z );
    } );

    pointCloud.sortParticles = true;
};

tween.onUpdate( onUpdate );
tweenBack.onUpdate( onUpdate );

function render() {
    TWEEN.update();
    // 以16.7次/秒的频率，更新Tween，然后重渲染场景
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h2"><span class="graybg">镜头控制</span></div>
<div class="blog_h3"><span class="graybg">TrackballControls</span></div>
<p>跟踪球控制，允许你使用鼠标进行镜头的平移（鼠标左键）、缩放（鼠标中键）、旋转操作（鼠标右键）。</p>
<p>使用该控制方式，需要引入：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/TrackballControls.js"&gt;&lt;/script&gt;</pre>
<p>然后，创建控制器对象：</p>
<pre class="crayon-plain-tag">// 关联到镜头
var trackballControls = new THREE.TrackballControls( camera );
// 设置速度
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;


function render() {
    // 获取上一次调用getDelta到现在流逝的时间
    var delta = clock.getDelta();
    // 传递此时间增量，控制器会根据移动速度来计算距离
    trackballControls.update( delta );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera )
}</pre>
<div class="blog_h3"><span class="graybg">FlyControls</span></div>
<p>飞行控制，好像你在驾驶一架飞机，在场景中穿梭。</p>
<p>使用该控制方式，需要引入：</p>
<pre class="crayon-plain-tag">&lt;script type="text/javascript" src="../libs/FlyControls.js"&gt;&lt;/script&gt;</pre>
<p>示例代码：</p>
<pre class="crayon-plain-tag">var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
// 需要指向渲染场景的DOM元素
flyControls.domElement = document.querySelector('#WebGL');
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;</pre>
<div class="blog_h3"><span class="graybg">FirstPersonControls</span></div>
<p>第一人称视角。示例代码：</p>
<pre class="crayon-plain-tag">var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.1;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
// 场景最初渲染时，镜头的位置
camControls.lon = -150;
camControls.lat = 120; </pre>
<div class="blog_h3"><span class="graybg">PointerLockControls</span></div>
<p>与上一个类似，但是提供鼠标锁定功能。避免镜头一直移动导致晃眼。具体查看<a href="https://threejs.org/examples/misc_controls_pointerlock.html">这个示例</a>。</p>
<div class="blog_h3"><span class="graybg">OrbitControl</span></div>
<p>这种方式可以很方便的旋转、平移、缩放位于场景中心位置的物体。例如太空场景中的星球。示例代码：</p>
<pre class="crayon-plain-tag">var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;
var clock = new THREE.Clock();
...
var delta = clock.getDelta();
orbitControls.update(delta);</pre>
<div class="blog_h2"><span class="graybg">变形与骨骼动画</span></div>
<p>当利用3D建模软件创建动画时，通常有两种机制—— 变形目标、骨骼动画。</p>
<div class="blog_h3"><span class="graybg">Morph targets</span></div>
<p>使用变形目标（Morph targets），你可以定义模型的变形（deformed）版本——Mesh的一个关键位置（key position）。对于此变形版本，所有顶点的位置被记录下来。根据原始版本、变形版本的顶点位置的变化，可以方便的创建变化。其本质就是移动顶点的位置。</p>
<p>变形目标是定义动画最直接的方式，其缺点是对于大的Mesh和大的动画，模型文件会边的庞大。</p>
<p>Three.js支持手工的从一个关键位置移动到另一个，但是手工控制比较麻烦，你需要跟踪当前位置、需要变形到的目标位置。THREE.MorphAnimMesh把这些细节封装起来，我们通常直接使用该类。下面的代码示例了如何加载内置了变形目标的模型：</p>
<pre class="crayon-plain-tag">var loader = new THREE.JSONLoader();
loader.load( '../assets/models/horse.js', function ( geometry, mat ) {

    var mat = new THREE.MeshLambertMaterial({
        morphTargets: true, // 一定要设置材质的morphTargets为true，否则不支持动画
        vertexColors: THREE.FaceColors
    } );

    geometry.computeVertexNormals();
    geometry.computeFaceNormals();
    // 在创建MorphAnimMesh之前，要调用computeMorphNormals确保变形目标的所有法向量被正确的计算
    // 此操作对于正确的灯光、阴影效果是必须的
    geometry.computeMorphNormals();
    if ( geometry.morphColors &amp;&amp; geometry.morphColors.length ) {
        // 你可以为某个特定的变形目标的面定制颜色
        var colorMap = geometry.morphColors[ 0 ];
        for ( var i = 0; i &lt; colorMap.colors.length; i++ ) {
            geometry.faces[ i ].color = colorMap.colors[ i ];
            geometry.faces[ i ].color.offsetHSL( 0, 0.3, 0 );
        }
    }
    meshAnim = new THREE.MorphAnimMesh( geometry, mat );
    meshAnim.duration = 1000;
    meshAnim.position.x = 200;
    meshAnim.position.z = 0;
    
    scene.add( meshAnim );
}, '../assets/models' );

function render() {
    var delta = clock.getDelta();
    webGLRenderer.clear();
    // 推进动画
    meshAnim.updateAnimation( delta * 1000 );
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<p>Three.js的默认行为是一下子运行所有动画，如果为单个Geometry定义了多个动画，则可以通过<pre class="crayon-plain-tag">parseAnimations()</pre> 和<pre class="crayon-plain-tag">playAnimation(name,fps)</pre> 来运行其中一个动画。</p>
<div class="blog_h3"><span class="graybg">Skeletal animation</span></div>
<p>这种方式允许你为模型定义骨骼，并且把顶点附着在骨骼上。当你移动骨骼的时候，所有相连的骨骼也跟随移动，并导致顶点移动，产生变形。</p>
<p>变形动画比较简单，Three.js只需要转换顶点位置就可以了。骨骼动画则要复杂一些，当你移动骨骼时，Three.js需要知道如何计算附着其上的皮肤（Mesh顶点）的位置。</p>
<p>下面这个例子是手工执行骨骼动画的代码：</p>
<pre class="crayon-plain-tag">var clock = new THREE.Clock();

var loader = new THREE.JSONLoader();
loader.load( '../assets/models/hand-1.js', function ( geometry, mat ) {
    // 注意设置skinning为true，否则不会看到任何骨骼移动的效果
    var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } );
    // 专门针对骨骼/皮肤几何图形的Mesh
    mesh = new THREE.SkinnedMesh( geometry, mat );
    scene.add( mesh );
    // 开始动画
    tween.start();

}, '../assets/models' );


var onUpdate = function () {
    var pos = this.pos;
    // 转动手指
    mesh.skeleton.bones[ 5 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 6 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 10 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 11 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 15 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 16 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 20 ].rotation.set( 0, 0, pos );
    mesh.skeleton.bones[ 21 ].rotation.set( 0, 0, pos );
    // 转动手腕
    mesh.skeleton.bones[ 1 ].rotation.set( pos, 0, 0 );
};
var tween = new TWEEN.Tween( { pos: -1 } )
    .to( { pos: 0 }, 3000 )
    .easing( TWEEN.Easing.Cubic.InOut )
    .yoyo( true ) // 下一次反向执行
    .repeat( Infinity ) // 无限执行
    .onUpdate( onUpdate );

render();

function render() {
    TWEEN.update();
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
} </pre>
<div class="blog_h2"><span class="graybg">从外部模型创建动画</span></div>
<p>前面我们讨论过，Three.js支持多种外部模型格式。这些格式中的一部分，支持动画：</p>
<ol>
<li>对于JSON格式，可以使用Blender with the JSON exporter导出</li>
<li>Collada，该格式支持动画</li>
</ol>
<div class="blog_h3"><span class="graybg">导入Blender骨骼动画</span></div>
<p>基于Blender创建骨骼动画时，要注意以下几点：</p>
<ol>
<li>模型的所有顶点，至少分配到一个顶点组（vertex group）中</li>
<li>顶点组的名称必须和控制它的骨骼的名称一致，这样Three.js才知道，移动骨骼时，需要修改哪些顶点</li>
<li>注意仅仅第一个Action被导出，因此要确保你需要导出的动画时第一个</li>
<li>创建关键帧时，最好选取所有骨头，即使某些不变化</li>
<li>导出模型时，需要保证模型处于rest pose，否则动画可能变形严重</li>
<li>导出时，注意勾选：Vertices、Faces、Normals、Skinning、UVs、Colors、Materials、Flip YZ、Skeletal animation</li>
</ol>
<p>这样，骨骼的移动路径会被一同导出，在Three.js中可以简单的进行回放：</p>
<pre class="crayon-plain-tag">var loader = new THREE.JSONLoader();
loader.load( '../assets/models/hand-2.js', function ( model, mat ) {

    var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } );
    mesh = new THREE.SkinnedMesh( model, mat );

    var animation = new THREE.Animation( mesh, model.animation );

    mesh.rotation.x = 0.5 * Math.PI;
    mesh.rotation.z = 0.7 * Math.PI;
    scene.add( mesh );
    // 此助手用于查看骨骼如何变化
    helper = new THREE.SkeletonHelper( mesh );
    helper.material.linewidth = 2;
    helper.visible = false;
    scene.add( helper );

    // 开始播放动画
    animation.play();

}, '../assets/models' );

render();

function render() {
    var delta = clock.getDelta();
    if ( mesh ) {
        helper.update();
        THREE.AnimationHandler.update( delta );
    }
    requestAnimationFrame( render );
    webGLRenderer.render( scene, camera );
}</pre>
<div class="blog_h3"><span class="graybg">导入Collada动画</span></div>
<p>和从JSON格式导入动画的方式差不多，但是要注意Collada可以存储整个场景，包括镜头、灯光、动画，因此你需要找到需要使用的那个附带动画的Mesh：</p>
<pre class="crayon-plain-tag">var child = collada.skins[0];  // THREE.SkinnedMesh
scene.add(child);
var animation = new THREE.Animation(child, child.geometry.animation);
animation.play(); </pre>
<div class="blog_h1"><span class="graybg">使用纹理</span></div>
<p>纹理在Three.js中有多种不同的使用方式，你可以用纹理来定义Mesh的颜色，或者定义发光效果、凹凸（bump）、反射效果。</p>
<div class="blog_h2"><span class="graybg">在材质中使用纹理</span></div>
<p>最基本的用例是加载纹理，并将其作为材质上的一个map。当你基于此材质创建Mesh时，Mesh被纹理着色：</p>
<pre class="crayon-plain-tag">var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile)
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var mesh = new THREE.Mesh(geom, mat);
return mesh;</pre>
<p>作为纹理的图片，可以是PNG、GIF或者JPG格式，且大小必须是2的N次方。 需要注意，纹理图片的加载是异步的，如果你希望纹理加载完毕之后再进行渲染，可以：</p>
<pre class="crayon-plain-tag">texture = THREE.ImageUtils.loadTexture('texture.png', {},function() { renderer.render(scene); });</pre>
<p>由于纹理图片的像素一般不能和面的像素一一对应，纹理需要放大或者缩小后使用。WebGL/Three.js提供了几个不同的选项。你可以设置纹理的magFilter、minFilter属性，来声明它将如何被缩放 。两个基本的取值为：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%;">THREE.NearestFilter</td>
<td>使用最临近的像素。当放大时，出现色块；当缩小时，丢失细节</td>
</tr>
<tr>
<td>THREE.LinearFilter</td>
<td>基于周围四个像素的值，来决定一个正确的颜色。缩小时仍然会丢失细节，但是放大时会更加平滑，不会出现色块</td>
</tr>
</tbody>
</table>
<p>除了这两个基本的取值之外，我们还可以使用mipmap——一系列纹理图片的集合，后者是前者的一半大小。mipmap可以在加载纹理时自动创建，结合以下filter取值使用：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%;">THREE.NearestMipMapNearestFilter</td>
<td>选取最匹配分辨率的mipmap，并应用NearestFilter规则。放大时仍然出现色块，但是看起来好很多</td>
</tr>
<tr>
<td>THREE.NearestMipMapLinearFilter</td>
<td>选取最接近的两个mipmap级别，在两个级别上分别应用NearestFilter规则，得到中间结果。这两个中间结果随之传递给LinearFilter获得最终结果</td>
</tr>
<tr>
<td>THREE.LinearMipMapNearestFilter</td>
<td> </td>
</tr>
<tr>
<td>THREE.LinearMipMapLinearFilter </td>
<td> </td>
</tr>
</tbody>
</table>
<p>如果不明确指定，magFilter默认取值THREE.LinearFilter，minFilter默认取值THREE.LinearMipMapLinearFilter。</p>
<p>纹理本身是方形的，但是Three.js可以确保不管对于什么形状，材质都能正确的覆盖（wrap around），此保证由UV mapping实现。</p>
<div class="blog_h2"><span class="graybg">创建凹凸效果</span></div>
<div class="blog_h3"><span class="graybg">基于bump map</span></div>
<p>所谓bump map，是一幅额外的纹理，用于在材质上添加更多的深度效果：</p>
<pre class="crayon-plain-tag">var texture = THREE.ImageUtils.loadTexture( "../assets/textures/general / " + imageFile)
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump )
mat.bumpMap = bump;
mat.bumpScale = 0.2;  // 设置凸起的高度（负值则表示凹下的深度）
var mesh = new THREE.Mesh( geom, mat );
return mesh;</pre>
<p>bump map通常都是灰度图，像素的密度代表了凸起的（相对）高度 。</p>
<div class="blog_h3"><span class="graybg">基于normal map</span></div>
<p>对于normal map来说，高度信息没有被保存，但是法线的方向被保存了。使用normal map你可以在仅使用很少点、面的情况下创建具有复杂细节的模型：</p>
<pre class="crayon-plain-tag">var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
var m = THREE.ImageUtils.loadTexture("../assets/textures/general/" + normal);
var mat2 = new THREE.MeshPhongMaterial();
mat2.map = t;
mat2.normalMap = m;
mat.normalScale.set(1,1); // 设置凸起的高度（负值则表示凹下的深度）
var mesh = new THREE.Mesh(geom, mat2);
return mesh;</pre>
<p>normal map的缺点是不容易创建，需要使用Blender/Photoshop之类的特殊工具。 </p>
<div class="blog_h2"><span class="graybg">创建假反射</span></div>
<p>计算环境反射效果是非常消耗资源的操作。在Three.js中你可以模拟这种效果。步骤如下：</p>
<ol>
<li>创建一个CubeMap对象，CubeMap是六个纹理的集合，可以被应用到Cube的六个面</li>
<li>使用CubeMap创建一个Box，此Box作为场景的环境，当你转动镜头时，看到的是此Box的内侧</li>
<li>把上述模拟环境的CubMap应用到需要反射效果的Mesh上面，Three.js可以确保其看起来就像是环境的反射</li>
</ol>
<div class="blog_h3"><span class="graybg">创建CubeMap</span></div>
<p>准备好<a href="http://www.humus.name/index.php?page=Textures">纹理图片</a>后，创建CubeMap非常容易。你需要的是能够组成完整环境的六幅图片：向前看时的图片（posz）、向后看时的图片（negz）、向上看的图片（posy）、向下看的图片（negy）、向右看的图片（posx）、向左看的图片（negx）。示例代码：</p>
<pre class="crayon-plain-tag">var path = "../assets/textures/cubemap/parliament/";
var format = '.jpg';
var urls = [
    path + 'posx' + format, path + 'negx' + format,
    path + 'posy' + format, path + 'negy' + format,
    path + 'posz' + format, path + 'negz' + format
];
var textureCube = THREE.ImageUtils.loadTextureCube( urls );</pre>
<p>如果你已经获得360度全景图片，可以利用<a href="//gonchar.me/%20panorama/">工具</a>将其切割为上面的六幅图。 或者直接让Three.js处理切割过程：</p>
<pre class="crayon-plain-tag">var textureCube = THREE.ImageUtils.loadTexture("360-degrees.png", new THREE.UVMapping());</pre>
<div class="blog_h3"><span class="graybg">创建Skybox</span></div>
<p>Three.js提供了一个特殊的着色器，用来基于CubeMap来创建Skybox（环境）：</p>
<pre class="crayon-plain-tag">var shader = THREE.ShaderLib[ "cube" ];
shader.uniforms[ "tCube" ].value = textureCube;

var material = new THREE.ShaderMaterial( {

    fragmentShader: shader.fragmentShader,
    vertexShader: shader.vertexShader,
    uniforms: shader.uniforms,
    depthWrite: false,
    side: THREE.DoubleSide

} );

var skybox = new THREE.Mesh( new THREE.BoxGeometry( 10000, 10000, 10000 ), material );
scene.add( skybox );</pre>
<div class="blog_h3"><span class="graybg">创建反射物体</span></div>
<pre class="crayon-plain-tag">// 此镜头看到的景象，将用于球体的动态反射效果
cubeCamera = new THREE.CubeCamera( 0.1, 20000, 256 );
scene.add( cubeCamera );

// 动态反射，不仅仅CubeMap出现在反射图像中，Mesh也是
// 两个动态反射的物体不支持相互反射
var dynamicEnvMaterial = new THREE.MeshBasicMaterial( { envMap: cubeCamera.renderTarget, side: THREE.DoubleSide } );
sphere = new THREE.Mesh( sphereGeometry, dynamicEnvMaterial );
scene.add( sphere );

// 静态反射
// 注意材质可以设置反射率
var envMaterial = new THREE.MeshBasicMaterial( { envMap: textureCube, side: THREE.DoubleSide， reflection: 1 } );
var cylinder = new THREE.Mesh( cylinderGeometry, envMaterial );
scene.add( cylinder );


function render() {
    // 此镜头看到的内容需要更新，否则动态反射物体漆黑一片
    cubeCamera.updateCubeMap( renderer, scene );
    requestAnimationFrame( render );
}</pre>
<p>材质的envMap属性可以设置为一个CubeMap对象，这样Mesh就可以反射CubeMap代表的环境。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/three-js-study-note">Three.js学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/three-js-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>PostCSS学习笔记</title>
		<link>https://blog.gmem.cc/postcss-study-note</link>
		<comments>https://blog.gmem.cc/postcss-study-note#comments</comments>
		<pubDate>Thu, 22 Dec 2016 08:03:47 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[CSS]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14147</guid>
		<description><![CDATA[<p>简介 由于CSS语言本身的表达能力较差，Web开发人员常常使用SASS、LESS之类的CSS预处理器语言来编写样式，然后再编译为普通的CSS代码。 PostCSS是一个类似的、较晚出现的CSS处理器，它基于JavaScript语言编写。PostCSS使用插件式的架构，而不是像以前的CSS预处理器那样内置所有特性。目前PostCSS的插件数量已经有数百个。 尽管名字中带有Post，PostCSS并不是所谓后处理器。它可以执行其它预处理器能够完成的任务。PostCSS本身只做两件事情： 将输入代码转换为抽象语法树（AST） 调用插件处理AST 插件机制 PostCSS依赖于插件完成实际的工作，例如Lint、变量和混入的支持、未来CSS特性支持（transpile）、内联图片，等等。本节列出常用的插件。 解决全局CSS问题 插件 说明 postcss-use 允许你在CSS中明确声明使用PostCSS特性，这样可以仅针对某些文件使用PostCSS postcss-modules 可以让你在任何地方使用CSS Modules——自动进行CSS选择器的命名隔离，不仅仅限于客户端 react-css-modules 针对React扩展CSS Modules postcss-autoreset 条件式的自动重置 postcss-initial <a class="read-more" href="https://blog.gmem.cc/postcss-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/postcss-study-note">PostCSS学习笔记</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>由于CSS语言本身的表达能力较差，Web开发人员常常使用SASS、LESS之类的CSS预处理器语言来编写样式，然后再编译为普通的CSS代码。</p>
<p>PostCSS是一个类似的、较晚出现的CSS处理器，它基于JavaScript语言编写。PostCSS使用插件式的架构，而不是像以前的CSS预处理器那样内置所有特性。目前PostCSS的插件数量已经有数百个。</p>
<p>尽管名字中带有Post，PostCSS并不是所谓后处理器。它可以执行其它预处理器能够完成的任务。PostCSS本身只做两件事情：</p>
<ol>
<li>将输入代码转换为抽象语法树（AST）</li>
<li>调用插件处理AST</li>
</ol>
<div class="blog_h2"><span class="graybg">插件机制</span></div>
<p>PostCSS依赖于插件完成实际的工作，例如Lint、变量和混入的支持、未来CSS特性支持（transpile）、内联图片，等等。本节列出常用的插件。</p>
<div class="blog_h3"><span class="graybg">解决全局CSS问题</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>postcss-use</td>
<td>
<p>允许你在CSS中明确声明使用PostCSS特性，这样可以仅针对某些文件使用PostCSS</p>
</td>
</tr>
<tr>
<td>postcss-modules</td>
<td>可以让你在任何地方使用CSS Modules——自动进行CSS选择器的命名隔离，不仅仅限于客户端</td>
</tr>
<tr>
<td>react-css-modules</td>
<td>针对React扩展CSS Modules</td>
</tr>
<tr>
<td>postcss-autoreset</td>
<td>条件式的自动重置</td>
</tr>
<tr>
<td>postcss-initial</td>
<td>支持<pre class="crayon-plain-tag">all: initial</pre> 规则，以便重置所有继承得到的样式</td>
</tr>
<tr>
<td>cq-prolyfill</td>
<td>添加容器查询（container query）支持，允许根据父元素的宽度来决定样式</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">使用未来的CSS特性</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>autoprefixer</td>
<td>自动添加厂商相关的规则前缀</td>
</tr>
<tr>
<td>postcss-cssnext</td>
<td>支持未来的CSS特性</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">CSS可读性</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>precss</td>
<td>
<p>提供SASS风格的特性，例如变量、嵌套、混入</p>
</td>
</tr>
<tr>
<td>postcss-sorting</td>
<td>对规则的内容进行排序</td>
</tr>
<tr>
<td>postcss-utilities</td>
<td>包含很多常用的助手工具</td>
</tr>
<tr>
<td>short</td>
<td>支持某些属性简写</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">图像和字体</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>postcss-assets</td>
<td>管理资产，把样式表和环境改变隔离。可以自动寻找图片的URL、自动生成图片的尺寸</td>
</tr>
<tr>
<td>postcss-sprites</td>
<td>生成图片sprites，自动合并多张小图片</td>
</tr>
<tr>
<td>font-magician</td>
<td>生成CSS中所有需要的@font-face规则</td>
</tr>
<tr>
<td>postcss-inline-svg</td>
<td>支持内联SVG图片并设置样式</td>
</tr>
<tr>
<td>postcss-write-svg</td>
<td>支持在CSS中直接写入简单的SVG</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">语法相关</span></div>
<p>PostCSS可以转换基于任何语法的样式，不仅仅是CSS</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>sugarss</td>
<td>类似于SASS的基于缩进的语法</td>
</tr>
<tr>
<td>postcss-scss</td>
<td>与SCSS语言一起工作，但是不进行SCSS编译</td>
</tr>
<tr>
<td>postcss-less</td>
<td>与LESS语言一起工作，但是不进行LESS编译</td>
</tr>
<tr>
<td>postcss-less-engine</td>
<td>与LESS语言一起工作，并且把LESS编译为CSS</td>
</tr>
<tr>
<td>postcss-js</td>
<td>
<p>支持在JS中编写样式，或者把转换React内联样式、Radium、JSS</p>
</td>
</tr>
<tr>
<td>postcss-safe-parser</td>
<td>查找并修复CSS语法错误</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">其它</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cssnano</td>
<td>模块化的CSS压缩器</td>
</tr>
<tr>
<td>lost</td>
<td>基于calc()的特性丰富的网格系统</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">使用PostCSS</span></div>
<div class="blog_h2"><span class="graybg">Webpack</span></div>
<p>基于postcss-loader：</p>
<pre class="crayon-plain-tag">module.exports = {
    module: {
        loaders: [
            {
                test: /\.css$/,
                // 需要在style、css等加载器之前执行
                loader: "style-loader!css-loader!postcss-loader"
            }
        ]
    },
    postcss: function () {
        // 列出启用的PostCSS插件
        return [ require( 'autoprefixer' ), require( 'precss' ) ];
    }
}</pre>
<div class="blog_h2"><span class="graybg">命令行</span></div>
<pre class="crayon-plain-tag">postcss --use autoprefixer -c options.json -o main.css css/*.css</pre>
<div class="blog_h2"><span class="graybg">Webstorm</span></div>
<p>你需要安装<a href="https://plugins.jetbrains.com/plugin/8578">PostCSS support</a>插件，以获得以下特性：</p>
<ol>
<li>自动识别.pcss文件</li>
<li>支持PostCSS语法高亮</li>
<li>智能的代码完成</li>
<li>可配置的代码样式和自动格式化</li>
<li>代码导航支持</li>
<li>对自定义选择器、自定义媒体查询的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">设置</span></div>
<p>如果要对任意.css文件启用PostCSS支持，你可以在Preferences ⇨ Languages &amp; Frameworks ⇨ Stylesheets ⇨ Dialects 把方言切换为PostCSS。</p>
<div class="blog_h1"><span class="graybg">常用插件</span></div>
<div class="blog_h2"><span class="graybg">Autoprefixer</span></div>
<p>这是一个必备的插件，可以让你脱离编写厂商特定的规则前缀的苦海。启用该插件后，如果需要编写弹性和子布局，你仅需要：</p>
<pre class="crayon-plain-tag">#content {
    display: flex;
}</pre>
<p>该插件会将其转换为：</p>
<pre class="crayon-plain-tag">#content {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}</pre>
<p>你可以配置需要支持的浏览器：</p>
<pre class="crayon-plain-tag">require( 'autoprefixer' )( {
    // 仅仅支持支持主流浏览器的最近两个版本
    browsers: [ 'last 2 versions' ]
    // ie 6-8 表示支持IE6-8
    // &gt; 1% 表示支持全球占用率超过1%的浏览器
} )</pre>
<div class="blog_h2"><span class="graybg">postcss-cssnext</span></div>
<p>该插件允许你使用未来版本CSS可能纳入标准的特性，它会将其编译为当前浏览器能够理解的CSS规则。</p>
<p>cssnext已经使用了Autoprefixer，因此不需要明确声明后者。</p>
<div class="blog_h3"><span class="graybg">自定义属性和变量</span></div>
<p>CSS 的层叠变量的自定义属性规范（CSS Custom Properties for Cascading Variables）允许在CSS中定义属性，并在样式规则中将其作为变量引用。自定义属性以<pre class="crayon-plain-tag">--</pre> 开头，你可以利用<pre class="crayon-plain-tag">var()</pre> 引用这些属性：</p>
<pre class="crayon-plain-tag">:root {
    /* 自定义属性 */
    --text-color: black;
}

body {
    /* 引用自定义属性 */
    color: var(--text-color);
}</pre>
<p>你还可以定义一个属性集，然后通过@apply来应用：</p>
<pre class="crayon-plain-tag">:root {
    /* 自定义属性集 */
    --danger-theme: {
        color: white;
        background-color: red;
    };
}

.danger {
    /* 应用属性集，新增两条样式规则 */
    @apply --danger-theme;
}</pre>
<p>你可以在calc()函数中引用属性：</p>
<pre class="crayon-plain-tag">:root {
    --fontSize: 1rem;
}

h1 {
    font-size: calc(var(--fontSize) * 2);
}</pre>
<div class="blog_h3"><span class="graybg">自定义媒体查询</span> </div>
<pre class="crayon-plain-tag">@custom-media --small-viewport (max-width: 30em);

@media (--small-viewport) {
    /* 对于小的viewport应用样式 */
}</pre>
<div class="blog_h3"><span class="graybg">媒体查询范围</span></div>
<p>使用操作符代替min- / max-，让语法更易读：</p>
<pre class="crayon-plain-tag">@media (width &gt;= 500px) and (width &lt;= 1200px) {
    /* 样式规则 */
}

/* 也可以和自定义媒体查询联用 */
@custom-media --only-medium-screen (width &gt;= 500px) and (width &lt;= 1200px);
@media (--only-medium-screen) {
    /* 样式规则 */
}</pre>
<div class="blog_h3"><span class="graybg">自定义选择器</span></div>
<p>CSS 扩展规范（CSS Extensions）允许自定义选择器，你可以创建一个选择器来引用多个既有选择器：</p>
<pre class="crayon-plain-tag">@custom-selector :--heading h1, h2, h3, h4, h5, h6;

:--heading {
    font-weight: bold;
}</pre>
<p>转换后的结果为：</p>
<pre class="crayon-plain-tag">h1, h2, h3, h4, h5, h6 {
 font-weight: bold;
}</pre>
<div class="blog_h3"><span class="graybg">样式规则嵌套</span></div>
<p>样式规则嵌套是非常实用的特性，可以减少重复的选择器声明。它是SASS、LESS等CSS预处理器能够流行的重要原因。</p>
<p>CSS 嵌套模块规范（CSS Nesting Module）中定义了标准的样式规则嵌套方式，cssnext支持这些规范。</p>
<p>规范引入了一种新的嵌套选择器，使用<pre class="crayon-plain-tag">&amp;</pre> 来表示。在嵌套样式规则（nested style rule）中使用该选择器时，它代表当前被匹配的父规则；在其它地方使用该选择器不代表任何东西。</p>
<p>嵌套样式规则具有两种语法：</p>
<ol>
<li>直接嵌套，直接编写在其它样式的内部，且规则的组合选择器（compound selector）的第一个必须是嵌套选择器：<br />
<pre class="crayon-plain-tag">.foo {
    color: blue;
    &amp; &gt; .bar { color: red; }
}
/* 转换后 */
.foo { color: blue; }
.foo &gt; .bar { color: red; }


.foo {
    color: blue;
    &amp;.bar { color: red; }
}
/* 转换后 */
.foo { color: blue; }
.foo.bar { color: red; }

.foo, .bar {
    color: blue;
    &amp; + .baz, &amp;.qux {
        color: red;
    }
}

/* 等价于 */
.foo, .bar { color: blue; }
:matches(.foo, .bar) + .baz, :matches(.foo, .bar).qux { color: red; }</pre>
</li>
<li>嵌套@Rule：这种方式更加灵活，没有嵌套选择器的那些限制：<br />
<pre class="crayon-plain-tag">.foo {
    color: red;
    @nest &amp; &gt; .bar { color: blue; }
}
/* 等价于 */
.foo { color: red; }
.foo &gt; .bar { color: blue; }


.foo {
    color: red;
    /* &amp; 仍然表示父规则，但是位置随意了 */
    @nest .parent &amp; { color: blue; }
}
/* 等价于 */
.foo { color: red; }
.parent .foo { color: blue; }

.foo {
    color: red;
    @nest :not(&amp;) { color: blue; }
}
/* 等价于 */
.foo { color: red; }
:not(.foo) { color: blue; }</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">颜色操控</span></div>
<p>color函数可以让颜色操控更加方便，该函数会被编译为rgba：</p>
<pre class="crayon-plain-tag">a {
    color: color(red alpha(-10%));
}

a:hover {
    color: color(red blackness(80%));
}</pre>
<p>更多的颜色修饰符参考<a href="https://github.com/postcss/postcss-color-function#list-of-color-adjuster">官方</a>。</p>
<p>hwb函数类似于hsl，但是更加易于人类理解，该函数会被编译为rgba：</p>
<pre class="crayon-plain-tag">body {
    color: hwb(90, 0%, 0%, 0.5);
}</pre>
<p>gray函数用于指定灰度颜色 ，该函数会被编译为rgba：</p>
<pre class="crayon-plain-tag">.foo {
    color: gray(85);
}

.bar {
    color: gray(10%, 50%);
}</pre>
<p>四位或者八位的<pre class="crayon-plain-tag">#rrggbbaa</pre> 格式的颜色代码被支持：</p>
<pre class="crayon-plain-tag">body {
    background: #9d9c;
}</pre>
<div class="blog_h3"><span class="graybg">initial值</span></div>
<p>cssnext允许任何样式属性取值initial，这个值不是浏览器默认值，而是规定的某个属性的默认值。例如display的默认值总是inline。</p>
<p>你可以使用</p>
<pre class="crayon-plain-tag">div {
    all: initial;
}</pre>
<p>把所有属性设置为默认值。</p>
<div class="blog_h3"><span class="graybg">:any-link伪类</span></div>
<p>该伪类匹配任何具有href属性的a/area/link元素：</p>
<pre class="crayon-plain-tag">nav :any-link {
    background-color: yellow;
}</pre>
<div class="blog_h3"><span class="graybg">:matches伪类</span></div>
<p>该伪类：</p>
<pre class="crayon-plain-tag">p:matches(:first-child, .special) {
    color: red;
}</pre>
<div class="blog_h3"><span class="graybg">:not伪类</span></div>
<p>允许该伪类指定多个选择器：</p>
<pre class="crayon-plain-tag">p:not(:first-child, .special) {
  color: red;
}</pre>
<div class="blog_h3"><span class="graybg">::伪语法</span></div>
<p>对于不支持该语法的旧浏览器，自动转换为单个冒号：</p>
<pre class="crayon-plain-tag">a::before {  }</pre>
<div class="blog_h2"><span class="graybg">postcss-use</span></div>
<p>在某个样式类文件中启用PostCSS插件。例如清除CSS代码中的注释：</p>
<pre class="crayon-plain-tag">/* 标准语法 */
@use postcss-discard-comments(removeAll: true);
/* 备选语法 */
@use postcss-discard-comments {
    removeAll: true
}

/* 这里的注释会被清除 */
h1 {
    color: red
}</pre>
<div class="blog_h3"><span class="graybg">short</span></div>
<p>为某些样式属性提供简写：</p>
<pre class="crayon-plain-tag">/* 简写 */
.icon {
    size: 48px;
}
/* 等价于 */
.icon {
    width: 48px;
    height: 48px;
}


/* 简写 */
.frame {
    /* 第一个是下上，第二个是左右 */
    margin: * auto;
}
/* 等价于 */
.frame {
    margin-right: auto;
    margin-left: auto;
}


/* 简写 */
.banner {
    position: fixed 0 0 *;
}
/* 等价于 */
.banner {
    position: fixed;
    top: 0;
    right: 0;
    left: 0;
}

/* 简写 */
.canvas {
    color: #abccfc #212231;
}
/* 等价于 */
.canvas {
    color: #abccfc;
    background-color: #212231;
}</pre>
<p>目前IDE对这些简写的支持不太好，可能误判为语法错误。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/postcss-study-note">PostCSS学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/postcss-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>CSS Modules学习笔记</title>
		<link>https://blog.gmem.cc/css-modules-study-note</link>
		<comments>https://blog.gmem.cc/css-modules-study-note#comments</comments>
		<pubDate>Thu, 22 Dec 2016 05:25:07 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[CSS]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14132</guid>
		<description><![CDATA[<p>CSS Modules 简介 CSS Modules是一个开源项目，它是一个简单的CSS模块化规范，主要完成两件事情： 样式类名、动画名的作用域支持。这可以避免命名冲突 模块化支持，允许CSS文件之间的依赖关系 与Less、SASS、PostCSS不同，CM并不尝试把CSS变得像一门编程语言（比如支持控制结构、变量），它仅仅解决模块化的基本问题——作用域和模块依赖。 使用CM时所有[crayon-69db90ad6f473939012248-i/] 和[crayon-69db90ad6f477063834867-i/] 所操作的URL均为模块请求格式（module request format）： [crayon-69db90ad6f47a925555250-i/]和[crayon-69db90ad6f47d087797405-i/] 这样的URL表示想对路径 [crayon-69db90ad6f480989912989-i/]和[crayon-69db90ad6f482499293609-i/] 这样的URL表示目标位于模块目录（例如node_modules）内部 作用域 使用CM时，样式类名、动画名默认仅具有局部作用域。CM会把CSS文件编译为一个低级别ICSS（Interoperable CSS）格式，不过你编写的时候仍然使用普通CSS语法： [crayon-69db90ad6f484752727834/] ICSS会导出一个对象，其键是局部名称（即你在CSS中声明的样式类名），其值则是编译后的全局名称。全局名称正是运行时使用的真实CSS样式类名，默认情况下全局名称是依据局部名称生成的哈希串。 从JS模块引入一个CSS模块时，你自然获得上述导出对象，可以通过键引用CSS类名： [crayon-69db90ad6f486378543853/] 命名 CM建议局部名称一律使用驼峰式大小写，单这不是必须的。 <a class="read-more" href="https://blog.gmem.cc/css-modules-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/css-modules-study-note">CSS Modules学习笔记</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">CSS Modules</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p><a href="https://github.com/css-modules/css-modules">CSS Modules</a>是一个开源项目，它是一个简单的CSS模块化规范，主要完成两件事情：</p>
<ol>
<li>样式类名、动画名的作用域支持。这可以避免命名冲突</li>
<li>模块化支持，允许CSS文件之间的依赖关系</li>
</ol>
<p>与Less、SASS、PostCSS不同，CM并不尝试把CSS变得像一门编程语言（比如支持控制结构、变量），它仅仅解决模块化的基本问题——作用域和模块依赖。</p>
<p>使用CM时所有<pre class="crayon-plain-tag">url(...)</pre> 和<pre class="crayon-plain-tag">import</pre> 所操作的URL均为模块请求格式（module request format）：</p>
<ol>
<li><pre class="crayon-plain-tag">./xxx</pre>和<pre class="crayon-plain-tag">../xxx</pre> 这样的URL表示想对路径</li>
<li><pre class="crayon-plain-tag">xxx</pre>和<pre class="crayon-plain-tag">xxx/yyy</pre> 这样的URL表示目标位于模块目录（例如node_modules）内部</li>
</ol>
<div class="blog_h2"><span class="graybg">作用域</span></div>
<p>使用CM时，样式类名、动画名默认仅具有局部作用域。CM会把CSS文件编译为一个低级别ICSS（Interoperable CSS）格式，不过你编写的时候仍然使用普通CSS语法：</p>
<pre class="crayon-plain-tag">/* style.css */
.className {
    color: green;
}</pre>
<p>ICSS会导出一个对象，其<span style="background-color: #c0c0c0;">键是局部名称（即你在CSS中声明的样式类名），其值则是编译后的全局名称</span>。全局名称正是运行时使用的真实CSS样式类名，默认情况下全局名称是依据局部名称生成的哈希串。</p>
<p>从JS模块引入一个CSS模块时，你自然获得上述导出对象，可以通过键引用CSS类名：</p>
<pre class="crayon-plain-tag">// 导入全部映射
import styles from "./style.css";
// 导入需要的映射
import { className } from "./style.css";

// 基于键引用全局样式类名
element.innerHTML = '&lt;div class="' + styles.className + '"&gt;';</pre>
<div class="blog_h3"><span class="graybg">命名</span></div>
<p>CM建议局部名称一律使用驼峰式大小写，单这不是必须的。</p>
<div class="blog_h3"><span class="graybg">全局作用域</span></div>
<p>使用特殊伪类<pre class="crayon-plain-tag">:global</pre> 可以声明一个全局作用域：</p>
<pre class="crayon-plain-tag">:global(.className) {
    color: green;
}

@keyframes :global(animeName){
}</pre>
<p>全局名称不会被编译成哈希串，在JS中可以直接使用： </p>
<pre class="crayon-plain-tag">element.innerHTML = '&lt;div class="className"&gt;';</pre>
<p>尽管通常情况下没有必要，你可以显式的声明局部作用域： </p>
<pre class="crayon-plain-tag">:local(.className) {
    color: green;
}</pre>
<div class="blog_h2"><span class="graybg">组合</span></div>
<p>CM支持让一个选择器compose另一个选择器定义的样式规则，并称其为组合（Composition）。例如：</p>
<pre class="crayon-plain-tag">.className {
    color: green;
    background: red;
}

.otherClassName {
    composes: className;
    color: yellow;
}

/* 编译结果 */
.global_name_className{
   color: green;
   background: red;
}
.global_name_otherClassName{
    color: yellow;
}</pre>
<p>引用otherClassName的JS代码，会被编译为分别引用上面两个选择器的形式：</p>
<pre class="crayon-plain-tag">`&lt;div class="${otherClassName}"&gt;`
&lt;!-- 编译结果 --&gt;
&lt;div class="global_name_className global_name_otherClassName"&gt;</pre>
<p>注意：</p>
<ol>
<li>你可以在单个composes规则中指定多个目标类名，例如<pre class="crayon-plain-tag">composes: classNameA classNameB;</pre></li>
<li>你可以指定多次composes规则，但是必须位于其它规则的前面</li>
<li>组合仅仅支持局部选择器，并且选择器必须是单个样式类名</li>
</ol>
<div class="blog_h2"><span class="graybg">依赖</span></div>
<p>使用组合时，你可以compose来自其它CSS模块的样式类：</p>
<pre class="crayon-plain-tag">.title {
    composes: className from './another.css';
    color: red;
}</pre>
<p>注意： compose其它CSS文件中定义的样式类时，其应用顺序是不确定的，因此你不能假设同名规则的覆盖情况。</p>
<div class="blog_h2"><span class="graybg">在Webpack中使用</span></div>
<p>CM为流行的构建工具提供了插件支持。使用Webpack时，你可以通过css-loader来支持CM。配置示例：</p>
<pre class="crayon-plain-tag">// Webpack 1.x配置
module.exports = {
    module: {
        loaders: [
            {
                test: /\.css$/,
                // modules参数导致css-loader工作在module模式下
                loader: "style-loader!css-loader?modules"
                // 你可以使用localIdentName参数来定制全局名生成规则，默认规则为[hash:base64]
                loader: "style-loader!css-loader?modules&amp;localIdentName=[path][name]-[local]-[hash:base64:5]"
            },
        ]
    }
};</pre>
<p>当工作在modules模式下时，css-loader会把所有局部样式类名编译成唯一的全局名。 </p>
<div class="blog_h2"><span class="graybg">联用CSS预处理器</span></div>
<p>CM产生的ICSS文件与SASS/SCSS/LESS之类的CSS预处理器是兼容的。你可以把预处理器的loader添加到加载器链中：</p>
<pre class="crayon-plain-tag">{
    test: /\.scss$/,
    loaders: [
        // 注意链条是从右向左（从下向上）执行的
        'style',
        'css?modules&amp;importLoaders=1&amp;localIdentName=[path]___[name]__[local]___[hash:base64:5]',
        'resolve-url',
        'sass'
    ]
} </pre>
<div class="blog_h1"><span class="graybg">React CSS Modules</span></div>
<p>在开发React应用程序时，可以考虑使用PostCSS插件React CSS Modules来代替CM，其优势是：</p>
<ol>
<li>不要求你使用驼峰式大小写的类名。使用CM时，Webpack的css-loader强制要求驼峰式大小写</li>
<li>不需要在代码中到处引用styles对象中的属性</li>
<li>全局CSS与CSS模块很容易区分：<br />
<pre class="crayon-plain-tag">// RCS使用className引用全局CSS类名，styleName引用局部CSS类名
&lt;div className='global-css' styleName='local-module'&gt;&lt;/div&gt; </pre>
</li>
<li>如果styleName属性引用一个未定义的CSS模块，你可以得到一个警告而非错误</li>
</ol>
<div class="blog_h2"><span class="graybg">实现机制</span></div>
<p>RCS扩展了目标React组件的render方法，依据输出元素上的styleName属性的值来寻找styles对象中的CSS模块，然后把找到的全局样式类名<span style="background-color: #c0c0c0;">附加</span>到元素的className后面。</p>
<p>这意味着，组件必须要被RCS装饰。</p>
<div class="blog_h3">
<div class="blog_h3"><span class="graybg" style="color: #007755;">在Webpack中使用</span></div>
</div>
<div class="blog_h3"><span class="graybg">开发环境</span></div>
<p>在开发环境下，你可能希望启用Sourcemaps和热模块替换（Hot Module Replacement） 。加载器style-loader已经支持HMR，因此HMR是开箱即用的。</p>
<p>参考下面的内容配置加载器：</p>
<pre class="crayon-plain-tag">// 需要预先安装style-loader、css-loader
{
    test: /\.css$/,
    loaders: [
        'style?sourceMap',
        'css?modules&amp;importLoaders=1&amp;localIdentName=[path]___[name]__[local]___[hash:base64:5]'
    ]
}</pre>
<div class="blog_h3"><span class="graybg">生产环境</span></div>
<p>在生产环境下，你可能希望把CSS块合并到单个文件中：</p>
<pre class="crayon-plain-tag">// 需要预先安装style-loader、css-loader
// extract-text-webpack-plugin 用于把CSS块合并到单个文件
{
    module: {
        loaders: [
            // ExtractTextPlugin v2x
            {
                test: /\.css$/,
                loader: ExtractTextPlugin.extract( {
                    notExtractLoader: 'style-loader',
                    loader: 'css?modules&amp;importLoaders=1&amp;localIdentName=[path]___[name]__[local]___[hash:base64:5]!resolve-url!postcss',
                } ),
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin( {
            filename: 'app.css',
            allChunks: true
        } )
    ]
}</pre>
<div class="blog_h2"><span class="graybg">联用React</span></div>
<p>要使用RCM，你的React组件必须被CSSModules装饰：</p>
<pre class="crayon-plain-tag">import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './table.css';

// 可以使用ES7装饰器语法
@CSSModules(styles, options)
class Table extends React.Component {
    render() {
        return &lt;div styleName='table'&gt;
            &lt;div styleName='row'&gt;
                &lt;div styleName='cell'&gt;A0&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;;
    }
}

// 必须装饰React组件，否则无法使用styleName属性
export default CSSModules( Table, styles, options);</pre>
<p>其中options支持以下选项：</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>allowMultiple</td>
<td>默认false。是否允许多个CSS模块名，如果设置为false，以下代码会导致错误：<br />
<pre class="crayon-plain-tag">&lt;div styleName='foo bar' /&gt; </pre>
</td>
</tr>
<tr>
<td>errorWhenNotFound</td>
<td>默认true。如果styleName指定了无法在styles对象中找到的CSS模块，是否报错</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">styles属性</span></div>
<p>被装饰的React组件，会获得<pre class="crayon-plain-tag">this.props.styles</pre> 属性，它的值与CSSModules调用的第2参数是一个对象：</p>
<pre class="crayon-plain-tag">&lt;div&gt;
   // 这两种写法等价
    &lt;p styleName='foo'&gt;&lt;/p&gt;
    &lt;p className={this.props.styles.foo}&gt;&lt;/p&gt;
&lt;/div&gt;;</pre>
<div class="blog_h3"><span class="graybg">子组件</span></div>
<p>默认的，你不能在子组件中的输出元素中使用styleName属性，因为子组件没有被CSSModules装饰。你可以：</p>
<ol>
<li>使用装饰过的子组件</li>
<li>使用从被装饰过的父组件中继承得到的<pre class="crayon-plain-tag">this.props.styles</pre></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/css-modules-study-note">CSS Modules学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/css-modules-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>React Router学习笔记</title>
		<link>https://blog.gmem.cc/react-router-study-note</link>
		<comments>https://blog.gmem.cc/react-router-study-note#comments</comments>
		<pubDate>Tue, 20 Dec 2016 09:24:50 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=14091</guid>
		<description><![CDATA[<p>简介 React Router（本文后续简写为RR）是一个专门服务于React应用的强大的路由库。利用它你可以轻松的建立URL和UI之间的对应关系、在浏览器历史记录中自由导航。 本章先手工实现一个简单的路由机制，然后利用RR进行改造，以了解RR的优势和基本功能。 手工实现路由 [crayon-69db90ad6fce6443311300/] 通过RR实现路由 [crayon-69db90ad6fcea824791563/] 可以看到，RR知道如何（根据配置信息）构建嵌套的UI，你不需要手工的读取URL并找到对应的Child组件， App与其内部的子组件实现了解耦。 添加更多的UI 现在，我们在inbox这个路径下面再嵌套一级子路由：  [crayon-69db90ad6fced900557845/] 这样，当你访问/inbox/messages/123时，RR将会构建如下组件层次： [crayon-69db90ad6fcef326243365/] 当你访问/inbox时，则构建如下组件层次： [crayon-69db90ad6fcf2977158688/] 总之，URL的层次和组件的层次具有对应关系，上层组件的this.props.children，等于匹配的下层路由的component属性所指向的组件。注意这种对应关系不一定是“严格”的： URL中的一个“层次”，可以跨越多个以斜杠[crayon-69db90ad6fcf4721678963-i/] 划分的片断 组件中的一个“层次”，可以包含多级React元素。层次的结果取决于你在何处声明this.props.children 这种对应关系很自然，但是如果要手工实现的话需要编写不少罗嗦的代码。 获取路径变量和URL参数 <a class="read-more" href="https://blog.gmem.cc/react-router-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/react-router-study-note">React Router学习笔记</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>React Router（本文后续简写为RR）是一个专门服务于React应用的强大的路由库。利用它你可以轻松的建立URL和UI之间的对应关系、在浏览器历史记录中自由导航。</p>
<p>本章先手工实现一个简单的路由机制，然后利用RR进行改造，以了解RR的优势和基本功能。</p>
<div class="blog_h2"><span class="graybg">手工实现路由</span></div>
<pre class="crayon-plain-tag">import React from 'react'
import { render } from 'react-dom'

const About = React.createClass( { /*...*/ } )
const Inbox = React.createClass( { /*...*/ } )
const Home = React.createClass( { /*...*/ } )

const App = React.createClass( {
    getInitialState() {
        return {
            // 使用Hash部分进行路由
            route: window.location.hash.substr( 1 )
        }
    },

    componentDidMount() {
        // 当Hash部分变化后，需要改变React状态进行重渲染
        window.addEventListener( 'hashchange', () =&gt; {
            this.setState( {
                route: window.location.hash.substr( 1 )
            } )
        } )
    },

    render() {
        // 根据状态，也就是URL的Hash的不同，决定渲染哪个子组件
        let Child
        switch ( this.state.route ) {
            case '/about': Child = About; break;
            case '/inbox': Child = Inbox; break;
            default: Child = Home;
        }

        return (
            &lt;div&gt;
                &lt;h1&gt;App&lt;/h1&gt;
                &lt;ul&gt;
                    &lt;li&gt;&lt;a href="#/about"&gt;About&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="#/inbox"&gt;Inbox&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
                &lt;Child/&gt;
            &lt;/div&gt;
        )
    }
} )

render( &lt;App /&gt;, document.body )</pre>
<div class="blog_h2"><span class="graybg">通过RR实现路由</span></div>
<pre class="crayon-plain-tag">import React from 'react'
import ReactDom from 'react-dom'

import { Router, Route, IndexRoute, Link, hashHistory } from 'react-router'

const App = React.createClass( {
    render() {
        return (
            &lt;div&gt;
                &lt;h1&gt;App&lt;/h1&gt; 
                /* 将a替换为RR提供的Link组件 */
                &lt;ul&gt;
                    &lt;li&gt;&lt;Link to="/about" &gt;About&lt;/Link&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;Link to='/inbox' &gt;Inbox&lt;/Link&gt;&lt;/li&gt;
                &lt;/ul&gt;
                /* 这里不需要手工判断渲染哪个子路由组件，RR知道 */
                {this.props.children}
            &lt;/div&gt;
        )
    }
} )

/**
 * 渲染的是由Router/Route定义的路由规则，而非UI组件
 */
ReactDom.render( (
    /* 使用hashHistory，表示路由的判断依据是URL的Hash部分 */
    &lt;Router history={hashHistory}&gt;
        /* path指定路由基准URL */
        &lt;Route path="/" component={App}&gt;
            /* 路由的嵌套层次，与组件的嵌套层次对应 */
            &lt;IndexRoute component={Home}/&gt;  /* 默认UI */
            &lt;Route path="about" component={About}/&gt;  /* Hash部分是About时，例如/#/about */
            &lt;Route path="inbox" component={Inbox}/&gt;  /* Hash部分是Inbox时 */
        &lt;/Route&gt;
    &lt;/Router&gt;
), document.body )</pre>
<p>可以看到，RR知道如何（根据配置信息）构建嵌套的UI，你不需要手工的读取URL并找到对应的Child组件， App与其内部的子组件实现了解耦。</p>
<div class="blog_h3"><span class="graybg">添加更多的UI</span></div>
<p>现在，我们在inbox这个路径下面再嵌套一级子路由： </p>
<pre class="crayon-plain-tag">const Message = React.createClass( {
    render() {
        return &lt;h3&gt;Message&lt;/h3&gt;
    }
} )

const Inbox = React.createClass( {
    render() {
        return (
            &lt;div&gt;
                &lt;h2&gt;Inbox&lt;/h2&gt;
                /* 渲染下一级子路由组件 */
                {this.props.children}
            &lt;/div&gt;
        )
    }
} )

ReactDom.render( (
    &lt;Router history={hashHistory}&gt;
        &lt;Route path="/" component={App}&gt;
            &lt;IndexRoute component={Home}/&gt;
            &lt;Route path="about" component={About}/&gt;
            &lt;Route path="inbox" component={Inbox}&gt;
                /* 嵌套路由 */
                &lt;IndexRoute component={InboxStats}/&gt; /* 嵌套路由的默认UI */
                /* 在类似/inbox/messages/123这样的URL下渲染Message组件 */
                &lt;Route path="messages/:id" component={Message}/&gt;
            &lt;/Route&gt;
        &lt;/Route&gt;
    &lt;/Router&gt;
), document.body )</pre>
<p>这样，当你访问/inbox/messages/123时，RR将会构建如下组件层次：</p>
<pre class="crayon-plain-tag">&lt;App&gt;
    &lt;Inbox&gt;
        &lt;Message params={{ id: 'Jkei3c32' }}/&gt;
    &lt;/Inbox&gt;
&lt;/App&gt;</pre>
<p>当你访问/inbox时，则构建如下组件层次：</p>
<pre class="crayon-plain-tag">&lt;App&gt;
    &lt;Inbox&gt;
        &lt;InboxStats/&gt;
    &lt;/Inbox&gt;
&lt;/App&gt;</pre>
<p>总之，URL的层次和组件的层次具有对应关系，<span style="background-color: #c0c0c0;">上层组件的this.props.children，等于匹配的下层路由的component属性所指向的组件</span>。注意这种对应关系不一定是“严格”的：</p>
<ol>
<li>URL中的一个“层次”，可以跨越多个以斜杠<pre class="crayon-plain-tag">/</pre> 划分的片断</li>
<li>组件中的一个“层次”，可以包含多级React元素。层次的结果取决于你在何处声明this.props.children</li>
</ol>
<p>这种对应关系很自然，但是如果要手工实现的话需要编写不少罗嗦的代码。</p>
<div class="blog_h3"><span class="graybg">获取路径变量和URL参数</span></div>
<p>React组件会被RR自动注入路径变量，例如messages/:id中的id，可以通过<pre class="crayon-plain-tag">props.params.id</pre> 读取到：</p>
<pre class="crayon-plain-tag">const Message = React.createClass( {
    componentDidMount() {
        const id = this.props.params.id
        fetchMessage( id, function ( err, message ) {
            this.setState( { message: message } )
        } )
    }
} )</pre>
<p>URL中附带的查询参数也被注入，例如/user?name=alex中的name，可以通过<pre class="crayon-plain-tag">props.location.query.name</pre> 读取到。</p>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">Router组件</span></div>
<p>该组件用于将RR引入到React应用程序中，渲染时一般将其作为JSX根元素：</p>
<pre class="crayon-plain-tag">ReactDom.render( (
    &lt;Router history={hashHistory}&gt;
       /* ... */
    &lt;/Router&gt;
), document.body )</pre>
<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="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>history</td>
<td>该router需要监听的history对象，通常是browserHistory或hashHistory</td>
</tr>
<tr>
<td>children</td>
<td>一个或者多个Route、PlainRoute组件。指定路由的规则集</td>
</tr>
<tr>
<td>routes</td>
<td>children的别名</td>
</tr>
<tr>
<td>createElement</td>
<td>当router准备渲染一个组件树分支时，调用此函数进行React元素的创建。你可以用该方法控制创建过程：<br />
<pre class="crayon-plain-tag">&lt;Router createElement={createElement} /&gt;

// 默认行为
function createElement(Component, props) {
    return &lt;Component {...props} /&gt;
}</pre>
</td>
</tr>
<tr>
<td>onError</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">onError(error)</pre> </p>
<p>进行路由匹配的时候可能出现错误，这些错误通常来自于那些异步的特性，例如route.getComponents、route.getIndexRoute、route.getChildRoutes等。你可以基于此方法进行捕获和处理</p>
</td>
</tr>
<tr>
<td>onUpdate</td>
<td>
<p>一旦router改变其状态以对URL变更进行响应，即调用此方法</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Route组件</span></div>
<p>这是一个配置组件（Configuration Components），定义一个路由规则，该规则将一个URL片断和一个React组件对应起来。路由规则可以嵌套，并与URL嵌套、组件嵌套对应。</p>
<p>JSX中的Route元素实际上等价于Route的子类型PlainRoute。</p>
<p>属性列表：</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>path</td>
<td>此规则匹配的URL片断，可以使用绝对路径（以/开头），也可以使用相对于父Route元素的想对路径</td>
</tr>
<tr>
<td>component</td>
<td>对于根元素，指定渲染什么React组件，对于非根元素，指定上级Route元素的<pre class="crayon-plain-tag">props.children</pre> 对应什么组件</td>
</tr>
<tr>
<td>components</td>
<td>以对象形式指定多个<a id="named-components"></a>命名组件，父路由组件可以通过<pre class="crayon-plain-tag">props[name]</pre> 来访问这些组件。举例：<br />
<pre class="crayon-plain-tag">&lt;Route path="groups" components={{main: Groups, sidebar: GroupsSidebar}} /&gt;</pre></p>
<p> 在父路由组件中，你可以这些引用这两个子路由组件：</p>
<p><pre class="crayon-plain-tag">const {main, sidebar} = this.props
return (
    &lt;div&gt;
        &lt;div className="Main"&gt;
            {main}
        &lt;/div&gt;
        &lt;div className="Sidebar"&gt;
            {sidebar}
        &lt;/div&gt;
    &lt;/div&gt;
)</pre>
</td>
</tr>
<tr>
<td>getComponent</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">getComponent(nextState, callback)</pre> 。callback的签名：<pre class="crayon-plain-tag">cb(err, component)</pre> </p>
<p>类似于component，但是用于动态路由。RR会在需要的时候调用此函数，加载需要的组件。此方法的实现示例：</p>
<pre class="crayon-plain-tag">(nextState, cb) =&gt; {
    // 异步的查找、加载组件
    // 执行RR提供的回调
    cb(null, Course)
}</pre>
</td>
</tr>
<tr>
<td>onEnter</td>
<td rowspan="2">
<p>方法签名：
<pre class="crayon-plain-tag">// nextState 下一个（即将进入的）路由状态对象
// replace 函数，调用它可以重定向到另外一个位置
// callback(err) 如果提供此参数，则onEnter被异步调用，路由切换在callback调用前一直阻塞
// 函数中的this指向当前Route对象
onEnter(nextState, replace, callback?)

// 前一个（正要离开的）路由状态对象
onLeave(prevState)</pre>
<p>路由切换被确认时执行的钩子函数。使用这些钩子可以做很多事情，例如：</p>
<ol>
<li>Enter路由之前执行身份验证</li>
<li>Leave路由之前保存数据</li>
</ol>
<p>在路由切换时：</p>
<ol>
<li>首先在<span style="background-color: #c0c0c0;">旧叶子Route</span>上执行<pre class="crayon-plain-tag">onLeave</pre> 钩子，并向上逐级执行直到遇到<span style="background-color: #c0c0c0;">新旧URL公用的祖先路由（此祖先路由的onLeave不执行）为止</span></li>
<li>然后在<span style="background-color: #c0c0c0;">最顶级的新旧URL不同的祖先路由</span>上执行<pre class="crayon-plain-tag">onEnter</pre> 钩子，并向下逐级执行到<span style="background-color: #c0c0c0;">新叶子路由</span>为止</li>
</ol>
<p>示例：</p>
<p><pre class="crayon-plain-tag">const userIsInATeam = (nextState, replace, callback) =&gt; {
    // 路由切换前需要准备数据
    fetch(/**/) .then(response = response.json()) .then(userTeams =&gt; {
            // 某些条件下，可以重定向路由到其它位置
            if (userTeams.length === 0) {
                replace(`/users/${nextState.params.userId}/teams/new`)
            }
            // 调用RR提供的回调，以完成路由切换
            callback();
        }) .catch(error =&gt; {
            // 在此执行错误处理
            callback(error);
        })
}

&lt;Route path="/users/:userId/teams" onEnter={userIsInATeam}/&gt;</pre>
</td>
</tr>
<tr>
<td>onLeave</td>
</tr>
<tr>
<td>onChange</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">onChange(prevState, nextState, replace, callback?)</pre> ，参数作用类似于onEnter</p>
<p>当浏览器的地址发生改变，但是当前路由对象既不Enter也不Leave的情况下触发的钩子方法。触发时机举例：</p>
<ol>
<li>当前路由的子路由发生变化</li>
<li>URL查询参数部分发生变化</li>
</ol>
</td>
</tr>
<tr>
<td colspan="2">
<p><em><strong>以下属性为PlainRoute专有</strong></em></p>
</td>
</tr>
<tr>
<td>childRoutes</td>
<td>
<p>子路由集合，对应JSX中Route的子元素</p>
</td>
</tr>
<tr>
<td>getChildRoutes</td>
<td>
<p>方法签名：</p>
<pre class="crayon-plain-tag">// partialNextState 部分解析的下一状态，因为子路由尚未加载，因而路由匹配尚未完成
// callback(err, routesArray) 
getChildRoutes(partialNextState, callback)</pre>
<p>类似于childRoutes，但是用于动态路由。RR会在需要的时候调用此函数，以完成路由的匹配。示例：</p>
<pre class="crayon-plain-tag">// 静态路由，子路由代码需要同步加载
let myRoute = {
    path: 'course/:courseId',
    childRoutes: [
        announcementsRoute,
        gradesRoute,
        assignmentsRoute
    ]
}

// 异步子路由，仅仅在匹配 course/**/**这样的URL时才异步的加载
let myRoute = {
    path: 'course/:courseId',
    getChildRoutes(location, cb) {
        // 在此加载子路由
        // 然后调用RR提供的回调
        cb(null, [announcementsRoute, gradesRoute, assignmentsRoute])
    }
}


// 导航依赖的子路由，可以链接到某些状态
&lt;Link to="/picture/123" state={{ fromDashboard: true }}/&gt;

let myRoute = {
    path: 'picture/:id',
    getChildRoutes(partialNextState, cb) {
        let {state} = partialNextState

        // 根据条件，加载不同的子路由
        if (state &amp;&amp; state.fromDashboard) {
            cb(null, [dashboardPictureRoute])
        } else {
            cb(null, [pictureRoute])
        }
    }
}</pre>
</td>
</tr>
<tr>
<td>indexRoute</td>
<td>
<p>参考IndexRoute子元素
</td>
</tr>
<tr>
<td>getIndexRoute</td>
<td>
<p>方法签名：<pre class="crayon-plain-tag">getIndexRoute(partialNextState, callback)</pre> </p>
<p>异步的加载IndexRoute</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">IndexRoute组件</span></div>
<p>这是一个配置组件，属于特殊的路由规则，当（下层）URL片断为空时，匹配此规则。例如：</p>
<pre class="crayon-plain-tag">&lt;Route path="/" component={App}&gt;
    &lt;IndexRoute component={Dashboard} /&gt;
&lt;/Route&gt;</pre>
<p>当你访问URL / 时，会渲染App组件，且其children为一个Dashboard组件。</p>
<div class="blog_h2"><span class="graybg">Redirect组件</span></div>
<p>这是一个配置组件，用于URL的重定向，例如：</p>
<pre class="crayon-plain-tag">&lt;Redirect from="messages/:id" to="/messages/:id" /&gt;</pre>
<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="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>from</td>
<td>需要重定向的URL，包含路径变量部分</td>
</tr>
<tr>
<td>to</td>
<td>重定向到的目标</td>
</tr>
<tr>
<td>query</td>
<td>查询参数部分，默认情况下自动把from的查询参数带过去</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">IndexRedirect组件</span></div>
<p>这是一个配置组件，指定指定默认使用的下层URL。例如：</p>
<pre class="crayon-plain-tag">&lt;Route path="/" component={App}&gt;
    &lt;IndexRedirect to="/welcome" /&gt;
    // 访问 / 时自动重定向到 /welcome
    &lt;Route path="welcome" component={Welcome} /&gt;
&lt;/Route&gt;</pre>
<div class="blog_h2"><span class="graybg">Link组件</span></div>
<p>该组件用于触发路由切换，以便在应用程序中导航，该组件渲染为a标签。</p>
<p>如果Link指向的路由恰恰是应用中的当前路由（URL匹配），则RR自动给Link添加activeClassName样式类，并且其activeStyle指定的样式被启用。</p>
<p>属性列表：</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>to</td>
<td>
<p>一个位置描述符（location descriptor）对象，或者一个字符串。如果不指定该属性，生成一个没有href的a标签</p>
<p>使用字符串时，指定需要链接到的绝对路径。使用位置描述符时，可以指定以下属性：</p>
<ol>
<li>pathname  URL的路径部分</li>
<li>query  一个对象，指定URL查询参数部分</li>
<li>hash  URL的#部分，例如#a-hash</li>
<li>state 需要持久化到location的状态</li>
</ol>
<p>示例：</p>
<p><pre class="crayon-plain-tag">// 字符串
&lt;Link to="/hello"&gt;Hello&lt;/Link&gt;
// 位置描述符
&lt;Link to={{ pathname: '/hello', query: { name: 'ryan' } }}&gt;Hello&lt;/Link&gt;
// 返回位置描述符的函数
&lt;Link to={location =&gt; ({ ...location, query: { name: 'ryan' } })}&gt;Hello&lt;/Link&gt; </pre>
</td>
</tr>
<tr>
<td>activeClassName</td>
<td>to指定的路由是当前路由时，启用的样式类</td>
</tr>
<tr>
<td>activeStyle</td>
<td>to指定的路由是当前路由时，启用的样式</td>
</tr>
<tr>
<td>onClick</td>
<td>点击时执行的函数，在此函数调用e.preventDefault()可以阻止路由切换</td>
</tr>
<tr>
<td>onlyActiveOnIndex</td>
<td>
<p>仅当to与当前路由精确匹配时，才认为是Active。等价于<pre class="crayon-plain-tag">&lt;IndexLink&gt;</pre> 组件</p>
<p>如果不指定该属性，那么<pre class="crayon-plain-tag">&lt;Link to="/"&gt;Home&lt;/Link&gt;</pre> 这个链接总是Active，因为URL总是以/开头。此时可以使用<pre class="crayon-plain-tag">&lt;IndexLink to="/"&gt;Home&lt;/IndexLink&gt;</pre> 代替，这样仅当URL为/时才匹配</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">规则匹配算法</span></div>
<p>同级别的Route，<span style="background-color: #c0c0c0;">先声明的具有更高的优先级</span>。</p>
<p>URL语法和匹配规则如下：</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>:paramName</td>
<td>
<p>匹配一个URL片断，直到遇到 <pre class="crayon-plain-tag">/</pre> 、<pre class="crayon-plain-tag">？</pre> 或者<pre class="crayon-plain-tag">#</pre> ，组件自动获得<pre class="crayon-plain-tag">params.paramName</pre> 属性。举例：</p>
<p><pre class="crayon-plain-tag">// 匹配：/hello/alex  /hello/wong
&lt;Route path="/hello/:name"&gt;</pre>
</td>
</tr>
<tr>
<td>()</td>
<td>包围URL的一部分，表示该部分是可选的。举例：<br />
<pre class="crayon-plain-tag">// 匹配：/hello/alex  /hello
&lt;Route path="/hello(/:name)"&gt;</pre>
</td>
</tr>
<tr>
<td>*</td>
<td>非贪婪的通配，遇到此通配符后面指定的那个字符之前一直匹配。捕获到的匹配项会存入<pre class="crayon-plain-tag">params.splat</pre> 属性中。举例：<br />
<pre class="crayon-plain-tag">// 匹配：/files/hello.jpg  /files/hello.html
&lt;Route path="/files/*.*"&gt;</pre>
</td>
</tr>
<tr>
<td>**</td>
<td>贪婪的通配，直到遇到 <pre class="crayon-plain-tag">/</pre> 、<pre class="crayon-plain-tag">？</pre> 或者<pre class="crayon-plain-tag">#</pre> 。捕获到的匹配项会存入<pre class="crayon-plain-tag">params.splat</pre> 属性中。举例：<br />
<pre class="crayon-plain-tag">// 匹配：/files/hello.jpg   /files/path/to/file.jpg
&lt;Route path="/**/*.jpg"&gt;  </pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Histories</span></div>
<p>RR在<a href="https://github.com/mjackson/history">history</a>库的基础上构建，一个history对象可以监听浏览器的URL的改变，并把URL解析为一个location对象。RR使用此location对象来匹配路由规则并渲染正确的组件树。</p>
<p>缺省可用的history实现包括三种：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">History实现</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>browserHistory</td>
<td>
<p>对于运行在浏览器中的应用，这是推荐的实现。它使用浏览器内置的<a href="https://developer.mozilla.org/en-US/docs/Web/API/History">History</a> API来操控URL，能够创建“真实的”URL，例如 gmem.cc/some/path</p>
<p><strong>服务器配置</strong></p>
<p>要在所有浏览器中使用这种History实现，需要服务器的支持。你可能需要将<span style="background-color: #c0c0c0;">某个通配的路径映射到同一个HTML文件</span>，例如gmem.cc/**总是映射到gmem.cc/index.html。</p>
<p>使用Express作为服务器时，可以参考如下代码：</p>
<pre class="crayon-plain-tag">app.get('*', function (request, response){
    response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})</pre>
<p>使用Nginx时，可以使用try_files指令：</p>
<pre class="crayon-plain-tag">server {
    location / {
        try_files $uri /index.html;
    }
}</pre>
<p>使用Apache时可以使用rewrite模块：</p>
<pre class="crayon-plain-tag">RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]</pre>
<p><strong>IE8,IE9支持</strong></p>
<p>RR会自动检测浏览器是否支持History API，如果不支持，所有URL转换都会导致完全的页面reload</p>
</td>
</tr>
<tr>
<td>hashHistory</td>
<td>
<p>仅仅使用URL的哈希（#）部分，生成gmem.cc/#/some/path风格的URL</p>
<p>该实现的优点是不需要配置服务器。缺点是URL比较难堪而且不支持服务器端渲染</p>
</td>
</tr>
<tr>
<td>createMemoryHistory</td>
<td>不会操控浏览器地址栏，使用RR进行服务器端渲染时使用，也可以用于测试React Native等其它渲染环境</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">withRouter函数</span></div>
<p>用于包装其它React组件，为其提供<pre class="crayon-plain-tag">props.router</pre> 属性。函数签名：</p>
<pre class="crayon-plain-tag">/**
 * Component 被包装的组件
 * options 选项：
 *      withRef  如果为true，则包装后的组件的getWrappedInstance()返回被包装组件
 */
withRouter(Component, [options])</pre>
<p>props.router属性与context.router是同一种对象。</p>
<div class="blog_h2"><span class="graybg">RouterContext</span></div>
<p>依据给定的路由状态，渲染对应的组件树。Router组件使用了该组件，在React组件的上下文对象上添加一个<pre class="crayon-plain-tag">this.context.router</pre> 属性。</p>
<div class="blog_h3"><span class="graybg"><a id="context.router"></a>context.router</span></div>
<p>该对象提供与路由有关的方法和数据，你可以使用该对象进行编程式的路由控制：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 35%; text-align: center;">属性/方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><pre class="crayon-plain-tag">push(pathOrLoc)</pre> </td>
<td>切换路由到一个新的URL，并在浏览器历史中压入条目。示例：<br />
<pre class="crayon-plain-tag">// 使用字符串
router.push( '/users/alex' )
// 使用位置描述符对象
router.push( {
    pathname: '/users/alex',
    query: { modal: true },
    state: { fromDashboard: true }
} ) </pre>
</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">replace(pathOrLoc)</pre> </td>
<td>类似于push，但是替换浏览器历史的当前条目</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">go(n)</pre> </td>
<td>在浏览器历史中前进或者后退</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">goBack()</pre> </td>
<td>在浏览器历史中后退一步</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">goForward()</pre> </td>
<td>在浏览器历史中前进一步</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">setRouteLeaveHook(route, hook)</pre> </td>
<td>注册一个钩子，在离开route这个路由时调用，用于导航确认</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">createPath(pathOrLoc, query)</pre> </td>
<td>根据router的配置，把查询参数对象转换为URL路径名（不包括协议、域名部分）</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">createHref(pathOrLoc, query)</pre> </td>
<td>
<p>创建一个URL，如果使用hashHitory，会自动在URL路径名前面添加#/</p>
</td>
</tr>
<tr>
<td><pre class="crayon-plain-tag">isActive(pathOrLoc, indexOnly)</pre> </td>
<td>判断pathOrLoc是否对应当前路由。如果匹配路由R，则同样匹配R的祖先路由，那么在R及其祖先路由对应的Component中调用该方法，均返回true</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">路由组件</span></div>
<p>所谓路由组件，是指路由规则（Route）关联的组件，当路由规则被匹配、且父路由组件输出了当前路由组件时，路由组件被自动渲染。</p>
<p>路由组件被渲染时，RR自动为其注入一些属性。</p>
<div class="blog_h3"><span class="graybg">注入的属性</span></div>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">属性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>location</td>
<td>当前<a href="https://github.com/mjackson/history/blob/v2.x/docs/Location.md">location</a>对象</td>
</tr>
<tr>
<td>params</td>
<td>URL中的动态部分，包括路径变量捕获</td>
</tr>
<tr>
<td>route</td>
<td>导致此组件被渲染的路由对象</td>
</tr>
<tr>
<td>router</td>
<td>与<a href="#context.router">context.router</a>相同</td>
</tr>
<tr>
<td>routeParams</td>
<td>
<p>捕获到的、在Route.path中直接声明的路径变量。如果Route.path为users/:userId 而当前URL为/users/123/portfolios/345。那么：</p>
<ol>
<li>this.props.routeParams为<pre class="crayon-plain-tag">{userId: '123'}</pre> </li>
<li>this.props.params为<pre class="crayon-plain-tag">{userId: '123', portfolioId: '345'}</pre> </li>
</ol>
</td>
</tr>
<tr>
<td>children</td>
<td>匹配的、将被渲染的子路由组件。如果当前路由使用<a href="#named-components">命名组件</a>，则该属性为undefined。各命名组件将作为this.props的直接属性</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">高级主题</span></div>
<div class="blog_h2"><span class="graybg">动态路由</span></div>
<p>对于大型应用来说，下载尽可能少的JavaScript文件以启动应用很重要，最好仅下载与当前的UI相关的JS。如果不这样做，用户将忍受过长的加载时间。生产环境下我们通常使用模块化，配合代码分割（Code Splitting）技术来满足尽快启动的需求，然后随着用户的操作不断加载用到的JS。</p>
<p>路由定义了UI的样子，很自然的可以作为代码分割点。</p>
<p>RR支持异步的读取路由规则、异步的加载组件。在最初的捆绑文件（Bundle，即启动应用的那个代码分割块）中，你只需要提供一个路由规则，其它规则可以后续按需加载。</p>
<p>Route组件可以定义getChildRoutes、getIndexRoute、getComponents方法，这些方法仅在需要的时候才会被调用以完成规则匹配和组件渲染。RR称这种方式为渐进匹配（gradual  matching）——逐步的匹配URL片段且仅仅加载必要的信息。</p>
<p>下面是一个动态路由的示例：</p>
<pre class="crayon-plain-tag">const CourseRoute = {
    path: 'course/:courseId',
    // 当尝试导航到course/**/**时，下面的方法被调用
    getChildRoutes(partialNextState, callback) {
        // 这里使用Webpack的CommonJS扩展
        require.ensure([], function (require) {
            callback(null, [
                // 同步加载子路由定义模块
                require('./routes/Announcements'),
                require('./routes/Assignments'),
                require('./routes/Grades'),
            ])
        })
    },
    // 当尝试导航到course/**时，下面的方法被调用
    getIndexRoute(partialNextState, callback) {
        require.ensure([], function (require) {
            callback(null, {
                component: require('./components/Index'),
            })
        })
    },
    // 当尝试导航到course/**时，该方法被调用，加载对应的组件
    getComponents(nextState, callback) {
        require.ensure([], function (require) {
            callback(null, require('./components/Course'))
        })
    }
}</pre>
<div class="blog_h2"><span class="graybg">导航确认</span></div>
<p>你可以调用router的<pre class="crayon-plain-tag">setRouteLeaveHook</pre> 方法，设置一个钩子，当离开某个路由时，该钩子会被执行，你可以利用此钩子：</p>
<ol>
<li>向用户做出提示</li>
<li>阻止导航的发生</li>
</ol>
<p> 示例：</p>
<pre class="crayon-plain-tag">// v2.4.0引入的withRouter可以向组件注入当前router对象
const Home = withRouter(
    React.createClass({

        componentDidMount() {
            // 为当前组件对应的route对象设置钩子
            this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave)
        },

        routerWillLeave(nextLocation) {
            //  返回false禁止导航
            //  返回字符串则让用户确认是否导航
            if (!this.state.isSaved)
                return 'Your work is not saved! Are you sure you want to leave?'
        },

    })
)</pre>
<div class="blog_h2"><span class="graybg">组件外导航</span></div>
<p>你可以使用<pre class="crayon-plain-tag">withRouter</pre> 包装一个组件，从而通过<pre class="crayon-plain-tag">this.props.router</pre> 获得当前router对象的引用。有了router对象后你就可以随意导航（触发路由切换）了。</p>
<p>在React组件外部，例如Redux中间件或者Flux Action的代码中，你可以通过history对象进行导航：</p>
<pre class="crayon-plain-tag">import {browserHistory} from 'react-router'

// 导航到 /some/path.
browserHistory.push('/some/path')

// 后退到前一个URL
browserHistory.goBack()</pre>
<div class="blog_h2"><span class="graybg">最小化Bundle的尺寸</span></div>
<p>为了简便，RR通过顶级模块react-router暴露了完整的API。这导致整个RR库及其依赖被包含到入口点Bundle中，从而增加了Bundle的大小。为了避免此问题，可以从react-router/lib的子模块进行导入：</p>
<pre class="crayon-plain-tag">import { Link, Route, Router } from 'react-router'
// 可以改写为：
import Link from 'react-router/lib/Link'
import Route from 'react-router/lib/Route'
import Router from 'react-router/lib/Router'</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/react-router-study-note">React Router学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/react-router-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
