iOS-直播开发(开发从底层做起)
一直在忙, 也没写过几次博客! 但一直热衷于直播开发技术, 公司又不是直播方向的, 所以就年前忙里偷闲研究了一下直播开发, 然后翻阅了很多大神的技术博客等, 写了一个简单的Demo, 又根据网上大神们的技术博客搭建了简易的本地RTMP服务器! 由于时间问题, 没来记得来记录下来, 目前demo 只完成了直播音视频采集, 转码, RTMP协议推流, 和本地RTMP简易服务器 推流这一环节, 拉流还没来得及写, RTMP流的播放用的是VLC, 来实现视频流的播放的!
网上有各种大牛写的播客, 都很好的, 但我写这篇播客的目的就是, 想记录一下当时的思路, 还有分享出来, 让各位大神指点一下不足之处, 来完善这个小项目! 表达一下我对直播开发的热爱哈哈...如果有幸能给大家帮些忙, 我倍感荣幸!
该篇文章只是对直播需要了解的一些概念等的一下介绍, 然后还有如何进行视频采集! 近期有时间会逐步一点一点详细的写文章来介绍!
计划步骤下:
好, 废话不多说, 接下来我们直接开始!
iOS-直播开发(开发从底层做起)
2. iOS-直播开发(开发从底层做起) --- 音视频硬编码
3. iOS-直播开发(开发从底层做起) --- RTMP 协议推流
4. iOS-直播开发(开发从底层做起) --- nginx 直播本地服务器搭建
等等
来一张实战图
代码链接: Github: https://github.com/jessonliu/JFLivePlaye
技术部分------ ️
脑涂: ![ 直播思维导图.png ]
视频直播的大概流程就上脑涂上所画的, 还有一些没列出来, 比如, 聊天, 送礼, 踢出, 禁言, 等等一系列功能, 但本文只是针对视频直播的简单实现!
下边来说一下以下的几个点和使用到的类(后边会附上demo, 里边还有详细的备注)
> 1. 音视频采集
音视频采集, 网上也有很多大神些的技术博客, demo 等, 我这里边只针对iOS 原声的来介绍以下
利用AVFoundation框架, 进行音视频采集
AVCaptureSession // 音视频录制期间管理者
AVCaptureDevice // 设备管理者, (用来操作所闪光灯, 聚焦, 摄像头切换等)
AVCaptureDeviceInput // 音视频输入数据的管理对象
AVCaptureVideoDataOutput // 视频输出数据的管理者
AVCaptureAudioDataOutput // 音频输出数据的管理者
AVCaptureVideoPreviewLayer // 用来展示视频的图像
注意, 必须要设置音视频输出对象的代理方法, 然后在代理方法中获取sampleBuffer, 然后判断captureOutput是音频还是视频, 来进行音视频数据相应的编码
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
}
也可以利用GPUImageVideoCamera 来进行视频数据的采集获取, 可以利用GPUImage 进行美颜, 添加水印, 人脸识别等
2.流媒体
流媒体是指采用流式传输的方式在网上播放的媒体格式, 是边传边播的媒体,是多媒体的一种!
然后就是大家需要了解的几个关键词
帧:视频是由很多连续图像组成, 每一帧就代表一幅静止的图像
GOP:(Group of Pictures)画面组,一个GOP就是一组连续的画面,每个画面都是一帧,GOP就是很多帧的集合!
帧的分类:I帧、P帧、B帧
为了提高压缩比例,降低视频文件的大小,在针对连续动态图像编码时,一般会将连续若干幅图像编码为P、B、I三种帧类型
I帧:一组连续画面(GOP)的第一个帧, I帧采用帧内压缩法(也成关键帧压缩法), I帧的压缩不依靠与其他帧, 靠尽可能去除图像空间冗余信息来压缩的, 可以单独作为图像!
P帧:预测帧(也叫前向参考帧), P帧的压缩依赖于前一帧, 通过充分降低与图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像!
B帧:也叫双向预测帧, 当把一帧压缩成B帧时,它根据邻近的前几帧、本帧以及后几帧数据的不同点来压缩本帧,也即仅记录本帧与前后帧的差值。
帧率:就是在1秒钟时间里传输的图片的帧数,也可以理解为图形处理器每秒钟能够刷新几次,通常用FPS表示, 每秒钟帧数 (fps) 愈多,所显示的动作就会愈流畅!
码率: 也成为比特率, 是指每秒传送的比特(bit)数, 比特率越高,传送数据速度越快, 单位为 bps(Bit Per Second)。
3. 音视频的编解码
音视频编解码, 说白了就是对音视频数据进行压缩, 减少数据对空间的占用, 便于网络传输, 存储和使用!
目前直播常用的音视频编解码方式是h.264/AVC, AAC/MP3
硬软编解码的区别:
硬解码:由显卡核心GPU来对高清视频进行解码工作,CPU占用率很低,画质效果比软解码略差一点,需要对播放器进行设置。
优点:播放流畅、低功耗
缺点:受视频格式限制、功耗大、画质没有软解码好
软解码:由CPU负责解码进行播放
优点:不受视频格式限制、画质略好于硬解
缺点:会占用过高的资源、对于高清视频可能没有硬解码流畅(主要看CPU的能力)
苹果API有提供音视频硬编解码接口, 但只针对iOS8.0以上版本!
利用VideoToolbox 和AudioToolbox 这连个框架进行音视频的硬编码!
这里附上前辈们的关于VideoToolbox使用的简书, http://www.jianshu.com/p/6dfe49b5dab8
和AudioToolbox的技术简书http://www.jianshu.com/p/a671f5b17fc1
感兴趣的话可以研究一下!
4.流媒体数据封装
TS: 是流媒体封装格式的一种,流媒体封装的好处就是不需要加载索引再播放,大大降低了首次载入的延迟,两个TS片段可以无缝拼接,播放器能连续播放!
FLV: 也是一种流媒体的封装格式,但他形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,因此FLV格式成为了当今主流视频格式
5.RTMP推流
大家先看一张图, 常用的直播协议比较
这里只介绍一下RTMP协议, 如果还想了解更多的可在网上查找一下, 有很多关于流媒体协议的技术博客!
RTMP协议是基于TCP/IP 的协议簇;RTMP(Real Time Messaging Protocol)实时消息传送协议是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输 开发的开放协议
它有多种变种:
a, RTMP工作在TCP之上,默认使用端口1935;
b, RTMPE在RTMP的基础上增加了加密功能;
c, RTMPT封装在HTTP请求之上,可穿透防火墙;
d, RTMPS类似RTMPT,增加了TLS/SSL的安全功能;
它是一个互联网TCP/IP体系结构中应用层的协议。RTMP协议中基本的数据单元称为消息(Message)。当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为消息块(Chunk)。RTMP传输媒体数据的过程中,发送端首先把媒体数据封装成消息,然后把消息分割成消息块,最后将分割后的消息块通过TCP协议发送出去。接收端在通过TCP协议收到数据后,首先把消息块重新组合成消息,然后通过对消息进行解封装处理就可以恢复出媒体数据。
播放一个RTMP协议的流媒体需要经过以下几个步骤:握手,建立连接,建立流,播放。
demo中RTMP协议推流, 用的是librtmp-iOS框架! 参考https://my.oschina.net/jerikc/blog/501948
6. 播放器
IJKPlayer 是一个基于 ffplay 的轻量级 Android/iOS 视频播放器。API 易于集成;编译配置可裁剪,方便控制安装包大小;支持 硬件加速解码,更加省电。而DanmakuFlameMaster(开源弹幕框架) 架构清晰,简单易用,支持多种高效率绘制方式选择,支持多种自定义功能设置!
代码:
#import "JFLiveShowVC.h" 该类负责音视频采集及展示, 用于时间没问题, 没有吧音视频采集单独拿出来封装!
`
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 需要用到的线程
videoProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
audioProcessingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
_jfEncodeQueue_video = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_jfEncodeQueue_audio = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 检查权限和设备
[self checkDeviceAuth];
// 数据保存路径
self.documentDictionary = [(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)) objectAtIndex:0];
// 音频编码对象初始化
self.audioEncoder = [[AACEncoder alloc] init];
self.audioEncoder.delegate = self; // 设置代理
self.videoEncoder = [[JFVideoEncoder alloc] init]; // 视频编码对象初始化
self.videoEncoder.delegate = self; // 设置代理
_lock = dispatch_semaphore_create(1); // 当并行执行的处理更新数据时,会产生数据不一致的情况,使用Serial Dipatch queue 进行同步, 控制并发
}
// 检查是否授权摄像头的使用权限
- (void)checkDeviceAuth {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
case AVAuthorizationStatusAuthorized: // 已授权
NSLog(@"已授权");
[self initAVCaptureSession];
break;
case AVAuthorizationStatusNotDetermined: // 用户尚未进行允许或者拒绝,
{
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
NSLog(@"已授权");
[self initAVCaptureSession];
} else {
NSLog(@"用户拒绝授权摄像头的使用, 返回上一页, 请打开--> 设置 -- > 隐私 --> 通用等权限设置");
}
}];
}
break;
default:
{
NSLog(@"用户尚未授权摄像头的使用权");
}
break;
}
}
// 初始化 管理者
- (void)initAVCaptureSession {
self.session = [[AVCaptureSession alloc] init];
// 设置录像的分辨率
// 先判断是被是否支持要设置的分辨率
if ([self.session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
// 如果支持则设置
[self.session canSetSessionPreset:AVCaptureSessionPreset1280x720];
} else if ([self.session canSetSessionPreset:AVCaptureSessionPresetiFrame960x540]) {
[self.session canSetSessionPreset:AVCaptureSessionPresetiFrame960x540];
} else if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480]) {
[self.session canSetSessionPreset:AVCaptureSessionPreset640x480];
}
// 开始配置
[self.session beginConfiguration];
// 初始化视频管理
self.videoDevice = nil;
// 创建摄像头类型数组
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
// 便利管理抓捕道德所有支持制定类型的 设备集合
for (AVCaptureDevice *device in devices) {
if (device.position == AVCaptureDevicePositionFront) {
self.videoDevice = device;
}
}
// 视频
[self videoInputAndOutput];
// 音频
[self audioInputAndOutput];
// 录制的同时播放
[self initPreviewLayer];
// 提交配置
[self.session commitConfiguration];
}
// 视频输入输出
- (void)videoInputAndOutput {
NSError *error;
// 视频输入
// 初始化 根据输入设备来初始化输出对象
self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.videoDevice error:&error];
if (error) {
NSLog(@"-- 摄像头出错 -- %@", error);
return;
}
// 将输入对象添加到管理者 -- AVCaptureSession 中
// 先判断是否能搞添加输入对象
if ([self.session canAddInput:self.videoInput]) {
// 管理者能够添加 才可以添加
[self.session addInput:self.videoInput];
}
// 视频输出
// 初始化 输出对象
self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
// 是否允许卡顿时丢帧
self.videoOutput.alwaysDiscardsLateVideoFrames = NO;
if ([self supportsFastTextureUpload])
{
// 是否支持全频色彩编码 YUV 一种色彩编码方式, 即YCbCr, 现在视频一般采用该颜色空间, 可以分离亮度跟色彩, 在不影响清晰度的情况下来压缩视频
BOOL supportsFullYUVRange = NO;
// 获取输出对象 支持的像素格式
NSArray *supportedPixelFormats = self.videoOutput.availableVideoCVPixelFormatTypes;
for (NSNumber *currentPixelFormat in supportedPixelFormats)
{
if ([currentPixelFormat intValue] == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
{
supportsFullYUVRange = YES;
}
}
// 根据是否支持 来设置输出对象的视频像素压缩格式,
if (supportsFullYUVRange)
{
[self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
}
else
{
[self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
}
}
else
{
[self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
}
// 设置代理
[self.videoOutput setSampleBufferDelegate:self queue:videoProcessingQueue];
// 判断管理是否可以添加 输出对象
if ([self.session canAddOutput:self.videoOutput]) {
[self.session addOutput:self.videoOutput];
AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
// 设置视频的方向
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
// 视频稳定设置
if ([connection isVideoStabilizationSupported]) {
connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor;
}
}
// 音频输入输出
- (void)audioInputAndOutput {
NSError *jfError;
// 音频输入设备
self.audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
// 音频输入对象
self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.audioDevice error:&jfError];
if (jfError) {
NSLog(@"-- 录音设备出错 -- %@", jfError);
}
// 将输入对象添加到 管理者中
if ([self.session canAddInput:self.audioInput]) {
[self.session addInput:self.audioInput];
}
// 音频输出对象
self.audioOutput = [[AVCaptureAudioDataOutput alloc] init];
// 将输出对象添加到管理者中
if ([self.session canAddOutput:self.audioOutput]) {
[self.session addOutput:self.audioOutput];
}
// 设置代理
[self.audioOutput setSampleBufferDelegate:self queue:audioProcessingQueue];
}
// 播放同时进行播放
- (void)initPreviewLayer {
[self.view layoutIfNeeded];
// 初始化对象
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
self.previewLayer.frame = self.view.layer.bounds;
self.previewLayer.connection.videoOrientation = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo].videoOrientation;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.previewLayer.position = CGPointMake(self.liveView.frame.size.width*0.5,self.liveView.frame.size.height*0.5);
CALayer *layer = self.liveView.layer;
layer.masksToBounds = true;
[layer addSublayer:self.previewLayer];
}
#pragma mark 返回上一级
- (IBAction)backAction:(id)sender {
// 结束直播
[self.socket stop];
[self.session stopRunning];
[self.videoEncoder stopEncodeSession];
fclose(_h264File);
fclose(_aacFile);
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark 开始直播
- (IBAction)startLiveAction:(UIButton *)sender {
_h264File = fopen([[NSString stringWithFormat:@"%@/jf_encodeVideo.h264", self.documentDictionary] UTF8String], "wb");
_aacFile = fopen([[NSString stringWithFormat:@"%@/jf_encodeAudio.aac", self.documentDictionary] UTF8String], "wb");
// 初始化 直播流信息
JFLiveStreamInfo *streamInfo = [[JFLiveStreamInfo alloc] init];
streamInfo.url = @"rtmp://192.168.1.110:1935/rtmplive/room";
self.socket = [[JFRtmpSocket alloc] initWithStream:streamInfo];
self.socket.delegate = self;
[self.socket start];
// 开始直播
[self.session startRunning];
sender.hidden = YES;
}
#pragma mark -- AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
if (captureOutput == self.audioOutput) {
[self.audioEncoder encodeSampleBuffer:sampleBuffer timeStamp:self.currentTimestamp completionBlock:^(NSData *encodedData, NSError *error) {
fwrite(encodedData.bytes, 1, encodedData.length, _aacFile);
}];
} else {
[self.videoEncoder encodeWithSampleBuffer:sampleBuffer timeStamp:self.currentTimestamp completionBlock:^(NSData *data, NSInteger length) {
fwrite(data.bytes, 1, length, _h264File);
}];
}
}
- (void)dealloc {
if ([self.session isRunning]) {
[self.session stopRunning];
}
[self.videoOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()];
[self.audioOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()];
}
// 是否支持快速纹理更新
- (BOOL)supportsFastTextureUpload;
{
#if TARGET_IPHONE_SIMULATOR
return NO;
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
return (CVOpenGLESTextureCacheCreate != NULL);
#pragma clang diagnostic pop
#endif
}
// 保存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);
} else {
NSLog(@"_h264File null error, check if it open successed");
}
}
#pragma mark - JFRtmpSocketDelegate
- (void)jf_videoEncoder_call_back_videoFrame:(JFVideoFrame *)frame {
if (self.uploading) {
[self.socket sendFrame:frame];
}
}
#pragma mark - AACEncoderDelegate
- (void)jf_AACEncoder_call_back_audioFrame:(JFAudioFrame *)audionFrame {
if (self.uploading) {
[self.socket sendFrame:audionFrame];
}
}
#pragma mark -- JFRtmpSocketDelegate
- (void)socketStatus:(nullable JFRtmpSocket *)socket status:(JFLiveState)status {
switch (status) {
case JFLiveReady:
NSLog(@"准备");
break;
case JFLivePending:
NSLog(@"链接中");
break;
case JFLiveStart:
NSLog(@"已连接");
if (!self.uploading) {
self.timestamp = 0;
self.isFirstFrame = YES;
self.uploading = YES;
}
break;
case JFLiveStop:
NSLog(@"已断开");
break;
case JFLiveError:
NSLog(@"链接出错");
self.uploading = NO;
self.isFirstFrame = NO;
self.uploading = NO;
break;
default:
break;
}
}
// 获取当前时间戳
- (uint64_t)currentTimestamp{
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
uint64_t currentts = 0;
if(_isFirstFrame == true) {
_timestamp = NOW;
_isFirstFrame = false;
currentts = 0;
}
else {
currentts = NOW - _timestamp;
}
dispatch_semaphore_signal(_lock);
return currentts;
}
`
// 注: 必须控制好线程, 不然很容易出现卡死或闪退的情况!
关于音视频编解码的代码, 就不在这里展示了, 放在demo 中, 有需要的话话可以下载!
Github: https://github.com/jessonliu/JFLivePlaye
本地流媒体服务器的搭建这个给大家一个连接: http://www.jianshu.com/p/8ea016b2720e
以上就是直播开发中所要设计道德知识点和一些第三方框架, 如果全是用第三方的话, 就会省事很多, 用起来也很方便, 但我个人比较喜欢刨根问题, 想了解原理!
如果写的不妥当或不足的地方, 希望大神指正和补充! 由于前段时间比较忙, 拉流, 解码和播放还没来得及写, 我在直播的路上还有很长的路要走, 还需要不断地学习提高, 了解更底层的东西, 才能更好的掌握直播的整个流程技术, 后期写完会更新一个完整的Demo!