RTMP拉流播放详解

前面已经介绍了RTMP推流的整个流程,今天我们再来梳理一下拉流端以及播放的整个过程。

我们先来看一下拉流端整个流程的抓包数据,然后分析一下每一个数据包的具体内容。

从上面的抓包数据可以看出,和推流端的整个流程基本上是一致的,只是在connect结束之后有一点不一样的操作,推流端是发送publish命令,而拉流端发送的是play命令。

play

既然如此,那其他的数据包我在这里就不再一一分析,可以参考我之前写的推流详解这篇博客,现在我主要来解析一样play这个命令。

结合下面的代码来说明一下

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

其实和publish命令差不多的 ,只是将关键字改为 play.

SetBufferTime

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)sendSetBufferTime:(int)milliseconds{
dispatch_sync(_packageQueue, ^{
int streamId = 0;
NSMutableData *data = [NSMutableData data];
[data appendByte:2];
[data appendByte24:0];
[data appendByte24:10];
[data appendByte:LLYMSGTypeID_PING];
[data appendBytes:(uint8_t*)&streamId length:sizeof(int32_t)];
[data appendByte16:3];
[data appendByte32:_streamID];
[data appendByte32:milliseconds];
[self writeData:data];
});
}

在收到play的回调消息,我们还需要设置一下发送时间,这一句很重要哦,设置不成功或者不设置的话服务器是不会给你推音视频的数据的。

视频流的解析

ok 现在我们开始收到源源不断的视频流数据,那么我们怎么把他们解析成原始的数据格式播放出来呢,下面来分析一下这个过程。

如上图,我们先来看一下一帧视频流数据长啥样吧。

首先是头部信息,在rtmp格式中已经详细的分析了,这里不再介绍。

重点来看一下Body中的数据:

Control : 0x17(keyframe H.264)

这个0X17是个啥呢,如果还记得推流时我们怎么封装数据的应该对这个有印象

1
body[index++] = 0x17;//RTMP Body Control FrameType:1表示key frame CodecID:7: AVC

就是这行代码所表示的数据了。

通过这个数据,我们就能分析当前帧是不是关键帧,需不需要解析里面的SPS和PPS信息。

video data

这个里面就是我们想要的视频数据了,但是又并不全是我们想要的视频数据,因为我们在推流封装的时候,是根据AVC格式来,所以其实这个body data里面还有一部分AVC的数据,我们需要把这一部分的数据剔除掉,剩下的才是我们需要的原始的视频流数据(好复杂的感觉有木有)。

先把代码贴上来

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
- (void)updateFrame{
dispatch_sync(mDecodeQueue, ^{
if(packetBuffer == NULL) {
return ;
}
uint8_t avcType = packetBuffer[0];
long totalLength = packetSize;
while (avcType == 0x17 || avcType == 0x27) {
uint8_t type = packetBuffer[1];
if (type == 0) {
//获取sps
// int spsnum = packetBuffer[10]&0x1f;
int number_sps = 11;
int count_sps = 1;
int spsTotalLen = 0;
uint8_t *spsTmp;
// if (count_sps <= spsnum)
{
int spslen =(packetBuffer[number_sps]&0x000000FF)<<8 | (packetBuffer[number_sps+1]&0x000000FF);
number_sps += 2;
spsTmp = malloc(spslen + 4);
memcpy(spsTmp, lyStartCode, 4);
spsTotalLen += 4;
memcpy(spsTmp+4,packetBuffer + number_sps , spslen);
spsTotalLen += spslen;
number_sps += spslen;
totalLength -= number_sps;
count_sps ++;
}
[self decodeNalu:spsTmp withSize:spsTotalLen];
packetBuffer += number_sps+1;
//获取pps
// int ppsnum = packetBuffer[number_sps]&0x1f;
int number_pps = 0;
int count_pps = 1;
int ppsTotalLen = 0;
uint8_t *ppsTmp;
// if (count_pps <= ppsnum)
{
int ppslen =(packetBuffer[number_pps]&0x000000FF)<<8 | (packetBuffer[number_pps+1]&0x000000FF);
number_pps += 2;
ppsTmp = malloc(ppslen + 4);
memcpy(ppsTmp, lyStartCode, 4);
ppsTotalLen += 4;
memcpy(ppsTmp + 4,packetBuffer + number_pps,ppslen);
ppsTotalLen += ppslen;
number_pps += ppslen;
totalLength -= number_pps;
count_pps ++;
}
[self decodeNalu:ppsTmp withSize:ppsTotalLen];
packetBuffer += number_pps;
avcType = packetBuffer[0];
}
else if(type == 1){
BOOL isNalu = YES;
//获取avc nalu
int len =0;
int num =5;
int naluTotalLen = 0;
while (isNalu)
{
len = (packetBuffer[num]&0x000000FF)<<24 | (packetBuffer[num+1]&0x000000FF)<<16
| (packetBuffer[num+2]&0x000000FF)<<8 | (packetBuffer[num+3]&0x000000FF);
naluBuffer = malloc(len + 4);
naluTotalLen += 4;
naluTotalLen += len;
memcpy(naluBuffer,packetBuffer + num,len + 4);
num = num + len + 4;
totalLength -= num;
[self decodeNalu:naluBuffer withSize:naluTotalLen];
packetBuffer += num;
num = 0;
naluTotalLen = 0;
free(naluBuffer);
//可能存在下一个NALU
if (totalLength > 4) {
avcType = packetBuffer[0];
if (avcType == 0x17 || avcType == 0x27) {
isNalu = NO;
}
else{
len = (packetBuffer[num]&0x000000FF)<<24 | (packetBuffer[num+1]&0x000000FF)<<16
| (packetBuffer[num+2]&0x000000FF)<<8 | (packetBuffer[num+3]&0x000000FF);
if (len >= (totalLength - 4)) {
return;
}
}
}
else{
return;
}
}
}
}
});
}

从最上面开始分析

packetBuffer 即为上图中的RTMP Body数据,注意这里是剔除了RTMP Head的哦。

怎么剔除Head可以参考我的demo中下面这个方法。

1
- (void)parseData:(NSData *)data
1
2
//RTMP Body Control FrameType:1表示key frame CodecID:7: AVC
uint8_t avcType = packetBuffer[0];

然后我们取出Control信息 它可能包括0x17或者是0x27,其中17是关键帧,27不是关键帧。

1
2
3
4
// IF AVCPacketType ==0 AVCDecoderConfigurationRecord(AVC sequence header)
// IF AVCPacketType == 1 One or more NALUs (Full frames are required)
uint8_t type = packetBuffer[1];

然后是AVCType字段。

如果Type = 0 ,我们需要取出的是SPS和PPS信息,如果没有这部分信息是无法将H.264文件解码的。

当Type = 1,我们获取当前avc的NALU单元信息,这个数据就是我们需要显示的视频数据了。

然后我把取到的数据用下面方法解码为CVPixelBufferRef数据进行显示(播放)。

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
-(void) decodeNalu:(uint8_t *)frame withSize:(uint32_t)frameSize
{
// NSLog(@">>>>>>>>>>开始解码");
int nalu_type = (frame[4] & 0x1F);
CVPixelBufferRef pixelBuffer = NULL;
// uint32_t nalSize = (uint32_t)(frameSize - 4);
// uint8_t *pNalSize = (uint8_t*)(&nalSize);
// frame[0] = *(pNalSize + 3);
// frame[1] = *(pNalSize + 2);
// frame[2] = *(pNalSize + 1);
// frame[3] = *(pNalSize);
//传输的时候。关键帧不能丢数据 否则绿屏 B/P可以丢 这样会卡顿
switch (nalu_type)
{
case 0x05:
//NSLog(@"nalu_type:%d Nal type is IDR frame",nalu_type); //关键帧
{
uint32_t dataLength32 = htonl (frameSize - 4);
memcpy (frame, &dataLength32, sizeof (uint32_t));
[self initVideoToolBox];
pixelBuffer = [self decode:frame withSize:frameSize];
[self displayDecodedFrame:pixelBuffer];
}
break;
case 0x07:
//NSLog(@"nalu_type:%d Nal type is SPS",nalu_type); //sps
mSPSSize = frameSize - 4;
mSPS = malloc(mSPSSize);
memcpy(mSPS, frame + 4, mSPSSize);
break;
case 0x08:
{
//NSLog(@"nalu_type:%d Nal type is PPS",nalu_type); //pps
mPPSSize = frameSize - 4;
mPPS = malloc(mPPSSize);
memcpy(mPPS, frame + 4, mPPSSize);
break;
}
default:
{
//NSLog(@"Nal type is B/P frame");//其他帧
{
uint32_t dataLength32 = htonl (frameSize - 4);
memcpy (frame, &dataLength32, sizeof (uint32_t));
[self initVideoToolBox];
pixelBuffer = [self decode:frame withSize:frameSize];
[self displayDecodedFrame:pixelBuffer];
}
break;
}
}
// if(pixelBuffer)
// {
// dispatch_async(dispatch_get_main_queue(), ^{
// [self.mOpenGLView displayPixelBuffer:pixelBuffer];
// CVPixelBufferRelease(pixelBuffer);
// });
// }
}

播放

这里我用的是苹果官网给的一个类AAPLEAGLLayer。

我是demo