# Tutk **Repository Path**: damonYYB/Tutk ## Basic Information - **Project Name**: Tutk - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2017-10-24 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README Tutk 音视频播放集成 请务必使用真机调试 ,模拟器存在内存泄露,同时播放的时候视频延时非常多,请注意。 ## 一.编译: 1.拖入libIOTCAPIs_ALL 库 。确保 Link binary with libraries 存在这个.a库 2.拖入 client 文件 3.修改 ViewController - viewDidLoad 代码 去掉autorelease 4.发现client import报错 ,因为缺乏 include 文件 ,拖入 Include 文件夹 5.client.m 报错 fix UID 改为 bridge uid start_main 结尾添加return NULL; # 二.运行并获取音视频buffer:(忽略配网阶段) 1.Viewcontroller 替换client 的[client start:@"K0000000000000000001"] 为在直播的摄像头uid 2.client .m 处理视频 thread_ReceiveVideo 修改并添加一个方法,当获取到IPB帧 时,打印buffer (暂时不考虑丢帧) //视频帧 if(frameInfo.flags == IPC_FRAME_FLAG_IFRAME) { // got an IFrame, draw it. NSData *data = [NSData dataWithBytes:buf length:actualFrameSize]; NSLog(@"got an I Frame, %@",data); } if(frameInfo.flags == IPC_FRAME_FLAG_PBFRAME) { NSData *data = [NSData dataWithBytes:buf length:actualFrameSize]; NSLog(@"got an P/B Frame, %@",data); } 运行结果——— 出现很多打印行 —— 运行ok 三.硬解码 —— 对拿到的FrameBuffer 和 大小 进行解码 # 视频解码 首先是按照 00 00 00 01 拆分 因为所有的有效数据的起始码都是 00 00 00 01 这是拿到的buffer (我忽略了 后面的内容 因为后面不再包含 00 00 00 01 了 并且内容太长了) ``` <00000001 6764001e ad84010c 20086100 43080218 4010c200 843b5050 17fcb370 10101020 00000001 68ee3cb0 00000001 06e5015d 80000000 0165b800 00030382 9c11ffbb 4b50f579 2e816df8 0c7d8853 9adb8e1a 5bc0b2ca e8f2697a b73b7277 83d284e6 8a48a404 cf1fbc84 68dbdc10 97af5579 d305f2c2 0b29e8cb cc482ff0 106e13e8 72e055cf 8e4a9045 9b80b9d0 ffab1b8c 735a692e 4b5459fb 5a905c6c cf74556f 0a8c6efd 3298927a 4284d2b0 afb9a305 32f3d0d8 53af52c1 28c387cf f2845d7c a72a73d2 a3dbe87b 317ced9f 3e913e92 e6f549a9 e127058b eb95203b 70176c5b e26158b4 27a6f526 4a434987 3b8feb24 03ca13e0 c2a0c521 19451b05 c643277a c27dd529 4192c457 c28dbca8 dff783d5 219b5641 9ca4d394 e6aadd3c 2cf29f94 6d1feb05 b9fd038c b6bf6528 d8fceff6 27113ce4 cd2055a3 05a5e195 1a7dc846 f7e4238f 61826d8b d30869b8 12c99359 adda1275 a57701ca 14aedf7d 1d6ce9fc d69f7883 84972684 17f5353a 9d4cb7eb 7a213839 ec7a46aa f525a5a1 6abe24f6 a37bf779 1261c674 a97d90c2 86e0bd77 7a9315b8 c7d3457e 52a3160d 1c007f50 39abd0cb 0a049109 e7e0eaac 7aa003eb ca7ac4f9 f819effb c8063122 b2e2f78f b34b6a68 c32a5ace 73b5deca a65eef80 eff7778c 29b2df8c c343d51e 9e161258 997f521d b7e28899 8db853ab 852262fc 7b8d8fc2 a232495b 3192df7f ...> ``` 按照 起始码 00 00 00 01 进行拆分 拆分的第一段 (从第1位 到第 36 位 2个字节 是 1位 这里每组 8个字节 4位 总共 9 组 就是 36位) 是从第一个00000001 到下一个00000001 为止 `--------00000001 6764001e ad84010c 20086100 43080218 4010c200 843b5050 17fcb370 10101020 00000001---------------` `<00000001 6764001e ad84010c 20086100 43080218 4010c200 843b5050 17fcb370 10101020>` 拆分的第二段 (是从第二个00000001 到第三个00000001 为止的 ) `<00000001 68ee3cb0>` 拆分的第三段 (是从第三个00000001 到第四个00000001 为止的 ) `<00000001 06e5015d 80>` 拆分第四段 最后一段 (第四个到最后 再无发现00000001 直接拆出来 不再做分割) ``` <00000001 65b80000 030382 9c11ffbb 4b50f579 2e816df8 0c7d8853 9adb8e1a 5bc0b2ca e8f2697a b73b7277 83d284e6 8a48a404 cf1fbc84 68dbdc10 97af5579 d305f2c2 0b29e8cb cc482ff0 106e13e8 72e055cf 8e4a9045 9b80b9d0 ffab1b8c 735a692e 4b5459fb 5a905c6c cf74556f 0a8c6efd 3298927a 4284d2b0 afb9a305 32f3d0d8 53af52c1 28c387cf f2845d7c a72a73d2 a3dbe87b 317ced9f 3e913e92 e6f549a9 e127058b eb95203b 70176c5b e26158b4 27a6f526 4a434987 3b8feb24 03ca13e0 c2a0c521 19451b05 c643277a c27dd529 4192c457 c28dbca8 dff783d5 219b5641 9ca4d394 e6aadd3c 2cf29f94 6d1feb05 b9fd038c b6bf6528 d8fceff6 27113ce4 cd2055a3 05a5e195 1a7dc846 f7e4238f 61826d8b d30869b8 12c99359 adda1275 a57701ca 14aedf7d 1d6ce9fc d69f7883 84972684 17f5353a 9d4cb7eb 7a213839 ec7a46aa f525a5a1 6abe24f6 a37bf779 1261c674 a97d90c2 86e0bd77 7a9315b8 c7d3457e 52a3160d 1c007f50 39abd0cb 0a049109 e7e0eaac 7aa003eb ca7ac4f9 f819effb c8063122 b2e2f78f b34b6a68 c32a5ace 73b5deca a65eef80 eff7778c 29b2df8c c343d51e 9e161258 997f521d b7e28899 8db853ab 852262fc 7b8d8fc2 a232495b 3192df7f .......> ``` 这是拆分所用的代码 ``` - (void)clientDidGetVideoBytes:(char *)frameBytes size:(UInt32)frameSize timeStamp:(UInt32)timeStamp { uint8_t *bytes = (uint8_t *)frameBytes; int begin = 0; for (int i = 0; i < frameSize - 4; i++) { //判断是否为 00 00 00 01 if (bytes[i]==0x00 && bytes[i+1]==0x00 && bytes[i+2]==0x00 && bytes[i+3]==0x01) { //切分的时候 从 上一段的 00 00 00 01 到 这一段 00 00 00 01 的 00 位为止 if (i - 1 > begin) { [self.vtDecoder decodeWithBytes:(char *)(&frameBytes[begin]) length:(i - begin) timeStamp:timeStamp]; begin = i; } } else if (i >= frameSize - 5) { // 最后一段 到尽头已无 00 00 00 01 了 [self.vtDecoder decodeWithBytes:(char *)(&frameBytes[begin]) length:(frameSize - begin) timeStamp:timeStamp]; } } } ``` 这段代码本身没有什么问题 就是遍历的次数太多了 frameSize 经常是很大的 一般要到 几千 那么每次使用都要遍历几千次 ,实际上 当前的摄像头 发现最多有 三次 00 00 00 01 、如此一来 就设置 一个合适的数值 ,我选的是 64 因为 这些 头信息 的总长度不超过这个就行 else if 就修改为 i >= 64 - 5 那么久最多循环60 次 就可以走下一步了。 ``` - (void)clientDidGetVideoBytes:(char *)frameBytes size:(UInt32)frameSize timeStamp:(UInt32)timeStamp { uint8_t *bytes = (uint8_t *)frameBytes; int begin = 0; int len = 64;//减少循环次数 ,如果遇到问题 可以适当的调大这个数值 改成200 什么的 if (frameSize < len) { len = frameSize;//当本身buffer 比较小的时候 替换len 就好了 } for (int i = 0; i < len - 4; i++) { if (bytes[i]==0x00 && bytes[i+1]==0x00 && bytes[i+2]==0x00 && bytes[i+3]==0x01) { if (i - 1 > begin) { [self.vtDecoder decodeWithBytes:(char *)(&frameBytes[begin]) length:(i - begin) timeStamp:timeStamp]; begin = i; } } else if (i >= len - 5) { [self.vtDecoder decodeWithBytes:(char *)(&frameBytes[begin]) length:(frameSize - begin) timeStamp:timeStamp]; } } } ``` 到这里就完成了拆分 ,接下来就是有效信息的解码了 ------ 这里插一段介绍: H264是新一代的编码标准,以高压缩高质量和支持多种网络的流媒体传输著称,在编码方面,参照一段时间内图像的统计结果表明,在相邻几幅图像画面中,一般有差别的像素只有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帧的算法。 I帧 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR图像都是I帧图像。H.264引入IDR图像是为了解码的重同步,当解码器解码到IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。 ------ 那么从第一段开始解析 `<00000001 6764001e ad84010c 20086100 43080218 4010c200 843b5050 17fcb370 10101020>` 其中的00 00 00 01为起始码,而起始码之后的下一个字节就可以检测出这一帧的类型。 下一位是67 就是0x67 转换为二进制为 0110 0111 (1)第1位禁止位,值为1表示语法出错 (2)第2~3位为参考级别 (3)第4~8为是nal单元类型,在此处为 0 0111换算成十进制为7。根据上图可知道这段码流是【序列参数集】 就是(SPS)。 ![](http://my.csdn.net/uploads/201205/11/1336727471_3857.png) 继续 ,下一段码流 `<00000001 68ee3cb0>` 下一位是68 就是0x68 转换为二进制为 0110 1000 在此处为 0 0111换算成十进制为8 根据上图可知道这段码流是【图像参数集】就是(PPS)。 再下一段码流 `<00000001 06e5015d 80>` 下一位为 06 就是 0x06 转换为二进制为 0000 0110 在此处 0 0110 转换为十进制 为6 根据上图可知道这段码流是【补充增强信息单元】 就是(SEI) 最后一段码流 ``` <00000001 65b80000 030382 9c11ffbb 4b50f579 2e816df8 0c7d8853 9adb8e1a 5bc0b2ca e8f2697a b73b7277 83d284e6 8a48a404 cf1fbc84 68dbdc10 97af5579 d305f2c2 0b29e8cb cc482ff0 106e13e8 72e055cf 8e4a9045 9b80b9d0 ffab1b8c 735a692e 4b5459fb 5a905c6c cf74556f 0a8c6efd 3298927a 4284d2b0 afb9a305 32f3d0d8 53af52c1 28c387cf f2845d7c a72a73d2 a3dbe87b 317ced9f 3e913e92 e6f549a9 e127058b eb95203b 70176c5b e26158b4 27a6f526 4a434987 3b8feb24 03ca13e0 c2a0c521 19451b05 c643277a c27dd529 4192c457 c28dbca8 dff783d5 219b5641 9ca4d394 e6aadd3c 2cf29f94 6d1feb05 b9fd038c b6bf6528 d8fceff6 27113ce4 cd2055a3 05a5e195 1a7dc846 f7e4238f 61826d8b d30869b8 12c99359 adda1275 a57701ca 14aedf7d 1d6ce9fc d69f7883 84972684 17f5353a 9d4cb7eb 7a213839 ec7a46aa f525a5a1 6abe24f6 a37bf779 1261c674 a97d90c2 86e0bd77 7a9315b8 c7d3457e 52a3160d 1c007f50 39abd0cb 0a049109 e7e0eaac 7aa003eb ca7ac4f9 f819effb c8063122 b2e2f78f b34b6a68 c32a5ace 73b5deca a65eef80 eff7778c 29b2df8c c343d51e 9e161258 997f521d b7e28899 8db853ab 852262fc 7b8d8fc2 a232495b 3192df7f .......> ``` 下一位是 65 就是 0x65 转换为二进制为 0110 0101在此处 0 0101 转换为十进制 为 5 根据上图 可知道这段码流是 【IDR 图像中的片】即I帧 在代码上的判断方式就是 `int type = 0x65 & 0x1f` 为什么这样判断,是因为 0x1f 的二进制 是 0001 1111 就是后5位都位1 0x65 为 0110 0101 和 0001 1111 取& 与运算`(0&0=0; 0&1=0; 1&0=0; 1&1=1;)` 结果为 0 0101 这样可以二进制的后五位 并且转换为int 的十进制 而0x65 为 第四个字节 于是代码可以写成 `int type = h264Bytes[4] & 0x1F;` 然后根据type在表中查找即可获得需要的结果。这里就是上面所提到的简单的代码 ``` - (void)decodeWithBytes:(char *)bytes length:(uint32_t)length timeStamp:(UInt32)timeStamp { char *h264Bytes = calloc(length, sizeof(char)); memcpy(h264Bytes, bytes,length); // 按照VideoToolBox 要求替换头字节长度 将 00000001 换成对应的数据,否则无法解码 uint32_t nalSize = (uint32_t)(length - 4); uint32_t *pNalSize = (uint32_t *)h264Bytes; *pNalSize = CFSwapInt32HostToBig(nalSize); CVPixelBufferRef pixelBuffer = NULL; int nalType = h264Bytes[4] & 0x1F; switch (nalType) { case 0x01: //NSLog(@"Nal type is B/P frame"); break; case 0x05: //NSLog(@"Nal type is IDR frame"); break; case 0x07: //NSLog(@"Nal type is SPS"); break; case 0x08: //NSLog(@"Nal type is PPS"); break; default: break; } if (pixelBuffer != NULL) { // return pixelBuffer to ViewLayer } free(h264Bytes); } ``` 这里会从 VideoToolBox 要求替换头字节长度 这句注释开始创建一个异步的串行队列,保证数据流会按照过来的顺序进行解码,免得图像会显示时间错乱。 return pixelBuffer to ViewLayer 所做的事情就是最后一点 我们把图像显示在 手机上,是用的OpenGL 写的layer层 然后将layer层 添加到手机屏幕上 ,这是苹果官方写的 并提供了属性 CVPixelBufferRef pixelBuffer: 外部只需要不断传入这个属性值,就能够连续不断在layer上显示图像了。 ``` - (void)displayDecodedFrame:(CVImageBufferRef )imageBuffer { if(imageBuffer) { playLayer.pixelBuffer = imageBuffer; CVPixelBufferRelease(imageBuffer); } } ``` **VideoToolBox 硬解码就是把刚才不同type 的bytes 转换为 CVPixelBufferRef 的过程。** 首先是I 帧 ``` case 0x05: //NSLog(@"Nal type is IDR frame"); if([self decoderSetup]) { pixelBuffer = [self decode:h264Bytes length:length]; } break; ``` 如果VideoToolBox 没有VTDecompressionSessionRef会话不存在 ,就初始化VTDecompressionSessionRef 初始化方法 是 VTDecompressionSessionCreate 这个需要创建一个VTDecompressionOutputCallbackRecord 的回调函数 设置返回结果为 CVImageBufferRef 同时需要传入sps 和pps 的参数集,这些都是固定写法, 当遇到错误的时候,就直接调用解码失败 ``` #pragma mark - 硬解回调函数 static void didDecompress(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ){ CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon; *outputPixelBuffer = CVPixelBufferRetain(pixelBuffer); } #pragma mark - 硬解码器创建销毁 -(BOOL)decoderSetup { if(self.decompressionSession) { return YES; } const uint8_t* const parameterSetPointers[2] = { _sps, _pps }; const size_t parameterSetSizes[2] = { _spsSize, _ppsSize }; OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, //param count parameterSetPointers, parameterSetSizes, 4, //nal start code size &_videoFormatDescription); if(status == noErr) { NSDictionary* pixelBufferOptions = @{ // Output pixel type required here since it would default to video range (NSString*) kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange), (NSString*) kCVPixelBufferOpenGLESCompatibilityKey : @YES, (NSString*) kCVPixelBufferIOSurfacePropertiesKey : @{}}; VTDecompressionOutputCallbackRecord callBackRecord; callBackRecord.decompressionOutputCallback = didDecompress; callBackRecord.decompressionOutputRefCon = NULL; status = VTDecompressionSessionCreate(kCFAllocatorDefault, self.videoFormatDescription, NULL, (__bridge CFDictionaryRef _Nullable)(pixelBufferOptions), &callBackRecord, &_decompressionSession); if (status != noErr) { NSLog(@"IOS8VT: reset decoder session failed status=%d", (int)status); [self decoderSetdown]; return NO; } } else { NSLog(@"IOS8VT: reset decoder session failed status=%d", (int)status); [self decoderSetdown]; return NO; } return YES; } - (void)decoderSetdown { if(self.decompressionSession) { VTDecompressionSessionInvalidate(self.decompressionSession); CFRelease(self.decompressionSession); self.decompressionSession = NULL; } if(self.videoFormatDescription) { CFRelease(self.videoFormatDescription); self.videoFormatDescription = NULL; } } ``` 如果返回了回话已经ok 了 就可以开始解码了 解码需要调用VTDecompressionSessionDecodeFrame 方法,这个方法需要 传入CMSampleBufferRef 于是需要调用CMSampleBufferCreateReady 创建CMSampleBufferRef 而CMSampleBufferCreateReady又需要 传入CMBlockBufferRef参数 ,于是调用CMBlockBufferCreateWithMemoryBlock创建CMBlockBufferRef , CMBlockBufferRef 需要用户传入的bytes 数据 和length 长度 都ok的请求下就可以解码了,还是一样,失败了 就直接结束,成功了就把CVPixelBufferRef return 回去 这里也是固定写法 ``` - (CVPixelBufferRef)decode:(char *)bytes length:(uint32_t)length { CVPixelBufferRef outputPixelBuffer = NULL; CMBlockBufferRef blockBuffer = NULL; OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, (void*)bytes, length, kCFAllocatorNull, NULL, 0, length, 0, &blockBuffer); if (status == kCMBlockBufferNoErr) { CMSampleBufferRef sampleBuffer = NULL; const size_t sampleSizeArray[] = {length}; status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, self.videoFormatDescription, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer); if (status == kCMBlockBufferNoErr && sampleBuffer) { VTDecodeFrameFlags flags = 0; VTDecodeInfoFlags flagOut = 0; OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(self.decompressionSession, sampleBuffer, flags, &outputPixelBuffer, &flagOut); if (decodeStatus == kVTInvalidSessionErr) { NSLog(@"IOS8VT: Invalid session, reset decoder session"); CVPixelBufferRelease(outputPixelBuffer); outputPixelBuffer = NULL; [self decoderSetdown]; } else if (decodeStatus == kVTParameterErr) { NSLog(@"IOS8VT: decode failed status=%d(ParameterErr)", (int)decodeStatus); CVPixelBufferRelease(outputPixelBuffer); outputPixelBuffer = NULL; [self decoderSetdown]; } else if (decodeStatus == kVTVideoDecoderMalfunctionErr) { NSLog(@"IOS8VT: decode failed status=%d(MalfunctionErr)", (int)decodeStatus); CVPixelBufferRelease(outputPixelBuffer); outputPixelBuffer = NULL; [self decoderSetdown]; } else if (decodeStatus == kVTVideoDecoderBadDataErr) { NSLog(@"IOS8VT: decode failed status=%d(Bad data)", (int)decodeStatus); CVPixelBufferRelease(outputPixelBuffer); outputPixelBuffer = NULL; } else if (decodeStatus != noErr) { NSLog(@"IOS8VT: decode failed status=%d", (int)decodeStatus); CVPixelBufferRelease(outputPixelBuffer); outputPixelBuffer = NULL; } CFRelease(sampleBuffer); } CFRelease(blockBuffer); } return outputPixelBuffer; } ``` 到这里 I帧的处理就全部完成了 然后是B/P帧,B/P帧和 I帧不一样的地方是 不是初始帧, 所以不需要调用if([self decoderSetup])来判断是否已经开始解码 ``` case 0x01: //NSLog(@"Nal type is B/P frame"); pixelBuffer = [self decode:h264Bytes length:length]; break; ``` 这个 就是和I帧 一样解码的部分了,就不再重复写一遍了 接下来处理SPS 帧 ``` case 0x07: //NSLog(@"Nal type is SPS"); { if(_sps) { free(_sps); _sps = NULL; } _spsSize = length - 4;// VTD doesn't want you to include the start code header (4 bytes long) so we add the - 4 here _sps = malloc(_spsSize);// allocate enough data to fit the SPS and PPS parameters into our data objects. memcpy(_sps, h264Bytes + 4, _spsSize); // copy in the actual sps and pps values, again ignoring the 4 byte header break; } ``` 注意SPS 和PPS 都是类变量,SPSSize 也是, 因为提供的接口 是这样要求的 ,所以只能这么写 VideoToolBox 不要 SPS 的起始码 所以把 length 就是_spsSize - 4 然后初始化一个_sps 再拷贝h264Bytes 过去 从 第4位拷贝到最后即可 ,其他就不用处理了 再接下来就是PPS 帧,这个和SPS 帧是完全一样的 ``` case 0x08: { if(_pps) { free(_pps); _pps = NULL; } _ppsSize = length - 4; _pps = malloc(_ppsSize); memcpy(_pps, h264Bytes + 4, _ppsSize); break; } ``` 这样 全部的type 就处理完了 整个方法的写法是 ``` - (void)decodeWithBytes:(char *)bytes length:(uint32_t)length timeStamp:(UInt32)timeStamp { NSLog(@"start------------"); char *h264Bytes = calloc(length, sizeof(char)); memcpy(h264Bytes, bytes,length); dispatch_async(self.decodeQueue, ^{ uint32_t nalSize = (uint32_t)(length - 4); uint32_t *pNalSize = (uint32_t *)h264Bytes; *pNalSize = CFSwapInt32HostToBig(nalSize); CVPixelBufferRef pixelBuffer = NULL; int nalType = h264Bytes[4] & 0x1F; //NSLog(@"nal type:%d", nalType); switch (nalType) { case 0x01: //NSLog(@"Nal type is B/P frame"); pixelBuffer = [self decode:h264Bytes length:length]; break; case 0x05: //NSLog(@"Nal type is IDR frame"); if([self decoderSetup]) { pixelBuffer = [self decode:h264Bytes length:length]; } break; case 0x07: //NSLog(@"Nal type is SPS"); { if(_sps) { free(_sps); _sps = NULL; } _spsSize = length - 4;// VTD doesn't want you to include the start code header (4 bytes long) so we add the - 4 here _sps = malloc(_spsSize);// allocate enough data to fit the SPS and PPS parameters into our data objects. memcpy(_sps, h264Bytes + 4, _spsSize); // copy in the actual sps and pps values, again ignoring the 4 byte header break; } case 0x08: { if(_pps) { free(_pps); _pps = NULL; } _ppsSize = length - 4; _pps = malloc(_ppsSize); memcpy(_pps, h264Bytes + 4, _ppsSize); break; } default: break; } if (pixelBuffer != NULL) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(decoderPixelBuffer:timeStamp:)]) { [self.delegate decoderPixelBuffer:pixelBuffer timeStamp:timeStamp]; } }); } free(h264Bytes); }); } ``` 页面内剩余的初始化 和一些其他写的就是 ``` #import "VTDecode.h" #import @interface VTDecode () @property (nonatomic) VTDecompressionSessionRef decompressionSession; @property (nonatomic) CMVideoFormatDescriptionRef videoFormatDescription; @property (nonatomic, strong) dispatch_queue_t decodeQueue; @property (nonatomic) uint8_t *sps; @property (nonatomic) size_t spsSize; @property (nonatomic) uint8_t *pps; @property (nonatomic) size_t ppsSize; @end @implementation VTDecode - (instancetype)init { self = [super init]; if (self) { self.decodeQueue = dispatch_queue_create("VideoDecodeQueue", DISPATCH_QUEUE_SERIAL); } return self; } - (void)dealloc { [self decoderSetdown]; if (self.sps) { free(self.sps); self.sps = NULL; } if(self.pps){ free(self.pps); self.pps = NULL; } self.spsSize = self.ppsSize = 0; } ``` 就再也没有其他的什么方法了。 到此 ,简单的硬解码的部分就完全结束了。 解码完成将CVPixelBufferRef 通过代理回传 , ``` if (pixelBuffer != NULL) { dispatch_async(dispatch_get_main_queue(), ^{ if (self.delegate && [self.delegate respondsToSelector:@selector(decoderPixelBuffer:timeStamp:)]) { [self.delegate decoderPixelBuffer:pixelBuffer timeStamp:timeStamp]; } }); } ``` 控制器 接收到代理后 传给 AAPLEAGLLayer 自定义layer层 自定义layer 就会绘制 图像到屏幕上了。 ``` #pragma mark - VideoDecoderDelegate - (void)decoderPixelBuffer:(CVPixelBufferRef)pixelBuffer timeStamp:(UInt32)timeStamp { if (pixelBuffer) { if (playLayer) { NSLog(@"end--------"); playLayer.pixelBuffer = pixelBuffer; CVPixelBufferRelease(pixelBuffer); } } ``` ## 音频解码: 音频解码和视频解码的最开始是一样的,拿到音频流,然后转码成为pcm 格式,再用苹果系统的播放API播放 首先,需要知道 摄像头 传递过来的音频格式是什么,比如视频传递的就是H.264的格式。这里我们公司使用的是 G711的音频格式、我一开始没有问清楚,按照AAC 格式解析了很久,也没有声音。后来才知道是G711这个格 式,首先先插入一段G711的介绍 ------ 它是国际电信联盟ITU-T定制出来的一套语音压缩标准,它代表了对数PCM(logarithmic pulse-code modulation)抽样标准,主要用于电话。它主要用脉冲编码调制对音频采样,采样率为8k每秒。它利用一个 64Kbps 未压缩通道传输语音讯号。 起[压缩率](https://baike.baidu.com/item/%E5%8E%8B%E7%BC%A9%E7%8E%87)为1:2, 即把16位[数据压缩](https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9)成8位。G.711是主流的[波形声音](https://baike.baidu.com/item/%E6%B3%A2%E5%BD%A2%E5%A3%B0%E9%9F%B3)编解码器。 G.711 标准下主要有两种压缩算法。一种是**µ-law algorithm** (又称often u-law, ulaw, mu-law),主要运用于北 美和日本;另一种是**A-law algorithm**,主要运用于欧洲和世界其他地区。其中,后者是特别设计用来方便计算机 处理的。 这里公司使用的是U_law算法 看上去 不知所云对吧,其实就是把音频通过某种算法,通过把16位压缩为8位 ,来将一个音频的大小压缩为之前 的一半大小,所以你拿到音频流 以后,就要把他再解压缩回来,让其变为16位的,然后再传递给 PCM 的流播放 器使用。 G711主要是对16bit的PCM进行抽样,取到PCM的高位数据,去掉低位的数据,并且只保留8位。这样压缩的比率就达到了2:1。可知是有损压缩。 ------ 然后是ios 的G711转PCM 的代码,拿到以后 转码就好了 (这里编解码 没有涉及到 AudioToolbox) ``` int Client_G711_Decode(char* pRawData, unsigned char* pBuffer, int nBufferSize) { int i=0; short *out_data = (short*)pRawData; for(i=0; i> 4; int data = alaw & 0x0f; data <<= 4; data += 8; if (exponent != 0) data += 0x100; if (exponent > 1) data <<= (exponent - 1); return (short)(sign == 0 ? data : -data); } ``` 接下来是音频播放的部分Core Audio ,以下是苹果提供的相关的API ![](http://img.blog.csdn.net/20160818143132078?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) iOS下音频流的采集和播放通常采用如下两种方案:两种方案都可以播放PCM流 1 OpenAL 跨平台解决方案,可以在多个平台下使用。类似OpenGL。 2 Audio Queue iOS下对处理复杂的音频控制需求提供的一种解决方案。 这里主要介绍Audio Queue Severver 的播放过程 ![](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Art/playback_callback_function_2x.png) 当播放时,一个音频队列缓冲器正被发送到诸如扬声器的输出设备。缓冲区队列中的剩余缓冲区排列在当前缓冲区之后,等待依次播放。音频队列按播放顺序将播放的音频数据缓冲区交给您的回调。回调将新的音频数据读入缓冲区,然后将其排入队列。 图的意思是 拿到了音频buffer 就按顺序放到 初始化的三个buffer缓存器里面,每个缓冲器的大小是固定的。第一个放满了 就继续放第二个 ,第二个放满了就放第三个 当第一个buffer 缓冲器放满了以后,就会按照顺序放到 准备播放的播放队列里面。播放队列就会按照顺序开始播放了 当 第一个播放完成以后,就会通知给buffer 缓存器继续添加了。当buffer 再次充满的时候, 就又排队到后面 这样就形成了一个循环。直到没有 buffer 为止。 然后是代码实现 首先是初始化一个 AudioPlayer 并设置 音频的采样率,声道 和每个采样点 多少bit位。这个是每个公司不一样的,要提前设置好 ``` self.audioPlayer = [[AudioPlayer alloc] init]; self.audioPlayer.delegate = self; [self.audioPlayer setSampleRate:8000 channels:1 sampleBits:16]; ``` 接下来是设置音频参数 ``` - (void)audioPlayerSetUp { ///设置音频参数 _audioDescription.mSampleRate = _sampleRate;//采样率 _audioDescription.mFormatID = kAudioFormatLinearPCM;//选用pcm _audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; _audioDescription.mChannelsPerFrame = _channels;//2;//2声道 _audioDescription.mFramesPerPacket = 1;//每一个packet一侦数据 _audioDescription.mBitsPerChannel = _sampleBits;//16;//每个采样点16bit量化 _audioDescription.mBytesPerFrame = (_audioDescription.mBitsPerChannel/8) * _audioDescription.mChannelsPerFrame; _audioDescription.mBytesPerPacket = _audioDescription.mBytesPerFrame ; ///创建一个新的从audioqueue到硬件层的通道 //AudioQueueNewOutput(&_audioDescription, AudioPlayerAQInputCallback, (__bridge void*)self, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &_audioQueue);///使用当前线程播 AudioQueueNewOutput(&_audioDescription, audioQueueOutputCallback, (__bridge void *)(self), nil, nil, 0, &_audioQueue);//使用player的内部线程播 ////添加buffer区 OSStatus result = 0; for(int i = 0;i < kAudioQueueBufferSize; i++){ result = AudioQueueAllocateBuffer(_audioQueue, kAudioPCMBufferSize, &_audioQueueBuffers[i]);///创建buffer区,MIN_SIZE_PER_FRAME为每一侦所需要的最小的大小,该大小应该比每次往buffer里写的最大的一次还大 if (result >= 0) { _audioQueueBuffers[i]->mAudioDataByteSize = kAudioPCMBufferSize; memset(_audioQueueBuffers[i]->mAudioData, 0, kAudioPCMBufferSize); AudioQueueEnqueueBuffer(_audioQueue, _audioQueueBuffers[i], 0, NULL); } else { NSLog(@"AudioPlayer Error!"); } } _audioStatus = AudioPlayerInited; } ``` 其中_audioDescription 是音频的相关参数、 然后最下面是个循环数组,对应的图示第一张图创建3个buffer,(buffers 指向的那三个小盒子) 用来存储传过来的音频。 这个里面涉及到了一个回调 AudioPlayerAQInputCallback 回调是音频队列播放完成了一个buffer,可以继续下一个传入下一个buffer了。就是图示的最后一个图callback 到再添加一个buffer 进入队列 的过程 ``` static void audioQueueOutputCallback(void *input, AudioQueueRef outQ, AudioQueueBufferRef outQB) { AudioPlayer *audioPlayer = (__bridge AudioPlayer *)input; PCMData *pcm = [audioPlayer getPcm]; if(pcm){ outQB->mAudioDataByteSize = pcm.length; memcpy(outQB->mAudioData, pcm.pcmBytes, pcm.length); AudioQueueEnqueueBuffer(outQ, outQB, 0, NULL); }else{ memset(outQB->mAudioData, 0, outQB->mAudioDataByteSize); AudioQueueEnqueueBuffer(outQ, outQB, 0, NULL); } } ``` 这里就是整个播放的流程了,其中getpcm 是从流数组中取出第一项 然后删掉,这很明显是数组队列 ,取出数据 流播放的过程。audioDataArray 是用户维护的接收pcm 的数据流,每当Audio Queue Server播完了一个buffer audioDataArray就传一个buffer 给 Audio Queue Server 。 ``` - (PCMData *)getPcm { [_lock lock]; if (_audioDataArray.count > 0) { PCMData *pcmData = [_audioDataArray objectAtIndex:0]; [_audioDataArray removeObjectAtIndex:0]; [_lock unlock]; return pcmData; } [_lock unlock]; return nil; } ``` 相对应的就是拿到数据 写入数组队列的过程,这是接收到了pcm 就不断的添加到audioDataArray 的最后一项, 来保证音频顺序的准确性。 ``` #pragma mark - pcm数据添加 - (void)addPCM:(void *)pcmBytes length:(UInt32)pcmLength timeStamp:(UInt32)timeStamp { [_lock lock]; PCMData *pcmData = [[PCMData alloc] initWithBytes:pcmBytes length:pcmLength timeStamp:timeStamp]; [_audioDataArray addObject:pcmData]; [_lock unlock]; } ``` 这两步都加锁了这些都写完以后不要忘了开启音频播放 ``` - (void)audioPlayerStart { AudioQueueStart(_audioQueue, 0); } ``` 到这里,一个基本的音频播放流程就完成了。总结一下流程 1.通过AudioQueueNewOutput 创建一个音频队列服务Audio Queue Server,需要传入 AudioStreamBasicDescription 音频描述参数,包含 采样率,声道,每个采样点 多少bit。 2.写一个for循环,通过 AudioQueueAllocateBuffer 创建3个或者n个 Audio Queue 的buffer 区,用来存放 传入的音频, 这3个buffer 共同成为了 音频播放的缓冲区。(系统自己去管理这个顺序) 3.设置 AudioQueueOutputCallback inCallbackProc 回调函数, c语言的 ,当播放完一帧(一个buffer), 就会到这个回调里面,然后就再传一个buffer 数据给 回调的 这个buffer 池子,这样他就又排到Queue最后面 4.不要忘了启动 AudioQueueStart 这个 Queue ------