ibabyblue's Blog

心之所向 身之所往


  • Home

  • Tags

  • Categories

  • Archives

YUV与RGB格式转换

Posted on 2020-04-27 | In 计算机图形学
一、What is YUV?

YUV是编译true-color颜色空间(color space)的种类,Y’UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma),如果没有U(Cb)、V(Cr),只有Y亮度信息,也可以显示图像,即黑白图像没有色彩而已。

YUV这种彩色编码系统,主要用于视频、图形处理管线中(pipeline)。最终呈现在屏幕上的图像,都是有RGB格式呈现的,但是为什么引入YUV呢?这是因为YUV可节省带宽,YUV分为多种格式,其中YUV420P类型采样数据大小为RGB格式的一半,在直播模式下,数据传输时大小显得尤为重要。
直播模式下,YUV与RGB的应用:
1.摄像头采集视频帧,设置输出的数据格式为YUV420P。
2.视频帧编码
3.视频帧数据推流传输
4.视频帧数据解码
5.YUV420P数据转换为RGB格式,渲染。

YUV采样格式:
1.YUV444
2.YUV422
3.YUV420

YUV420:
1.YUV420P
1).YU12格式(YYYYUUVV)
2).YV12格式(YYYYVVUU)
即先存储Y分量,再存储U、V分量,区别在于:YU12是先Y再U后V,而YV12是先Y再V后U。
3.YUV420SP
1).NV12格式(YYYYUVUV)
2).NV21格式(YYYYVUVU)
NV12和NV21格式都属于YUV420SP类型。NV12先存Y分量,再UV进行交替存储,NV21先存Y分量,在VU交替存储。

YUV 类型 采样格式
YUV 420P 类型 YV12 格式、YU12 格式
YUV 420SP 类型 NV12 格式、NV21 格式

YUV其他格式可参考:一文读懂 YUV 的采样与格式

二、YUV、RGB转换公式

YCbCr是被ITU定义在标准ITU-R BT.601(标清)、ITU-R BT.709(高清)、ITU-R BT.2020(超高清)中的一种色彩空间,是一种针对RGB所做的编码,是YUV压缩和偏移的版本,YCbCr通常用于视频和图像压缩和传输,在视频编解码中的”YUV图像”指的就是 YCbCr。
YCbCr和RGB的转换,不同的标准有不同的转换公式,并且需要区分Video Range和Full Range。
Full Range:计算机设备 PC Level(色域全面[0-255])
Video Range:电视机等消费类产品 Studio Level(色域不够全面[16-235]),省略不常用的色域。
移动端在直播模式下通常使用Video Range(非必然)。
iOS CoreVideo中定义如下:

1
2
3
4
5
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange = '420v', 
/* Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */

kCVPixelFormatType_420YpCbCr8BiPlanarFullRange = '420f',
/* Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */

Full Range的取值范围为luma=[0,255] chroma=[1,255],而Video Range是luma=[16,235] chroma=[16,240]。

转换公式如下:
1、BT.601, which is the standard for SDTV.
转换公式-标清(SD)RGB to YUV:
Y = 0.299R+0.587G+0.114B
Cr = V = 0.713(R−Y)=0.500R−0.419G−0.081B
Cb = U = 0.564(B−Y)=−0.169R−0.331G+0.500B

YUV to RGB:
R = 1.164(Y−16.0/255)+1.596(V−128.0/255)
G = 1.164(Y−16.0/255)−0.813(V−128.0/255)−0.391(U−128.0/255)
B = 1.164(Y−16.0/255)+2.018(U−128.0/255)
即:
R = 1.164Y+1.596V-0.871;
G = 1.164Y-0.813V-0.391U+0.529;
B = 1.164Y+2.018U-1.0729;

上述RGB的取值范围是[0,1]。Y的范围是 [0,1],Cr和Cb的范围是[−0.5,0.5]。

也有转换公式是这样的:
RGB to YUV:
Y = 0.257R+0.504G+0.098B+16
Cr = V = 0.439R−0.368G−0.071B+128
Cb = U = −0.148R−0.291G+0.439B+128

YUV to RGB:
R = 1.164(Y−16)+1.596(V−128)
G = 1.164(Y−16)−0.813(V−128)−0.391(U−128)
B = 1.164(Y−16)+2.018(U−128)
注意在上面的式子中,RGB的范围是[0,255],Y的范围是[16,235],UV的范围是[16,239],如果超出这个范围就截断处理。

2、BT.601 full range
转换公式-fullrangeRGB to YUV:
Y = 0.299 * R + 0.587 * G + 0.114 * B    
Cr = V = -0.169 * R - 0.331 * G + 0.500 * B
Cb = U = 0.500 * R - 0.439 * G - 0.081 * B

YUV to RGB:
R = Y + 1.400V - 0.7
G = Y - 0.343U - 0.711V + 0.526
B = Y + 1.765U - 0.883
上述式子中RGB的范围是[0,255],Y的范围是[0,255],UV的范围是[0,255]如果超出这个范围就截断处理。

3、BT.709, which is the standard for HDTV.
转换公式-高清(HD)RGB to YUV:
Y = 0.0627 + 0.183 * R + 0.614 * g + 0.062 * b
Cb = U = 0.5 - 0.101 * R - 0.339 * g + 0.439 * b
Cr = V = 0.5 + 0.439 * R - 0.399 * g - 0.040 * b

YUV to RGB:
R = 1.164Y + 1.739V - 0.97
G = 1.164Y - 0.213U - 0.533V + 0.301
B = 1.164Y + 2.112U - 1.129
上述式子中,RGB的范围是[0,255],Y的范围是[16,235],UV的范围是[16,239],如果超出这个范围就截断处理。

三、常用矩阵

常用色彩3X3矩阵定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BT.601, which is the standard for SDTV.
GLfloat kColorConversion601Default[] = {
1.164, 1.164, 1.164,
0.0, -0.392, 2.017,
1.596, -0.813, 0.0,
};

// BT.601 full range
GLfloat kColorConversion601FullRangeDefault[] = {
1.0, 1.0, 1.0,
0.0, -0.343, 1.765,
1.4, -0.711, 0.0,
};

// BT.709, which is the standard for HDTV.
GLfloat kColorConversion709Default[] = {
1.164, 1.164, 1.164,
0.0, -0.213, 2.112,
1.793, -0.533, 0.0,
};
参考链接:

https://en.wikipedia.org/wiki/YUV

Cronet库编译

Posted on 2020-04-09 | In 网络编程

Cronet为Chrome开源项目Chromium的网络模块,此网络库支持HTTP,HTTPS,SPDY,QUIC,HTTP2.0 等协议。
编译Cronet库需要先获取Chromium项目,编译过程比较繁琐,而且Chromium项目比较大,总计19.5GB,google项目需要vpn。

Chromium项目的获取

  • 1.1首先需要获取depot_tools

    此工具在后续编译的时候会用到,安装步骤:

    • 1.1.1 克隆至本地(文件路径自定义)
      git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
    • 1.1.2 添加路径至.bash_profile(xxx为depot_tools克隆的文件路径)
      export PATH=”$PATH:xxx/depot_tools”
      注意:此处的xxx路径不能使用“ ~/ ”,否则会报错,“ ~ ”字符串冲突。
  • 1.2获取Chromium项目

    步骤如下:

    • 1.2.1 创建文件夹 chromium
      mkdir chromium && cd chromium
    • 1.2.2 获取项目 (fetch 即为depot_tools中工具)
      fetch —nohooks ios
      这里使用—nohooks是因为不要在获取完项目后直接进行其他依赖更新,一步一步来(当然出现问题解决完 也可以使用gclient sync继续获取)
    • 1.2.3 同步更新
      进入src文件夹执行更新命令
      gclient sync
1.2.2获取项目过程中存在的问题点如下:
  • 1.Git error:

    1
    2
    3
    4
    RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 54
    fatal: the remote end hung up unexpectedly
    fatal: early EOF
    fatal: index-pack failed

    原因:Git pull or push过大文件造成的错误
    解决方法:
    修改git配置项(500MB取值可变)
    git config –global http.postBuffer 524288000

  • 2.Git error:

    1
    2
    3
    fatal: the remote end hung up unexpectedly xxx
    fatal: early EOF
    fatal: index-pack failed

    原因:存在大文件需要压缩,需要配置压缩项
    解决方法:(ompression取值范围:-1~9,可参见:https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-config.html)
    git config –global –add core.compression -1

1.2.3同步时存在的问题点:
  • 1.ssl验证错误
    Python2.7.9 之后,当使用urllib.urlopen打开一个 https 链接时,会验证一次 SSL 证书。而当目标网站使用的是自签名的证书时就会抛出如下异常
    <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:777)>

    解决方法:
    全局取消证书验证

    1
    2
    import ssl
    ssl._create_default_https_context = ssl._create_unverified_context

    文件名如下:

    1
    2
    vpython src/third_party/depot_tools/download_from_google_storage.py
    vpython src/third_party/depot_tools/gsutil.py
  • 1.3构建Cronet

    • 1.3.1链接cr_cronet.py脚本文件
      此时所在文件位置为:xxx/chromium/src
      ln -s ./components/cronet/tools/cr_cronet.py xxx/chromium/src

    • 1.3.2构建环境
      cr_cronet.py gn 即创建默认的构建环境(默认是模拟器的Debug版本)
      这里构建真机发布版本:
      cr_cronet.py gn -i -r
      此时在out/目录下会生成Release-iphoneos文件夹
      ninjia -C out/Release-iphoneos cronet_package

      编译完成后
      在out/Release-iphoneos目录下生成framework文件
      Cronet.framework — 4.7MB
      在out/Release-iphoneos/Static目录下生成静态framework文件
      Cronet.framework — 24.2MB
      在out/Release-iphoneos/obj/components/cronet/ios目录下生成三个.a文件
      libcronet.a — 24.1MB
      libcronet_static.a — 1.4MB
      libcronet_deps_complete.a — 249MB

      Android 库:
      out/Release-iphoneos/lib.java/components/cronet/android/ 所有 jar 包在此目录下,一般不用;
      out/Release-iphoneos/cronet/ 需要使用cronet库的java API的so、jar包;
      out/Release-iphoneos/libcronet.77.0.3825.0.so,strip 后的库,6M;
      out/Release-iphoneos/lib.unstripped,未 strip 的库在此目录下,50.9M;
      out/Release-iphoneos/gen/components/cronet/android/cronet_jni_registration.h,该文件便是自动生成的JNI头文件

      以上脚本默认架构为arm64 如果需要其他架构,需要修改cr_cronet.py脚本cpu对应的参数
      直接修改脚本文件方式欠妥,执行命令动态修改cpu参数即可:
      在创建构建环境后,执行命令
      gn args

      使用:
      gn args out/Release-iphoneos
      在弹出来的文件编辑器里边修改target_cpu参数即可,参数可能取值(使用命令gn help target_cpu查看):
      “arm”,“arm64”,“x86”,“x64”,“mipsel”
      目前想要支持多架构,只能手动合成多架构版本。

关于log:
开启CronetLog模块:
[Cronet startNetLogToFile:[self currentNetLogFileName] logBytes:NO];
[Cronet stopNetLog];
因为Log是json格式,需要主动结束以保证其完整性。
可以使用 https://netlog-viewer.appspot.com/#import 分析Log,也可以使用Chrome扩展程序 Chromium NetLog dump viewer

参考链接:

https://chromium.googlesource.com/chromium/src/+/master/components/cronet/ios/docs/BUILD.md
https://chromium.googlesource.com/chromium/src/+/master/components/cronet/build_instructions.md

iOS音视频开发-音频硬编码-AudioToolbox-PCMToAAC

Posted on 2018-02-26 | In iOS音视频技术

之前几篇文章记录了视频的软、硬编码过程,接下来将记录下音频的软、硬编码过程,学习、工作之余,以免忘记。
视频编码地址:
iOS音视频开发-视频会话捕捉
iOS音视频开发-视频硬编码(H264)
iOS音视频开发-视频软编码(x264编码H.264文件)
iOS音视频开发-视频软编码(FFmpeg+x264编码H.264文件)

PCM数据
PCM(Pulse Code Modulation)也被称为脉冲编码调制。PCM音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。
移动端对音频的实时采集编码传输,一般为将采集的音频数据设置为PCM格式数据,然后将PCM编码为AAC格式数据,以便后续传输。
PCM的数据格式,这里有篇文章介绍的很好,后续代码中的采样率、声道等均参考此文章设置。
PCM数据格式文章地址:点这里

ADTS
ADTS全称是(Audio Data Transport Stream),是AAC的一种十分常见的传输格式。
将PCM数据编码为AAC的时候需要将每帧的AAC数据添加ADTS header,否则将无法解码播放。
ADTS数据格式分为两部分:
固定头部:adts_fixed_header
可变头部:adts_variable_header
详见Wiki,地址在文章末尾。

主要代码
1、音频捕获代码

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#import "BBAudioCapture.h"
#import <AVFoundation/AVFoundation.h>
#import "BBAudioConfig.h"
#import "BBAudioHardEncoder.h"
#import "BBAudioHardEncoder.h"

@interface BBAudioCapture ()
{
AudioComponentInstance _outInstance;
}
@property (nonatomic, assign) AudioComponent component;
@property (nonatomic, strong) AVAudioSession *session;
@property (nonatomic, strong) BBAudioHardEncoder *encoder;
@property (nonatomic, strong) NSFileHandle *handle;
@end

@implementation BBAudioCapture

#pragma mark -- 对象销毁方法
- (void)dealloc{
AudioComponentInstanceDispose(_outInstance);
}

#pragma mark -- 对外API(控制是否捕捉音频数据)
- (void)startRunning{
AudioOutputUnitStart(_outInstance);
}

-(void)stopRunning{
AudioOutputUnitStop(_outInstance);
}

#pragma mark -- 对外API(设置捕获音频数据配置项)
- (void)setConfig:(BBAudioConfig *)config{
_config = config;
[self private_setupAudioSession];
}

#pragma mark -- 私有API(初始化音频会话)
- (void)private_setupAudioSession{

//0.初始化编码器
self.encoder = [[BBAudioHardEncoder alloc] init];
self.encoder.config = self.config;

//1.获取音频会话实例
self.session = [AVAudioSession sharedInstance];

NSError *error = nil;
[self.session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error];

if (error) {
NSLog(@"AVAudioSession setupError");
error = nil;
return;
}

//2.激活会话
[self.session setActive:YES error:&error];

if (error) {
NSLog(@"AVAudioSession setActiveError");
error = nil;
return;
}

//3.设置模式
[self.session setMode:AVAudioSessionModeVideoRecording error:&error];

if (error) {
NSLog(@"AVAudioSession setModeError");
error = nil;
return;
}

//4.设置音频单元
AudioComponentDescription acd = {
.componentType = kAudioUnitType_Output,
.componentSubType = kAudioUnitSubType_RemoteIO,
.componentManufacturer = kAudioUnitManufacturer_Apple,
.componentFlags = 0,
.componentFlagsMask = 0,
};

//5.查找音频单元
self.component = AudioComponentFindNext(NULL, &acd);

//6.获取音频单元实例
OSStatus status = AudioComponentInstanceNew(self.component, &_outInstance);

if (status != noErr) {
NSLog(@"AudioSource new AudioComponent error");
status = noErr;
return;
}

//7.设置音频单元属性-->可读写 0-->不可读写 1-->可读写
UInt32 flagOne = 1;
AudioUnitSetProperty(_outInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne));

//8.设置音频单元属性-->音频流
AudioStreamBasicDescription asbd = {0};
asbd.mSampleRate = self.config.sampleRate;//采样率
asbd.mFormatID = kAudioFormatLinearPCM;//原始数据为PCM格式
asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
asbd.mChannelsPerFrame = (UInt32)self.config.channels;//每帧的声道数量
asbd.mFramesPerPacket = 1;//每个数据包多少帧
asbd.mBitsPerChannel = 16;//16位
asbd.mBytesPerFrame = asbd.mChannelsPerFrame * asbd.mBitsPerChannel / 8;//每帧多少字节 bytes -> bit / 8
asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame;//每个包多少字节

status = AudioUnitSetProperty(_outInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));

if (status != noErr) {
NSLog(@"AudioUnitSetProperty StreamFormat error");
status = noErr;
return;
}

//9.设置回调函数
AURenderCallbackStruct cb;
cb.inputProcRefCon = (__bridge void *)self;
cb.inputProc = audioBufferCallBack;

status = AudioUnitSetProperty(_outInstance, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &cb, sizeof(cb));

if(status != noErr){
NSLog(@"AudioUnitSetProperty StreamFormat InputCallback error");
status = noErr;
return;
}

//10.初始化音频单元
status = AudioUnitInitialize(_outInstance);

if (status != noErr) {
NSLog(@"AudioUnitInitialize error");
status = noErr;
return;
}

//11.设置优先采样率
[self.session setPreferredSampleRate:self.config.sampleRate error:&error];

if (error) {
NSLog(@"AudioSource setPreferredSampleRate error");
error = nil;
return;
}

//12.aac文件夹地址
NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.aac"];
[[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil];
[[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil];
self.handle = [NSFileHandle fileHandleForWritingAtPath:audioPath];

}

#pragma mark -- 音频流回调函数
static OSStatus audioBufferCallBack(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
@autoreleasepool {
BBAudioCapture *capture = (__bridge BBAudioCapture *)inRefCon;
if(!capture) return -1;

AudioBuffer buffer;
buffer.mData = NULL;
buffer.mDataByteSize = 0;
buffer.mNumberChannels = 1;

AudioBufferList buffers;
buffers.mNumberBuffers = 1;
buffers.mBuffers[0] = buffer;

OSStatus status = AudioUnitRender(capture->_outInstance,
ioActionFlags,
inTimeStamp,
inBusNumber,
inNumberFrames,
&buffers);

if(status == noErr) {
[capture.encoder encodeWithBufferList:buffers completianBlock:^(NSData *encodedData, NSError *error) {
if (error) {
NSLog(@"error:%@",error);
return;
}

NSLog(@"write to file!");
[capture.handle writeData:encodedData];
}];
}
return status;
}
}

@end

2、编码代码

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#import "BBAudioHardEncoder.h"
#import "BBAudioConfig.h"

@interface BBAudioHardEncoder ()
@property (nonatomic, assign) AudioConverterRef converterRef;
@end
@implementation BBAudioHardEncoder

- (AudioConverterRef)converterRef{
if (_converterRef == nil) {
[self private_setupAudioConvert];
}
return _converterRef;
}

- (void)dealloc {
AudioConverterDispose(_converterRef);
}

- (void)private_setupAudioConvert{

//1.输入流
AudioStreamBasicDescription inputFormat = {0};
inputFormat.mSampleRate = self.config.sampleRate;//采样率
inputFormat.mFormatID = kAudioFormatLinearPCM;//PCM采样
inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
inputFormat.mChannelsPerFrame = (UInt32)self.config.channels;//每帧声道数
inputFormat.mFramesPerPacket = 1;//每包帧数
inputFormat.mBitsPerChannel = 16;//每声道位数
inputFormat.mBytesPerFrame = inputFormat.mBitsPerChannel / 8 * inputFormat.mChannelsPerFrame;//每帧的字节数
inputFormat.mBytesPerPacket = inputFormat.mBytesPerFrame * inputFormat.mFramesPerPacket;//每包字节数

//2.输出流
AudioStreamBasicDescription outputFormat;
//2.1初始清零
memset(&outputFormat, 0, sizeof(outputFormat));
//2.2音频流,在正常播放情况下的帧率。如果是压缩的格式,这个属性表示解压缩后的帧率。帧率不能为0。
outputFormat.mSampleRate = inputFormat.mSampleRate;
//2.3AAC编码 kAudioFormatMPEG4AAC kAudioFormatMPEG4AAC_HE_V2
outputFormat.mFormatID = kAudioFormatMPEG4AAC;
//2.4无损编码,0则无
outputFormat.mFormatFlags = kMPEG4Object_AAC_LC;
//2.5每一个packet的音频数据大小。如果的动态大小设置为0。动态大小的格式需要用AudioStreamPacketDescription来确定每个packet的大小。
outputFormat.mBytesPerPacket = 0;
//2.6每帧的声道数
outputFormat.mChannelsPerFrame = (UInt32)self.config.channels;
//2.7每个packet的帧数。如果是未压缩的音频数据,值是1。动态帧率格式,这个值是一个较大的固定数字,比如说AAC的1024。如果是动态大小帧数(比如Ogg格式)设置为0。
outputFormat.mFramesPerPacket = 1024;
//2.8每帧的bytes数,每帧的大小。每一帧的起始点到下一帧的起始点。如果是压缩格式,设置为0 。
outputFormat.mBytesPerFrame = 0;
//2.9语音每采样点占用位数 压缩格式设置为0
outputFormat.mBitsPerChannel = 0;
//2.10字节对齐,填0.
outputFormat.mReserved = 0;

//3.编码器参数
const OSType subtype = kAudioFormatMPEG4AAC;
AudioClassDescription requestedCodecs[2] = {
{
kAudioEncoderComponentType,
subtype,
kAppleSoftwareAudioCodecManufacturer
},
{
kAudioEncoderComponentType,
subtype,
kAppleHardwareAudioCodecManufacturer
}
};

//4.编码器
OSStatus result = AudioConverterNewSpecific(&inputFormat, &outputFormat, 2, requestedCodecs, &_converterRef);

if (result == noErr) {
NSLog(@"creat convert success!");
}else{
NSLog(@"creat convert error!");
_converterRef = nil;
}

}

- (void)encodeWithBufferList:(AudioBufferList)bufferList completianBlock:(void (^)(NSData *encodedData, NSError *error))completionBlock{
if (!self.converterRef) {
return;
}
int size = bufferList.mBuffers[0].mDataByteSize;

if (size <= 0) {
return;
}

char *aacBuf = malloc(size);

//1.初始化一个输出缓冲列表
AudioBufferList outBufferList;
outBufferList.mNumberBuffers = 1;
outBufferList.mBuffers[0].mNumberChannels = bufferList.mBuffers[0].mNumberChannels;
outBufferList.mBuffers[0].mDataByteSize = bufferList.mBuffers[0].mDataByteSize; // 设置缓冲区大小
outBufferList.mBuffers[0].mData = aacBuf; // 设置AAC缓冲区
UInt32 outputDataPacketSize = 1;

NSData *data = nil;
NSError *error = nil;
OSStatus status = AudioConverterFillComplexBuffer(_converterRef, inputDataProc, &bufferList, &outputDataPacketSize, &outBufferList, NULL);
if (status == 0){
NSData *rawAAC = [NSData dataWithBytes:outBufferList.mBuffers[0].mData length:outBufferList.mBuffers[0].mDataByteSize];
NSData *adtsHeader = [self getADTSDataWithPacketLength:rawAAC.length];
NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
[fullData appendData:rawAAC];
data = fullData;
}else{
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"音频编码失败");
return;
}

if (completionBlock) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
completionBlock(data, error);
});
}
free(aacBuf);
}

#pragma mark -- AudioCallBack
OSStatus inputDataProc(AudioConverterRef inConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData,AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) {
//填充PCM到缓冲区
AudioBufferList bufferList = *(AudioBufferList*)inUserData;
ioData->mBuffers[0].mNumberChannels = 1;
ioData->mBuffers[0].mData = bufferList.mBuffers[0].mData;
ioData->mBuffers[0].mDataByteSize = bufferList.mBuffers[0].mDataByteSize;
ioData->mNumberBuffers = 1;
return noErr;
}

/**
* Add ADTS header at the beginning of each and every AAC packet.
* This is needed as MediaCodec encoder generates a packet of raw
* AAC data.
*
* Note the packetLen must count in the ADTS header itself.
* See: http://wiki.multimedia.cx/index.php?title=ADTS
* Also: http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations
**/
- (NSData *)getADTSDataWithPacketLength:(NSInteger)packetLength {

int adtsLength = 7;
char *packet = malloc(sizeof(char) * adtsLength);
// Variables Recycled by addADTStoPacket
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //44.1KHz
int chanCfg = 1; //MPEG-4 Audio Channel Configuration. 1 Channel front-center
NSUInteger fullLength = adtsLength + packetLength;
// fill in ADTS data
packet[0] = (char)0xFF; // 11111111 = syncword
packet[1] = (char)0xF9; // 1111 1 00 1 = syncword MPEG-2 Layer CRC
packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));
packet[4] = (char)((fullLength&0x7FF) >> 3);
packet[5] = (char)(((fullLength&7)<<5) + 0x1F);
packet[6] = (char)0xFC;
NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];
return data;
}
@end

以上代码为主要代码,配置代码无非就是采样率:44.1kHz,声道:双声道。
完整代码地址:https://github.com/ibabyblue/PCMHardEncodeToAAC
将编码的AAC数据写入本地文件,利用VLC播放器可以直接播放.aac格式文件,测试很方便。

写在最后,一路学习,坎坷良多,非常感谢无私分享的开发者们,感谢Jovins、iossigner分享的干货,感恩!
参考地址:
http://blog.csdn.net/ownwell/article/details/8114121/
https://wiki.multimedia.cx/index.php?title=ADTS
https://developer.apple.com/documentation/audiotoolbox/audio_converter_services?language=objc

iOS音视频开发-视频软编码-FFmpeg-x264编码H264文件

Posted on 2018-02-22 | In iOS音视频技术

上篇记录了利用x264编码的实现,这里记录一下FFmpeg+x264编码为H.264的实现过程,为什么使用FFmpeg+x264,因为两种框架互补组成了强大的编解码器(FFmpeg解码,x246编码)。

编译FFmpeg+x264
FFmpeg
地址:https://github.com/FFmpeg/FFmpeg
编译脚本:https://github.com/kewlbear/FFmpeg-iOS-build-script
x264
地址:http://www.videolan.org/developers/x264.html
编译脚本:https://github.com/kewlbear/x264-ios
具体编译过程:x264的编译过程在上篇iOS音视频开发-视频软编码(x264编码H.264文件)
FFmpeg+x264编译步骤:

  • 1、将编译好的x264文件夹放置在FFmpeg脚本目录下,并将文件夹改名为fat-x264(因为脚本中定义的引用x264文件夹的名称为fat-x264);

  • 2、执行脚本文件:./build-ffmpeg.sh。
    这里需要注意的问题,之前编译并没有遇到,此次总结重新编译了一次,遇到一些问题记录一下。
    1)现阶段最新的FFmpeg版本为:n3.4.2,脚本中使用版本号为:n3.4。执行脚本的时候会出现诸如此类的错误:

    1
    2
    3
    libavcodec/libx264.c: In function 'x264_init_static':
    libavcodec/libx264.c:892.9 error: 'x264_bit_depth' undeclared(first use in this function)
    if(x264_bit_depth== 8)

    解决办法:修改脚本文件中的版本号:FF_VERSION=”3.4->FF_VERSION=”3.4.2”。

    2)要将x264编译进FFmpeg中,需要取消脚本中对该句代码的注销:

    1
    #X264=`pwd`/fat-x264 ->X264=`pwd`/fat-x264

    3)关于bitcode,现阶段在编译库文件的时候,支持bitcode还是有必要的(毕竟某个工程因为使用了此编译库文件放弃bitcode功能,向上层开发者提供了功能,是不使用是他们的事了),脚本中已经实现了支持bitcode功能,不必修改。
    4)将编译好的文件拖拽到工程中,需要添加的依赖库为:
    libiconv.dylib、
    libz.dylib/libbz2.dylib、
    CoreMedia.framework、
    AVFoundation.framework、
    VideoToolbox.framework,
    不然会报并未支持arm64……架构的错误。
    命令行命令:

    1
    2
    3
    4
    //查看是否支持相应的架构:arm64 i386....
    lipo -info libxx.a
    //查看是够支持bitcode >=0
    otool -l libx264.a | grep __bitcode | wc -l

介绍:不错的编译过程文章
视频捕获和编码配置代码在前面已记录,这里就不赘述了。FFmpeg+x264编码实现代码如下:

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
@class BBVideoConfig;
@interface BBH264SoftEncoder : NSObject
/*
* 设置编码后文件的保存路径
*/
- (void)setFilePath:(NSString *)path;

/*
* 初始化编码配置
*/
- (void)setupEncodeWithConfig:(BBVideoConfig *)config;

/*
* 将CMSampleBufferRef格式的数据编码成h264并写入文件
*/
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer;

/*
* 释放资源
*/
- (void)freeX264Resource;
@end

#import "BBH264SoftEncoder.h"
#import "BBVideoConfig.h"

#ifdef __cplusplus
extern "C" {
#endif

#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#ifdef __cplusplus
};
#endif

@implementation BBH264SoftEncoder
{
AVFormatContext *pFormatCtx;
AVOutputFormat *fmt;
AVStream *video_st;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVPacket pkt;
uint8_t *picture_buf;
AVFrame *pFrame;
int picture_size;
int y_size;
int framecnt;
char *out_file;

int encoder_h264_frame_width; // 编码的图像宽度
int encoder_h264_frame_height; // 编码的图像高度
}

/*
* 设置编码后文件的文件名,保存路径
*/
- (void)setFilePath:(NSString *)path;
{
out_file = [self nsstring2char:path];
}

/*
* 将路径转成C语言字符串(传入路径为C字符串)
*/
- (char*)nsstring2char:(NSString *)path
{

NSUInteger len = [path length];
char *filepath = (char*)malloc(sizeof(char) * (len + 1));

[path getCString:filepath maxLength:len + 1 encoding:[NSString defaultCStringEncoding]];

return filepath;
}


/*
* 设置X264
*/
- (void)setupEncodeWithConfig:(BBVideoConfig *)config
{
// 1.默认从第0帧开始(记录当前的帧数)
framecnt = 0;

// 2.记录传入的宽度&高度
encoder_h264_frame_width = config.videoSize.width;
encoder_h264_frame_height = config.videoSize.height;

// 3.注册FFmpeg所有编解码器(无论编码还是解码都需要该步骤)
av_register_all();

// 4.初始化AVFormatContext: 用作之后写入视频帧并编码成 h264,贯穿整个工程当中(释放资源时需要销毁)
pFormatCtx = avformat_alloc_context();

// 5.设置输出文件的路径,fmt初始化的时候根据传入的参数猜出video_codec、mime_type、extensions等等信息。
fmt = av_guess_format(NULL, out_file, NULL);
pFormatCtx->oformat = fmt;

// 6.打开文件的缓冲区输入输出,flags 标识为 AVIO_FLAG_READ_WRITE ,可读写
if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
printf("Failed to open output file! \n");
}

// 7.创建新的输出流, 用于写入文件
video_st = avformat_new_stream(pFormatCtx, 0);

if (video_st==NULL){ printf("Failed to setup stream! \n"); return; }

// 8.pCodecCtx 用户存储编码所需的参数格式等等
// 8.1.从媒体流中获取到编码结构体,他们是一一对应的关系,一个 AVStream 对应一个 AVCodecContext
AVCodec *codec = avcodec_find_encoder(pFormatCtx->oformat->video_codec);
pCodecCtx = avcodec_alloc_context3(codec);

// 8.2.设置编码器的编码格式(是一个id),每一个编码器都对应着自己的 id,例如 h264 的编码 id 就是 AV_CODEC_ID_H264
pCodecCtx->codec_id = fmt->video_codec;

// 8.3.设置编码类型为 视频编码
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;

// 8.4.设置像素格式为 yuv 格式
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;

// 8.5.设置视频的宽高
pCodecCtx->width = encoder_h264_frame_width;
pCodecCtx->height = encoder_h264_frame_height;

// 8.6.设置帧率
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;

// 8.7.设置码率(比特率)
pCodecCtx->bit_rate = config.bitrate;

// 8.8.视频质量度量标准(常见qmin=10, qmax=51)
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;

// 8.9.设置图像组层的大小(GOP-->两个I帧之间的间隔)
pCodecCtx->gop_size = 30;

// 8.10.设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,也就是说相同码率的情况下,
// 越多 B 帧的视频,越清晰,现在很多打视频网站的高清视频,就是采用多编码 B 帧去提高清晰度,
// 但同时对于编解码的复杂度比较高,比较消耗性能与时间
pCodecCtx->max_b_frames = 5;

// 9.可选设置
AVDictionary *param = 0;
// H.264
if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
// 通过--preset的参数调节编码速度和质量的平衡。
av_dict_set(&param, "preset", "slow", 0);

// 通过--tune的参数值指定片子的类型,是和视觉优化的参数,或有特别的情况。
// zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如视频直播的编码
av_dict_set(&param, "tune", "zerolatency", 0);
}

// 10.输出打印信息,内部是通过printf函数输出(不需要输出可以注释掉)
av_dump_format(pFormatCtx, 0, out_file, 1);

// 11.通过 codec_id 找到对应的编码器
pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
if (!pCodec) {
printf("Can not find encoder! \n");
}

// 12.打开编码器,并设置参数 param
if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
printf("Failed to open encoder! \n");
}

// 13.将AVCodecContext的成员复制到AVCodecParameters结构体
avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);

// 14.真实帧率
AVRational rational = {1, 25};
av_stream_set_r_frame_rate(video_st, rational);

// 15.初始化原始数据对象: AVFrame
pFrame = av_frame_alloc();

// 16.通过像素格式(这里为 YUV)获取图片的真实大小,例如将 480 * 720 转换成 int 类型
av_image_fill_arrays(pFrame->data, pFrame->linesize, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1);

// 17.h264 封装格式的文件头部,基本上每种编码都有着自己的格式的头部。
if (avformat_write_header(pFormatCtx, NULL) < 0) { printf("Failed to write! \n"); return; }

// 18.创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据
av_new_packet(&pkt, picture_size);

// 19.设置 yuv 数据中 y 图的宽高
y_size = pCodecCtx->width * pCodecCtx->height;

}

/*
* 将CMSampleBufferRef格式的数据编码成h264并写入文件
*
*/
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
// 1.通过CMSampleBufferRef对象获取CVPixelBufferRef对象
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);

// 2.锁定imageBuffer内存地址开始进行编码
if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
// 3.从CVPixelBufferRef读取YUV的值
// NV12和NV21属于YUV格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane
// 3.1.获取Y分量的地址
UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
// 3.2.获取UV分量的地址
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);

// 3.3.根据像素获取图片的真实宽度&高度
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 获取Y分量长度
size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
UInt8 *yuv420_data = (UInt8 *)malloc(width * height * 3 / 2);

// 3.4.将NV12数据转成YUV420P(I420)数据
UInt8 *pY = bufferPtr;
UInt8 *pUV = bufferPtr1;
UInt8 *pU = yuv420_data + width * height;
UInt8 *pV = pU + width * height / 4;
for(int i =0;i<height;i++)
{
memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
}
for(int j = 0;j<height/2;j++)
{
for(int i =0;i<width/2;i++)
{
*(pU++) = pUV[i<<1];
*(pV++) = pUV[(i<<1) + 1];
}
pUV += bytesrow1;
}

// 3.5.分别读取YUV的数据
picture_buf = yuv420_data;
pFrame->data[0] = picture_buf; // Y
pFrame->data[1] = picture_buf + y_size; // U
pFrame->data[2] = picture_buf + y_size * 5 / 4; // V

// 4.设置当前帧
pFrame->pts = framecnt;

// 4.设置宽度高度以及YUV格式
pFrame->width = encoder_h264_frame_width;
pFrame->height = encoder_h264_frame_height;
pFrame->format = AV_PIX_FMT_YUV420P;

// 5.对编码前的原始数据(AVFormat)利用编码器进行编码,将 pFrame 编码后的数据传入pkt 中
int ret = avcodec_send_frame(pCodecCtx, pFrame);
if (ret != 0) {
printf("Failed to encode! \n");
return;
}

while (avcodec_receive_packet(pCodecCtx, &pkt) == 0) {
framecnt++;
pkt.stream_index = video_st->index;
//也可以使用C语言函数:fwrite()、fflush()写文件和清空文件写入缓冲区。
ret = av_write_frame(pFormatCtx, &pkt);
if (ret < 0) {
printf("Failed write to file!\n");
}
//释放packet
av_packet_unref(&pkt);
}

// 7.释放yuv数据
free(yuv420_data);
}

CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

/*
* 释放资源
*/
- (void)freeX264Resource
{

// 1.将还未输出的AVPacket输出出来
av_write_trailer(pFormatCtx);

// 2.关闭资源
avcodec_close(pCodecCtx);
av_free(pFrame);

avio_close(pFormatCtx->pb);
avformat_free_context(pFormatCtx);
}
@end

编码使用FFmpeg新版API实现,网上很多都是较旧的API代码,但不影响,只是个别接口变更而已,整体的实现思路一致。
学习过程中,查阅了大量的资料,收获颇丰,非常感谢学习路上各位coder的无私分享,尤其coderWhy先森、七牛的深爱、雷霄骅(致敬)。
参考链接:
https://depthlove.github.io/2015/09/18/use-ffmpeg-and-x264-encode-iOS-camera-video-to-h264/
http://blog.csdn.net/leixiaohua1020/article/details/25430425

iOS音视频开发-视频软编码的实现-x264

Posted on 2018-02-11 | In iOS音视频技术

视频软编码:
软编码主要是利用CPU编码的过程,通常为FFmpeg+x264。

FFmpeg
FFmpeg是一个非常强大的音视频处理库,包括视频采集功能、视频格式转换、视频抓图、给视频加水印等。
FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、Mac OS X等。
x264
H.264是ITU制定的视频编码标准
而x264是一个开源的H.264/MPEG-4 AVC视频编码函数库,是最好的有损视频编码器,里面集成了非常多优秀的算法用于视频编码.
x264官网
PS:FFmpeg本身并不包含编码器,但存在强大的解码器,而x264只提供了强大的编码器,但是其独立存在,社区提供了将x264编译进FFmpeg的方法,所以开发时使用的为FFmpeg+x264。这里记录x264编码器的使用方法,FFmpeg+x264的使用后续记录。
编译x264
下载x264源码:
https://www.videolan.org/developers/x264.html

下载gas-preprocessor文件:
https://github.com/libav/gas-preprocessor。
将下载的gas-preprocessor文件拷贝到/usr/local/bin目录下。
修改文件权限:chmod 777 /usr/local/bin/gas-preprocessor.pl。

下载x264编译脚本文件:
https://github.com/kewlbear/x264-ios
将脚本文件build-x264.sh 放在x264源码文件同级目录下,并不是x264文件夹里面。

同级目录.png
修改权限、执行脚本:

sudo chmod u+x build-x264.sh
sudo ./build-x264.sh
当脚本执行过程中可能会出现警告导致不能编译成功,一般为yasm版本过低或者nasm版本过低导致的(我遇到的)。

1
2
3
Found yasm x.x.x.xxxx
Minimum version is yasm-x.x.x
If you really want to compile without asm, configure with --disable-asm.

或者

1
2
3
Found nasm x.x.x.xxxx
Minimum version is nasm-x.x.x
If you really want to compile without asm, configure with --disable-asm.

解决办法:
下载Homebrew,利用Homebrew下载yasm/nasm。
Homebrew下载安装:

地址:https://brew.sh/
Homebrew安装yasm命令:brew install yasm
Homebrew安装nasm命令:brew install nasm
脚本执行完毕生成的文件:

编译好的x264文件夹.png
使用命令行工具查看编译好的.a文件支持的架构:

1
2
命令:lipo -info libx264.a
结果:Architectures in the fat file: libx264.a are: armv7 armv7s i386 x86_64 arm64

当然执行脚本的时候你可以选择想要的架构:

1
2
3
4
5
6
7
8
9
10
11
To build everything://支持所有架构
./build-x264.sh

To build for arm64://只支持arm64
./build-x264.sh arm64

To build fat library for armv7 and x86_64 (64-bit simulator)://只支持armv7和x86_64
./build-x264.sh armv7 x86_64

To build fat library from separately built thin libraries://支持各架构独立库文件
./build-x264.sh lipo

x264编码实现
将编译好的x264-iOS文件夹拖入工程即可。
x264编码参数设置:

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
- (void)setupEncodeWithConfig:(BBVideoConfig *)config{

_config = config;

pX264Param = (x264_param_t *)malloc(sizeof(x264_param_t));
assert(pX264Param);
/* 配置参数预设置
* 主要是zerolatency该参数,即时编码。
* static const char * const x264_tune_names[] = { "film", "animation", "grain", "stillimage", "psnr", "ssim", "fastdecode", "zerolatency", 0 };
*/
x264_param_default_preset(pX264Param, "veryfast", "zerolatency");

/* 设置Profile.使用Baseline profile
* static const char * const x264_profile_names[] = { "baseline", "main", "high", "high10", "high422", "high444", 0 };
*/
x264_param_apply_profile(pX264Param, "baseline");

// cpuFlags
pX264Param->i_threads = X264_SYNC_LOOKAHEAD_AUTO; // 取空缓冲区继续使用不死锁的保证

// 视频宽高
pX264Param->i_width = config.videoSize.width; // 要编码的图像宽度.
pX264Param->i_height = config.videoSize.height; // 要编码的图像高度
pX264Param->i_frame_total = 0; //编码总帧数,未知设置为0

// 流参数
pX264Param->b_cabac = 0; //支持利用基于上下文的自适应的算术编码 0为不支持
pX264Param->i_bframe = 5;//两个参考帧之间B帧的数量
pX264Param->b_interlaced = 0;//隔行扫描
pX264Param->rc.i_rc_method = X264_RC_ABR; // 码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
pX264Param->i_level_idc = 30; // 编码复杂度

// 图像质量
pX264Param->rc.f_rf_constant = 15; // rc.f_rf_constant是实际质量,越大图像越花,越小越清晰
pX264Param->rc.f_rf_constant_max = 45; // param.rc.f_rf_constant_max ,图像质量的最大值。

// 速率控制参数 通常为屏幕分辨率*3 (宽x高x3)
pX264Param->rc.i_bitrate = config.bitrate / 1000; // 码率(比特率), x264使用的bitrate需要/1000。
// pX264Param->rc.i_vbv_max_bitrate=(int)((m_bitRate * 1.2) / 1000) ; // 平均码率模式下,最大瞬时码率,默认0(与-B设置相同)
pX264Param->rc.i_vbv_buffer_size = pX264Param->rc.i_vbv_max_bitrate = (int)((config.bitrate * 1.2) / 1000);
pX264Param->rc.f_vbv_buffer_init = 0.9;//默认0.9


// 使用实时视频传输时,需要实时发送sps,pps数据
pX264Param->b_repeat_headers = 1; // 重复SPS/PPS 放到关键帧前面。该参数设置是让每个I帧都附带sps/pps。

// 帧率
pX264Param->i_fps_num = config.fps; // 帧率分子
pX264Param->i_fps_den = 1; // 帧率分母
pX264Param->i_timebase_den = pX264Param->i_fps_num;
pX264Param->i_timebase_num = pX264Param->i_fps_den;

/* I帧间隔 GOP
* 一般为帧率的整数倍,通常设置2倍,即 GOP = 帧率 * 2;
*/
pX264Param->b_intra_refresh = 1;
pX264Param->b_annexb = 1;
pX264Param->i_keyint_max = config.fps * 2;


// Log参数,打印编码信息
pX264Param->i_log_level = X264_LOG_DEBUG;

// 编码需要的辅助变量
iNal = 0;
pNals = NULL;

pPicIn = (x264_picture_t *)malloc(sizeof(x264_picture_t));
memset(pPicIn, 0, sizeof(x264_picture_t));
x264_picture_alloc(pPicIn, X264_CSP_I420, pX264Param->i_width, pX264Param->i_height);
pPicIn->i_type = X264_TYPE_AUTO;
pPicIn->img.i_plane = 3;

pPicOut = (x264_picture_t *)malloc(sizeof(x264_picture_t));
memset(pPicOut, 0, sizeof(x264_picture_t));
x264_picture_init(pPicOut);

// 打开编码器句柄,通过x264_encoder_parameters得到设置给X264
// 的参数.通过x264_encoder_reconfig更新X264的参数
pX264Handle = x264_encoder_open(pX264Param);
assert(pX264Handle);

}

x264编码主要实现代码:

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
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer{

CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);

CVPixelBufferLockBaseAddress(imageBuffer, 0);

UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);

size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);

size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
size_t bytesrow1 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);

UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/ 2);//buffer to store YUV with layout YYYYYYYYUUVV


/* convert NV12 data to YUV420*/
UInt8 *pY = bufferPtr ;
UInt8 *pUV = bufferPtr1;
UInt8 *pU = yuv420_data + width * height;
UInt8 *pV = pU + width * height / 4;
for(int i = 0; i < height; i++)
{
memcpy(yuv420_data + i * width, pY + i * bytesrow0, width);
}
for(int j = 0;j < height/2; j++)
{
for(int i = 0; i < width/2; i++)
{
*(pU++) = pUV[i<<1];
*(pV++) = pUV[(i<<1) + 1];
}
pUV += bytesrow1;
}

// yuv420_data <==> pInFrame
pPicIn->img.plane[0] = yuv420_data;
pPicIn->img.plane[1] = pPicIn->img.plane[0] + (int)_config.videoSize.width * (int)_config.videoSize.height;
pPicIn->img.plane[2] = pPicIn->img.plane[1] + (int)(_config.videoSize.width * _config.videoSize.height / 4);
pPicIn->img.i_stride[0] = _config.videoSize.width;
pPicIn->img.i_stride[1] = _config.videoSize.width / 2;
pPicIn->img.i_stride[2] = _config.videoSize.width / 2;

// 编码
int frame_size = x264_encoder_encode(pX264Handle, &pNals, &iNal, pPicIn, pPicOut);

// 将编码数据写入文件
if(frame_size > 0) {

for (int i = 0; i < iNal; ++i)
{
fwrite(pNals[i].p_payload, 1, pNals[i].i_payload, pFile);
}

}

CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}
释放资源:
- (void)freeX264Resource{
// 清除图像区域
x264_picture_clean(pPicIn);
// 关闭编码器句柄
x264_encoder_close(pX264Handle);
pX264Handle = NULL;
free(pPicIn);
pPicIn = NULL;
free(pPicOut);
pPicOut = NULL;
free(pX264Param);
pX264Param = NULL;
fclose(pFile);
pFile = NULL;
}

代码地址:
参考链接:
https://www.cnblogs.com/fojian/archive/2012/09/01/2666627.html
https://web.archive.org/web/20150207075004/http://mewiki.project357.com/wiki/X264_Settings

iOS音视频开发-视频硬编码的实现

Posted on 2018-02-07 | In iOS音视频技术

视频编码
视频编码分为软编码和硬编码:

软编码:
1.利用CPU进行大批量的编码计算处理。
2.兼容性好。
3.耗电量大,手机发烫(很烫,感觉要爆炸了O(∩_∩)O~)
硬编码
1.利用GPU进行编码处理。
2.兼容性略差。
3.手机不会很烫。(硬编码需要iOS8及以上版本可以使用,之前并未开发,之前版本只能软编码。)
这里记录硬编码的实现,软编码后续会记录。

H264
视频编码需要了解的编码格式,H264/AVC为视频编码格式,需要将采集到的视频帧编码为H264格式的数据。
H264的特点:
1.更高的编码效率:同H.263等标准的特率效率相比,能够平均节省大于50%的码率。
2.高质量的视频画面:H.264能够在低码率情况下提供高质量的视频图像,在较低带宽上提供高质量的图像传输是H.264的应用亮点。
3.提高网络适应能力:H.264可以工作在实时通信应用(如视频会议)低延时模式下,也可以工作在没有延时的视频存储或视频流服务器中。
H264的优势:
H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。低码率(Low Bit Rate)对H.264的高的压缩比起到了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。
PS:以上摘自百度百科。需要了解的可自行百度。

我们将采集到的视频数据编码为H264数据流,那采集到的原始视频数据是什么呢?实际上是YUV420格式的数据,上篇视频采集的文章记录了设置输出设备的输出格式为:
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 表示原始数据的格式为YUV420。
那我们为什么要设置输出为YUV420数据呢,YUV420数据是什么呢?这篇文章介绍的很详细YUV和RGB。
简单来说有一下几点:
1.YUV420采样数据大小为RGB格式的一半(采样数据后续涉及到推流,所以数据越小越好)。
2.YUV格式所有编码器都支持,RGB格式却存在不兼容的情况。
3.YUV420格式适用于便携式设备。

代码如下:

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>

@interface BBH264Encoder : NSObject
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;
@end

#import "BBH264Encoder.h"

@interface BBH264Encoder()
/** 记录当前的帧数 */
@property (nonatomic, assign) NSInteger frameID;

/** 编码会话 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSessionRef;

/** 文件写入对象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end

@implementation BBH264Encoder

- (instancetype)init{
if (self = [super init]) {
// 1.初始化写入文件的对象(NSFileHandle用于写入二进制文件)
[self setupFileHandle];

// 2.初始化压缩编码的会话
[self setupCompressionSession];

}
return self;
}

- (void)setupFileHandle {
// 1.获取沙盒路径
NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.h264"];

// 2.如果原来有文件,则删除
[[NSFileManager defaultManager] removeItemAtPath:file error:nil];
[[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];

// 3.创建对象
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}

- (void)setupCompressionSession{

//0.用于记录当前是第几帧数据(画面帧数非常多)
_frameID = 0;

//1.清空压缩上下文
if (_compressionSessionRef) {
VTCompressionSessionCompleteFrames(_compressionSessionRef, kCMTimeInvalid);
VTCompressionSessionInvalidate(_compressionSessionRef);
CFRelease(_compressionSessionRef);
_compressionSessionRef = NULL;
}

//2.录制视频的宽度&高度
int width = [UIScreen mainScreen].bounds.size.width;
int height = [UIScreen mainScreen].bounds.size.height;

//3.创建压缩会话
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, bbCompressionSessionCallback, (__bridge void * _Nullable)(self), &_compressionSessionRef);

//4.判断状态
if (status != noErr) return;

//5.设置参数
//Profile_level,h264的协议等级,不同的清晰度使用不同的ProfileLevel
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);

// 关键帧最大间隔
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(30)));

// 设置平均码率 单位是byte
int bitRate = [self getResolution];
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);

// 码率上限 接收数组类型CFArray[CFNumber] [bytes,seconds,bytes,seconds...] 单位是bps
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef _Nullable)@[@(bitRate*1.5/8), @1]);

// 设置期望帧率
int fps = 30;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

// 设置实时编码
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);

// 关闭重排Frame
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);

// 设置比例16:9(分辨率宽高比)
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AspectRatio16x9, kCFBooleanTrue);

//6.准备编码
VTCompressionSessionPrepareToEncodeFrames(_compressionSessionRef);

}

/**
编码回调
*/
static void bbCompressionSessionCallback(
void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer ){

BBH264Encoder *encoder = (__bridge BBH264Encoder *)(outputCallbackRefCon);

//1.判断状态是否为没有错误
if (status != noErr) { return; }

CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
BOOL isKeyframe = NO;
if (attachments != NULL) {
CFDictionaryRef attachment;
CFBooleanRef dependsOnOthers;
attachment = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
dependsOnOthers = CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
dependsOnOthers == kCFBooleanFalse ? (isKeyframe = YES) : (isKeyframe = NO);
}

//2.是否为关键帧
if (isKeyframe) {
//SPS and PPS.
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
size_t spsSize, ppsSize;
size_t parmCount;
const uint8_t* sps, *pps;

OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &parmCount, NULL );
//获取SPS无错误则继续获取PPS
if (status == noErr) {
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &parmCount, NULL );

NSData *spsData = [NSData dataWithBytes:sps length:spsSize];
NSData *ppsData = [NSData dataWithBytes:pps length:ppsSize];

//写入文件
[encoder gotSpsPps:spsData pps:ppsData];

}else{
return;
}
}


//3.前4个字节表示长度,后面的数据的长度
// 除了关键帧,其它帧只有一个数据
char *buffer;
size_t total;
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, NULL, &total, &buffer);

if (statusCodeRet == noErr) {
size_t offset = 0;
//返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
int const headerLenght = 4;

//循环获取NAL unit数据
while (offset < total - headerLenght) {
int NALUnitLength = 0;
// Read the NAL unit length
memcpy(&NALUnitLength, buffer + offset, headerLenght);

//从大端转系统端
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
NSData *data = [NSData dataWithBytes:buffer + headerLenght + offset length:NALUnitLength];

// Move to the next NAL unit in the block buffer
offset += headerLenght + NALUnitLength;

[encoder gotEncodedData:data isKeyFrame:isKeyframe];
}
}
}

/**
获取屏幕分辨率
*/
- (int)getResolution{
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGSize screenSize = screenRect.size;
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat screenX = screenSize.width * scale;
CGFloat screenY = screenSize.height * scale;
return screenX * screenY;
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
// 1.拼接NALU的header
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1;
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];

// 2.将NALU的头&NALU的体写入文件
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:sps];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:pps];

}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
NSLog(@"gotEncodedData %d", (int)[data length]);
if (self.fileHandle != NULL)
{
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:data];
}
}

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
// 1.将sampleBuffer转成imageBuffer
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);

// 2.根据当前的帧数,创建CMTime的时间
CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
VTEncodeInfoFlags flags;

// 3.开始编码该帧数据
OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSessionRef,
imageBuffer,
presentationTimeStamp,
kCMTimeInvalid,
NULL, (__bridge void * _Nullable)(self), &flags);
if (statusCode == noErr) {
NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
}
}

- (void)endEncode {
VTCompressionSessionCompleteFrames(self.compressionSessionRef, kCMTimeInvalid);
VTCompressionSessionInvalidate(self.compressionSessionRef);
CFRelease(self.compressionSessionRef);
self.compressionSessionRef = NULL;
[self.fileHandle closeFile];
self.fileHandle = NULL;
}
@end

上述H264码流的NALU和SPS、PPS是什么呢?关于H264码流结构NALU、SPS\PPS。
此代码是将采集到的原始数据编码为H.264码流写入本地文件,此文件可以利用VLC播放器直接播放,测试结果,注意需要真机测试。
真机获取沙盒文件的方法请见:真机获取沙盒文件。
感谢coderWhy和iOSSinger两位的分享。

iOS音视频开发-视频会话捕捉

Posted on 2018-02-05 | In iOS音视频技术

iOS音视频开发抽出时间整理一下,权当备忘吧。iOS音视频开发原理文章在网上有很多了,就不记录了。后面会记录每步骤的实现。

音视频开发首先需要捕捉视频会话,这里只是捕获了视频会话,音频会话后续会记录。废话少说,贴代码:

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
#import <UIKit/UIKit.h>

@interface BBVideoCapture : NSObject

/**
开始捕获视频

@param preview 捕获视频显示的父控件
*/
- (void)startCapture:(UIView *)preview;

/**
结束捕获视频
*/
- (void)stopCapture;

@end

#import <AVFoundation/AVFoundation.h>
#import "BBVideoCapture.h"

@interface BBVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate>

/** 捕捉画面执行的线程队列 */
@property (nonatomic, strong) dispatch_queue_t captureQueue;

/** 捕捉会话*/
@property (nonatomic, weak) AVCaptureSession *captureSession;

/** 预览图层 */
@property (nonatomic, weak) AVCaptureVideoPreviewLayer *previewLayer;

@end

@implementation BBVideoCapture

- (void)startCapture:(UIView *)preview
{

// 1.创建捕捉会话
AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPresetHigh;
self.captureSession = session;

// 2.设置输入设备
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 2.1自动变焦
if([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]){
if([device lockForConfiguration:nil]){
device.focusMode = AVCaptureFocusModeContinuousAutoFocus;
}
}
NSError *error = nil;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
if ([session canAddInput:input]) {
[session addInput:input];
}

// 3.添加输出设备
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
self.captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[output setSampleBufferDelegate:self queue:self.captureQueue];
// 3.1kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 表示原始数据的格式为YUV420
// 这里YUV420为后续编码设置,暂时可忽略,后续会写。
NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey, nil];
output.videoSettings = settings;
output.alwaysDiscardsLateVideoFrames = YES;
if ([session canAddOutput:output]) {
[session addOutput:output];
}

// 4.设置录制视频的方向
AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];

// 5.添加预览图层
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
previewLayer.frame = preview.bounds;
[preview.layer insertSublayer:previewLayer atIndex:0];
self.previewLayer = previewLayer;

// 6.开始捕捉
[self.captureSession startRunning];
}

- (void)stopCapture {
[self.captureSession stopRunning];
[self.previewLayer removeFromSuperlayer];
}

#pragma mark - 获取视频数据代理
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
/**这里就是捕获到视频数据的地方,后续再这里实现视频编码*/
}

@end

视频捕获会话相对较简单,代码中注释很详细,入门时参考了很多资料,感谢分享者,此代码很多参考coderWhy的分享,当然存在自己优化与改动,再次感谢!

使用Hexo搭建个人Github.io博客

Posted on 2018-02-01 | In 其它

回想当初,想要搭建自己的个人博客的冲动是怎样产生的?知识匮乏,无人指导,独自摸索,迷茫的徘徊在无知的十字路口(无知:管我什么事啊?)。
好在现今网络发达,富有分享精神的coder,犹如繁星指路,感谢我成长路上帮助到我的coder们。分享者们搭建富有极客风格的个人博客,发表技术含量较高的技术文章,学习知识之余,不乏搭建个人博客的想法,向其学习,学习知识、分享知识,同时也是一种提升自身方式。
不过搭建博客的想法终究是想法,工(zhao)作(ge)一(jie)忙(kou),想法就搁置了…

Hexo
1.什么是Hexo
Hexo官方网站的醒目标题:A fast, simple & powerful blog framework!其寓意言简意赅:一个快速、简单和功能强大的博客框架!
Hexo基于Node.js的静态博客程序,可以方便将生成的静态网页托管到Github上,可以随意设置丰富、优秀的主题。
研究了一天时间,搭建了极简风格的个人博客,记录一下,未涉及到的功能,后续会继续学习使用。

1.Hexo的官方文档写的很详细,文档见官方文档,可以切换为简体中文,很方便。
2.什么是NexT
NexT是一种优雅、漂亮的Hexo主题。

NexT主题,正如官网所说精于心,简于形。我比较喜欢简单不失优雅的风格。
1.NexT使用方法官方文档
http://theme-next.iissnan.com/getting-started.html
2.记录一下设置过程中我遇到的问题。
下载主题的时候会有最新和稳定版本之分,当初我就下载的最新版本,然后根据文档设置的主题风格,然后困扰了我很久的问题就出现了。

next版本
在设置菜单的时候,不要按照文档更改菜单设置,就是不用更改任何菜单设置就可以,因为按照文档更改之后,菜单的图片显示不了。因为next里面的实现变了。
下载的主题配置:
主题配置菜单的时候官方文档有个坑~

箭头所指的地方应该没有空格才会正确显示和响应菜单栏,如下:

其他就没有什么问题了,按照官方文档设置你想要的风格。
后续添加其他的小功能,比如评论、统计等等,并不是真正的需要这些功能,这完全是自己好奇心,去了解这是如何做可以更加像一个完整的Blog。

做自己喜欢的事,就是最幸福的事。
第一次使用,惯例:
Say: Hello world!
bye~

ibabyblue

ibabyblue

一名就职于猎豹移动的iOS Coder

8 posts
4 categories
7 tags
GitHub E-Mail
© 2020 ibabyblue
Powered by Hexo
|
Theme — NexT.Muse v5.1.4