iOS直播推流实现-采集

将最近学习的直播推流技术做个笔记。
iOS推流的主要流程如下:

  1. 视频音频采集
  2. 视频美颜滤镜和贴纸
  3. 视频编码和音频编码
  4. 推流到流服务器

采集

iOS一般使用AVFundation进行采集,涉及到的类如下:

  • AVCaptureSession 采集的会话对象,它一头连接输入对象,一头连接输出对象向app提供采集好的原始音视频数据,通过它管理采集的开始与结束

  • AVCaptureDevice 采集用的设备,比如麦克风还是摄像头,是前置摄像头还是后置摄像头。所以初始化时一般指定mediaType:AVMediaTypeAudio(音频) or AVMediaTypeVideo(视频)

  • AVCaptureDeviceInput 采集输入对象,通过AVCaptureDevice对象进行初始化

  • AVCaptureVideoDataOutputAVCaptureAudioDataOutput 视频数据和音频数据采集后的输出对象,与输入对象对应。废话不多说了看代码吧!
    // 先创建一个采集类,并进行必要属性定义吧

@interface Capture()<AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate> {
    // 采集相关对象
    AVCaptureVideoDataOutput *videoOutput;
    AVCaptureAudioDataOutput *audioOutput;
    
    AVCaptureConnection *videoConnection;
    AVCaptureConnection *audioConnection;
        
    dispatch_queue_t acaptureQueue;
    dispatch_queue_t vcaptureQueue;
    dispatch_semaphore_t samaphore;
    
    AVCaptureDeviceInput *_deviceInput;
    int sampleCount;
 }
@property (nonatomic, assign) AVCaptureDevicePosition devicePosition;
@property (nonatomic, assign) AVCaptureVideoOrientation orientation;
@property (nonatomic, assign) AVCaptureSessionPreset preset;
@property (nonatomic, assign) BOOL isMirrored;
@end
  1. 初始化
// 1.创建一个采集类会话 AVCaptureSession
_session = [[AVCaptureSession alloc] init];
// 设置视频采集的宽高参数
 _session.sessionPreset = _preset; // @sea AVCaptureSessionPreset1280x720
// 2.设置音频采集
AVCaptureDevice *micro = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInMicrophone mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified];
    AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:micro error:nil];
    if (![_session canAddInput:audioInput]) {
        NSLog(@"can not add audioInput");
        return  NO;
    }
    // 将采集输入源添加到session
    [_session addInput:audioInput];
    // 3. 采集物理设备对象,选择后置摄像头
    AVCaptureDevice *camera = [self videoDeviceWitchPosition:_devicePosition];
    // 通过视频物理设备对象创建视频输入对象
    AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:camera error:nil];
    if (![_session canAddInput:videoInput]) {
        NSLog(@"can not add video input");
        return NO;
    }
    _deviceInput = videoInput;
    [_session addInput:videoInput];
    /* 如果调用了[session startRunning]之后要想改变音视频输出对象配置参数,则必须调用[session beginConfiguration];和 [session commitConfiguration];才能生效。
如果没有调用[session startRunning]则这两句代码可以不写 */
    [_session beginConfiguration];
    // 4. 创建视频输出对象
    videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    // 设置视频输出参数,这里设置输出格式为YUV420YpCbCr8
    NSDictionary *videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)};
    videoOutput.videoSettings = videoSettings;
    // 当采集速度过快而处理速度跟不上时的丢弃策略,默认丢弃最新采集的视频。这里设置为NO,表示不丢弃缓存起来
    videoOutput.alwaysDiscardsLateVideoFrames = YES;
   // vcaptureQueue 输出执行的视频队列,它是一个串行队列  
   // 视频输出代理
    [videoOutput setSampleBufferDelegate:self queue:vcaptureQueue];
   // 添加到输出
    [_session addOutput:videoOutput];
    
    // 5. 创建音频输出对象
    audioOutput = [[AVCaptureAudioDataOutput alloc] init];
   // acaptureQueue 输出执行的音频队列,它是一个串行队列  
   // 音频输出代理
    [audioOutput setSampleBufferDelegate:self queue:acaptureQueue];
   // 添加到输出
    [_session addOutput:audioOutput];
    // 6. 分别创建音频和视频AVCaptureConnection
    /*AVCaptureConnection表示avcaptureputport或端口之间的连接, 可用AVCaptureVideoPreviewLayer呈现采集    
      的内容。*/
    videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
    audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio];
    // 视频镜像采集
    videoConnection.videoMirrored = _isMirrored;
    // 设置采集的方向,如果不设置采集到的视频旋转了90度。
    if([videoConnection isVideoOrientationSupported]) {
        videoConnection.videoOrientation = _orientation;
    }
    // 提交配置
    [_session commitConfiguration];

  1. 以上是初始化的过程,开始采集和结束采集还需要调用:
///  开始采集
- (void)startRunning {
    [_session startRunning];
}
/// 停止采集
- (void)stopRunning {
    [_session stopRunning];
}
  1. 设置代理
// 这里的sampleBuffer就是采集到的数据了,根据connection来判断,是Video还是Audio的数据
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    // 这里的sampleBuffer就是采集到的数据了,根据connection来判断,是Video还是Audio的数据
    if (connection == videoConnection) {
        NSLog(@"这里获的 video sampleBuffer,做进一步处理(编码H.264)%i", ++sampleCount);
        if (self.delegate) {
            [self.delegate capture:self videoBuffer:sampleBuffer];
        }
    } else if (connection == audioConnection) {
        NSLog(@"这里获得 audio sampleBuffer,做进一步处理(编码AAC)");
        if (self.delegate) {
            [self.delegate capture:self audioBuffer:sampleBuffer];
        }
    }
}
  1. 显示,使用AVCaptureVideoPreviewLayer可以显示采集的视频,自定义一个UIView, 命名CapturePreviewView,设置layerClass为[AVCaptureVideoPreviewLayer class],设置layer基本参数:
        self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; 
        self.previewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;

在viewDidLoad里调用

- (void)showPreview {
    self.preview = [[CapturePreviewView alloc] initWithFrame:self.view.bounds];
    _preview.previewLayer.session = self.capture.session;
    _preview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view insertSubview:_preview atIndex:0];
}
  1. 我们可以对采集到的数据进行预览播放,使用AVSampleBufferDisplayLayer播放 CMSampleBufferRef格式数据
-(void)showSampleLayer {
    _displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
    _displayLayer.frame = CGRectMake(0, 0, _videoEncoder.config->width / 5.0, _videoEncoder.config->height / 5.0);
    _displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    [self.preview.layer insertSublayer:_displayLayer above:_preview.previewLayer];
}

// 在采集到视频数据的回调里播放buffer
-(void)capture:(Capture *)capture videoBuffer:(CMSampleBufferRef _Nullable)buffer
{
    // 显示播放
     [_displayLayer enqueueSampleBuffer:buffer];
    // [_videoEncoder encode:buffer timeStamp:CACurrentMediaTime()*1000];
}

6.修改采样参数,如采样率,帧率等等

/// 更新帧率
- (void)updateFps:(int32_t)fps {
 
    AVCaptureDevice *vDevice = [self videoDeviceWitchPosition:_devicePosition];
    //获取当前支持的最大fps
    float maxRate = [(AVFrameRateRange *)[vDevice.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0] maxFrameRate];
    //如果想要设置的fps小于或等于做大fps,就进行修改
    if (maxRate >= fps) {
        //实际修改fps的代码
        if ([vDevice lockForConfiguration:NULL]) {
            vDevice.activeVideoMinFrameDuration = CMTimeMake(10, (int)(fps * 10));
            vDevice.activeVideoMaxFrameDuration = vDevice.activeVideoMinFrameDuration;
            [vDevice unlockForConfiguration];
        }
    }
}
/// 切换摄像头(前置或后置)
- (void)changeCamaraPosition {
    dispatch_async(vcaptureQueue, ^{
        if (self.devicePosition == AVCaptureDevicePositionFront) {
            self.devicePosition = AVCaptureDevicePositionBack;
        } else {
            self.devicePosition = AVCaptureDevicePositionFront;
        }
        AVCaptureDevice *camera = [self videoDeviceWitchPosition:self.devicePosition];
        [self.session beginConfiguration];
        [self.session removeInput:self->_deviceInput];
        
        AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:camera error:nil];
        if (!videoInput) {
            NSLog(@"can not init video input");
            return;
        }
        if (![self.session canAddInput:videoInput]) {
            NSLog(@"can not add video input");
            return;
        }
        self->_deviceInput = videoInput;
        [self.session addInput:videoInput];

        [self.session commitConfiguration];
        
     });
 
}
/// 设置视频采集方向
- (void)setVideoOrientation:(AVCaptureVideoOrientation)orientation
{
    _orientation = orientation;
    dispatch_async(vcaptureQueue, ^{
        self->videoConnection.videoOrientation = orientation;
    });
}
/// 设置是否镜像
- (void)setVideoMirrored:(BOOL)isMirrored
{
    _isMirrored = isMirrored;
    dispatch_async(vcaptureQueue, ^{
        self->videoConnection.videoMirrored = isMirrored;
    });
}
/// 设置采集分辨率
- (void)setVideoDimension:(AVCaptureSessionPreset)preset
{
    _preset = preset;
    dispatch_async(vcaptureQueue, ^{
        [self.session beginConfiguration];
        if ([self.session canSetSessionPreset:preset]) {
            [self.session setSessionPreset:preset];
        };
        [self.session commitConfiguration];
    });
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • 推流,就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。 推流前的工作:采集,处理,编码压缩 推流中做...
    小緈福阅读 3,011评论 1 19
  • 推流,就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。 推流前的工作:采集,处理,编码压缩 推流中做...
    木马不在转阅读 7,397评论 13 30
  • 需求 众所周知,原始的音视频数据无法直接在网络上传输,推流需要编码后的音视频数据以合成的视频流,如flv, mov...
    小东邪啊阅读 5,774评论 3 21
  • 一直在忙, 也没写过几次博客! 但一直热衷于直播开发技术, 公司又不是直播方向的, 所以就年前忙里偷闲研究了一下直...
    叫我丰叔阅读 17,367评论 24 132
  • #直播总结 ##1.概述 关于直播的技术文章不少,成体系的不多。我们将用这篇文章,更系统化地介绍当下大热的视频直播...
    盖世英雄_ix4n04阅读 1,369评论 0 2