IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> iOS音视频开发二 iOS硬编码实现 -> 正文阅读

[移动开发]iOS音视频开发二 iOS硬编码实现

简述

上一章,我们介绍了iOS采集相关细节。这一次,我来介绍下iOS硬编码相关知识。

首先,为什么需要编码,在上一次中,我们提到了一个东西,CMSampleBuffer,这个既可以用来封装ImageBuffer,也可以用于存储裸流数据,是一个通用的结构体。我先介绍一下这个是什么东西,之后就明白为啥需要编码了。

初识CVPixelBufferRef

在我们通常的颜色世界里面,我们都知道RGB三原色,使用这三种颜色的混合搭配,可以组成世界上的绝大多数色彩。
在计算机里面,我们称这种格式为kCVPixelFormatType_32RGBA,即每32位的数据里面,包含8位的R,8位的G,8位的B,8位的A。而通常,我们使用的却不是这个,而是另外一个,即kCVPixelFormatType_32BGRA。

到这里,我们能明白,如果使用RGBA来代表一张图,一个像素点就包含4字节,每一个字节代表个字位的原色。那么我们如果需要看一个60秒30fps720p的视频,那么我们需要720 * 1280 * 4 * 60 * 30 = 6635520000b数据,相当于791MB数据,我觉得大部分人的网速连10MB/s都可能达不到,更何况791MB/s,如果不经过视频编码,我们根本无法再现观看视频,更不用说直播了。

那在iOS里面是如何存储这些数据呢?那个对象,就是CVPixelBufferRef,CVPixelBufferRef实际上就是一张图画的像素点的布局。它可以是以下几种类型中的一种:

图像类型含义
kCVPixelFormatType_32RGBARGBA32位,布局为RGBA
kCVPixelFormatType_32BGRABGRA32为,布局为BGRA
kCVPixelFormatType_420YpCbCr8Planar / kCVPixelFormatType_420YpCbCr8PlanarFullRangeI420数据排列,布局为前面height * strides[0]为Y分量,中间height * strides[1] / 2为U分量,后面的数据为V分量
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRangeNV12数据排列,布局为height * strides[0]为Y分量,height * strides[0]为UV分量,为比较常用的类型,运用于编码,解码,渲染中。UV数据范围为(luma=[16,235] chroma=[16,240])
kCVPixelFormatType_420YpCbCr8BiPlanarFullRangeNV12数据排列,布局为height * strides[0]为Y分量,height * strides[0]为UV分量,为比较常用的类型,运用于编码,解码,渲染中。UV数据范围为(luma=[0,255] chroma=[0,255])
kCVPixelFormatType_OneComponent8Gray分量,即Y分量,常用于端上画质增强的输入源。
kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange / kCVPixelFormatType_420YpCbCr10BiPlanarFullRangeHDR10相关数据源,后期会解释如何解码得到这种数据源。

我们知道了CVPixelBufferRef是一个什么样的东西,那我们如何创建一个CVPixelBuffer呢?我们看下相关的函数原型

/*!
 * @param CFAllocatorRef 内存申请函数,可采用默认的kCFAllocatorDefault。
 * @param width 需要创建的画布的宽。
 * @param height 需要创建的画布的高。
 * @param pixelFormatType 画布的类型,可以是之前提到的几个中的一个,或者是定义好的。
 * @param pixelBufferAttributes 画布的属性,可以指定iOSSurface或者OpenGL属性。
 * @param pixelBufferOut 创建的画布实例的引用指针。
 */
CV_EXPORT CVReturn CVPixelBufferCreate(
    CFAllocatorRef CV_NULLABLE allocator,
    size_t width,
    size_t height,
    OSType pixelFormatType,
    CFDictionaryRef CV_NULLABLE pixelBufferAttributes,
    CV_RETURNS_RETAINED_PARAMETER CVPixelBufferRef CV_NULLABLE * CV_NONNULL pixelBufferOut) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);

示例代码:

int width = 1280;
int height = 720;
OSType format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;

const void *keys[] = {
    kCVPixelBufferOpenGLESCompatibilityKey,
    kCVPixelBufferIOSurfacePropertiesKey
};
        
const void *values[] = {
    (__bridge void *)[NSNumber numberWithBool:YES],
    (__bridge void *)[NSDictionary dictionary]
};
CFDictionaryRef attributes = CFDictionaryCreate(NULL, keys, values, 2, NULL, NULL);
CVPixelBufferRef pixelBuffer = nil;
CVReturn ret = CVPixelBufferCreate(kCFAllocatorDefault, 
                                   width, 
                                   height, 
                                   format, 
                                   attributes, 
                                   &pixelBuffer);

我们已经可以自己创建CVPixelBufferRef了,yeah~不过,好像和我们本章的话题没啥关系。
其实也不然,在音视频里面,时常都需要进行图像的裁剪或者美颜,需要对图像能进行处理,而深入理解CVPixelBufferRef,有助于我们很好的处理这样的一种图源。接下来,我们来访问下这个CVPixelBufferRef相关的图源数据吧。
函数原型:

/*!
 * @brief 获取Buffer的内存指针偏移量,用于访问内存。
 * @param pixelBuffer 需要访问内存的buffer
 * @param planeIndex 需要访问内存的buffer的第几分量。
 */
CV_EXPORT void * CV_NULLABLE CVPixelBufferGetBaseAddressOfPlane(CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);

/**
 * @brief 用于获取一行像素有多少个元素,类似stride,这是由于底层会采用32位或者64位来对齐数据。
 * @param pixelBuffer 需要访问的buffer
 * @param planeIndex 需要访问的第几分量
 */
CV_EXPORT size_t CVPixelBufferGetBytesPerRowOfPlane( CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);

/**
 * @brief 获取Buffer中,某个分量的宽
 */
CV_EXPORT size_t CVPixelBufferGetWidthOfPlane( CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);

/**
 * @brief 获取Buffer中,某个分量的高
 */
CV_EXPORT size_t CVPixelBufferGetHeightOfPlane( CVPixelBufferRef CV_NONNULL pixelBuffer, size_t planeIndex ) __OSX_AVAILABLE_STARTING(__MAC_10_4,__IPHONE_4_0);

示例代码:

// 获取第n个分量的指针,类似YUV420,第0分量为Y,第1分量为U,第2分量为V
void* address = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, n);
size_t bytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, n);
size_t height = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, n);
for (size_t i = 0; i < (n == 0 ? height : height / 2); ++i) {
    for (size_t j = 0; j < bytes; ++j) {
        // todo update address
    }
}

至于CVPixelBufferRef,它本是一个c层的指针数据,不具有oc的内存自动回收机制,需要自己释放内存,释放内存的代码如下:

CVPixelBufferRelease(pixelBuffer);

到此,你已经初步的知道,如何生成CVPixelBufferRef,如何操作CVPixelBufferRef,如何销毁CVPixelBufferRef了。这对于我们后续接入x264软编码器还是很重要的。

VTCompressionSessionRef

既然我们理解了iOS是如何存储图像,那我们就来聊聊iOS的硬编码器。
其实,无论是哪个编码器,都变不了流程上的问题。编码的流程如下:

  • 创建编码器
  • 配置编码器
  • 开始编码
  • 重置编码器
  • 销毁编码器

按着以上的流程,无论是Android编码器,还是软编码器,还是其他的编码器,其根本都是这样的,我们只要能找到相关的流程代码,就可以完成相关流程的代码了。

创建编码器

/*!
 * @brief 创建VTB编码器函数原型
 * @param allocator 指定相关的内存申请函数
 * @param width 编码的宽
 * @param height 编码的高
 * @param codecType 编码类型,可以为AVC(kCMVideoCodecType_H264),也可以为HEVC(kCMVideoCodecType_HEVC)
 * @param encoderSpecification
 * @param compressedDataAllocator 用于指定是否采用iOSSurface以及输入的图像类型,参考之前的kCVPixelFormatType
 * @param outputCallback 设置回调函数,用于接受编码完成的数据源。
 * @param compressionSessionOut 编码器指针
 */
VT_EXPORT OSStatus 
VTCompressionSessionCreate(
	CM_NULLABLE CFAllocatorRef							allocator,
	int32_t												width,
	int32_t												height,
	CMVideoCodecType									codecType,
	CM_NULLABLE CFDictionaryRef							encoderSpecification,
	CM_NULLABLE CFDictionaryRef							sourceImageBufferAttributes,
	CM_NULLABLE CFAllocatorRef							compressedDataAllocator,
	CM_NULLABLE VTCompressionOutputCallback				outputCallback,
	void * CM_NULLABLE									outputCallbackRefCon,
	CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

上面为编码器的创建函数原型,如何使用,可以参考LWVideoEncoder.m里面的createSession函数,下面为具体的代码示例:

void compressSessionOutputCallback(void *opaque,
                                   void *sourceFrameRef,
                                   OSStatus compressStatus,
                                   VTEncodeInfoFlags infoFlags,
                                   CMSampleBufferRef sampleBuf) {
}

// ......
long pixelBufferType = self.config.pixelBufferType;
CFDictionaryRef ioSurfaceValue = CFDictionaryCreate(kCFAllocatorDefault,
                                                    nil,
                                                    nil,
                                                    0,
                                                    &kCFTypeDictionaryKeyCallBacks,
                                                    &kCFTypeDictionaryValueCallBacks);
CFTypeRef pixelBufferFormatValue = (__bridge CFTypeRef)@(pixelBufferType);
CFTypeRef keys[3] = {
    kCVPixelBufferOpenGLESCompatibilityKey, kCVPixelBufferIOSurfacePropertiesKey, kCVPixelBufferPixelFormatTypeKey
};
CFTypeRef values[3] = {
    kCFBooleanTrue, ioSurfaceValue, pixelBufferFormatValue
};
CFDictionaryRef sourceAttributes = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
if (ioSurfaceValue) {
    CFRelease(ioSurfaceValue);
    ioSurfaceValue = nil;
}

status = VTCompressionSessionCreate(nil,
                                    self.config.width,
                                    self.config.height,
                                    self.config.codecType,
                                    0,
                                    sourceAttributes,
                                    0,
                                    compressSessionOutputCallback,
                                    (__bridge void * _Nullable)(self),
                                    &_encoderSession);

配置编码器

配置编码器则为编码器工作的主要任务,当然,也有一些开发人员需要对编码器内核进行修正,优化,当然,作为端上编码SDK的开发,日常还是调试参数而已。既然说到调试参数,就不得不把iOS硬编码器的所有参数都列举一遍,后面可以好好学习下。

配置变量含义
kVTCompressionPropertyKey_RealTime实时编码,1为开启,0为关闭。
开启,编码器会以速度为优先考虑,而不考虑画质,在这个模式里面,有可能出现中低码率马赛克问题,但是编码速度相比于关闭有了一点提升,常用于直播行业低延时要求。
关闭,速度上会得到一些限制,但是画质上得到了满足,可以缓解中低码率的马赛克问题,提供画质,常用于端上转码。
kVTCompressionPropertyKey_AllowFrameReordering是否允许B帧编码,开启,则允许,否则,关闭B帧编码
kVTCompressionPropertyKey_AllowTemporalCompression是否允许压缩,默认为是,可以编码P,B帧,否则,只编码I帧
kVTCompressionPropertyKey_MaxKeyFrameInterval可以近似的认为是GOP,即多少个帧之后出来一个I帧
kVTCompressionPropertyKey_AllowOpenGOP开放GOP,允许编码的时候前向参考I帧
kVTCompressionPropertyKey_NumberOfPendingFrames设置缓冲队列的大小,设置为1,则每一帧都实时输出
kVTCompressionPropertyKey_ProfileLevel设置session的profile和level,一般设置为kVTProfileLevel_H264_Baseline_AutoLevel, kVTProfileLevel_H264_Main_AutoLevel, kVTProfileLevel_H264_High_AutoLevel, kVTProfileLevel_HEVC_Main_AutoLevel这几个
kVTCompressionPropertyKey_ExpectedFrameRate期望帧率,这东西,其实没啥用,输出是根据设置的dts/pts设置帧率的
kVTEncodeFrameOptionKey_ForceKeyFrame是否强制为I帧
kVTCompressionPropertyKey_AverageBitRate编码码率配置

上面就是目前用过的相关配置,下面给几个简单的示例代码:

// 配置CFBooleanRef
VTSessionSetProperty(_encoderSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);

// 配置数字
VTSessionSetProperty(_encoderSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(value));

开始编码

CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
CMTime duration = CMSampleBufferGetDuration(sampleBuffer);
CVImageBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
VTEncodeInfoFlags infoFlags = 0;
// 参数一,指定编码器session。
// 参数二,指定需要编码的PixelBuffer。
// 参数三,指定相关的pts,影响画质+帧率,由回调数据SampleBuffer带出来。
// 参数四,指定相关的duration。
// 参数五,可以指定是否输出关键帧。
// 参数六,透传数据。
// 参数七,一个info的引用,回调会提示该帧是丢弃还是等待同步。
OSStatus status = VTCompressionSessionEncodeFrame(_encoderSession, pixelBuffer, pts, duration, nil, sourceFrameRef, &infoFlags);

// 如果不成功,则判断下是否当前session已经失效了
// 由于切后台,iOS会把所有的session都重置为失效,这个时候,需要销毁然后重新创建编码器。
if (status != kCVReturnSuccess) {
    if (status == kVTInvalidSessionErr) {
        [self destroySession];
        [self createSession:self.config];
    }
    return kLWVideoEncodeStatus_Err_Encode;
}

这样子,我们从采集拿到的CMSampleBufferRef就可以直接使用了。编码完毕之后,我们可以在compressSessionOutputCallback里面获取到对应的输出。到此,硬编码的流程就跑通了。

重置编码器

这一块比较常用的应该在设置码率上面,也可以从项目里面看到:

// 动态设置编码码率
- (void)setBitrate:(int)bitrate {
    bitrate = bitrate * 1024;
    OSType status = VTSessionSetProperty(_encoderSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(bitrate));
    if (status != kCVReturnSuccess) {
        NSLog(@"%s:%d error with %d", __func__, __LINE__, status);
    }
}

其他的,硬编码只能通过重启编码器来重置相关属性。

销毁编码器

- (void)destroySession {
    if (_encoderSession) {
        VTCompressionSessionCompleteFrames(_encoderSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(_encoderSession);
        CFRelease(_encoderSession);
        _encoderSession = nil;
    }
    NSLog(@"%s:%d", __func__, __LINE__);
}

总结

到此,关于iOS硬编码相关的知识已经全部梳理完毕,小伙伴们快动手做做吧。下一期,我们来讲解下如何提取出输出的数据,转化为AVC或者HEVC,感兴趣的小伙伴也可以直接先动手查阅资料。


相关代码可查询这里

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-03 12:02:02  更:2021-09-03 12:02:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/31 5:54:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码