lly's Blog

用心记录点滴


  • 首页

  • 归档

HTTP协议学习笔记

发表于 2017-03-31   |  

什么是HTTP协议

协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则,超文本传输协议(HTTP)是一种通信协议,它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器

客户端和服务器如何通信呢

当我们打开浏览器,在地址栏中输入URL,然后我们就看到了网页。 原理是怎样的呢?

实际上我们输入URL后,我们的浏览器给Web服务器发送了一个Request, Web服务器接到Request后进行处理,生成相应的Response,然后发送给浏览器, 浏览器解析Response中的HTML,这样我们就看到了网页。

我们的Request 有可能是经过了代理服务器,最后才到达Web服务器的。

代理服务器就是网络信息的中转站,有什么功能呢?

  1. 提高访问速度, 大多数的代理服务器都有缓存功能。

  2. 突破限制, 也就是FQ了

  3. 隐藏身份。

URL详解

URL(Uniform Resource Locator) 地址用于描述一个网络上的资源, 基本格式如下:

1
scheme://host[:port#]/path/.../[?query-string][#anchor]

scheme 指定低层使用的协议(例如:http, https, ftp)

host HTTP服务器的IP地址或者域名

port# HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明,例如 http://www.cnblogs.com:8080/

path 访问资源的路径

query-string 发送给http服务器的数据

anchor- 锚

URL 的一个例子

1
2
3
4
5
6
7
http://www.mywebsite.com/sj/test/test.aspx?name=sviergn&x=true#stuff
Schema: http
host: www.mywebsite.com
path: /sj/test/test.aspx
Query String: name=sviergn&x=true
Anchor: stuff

HTTP协议是无状态的

http协议是无状态的,同一个客户端的这次请求和上次请求是没有对应关系,对http服务器来说,它并不知道这两个请求来自同一个客户端。 为了解决这个问题, Web程序引入了Cookie机制来维护状态.

打开一个网页需要浏览器发送很多次Request

  1. 当你在浏览器输入URL http://www.cnblogs.com 的时候,浏览器发送一个Request去获取 http://www.cnblogs.com 的html. 服务器把Response发送回给浏览器.

  2. 浏览器分析Response中的 HTML,发现其中引用了很多其他文件,比如图片,CSS文件,JS文件。

  3. 浏览器会自动再次发送Request去获取图片,CSS文件,或者JS文件。

  4. 等所有的文件都下载成功后。 网页就被显示出来了。

HTTP消息的结构

Request消息的结构

Request 消息分为3部分,第一部分叫Request line, 第二部分叫Request header, 第三部分是body. header和body之间有个空行, 结构如下图

第一行中的Method表示请求方法,比如”POST”,”GET”, Path-to-resoure表示请求的资源, Http/version-number 表示HTTP协议的版本号

当使用的是”GET” 方法的时候, body是为空的.

Response消息结构

和Request消息的结构基本一样。 同样也分为三部分,第一部分叫Response line, 第二部分叫Response header,第三部分是body. header和body之间也有个空行, 结构如下图

HTTP/version-number表示HTTP协议的版本号, status-code 和message 请看下节[状态代码]的详细解释.

Get和Post方法的区别

Http协议定义了很多与服务器交互的方法,最基本的有4种,分别是GET,POST,PUT,DELETE. 一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着对这个资源的查,改,增,删4个操作。 我们最常见的就是GET和POST了。

GET一般用于获取/查询资源信息,而POST一般用于更新资源信息.

我们看看GET和POST的区别

  1. GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中.

  2. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制.

  3. GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。

  4. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码.

状态码

Response 消息中的第一行叫做状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。

状态码用来告诉HTTP客户端,HTTP服务器是否产生了预期的Response.

HTTP/1.1中定义了5类状态码, 状态码由三位数字组成,第一个数字定义了响应的类别

1XX 提示信息 - 表示请求已被成功接收,继续处理

2XX 成功 - 表示请求已被成功接收,理解,接受

3XX 重定向 - 要完成请求必须进行更进一步的处理

4XX 客户端错误 - 请求有语法错误或请求无法实现

5XX 服务器端错误 - 服务器未能实现合法的请求

常见的一些状态码:

200 OK

最常见的就是成功响应状态码200了, 这表明该请求被成功地完成,所请求的资源发送回客户端

302 Found

重定向,新的URL会在response 中的Location中返回,浏览器将会自动使用新的URL发出新的Request

304 Not Modified

代表上次的文档已经被缓存了, 还可以继续使用

400 Bad Request

客户端请求与语法错误,不能被服务器所理解

403 Forbidden

服务器收到请求,但是拒绝提供服务

404 Not Found

请求资源不存在(输错了URL)

500 Internal Server Error

服务器发生了不可预期的错误

503 Server Unavailable

服务器当前不能处理客户端的请求,一段时间后可能恢复正常

HTTP Request header

get请求:

post请求:

Cache 头域

这个好像只在web中使用

If-Modified-Since

作用: 把浏览器端缓存页面的最后修改时间发送到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行对比。如果时间一致,那么返回304,客户端就直接使用本地缓存文件。如果时间不一致,就会返回200和新的文件内容。客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示在浏览器中.

If-None-Match

作用: If-None-Match和ETag一起工作,工作原理是在HTTP Response中添加ETag信息。 当用户再次请求该资源时,将在HTTP Request 中加入If-None-Match信息(ETag的值)。如果服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。否则将返回200状态和新的资源和Etag. 使用这样的机制将提高网站的性能

Pragma

作用: 防止页面被缓存, 在HTTP/1.1版本中,它和Cache-Control:no-cache作用一模一样

Pargma只有一个用法, 例如: Pragma: no-cache

Cache-Control

作用: 这个是非常重要的规则。 这个用来指定Response-Request遵循的缓存机制。各个指令含义如下

Cache-Control:Public 可以被任何缓存所缓存()

Cache-Control:Private 内容只缓存到私有缓存中

Cache-Control:no-cache 所有内容都不会被缓存

Client头域

Accept

作用: 浏览器端可以接受的媒体类型,

例如: Accept: text/html 代表浏览器可以接受服务器回发的类型为 text/html 也就是我们常说的html文档,

如果服务器无法返回text/html类型的数据,服务器应该返回一个406错误(non acceptable)

通配符 * 代表任意类型

例如 Accept: / 代表浏览器可以处理所有类型,(一般浏览器发给服务器都是发这个)

Accept-Encoding:

作用: 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码);

例如: Accept-Encoding: gzip, deflate

Accept-Language

作用: 浏览器申明自己接收的语言。

语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等;

例如: Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9

User-Agent

作用:告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本.

我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。

例如:FocusLive/1.1.1.t (iPhone; iOS 10.0.2; Scale/2.00)

Connection

该请求是否一致保持连接

例如: Connection: keep-alive 当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接

例如: Connection: close 代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭, 当客户端再次发送Request,需要重新建立TCP连接。

Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间

Cookie:

请求者的身份识别

作用: 最重要的header, 将cookie的值发送给HTTP服务器

OpenGL ES 3.0学习笔记-着色器和程序

发表于 2017-03-26   |  

概述

如果需要使用着色器进行渲染的话,则首先必须有两个对象,分别是着色器对象及程序对象。那么如何理解这两者呢?可以将其理解为C语言的编译器和链接器。

流程是这样的,源代码中提供着色器对象,然后着色器被编译成一个目标形式,再然后链接到一个程序对象。一个程序对象对应多个着色器对象,每个程序对象必须有一个顶点着色器和一个片段着色器。

流程如下:

1.创建一个顶点着色器和一个片段着色器

2.将源代码连接到每个着色器对象

3.编译着色器对象

4.创建一个程序对象

5.将上面得到的着色器对象链接进程序对象中

6.链接程序对象

创建和编译一个着色器

创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GLuint shader;
GLint compiled;
// Create the shader object
shader = glCreateShader ( type );
if ( shader == 0 )
{
return 0;
}
//type
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_VERTEX_SHADER 0x8B31

删除

1
glDeleteShader ( shader );

删除着色器对象的句柄

注:如果着色器已经链接到程序对象中的话,这时候直接调用glDeleteShader不会立刻去删除着色器,而是将其标注,等到着色器不在连接到任何程序对象时,其就会被删除

提供着色器源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void glShaderSource(GLuint shader, //指向着色器的句柄
GLsizei count, //着色器源字符串的数量,虽然每个着色器可以有多个源字符串组成,但是每个着色器只有一个main函数
const GLchar *const*string, //指向保存数量为count的着色器源字符串的数组指针
const GLint *length //指向保存了每个着色器字符串大小且元素大小为count的整数数组指针
)
char vShaderStr[] =
#version 300 es
layout(location = 0) in vec4 vPosition;
void main()
{
gl_Position = vPosition;
}
glShaderSource (shader, 1, &shaderSrc, NULL );

编译着色器

1
2
// Compile the shader
glCompileShader ( shader );

调用glCompileShader将编译已经保存在着色器对象的着色器源代码。和常规的语言编译器一样,编译之后你想知道的第一件事情是有没有错误。你可以使用glGetShaderiv查询查询这一信息和其他有关着色器对象的信息。

检测着色器是否成功编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Check the compile status
glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled );
if ( !compiled )
{
GLint infoLen = 0;
glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
if ( infoLen > 1 )
{
char *infoLog = malloc ( sizeof ( char ) * infoLen );
glGetShaderInfoLog ( shader, infoLen, NULL, infoLog );
esLogMessage ( "Error compiling shader:\n%s\n", infoLog );
free ( infoLog );
}
glDeleteShader ( shader );
return 0;
}
return shader;

创建和链接程序

创建程序

1
2
3
4
GLuint programObject;
// Create the program object
programObject = glCreateProgram ( );

删除程序

1
glDeleteProgram ( programObject );

关联着色器和程序

1
2
glAttachShader ( programObject, vertexShader );
glAttachShader ( programObject, fragmentShader );

注:对于程序对象和着色器对象的连接没有具体的时间要求,但是没有程序对象只能有一个顶点着色器和片段着色器与之连接

断开着色器和程序

1
void glDetachShader(GLuint program, GLuint shader)

链接着色器和程序

1
2
// Link the program
glLinkProgram ( programObject );

上述的工作都已经完成了之后需要链接程序的对象了。连接操作负责生成最终的可执行程序。在连接的时候将检查各种对象的数量,确保可以链接成功。

检测链接是否成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Check the link status
glGetProgramiv ( programObject, GL_LINK_STATUS, &linked );
if ( !linked )
{
GLint infoLen = 0;
glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen );
if ( infoLen > 1 )
{
char *infoLog = malloc ( sizeof ( char ) * infoLen );
glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog );
esLogMessage ( "Error linking program:\n%s\n", infoLog );
free ( infoLog );
}
glDeleteProgram ( programObject );
return FALSE;
}

设置程序对象为活动对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Draw ( ESContext *esContext )
{
UserData *userData = esContext->userData;
GLfloat vVertices[] = { 0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
// Set the viewport
glViewport ( 0, 0, esContext->width, esContext->height );
// Clear the color buffer
glClear ( GL_COLOR_BUFFER_BIT );
// Use the program object
glUseProgram ( userData->programObject );
// Load the vertex data
glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices );
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0, 3 );
}

RTMP推流流程详解

发表于 2017-03-24   |  

通过抓取主播端的rtmp数据包,我们能清晰的看到整个从握手到推流的过程,然后我们来分析一下每一个包的内容吧。

三次握手

handshake c0+c1

具体数据格式可以参考我的这一片博客

具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
//c0
char c0Byte = 0x03;//rtmp版本号
NSData *c0 = [NSData dataWithBytes:&c0Byte length:1];
[self writeData:c0];
//c1
uint8_t *c1Bytes = (uint8_t *)malloc(kRtmpSignatureSize);
memset(c1Bytes, 0, 4 + 4);
NSData *c1 = [NSData dataWithBytes:c1Bytes length:kRtmpSignatureSize];
free(c1Bytes);
[self writeData:c1];

handshake c2

代码实现:

1
2
3
4
5
6
NSData *s1 = [self.handShake subdataWithRange:NSMakeRange(0, kRtmpSignatureSize)];
//c2
uint8_t *s1Bytes = (uint8_t *)s1.bytes;
memset(s1Bytes + 4, 0, 4);
NSData *c2 = [NSData dataWithBytes:s1Bytes length:s1.length];
[self writeData:c2];

handshake s0s1s2

这个数据是服务器那边发送过来的 需要我们本地保存一份。

协议消息

流程如下

connect

当客户端检测到收到s2的消息后,就可以发送‘connect’了。

看看connect里面的具体数据

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)sendConnectPacket{
NSLog(@"sendConnectPacket");
// AMF格式
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_INVOKE;
NSString *url;
NSMutableData *buff = [NSMutableData data];
if (_url.port > 0) {
url = [NSString stringWithFormat:@"%@://%@:%zd/%@",_url.scheme,_url.host,_url.port,_url.app];
}else{
url = [NSString stringWithFormat:@"%@://%@/%@",_url.scheme,_url.host,_url.app];
}
[buff appendString:@"connect"];
[buff appendDouble:++_numOfInvokes];
self.trackedCommands[@(_numOfInvokes)] = @"connect";
[buff appendByte:kAMFObject];
[buff putKey:@"app" stringValue:_url.app];
[buff putKey:@"type" stringValue:@"nonprivate"];
[buff putKey:@"tcUrl" stringValue:url];
[buff putKey:@"fpad" boolValue:NO];//是否使用代理
[buff putKey:@"capabilities" doubleValue:15.];
[buff putKey:@"audioCodecs" doubleValue:10.];
[buff putKey:@"videoCodecs" doubleValue:7.];
[buff putKey:@"videoFunction" doubleValue:1.];
[buff appendByte16:0];
[buff appendByte:kAMFObjectEnd];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

Window Acknowledgment size

发送端在接收到接受端返回的两个ACK间最多可以发送的字节数

Set Peer Bandwidth && Set Chunk Size && AFM0 Command _result

Set Peer Bandwidth

限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。

Set Chunk Size

这里为默认值128个字节。

AFM0 Command _result(‘NetConnection.Connect.Success’)

这个是上面发送的connect消息的回应消息,消息类型为AFM0 0x14,从返回的code和describtion可以看出,connect已经成功。

接下来客户端需要发送 releaseStream , FCPublish 和 CreateStream 消息

releaseStream 消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)sendReleaseStream{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_NOTIFY;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"releaseStream"];
[buff appendDouble:++_numOfInvokes];
self.trackedCommands[@(_numOfInvokes)] = @"releaseStream";
[buff appendByte:kAMFNull];
[buff appendString:_url.playPath];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

FCPublish消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)sendFCPublish{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_NOTIFY;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"FCPublish"];
[buff appendDouble:(++_numOfInvokes)];
self.trackedCommands[@(_numOfInvokes)] = @"FCPublish";
[buff appendByte:kAMFNull];
[buff appendString:_url.playPath];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

createStream消息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)sendCreateStream{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDInvoke;
metadata.msg_type_id = LLYMSGTypeID_INVOKE;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"createStream"];
self.trackedCommands[@(++_numOfInvokes)] = @"createStream";
[buff appendDouble:_numOfInvokes];
[buff appendByte:kAMFNull];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

之后客户端会收到startStream的回应消息。

startStream result

在收到这个消息后,就可以准备推流了。此时 客户端需要发送一个publish消息。

publish

这个消息告诉服务器,我要准备开始推流了,之后等待服务器给一个可以开始推流的回应。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)sendPublish{
RTMPChunk_0 metadata = {0};
metadata.msg_stream_id = LLYStreamIDAudio;
metadata.msg_type_id = LLYMSGTypeID_INVOKE;
NSMutableData *buff = [NSMutableData data];
[buff appendString:@"publish"];
[buff appendDouble:++_numOfInvokes];
self.trackedCommands[@(_numOfInvokes)] = @"publish";
[buff appendByte:kAMFNull];
[buff appendString:_url.playPath];
[buff appendString:@"live"];
metadata.msg_length.data = (int)buff.length;
[self sendPacket:buff :metadata];
}

onStatus(‘NetStream.Publish.Start’)

客户端收到这个消息,表示服务器已经准备好接收流数据,客户端可以正式开始推流。

参考文章和demo

使用WireShark抓取RTMP协议包

发表于 2017-03-21   |  

前言

最近在学习rtmp协议,虽然文档上对rtmp的握手过程和协议结构都讲得很清楚,但不看到实实在在的数据,心里还是没有底啊,所以今天尝试抓个包看看,因为我们正在做一个直播项目,所以想抓主播或者观众的包都是很容易的哈。

WireShark + XQuartz

升级到最新版的wireShark(Version 2.2.3)
升级到最新版的XQuartz(2.7.11)

监听手机网卡

获取到手机的udid,然后再命令行执行下面的命令:

1
rvictl -s udid

然后wireshark首页网卡列表会显示当前手机的网卡,如下图:

双击进入,界面上应该就有手机当前的一些网络包了

添加rtmp过滤器

因为我们只需要看rtmp相关数据包,所以我们新建一个过滤器,wireshark提供了方便的入口

点击上图右上角的表达式按钮,进入新建过滤器界面

然后搜索rtmpt 这个要多一个t 因为rtmp字段被另外一个协议占用了。

然后选中当前搜索结果,点击OK,过滤器就添加好了。

开始抓包

一切准备完毕,现在可以开始抓包了。

主播端

新建一个直播间,正常直播就可以了。下面是主播端的抓包数据

可以清楚的看到握手的过程和每个数据包。

观众端

点开一个正在直播的直播间。

可以看到,观众在握手成功后,向服务器发送了个play的指令。这也解惑了我之前的观众端如何开始拉流的疑问。

只要愿意动手,抓包原来很简单。

–end–

RTMP协议学习笔记

发表于 2017-03-20   |  

前言

RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。随着VR技术的发展,视频直播等领域逐渐活跃起来,RTMP作为业内广泛使用的协议也重新被相关开发者重视起来。

rtmp协议简介

RTMP协议是应用层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接,在Connection链接上会传输一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。

建立rtmp连接(三次握手)

一个RTMP连接以握手开始。这里的握手和其他协议的握手不一样。这里的握手由三个固定大小的块组成,而不是可变大小的块加上头。

客户端(发起连接的一方)和服务端各自发送三个相同的块。这些块如果是客户端发送的话记为C0,C1和C2,如果是服务端发送的话记为S0,S1和S2。

握手队列

握手开始于客户端发送C0,C1块。

在发送C2之前客户端必须等待接收S1 。

在发送任何数据之前客户端必须等待接收S2。

服务端在发送S0和S1之前必须等待接收C0,也可以等待接收C1。

服务端在发送S2之前必须等待接收C1。

服务端在发送任何数据之前必须等待接收C2。

C0/S0 消息格式

C0 和 S0是单独的一个字节,表示当前选择的RTMP版本

我现在看的资料当前rtmp版本是3。0-2是早期产品所用的,已被丢弃;4-31保留在未来使用 ;32-255不允许使用 (为了区分其他以某一字符开始的文本协议)。如果服务无法识别客户端请求的版本,应该返回3 。客户端可以选择减到版本3或选择取消握手。

C1/S1 消息格式

C1和S1消息有1536字节长,由上面字段组成

时间:4字节,本字段包含时间戳。该时间戳应该是发送这个数据块的端点的后续块的时间起始点。可以是0,或其他的任何值。为了同步多个流,端点可能发送其块流的当前值。

零:4字节,本字段必须是全零。

随机数据:1528字节,本字段可以包含任何值。因为每个端点必须用自己初始化的握手和对端初始化的握手来区分身份,所以这个数据应有充分的随机性。但是并不需要加密安全的随机值,或者动态值。

C2/S2 消息格式

C2和S2消息有1536字节长。是S1和C1的回复。本消息由以上字段组成。

时间:4字节,本字段必须包含对等段发送的时间(对C2来说是S1,对S2来说是C1)。

时间2:4字节,本字段必须包含先前发送的并被对端读取的包的时间戳。

随机回复:1528字节,本字段必须包含对端发送的随机数据字段(对C2来说是S1,对S2来说是C1)。

每个对等端可以用时间和时间2字段中的时间戳来快速地估计带宽和延迟。但这样做可能并不实用。

握手示意图

流程和状态的对应表

Message分块(Chunking)

RTMP在收发数据的时候并不是以Message为单位的,而是把Message拆分成Chunk发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中带有MessageID代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。

为什么RTMP要将Message拆分成不同的Chunk呢?通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的Message,可以通过对Chunk Header的字段来压缩信息,从而减少信息的传输量。

Chunk的默认大小是128字节,在传输过程中,通过一个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大大小。大一点的Chunk减少了计算每个chunk的时间从而减少了CPU的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的Chunk可以减少这种阻塞问题,但小的Chunk会引入过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。在实际发送时应对要发送的数据用不同的Chunk Size去尝试,通过抓包分析等手段得出合适的Chunk大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整Chunk的大小,从而尽量提高CPU的利用率并减少信息的阻塞机率。

具体Chunk格式如下:

Basic Header (基本的头信息)

包含了chunk stream ID(流通道Id)和chunk type(chunk的类型),chunk stream id一般被简写为CSID,用来唯一标识一个特定的流通道,chunk type决定了后面Message Header的格式。Basic Header的长度可能是1,2,或3个字节,其中chunk type的长度是固定的(占2位,注意单位是位,bit),Basic Header的长度取决于CSID的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入Header增加的数据量。

RTMP协议支持用户自定义[3,65599]之间的CSID,0,1,2由协议保留表示特殊信息。0代表Basic Header总共要占用2个字节,CSID在[64,319]之间,1代表占用3个字节,CSID在[64,65599]之间,2代表该chunk是控制信息和一些命令信息,后面会有详细的介绍。
chunk type的长度固定为2位,因此CSID的长度是(6=8-2)、(14=16-2)、(22=24-2)中的一个。

当Basic Header为1个字节时,CSID占6位,6位最多可以表示64个数,因此这种情况下CSID在[0,63]之间,其中用户可自定义的范围为[3,63]。格式如下图所示:

当Basic Header为2个字节时,CSID占14位,此时协议将chunk type所在字节的其他位都置为0,剩下的一个字节来表示CSID - 64,这样共有14位存储CSID,8位可以表示[0,255]共256个数,因此这种情况下CSID在[64,319],其中319=255+64。

当Basic Header为3个字节时,CSID占22位,此时协议将[2,8]位置为1,余下的16个位表示CSID-64,这样共有16个位来存储CSID,16位可以表示[0,65535]共65536个数,因此这种情况下CSID在[64,65599],其中65599=65535+64,需要注意的是,Basic Header是采用小端存储的方式,越往后的字节数量级越高,因此通过这3个字节每一位的值来计算CSID时,应该是:[第三个字节的值] * 256 + [第二个字节的值] + 64

可以看到2个字节和3个字节的Basic Header所能表示的CSID是有交集的[64,319],但实际实现时还是应该秉着最少字节的原则使用2个字节的表示方式来表示[64,319]的CSID。

Message Header(消息的头信息)

包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type,共有4种不同的格式,由上面所提到的Basic Header中的fmt字段控制。其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。以下按照字节数从多到少的顺序分别介绍这4种格式的Message Header。

Type=0:

type=0时Message Header占用11个字节,其他三种能表示的数据它都能表示,但在chunk stream的开始的第一个chunk和头信息中的时间戳后退(即值与上一个chunk相比减小,通常在回退播放的时候会出现这种情况)的时候必须采用这种格式。

timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF=2^24-1, 当它的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到Extended Timestamp字段中,接受端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析实际的时间戳。

message length(消息数据的长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总数据长度,而不是chunk本身Data的数据的长度。

message type id(消息的类型id):占用1个字节,表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。
msg stream id(消息的流id):占用4个字节,表示该chunk所在的流的ID,和Basic Header的CSID一样,它采用小端存储的方式,

Type = 1:

type=1时Message Header占用7个字节,省去了表示msg stream id的4个字节,表示此chunk和上一次发的chunk所在的流相同,如果在发送端只和对端有一个流链接的时候可以尽量去采取这种格式。

timestamp delta:占用3个字节,注意这里和type=0时不同,存储的是和上一个chunk的时间差。类似上面提到的timestamp,当它的值超过3个字节所能表示的最大值时,三个字节都置为1,实际的时间戳差值就会转存到Extended Timestamp字段中,接受端在判断timestamp delta字段24个位都为1时就会去Extended timestamp中解析时机的与上次时间戳的差值。

Type = 2:

type=2时Message Header占用3个字节,相对于type=1格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此chunk和上一次发送的chunk所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示timestamp delta,使用同type=1

Type = 3

0字节!!!好吧,它表示这个chunk的Message Header和上一个是完全相同的,自然就不用再传输一遍了。

当它跟在Type=0的chunk后面时,表示和前一个chunk的时间戳都是相同的。什么时候连时间戳都相同呢?就是一个Message拆分成了多个chunk,这个chunk和上一个chunk同属于一个Message。

而当它跟在Type=1或者Type=2的chunk后面时,表示和前一个chunk的时间戳的差是相同的。比如第一个chunk的Type=0,timestamp=100,第二个chunk的Type=2,timestamp delta=20,表示时间戳为100+20=120,第三个chunk的Type=3,表示timestamp delta=20,时间戳为120+20=140

Extended Timestamp(扩展时间戳):

上面我们提到在chunk中会有时间戳timestamp和时间戳差timestamp delta,并且它们不会同时存在,只有这两者之一大于3个字节能表示的最大数值0xFFFFFF=16777215时,才会用这个字段来表示真正的时间戳,否则这个字段为0。扩展时间戳占4个字节,能表示的最大数值就是0xFFFFFFFF=4294967295。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。

Chunk Data(块数据)

用户层面上真正想要发送的与协议无关的数据,长度在(0,chunkSize]之间

chunk示例

首先包含第一个Message的chunk的Chunk Type为0,因为它没有前面可参考的chunk,timestamp为1000,表示时间戳。type为0的header占用11个字节,假定chunkstreamId为3<64,因此Basic Header占用1个字节,再加上Data的32个字节,因此第一个chunk共44=11+1+32个字节。

第二个chunk和第一个chunk的CSID,TypeId,Data的长度都相同,因此采用Chunk Type=2,timestamp delta=1020-1000=20,因此第二个chunk占用36=3+1+32个字节。

第三个chunk和第二个chunk的CSID,TypeId,Data的长度和时间戳差都相同,因此采用Chunk Type=3省去全部Message Header的信息,占用33=1+32个字节。

第四个chunk和第三个chunk情况相同,也占用33=1+32个字节。

最后实际发送的chunk如下:

chunk示例2

注意到Data的Length=307>128,因此这个Message要切分成几个chunk发送.

第一个chunk的Type=0,Timestamp=1000,承担128个字节的Data,因此共占用140=11+1+128个字节。

第二个chunk也要发送128个字节,其他字段也同第一个chunk,因此采用Chunk Type=3,此时时间戳也为1000,共占用129=1+128个字节。

第三个chunk要发送的Data的长度为307-128-128=51个字节,还是采用Type=3,共占用1+51=52个字节。

最后实际发送的chunk如下:

协议控制消息(Protocol Control Message)

在RTMP的chunk流会用一些特殊的值来代表协议的控制消息,它们的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1,2,3,5,6,具体代表的消息会在下面依次说明。控制消息的接受端会忽略掉chunk中的时间戳,收到后立即生效。

Set Chunk Size(Message Type ID=1)

设置chunk中Data字段所能承载的最大字节数,默认为128B,通信过程中可以通过发送该消息来设置chunk Size的大小(不得小于128B),而且通信双方会各自维护一个chunkSize,两端的chunkSize是独立的。比如当A想向B发送一个200B的Message,但默认的chunkSize是128B,因此就要将该消息拆分为Data分别为128B和72B的两个chunk发送,如果此时先发送一个设置chunkSize为256B的消息,再发送Data为200B的chunk,本地不再划分Message,B接受到Set Chunk Size的协议控制消息时会调整的接受的chunk的Data的大小,也不用再将两个chunk组成为一个Message。

Abort Message(Message Type ID=2)

当一个Message被切分为多个chunk,接受端只接收到了部分chunk时,发送该控制消息表示发送端不再传输同Message的chunk,接受端接收到这个消息后要丢弃这些不完整的chunk。Data数据中只需要一个CSID,表示丢弃该CSID的所有已接收到的chunk。

Acknowledgement(Message Type ID=3)

当收到对端的消息大小等于窗口大小(Window Size)时接受端要回馈一个ACK给发送端告知对方可以继续发送数据。窗口大小就是指收到接受端返回的ACK前最多可以发送的字节数量,返回的ACK中会带有从发送上一个ACK后接收到的字节数。

Window Acknowledgement Size(Message Type ID=5)

发送端在接收到接受端返回的两个ACK间最多可以发送的字节数。

Set Peer Bandwidth(Message Type ID=6)

限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。

Hard(Limit Type=0)接受端应该将Window Ack Size设置为消息中的值

Soft(Limit Type=1)接受端可以将Window Ack Size设为消息中的值,也可以保存原来的值(前提是原来的Size小与该控制消息中的Window Ack Size)

Dynamic(Limit Type=2)如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size。

不同类型的RTMP Message

Command Message(命令消息,Message Type ID=17或20)

表示在客户端和服务器间传递的在对端执行某些操作的命令消息。如
connect表示连接对端,对端如果同意连接的话会记录发送端信息并返回连接成功消息。publish表示开始向对方推流,接受端接到命令后准备好接受对端发送的流信息,下面会对比较常见的Command Message具体介绍。当信息使用AMF0编码时,Message Type ID=20,AMF3编码时Message Type ID=17.

发送端发送时会带有命令的名字,如connect,TransactionID表示此次命令的标识,Command Object表示相关参数。接受端收到命令后,会返回以下三种消息中的一种:_result 消息表示接受该命令,对端可以继续往下执行流程,_error消息代表拒绝该命令要执行的操作,method name消息代表要在之前命令的发送端执行的函数名称。这三种回应的消息都要带有收到的命令消息中的Transaction Id来表示本次的回应作用于哪个命令。

可以认为发送命令消息的对象有两种,一种是NetConnection,表示双端的上层连接,一种是NetStream,表示流信息的传输通道,控制流信息的状态,如Play播放流,Pause暂停。

NetConnection Commands(连接层的命令)

用来管理双端之间的连接状态,同时也提供了异步远程方法调用(RPC)在对端执行某方法,以下是常见的连接层的命令:

  • connect:用于客户端向服务器发送连接请求

  • Call:用于在对端执行某函数,即常说的RPC:远程进程调用

  • Create Stream:创建传递具体信息的通道,从而可以在这个流中传递具体信息,传输信息单元为Chunk

NetStream Commands(流连接上的命令)

Netstream建立在NetConnection之上,通过NetConnection的createStream命令创建,用于传输具体的音频、视频等信息。在传输层协议之上只能连接一个NetConnection,但一个NetConnection可以建立多个NetStream来建立不同的流通道传输数据。

以下会列出一些常用的NetStream Commands,服务端收到命令后会通过onStatus的命令来响应客户端,表示当前NetStream的状态。

  • play(播放):由客户端向服务器发起请求从服务器端接受数据(如果传输的信息是视频的话就是请求开始播流),可以多次调用,这样本地就会形成一组数据流的接收者。注意其中有一个reset字段,表示是覆盖之前的播流(设为true)还是重新开始一路播放(设为false)。

  • play2(播放):和上面的play命令不同的是,play2命令可以将当前正在播放的流切换到同样数据但不同比特率的流上,服务器端会维护多种比特率的文件来供客户端使用play2命令来切换。

  • deleteStream(删除流):用于客户端告知服务器端本地的某个流对象已被删除,不需要再传输此路流。

  • receiveAudio(接收音频):通知服务器端该客户端是否要发送音频

  • receiveVideo(接收视频):通知服务器端该客户端是否要发送视频

  • publish(推送数据):由客户端向服务器发起请求推流到服务器。

  • seek(定位流的位置):定位到视频或音频的某个位置,以毫秒为单位。

  • pause(暂停):客户端告知服务端停止或恢复播放。
    如果Pause为true即表示客户端请求暂停的话,服务端暂停对应的流会返回NetStream.Pause.Notify的onStatus命令来告知客户端当前流处于暂停的状态,当Pause为false时,服务端会返回NetStream.Unpause.Notify的命令来告知客户端当前流恢复。如果服务端对该命令响应失败,返回_error信息。

Data Message(数据消息,Message Type ID=15或18)

传递一些元数据(MetaData,比如视频名,分辨率等等)或者用户自定义的一些消息。当信息使用AMF0编码时,Message Type ID=18,AMF3编码时Message Type ID=15.

Shared Object Message(共享消息,Message Type ID=16或19)

表示一个Flash类型的对象,由键值对的集合组成,用于多客户端,多实例时使用。当信息使用AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16.

Audio Message(音频信息,Message Type ID=8):音频数据。

Video Message(视频信息,Message Type ID=9):视频数据。

Aggregate Message (聚集信息,Message Type ID=22):多个RTMP子消息的集合

User Control Message Events(用户控制消息,Message Type ID=4)

告知对方执行该信息中包含的用户控制事件,比如Stream Begin事件告知对方流信息开始传输。和前面提到的协议控制信息(Protocol Control Message)不同,这是在RTMP协议层的,而不是在RTMP chunk流协议层的,这个很容易弄混。该信息在chunk流中发送时,Message Stream ID=0,Chunk Stream Id=2,Message Type Id=4。

RTMP 推流和播放流程图

推流

命令执行过程中的消息流是:

  • 1.客户端发送连接命令到服务端,请求与一个服务应用实例建立连接。

  • 2.接收到连接命令后,服务端发送”窗口确认(致谢)消息”到客户端。服务端同时连接到连接命令中提到的应用。

  • 3.服务端发送”设置带宽”协议消息到客户端。

  • 4.在处理完”设置带宽”消息后,客户端发送”窗口确认(致谢)大小”消息到服务端。

  • 5.服务端发送用户控制消息中的流开始消息到客户端。

  • 6.服务端发送结果命令消息通知客户端连接状态。该命令指定传输ID(对于连接命令总是1)。同时还指定一些属性,例如,Flash media server 版本(字符串),能力(数字),以及其他的连接信息,例如,层(字符串),代码(字符串),描述(字符串),对象编码(数字)等。

播放

命令执行过程中的消息流是:

  • 1.客户端发送连接命令到服务端,请求与一个服务应用实例建立连接。

  • 2.接收到连接命令后,服务端发送”窗口确认(致谢)消息”到客户端。服务端同时连接到连接命令中提到的应用。

  • 3.服务端发送”设置带宽”协议消息到客户端。

  • 4.在处理完”设置带宽”消息后,客户端发送”窗口确认(致谢)大小”消息到服务端。

  • 5.服务端发送用户控制消息中的流开始消息到客户端。

  • 6.服务端发送结果命令消息通知客户端连接状态。该命令指定传输ID(对于连接命令总是1)。同时还指定一些属性,例如,Flash media server 版本(字符串),能力(数字),以及其他的连接信息,例如,层(字符串),代码(字符串),描述(字符串),对象编码(数字)等。

H.264文件结构学习笔记

发表于 2017-03-16   |  

简介

相关研究显示 H.264/AVC 与 MPEG-2 及 MPEG-4 相较之下,无论是压缩率或视讯质量皆有大幅的提升,而且H.264/AVC也首次将 视讯编码层(Video Coding Layer,VCL)与网络提取层(Network Abstraction Layer,NAL)的概念涵盖进来,以往视讯标准着重的是压缩效能部分,而H.264/AVC包含一个内建的NAL网络协议适应层,即由NAL来提供网络的状态,可以让VCL有更好的编译码弹性与纠错能力,使得H.264/AVC非常适用于多媒体串流(multimedia streaming)及行动电视(mobile TV)的相关应用

编码核心思想

在编码方面,我理解的他的理论依据是:参照一段时间内图像的统计结果表明,在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内。所以对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小!B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。这段图像我们称为一个序列(序列就是有相同特点的一段数据),当某个图像与之前的图像变化很大,无法参考前面的帧来生成,那我们就结束上一个序列,开始下一段序列,也就是对这个图像生成一个完整帧A1,随后的图像就参考A1生成,只写入与A1的差别内容。

在H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。

H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。

关于序列的说明

在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。

一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像,但不是所有I帧图像都是IDR图像。H.264 引入 IDR 图像是为了解码的同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。

一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。

IPB帧

I帧

1.它是一个全帧压缩编码帧。它将全帧图像信息进行JPEG压缩编码及传输;

2.解码时仅用I帧的数据就可重构完整图像;

3.I帧描述了图像背景和运动主体的详情;

4.I帧不需要参考其他画面而生成;

5.I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);

6.I帧是帧组GOP(既一个帧序列)的基础帧(第一帧),在一组中只有一个I帧;

7.I帧不需要考虑运动矢量;

8.I帧所占数据的信息量比较大

P帧

前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)

P帧的预测与重构:P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。

1.P帧是I帧后面相隔1~2帧的编码帧;

2.P帧采用运动补偿的方法传送它与前面的I或P帧的差值及运动矢量(预测误差);

3.解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;

4.P帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧;

5.P帧可以是其后面P帧的参考帧,也可以是其前后的B帧的参考帧;

6.由于P帧是参考帧,它可能造成解码错误的扩散;

7.由于是差值传送,P帧的压缩比较高。

B帧

双向预测内插编码帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况,但我这样说简单些),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。

B帧以前面的I或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。

1.B帧是由前面的I或P帧和后面的P帧来进行预测的;

2.B帧传送的是它与前面的I或P帧和后面的P帧之间的预测误差及运动矢量;

3.B帧是双向预测编码帧;

4.B帧压缩比最高,因为它只反映了参考帧间运动主体的变化情况,预测比较准确;

5.B帧不是参考帧,不会造成解码错误的扩散。

注:I、B、P各帧是根据压缩算法的需要,是人为定义的,它们都是实实在在的物理帧。一般来说,I帧的压缩率是7(跟JPG差不多),P帧是20,B帧可以达到50。可见使用B帧能节省大量空间,节省出来的空间可以用来保存多一些I帧,这样在相同码率下,可以提供更好的画质。

压缩方法

h264的压缩方法:

1.分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多。

2.定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧;

3.预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;

4.数据传输:最后将I帧数据与预测的差值信息进行存储和传输。

帧内(Intraframe)压缩也称为空间压缩(Spatial compression)。当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩一般达不到很高的压缩,跟编码jpeg差不多。

帧间(Interframe)压缩的原理是:相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩(Temporal compression),它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。帧差值(Frame differencing)算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。

网络提取层(NAL)

以NAL封包为单位的方式来做为VCL编译码的运算单位,这样传输层拿到NAL封包之后不需要再进行切割,只需附加该传输协议的文件头信息(adding header only)就可以交由底层传送出去。

如图所示,可以将NAL当成是一个专作封装(packaging)的模块,用来将VCL压缩过的bitstream封装成适当大小的封包单位(NAL-unit),并在NAL-unit Header中的NAL-unit Type字段记载此封包的型式,每种型式分别对应到VCL中不同的编解碼工具。NAL另外一个重要的功能为当网络发生壅塞而导致封包错误或接收次序错乱(out-of-order)的状况时,传输层协议会在Reference Flag作设定的动作,接收端的VCL在收到这种NAL封包时,就知道要进行所谓的纠错运算(error concealment),在解压缩的同时也会尝试将错误修正回来

一个完整的H.264/AVC bitstream是由多个NAL-units所组成的,所以此bitstream也称之为NAL unit stream,一个NAL unit stream内可以包含多个压缩视讯序列(coded video sequence),一个单独的压缩视讯序列代表一部视讯影片,而压缩视讯序列又是由多个access units所组成,当接收端收到一个access unit后,可以完整地译码成单张的画面,而每个压缩视讯序列的第一个access unit必须为Instantaneous Decoding Refresh (IDR) access unit,IDRaccess unit的内容全是采用intra-prediction编码,所以自己本身即可完全译码,不用参考其他access unit的数据。access unit亦是由多个NAL-units所组成,标准中总共规范12种的NAL-unit型式,这些可以进一步分类成VCL NAL-unit及non-VCL NAL-unit,所谓的VCL NAL-unit纯粹是压缩影像的内容,而所谓的non-VCL NAL-unit则有两种:Parameter Sets与Supplemental Enhancement Information (SEI),SEI可以存放影片简介、版权宣告、用户自行定义的数据…等;Parameter Sets主要是描述整个压缩视讯序列的参数,例如:长宽比例、影像显现的时间点(timestamp)、相关译码所需的参数…等,这些信息非常重要,万一在传送的过程中发生错误,会导致整段影片无法译码,以往像MPEG-2/-4都把这些信息放在一般的packet header,所以很容易随着packet loss而消失,现在H.264/AVC将这些信息独立出来成为特殊的parameter set,可以采用所谓的out-of-band的方式来传送,以便将out-of-band channel用最高层级的信道编码(channel coding)保护机制,来保证传输的正确性。

总结:

第一层是多个序列帧(多个GOP)。

第二层是每个序列帧中具体的IPB帧。

第三层是每一帧具体的数据(NALU)。

NALU

NAL单元格式:

NAL头 + RBSP

RBSP:封装于网络抽象单元的数据称之为原始字节序列载荷RBSP,它是NAL的基本传输单元。其中,RBSP又分为视频编码数据和控制数据。其基本结构是:在原始编码数据的后面填加了结尾比特。一个bit“1”若干比特“0”,以便字节对齐。

RBSP的类型:

RBSP 类型之一 PS: 包括序列参数集 SPS 和 图像参数集 PPS

SPS:包含的是针对一连续编码视频序列的参数,如标识符 seq_parameter_set_id、帧数及 POC 的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等等。

PPS:对应的是一个序列中某一幅图像或者某几幅图像,其参数如标识符 pic_parameter_set_id、可选的 seq_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等等。

NALU类型

标识NAL单元中的RBSP数据类型,其中,nal_unit_type为1, 2, 3, 4, 5及12的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。

0:未规定

1:非IDR图像中不采用数据划分的片段

2:非IDR图像中A类数据划分片段

3:非IDR图像中B类数据划分片段

4:非IDR图像中C类数据划分片段

5:IDR图像的片段

6:补充增强信息 (SEI)

7:序列参数集

8:图像参数集

9:分割符

10:序列结束符

11:流结束符

12:填充数据

13 – 23:保留

24 – 31:未规定

视讯编码层(VCL)

视频压缩的原理是利用影像在时间与空间上存有相似性,这些相似的数据经过压缩算法处理之后,可以将人眼无法感知的部分抽离出来,这些称为视觉冗余(visual redundancy)的部分在去除之后,就可以达到视频压缩的目的。如图1所示,H.264/AVC的视讯编码机制是以图块(block-based)为基础单元,也就是说先将整张影像分割成许多矩形的小区域,称之为巨图块(macroblock,MB),再将这些巨图块进行编码,先使用画面内预测(intra-prediction)与画面间预测(inter-prediction)技术,以去除影像之间的相似性来得到所谓的差余影像(residual),再将差余影像施以空间转换(transform)与量化(quantize)来去除视觉冗余,最后视讯编码层会输出编码过的比特流(bitstream),之后再包装成网络提取层的单元封包(NAL-unit),经由网络传送到远程或储存在储存媒体中。

H.264/AVC影像格式阶层架构

H.264/AVC的阶层架构由小到大依序是sub-block、block、macroblock、slice、slicegroup、frame/field-picture、sequence。对一个采用4:2:0取样的MB而言,它是由16x16点的Luma与相对应的2个8x8点Chroma来组成,而在H.264/AVC的规范中,MB可再分割成多个16x8、8x16、8x8、8x4、4x8、4x4格式的sub-blocks。所谓的slice是许多MB的集合,而一张影像是由许多slice所组成(图3),slice为H.264/AVC格式中的最小可译码单位(self-decodable unit),也就是说一个slice单靠本身的压缩数据就能译码,而不必依靠其他slice,这样的好处是当传送到远程时,每接收完一笔slice的压缩数据就能马上译码,不用等待整张的数据接收完后才能开始,而且万一传送的过程中发生数据遗失或错误,也只是影响该笔slice,不会对其他slice有所影响,但跟MPEG-2的slice不同处在于它允许slice的范围可以超过一行MB,也就是说H.264/AVC允许整张影像只由单一个slice组成。

Slice的编码模式(IPB帧)

H.264/AVC的slice依照编码的类型可以分成下列种类:(1)I-slice: slice的全部MB都采用intra-prediction的方式来编码;(2) P-slice: slice中的MB使用intra-prediction和inter-prediction的方式来编码,但每一个inter-prediction block最多只能使用一个移动向量;(3) B-slice:与P-slice类似,但每一个inter-prediction block可以使用二个移动向量。比较特别的是B-slice的‘B’是指Bi-predictive,与MPEG-2/-4 B-frame的Bi-directional概念有很大的不同,MPEG-2/-4 B-frame被限定只能由前一张和后一张的I(或P)-frame来做inter- prediction,但是H.264/AVC B-slice除了可由前一张和后一张影像的I(或P、B)-slice外,也能从前二张不同影像的I(或P、B)-slice来做inter- prediction,

而H.264/AVC另外增加两种特殊slice类型:

(1) SP-slice:即所谓的Switching P slice,为P-slice的一种特殊类型,用来串接两个不同bitrate的bitstream;

(2) SI-slice: 即所谓的Switching I slice,为I-slice的一种特殊类型,除了用来串接两个不同content的bitstream外,也可用来执行随机存取(random access)来达到网络VCR的功能。

这两种特殊的slice主要是考虑当进行Video-On-Demand streaming的应用时,对同一个视讯内容的影片来说,server会预先存放不同bitrate的压缩影片,而当带宽改变时,server就会送出适合当时带宽比特率的影片,传统的做法是需要等到适当的时间点来传送新的I-slice (容量较P-slice大上许多),但因为带宽变小导致需要较多的时间来传送I-slice,如此会让client端的影像有所延迟,为了让相同content但不同bitrate的bitstream可以较平顺地串接,使用SP-slice会很容易来达成(图4),不仅可以直接送出新的bitstream,也因为传送的P-slice的容量较小,所以不会有时间延迟的情形出现

SP-slice

SI-slice

画面内预测技术(Intra-frame Prediction)-帧内压缩

以往的压缩标准在进行intra-prediction时,多半只是将转换系数做差值编码,而H.264/AVC在空间领域(spatial domain)来进行像点之间的预测,而不是用转换过的系数,它提供两种intra-prediction的型式:intra_4x4及intra_16x16,所谓的intra_4x4是以Luma 4x4 sub-block为单位,找出它的参考对象(predictor)后,再将其与参考对象相减后所产生的差余影像(residual)送入转换算法,而寻找参考对象的模式共有9种预测的方向(上图),以mode 0 (vertical)为例,{a,e,i,m}、{b,f,j,n}、{c,g,k,o}、{d,h,l,p}的参考对象分别为A、B、C、D;Luma intra_16x16与Chroma的模式跟Luma intra_4x4类似

画面间预测技术(Inter-frame Prediction)-帧间压缩

至于横跨每张画面之间的预测技术,H.264/AVC提供了更丰富的编码模式,计有下述几种区块分割(partition)的方法:16x16、16x8、8x16、8x8、8x4、4x8、4x4,多样的分割方式可以让移动向量的预测更准确,如上图所示,画面中有些移动的区域并不是正方形,使用长方形或较小的4x4分割来做预测的区域,可以大幅降低差余影像的数值来增加了压缩比,但也因此P-slice中的MB最多可有16个移动向量(motion vector),而B-slice中的MB最多可拥有32个移动向量,虽然这些会增加移动向量档头(header)的容量,但整体来说对压缩比仍有正面的益处。

以往的压缩标准所使用的动态估测(motion estimation),只有使用前一张图像来作为预测的对象,H.264/AVC提供了多重参考图像(multiple reference frames)的概念,使得移动向量不再只限于前后相邻的影像,而是可以跨过多张影像,如上图所示,在时间点t的图块,可以使用t-1到t-2图像中的图块来作为预测的对象,当影片有周期重复性的内容时,例如:背景影像周期性的出现或被遮盖、对象有来回跳动的行为、形状忽大忽小,或是摄影机在拍摄时,因为有多处的取景点,并且摄影画面在取景点之间来回移动,这种情形在球类比赛转播时常出现,这些状况都能得到较好的动态预测结果,因而提高了压缩的效能。

iOS视频硬编码

发表于 2017-03-14   |  

概述

iOS8之后 系统提供了VideoToolBox用来处理音频和视频的编解码(硬解使用GPU),在iOS8之前,普遍使用的是ffmpeg(软解使用CPU)。

处理流程

1.采集

使用AVFoundation提供的AVCapture系列类自定义一个视频流采集的相机,在采集到数据的回调内处理视频编码。

相机自定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
self.session = [[AVCaptureSession alloc]init];
if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480]) {
self.session.sessionPreset = AVCaptureSessionPreset640x480;
}
self.videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([self.videoDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
if ([self.videoDevice lockForConfiguration:nil]) {
self.videoDevice.focusMode = AVCaptureFocusModeContinuousAutoFocus;
}
}
self.videoInput = [[AVCaptureDeviceInput alloc]initWithDevice:self.videoDevice error:nil];
if ([self.session canAddInput:self.videoInput]) {
[self.session addInput:self.videoInput];
}
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc]init];
//kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 表示原始数据的格式为YUV420
// YUV 4:4:4采样,每一个Y对应一组UV分量。
// YUV 4:2:2采样,每两个Y共用一组UV分量。
// YUV 4:2:0采样,每四个Y共用一组UV分量。
NSDictionary *settings = [[NSDictionary alloc]initWithObjectsAndKeys:[NSNumber numberWithUnsignedInteger:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],kCVPixelBufferPixelFormatTypeKey,nil];
self.videoDataOutput.videoSettings = settings;
self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
_videoOutputQueue = dispatch_queue_create("videoOutputQueue", DISPATCH_QUEUE_SERIAL);
[self.videoDataOutput setSampleBufferDelegate:self queue:_videoOutputQueue];
if ([self.session canAddOutput:self.videoDataOutput]) {
[self.session addOutput:self.videoDataOutput];
}
self.videoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
[self.videoConnection setVideoScaleAndCropFactor:1];
self.videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;

回调处理:

1
2
3
4
5
6
7
8
9
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
__weak typeof(self) weakSelf = self;
// CVPixelBufferRef pixelBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer);
if ([self.delegate respondsToSelector:@selector(videOutputHandler:didOutputSampleBuffer:)]) {
[self.delegate videOutputHandler:weakSelf didOutputSampleBuffer:sampleBuffer];
}
}

sampleBuffer 这个就是视频流的原始数据。

2.开始编码

使用VideoToolBox库的相关API可能很方便的编码上面的sampleBuffer数据.

首先要初始化一个VTCompressionSessionRef对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
OSStatus status = VTCompressionSessionCreate(NULL, _videoConfig.videoSize.width, _videoConfig.videoSize.height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressBuffer, (__bridge void *)self, &_compressionSession);
if(status != noErr){
return;
}
//关键帧间隔 一般为帧率的2倍 间隔越大 压缩比越高
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval,(__bridge CFTypeRef)@(_videoConfig.keyframeInterval));
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration,(__bridge CFTypeRef)@(_videoConfig.keyframeInterval));
//Just remember that kVTCompressionPropertyKey_AverageBitRate takes bits and kVTCompressionPropertyKey_DataRateLimits takes bytes and seconds.
// status = VTSessionSetProperty(session, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(600 * 1024));
// status = VTSessionSetProperty(session, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[800 * 1024 / 8, 1]);
//码率 单位是bit
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(_videoConfig.bitrate * 8));
//码率上限 单位为 byte/s
NSArray *limit = @[@(_videoConfig.bitrate),@(1)];
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(_videoConfig.fps));
// 设置实时编码输出(避免延迟)
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanFalse);
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
//控制是否产生B帧
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
//16:9
VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AspectRatio16x9, kCFBooleanTrue);
VTCompressionSessionPrepareToEncodeFrames(_compressionSession);

开始编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//开始编码
- (void)videoEncodeData:(CVPixelBufferRef)pixelBuffer time:(uint64_t)time{
frameCount++;
//CMTimeMake(a,b) a当前第几帧,b每秒钟多少帧。当前播放时间a/b
CMTime presentationTimeStamp = CMTimeMake(frameCount, 1000);
//每一帧需要播放的时间
VTEncodeInfoFlags flags;
CMTime duration = CMTimeMake(1, _videoConfig.fps);
NSDictionary *properties = nil;
if(frameCount % (int32_t)(self.videoConfig.keyframeInterval) == 0){//强制关键帧
properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @YES};
}
NSNumber *timeNumber = @(time);
OSStatus statusCode = VTCompressionSessionEncodeFrame(_compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge void *)timeNumber, &flags);
if (statusCode != noErr) {
NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
VTCompressionSessionInvalidate(_compressionSession);
CFRelease(_compressionSession);
_compressionSession = NULL;
return;
}
NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
}

编码成功回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//编码完成的回调
static void didCompressBuffer(void *VTref, void *VTFrameRef, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer){
LLYVideoEncode *videoEncode = (__bridge LLYVideoEncode *)VTref;
uint64_t timeStamp = [((__bridge_transfer NSNumber*)VTFrameRef) longLongValue];
//编码后的原始数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
//判断关键帧
BOOL isKeyFrame = NO;
if (attachments != NULL) {
CFDictionaryRef attachment;
CFBooleanRef dependsOnOthers;
attachment = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
dependsOnOthers = (CFBooleanRef)CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
isKeyFrame = (dependsOnOthers == kCFBooleanFalse);
}
//关键帧需要把sps pps信息取出
if (isKeyFrame) {
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
size_t sparameterSetSize, sparameterSetCount;
const uint8_t *sparameterSet;
//sps
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, NULL );
if (statusCode == noErr) {
//pps
size_t pparameterSetSize, pparameterSetCount;
const uint8_t *pparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, NULL );
if (statusCode == noErr) {
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if ([videoEncode.delegate respondsToSelector:@selector(videoEncode:sps:pps:time:)] ) {
[videoEncode.delegate videoEncode:videoEncode sps:sps pps:pps time:timeStamp];
}
}
}
}
//视频数据 不管是不是关键帧都需要取出
//前4个字节表示长度后面的数据的长度
//除了关键帧,其它帧只有一个数据
size_t length, totalLength;
char *dataPointer;
size_t offset = 0;
int const headLen = 4;// 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(blockBuffer, 0, &length, &totalLength, &dataPointer);
if (statusCodeRet == noErr) {
// 循环获取nalu数据
while (offset < totalLength - headLen) {
int NALUnitLength = 0;
memcpy(&NALUnitLength, dataPointer + offset, headLen);
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
NSData *naluData = [NSData dataWithBytes:dataPointer + headLen + offset length:NALUnitLength];
offset += headLen + NALUnitLength;
if ([videoEncode.delegate respondsToSelector:@selector(videoEncode:frame:time:isKeyFrame:)]) {
[videoEncode.delegate videoEncode:videoEncode frame:naluData time:timeStamp isKeyFrame:isKeyFrame];
}
}
}
}

这里是编码流程中比较关键的一环,需要将编码后的数据(H264数据)根据相应的数据格式取出来,用来做进行二次封装(封装为rtmp协议对应格式的数据)进行rtmp传输。不过这几种数据都是固定格式的,所以只要清楚相应的格式后,拆分了封装应该都是能解决的。

3.拆分后的视频数据二次封装为rtmp格式包

sps和pps数据的封装(关键帧数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
- (void)packageKeyFrameSps:(NSData *)spsData pps:(NSData *)ppsData timestamp:(uint64_t)timestamp{
if (spsData.length <= 0 || ppsData <= 0) {
return;
}
if (_hasSendKeyFrame) {
return;
}
_hasSendKeyFrame = YES;
const char *sps = spsData.bytes;
const char *pps = ppsData.bytes;
NSInteger sps_len = spsData.length;
NSInteger pps_len = ppsData.length;
NSInteger total = sps_len + pps_len + 16;
uint8_t *body = (uint8_t *)malloc(total);
int index = 0;
memset(body,0,total);
body[index++] = 0x17;
body[index++] = 0x00;//sps_pps
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x01;
body[index++] = sps[1];
body[index++] = sps[2];
body[index++] = sps[3];
body[index++] = 0xff;
/*sps*/
body[index++] = 0xe1;
body[index++] = (sps_len >> 8) & 0xff;
body[index++] = sps_len & 0xff;
memcpy(&body[index],sps,sps_len);
index += sps_len;
/*pps*/
body[index++] = 0x01;
body[index++] = (pps_len >> 8) & 0xff;
body[index++] = (pps_len) & 0xff;
memcpy(&body[index], pps, pps_len);
index += pps_len;
if ([self.delegate respondsToSelector:@selector(videoPackage:didPacketFrame:)]) {
NSData *data = [NSData dataWithBytes:body length:index];
LLYFrame *frame = [[LLYFrame alloc] init];
frame.data = data;
frame.timestamp = 0;//一定是0
frame.msgLength = (int)data.length;
frame.msgTypeId = LLYMSGTypeID_VIDEO;
frame.msgStreamId = LLYStreamIDVideo;//video
frame.isKeyframe = YES;
[self.delegate videoPackage:self didPacketFrame:frame];
}
}

非关键帧数据的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
- (void)packageFrame:(NSData *)data timestamp:(uint64_t)timestamp isKeyFrame:(BOOL)isKeyFrame{
if (!_hasSendKeyFrame) {
return;
}
NSInteger i = 0;
NSInteger total = data.length + 9;
unsigned char *body = (unsigned char *)malloc(total);
memset(body, 0, total);
if (isKeyFrame) {
body[i++] = 0x17;//1:I Frame 7:AVC
}
else{
body[i++] = 0x27;//2:P Frame 7:AVC
}
body[i++] = 0x01;//AVC NALU 不是psp_pps
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;//pts - dts
//长度数据
body[i++] = (data.length >> 24) & 0xff;
body[i++] = (data.length >> 16) & 0xff;
body[i++] = (data.length >> 8) & 0xff;
body[i++] = (data.length ) & 0xff;
memcpy(&body[i], data.bytes, data.length);
if ([self.delegate respondsToSelector:@selector(videoPackage:didPacketFrame:)]) {
NSData *data = [NSData dataWithBytes:body length:total];
LLYFrame *frame = [[LLYFrame alloc]init];
frame.data = data;
frame.timestamp = (int)timestamp;
frame.msgLength = (int)data.length;
frame.msgTypeId = LLYMSGTypeID_VIDEO;
frame.msgStreamId = LLYStreamIDVideo;
frame.isKeyframe = isKeyFrame;
[self.delegate videoPackage:self didPacketFrame:frame];
}
}

4.建立rtmp通道

先要开始一个socket的通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)connectToServer:(NSString *)host port:(UInt32)port{
if (self.streamStatus > 0) {
[self close];
}
//输入流 用来读取数据
CFReadStreamRef readStream;
//输出流,用来发送数据
CFWriteStreamRef writeStream;
if (port <= 0) {
//RTMP默认端口1935
port = 1935;
}
//建立socket链接
CFStreamCreatePairWithSocketToHost(NULL,(__bridge CFStringRef)host, port, &readStream, &writeStream);
//注意__bridge_transfer,转移对象的内存管理权
_inputStream = (__bridge_transfer NSInputStream *)readStream;
_outputStream = (__bridge_transfer NSOutputStream *)writeStream;
_inputStream.delegate = self;
_outputStream.delegate = self;
[_outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_inputStream open];
[_outputStream open];
}

和服务器建立一个rtmp通信,通过服务器返回的状态码发送相应的握手请求,在3次握手成功后,rtmp通道建立完成,就可以发送封二次封装好的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)streamSession:(LLYStreamSession *)session didChangeStatus:(LLYStreamStatus)streamStatus{
if (streamStatus & NSStreamEventHasBytesAvailable) {//收到数据
[self didReceivedata];
return;//return
}
if (streamStatus & NSStreamEventHasSpaceAvailable){ //可以写数据
if (_rtmpStatus == LLYRtmpSessionStatusConnected) {
[self handshake0];
}
return;//return
}
if ((streamStatus & NSStreamEventOpenCompleted) &&
_rtmpStatus < LLYRtmpSessionStatusConnected) {
self.rtmpStatus = LLYRtmpSessionStatusConnected;
}
if (streamStatus & NSStreamEventErrorOccurred) {
self.rtmpStatus = LLYRtmpSessionStatusError;
}
if (streamStatus & NSStreamEventEndEncountered) {
self.rtmpStatus = LLYRtmpSessionStatusNotConnected;
}
}

5.rtmp推流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Chunk Basic Header: HeaderType+ChannelID组成 1个字节
* >HeaderType(前两bit): 00->12字节 01->8字节
* >ChannelID(后6个bit): 02->Ping和ByteRead通道 03->Invoke通道 connect() publish()和自己写的NetConnection.Call() 04->Audio和Vidio通道
*
* 12字节举例
* Chunk Message Header:timestamp + message_length+message_typ + msg_stream_id
* message_typ :type为1,2,3,5,6的时候是协议控制消息
*
* type为4的时候表示 User Control Messages [Event_type + Event_Data] Event_type有Stream Begin,Stream End...
*
* type为8,音频数据
*
* type为9,视频数据
*
* type为18 元数据消息[AMF0]
*
* type为20 命令消息 Command Message(RPC Message)
* 例如connect, createStream, publish, play, pause on the peer
*
*
*
*/
- (void)sendBuffer:(LLYFrame *)frame{
dispatch_sync(_packageQueue, ^{
uint64_t ts = frame.timestamp;
int streamId = frame.msgStreamId;
NSLog(@"streamId------%d",streamId);
NSNumber *preTimestamp = self.preChunk[@(streamId)];
uint8_t *chunk;
int offset = 0;
if (preTimestamp == nil) {//第一帧,音频或者视频
chunk = malloc(12);
chunk[0] = RTMP_CHUNK_TYPE_0/*0x00*/ | (streamId & 0x1F); //前两个字节 00 表示12字节
offset += 1;
memcpy(chunk+offset, [NSMutableData be24:(uint32_t)ts], 3);
offset += 3;//时间戳3个字节
memcpy(chunk+offset, [NSMutableData be24:frame.msgLength], 3);
offset += 3;//消息长度3个字节
int msgTypeId = frame.msgTypeId;//一个字节的消息类型
memcpy(chunk+offset, &msgTypeId, 1);
offset += 1;
memcpy(chunk+offset, (uint8_t *)&(_streamID), sizeof(_streamID));
offset += sizeof(_streamID);
}else{//不是第一帧
chunk = malloc(8);
chunk[0] = RTMP_CHUNK_TYPE_1/*0x40*/ | (streamId & 0x1F);//前两个字节01表示8字节
offset += 1;
char *temp = [NSMutableData be24:(uint32_t)(ts - preTimestamp.integerValue)];
memcpy(chunk+offset, temp, 3);
offset += 3;
memcpy(chunk+offset, [NSMutableData be24:frame.msgLength], 3);
offset += 3;
int msgTypeId = frame.msgTypeId;
memcpy(chunk+offset, &msgTypeId, 1);
offset += 1;
}
self.preChunk[@(streamId)] = @(ts);
uint8_t *bufferData = (uint8_t *)frame.data.bytes;
uint8_t *outp = (uint8_t *)malloc(frame.data.length + 64);
memcpy(outp, chunk, offset);
free(chunk);
NSUInteger total = frame.data.length;
NSInteger step = MIN(total, _outChunkSize);
memcpy(outp+offset, bufferData, step);
offset += step;
total -= step;
bufferData += step;
while (total > 0) {
step = MIN(total, _outChunkSize);
bufferData[-1] = RTMP_CHUNK_TYPE_3/*0xC0*/ | (streamId & 0x1F);//11表示一个字节,直接跳过这个字节;
memcpy(outp+offset, bufferData - 1, step + 1);
offset += step + 1;
total -= step;
bufferData += step;
}
NSData *tosend = [NSData dataWithBytes:outp length:offset];
free(outp);
[self writeData:tosend];
});
}

如果一切OK 推流地址应该就可以用VLC播放了。

具体代码参考我的 demo.

参考文档1

参考文档2

OpenGL ES 3.0学习笔记-简介

发表于 2017-02-18   |  

可编程管线的各个阶段

1.顶点着色器

顶点着色器实现了顶点操作的通用可编程方法。

顶点着色器的输入包括:

  • 着色器程序–描述顶点上执行操作的顶点着色器程序源代码或者可执行文件。

  • 顶点着色器输入(或者属性)– 用顶点数组提供的每个顶点的数据。

  • 统一变量– 顶点(或者片段)着色器使用的不变数据

  • 采样器–代表顶点着色器使用纹理的特殊统一变量类型

顶点着色器可以用于通过矩阵变换位置,计算照明公式来生成逐顶点颜色以及生成或者变换纹理坐标等基于顶点的传统操作。此外,因为顶点着色器由应用程序规定,所以它可以用于执行自定义计算,实施新的变换,照明或者传统的固定功能管线所不允许的基于顶点的效果。

简单实例

1
2
3
4
5
6
7
8
9
10
11
12
#version 300 es//版本号
uniform mat4 u_mvpMatrix;//变换矩阵
//输入数据
in vec4 a_position;//顶点坐标
in vec4 a_color;//顶点颜色
//输出数据
out vec4 v_color;//顶点颜色
void main()
{
v_color = a_color;
gl_Position = a_position * u_mvpMatrix;
}

2.图元装配

顶点着色器之后,管线的下一个阶段是图元装配。
图元是三角形,直线或者点精灵等几何对象。

对于每个图元,必须确定图元是否位于视锥体(屏幕上可见的3D空间区域)内,如果图元没有完全在视锥体内,则可能需要进行裁剪,如果图元完全处于该区域之外,它就会被抛弃。裁剪之后,顶点位置被转换为屏幕坐标。也可以执行一次淘汰操作,根据图元面向前方或者后方抛弃它们,裁剪和淘汰之后,图元便准备传递给管线的下一个阶段–光栅化阶段。

3.光栅化

光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理。这些二维片段代表着可在屏幕上绘制的像素。

4.片段着色器

片段着色器为片段上的操作实现了通用的可编程方法。
片段着色器的输入:

  • 着色器程序–描述片段上所执行操作的片段着色器程序源代码或者可执行文件。

  • 输入变量–光栅化单元用插值为每个片段生成的顶点着色器输出。

  • 统一变量–片段(或者顶点)着色器使用的不变数据。

  • 采样器–代表片段着色器所用纹理的特殊统一变量类型。

简单实例:

1
2
3
4
5
6
7
8
9
#version 300 es
precision mediump float;//默认的精度限定符
in vec4 v_color;
out vec4 fragColor;
void main()
{
fragColor = v_color;
}

5.逐片段操作

在逐片段操作阶段,在每个片段上执行如下功能:

  • 像素归属测试–这个测试确定帧缓冲区中位置(Xw,Yw)的像素目前是不是归OpenGL ES 所有。

  • 裁剪测试–裁剪测试确定(Xw,Yw)是否位于作为OpenGL ES状态的一部分的裁剪矩形范围内。如果该片段位于裁剪区域之外,则被抛弃。

  • 模板和深度测试–这些测试在输入片段的模板和深度值上进行,以确保片段是否应该被拒绝。

  • 混合–混合将新生成的片段颜色值与保存在帧缓冲区(Xw,Yw)位置的颜色值组合起来。

  • 抖动–抖动可用于最小化因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像。

在逐片段操作的最后,片段或者被拒绝,或者在帧缓冲区的(Xw,Yw)位置写入片段的代码,深度或者模板值。写入片段颜色 深度和模板值取决于启用的相应写入掩码。写入掩码可以更精细的控制写入相关缓冲区的颜色,深度和模板值。

OpenGL ES 3.0新功能

  • 纹理

  • 着色器

  • 几何形状

  • 缓冲区对象

  • 帧缓冲区

OpenGL ES 3.0 和向后兼容性

EGL(iOS不支持)

UML类图中类的关系

发表于 2017-02-08   |  

一、关联关系

关联(Association)关系是类与类之间最常用的一种关系,它是一种结构化关系,用于表示一类对象与另一类对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生等等。在UML类图中,用实线连接有关联关系的对象所对应的类,在使用Java、C#和C++等编程语言实现关联关系时,通常将一个类的对象作为另一个类的成员变量。在使用类图表示关联关系时可以在关联线上标注角色名,一般使用一个表示两者之间关系的动词或者名词表示角色名(有时该名词为实例对象名),关系的两端代表两种不同的角色,因此在一个关联关系中可以包含两个角色名,角色名不是必须的,可以根据需要增加,其目的是使类之间的关系更加明确。

1.双向关联

默认情况下,关联是双向的。例如:顾客(Customer)购买商品(Product)并拥有商品,反之,卖出的商品总有某个顾客与之相关联。因此,Customer类和Product类之间具有双向关联关系 如下图:

2.单向关联

类的关联关系也可以是单向的,单向关联用带箭头的实线表示。例如:顾客(Customer)拥有地址(Address),则Customer类与Address类具有单向关联关系 如下图:

3.自关联

在系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系称为自关联。例如:一个节点类(Node)的成员又是节点Node类型的对象
如下图:

4.多重性关联

多重性关联关系又称为重数性(Multiplicity)关联关系,表示两个关联对象在数量上的对应关系。在UML中,对象之间的多重性可以直接在关联直线上用一个数字或一个数字范围表示。

对象之间可以存在多种多重性关联关系,常见的多重性表示方式如下所示:

表示方式
多重性说明

1..1
表示另一个类的一个对象只与该类的一个对象有关系

0..*
表示另一个类的一个对象与该类的零个或多个对象有关系

1..*
表示另一个类的一个对象与该类的一个或多个对象有关系

0..1
表示另一个类的一个对象没有或只与该类的一个对象有关系

m..n
表示另一个类的一个对象与该类最少m,最多n个对象有关系 (m≤n)

如下图

5.聚合关系

聚合(Aggregation)关系表示整体与部分的关系。在聚合关系中,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。在UML中,聚合关系用带空心菱形的直线表示。例如:汽车发动机(Engine)是汽车(Car)的组成部分,但是汽车发动机可以独立存在,因此,汽车和发动机是聚合关系

如下图

6.组合关系

组合(Composition)关系也表示类之间整体和部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有同生共死的关系。在UML中,组合关系用带实心菱形的直线表示。例如:人的头(Head)与嘴巴(Mouth),嘴巴是头的组成部分之一,而且如果头没了,嘴巴也就没了,因此头和嘴巴是组合关系

如下图

二、依赖关系

依赖(Dependency)关系是一种使用关系,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系。大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数。在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。例如:驾驶员开车,在Driver类的drive()方法中将Car类型的对象car作为一个参数传递,以便在drive()方法中能够调用car的move()方法,且驾驶员的drive()方法依赖车的move()方法,因此类Driver依赖类Car

如下图

三、泛化关系

泛化(Generalization)关系也就是继承关系,用于描述父类与子类之间的关系,父类又称作基类或超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的直线来表示。在代码实现时,我们使用面向对象的继承机制来实现泛化关系,如在Java语言中使用extends关键字、在C++/C#中使用冒号“:”来实现。例如:Student类和Teacher类都是Person类的子类,Student类和Teacher类继承了Person类的属性和方法,Person类的属性包含姓名(name)和年龄(age),每一个Student和Teacher也都具有这两个属性,另外Student类增加了属性学号(studentNo),Teacher类增加了属性教师编号(teacherNo),Person类的方法包括行走move()和说话say(),Student类和Teacher类继承了这两个方法,而且Student类还新增方法study(),Teacher类还新增方法teach()

如下图

四、接口与实现关系

接口之间也可以有与类之间关系类似的继承关系和依赖关系,但是接口和类之间还存在一种实现(Realization)关系,在这种关系中,类实现了接口,类中的操作实现了接口中所声明的操作。在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。例如:定义了一个交通工具接口Vehicle,包含一个抽象操作move(),在类Ship和类Car中都实现了该move()操作,不过具体的实现细节将会不一样

如下图

iOS并发编程指南

发表于 2016-12-08   |  

最近又看了下官方关于并发编程的文档,简单做一下总结。

并发和程序设计

并发编程的几个概念:

-

并发:并发表示同时发生多件事情的概念

并行:并行是多个任务同时发生并且同时运行直到结束

-

同步:同步指函数运行时会阻塞当前线程一直运行结束

异步:异步指函数运行时不会阻塞当前线程

-

串行:当前队列中的任务是顺序执行的

并行: 当前队列中的任务是并发执行的,没有特定的顺序

-

并发的优点和缺点

优点:

•    充分发挥多核处理器优势,将不同线程任务分配给不同的处理器,真正进入“并行运算”状态
•    将耗时的任务分配到其他线程执行,由主线程负责统一更新界面会使应用程序更加流畅,用户体验更好
•    当硬件处理器的数量增加,程序会运行更快,而程序无需做任何调整

缺点:

增加了开销,并增加了整体的代码的复杂性,使得代码更难编写和调试

使用并发编程之前,你应该分解出可执行的工作单元,并确定你需要的队列。

Operation Queues

NSOperation是一个面向对象的方式来封装要异步执行的工作,被设计成结合操作队列(NSOperationQueue)使用,也可以自己单独使用

子类:

Target/Action 方式初始化

1
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomethingWithObj:) object:nil];

Block方式初始化

1
2
3
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
//Do something here.
}];

所有的NSOperation对象支持以下特性:

1.设置依赖关系

2.设置完成块

3.使用KVO通知监听你的Operations执行状态改变

4.设置优先级

5.取消操作

自定义Operation(默认start为非并发)

1.继承NSOperation基类

2.至少实现下面两个方法:

initialization

main

3.其他可添加的方法

自定义的在主函数调用的方法

数据的存取方法

NSCoding协议的方法

并发执行的配置操作

1.重写isConcurrent函数并返回YES

2.重写start函数

3.重写isExecuting和isFinished函数

NSOperation属性列表

isCancelled 是否取消

isConcurrent 是否并发

isExecuting 是否正在执行

isFinished 是否已经完成

isReady 是否准备好要执行

dependencies(array) 依赖关系数组

queuePriority 优先级

completionBlock 完成块

优先级:

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

Dispatch Queues

串行队列(创建一个线程)

1
dispatch_queue_t sQueue = dispatch_queue_create("这是串行队列", NULL);

并发队列(创建多个线程)

1
dispatch_queue_t cQueue = dispatch_queue_create("这是并发队列", DISPATCH_QUEUE_CONCURRENT);

主队列

1
dispatch_queue_t mQueue = dispatch_get_main_queue();

后台队列

1
2
3
4
5
6
dispatch_queue_t gQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

异步执行

1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

同步执行(不会开辟新的线程)

1
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

以上可能有四种组合方式:

dispatch_sync + 串行队列 (串行执行)

dispatch_sync + 并发队列 (串行执行)

dispatch_async + 串行队列 (串行执行)

dispatch_async + 并发队列 (并行执行)

Dispatch Group

将多个任务放到这个组里面,可以在所有的任务都结束后得到一个回调。

dispatch_barrier_async + dispatch_queue_create()

使用时需要自己创建并发队列,不能使用后台队列。

dispatch_apply

和dispatch_sync一样,是同步运行的,会阻塞当前线程。

Dispatch Semaphore

可以根据自己需要创建需要的signal数量,如果只创建一个则为串行执行。

注意

循环引用

死锁

相关demo

- END -

1…678
lly

lly

耿直的一Boy

76 日志
© 2021 lly
由 Hexo 强力驱动
主题 - NexT.Mist