界面
- (void)openVideoCapture {
self.session = [[AVCaptureSession alloc] init];
[self.session setSessionPreset:AVCaptureSessionPreset640x480];
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
UIView *cameraView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
previewLayer.frame = cameraView.bounds;
[cameraView.layer addSublayer:previewLayer];
[self.view addSubview:cameraView];
// 摄像头以及视频输入
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
videoDevice = [self cameraWithPosition:AVCaptureDevicePositionFront];
NSError *error = nil;
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if ([self.session canAddInput:videoInput]) {
[self.session addInput:videoInput];
}
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
videoOutput.videoSettings = [NSDictionary dictionaryWithObject:
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]forKey:(NSString *)kCVPixelBufferPixelFormatTypeKey];
[videoOutput setAlwaysDiscardsLateVideoFrames:YES];
if ([_session canAddOutput:videoOutput] == NO)
{
YGNLog(@"Couldn't add video output");
return ;
}
[_session addOutput:videoOutput];
_videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];// 设置采集图像的方向,如果不设置,采集回来的图形会是旋转90度的
_videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
dispatch_queue_t queue = dispatch_queue_create("VideoCaptureQueue", DISPATCH_QUEUE_SERIAL);// 摄像头采集queue
[videoOutput setSampleBufferDelegate:self queue:queue];
_h264File = fopen([[NSString stringWithFormat:@"%@/vt_encode.h264", self.documentDictionary] UTF8String], "wb");
//麦克风以及音频输入
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
if ([self.session canAddInput:audioInput]) {
[self.session addInput:audioInput];
}
AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
dispatch_queue_t queue1 = dispatch_queue_create("AudioCaptureQueue", DISPATCH_QUEUE_SERIAL);// 摄像头采集queue
[audioOutput setSampleBufferDelegate:self queue:queue1];
if ([_session canAddOutput:audioOutput] == NO)
{
YGNLog(@"Couldn't add audio output");
return ;
}
_audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio];
[_session addOutput:audioOutput];
// 文件保存在document文件夹下,可以直接通过iTunes将文件导出到电脑,在plist文件中添加Application supports iTunes file sharing = YES
[self.session commitConfiguration];
return ;
}
代理方法
#pragma mark - AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
if (_mediuChatType == YGMessageChatCallType_AudioReceive || _mediuChatType == YGMessageChatCallType_AudioCall) {
// NSData *PCMData = [self convertAudioSmapleBufferToPcmData:sampleBuffer];
if (ipTestString != nil && [YGUDPNatManager sharedManager].isUdpNatTranting == YES) {
// YGNLog(@"PCMData.length======%lu",(unsigned long)PCMData.length);
// [[YGUDPNatManager sharedManager] sendTestData:PCMData withIP:ipTestString withPort:1234];
}
} else {
//视频采集
if(connection == _videoConnection){
[self encodeFrame:sampleBuffer];
}else{
NSData* data = [self convertAudioSmapleBufferToPcmData:sampleBuffer];
if(data) {
[_audioData appendData:data];
}
}
}
}
音频MP3处理
//提取pcm
-(NSData *) convertAudioSmapleBufferToPcmData:(CMSampleBufferRef) audioSample{
//获取pcm数据大小
NSInteger audioDataSize = CMSampleBufferGetTotalSampleSize(audioSample);
//分配空间
int8_t *audio_data = malloc((int32_t)audioDataSize);
//获取CMBlockBufferRef
//这个结构里面就保存了 PCM数据
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(audioSample);
//直接将数据copy至我们自己分配的内存中
CMBlockBufferCopyDataBytes(dataBuffer, 0, audioDataSize, audio_data);
NSData *pcmData = [NSData dataWithBytesNoCopy:audio_data length:audioDataSize];
//返回数据
return pcmData;
}
#pragma mark ---------pcm 转MP3
- (void)conventToMp3 {
NSLog(@"convert begin!!");
NSString *cafFilePath = [[YGMessageMediaUtily sharedMediaUtily] stringPathWithType:YGMessageMediaType_ImFile sessionID:0 mType:0 fileName:@"vt_encode.pcm"];
NSString *mp3FilePath = [[NSHomeDirectory() stringByAppendingFormat:@"/Documents/"] stringByAppendingPathComponent:@"vy_encode.mp3"];
@try {
int read, write;
FILE *pcm = fopen([cafFilePath cStringUsingEncoding:NSASCIIStringEncoding], "rb");
fseek(pcm, 4*1024, SEEK_CUR);
FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:NSASCIIStringEncoding], "wb");
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE * 2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init();
lame_set_in_samplerate(lame,22000);//采样播音速度,值越大播报速度越快,反之。
lame_set_brate(lame,128);
lame_set_num_channels(lame, 2);
lame_set_mode(lame,MONO);
lame_set_quality (lame, 2); /* 2=high 5 = medium 7=low 音 质 */
lame_set_VBR(lame, vbr_default);
lame_init_params(lame);
do {
read = (int)fread(pcm_buffer, 2 * sizeof(short int), PCM_SIZE, pcm);
if (read == 0)
write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
else
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
} while (read != 0);
lame_close(lame);
fclose(mp3);
fclose(pcm);
}
@catch (NSException *exception) {
NSLog(@"%@", [exception description]);
}
@finally {
NSLog(@"convert mp3 finish!!!");
}
}
===========挂断的时候调用==============
[YGUDPNatManager sharedManager].isUdpNatTranting = YES;
[[YGMessageMediaUtily sharedMediaUtily] storeData:_audioData forFileName:@"vt_encode.pcm" sessionID:0 type:YGMessageMediaType_ImFile mType:0];//存储数据到本地
[self conventToMp3];
视频h264处理
- (int)startEncodeSession:(int)width height:(int)height framerate:(int)fps bitrate:(int)bt
{
OSStatus status;
_frameCount = 0;
VTCompressionOutputCallback cb = encodeOutputCallback;
status = VTCompressionSessionCreate(kCFAllocatorDefault, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, cb, (__bridge void *)(self), &_encodeSesion);
if (status != noErr) {
NSLog(@"VTCompressionSessionCreate failed. ret=%d", (int)status);
return -1;
}
// 设置实时编码输出,降低编码延迟
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
NSLog(@"set realtime return: %d", (int)status);
// h264 profile, 直播一般使用baseline,可减少由于b帧带来的延时
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
NSLog(@"set profile return: %d", (int)status);
// 设置编码码率(比特率),如果不设置,默认将会以很低的码率编码,导致编码出来的视频很模糊
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(bt)); // bps
status += VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)@[@(bt*2/8), @1]); // Bps
NSLog(@"set bitrate return: %d", (int)status);
// 设置关键帧间隔,即gop size
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(fps*2));
// 设置帧率,只用于初始化session,不是实际FPS
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(fps));
NSLog(@"set framerate return: %d", (int)status);
// 开始编码
status = VTCompressionSessionPrepareToEncodeFrames(_encodeSesion);
NSLog(@"start encode return: %d", (int)status);
return 0;
}
// 编码一帧图像,使用queue,防止阻塞系统摄像头采集线程
- (void) encodeFrame:(CMSampleBufferRef )sampleBuffer
{
dispatch_sync(_encodeQueue, ^{
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// pts,必须设置,否则会导致编码出来的数据非常大,原因未知
CMTime pts = CMTimeMake(_frameCount, 1000);
CMTime duration = kCMTimeInvalid;
VTEncodeInfoFlags flags;
// 送入编码器编码
OSStatus statusCode = VTCompressionSessionEncodeFrame(_encodeSesion,
imageBuffer,
pts, duration,
NULL, NULL, &flags);
if (statusCode != noErr) {
NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
[self stopEncodeSession];
return;
}
});
}
- (void) stopEncodeSession
{
VTCompressionSessionCompleteFrames(_encodeSesion, kCMTimeInvalid);
VTCompressionSessionInvalidate(_encodeSesion);
CFRelease(_encodeSesion);
_encodeSesion = NULL;
}
// 编码回调,每当系统编码完一帧之后,会异步掉用该方法,此为c语言方法
void encodeOutputCallback(void *userData, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer )
{
if (status != noErr) {
NSLog(@"didCompressH264 error: with status %d, infoFlags %d", (int)status, (int)infoFlags);
return;
}
if (!CMSampleBufferDataIsReady(sampleBuffer))
{
NSLog(@"didCompressH264 data is not ready ");
return;
}
YGVideoChatViewController* vc = (__bridge YGVideoChatViewController*)userData;
// 判断当前帧是否为关键帧
bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
// 获取sps & pps数据. sps pps只需获取一次,保存在h264文件开头即可
if (keyframe && !vc->_spsppsFound)
{
size_t spsSize, spsCount;
size_t ppsSize, ppsCount;
const uint8_t *spsData, *ppsData;
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
OSStatus err0 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0 );
OSStatus err1 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0 );
if (err0==noErr && err1==noErr)
{
vc->_spsppsFound = 1;
[vc writeH264Data:(void *)spsData length:spsSize addStartCode:YES];
[vc writeH264Data:(void *)ppsData length:ppsSize addStartCode:YES];
NSLog(@"got sps/pps data. Length: sps=%zu, pps=%zu", spsSize, ppsSize);
}
}
size_t lengthAtOffset, totalLength;
char *data;
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus error = CMBlockBufferGetDataPointer(dataBuffer, 0, &lengthAtOffset, &totalLength, &data);
if (error == noErr) {
size_t offset = 0;
const int lengthInfoSize = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
// 循环获取nalu数据
while (offset < totalLength - lengthInfoSize) {
uint32_t naluLength = 0;
memcpy(&naluLength, data + offset, lengthInfoSize); // 获取nalu的长度,
// 大端模式转化为系统端模式
naluLength = CFSwapInt32BigToHost(naluLength);
NSLog(@"got nalu data, length=%d, totalLength=%zu", naluLength, totalLength);
// 保存nalu数据到文件
[vc writeH264Data:data+offset+lengthInfoSize length:naluLength addStartCode:YES];
// 读取下一个nalu,一次回调可能包含多个nal H264: VTCompressionSessionEncodeFrame failed with -12902u
offset += lengthInfoSize + naluLength;
}
}
}
// 保存h264数据到文件
- (void) writeH264Data:(void*)data length:(size_t)length addStartCode:(BOOL)b
{
// 添加4字节的 h264 协议 start code
const Byte bytes[] = "\x00\x00\x00\x01";
if (_h264File) {
if(b)
fwrite(bytes, 1, 4, _h264File);
fwrite(data, 1, length, _h264File);
NSLog(@"save success");
} else {
NSLog(@"_h264File null error, check if it open successed");
}
}
- (void)viewDidAppear:(BOOL)animated {
[self startEncodeSession:480 height:640 framerate:25 bitrate:640*1000];
[_session startRunning];
}
总结
此处视频是实时转,视频中的音频是先存为pcm文件。然后挂断的时候将 pcm 转为MP3。另外,在转MP3
的时候 采样率以及相关参数一定要设置对。如果有变音就是采样率的问题。有问题或有补充的盆友欢迎底下留言。