如何开发出一款仿映客直播APP项目实践篇 -【采集篇 】

【目录】

【采集基本原理】

采集: 硬件(摄像头)视频图像
推流: 就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。

推流前的工作:采集,处理,编码压缩
推流中做的工作: 封装,上传

推流前的工作

推流——采集到的音频,视频数据通过流媒体协议发送到流媒体服务器

【视频采集】

- 方法一:利用封装库LFLiveKit(推荐)

- 方法二:利用系统库AVFoundation

接下来,我会分别贴上两种方法代码
** 其实 LFLiveKit 已经实现了 后台录制、美颜功能、支持h264、AAC硬编码,动态改变速率,RTMP传输等,对AVFoundation库进行了封装,我们真正开发的时候直接使用就很方便啦。**另外也有:
LiveVideoCoreSDK : 实现了美颜直播和滤镜功能,我们只要填写RTMP服务地址,直接就可以进行推流啦。
PLCameraStreamingKit: 也是一个不错的 RTMP 直播推流 SDK。

虽然推荐用 LFLiveKit 已包含采集美颜编码推流等功能,而为了进一步了解采集到推流完整过程,可以参观方法二代码按自己的步子试着走走,详细讲解每个流程的原理。

方法一、利用LFLiveKit

xib上添加两个Button 和一个Label (主要监听连接状态)


Snip20161124_10.png

2.创建CaputuereLiveViewController.m类 注释都写在文档中

//  CaputuereLiveViewController.m
//  ZKKLiveAPP
//
//  Created by Kevin on 16/11/12.
//  Copyright © 2016年 zhangkk. All rights reserved.
//
#import "CaputuereLiveViewController.h"
#import <LFLiveKit/LFLiveKit.h>
@interface CaputuereLiveViewController ()<LFLiveSessionDelegate>{
  LFLiveSession *_session;
}
//总控制对象
@property(nonatomic,strong)LFLiveSession *session; 
// 推流状态(下一篇推流时用到的)
@property (weak, nonatomic) IBOutlet UILabel *linkStatusLb;
//美颜
@property (weak, nonatomic) IBOutlet UIButton *beautyBtn;
- (IBAction)beautyBtn:(UIButton *)sender;
//切换摄像头
@property (weak, nonatomic) IBOutlet UIButton *changCamreBtn;
- (IBAction)changCamreBtn:(UIButton *)sender;
- (IBAction)backBtn:(UIButton *)sender;

@end

@implementation CaputuereLiveViewController

-(void )viewWillAppear:(BOOL)animated{
  [super viewWillAppear:YES];
  [UIApplication sharedApplication].statusBarHidden = YES;
  self.tabBarController.tabBar.hidden = YES;
  self.hidesBottomBarWhenPushed = YES;

      [self requestAccessForVideo];//请求视频采集权限
  [self requestAccessForAudio];//请求音频权限
      //开始录制
      [self startLive];
  
}
- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor= [UIColor clearColor];

}

-(void)viewWillDisappear:(BOOL)animated{
  [super viewWillDisappear:YES];
  [self stopLive];
}

#pragma mark -- Public Method
-(void)requestAccessForVideo{
  __weak typeof(self) _self = self;
  AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
  switch (status) {
  case AVAuthorizationStatusNotDetermined:
      {
          //许可对话没有出现 则设置请求
          [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
              if(granted){
              dispatch_async(dispatch_get_main_queue(), ^{
                  [_self.session setRunning:YES];
              });
              }
          }];
          break;
      }
  case AVAuthorizationStatusAuthorized:
      {
         dispatch_async(dispatch_get_main_queue(), ^{
             [_self.session setRunning:YES];
         });
          break;
      }
  case AVAuthorizationStatusDenied:
  case AVAuthorizationStatusRestricted:
          //用户获取失败
          break;
  default:
          break;
  }
  
}
-(void)requestAccessForAudio{
  AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
  switch (status) {
case AVAuthorizationStatusNotDetermined:{
    
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
        
    }];
}
          break;
          
      case AVAuthorizationStatusAuthorized:
          break;
      case AVAuthorizationStatusRestricted:
      case AVAuthorizationStatusDenied:
          break;
default:
          break;
  }
  
}
#pragma mark -- LFStreamingSessionDelegate

/**
链接状态
*/
-(void)liveSession:(LFLiveSession *)session liveStateDidChange:(LFLiveState)state{
  switch (state) {
  case LFLiveReady:
          _linkStatusLb.text = @"未连接";
          break;
  case LFLivePending:
          _linkStatusLb.text = @"连接中...";
          break;
  case LFLiveStart:
          _linkStatusLb.text = @"开始连接";
          break;
  case LFLiveStop:
          _linkStatusLb.text = @"断开连接";
          break;
  case LFLiveError:
          _linkStatusLb.text = @"连接错误";
  default:
          break;
  }
}
/*dug CallBack*/
-(void)liveSession:(LFLiveSession *)session debugInfo:(LFLiveDebug *)debugInfo{
  
  NSLog(@"bugInfo:%@",debugInfo);
}
/** callback socket errorcode */
- (void)liveSession:(nullable LFLiveSession *)session errorCode:(LFLiveSocketErrorCode)errorCode {
  NSLog(@"errorCode: %ld", errorCode);
}
/**
**Live
*/
-(void )startLive{
  LFLiveStreamInfo *stream = [LFLiveStreamInfo new];
  /*stream.url = @"rtmp://192.168.0.2:1990/liveApp/room";
  [self.session startLive:stream];*/后续推流时使用
}
-(void)stopLive{
  [self.session stopLive];
}
- (LFLiveSession*)session {
  if (!_session) {
      _session = [[LFLiveSession alloc] initWithAudioConfiguration:[LFLiveAudioConfiguration defaultConfiguration] videoConfiguration:[LFLiveVideoConfiguration defaultConfiguration]];
      _session.preView = self.view;//将摄像头采集数据源渲染到view上
      _session.delegate = self;
  }
  return _session;
}
- (void)didReceiveMemoryWarning {
  [super didReceiveMemoryWarning];
  // Dispose of any resources that can be recreated.
}

/**
**Action 美颜/切换前后摄像头

@param sender button
*/
- (IBAction)beautyBtn:(UIButton *)sender {
  sender.selected = !sender.selected;
  self.session.beautyFace = !self.session.beautyFace;
}
- (IBAction)changCamreBtn:(UIButton *)sender {
  AVCaptureDevicePosition position = self.session.captureDevicePosition;
  self.session.captureDevicePosition = (position == AVCaptureDevicePositionBack)?AVCaptureDevicePositionBack:AVCaptureDevicePositionFront;
}

- (IBAction)backBtn:(UIButton *)sender {
  NSLog(@"返回");
//  self.view.window.rootViewController =   self.tabBarController;

  [self.tabBarController setSelectedIndex:0];
  self.tabBarController.tabBar.hidden = NO;
}

*/


@end


方法二、利用系统AVFoundation采集视频

一、采集硬件(摄像头)视频图像

#import "CaputureViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "GPUImageBeautifyFilter.h"
@interface CaputureViewController ()<AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>

/**采集视频*/
//切换屏幕按钮
@property (weak, nonatomic) IBOutlet UIButton *changScreenBtn;
//采集视频总控制
@property(nonatomic,strong)AVCaptureSession *captureSession;
//视频采集输入数据源
@property(nonatomic,strong)AVCaptureDeviceInput *currentVideoDeviceInput;
//将摄像头采集数据源显示在屏幕上 
@property(nonatomic,weak)AVCaptureVideoPreviewLayer *previedLayer;
//采集的截取数据流 一般用与美颜等处理
@property(nonatomic,weak)AVCaptureConnection *videoConnection;
- (IBAction)changScreenBtn:(UIButton *)sender;

/*开启美颜*/
@property (weak, nonatomic) IBOutlet UISwitch *openBeautySwitch;

- (IBAction)switch:(UISwitch *)sender;
//@property(nonatomic,)BOOL isOpenBeauty;
//@property(nonatomic,strong)<#type#> *<#Name#>;



@end

@implementation CaputureViewController

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:YES];
    if (_captureSession) {
        [_captureSession startRunning];
    }

}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.focusCursorImageView];
    self.view.backgroundColor = [UIColor whiteColor];
    /*1. 采集视频 -avfoundation */
    [self setupCaputureVideo];
    /*2. GPUImage 美颜视图 */
    
    

}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:YES];
    if (_captureSession) {
        [_captureSession stopRunning];
    }
}

/**
 音视频捕获
 */
-(void)setupCaputureVideo{
    //创建管理对象
    _captureSession = [[AVCaptureSession alloc]init];
    
    //获取摄像头和音频
//  AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    AVCaptureDevice *videoDevice = [self getVideoDevice:AVCaptureDevicePositionFront];
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    
    //创建对应音视频设备输入对象
    AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:nil];
    AVCaptureDeviceInput * audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];
    _currentVideoDeviceInput = videoDeviceInput;
    
    if ([_captureSession canAddInput:_currentVideoDeviceInput]) {
        [_captureSession addInput:_currentVideoDeviceInput];
    }
    if ([_captureSession canAddInput:audioDeviceInput]) {
        [_captureSession canAddInput:audioDeviceInput];
    }
    
    //获取系统输出的视频源
    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc]init];
    AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc]init];
    //串行对列
    dispatch_queue_t videoQueue = dispatch_queue_create("VideoQueue",DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t audioQueue = dispatch_queue_create("audioQueue", DISPATCH_QUEUE_SERIAL);
    [videoOutput setSampleBufferDelegate:self queue:videoQueue];
    [audioOutput setSampleBufferDelegate:self queue:audioQueue];
    videoOutput.videoSettings = @{(NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
//  _videoOutput.videoSettings = captureSettings;
    //添加输出源 到控制类session中
    if ([_captureSession canAddOutput:videoOutput]) {
        [_captureSession addOutput: videoOutput];
    }
    if ([_captureSession canAddOutput:audioOutput]) {
        [_captureSession addOutput:audioOutput];
    }
    
    //获取视频输入和输出的链接 用于分辨音视频数据 做处理时用到
    _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
    
    
    //将视屏数据加入视图层 显示
    AVCaptureVideoPreviewLayer  *previedLayer = [AVCaptureVideoPreviewLayer layerWithSession:_captureSession];
    previedLayer.frame = [UIScreen mainScreen].bounds;
    [self.view.layer insertSublayer:previedLayer atIndex:0];
    [self.view.layer insertSublayer:_changScreenBtn.layer atIndex:1];
    _previedLayer = previedLayer;
    
    [_captureSession startRunning];
    
}
//获取切换后的摄像头
- (IBAction)changScreenBtn:(UIButton *)sender {
    //获取当前的摄像头
    AVCaptureDevicePosition curPosition = _currentVideoDeviceInput.device.position;
    //获取改变的方向
    AVCaptureDevicePosition togglePosition = curPosition == AVCaptureDevicePositionFront?AVCaptureDevicePositionBack:AVCaptureDevicePositionFront;
    //获取当前的摄像头
    AVCaptureDevice *toggleDevice = [self getVideoDevice:togglePosition];
    
    //切换输入设备
    AVCaptureDeviceInput *toggleDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:toggleDevice error:nil];
    [_captureSession removeInput:_currentVideoDeviceInput];
    [_captureSession addInput:toggleDeviceInput];
    _currentVideoDeviceInput = toggleDeviceInput;
    
}
-(AVCaptureDevice *)getVideoDevice:(AVCaptureDevicePosition)position {
    
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for( AVCaptureDevice *device in devices) {
        if (device .position == position) {
            return device;
        }
    }
    return nil;
}


-(UIImageView *)focusCursorImageView{
    if (!_focusCursorImageView) {
        _focusCursorImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"focus"]];
    }
    return _focusCursorImageView;
}

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
//截取输出的视频数据
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
    
    if (_videoConnection == connection) {
        NSLog(@"采集的视频数据");
        /*美颜*/
        
    }else{
        NSLog(@"采集的音频数据");
        
    }
}

上述是大致实现获取最基本数据的情况,一些细节(尺寸、方向)暂时没有深入,真正做直播的时候,一般是视频和音频是分开处理的,只有重点注意那个代理方法。
二、GPUImage 处理

在进行编码 H.264 之前,一般来说肯定会做一些美颜处理的,否则那播出的感觉太真实,就有点丑啦,在此以磨皮和美白为例简单了解。(具体参考的是:琨君 基于 GPUImage 的实时美颜滤镜
直接用 BeautifyFaceDemo 中的类 GPUImageBeautifyFilter
, 可以对的图片直接进行处理:

GPUImageBeautifyFilter *filter = [[GPUImageBeautifyFilter alloc] init];
UIImage *image = [UIImage imageNamed:@"testMan"];
UIImage *resultImage = [filterimageByFilteringImage:image];
self.backgroundView.image = resultImage;

但是视频中是怎样进行美容处理呢?怎样将其转换的呢?平常我们这样直接使用:

GPUImageBeautifyFilter *beautifyFilter = [[GPUImageBeautifyFilter alloc] init];[self.videoCamera addTarget:beautifyFilter];[beautifyFilter addTarget:self.gpuImageView];

此处用到了 GPUImageVideoCamera,可以大致了解下 GPUImage详细解析(三)- 实时美颜滤镜
GPUImageVideoCamera: GPUImageOutput的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。
GPUImageView:响应链的终点,一般用于显示GPUImage的图像。
GPUImageFilter:用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
GPUImageFilterGroup:多个GPUImageFilter的集合。
GPUImageBeautifyFilter

@interface GPUImageBeautifyFilter : GPUImageFilterGroup { 
GPUImageBilateralFilter *bilateralFilter; 
GPUImageCannyEdgeDetectionFilter *cannyEdgeFilter; 
GPUImageCombinationFilter *combinationFilter; 
GPUImageHSBFilter *hsbFilter;
}

简单理解这个美颜的流程

不得不说GPUImage 是相当强大的,此处的功能也只是显现了一小部分,其中 filter 那块的处理个人目前还有好多不理解,需要去深入了解啃源码,暂时不过多引入。通过这个过程将 sampleBuffer 美容处理后,自然是进行编码啦。
三、视频、音频压缩编码

而编码是用 硬编码呢 还是软编码呢? 相同码率,软编图像质量更清晰,但是耗电更高,而且会导致CPU过热烫到摄像头。不过硬编码会涉及到其他平台的解码,有很多坑。综合来说,iOS 端硬件兼容性较好,iOS 8.0占有率也已经很高了,可以直接采用硬编。
硬编码:下面几个DEMO 可以对比下,当然看 LFLiveKit 更直接。
VideoToolboxPlus
iOSHardwareDecoder
-VideoToolboxDemo
iOS-h264Hw-Toolbox

四、推流

封装数据成 FLV,通过 RTMP 协议打包上传,从主播端到服务端即基本完成推流。
4-1、封装数据通常是封装成 FLV
FLV流媒体格式是一种新的视频格式,全称为FlashVideo。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等缺点。
封包 FLV):一般FLV 文件结构里是这样存放的:
[[Flv Header]
[Metainfo Tag]
[Video Tag]
[Audio Tag]
[Video Tag]
[Audio Tag]
[Other Tag]…]
其中 AudioTag 和 VideoTag 出现的顺序随机的,没有严格的定义。Flv Header 是文件的头部,用FLV字符串标明了文件的类型,以及是否有音频、视频等信息。之后会有几个字节告诉接下来的包字节数。Metainfo 中用来描述Flv中的各种参数信息,例如视频的编码格式、分辨率、采样率等等。如果是本地文件(非实时直播流),还会有偏移时间戳之类的信息用于支持快进等操作。VideoTag 存放视频数据。对于H.264来说,第一帧发送的NALU应为 SPS和PPS,这个相当于H.264的文件头部,播放器解码流必须先要找到这个才能进行播放。之后的数据为I帧或P帧。AudioTag 存放音频数据。对于AAC来说,我们只需要在每次硬编码完成后给数据加上adts头部信息即可。
iOS 中的使用:详细看看 LFLiveKit 中的 LFStreamRTMPSocket 类。

总的说来,这又是一个粗略的过程,站在好多个巨人的肩膀上,但是还是基本了解了一个推流的流程,没有正式项目的经验,肯定有太很多细节点忽略了和好多坑需要填,还是那个目的,暂时先作为自己的预备知识点吧,不过此处可以扩展和深入的知识点真的太多啦,如 LFLiveKitGPUImage 仅仅展露的是冰山一角。

代码地址:
gitHub : https://github.com/one-tea/ZKKLiveDemo

备注参考:
LiveVideoCoreSDK
LFLiveKit
GPUImage
LMLiveStreaming
PLCameraStreamingKit
iOS手机直播Demo技术简介
iOS视频开发经验
iOS 上的相机捕捉
CMSampleBufferRef 与 UIImage 的转换
GPUImage详细解析(三)- 实时美颜滤镜
iOS8系统H264视频硬件编解码说明
利用FFmpeg+x264将iOS摄像头实时视频流编码为h264文件
使用VideoToolbox硬编码H.264
使用iOS自带AAC编码器
如何搭建一个完整的视频直播系统?
直播中累积延时的优化
使用VLC做流媒体服务器(直播形式)

gitHub代码地址

Object-C版 : https://github.com/one-tea/ZKKLiveDemo
Swift版 : https://github.com/one-tea/ZKKLiveAPP_Swift3.0

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

推荐阅读更多精彩内容