IOS App项目中音视频开发杂谈

上一篇我们侃侃而谈了下Android下的App音视频开发杂谈,我们从入手到深入再到实际项目的遇到的问题以及解决方案都聊了下,那么这一次我们来杂谈下IOS项目中音视频的内容,这篇内容主要是对比上篇Android的内容,为的是熟悉IOS的朋友方便阅读观看,让我们开始吧:

首先需要了解的是音视频处理的流程:

  • 数据分别经历了解协议,解封装,音/视频解码,播放步骤,再次请上这张图:
image.png

其次是了解音频PCM的数据是怎么来的包括:

  • 怎么采样采样率是什么(8kHZ,44.1kHZ),
  • 单/双通道,
  • 样本怎么存储(8bit/16bit),
  • 一帧音频为多少样本(通常是按1024个采样点一帧,每帧采样间隔为23.22ms)
  • 每帧PCM数据大小:(PCM Buffersize=采样率采样时间采样位深/8*通道数(Bytes))
  • 每秒的PCM数据大小:(采样率×采样位深/8×声道数bps)

了解视频YUV数据是怎么来的包括:

  • YUV数据的几种格式(YUV420P,YUV420SP,NV12,NV21)的排布是怎么样的
  • 怎么计算例如YUV420P的大小
  • 怎么分解明亮度与色度

既然是音视频肯定要涉及压缩编码,那么首先应该要了解:

  • 国际标准化组织(ISO)的MPEG-1、MPEG-2与MPEG-4,的规范和标准是哪些

  • 其次要了解这个这个主流标准里面MPEG-4的音频/视频具体的一种编码格式,一般来说是AAC(MP3)与H264

  • AAC编码格式数据:要了解AAC编码的ADTS frame与ADTS头是怎么样子的

  • H264编码格式数据:要了解H264的编码格式一般主流是两种AVCC(IOS默认硬编码),Annex-B(Android默认硬编码)

  • Annex-B格式里面每个NALU的格式:包含头与payload是什么样的

  • AVCC里面extradata里面的数据格式是怎么样的(包含SPS,PPS在里面)

  • H264里面的SPS,PPS,I帧,P帧,B帧所表示的意义

说了编码当然要有解码:

  • IOS里面音频的硬解(VideoToolbox),软解(ffpmeg)怎么实现
  • IOS里面视频的硬解(AudioToolbox),软解(ffmpeg)怎么实现;

解码以后怎么播放,音频播放:

  • IOS :(包括不限于:AudioUnit ,OpenAL);
  • 播放中音频重采样(播放环境如果与样本环境不兼容则需要重采样);

解码后视频播放:

  • IOS:(包括不限于:CMSampleBuffer ,OpenGLES);
  • IOS平台 EAGL的使用

其中OpenGLES 特别是可以作为一个分支来进行加强:

  • 物体坐标系:是指绘制物体的坐标系。
  • 世界坐标系:是指摆放物体的坐标系。
  • 摄像机坐标系:摄像机的在三维空间的位置,摄像机lookat的方向向量,摄像机的up方向向量
  • 简单的绘制一些基本图形:三角形,正方形,球形
  • 纹理坐标:纹理贴图的方向以及大小
    两种投影:正射投影,透视投影
  • 着色器语言GLSL的基本语法以及使用
  • 纹理贴图显示图片
  • 处理平移、旋转、缩放等一些3x3 ,4X4的基本矩阵运算
  • FBO离屏渲染

什么是封包:

  • 然后是数据封包格式:包括MP4,TS的格式大致是什么样子的,支持哪几种音视频的编码格式;
  • DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)代表的意义;
  • TimeBase时间基在做音视频同步的意义;

音视频流媒体在网络上怎么传输:

  • 音视频在网络传输方式:HTTP,HLS,RTMP,HttpFlv

音视频应用层框架有哪些:

  • 高级应用框架:ffmpeg的基本使用
  • 高级应用框架:OpenCV的基本使用

额外需要掌握哪些技能:

  • C/C++ 基础;(话说搞OC的工程师应该都对于C有很好的理解才对)

以上是我认为作为音视频工程师入门应该掌握的知识点,我觉得掌握了这些不敢说成为了一个高手,但应该是成为一个合格的音视频工程师的 基本功

PS:基本功重要吗?我认为非常重要,往小了说基本功显示了一个人的技能扎实,拥有了扎实的基础才能往更深的方向发展;往大了说基本功显示了一个人可靠,处事沉稳可以做到了解一个事物的本质能做到万变不离其中

有了这些基本功那么我们可以接触一些实际的案例了,如果你想要更进阶那么我推荐一本我认为音视频内容比较全,而且里面有很多实战例子作为参考的书,😳再次请出这本书:

image.png

这本书我认为有几点比较好的:

  • 第一是这本书出于实战出发(据说是 唱吧App 架构师在做唱吧的时候总结了很多经验写的),

  • 第二这本书的内容包含了Android,IOS两个版本的所以有对比参考性,第三这本书从基础的音视频到高级的应用场景都介绍了,可谓是内容丰富;

说了这么多好的再说说这本书的一些不好的地方:

  • 首先就是我认为这本书不太适合刚刚入门的新手(注意是刚刚入门)如果是这类的工程师一些概念都没搞清楚的就看这个其实不是很合适;

  • 其次就是里面的例子的代码段过于松散,阅读起来需要不是很顺畅,而且git里面的Demo感觉也跟不上书里面的代码,里面的Demo目录结构不是很清晰(一般来说我们见得多的是1章分为一个或多个项目,分别讲解对应的内容互相不会干扰,书里面是git commit来区分的感觉体验性不是很好)

但是瑕不掩瑜如果你是有基础的话,那么这本书肯定能给你带了项目中的帮助。

好了,介绍了这么多基础我们马上进入项目中去看看,IOS音视频的项目问题以及解决方案

我们要实现的功能:

  • App音视频的数据怎么传输
  • App实现音视频解码
  • App实现音视频播放
  • App实现截图拍照
  • App实现录制视频
  • App实现音视频同步

App音视频的数据怎么传输:

  • App这边与嵌入式定好传输协议,协议数据大致分为协议头,协议体,协议头:包括同步码字段,帧类型,数据长度,数据方向,时间戳等等拿到数据头以后
    就可以按照长度拿到协议体数据就可以开始解码了
typedef struct
{
    HLE_U8 sync_code[3];    /*帧头同步码,固定为0x00,0x00,0x01*/
    HLE_U8 type;            /*帧类型, */
    HLE_U8 enc_std;         //编码标准,0:H264 ; 1:H265
    HLE_U8 framerate;       //帧率(仅I帧有效)
    HLE_U16 reserved;       //保留位
    HLE_U16 pic_width;      //图片宽(仅I帧有效)
    HLE_U16 pic_height;     //图片高(仅I帧有效)
    HLE_SYS_TIME rtc_time;  //当前帧时间戳,精确到秒,非关键帧时间戳需根据帧率来计算(仅I帧有效)8字节
    HLE_U32 length;         //帧数据长度
    HLE_U64 pts_msec;       //毫秒级时间戳,一直累加,溢出后自动回绕
} P2P_FRAME_HDR; //32字节

App实现实时音视频解码:

硬件码优势:更加省电,适合长时间的移动端视频播放器和直播,手机电池有限的情况下,使用硬件解码会更加好。减少CPU的占用,可以把CUP让给别的线程使用,有利于手机的流畅度。

软解码优势:具有更好的适应性,软件解码主要是会占用CUP的运行,软解不考虑社备的硬件解码支持情况,有CPU就可以使用了,但是占用了更多的CUP那就意味着很耗费性能,很耗电,在设备电量充足的情况下,或者设备硬件解码支持不足的情况下使用软件解码更加好!
  • IOS音频的硬解码:IOS的硬解码比Android的硬解码要好上太多了,IOS从8.0就开始加入了 AudioToolBoxVideoToolbox 来进行音视频的硬编解码,目前Iphone手机基本上都是8.0了,而且Iphone4S以上都支持硬解码所以兼容性肯定没的说(封闭也有封闭的好处,标准全部统一,对于开发来说就简单),而且SDK的使用其实也很简单,我们先来聊聊音频的硬解码 AudioToolBox 的使用,主要是这个方法:
AudioConverterFillComplexBuffer(    AudioConverterRef                   inAudioConverter,
                                    AudioConverterComplexInputDataProc  inInputDataProc,
                                    void * __nullable                   inInputDataProcUserData,
                                    UInt32 *                            ioOutputDataPacketSize,
                                    AudioBufferList *                   outOutputData,
                                    AudioStreamPacketDescription * __nullable outPacketDescription)

inAudioConverter : 转码器
inInputDataProc : 回调函数。用于将AAC数据喂给解码器。
inInputDataProcUserData : 用户自定义数据指针。
ioOutputDataPacketSize : 输出数据包大小。
outOutputData : 输出数据 AudioBufferList 指针。
outPacketDescription : 输出包描述符。

解码的具体步骤如下:首先,从媒体文件中取出一个音视帧。其次,设置输出地址。然后,调用 AudioConverterFillComplexBuffer 方法,该方法又会调用 inInputDataProc 回调函数,将输入数据拷贝到编码器中。最后,解码。将解码后的数据输出到指定的输出变量中。

  • IOS视频的硬解码:刚刚聊了音频的硬解码是用 AudioToolBox ,下面到视频的硬解码实现,下面请出 VideoToolbox ,首先创建解码器:
VTDecompressionSessionCreate(
    CM_NULLABLE CFAllocatorRef                              allocator,
    CM_NONNULL CMVideoFormatDescriptionRef                  videoFormatDescription,
    CM_NULLABLE CFDictionaryRef                             videoDecoderSpecification,
    CM_NULLABLE CFDictionaryRef                             destinationImageBufferAttributes,
    const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback,
    CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut)

各参数详细介绍:
allocator : session分配器,NULL使用默认分配器。
videoFormatDescription : 源视频帧格式描述信息。
videoDecoderSpecification : 视频解码器。如果是NULL表式让 VideoToolbox自己选择视频解码器。
destinationImageBufferAttributes: 像素缓冲区要求的属性。
outputCallback: 解码后的回调函数。
decompressionSessionOut: 输出Session实列。

然后开始解码:

VT_EXPORT OSStatus
VTDecompressionSessionDecodeFrame(
    CM_NONNULL VTDecompressionSessionRef    session,
    CM_NONNULL CMSampleBufferRef            sampleBuffer,
    VTDecodeFrameFlags                      decodeFlags, // bit 0 is enableAsynchronousDecompression
    void * CM_NULLABLE                      sourceFrameRefCon,
    VTDecodeInfoFlags * CM_NULLABLE         infoFlagsOut) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

session : 创建解码器时创建的 Session。
sampleBuffer : 准备被解码的视频帧。
decodeFlags : 解码标志符。 0:代表异步解码。
sourceFrameRefCon : 用户自定义参数。(输出解码数据)
infoFlagsOut : 输出参数标记。

需要注意的是,如果你的硬解码出来的数据是要转换为 UIImage 贴图显示的话那么在配置解码器的时候要注意配置参数:

    //      kCVPixelFormatType_420YpCbCr8Planar is YUV420
    //      kCVPixelFormatType_420YpCbCr8BiPlanarFullRange is NV12
    //      kCVPixelFormatType_24RGB    //使用24位bitsPerPixel
    //      kCVPixelFormatType_32BGRA   //使用32位bitsPerPixel,kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst
    uint32_t pixelFormatType = kCVPixelFormatType_32BGRA;
    const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };
    const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &pixelFormatType) };
    CFDictionaryRef attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    VTDecompressionOutputCallbackRecord callBackRecord;
    callBackRecord.decompressionOutputCallback = didDecompress;    
    callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
    status = VTDecompressionSessionCreate(kCFAllocatorDefault,
                                          mDecoderFormatDescription,
                                          NULL,
                                          attrs,
                                          &callBackRecord,
                                          &mDeocderSession);

我是利用 kCVPixelFormatType_32BGRA 与 kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst 来配合出图的(下面视频播放的时候我会再提到这种方式)

  • IOS音视频的软解码:
    软解码首推的就是ffmpeg,ffmpeg的使用还是很简单的,简单的来说你只需要一开始初始化 编解码格式对象 AVCodecContext 与编解码器 AVCodec ,然后把数据填充AvPacket ,然后解码成 AvFrame 就可以了。

App实现音频的播放:

  • 音频的重采样:有时候在音频播放的时候,会出现你的音源与播放设备的硬件条件不匹配,例如播放每帧的样本数不匹配,采样位数不匹配的情况,那么这个时候需要用到对于音源PCM重采样,重采样以后才能正常播放,
int len = swr_convert(actx,outArr,frame->nb_samples,(const uint8_t **)frame->data,frame->nb_samples);

主要是通过 swr_convert 来进行转换

/** Convert audio.
 *
 * in and in_count can be set to 0 to flush the last few samples out at the
 * end.
 *
 * If more input is provided than output space, then the input will be buffered.
 * You can avoid this buffering by using swr_get_out_samples() to retrieve an
 * upper bound on the required number of output samples for the given number of
 * input samples. Conversion will run directly without copying whenever possible.
 *
 * @param s         allocated Swr context, with parameters set
 * @param out       output buffers, only the first one need be set in case of packed audio
 * @param out_count amount of space available for output in samples per channel
 * @param in        input buffers, only the first one need to be set in case of packed audio
 * @param in_count  number of input samples available in one channel
 *
 * @return number of samples output per channel, negative value on error
 */
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
                                const uint8_t **in , int in_count);

out表示的是输出buffer的指针;
out_count表示的是输出的样本大小;
in表示的输入buffer的指针;
in_count表示的是输入样品的大小;

转换成功后输出的音频数据再拿来播放就可以在指定的条件进行指定的播放

  • 音频软解码的播放:这种情况下一般我们推荐的还是利用 OpenSLES 来播放
    //设置回调函数,播放队列空调用
    (*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
    //设置为播放状态
    (*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
    //启动队列回调
    (*pcmQue)->Enqueue(pcmQue,"",1);
  • 音频的硬解码播放:这种情况下播放使用SDK自带的 AudioUnit 来进行播放,首先创建对象:
// 获得 Audio Unit
status = AudioComponentInstanceNew(inputComponent, &audioUnit);

然后配置属性:

// 为播放打开 IO
status = AudioUnitSetProperty(audioUnit, 
                              kAudioOutputUnitProperty_EnableIO, 
                              kAudioUnitScope_Output, 
                              kOutputBus,
                              &flag, 
                              sizeof(flag));
checkStatus(status);

// 设置播放格式
status = AudioUnitSetProperty(audioUnit, 
                              kAudioUnitProperty_StreamFormat, 
                              kAudioUnitScope_Input, 
                              kOutputBus, 
                              & outputFormat,  //参见编码器格式
                              sizeof(audioFormat));
checkStatus(status);

// 设置声音输出回调函数。当speaker需要数据时就会调用回调函数去获取数据。它是 "拉" 数据的概念。
callbackStruct.inputProc = playbackCallback;
callbackStruct.inputProcRefCon = self;
status = AudioUnitSetProperty(audioUnit, 
                              kAudioUnitProperty_SetRenderCallback, 
                              kAudioUnitScope_Global, 
                              kOutputBus,
                              &callbackStruct, 
                              sizeof(callbackStruct));

然后播放PCM:

AudioOutputUnitStart(audioUnit);

App 视频的播放:

  • 视频软解播放:这个当然是首先 opengles ,拿到YUV数据,设置好贴图坐标,使用YUV数据分别贴图来播放显示,例子如下:
        sh.GetTexture(0,width,height,data[0]);  // Y
        if(type == XTEXTURE_YUV420P)
        {
            sh.GetTexture(1,width/2,height/2,data[1]);  // U
            sh.GetTexture(2,width/2,height/2,data[2]);  // V
        }
        else
        {
            sh.GetTexture(1,width/2,height/2,data[1], true);  // UV
        }
        sh.Draw();
  • 视频硬解的播放:这个方式非常直接,利用SDK硬解码出来的数据 CVPixelBufferRef 转换为 UIImage这种方式看似简单但是坑也最多,我总结了以下几总转换的方式以及测试结果,😁敲黑板了注意听讲:

我的测试手机为两部一部IphoneX,一部为Iphone5S(一部高端的一部低端的), didDecompress 方法是硬解码的回调函数,这个不解释了

  • 第一种是:
- static void didDecompress(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration )
{
    VDh264Decoder *delegateSelf = (__bridge VDh264Decoder *)decompressionOutputRefCon;
    if (pixelBuffer==nil) {
        return;
    }
    
    CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
    *outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
}

   CVImageBufferRef imageBuffer = pixelBuffer;
   CVPixelBufferLockBaseAddress(imageBuffer, 0);
   void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
   size_t width = CVPixelBufferGetWidth(imageBuffer);
   size_t height = CVPixelBufferGetHeight(imageBuffer);
   size_t bufferSize = CVPixelBufferGetDataSize(imageBuffer);
   size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
   CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
   CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, baseAddress, bufferSize, NULL);
CGImageRef cgImage= CGImageCreate(width, height, 8,32, bytesPerRow, rgbColorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, provider, NULL, false, kCGRenderingIntentDefault);
   UIImage * image = [UIImage imageWithCGImage:cgImage];

  if (delegateSelf.delegate && [delegateSelf.delegate respondsToSelector:@selector(decoderSuccessGetImg:saveImg:)]) {
        [delegateSelf.delegate decoderSuccessGetImg:nil saveImg:image];
   }

   CGImageRelease(cgImage);
   CGDataProviderRelease(provider);
   CGColorSpaceRelease(rgbColorSpace);
   CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
   CVPixelBufferRelease(imageBuffer);

这种方式理论上说不能正常运行,我调试了很久原因就在 CVPixelBufferRef 这个对象的释放问题,因为一开始就对他进行了Retain(CVPixelBufferRef 是C对象不是OC对象所以没有办法进行ARC,需要手动的Retain,Release)

CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
    *outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);

但是你最后这句releases会引发空指针问题,

CVPixelBufferRelease(imageBuffer);

究其原因我猜想是由于,生成的 UIImage 正在使用,虽然你在他后面才进行了release,但是这种还是会影响他这块内存所以会有空指针问题(网络上基本上搜不到答案,我的结论是我自己测试出来的,听我往下讲

于是我把前面的retain ,release 去掉:也就是这三句话去掉

CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;  //去掉
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);  //去掉
CVPixelBufferRelease(imageBuffer);  //去掉

很悲剧的这种方式直接空指针报错,根据调试开看应该是 CVPixelBuffer 被提前释放了,所以你生成的 UIImage 没法在主线程使用

那把末尾的release去掉呢,

CVPixelBufferRelease(imageBuffer); //去掉

这种情况会出图但是,你会发现你的内存在暴涨,因为这个 CVPixelBuffer 这个对象没有手动释放,(也说明了这种生成图片的方式,对于 CVPixelBuffer 的释放不太好处理至少SDK没有什么好的办法),我甚至想了个办法把这个 CVPixelBuffer 对象转为OC对象想用ARC来管理它还是不行,这种生成图片的方式Pass掉

  • 第二种最简单,也是网络上经常看见的方法:
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
UIImage *image = [UIImage imageWithCIImage:ciImage]; 

简单归简单,但是这种方式太耗内存了,不是说内存一直涨,而是固定就很高,尤其是IphoneX上面非常明显,为了性能着想不可取(其实也没到使用不了的地步,只不过是想要最优的方案,才有了下面的尝试)

  • 第三总在第二总的方式上面做了些许改动:
CIContext *context = [CIContext contextWithOptions:nil];
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
UIImage *image = [[UIImage alloc] initWithCGImage:cgImage];
CGImageRelease(cgImage); //没有此句话无法释放内存

这种方式内存没有那么夸张了,但是CPU使用却上来了,而且上升很明显,Iphone快达到了50%,Iphone5S已经接近90%,也不可取

  • 最后一种稳定的方式:
      CVImageBufferRef imageBuffer = pixelBuffer;
      CVPixelBufferLockBaseAddress(imageBuffer, 0);
      uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer);
      size_t width = CVPixelBufferGetWidth(imageBuffer);
      size_t height = CVPixelBufferGetHeight(imageBuffer);
      size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);

      CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
      CGContextRef cgContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
      CGImageRef cgImage = CGBitmapContextCreateImage(cgContext);
      UIImage *image = [UIImage imageWithCGImage:cgImage];

      if (delegateSelf.delegate && [delegateSelf.delegate respondsToSelector:@selector(decoderSuccessGetImg:saveImg:)]) {
            [delegateSelf.delegate decoderSuccessGetImg:nil saveImg:image];
       }

      CGImageRelease(cgImage);
      CGContextRelease(cgContext);
      CGColorSpaceRelease(rgbColorSpace);
      CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

这种方式不需要手动retain,release CVPixelBuffer 了,而且使用 CGContextRef 代替了 CGDataProviderRef 去生成 CGImageRef ,经过长时间测试这种方式CPU与内存都是稳定输出

经过测试与观察这种方式其实效率看起来并不低,Iphone5S都能正常的播放,而且参照了同类方案商的SDK,分析了他们的显示发现也是转为 UIImage 来进行显示的,说明这种显示方式应该是一种主流的方式,不像网络上说的那样性能低下,性能低下很可能主要是使用方式不对造成的,最后要注意的是生成 CGContextRef 使用这个配置:

CGContextRef cgContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);

App实现截图拍照:

  • 不论是硬解码,还是软解码最后出来的数据应该都是YUV数据那么,利用YUV数据生成图片方法很多,要看具体需求,例如 libyuv 库来做这个;不过IOS平台如果你是硬解码成 CVPixelBufferRef 以后以 UIImage 来显示的话,那么你直接可以利用 UIImage 来生成图片更简单(我们目前就是)
UIImage *getImage = [UIImage imageWithContentsOfFile:file];
NSData *data;
if (UIImagePNGRepresentation(getImage) == nil){
   data = UIImageJPEGRepresentation(getImage, 1);
} else {
   data = UIImagePNGRepresentation(getImage);
}

App实现录制视频:
录制视频说白了就是封包,把编码过的音频AAC,视频H264封装为一个数据格式,常见的格式Mp4,TS等等

  • 音视频硬解码的封包:

如果是通过 AudioToolBox 与 VideoToolbox 硬解码音视频的话,那么封包就就是用SDK里面的 AVFoundation中的AVAssetWriter 来进行写封包:

assetVideoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:compressionVideoSetting];
assetAudioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:compressionAudioSetting];

[assetVideoWriterInput appendSampleBuffer:buffer];
[assetAudioWriterInput appendSampleBuffer:buffer];

但是这种SDK封包的时候要注意几个事项,我们是打算封装成视频H264 ,音频AAC的Mp4文件在进行的时候就总结出以下几个问题:

  • 1 如果是单独封装H264编码过的视频的话没有问题,AVAssetWriter封装Mp4能成功,传到手机能播放,第三方播放器可以播放

  • 2 如果是音频编码过的AAC,视频编码过的H264,利用AVAssetWriter封装Mp4能输出文件,但是传到手机就是不能正常播放,但是第三方部分播放器可以播放

  • 3 后来再试了音频PCM,视频YUV进行封包AVAssetWriter封装Mp4能成功,传到手机能播放,第三方播放器可以播放

  • 4 后来实在不行我们就试了音频用PCM裸音源,视频用H264来进行Mp4封包就可以了,传到手机能播放,第三方播放器可以播放

  • 5 再后来我们对比了同类产品的10秒Mp4封包体积,发现个问题基本上同类产品的体积都比我的大,我们的体积是他们的1/3左右,估计他们就是PCM,YUV进行封包的所以体积比较大,我们算是这个体验比对手产品的要好

  • 如果是ffmpeg软解码的话那么ffmpeg的SDK里面就包含了封包的方法:
    初始化三个** AVFormatContext** 容器,一个音频一个视频的用来作为输入的AAC,H264的容器,另外一个作为输出的容器,还有一个 AVOutputFormat
    输出格式化对象,简单的来说就是读出一个AvPacket然后处理好PTS,DTS以后往对应流的输出容器去写即可,涉及的函数:
avformat_open_input():打开输入文件。
avcodec_copy_context():赋值AVCodecContext的参数。
avformat_alloc_output_context2():初始化输出文件。
avio_open():打开输出文件。
avformat_write_header():写入文件头。
av_compare_ts():比较时间戳,决定写入视频还是写入音频。这个函数相对要少见一些。
av_read_frame():从输入文件读取一个AVPacket。
av_interleaved_write_frame():写入一个AVPacket到输出文件。
av_write_trailer():写入文件尾。

App实现音视频同步:

  • 音视频同步的话选择一般来说有以下三种:
将视频同步到音频上:就是以音频的播放速度为基准来同步视频。
将音频同步到视频上:就是以视频的播放速度为基准来同步音频。
将视频和音频同步外部的时钟上:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

这三种是最基本的策略,考虑到人对声音的敏感度要强于视频,频繁调节音频会带来较差的观感体验,且音频的播放时钟为线性增长,所以一般会以音频时钟为参考时钟,视频同步到音频上,音频作为主导视频作为次要,用视频流来同步音频流,由于不论是哪一个平台播放音频的引擎,都可以保证播放音频的时间长度与实际这段音频所代表的时间长度是一致的,所以我们可以依赖于音频的顺序播放为我们提供的时间戳,当客户端代码请求发送视频帧的时候,会先计算出当前视频队列头部的视频帧元素的时间戳与当前音频播放帧的时间戳的差值。如果在阈值范围内,就可以渲染这一帧视频帧;如果不在阈值范围内,则要进行对齐操作。具体的对齐操作方法就是:如果当前队列头部的视频帧的时间戳小于当前播放音频帧的时间戳,那么就进行跳帧操作(具体的跳帧操作可以是加快速度播放的实现,也可以是丢弃一部分视频帧的实现 );如果大于当前播放音频帧的时间戳,那么就进行等待(重复渲染上一帧或者不进行渲染)的操作。其优点是音频可以连续地播放,缺点是视频画面有可能会有跳帧的操作,但是对于视频画面的丢帧和跳帧,用户的眼睛是不太容易分辨得出来的

一般来说视频丢帧是我们常见的处理视频慢于音频的方式,可以先计算出需要加快多少时间,然后根据一个GOP算出每一帧的时间是多少,可以得出需要丢多少帧,然后丢帧的时候要注意的是必须要判断,不能把I帧丢了,否则接下来的P帧就根本用不了,而应该丢的是P帧,也就是一个GOP的后半部分,最合适的情况就是丢一整个GOP,如果是丢GOP后半部分的话你需要一开始播放GOP的时候弄一个变量记录当前是第几个P帧了,然后计算出需要丢几个P帧才能和音频同步,然后到了那一个需要丢的帧到来的时候直接抛弃,即到下一个I帧到来的时候才进行渲染(这里面有可能丢的不是那么准确,可能需要经过几个的丢帧步骤才能准确同步)

好了,我们IOS开发中的音视频杂谈就到这里了,我们洋洋洒洒的谈了这么多,主要是方案部分,也包括了项目中的一些“坑”,如果大家喜欢的话接下来我会把细节部分再分别写一些东西出来,😊希望大家多多留言讨论,想看Android音视频开发杂谈的出门左转即可

《Android App项目中音视频开发杂谈》

···

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容