最近学习的总结!
首先来介绍下简单的推流.
iOS 直播 —— 推流
推流,就是将采集到的音频,视频数据通过流媒体协议发送到流媒体服务器。
• 推流前的工作:采集,处理,编码压缩
• 推流中做的工作: 封装,上传
推流前的工作:
采集到的音视频数据通过流媒体协议发送到流媒体服务器上:
其实有一个库 LFLiveKit 已经实现了 后台录制、美颜功能、支持h264、AAC硬编码,动态改变速率,RTMP传输等,我们真正开发的时候直接使用就很方便啦。
• LiveVideoCoreSDK : 实现了美颜直播和滤镜功能,我们只要填写RTMP服务地址,直接就可以进行推流啦。
• PLCameraStreamingKit: 也是一个不错的 RTMP 直播推流 SDK。
但还是推荐用 LFLiveKit。
一、采集视频
AVCaptureVideoDataOutput *videoOutput; //视频采集
AVCaptureAudioDataOutput *audioOutput; //音频采集
二、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;
但是视频中是怎样进行美容处理呢?怎样将其转换的呢?平常我们这样直接使用:
GPUImageBeautifyFilter *beautifyFilter = [[GPUImageBeautifyFilter alloc] init];
[self.videoCamera addTarget:beautifyFilter];
[beautifyFilter addTarget:self.gpuImageView];
此处用到了 GPUImageVideoCamera,可以大致了解下 GPUImage详细解析(三)- 实时美颜滤镜:
三、视频、音频压缩编码
而编码是用 硬编码呢 还是软编码呢? 相同码率,软编图像质量更清晰,但是耗电更高,而且会导致CPU过热烫到摄像头。不过硬编码会涉及到其他平台的解码,有很多坑。综合来说,iOS 端硬件兼容性较好,iOS 8.0占有率也已经很高了,可以直接采用硬编。
硬编码:下面几个DEMO 可以对比下,当然看 LFLiveKit 更直接。
• VideoToolboxPlus
• iOSHardwareDecoder
• -VideoToolboxDemo
• iOS-h264Hw-Toolbox
我直接使用了 LFLiveKit ,里面已经封装的很好啦,此处对 Audiotoolbox && VideoToolbox 简单了解下:
1.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音频格式之间的转换。
2.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;
四、推流
4-1、封装数据成 FLV,通过 RTMP 协议打包上传,从主播端到服务端即基本完成推流。
FLV流媒体格式是一种新的视频格式,全称为FlashVideo。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等缺点。
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常用基本命令。
4-4、手机直播 - VLC上 显示
为了更好的感受下,我们可以直接 用 LMLiveStreaming,然后打开 VLC 中 的 file -- Open Network, 直接输入代码中的 url:
另外其实好多第三方的集成也很好用,可参考
• 七牛云
• 腾讯的直播 LVB
• 网易云信 SDK
• 趣拍云
备注参考:
• LiveVideoCoreSDK
• LFLiveKit
• GPUImage
• LMLiveStreaming
• PLCameraStreamingKit
• iOS手机直播Demo技术简介
• iOS视频开发经验
• iOS 上的相机捕捉
• CMSampleBufferRef 与 UIImage 的转换
• GPUImage详细解析(三)- 实时美颜滤镜
• iOS8系统H264视频硬件编解码说明
• 利用FFmpeg+x264将iOS摄像头实时视频流编码为h264文件
• 使用VideoToolbox硬编码H.264
• 使用iOS自带AAC编码器
• 如何搭建一个完整的视频直播系统?
• 直播中累积延时的优化
• 使用VLC做流媒体服务器(直播形式)
配置完成后,
一. 下载ijkplayer
ijkplayer下载地址:https://github.com/Bilibili/ijkplayer
下载完成后解压.
二. 编译 ijkplayer
说是编译 ijkplayer, 其实是编译 ffmpeg, 在这里我们已经下载好了ijkplayer, 所以 github 上README.md中的Build iOS那一步中有一些步骤是不需要的.
下面开始一步一步编译:
1.打开终端, cd 到jkplayer-master文件夹中, 也就是下载完解压后的文件夹, 如下图:
2.执行命令行./init-ios.sh, 这一步是去下载 ffmpeg 的, 时间会久一点, 耐心等一下.
3.在第2步中下载完成后, 执行cd ios, 也就是进入到 ios目录中, 如下图:
4.进入 ios 文件夹后, 在终端依次执行./compile-ffmpeg.sh clean和./compile-ffmpeg.sh all命令, 编译 ffmpeg, 也就是README.md中这两步, 如下图:
编译时间较久, 耐心等待一下.
三. 打包IJKMediaFramework.framework框架
集成 ijkplayer 有两种方法:
一种方法是按照IJKMediaDemo工程中那样, 直接导入工程IJKMediaPlayer.xcodeproj, 在这里不做介绍, 如下图:
第二种集成方法是把 ijkplayer 打包成framework导入工程中使用. 下面开始介绍如何打包IJKMediaFramework.framework, 按下面步骤开始一步一步做:
1. 首先打开工程IJKMediaPlayer.xcodeproj, 位置如下图:
打开后是这样的, 如下图:
2.工程打开后设置工程的 scheme, 具体步骤如下图:
3.设置好 scheme 后, 分别选择真机和模拟器进行编译, 编译完成后, 进入 Finder, 如下图:
进入 Finder 后, 可以看到有真机和模拟器两个版本的编译结果, 如下图:
下面开始合并真机和模拟器版本的 framework, 注意不要合并错了, 合并的是这个文件, 如下图:
打开终端, 进行合并, 命令行具体格式为:
lipo -create "真机版本路径" "模拟器版本路径" -output "合并后的文件路径"
合并后如下图:
下面很重要, 需要用合并后的IJKMediaFramework把原来的IJKMediaFramework替换掉, 如下图, 希望你能看懂:
上图中的1、2两步完成后, 绿色框住的那个IJKMediaFramework.framework文件就是我们需要的框架了, 可以复制出来, 稍后我们需要导入工程使用.
四. iOS工程中集成ijkplayer
新建工程, 导入合并后的IJKMediaFramework.framework以及相关依赖框架以及相关依赖框架,如下图:
导入框架后, 在ViewController.m进行测试, 首先导入IJKMediaFramework.h头文件, 编译看有没有错, 如果没有错说明集成成功.
代码见下
IJKFFMoviePlayerController 注册通知中心
-
(void)loadStateDidChange:(NSNotification*)notification {
IJKMPMovieLoadState loadState = _player.loadState;////状态为缓冲几乎完成,可以连续播放
if ((loadState & IJKMPMovieLoadStatePlaythroughOK) != 0) {
NSLog(@"LoadStateDidChange: IJKMovieLoadStatePlayThroughOK: %d\n",(int)loadState);
}
////缓冲中
else if ((loadState & IJKMPMovieLoadStateStalled) != 0) {
/*
这里主播可能已经结束直播了。我们需要请求服务器查看主播是否已经结束直播。
方法:
1、从服务器获取主播是否已经关闭直播。
优点:能够正确的获取主播端是否正在直播。
缺点:主播端异常crash的情况下是没有办法通知服务器该直播关闭的。
2、用户http请求该地址,若请求成功表示直播未结束,否则结束
优点:能够真实的获取主播端是否有推流数据
缺点:如果主播端丢包率太低,但是能够恢复的情况下,数据请求同样是失败的。
*/
NSLog(@"loadStateDidChange: IJKMPMovieLoadStateStalled: %d\n", (int)loadState);} else {
NSLog(@"loadStateDidChange: ???: %d\n", (int)loadState);
}
} -
(void)moviePlayBackFinish:(NSNotification*)notification {
int reason =[[[notification userInfo] valueForKey:IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue];
switch (reason) {
case IJKMPMovieFinishReasonPlaybackEnded:
NSLog(@"playbackStateDidChange: IJKMPMovieFinishReasonPlaybackEnded: %d\n", reason);
break;case IJKMPMovieFinishReasonUserExited: NSLog(@"playbackStateDidChange: IJKMPMovieFinishReasonUserExited: %d\n", reason); break; case IJKMPMovieFinishReasonPlaybackError: NSLog(@"playbackStateDidChange: IJKMPMovieFinishReasonPlaybackError: %d\n", reason); break; default: NSLog(@"playbackPlayBackDidFinish: ???: %d\n", reason); break;
}
} (void)mediaIsPreparedToPlayDidChange:(NSNotification*)notification {
NSLog(@"mediaIsPrepareToPlayDidChange\n");
}(void)moviePlayBackStateDidChange:(NSNotification*)notification {
if 0
// NSLog(@"%@",notification);
// IJKMPMoviePlaybackStateStopped, 停止
// IJKMPMoviePlaybackStatePlaying, 正在播放
// IJKMPMoviePlaybackStatePaused, 暂停
// IJKMPMoviePlaybackStateInterrupted, 打断
// IJKMPMoviePlaybackStateSeekingForward, 快进
// IJKMPMoviePlaybackStateSeekingBackward 快退
switch (self.player.playbackState) {
case IJKMPMoviePlaybackStateStopped:
NSLog(@"停止");
break;
case IJKMPMoviePlaybackStatePlaying:
NSLog(@"正在播放");
break;
case IJKMPMoviePlaybackStatePaused:
NSLog(@"暂停");
break;
case IJKMPMoviePlaybackStateInterrupted:
NSLog(@"打断");
break;
case IJKMPMoviePlaybackStateSeekingForward:
NSLog(@"快进");
break;
case IJKMPMoviePlaybackStateSeekingBackward:
NSLog(@"快退");
break;
default:
break;
}
endif
switch (_player.playbackState) {
case IJKMPMoviePlaybackStateStopped:
NSLog(@"IJKMPMoviePlayBackStateDidChange %d: stoped", (int)_player.playbackState);
break;
case IJKMPMoviePlaybackStatePlaying:
NSLog(@"IJKMPMoviePlayBackStateDidChange %d: playing", (int)_player.playbackState);
break;
case IJKMPMoviePlaybackStatePaused:
NSLog(@"IJKMPMoviePlayBackStateDidChange %d: paused", (int)_player.playbackState);
break;
case IJKMPMoviePlaybackStateInterrupted:
NSLog(@"IJKMPMoviePlayBackStateDidChange %d: interrupted", (int)_player.playbackState);
break;
case IJKMPMoviePlaybackStateSeekingForward:
case IJKMPMoviePlaybackStateSeekingBackward: {
NSLog(@"IJKMPMoviePlayBackStateDidChange %d: seeking", (int)_player.playbackState);
break;
}
default: {
NSLog(@"IJKMPMoviePlayBackStateDidChange %d: unknown", (int)_player.playbackState);
break;
}
}
}
pragma Install Notifiacation
-
(void)installMovieNotificationObservers {
/*
IJKFFMoviePlayerController 支持的通知有很多,常见的有:IJKMPMoviePlayerLoadStateDidChangeNotification(加载状态改变通知)
IJKMPMoviePlayerPlaybackDidFinishNotification(播放结束通知)
IJKMPMoviePlayerPlaybackStateDidChangeNotification(播放状态改变通知)
*///监听加载状态改变通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(loadStateDidChange:)
name:IJKMPMoviePlayerLoadStateDidChangeNotification
object:_player];
//播放结束通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayBackFinish:)
name:IJKMPMoviePlayerPlaybackDidFinishNotification
object:_player];[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(mediaIsPreparedToPlayDidChange:)
name:IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
object:_player];
//播放状态改变通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayBackStateDidChange:)
name:IJKMPMoviePlayerPlaybackStateDidChangeNotification
object:_player];
}
- (void)removeMovieNotificationObservers {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:IJKMPMoviePlayerLoadStateDidChangeNotification
object:_player];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:IJKMPMoviePlayerPlaybackDidFinishNotification
object:_player];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
object:_player];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:IJKMPMoviePlayerPlaybackStateDidChangeNotification
object:_player];
}
注意:在iOS8以上建议开启硬件码:
//videotoolbox 配置(硬件解码)
[options setPlayerOptionIntValue:1 forKey:@"videotoolbox"];