代码下载
此图为客户端采集数据,下面的视频是摄像头采集的数据
上面的视频是摄像头解码h264的试试绘制效果
IP地址 是 同一个局域网内另外一台设备的ip地址,在wifi里面详情就能看到
上图是tcp接收端的显示效果
ios 录音播放udp数据包传输过来的pcmData流
在这篇文章中,我实现了在局域网内通过udp协议 完成了pcm音频的录音与播放,
于是想把视频也能够通过局域网达到传输的效果,
这就有了本篇代码的实现方案。
最开始我和音频一样采用了udp的方案,发现了视频花屏非常严重,
意识到是udp丢包导致的,于是转换方案采用了tcp的方案,tcp不再丢包,
但在tcp接收端处理视频流的地方,遇到了分包和粘包的问题,
解决分包、粘包,只能通过协议包头和内容长度机制来解决。
以下是分包、粘包的解决思路:
由于tcp是可靠性连接,那么数据都不会丢失。
当我收到第一个包的前4个字节,我就可以解析这4个字节,
判定是否为发送端定义的包头,如果和包头一致,
我们就知道 这是视频流数据的包头了,然后再继续解析4个字节
,一般我们将这4个字节解析成为包内容的长度,然后我们判定
我们现在的包是否达到了包内容的长度,如果不够就继续接收下去
如果够了,就把包内容解析出来,解析完成后把数据缓冲清空。
总结上面这一段的意思
1.来了任何的数据都拼接到缓冲队列的最后
2.开一个while循环,当字节头大于4个字节就开始解析,
解析出包体长度后,如果当前缓冲长度不够,就继续等待数据拼接
3.如果缓冲长度够了,就解析这段内容,然后将这段内容从缓冲队列中移除。
代码一
// 接收到了数据 自动回调 sock客户端
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
[self.currentLock lock];
//收到一段data
[self.completeData appendData:data];
[self.currentLock unlock];
[sock readDataWithTimeout:-1 tag:0];
}
while缓冲的内容,这里加了一个自动释放池防止内存增长过快
- (void)receiveDataHanle {
while (!_exit) {
@autoreleasepool {
if (self.completeData.length <16) {
usleep(20*1000);
continue;
}
//缓冲队列大小大于16才能继续处理
NSData *data = [self.completeData copy];
NSRange range = NSMakeRange(0, 4);
NSData *headData = [data subdataWithRange:range];
int head= [self intWithData:headData];
NSRange countRange = NSMakeRange(4,4);
NSData *countData = [data subdataWithRange:countRange];
int count = [self intWithData:countData];
NSRange lengthRange = NSMakeRange(8,4);
NSData *lengthData = [data subdataWithRange:lengthRange];
int length = [self intWithData:lengthData];
NSRange typeRange = NSMakeRange(12,4);
NSData *typeData = [data subdataWithRange:typeRange];
int type = [self intWithData:typeData];
int completeDataLength = length+16;
//打印字节头 序列号 消息内容长度 这个包总长
NSLog(@"head=%d count=%d bodylength=%d completeDataLength=%d completeData.length=%lu",head,count,length,completeDataLength,(unsigned long)data.length);
if (data.length>=completeDataLength) {
//取出body数据
NSRange dataRange = NSMakeRange(16, length);
NSData *mData = [data subdataWithRange:dataRange];
NSString *msg = [NSString stringWithFormat:@"序号[%d]类型[%@]应收长度[%d]收到长度[%lu]",count,type==0?@"A":@"V",length,(unsigned long)mData.length];
//移除缓冲队列里面处理的部分
NSLog(@"%@",msg);
NSLog(@"[before]%lu",(unsigned long)self.completeData.length);
[self.currentLock lock];
self.completeData = [[self.completeData subdataWithRange:NSMakeRange(completeDataLength, self.completeData.length - completeDataLength)] mutableCopy];
[self.currentLock unlock];
NSLog(@"[after]%lu",(unsigned long)self.completeData.length);
if (type == 0) {
// 音频
[self.audioPlayer addPCM:(void *)mData.bytes length:(UInt32)mData.length timeStamp:count];
}else if (type == 1) {
// 视频
[self.decoder startH264DecodeWithVideoData:(char *)mData.bytes andLength:(int)mData.length andReturnDecodedData:^(CVPixelBufferRef pixelBuffer) {
[self.playView displayPixelBuffer:pixelBuffer];
}];
}
} else {
//遇到了拆包 需要继续接收 直到满足 包大小;
usleep(10*1000);
continue;
}
}
}
}
可以看下发送端的逻辑
视频发送
// 默认情况下,为30 fps,意味着该函数每秒调用30次
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
// 获取输入设备数据,有可能是音频有可能是视频
if (captureOutput == self.videoOutput) {
if ([sendSocket isConnected]) {
// 收到数据,开始编码
[self.videoEncoder startH264EncodeWithSampleBuffer:sampleBuffer andReturnData:^(NSData *data) {
_count++;
NSMutableData *mData = [[NSMutableData alloc] init];
NSData *headData = [self bytewithInt:60000];//协议头 4位 (0-4)
NSData *countData = [self bytewithInt:_count];//发到第几个包 4位 (4-8)
NSData *legnthData = [self bytewithInt:(int)data.length];//当前包的长度 4位 (8-12)
NSData *dataType = [self bytewithInt:1];//type 1为视频
[mData appendData:headData];
[mData appendData:countData];
[mData appendData:legnthData];
[mData appendData:dataType];
[mData appendData:data];
NSLog(@"序号[%d]视频 %lu data[%d]",_count,(unsigned long)data.length,mData.length);
[sendSocket writeData:[mData copy] withTimeout:60 tag:201];
[self.decoder startH264DecodeWithVideoData:(char *)data.bytes andLength:(int)data.length andReturnDecodedData:^(CVPixelBufferRef pixelBuffer) {
[self.playView displayPixelBuffer:pixelBuffer];
}];
}];
}
}
}
音频发送
- (void)didReceiveATAudioData:(NSData*)data {
_count++;
NSMutableData *mData = [[NSMutableData alloc] init];
NSData *headData = [self bytewithInt:60000];//协议头 4位 (0-4)
NSData *countData = [self bytewithInt:_count];//发到第几个包 4位 (4-8)
NSData *legnthData = [self bytewithInt:(int)data.length];//当前包的长度 4位 (8-12)
NSData *dataType = [self bytewithInt:0];//type 0为音频
[mData appendData:headData];
[mData appendData:countData];
[mData appendData:legnthData];
[mData appendData:dataType];
[mData appendData:data];
NSLog(@"音频 data%lu",(unsigned long)data.length);
if ([sendSocket isConnected]) {
[sendSocket writeData:[mData copy] withTimeout:60 tag:200];
}
}
这两段代码只是为了让你理解发送端会将int型的固定长度的约定字段 转换成二进制,
而接收端会则会将二进制再次转换为约定字段,来判断协议头,当前序号,是音频还是视频,内容长度。没有这些约定内容,就算你有数据也不知道如何解析。
解决了这个问题后,可以看到tcp 的视频效果远比udp的花屏效果好,
而视频采集转码,使用的都是videotoolbox转换成H264
播放也是用videotoolbox再将H264转换回来。
附byte 和data型数据的互转
这个要求都是用4位的,如果你用别的位数,可以写个循环即可。
- (NSData * )bytewithInt:(int )i {
Byte b1=i & 0xff;
Byte b2=(i>>8) & 0xff;
Byte b3=(i>>16) & 0xff;
Byte b4=(i>>24) & 0xff;
Byte byte[] = {b4,b3,b2,b1};
NSData *data = [NSData dataWithBytes:byte length:sizeof(byte)];
return data;
}
- (int)intWithData:(NSData *)data {
Byte *byteK = (Byte *)data.bytes;
int valueK;
valueK = (int) (((byteK[0] & 0xFF)<<24)
|((byteK[1] & 0xFF)<<16)
|((byteK[2] & 0xFF)<<8)
|(byteK[3] & 0xFF));
return valueK;
}