iOS 直播 —— 推流

作者: 天空中的球 | 来源:发表于2016-09-05 01:54 被阅读9812次

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

  • 推流前的工作:采集,处理,编码压缩
  • 推流中做的工作: 封装,上传
推流前的工作 推流——采集到的音频,视频数据通过流媒体协议发送到流媒体服务器

话说回来,** 其实有一个库 LFLiveKit 已经实现了 后台录制、美颜功能、支持h264、AAC硬编码,动态改变速率,RTMP传输等,我们真正开发的时候直接使用就很方便啦。**
另外也有:

  • LiveVideoCoreSDK : 实现了美颜直播和滤镜功能,我们只要填写RTMP服务地址,直接就可以进行推流啦。
  • PLCameraStreamingKit: 也是一个不错的 RTMP 直播推流 SDK。

但还是推荐用 LFLiveKit,而为了进一步了解推流这个过程,先按自己的步子试着走走,了解下。

一、采集视频

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

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

@interface MovieViewController ()<AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>

@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoOutput;
@property (nonatomic, strong) AVCaptureAudioDataOutput *audioOutput;

@property (nonatomic, strong) dispatch_queue_t videoQueue;
@property (nonatomic, strong) dispatch_queue_t audioQueue;

@property (nonatomic, strong) AVCaptureConnection *videoConnection;
@property (nonatomic, strong) AVCaptureConnection *audioConnection;

@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;

@end

@implementation MovieViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initSession];
    [self showPlayer];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.session startRunning];
}

- (void)viewDidDisappear:(BOOL)animated {
    [self.session stopRunning];
}

- (void)initSession {
    // 初始化 session
    _session = [[AVCaptureSession alloc] init];
    
    // 配置采集输入源(摄像头)
    NSError *error = nil;
    // 获得一个采集设备, 默认后置摄像头
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    // 用设备初始化一个采集的输入对象
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
    if (error) {
        NSLog(@"Error getting  input device: %@", error.description);
        return;
    }
    
    if ([_session canAddInput:videoInput]) {
        [_session addInput:videoInput]; // 添加到Session
    }
    if ([_session canAddInput:audioInput]) {
        [_session addInput:audioInput]; // 添加到Session
    }
    // 配置采集输出,即我们取得视频图像的接口
    _videoQueue = dispatch_queue_create("Video Capture Queue", DISPATCH_QUEUE_SERIAL);
    _audioQueue = dispatch_queue_create("Audio Capture Queue", DISPATCH_QUEUE_SERIAL);
    
    _videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    _audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    
    [_videoOutput setSampleBufferDelegate:self queue:_videoQueue];
    [_audioOutput setSampleBufferDelegate:self queue:_audioQueue];
    
    // 配置输出视频图像格式
    NSDictionary *captureSettings = @{(NSString*)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
    _videoOutput.videoSettings = captureSettings;
    _videoOutput.alwaysDiscardsLateVideoFrames = YES;
    if ([_session canAddOutput:_videoOutput]) {
       [_session addOutput:_videoOutput];  // 添加到Session
    }
    
    if ([_session canAddOutput:_audioOutput]) {
        [_session addOutput:_audioOutput]; // 添加到Session
    }
    // 保存Connection,用于在SampleBufferDelegate中判断数据来源(Video/Audio)
    _videoConnection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
    _audioConnection = [_audioOutput connectionWithMediaType:AVMediaTypeAudio];

}

- (void)showPlayer {
    _previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
    _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; // 设置预览时的视频缩放方式
    [[_previewLayer connection] setVideoOrientation:AVCaptureVideoOrientationPortrait]; // 设置视频的朝向
    _previewLayer.frame = self.view.layer.bounds;
    [self.view.layer addSublayer:_previewLayer];
}

#pragma mark 获取 AVCapture Delegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    
    // 这里的sampleBuffer就是采集到的数据了,根据connection来判断,是Video还是Audio的数据
    if (connection == self.videoConnection) {  // Video
        NSLog(@"这里获的 video sampleBuffer,做进一步处理(编码H.264)");
    } else if (connection == self.audioConnection) {  // Audio
        NSLog(@"这里获得 audio sampleBuffer,做进一步处理(编码AAC)");
    }
}

@end

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

二、GPUImage 处理

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

直接用 BeautifyFaceDemo 中的类 GPUImageBeautifyFilter, 可以对的图片直接进行处理:

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

备注下 CMSampleBufferRef 与 UIImage 的转换

- (UIImage *)sampleBufferToImage:(CMSampleBufferRef)sampleBuffer {
    //制作 CVImageBufferRef
    CVImageBufferRef buffer;
    buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    CVPixelBufferLockBaseAddress(buffer, 0);
    
    //从 CVImageBufferRef 取得影像的细部信息
    uint8_t *base;
    size_t width, height, bytesPerRow;
    base = CVPixelBufferGetBaseAddress(buffer);
    width = CVPixelBufferGetWidth(buffer);
    height = CVPixelBufferGetHeight(buffer);
    bytesPerRow = CVPixelBufferGetBytesPerRow(buffer);
    
    //利用取得影像细部信息格式化 CGContextRef
    CGColorSpaceRef colorSpace;
    CGContextRef cgContext;
    colorSpace = CGColorSpaceCreateDeviceRGB();
    cgContext = CGBitmapContextCreate(base, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(colorSpace);
    
    //透过 CGImageRef 将 CGContextRef 转换成 UIImage
    CGImageRef cgImage;
    UIImage *image;
    cgImage = CGBitmapContextCreateImage(cgContext);
    image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    CGContextRelease(cgContext);
    CVPixelBufferUnlockBaseAddress(buffer, 0);
    return image;
}

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

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 更直接。

软编码: 利用FFmpeg+x264将iOS摄像头实时视频流编码为h264文件 ,备忘下: FFmpeg-X264-Encode-for-iOS

我直接使用了 LFLiveKit ,里面已经封装的很好啦,此处对 Audiotoolbox && VideoToolbox 简单了解下:

  • AudioToolbox
    iOS使用AudioToolbox中的AudioConverter API 把源格式转换成目标格式, 详细可以看 使用iOS自带AAC编码器
// 1、根据输入样本初始化一个编码转换器
AudioStreamBasicDescription 根据指定的源格式和目标格式创建 audio converter
// 2、初始化一个输出缓冲列表 outBufferList 
// 3、获取 AudioCallBack
OSStatus inputDataProc(AudioConverterRef inConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData,AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) 
// 4、音频格式完成转换
AudioConverterFillComplexBuffer 实现inBufferList 和 outBufferList、inputDataProc音频格式之间的转换。
  • VideoToolbox
    iOS8之后的硬解码、硬编码API,此处只做编码用。
// 1、初始化 VTCompressionSessionRef  
  - (void)initCompressionSession;
// 2、传入  解码一个frame
VTCompressionSessionEncodeFrame(compressionSession, pixelBuffer, presentationTimeStamp, duration, (__bridge CFDictionaryRef)properties, (__bridge_retained void *)timeNumber, &flags);
// 3、回调,处理 取得PPS和SPS
 static void VideoCompressonOutputCallback(void *VTref, void *VTFrameRef, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
// 4、完成编码,然后销毁session
VTCompressionSessionCompleteFrames(compressionSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(compressionSession);
CFRelease(compressionSession);
compressionSession = NULL;

四、推流

封装数据成 FLV,通过 RTMP 协议打包上传,从主播端到服务端即基本完成推流。

4-1、封装数据通常是封装成 FLV
  • FLV流媒体格式是一种新的视频格式,全称为FlashVideo。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等缺点。(What)

  • 格式: 源自(封包 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 类。
4-2、RTMP

从推流端到服务端,数据经过处理后,最常用的协议是RTMP(Real Time Messaging Protocol,实时消息传送协议)。

RTMP的传输延迟通常在1-3秒,符合手机直播对性能的要求,因此RTMP是手机直播中最常见的传输协议。

但是网络延迟和阻塞等问题的一直存在的,所以通过Quality of Servic一种网络机制将流数据推送到网络端,通过CDN分发是必要的。

另外,服务端还需要对数据流一定的处理,转码,使得数据流支持HLS,HTTP-FLV,RTMP等格式的拉流,支持一转多,适配不同网络、分辨率的终端。(当然这就是服务端要做的事情啦)

可以用 LFLiveKit 直接尝试,或者也可以看看 LMLiveStreaming,当然此处先用一个本地视频推送尝试一下。

4-3、本地模拟推流

此处是跟着 快速集成iOS基于RTMP的视频推流 来实现的,否则就连基本的展示都不能啦啊。此处也可以配合着Mac搭建nginx+rtmp服务器 来安装,安装好 nginx 之后,安装ffmpeg、下载 VLC 就可以直接开始啦

起初在用 ffmpeg 的时候,遇到下面那个错:

一个输入的错

后来发现是自己输入错了,还是要仔细:
视频文件地址:/Users/qiu/Desktop/kobe.mp4(自己的一个测试视频)
推流拉流地址:rtmp://localhost:1935/rtmplive/room

~ ffmpeg -re -i /Users/qiu/Desktop/kobe -vcodec libx264 -acodec aac -strict -2 -f flv rtmp://localhost:1935/rtmplive/room

那个-vcodec libx264 -acodec aac -strict -2 -f flv命令也不要写错了,ffmpeg 命令可参考 FFmpeg常用基本命令

kobeAndOneal.gif
4-4、手机直播 - VLC上 显示

为了更好的感受下,我们可以直接 用 LMLiveStreaming,然后打开 VLC 中 的 file -- Open Network, 直接输入代码中的 url:

代码中的这个地址

然后我们电脑端就可以显示啦

Live.gif

而目前有延迟2秒的情况,话说这是正常的。但如何优化呢?不知道,如有朋友有好建议欢迎告之。备注下:直播中累积延时的优化

总结

PS:上面传输只是推流端到服务端的模拟过程,然而传输一般是包括系统的多个部分,连接推流端,服务端,播放端等多个部分。而 iOS 这块播放端直接用 ijkplayer, 像上一个笔记——直播初探 , 就很快实现了拉流的过程,当然也是 ijkplayer 过于强大的原因咯。

下面宏观上了解下整个传输过程:

整体传输流程
PS: 另外其实好多第三方的集成也很好用,可参考

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

备注参考:

相关文章

  • Demo

    IOS视频直播 + 推流实现 采用开源框架ijkplayer 以及LFLiveKit实现推流,主要完善关注,分享和登录

  • iOS 直播 —— 推流

    推流,就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。 推流前的工作:采集,处理,编码压缩 推流中做...

  • iOS 直播 —— 推流

    *推流,就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。*推流前的工作:采集,处理,编码压缩*推流中...

  • iOS 直播 —— 推流

    推流,就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。 推流前的工作:采集,处理,编码压缩 推流中做...

  • iOS直播推流实现-推流

    将最近学习的直播推流技术做个笔记。iOS推流的主要流程如下: 视频音频采集[https://www.jianshu...

  • 直播---iOS直播推流/拉流

    工作空闲之余, 闲来无事, 弄了个直播的项目, 基本上主流的功能都有了... 直播推流(主播端直播/美颜/切换摄...

  • iOS直播推流实现-采集

    将最近学习的直播推流技术做个笔记。iOS推流的主要流程如下: 视频音频采集[https://www.jianshu...

  • iOS直播推流实现-音视频编码

    将最近学习的直播推流技术做个笔记。iOS推流的主要流程如下: 视频音频采集[https://www.jianshu...

  • iOS直播推流实现-滤镜

    将最近学习的直播推流技术做个笔记。iOS推流的主要流程如下: 视频音频采集[https://www.jianshu...

  • 七牛云3

    直播推流地址: rtmp://推流地址/直播空间名/推流请求返回的 title?key=publishKey 直播...

网友评论

  • FredYJH:问下,类似视频监控的做过吗?视频的数据源已经有了,通过UDP以组播方式传到手机端,然后手机端解析数据,请问下手机端有什么好的开源框架吗
  • 3f270e490cb7:大牛直播的推送+播放的SDK你看看
    https://github.com/daniulive/SmarterStreaming
    应该轻松秒掉你提到的这些SDK
    shengshenger:@曾经在奋斗 请问大牛直播是开源的么?能看到源代码么?
    3f270e490cb7:@天空中的球 实践是检验真理的唯一标准,你可以试试,呵呵
    天空中的球::sweat: ,秒掉?其他PC 端 安卓端的不知道,单看 iOS 处 Demo ,并不觉的能秒掉哦,其实这个还是看项目需求的。而且无论哪个 SDK 都有其优缺点,一概而论并不赞同。
  • neobuger:非常感谢 等会试试
  • TonyDuan:楼主请问下怎么使用LFLiveKit录制mp4文件呢,我只想用其录制文件?感谢
    TonyDuan:@天空中的球 谢谢,我后面找到了其暴露的api可以用来录制,如果只录制一部分视频还得修改下。
    天空中的球:@TonyDuan 只录制?没这样试过哎,感觉只录制的话没必要用它吧,如果硬是要用,可以看看其录制文件的方法
  • Wow_我了个去:手机端推,手机端看怎么测试嘞? :persevere:
  • 西門斗艮:这里有个很严重的 问题 ~ 就是我把 LFLivePreview添加到'A控制器'里 , 当销毁'A控制器'的时候 (dismissVC)会崩 , LFLiveKit里的 LFAudioCapture类 销毁的时候会崩溃 大家都没有遇见过这个情况么?? :flushed:
    467d24f779b5:@西門斗艮 :-) 感謝
    西門斗艮:@zswg 我解决了 : 解决方法就是不释放对象 直接把对象做成个单例工具. 就不用销毁掉了 ,占内纯也不大
    467d24f779b5:@西門斗艮 我也是遇見這狀況...請問兄台你解了嗎?@@
  • 65cb190bef16:你好!我想问下怎么推流实时的数据而不是本地的文件夹
    天空中的球:@杨荣me 具体可以看看 LMLiveStreaming 中的实现
  • af4e8a35bc89:以后可能要做,先Mark.
  • e13c761284d8::+1:,找了好久
  • 今天星期:怎么才能看到 封包的 数据
    天空中的球:@今天星期 可以自己截取打印的吧
  • a02efb1fb47a:请问一下你的samplebuffer怎么传进去美颜的.
    天空中的球:@杨某某D 我后来直接用 LFLiveKit尝试的。具体怎样传入的可以看(GPUImage) GPUImageVideoCamera 中的那个AVCaptureVideoDataOutputSampleBufferDelegate的处理(sample buffer的传入) 和(LFLiveKit) LFVideoCapture 中 reloadFilter 方法(美颜的添加),当然最主要的就是要去了解 GPUImage 。
  • 醉卧栏杆听雨声:很好很强大。
  • 管你爱不爱: LFLiveKit是免费吗?
    管你爱不爱:@天空中的球 LiveVideoCoreSDK这个库如何导入项目中去用?
    管你爱不爱:@天空中的球 系统最低支持多少?
    天空中的球:@管你爱不爱 啊,它目前就是一个开源库的啊,当然
  • b596c82fcbc1:请问一下图是用什么软件画的?
    天空中的球:@iOSer_ZChao 此处用的是iThoughtsX。

本文标题:iOS 直播 —— 推流

本文链接:https://www.haomeiwen.com/subject/amckettx.html