视频
视频实质:
纯粹的视频(不包括音频)实质上就是一组帧图片,经过视频编码成为视频(video)文件再把音频(audio)文件有些还有字幕文件组装在一起成为我们看到的视频(movie)文件。1秒内出现的图片数就是帧率,图片间隔越小画面就越流畅,所以帧率越高效果就越好,需要的存储空间也就越多。
- 协议层(Protocol Layer):该层处理的数据为符合特定流媒体协议规范的数据,例如http,rtmp,file等;
- 封装层(Format Layer):该层处理的数据为符合特定封装格式规范的数据,例如mkv,mp4,flv,mpegts,avi等;
- 编码层(Codec Layer):该层处理的数据为符合特定编码标准规范的数据,例如h264,h265,mpeg2,mpeg4等;
- 像素层(Pixel Layer):该层处理的数据为符合特定像素格式规范的数据,例如yuv420p,yuv422p,yuv444p,rgb24等;
视频编码:
因为不进行编码的视频数据量非常大,会造成存储和传输上的困难,所以视频文件都需要在录制完成后进行编码。视频编码主要从两个维度压缩数据。
1、单张图像某一区域相邻像素相似,比如一片红色只记录红色色值和区域,不用记录这个区域的每一个像素点。
2、相邻图像之间内容相似,因为相邻两帧要制造连续的效果,所以两帧之间的内容一般非常接近。目前主流的视频编码技术都是用图像编码方法对第一帧进行编码,然后用某种方式描述接下来的帧相对于附近的帧有什么区别。
视频格式:
MP4、MOV、AVI、RMVB这些播放格式其实都是封装格式,除了RMVB比较特殊外,其他格式内封装的视频编码格式都是H264,H264以高压缩率闻名于世,压缩效率比MEPG-2提升一倍多,但是世上没有两全其美的事,H264的解码难度提高了3倍多。
视频码率:
视频文件的大小除以是视频的时长定义为码率。
码率和分辨率跟视频质量的关系:
码率可以理解为取样率,单位时间内取样率越大,精度就越高,同时体积也越大。
当视频没有经过编码时,如果分辨率越高,那么视频图像的细节越清晰。
但如果视频经过编码,被限制在一定码率内,编码器就必须舍弃掉一部分细节。
所以分辨率和码率都同清晰度有关。
软解码和硬解码:
对H264的视频解码给CPU造成了很大负担,所以手机工程师把这部分工作交给了更善于进行处理简单工作但是数据量较大的GPU。
GPU解码就是所谓的硬解码
CPU解码就是软解码。
iOS提供的播放器类使用的是硬解码,所以视频播放对CPU不会有很大的压力,但是支持的播放格式比较单一,一般就是MP4、MOV、M4V这几个。
视频压缩原理
1、压缩的方向
数字化后的视频信号具有很大的数据冗余,压缩的本质就是去掉这些冗余。
- 空间冗余,视频的背景和整体颜色相近并且平稳变化,可以利用帧内编码进行压缩;(无损)
- 时间冗余,两个视频帧之间具有强相关性,利用运动估计和运动补偿进行帧间压缩;(无损)
- 结构冗余,图像内部存在相似性,通过这种关系可以进行分形编码;
- 编码冗余,出现概率大的颜色编码长度短,概率小的颜色编码长度长;(可变长度编码)
- 视觉冗余,利用人眼对亮度和色度的敏感度不同,在编码时进行数据压缩;(有损压缩)
2、变换
空间域描述的图像相关性不太明显,需要变换到频率域。常用的正交变换有离散傅里叶变换,离散余弦变换等等。数字视频压缩过程中应用广泛的是离散余弦变换。
- 空间域(spatial domain),又称图像空间(image space)。由图像像元组成的空间。在图像空间中以长度(距离)为自变量直接对像元值进行处理称为空间域处理。
- 频率域(spatial frequency domain),以空间频率为自变量描述图像的特征,可以将一幅图像像元值在空间上的变化分解为具有不同振幅、空间频率和相位的简振函数的线性叠加,图像中各种空间频率成分的组成和分布称为空间频谱。这种对图像的空间频率特征进行分解、处理和分析称为频率域处理或波数域处理。
3.H.264格式
I帧是关键帧,解码时只需要本帧数据;
P帧是参考帧,表示这一帧与前一个关键帧(或P帧)的差别;
B帧是双向参考帧,表示本帧与前后帧的差别;(B帧压缩率高,解码复杂,直播中较少用)
IDR帧是第一个I帧,为的是和其他I帧区别开,方便控制编码和解码;
IDR会导致DPB(DecodedPictureBuffer 参考帧列表)清空,而I不会。
GOP(Group Of Picture)是图像组,是一组连续的画面;(直播实现秒开,关键就是CDN节点缓存GOP,编码器拿到第一个GOP后马上解码播放)
帧内压缩:当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息;(帧内压缩一般达不到很高的压缩,跟编码jpeg差不多)
帧间压缩:利用相邻帧的相关性提高压缩量,减少冗余;(运动补偿和运动估计是常用的技术)
音频压缩原理
数字音频压缩编码在保证信号在听觉方面不产生失真的前提下,对音频数据信号进行尽可能大的压缩。数字音频压缩编码采取去除声音信号中冗余成分的方法来实现。所谓冗余成分指的是音频中不能被人耳感知到的信号,它们对确定声音的音色,音调等信息没有任何的帮助。
冗余信号包含人耳听觉范围外的音频信号以及被掩蔽掉的音频信号等。
人耳听觉的掩蔽效应:当一个强音信号与一个弱音信号同时存在时,弱音信号将被强音信号所掩蔽而听不见,这样弱音信号就可以视为冗余信号而不用传送。
频谱掩蔽效应
一个频率的声音能量小于某个阈值之后,人耳就会听不到,这个阈值称为最小可闻阈。当有另外能量较大的声音出现的时候,该声音频率附近的阈值会提高很多,即所谓的掩蔽效应。
时域掩蔽效应
当强音信号和弱音信号同时出现时,还存在时域掩蔽效应。即两者发生时间很接近的时候,也会发生掩蔽效应。时域掩蔽过程曲线如图所示,分为前掩蔽、同时掩蔽和后掩蔽三部分。
- PCM是编码格式,经过话筒录音后直接得到的未经压缩的数据流;
数据大小=采样频率采样位数声道*秒数/8。
采样定理表明采样频率必须大于被采样信号带宽的两倍,另外一种等同的说法是奈奎斯特频率必须大于被采样信号的带宽。如果信号的带宽是 100Hz,那么为了避免混叠现象采样频率必须大于200Hz。换句话说就是采样频率必须至少是信号中最大频率分量频率的两倍,否则就不能从信号采样中恢复原始信号。
AAC是编解码标准,基于MPEG-2的音频编码技术;
PCM采样率是44100Hz,那么AAC码率可设置64000bps;
如果是16K,可设置为32000bps;MP3是封装格式,所存放数据使用的编码方式称为MPEG1 Layer-3 ;
AMR是封装格式,专用于有效地压缩语音频率;
WAV是封装格式,里面可以存放多种编码格式的数据,一般是PCM数据;
iOS音频
HTTP Live Streaming
HLS简介
HTTP Live Streaming(缩写是 HLS)是一个由苹果公司提出的基于HTTP的流媒体网络传输协议。它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。
当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。支持的视频流编码为H.264。我们在视频网站上看到的M3U8后缀的播放链接就是使用HLS协议的视频。
HLS优点
1、看完一段缓存一段,防止只看一段视频但是把整个视频文件都缓存下来的用户,减少服务器压力和节省流量。
2、根据用户网速切换不同的码率,兼顾流程性和清晰度。
HLS支持情况
iOS 3.0及之后的版本
Android 3.0及之后的版本
HTML5。
终端播放格式的选取
Android由于3.0之后才支持HLS,所以Android2.3只能用MP4。
Android3.0及之后支持HLS。可以用m3u8、mp4格式
iOS支持HLS,但不支持flash。可以用m3u8、mp4格式
支持HTML5的浏览器 可以用m3u8。
不支持HTML5的浏览器只能用flash播放swf。
由于以上原因,目前无法实现一个播放地址在所有的平台都通用。
iOS视频播放:
iOS提供MPMoviePlayerController类进行播放,支持流媒体和文件播放。视频内容会渲染到他的View上,可以放在你想放的任何地方,用起来比较方便。这个类设计上不合理的是视频播放状态和视频加载状态都是通过Notification通知的,而不是通过block或者delegate。
iOS视频录制:
同拍照一样视频录制功能有两种实现方式
1、UIImagePickerViewController
2、AVFoundation。
这里只讨论AVFoundation框架,这个框架是苹果提供的底层多媒体框架,用于音视频采集、音视频解码、视频编辑等,多媒体基本上都依赖AVFoundation框架。
视频录制和拍照需要做的工作差不多,主要有以下5步:
1、创建会话AVCaptureSession,用于控制input到output的流向。
2、获取设备AVCaptureDevice,摄像头用于视频采集,话筒用于音频采集。
3、创建输入设备AVCaptureDeviceInput,将设备绑定到input口中,并添加到session上
4、创建输出AVCaptureOutput,可以输出到文件和屏幕上。 AVCaptureMovieFileOutput 输出一个电影文件 AVCaptureVideoDataOutput 输出处理视频帧,用于显示正在录制的视频 AVCaptureAudioDataOutput 输出音频数据
5、音视频合成到一个文件中
iOS对视频实时处理:
如果需要对视频进行实时处理(当然需要否则看不到正在录制的内容),则需要直接对相机缓冲区(camera buffer)中的视频流进行处理。
1、定义一个视频数据输出(AVCaptureVideoDataOutput), 并将其添加到session上。
2、设置接受的controller作为视频数据输出缓冲区(sample buffer)的代理。
3、实现代理方法
-(void)captureOutput:(AVCaptureOutput )captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection )connection
当数据缓冲区(data buffer)一有数据时,AVFoundation就调用该方法。在该代理方法中,我们可以获取视频帧、处理视频帧、显示视频帧。实时滤镜就是在这里进行处理的。在这个方法中将缓冲区中的视频数据(就是帧图片)输出到要显示的layer上。
视频编码与解码
移动端编码无外乎两种:
软编码,利用CPU来对视频做编码和解码的,但效率不高.(大部分移动端通过FFMpeg来实现软编码。)
硬编码,利用GPU或者专用处理器来对视频做编码和解码。iOS 8.0之后开放的Video ToolBox框架就是来实现硬件的编码和解码的.
视频数据硬编码
视频压缩编码通过Video Toolbox框架下的VTCompressionSession完成。Video ToolBox 是一个基于 CoreMedia,CoreVideo,CoreFoundation 框架的 C 语言 API,来处理硬件的编码和解码,在iOS 8.0后,苹果将该框架引入iOS系统,苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能。
由于Video ToolBox 提供的是 C 的API,所以编码时会用到一些swift的指针操作。
定义创建配置CompressionSession:
private var session:VTCompressionSessionRef?
// 将
VTCompressionSessionCreate(
kCFAllocatorDefault,
480, // encode height
640,// encode width
kCMVideoCodecType_H264, //encode type,h.264
nil,
attributes,
nil,
callback,
unsafeBitCast(self, UnsafeMutablePointer<Void>.self),
&session)
// 设置编码的属性
VTSessionSetProperties(session!, properties)
VTCompressionSessionPrepareToEncodeFrames(session!)
VTCompressionSessionCreate 的encode height, encode width设置编码输出的h.264文件的宽高,单位px(像素)。
编码类型主要用的就是kCMVideoCodecType_H264(h.264),其他的类型还有h.263等,好像最新的 iphone 6s 已经支持 h.265的编码,但还没有放出接口。
硬解码和硬编码使用方法
1.创建AVAssetReader,将H264码流转换成解码前的CMSampleBuffer。
(1)提取sps和pps生成format descriptio
(2)提取视频图像数据生成CMBlockBuffer。
(3)根据需要,生成CMTime信息。
2.创建AVAssetWriter,设置输出及压缩属性。
iOS视频直播~推流、拉流原理
HLS协议:
简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。
每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 cdn 上。
.m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件:
#EXTM3U m3u文件头
#EXT-X-MEDIA-SEQUENCE 第一个TS分片的序列号
#EXT-X-TARGETDURATION 每个分片TS的最大的时长
#EXT-X-ALLOW-CACHE 是否允许cache
#EXT-X-ENDLIST m3u8文件结束符
#EXTINF 指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效mystream-12.ts
HLS 的请求流程是:
1.http 请求 m3u8 的 url。
2.服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。
3.客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。
关于HLS延迟原因:
hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的ts时长时10s,所以这样就会大改有30s的延迟;
数据采集原理:
下面将利用 ios 上的摄像头,进行音视频的数据采集,主要分为以下几个步骤:
- 音视频的采集,ios 中,利用 AVCaptureSession和AVCaptureDevice 可以采集到原始的音视频数据流。
- 对视频进行 H264 编码,对音频进行 AAC 编码,在 ios 中分别有已经封装好的编码库来实现对音视频的编码。
- 对编码后的音、视频数据进行组装封包;
- 建立 RTMP 连接并上推到服务端。
ps:由于编码库大多使用 c 语言编写,需要自己使用时编译,对于 ios,可以使用已经编译好的编码库。
RTMP介绍:
Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议。和 HLS 一样都可以应用于视频直播,区别是 RTMP 基于 flash 无法在 ios 的浏览器里播放,但是实时性比 HLS 要好。所以一般使用这种协议来上传视频流,也就是视频流推送到服务器。
对比:
- RTMP 首先就是延迟低,基于TCP的长链接,对于数据处理及时,收到即刻发送,推荐使用场景:即时互动。
- HLS 延迟高,短链接,原理是集合了一段时间的视频数据,切割ts片,逐个下载播放。优点是跨平台。
推流:
推流,就是将我们已经编码好的音视频数据发往视频流服务器中,一般常用的是使用 rtmp 推流,可以使用第三方库 librtmp-iOS 进行推流,librtmp 封装了一些核心的 api 供使用者调用,如果觉得麻烦,可以使用现成的 ios 视频推流sdk,也是基于 rtmp 的。具体说下:
也就是对编码好的音视频数据推到服务器上,这里我们又分为两类推流模式:手机端推流,服务器本地推流。就拿我上一家公司的电视直播来说,视频源是来自电视台的,需要通过ffmpeg命令来进行个推流,那么推流协议的话这里又分为了:HLS推流和rtmp推流,这里的取舍主要涉及到了是否需要及其实时的直播问题,也就是延迟20 30s是否接受,当然电视直播并不是主播实时互动,所以不需要使用实时流媒体协议的rtmp,所以通过ffmpeg -loglevel 这么一个命令将电视台给的视频进行各像nginx服务器的一个推流,那么我们就可以通过nginx服务器给的链接,配合我的第三方的直播框架,就可以实现个直播,这个是服务器本地的HLS协议的一个推流。当然如果我们要做一个没有延迟的比如实现各主播互动的一个直播,那么就是iOS客户端用rtmp协议的一个往nginx服务器的一个推流了。在iOS设备上进行各推流的话,是通过AVCaptureSession这么一个捕捉会话,指定两个AVCaptureDevice 也就是iOS的摄像头和麦克风,获取个原始视频和音频,然后需要进行个H.264的视频编码和AAC的音频编码,再将编码后的数据整合成一个音视频包,通过rmtp推送到nginx服务器。这里这些步骤,我们可以通过各第三方集成好的推流工具进行推流,这个工具有librtmp,和腾讯的GDLiveStreaming进行个推流。
直播
流媒体(直播需要用到流媒体)
- 流媒体开发:网络层(socket或st)负责传输,协议层(rtmp或hls)负责网络打包,封装层(flv、ts)负责编解码数据的封装,编码层(h.264和aac)负责图像,音频压缩。
帧:每帧代表一幅静止的图像 - GOP:(Group of Pictures)画面组,一个GOP就是一组连续的画面,每个画面都是一帧,一个GOP就是很多帧的集合
直播的数据,其实是一组图片,包括I帧、P帧、B帧,当用户第一次观看的时候,会寻找I帧,而播放器会到服务器寻找到最近的I帧反馈给用户。因此,GOP Cache增加了端到端延迟,因为它必须要拿到最近的I帧
GOP Cache的长度越长,画面质量越好 - 码率:图片进行压缩后每秒显示的数据量。
- 帧率:每秒显示的图片数。影响画面流畅度,与画面流畅度成正比:帧率越大,画面越流畅;帧率越小,画面越有跳动感。
由于人类眼睛的特殊生理结构,如果所看画面之帧率高于16的时候,就会认为是连贯的,此现象称之为视觉暂留。并且当帧速达到一定数值后,再增长的话,人眼也不容易察觉到有明显的流畅度提升了。 - 分辨率:(矩形)图片的长度和宽度,即图片的尺寸
- 压缩前的每秒数据量:帧率X分辨率(单位应该是若干个字节)
- 压缩比:压缩前的每秒数据量/码率 (对于同一个视频源并采用同一种视频编码算法,则:压缩比越高,画面质量越差。)
- 视频文件格式:文件的后缀,比如.wmv,.mov,.mp4,.mp3,.avi,
主要用处,根据文件格式,系统会自动判断用什么软件打开,
注意: 随意修改文件格式,对文件的本身不会造成太大的影响,比如把avi改成mp4,文件还是avi. - 视频封装格式:一种储存视频信息的容器,流式封装可以有TS、FLV等,索引式的封装有MP4,MOV,AVI等,
主要作用:一个视频文件往往会包含图像和音频,还有一些配置信息(如图像和音频的关联,如何解码它们等):这些内容需要按照一定的规则组织、封装起来.
注意:会发现封装格式跟文件格式一样,因为一般视频文件格式的后缀名即采用相应的视频封装格式的名称,所以视频文件格式就是视频封装格式。 - 视频封装格式和视频压缩编码标准:就好像项目工程和编程语言,封装格式就是一个项目的工程,视频编码方式就是编程语言,一个项目工程可以用不同语言开发。
视频编码框架
- FFmpeg:是一个跨平台的开源视频框架,能实现如视频编码,解码,转码,串流,播放等丰富的功能。其支持的视频格式以及播放协议非常丰富,几乎包含了所有音视频编解码、封装格式以及播放协议。
-Libswresample:可以对音频进行重采样,rematrixing 以及转换采样格式等操 作。
-Libavcodec:提供了一个通用的编解码框架,包含了许多视频,音频,字幕流 等编码/解码器。
-Libavformat:用于对视频进行封装/解封装。
-Libavutil:包含一些共用的函数,如随机数生成,数据结构,数学运算等。
-Libpostproc:用于进行视频的一些后期处理。
-Libswscale:用于视频图像缩放,颜色空间转换等。
-Libavfilter:提供滤镜功能。 - X264:把视频原数据YUV编码压缩成H.264格式
- VideoToolbox:苹果自带的视频硬解码和硬编码API,但是在iOS8之后才开放。
- AudioToolbox:苹果自带的音频硬解码和硬编码API
流媒体服务器
- SRS:一款国人开发的优秀开源流媒体服务器系统
- BMS:也是一款流媒体服务器系统,但不开源,是SRS的商业版,比SRS功能更多
- nginx:免费开源web服务器,常用来配置流媒体服务器。
数据分发
- CDN:(Content Delivery Network),即内容分发网络,将网站的内容发布到最接近用户的网络”边缘”,使用户可以就近取得所需的内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度.
- CDN:代理服务器,相当于一个中介。
- CDN工作原理:比如请求流媒体数据
1.上传流媒体数据到服务器(源站)
2.源站存储流媒体数据
3.客户端播放流媒体,向CDN请求编码后的流媒体数据
4.CDN的服务器响应请求,若节点上没有该流媒体数据存在,则向源站继续请求流媒体数据;若节点上已经缓存了该视频文件,则跳到第6步。
5.源站响应CDN的请求,将流媒体分发到相应的CDN节点上
6.CDN将流媒体数据发送到客户端 - 回源:当有用户访问某一个URL的时候,如果被解析到的那个CDN节点没有缓存响应的内容,或者是缓存已经到期,就会回源站去获取搜索。如果没有人访问,那么CDN节点不会主动去源站拿.
- 带宽:在固定的时间可传输的数据总量,
比如64位、800MHz的前端总线,它的数据传输率就等于64bit×800MHz÷8(Byte)=6.4GB/s - 负载均衡: 由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,都可以单独对外提供服务而无须其他服务器的辅助.
通过某种负载分担技术,将外部发送来的请求均匀分配到对称结构中的某一台服务器上,而接收到请求的服务器独立地回应客户的请求。
均衡负载能够平均分配客户请求到服务器列阵,籍此提供快速获取重要数据,解决大量并发访问服务问题。
这种群集技术可以用最少的投资获得接近于大型主机的性能。 - QoS(带宽管理):限制每一个组群的带宽,让有限的带宽发挥最大的效用
直播协议
- HLS:由Apple公司定义的用于实时流传输的协议,HLS基于HTTP协议实现,传输内容包括两部分,一是M3U8描述文件,二是TS媒体文件。可实现流媒体的直播和点播,主要应用在iOS系统
HLS是以点播的技术方式来实现直播
HLS是自适应码率流播,客户端会根据网络状况自动选择不同码率的视频流,条件允许的情况下使用高码率,网络繁忙的时候使用低码率,并且自动在二者间随意切
换。这对移动设备网络状况不稳定的情况下保障流畅播放非常有帮助。
实现方法是服务器端提供多码率视频流,并且在列表文件中注明,播放器根据播放进度和下载速度自动调整。
HLS与RTMP对比:HLS主要是延时比较大,RTMP主要优势在于延时低
HLS协议的小切片方式会生成大量的文件,存储或处理这些文件会造成大量资源浪费
相比使用RTSP协议的好处在于,一旦切分完成,之后的分发过程完全不需要额外使用任何专门软件,普通的网络服务器即可,大大降低了CDN边缘服务器的配置要求,可以使用任何现成的CDN,而一般服务器很少支持RTSP。 - HTTP-FLV:基于HTTP协议流式的传输媒体内容。
相对于RTMP,HTTP更简单和广为人知,内容延迟同样可以做到1~3秒,打开速度更快,因为HTTP本身没有复杂的状态交互。所以从延迟角度来看,HTTP-FLV要优于RTMP。 - RTSP:实时流传输协议,定义了一对多应用程序如何有效地通过IP网络传送多媒体数据.
- RTP:实时传输协议,RTP是建立在UDP协议上的,常与RTCP一起使用,其本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于低层服务去实现这一过程。
- RTCP:RTP的配套协议,主要功能是为RTP所提供的服务质量(QoS)提供反馈,收集相关媒体连接的统计信息,例如传输字节数,传输分组数,丢失分组数,单向和双向网络延迟等等。
iOS视频边下边播--缓存播放数据流
使用本地代理服务器的方式,原理很简单,但是缺点也很明显,需要自己写一个本地代理服务器或者使用第三方库httpSever。如果使用httpSever作为本地代理服务器,如果只缓存一个视频是没有问题的,如果缓存多个视频互相切换,本地代理服务器提供的数据很不稳定,crash概率非常大。
这里我采用ios7以后系统自带的方法实现视频边下边播,这里的边下边播不是单独开一个子线程去下载,而是把视频播放的数据给保存到本地。简而言之,就是使用一遍的流量,既播放了视频,也保存了视频。
用到的框架:<AVFoundation/AVFoundation.h>
用到的播放器:AVplayer
先说一下avplayer自身的播放原理,当我们给播放器设置好url等一些参数后,播放器就会向url所在的服务器发送请求(请求参数有两个值,一个是offset偏移量,另一个是length长度,其实就相当于NSRange一样),服务器就根据range参数给播放器返回数据。
产品需求:
1.支持正常播放器的一切功能,包括暂停、播放和拖拽
2.如果视频加载完成且完整,将视频文件保存到本地cache,下一次播放本地cache中的视频,不再请求网络数据
3.如果视频没有加载完(半路关闭或者拖拽)就不用保存到本地cache
实现方案:
1.需要在视频播放器和服务器之间添加一层类似代理的机制,视频播放器不再直接访问服务器,而是访问代理对象,代理对象去访问服务器获得数据,之后返回给视频播放器,同时代理对象根据一定的策略缓存数据。
2.AVURLAsset中的resourceLoader可以实现这个机制,resourceLoader的delegate就是上述的代理对象。
3.视频播放器在开始播放之前首先检测是本地cache中是否有此视频,如果没有才通过代理获得数据,如果有,则直接播放本地cache中的视频即可。
代理对象需要实现的功能
1.接收视频播放器的请求,并根据请求的range向服务器请求本地没有获得的数据
2.缓存向服务器请求回的数据到本地
3.如果向服务器的请求出现错误,需要通知给视频播放器,以便视频播放器对用户进行提示
流程图:
视频播放器处理流程
1.当开始播放视频时,通过视频url判断本地cache中是否已经缓存当前视频,如果有,则直接播放本地cache中视频
2.如果本地cache中没有视频,则视频播放器向代理请求数据
3.加载视频时展示正在加载的提示(菊花转)
4.如果可以正常播放视频,则去掉加载提示,播放视频,如果加载失败,去掉加载提示并显示失败提示
5.在播放过程中如果由于网络过慢或拖拽原因导致没有播放数据时,要展示加载提示,跳转到第4步
代理对象处理流程
1.当视频播放器向代理请求dataRequest时,判断代理是否已经向服务器发起了请求,如果没有,则发起下载整个视频文件的请求
2.如果代理已经和服务器建立链接,则判断当前的dataRequest请求的offset是否大于当前已经缓存的文件的offset,如果大于则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向后拖拽,并且超过了已缓存的数据时才会出现)
3.如果当前的dataRequest请求的offset小于已经缓存的文件的offset,同时大于代理向服务器请求的range的offset,说明有一部分已经缓存的数据可以传给播放器,则将这部分数据返回给播放器(此时应该是由于播放器向前拖拽,请求的数据已经缓存过才会出现)
4.如果当前的dataRequest请求的offset小于代理向服务器请求的range的offset,则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向前拖拽,并且超过了已缓存的数据时才会出现)
5.只要代理重新向服务器发起请求,就会导致缓存的数据不连续,则加载结束后不用将缓存的数据放入本地cache
6.如果代理和服务器的链接超时,重试一次,如果还是错误则通知播放器网络错误
7.如果服务器返回其他错误,则代理通知播放器网络错误
resourceLoader的难点处理
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
[self.pendingRequests addObject:loadingRequest];
[self dealWithLoadingRequest:loadingRequest];
return YES;
}
播放器发出的数据请求从这里开始,我们保存从这里发出的所有请求存放到数组,自己来处理这些请求,当一个请求完成后,对请求发出finishLoading消息,并从数组中移除。正常状态下,当播放器发出下一个请求的时候,会把上一个请求给finish。
下面这个方法发出的请求说明播放器自己关闭了这个请求,我们不需要再对这个请求进行处理,系统每次结束一个旧的请求,便必然会发出一个或多个新的请求,除了播放器已经获得整个视频完整的数据,这时候就不会再发起请求。
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
[self.pendingRequests removeObject:loadingRequest];
}
下面这个方法是对播放器发出的请求进行填充数据
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != 0) {
startOffset = dataRequest.currentOffset;
}
if ((self.task.offset +self.task.downLoadingOffset) < startOffset)
{
//NSLog(@"NO DATA FOR REQUEST");
return NO;
}
if (startOffset < self.task.offset) {
return NO;
}
NSData *filedata = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:_videoPath] options:NSDataReadingMappedIfSafe error:nil];
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.task.downLoadingOffset - ((NSInteger)startOffset - self.task.offset);
// Respond with whatever is available if we can't satisfy the request fully yet
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
[dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.task.offset, (NSUInteger)numberOfBytesToRespondWith)]];
long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = (self.task.offset + self.task.downLoadingOffset) >= endOffset;
return didRespondFully;
}
这是对存放所有的请求的数组进行处理
- (void)processPendingRequests
{
NSMutableArray *requestsCompleted = [NSMutableArray array]; //请求完成的数组
//每次下载一块数据都是一次请求,把这些请求放到数组,遍历数组
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
{
[self fillInContentInformation:loadingRequest.contentInformationRequest]; //对每次请求加上长度,文件类型等信息
BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判断此次请求的数据是否处理完全
if (didRespondCompletely) {
[requestsCompleted addObject:loadingRequest]; //如果完整,把此次请求放进 请求完成的数组
[loadingRequest finishLoading];
}
}
[self.pendingRequests removeObjectsInArray:requestsCompleted]; //在所有请求的数组中移除已经完成的
}
resourceLoader的难点基本上就是上面这点了,说到播放器,下面便顺便讲下AVPlayer的难点。
难点:对播放器状态的捕获
举个简单的例子,视频总长度60分,现在缓冲的数据才10分钟,然后拖动到20分钟的位置进行播放,在网速较慢的时候,视频从当前位置开始播放,必然会出现一段时间的卡顿,为了有一个更好的用户体验,在卡顿的时候,我们需要加一个菊花转的状态,现在问题就来了。
在拖动到未缓冲区域内,是否需要加菊花转,如果加,要显示多久再消失,而且如果在网速很慢的时候,播放器如果等了太久,哪怕最后有数据了,播放器也已经“死”了,它自己无法恢复播放,这个时候需要我们人为的去恢复播放,如果恢复播放不成功,那么过一段时间需要再次恢复播放,是否恢复播放成功,这里也需要捕获其状态。所以,如果要有一个好的用户体验,我们需要时时知道播放器的状态。
有两个状态需要捕获,一个是正在缓冲,一个是正在播放,监听播放的“playbackBufferEmpty”属性就可以捕获正在缓冲状态,播放器的时间监听器则可以捕获正在播放状态,我的demo中一共有4个状态:
typedef NS_ENUM(NSInteger, TBPlayerState) {
TBPlayerStateBuffering = 1,
TBPlayerStatePlaying = 2,
TBPlayerStateStopped = 3,
TBPlayerStatePause = 4
};
这样可以对播放器更好的把握和处理了。
然后说一说在缓冲时候的处理,以及缓冲后多久去播放,处理方法:
进入缓冲状态后,缓冲2秒后去手动播放,如果播放不成功(缓冲的数据太少,还不足以播放),那就再缓冲2秒再次播放,如此循环,看详细代码:
- (void)bufferingSomeSecond
{
// playbackBufferEmpty会反复进入,因此在bufferingOneSecond延时播放执行完之前再调用bufferingSomeSecond都忽略
static BOOL isBuffering = NO;
if (isBuffering) {
return;
}
isBuffering = YES;
// 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来
[self.player pause];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 如果此时用户已经暂停了,则不再需要开启播放了
if (self.isPauseByUser) {
isBuffering = NO;
return;
}
[self.player play];
// 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间
isBuffering = NO;
if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp) {
[self bufferingSomeSecond];
}
});
}
利用GPUImage处理直播过程中美颜的流程
采集视频 => 获取每一帧图片 => 滤镜处理 => GPUImageView展示
- GPU:(Graphic Processor Unit图形处理单元)手机或者电脑用于图像处理和渲染的硬件
- GPU工作原理:采集数据-> 存入主内存(RAM) -> CPU(计算处理) -> 存入显存(VRAM) -> GPU(完成图像渲染) -> 帧缓冲区 -> 显示器
*OpenGL ES:(Open Graphics Library For Embedded(嵌入的) Systems 开源嵌入式系统图形处理框架),一套图形与硬件接口,用于把处理好的图片显示到屏幕上。 - GPUImage:是一个基于OpenGL ES 2.0图像和视频处理的开源iOS框架,提供各种各样的图像处理滤镜,并且支持照相机和摄像机的实时滤镜,内置120多种滤镜效果,并且能够自定义图像滤镜。
- 滤镜处理的原理:就是把静态图片或者视频的每一帧进行图形变换再显示出来。它的本质就是像素点的坐标和颜色变化
GPUImage处理画面原理
GPUImage采用链式方式来处理画面,通过addTarget:方法为链条添加每个环节的对象,处理完一个target,就会把上一个环节处理好的图像数据传递下一个target去处理,称为GPUImage处理链。
零碎的知识
FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,由于其视频文件体积轻巧、封装简单等特点,使其很适合在互联网上进行应用。
RTSP:实时流传输协议,是TCP/IP协议体系中的一个应用层协议;
M4A:.m4a是MPEG-4 音频标准的文件的扩展名,Apple在iTunes以及 iPod中使用“.m4a”以区别MPEG4的视频和音频文件;
音视频同步:时间戳,时间戳即为一帧的采集时间,音视频采取同一个参考时间,给每个帧打上时间戳。
rtmp发送音视频:xcode中编译librtmp库,遵循rtmp协议,将数据发送到指定服务器;
AudioToolbox.framework:提供CoreAudio的中高级别的API服务,处理电话和其他高优先级语音处理而导致的中断和恢复操作等;
AudioUnit.framework:提供DSP数字信号处理相关的插件,包括编解码,混音,音频均衡等;
AVFoundation.framework:提供一个精简的音乐播放类,可以播放所有IOS支持的音频;
OpenAL.framework:提供3D音效播放;
直播平台的高并发架构设计
需求:
用户那边提的需求就是推流端不能卡,画质要好,不能太烫
低延时:
低延时的操作大部分来自端的配合,服务端只要是做好缓存,保证这个数据是连贯的。如果要丢数据的话,把关键帧保留好,丢GOP中间那些PB帧,主要是在端上会收到。
首屏时间:
就是用户点开就要看,以前那些开源架构就是rtmp server,它是做不到一点开就能看的,现在一些开源的国内资源写得也比较好了,可以看到。我们是自己开发的,能保存之前的关键帧的信息,用户一点开就能看,这个就是很细节的东西了。如果这个做不好的话,会黑屏、绿屏,或者是半天看不着图像。
不能卡,不能延迟太高:
要满足这些需求,我们需要做好多分辨率的适配,保证好流畅性,保证好我们追赶的策略不会出现任何异常。所以这三个端很多是相互耦合的,像推流和分发在一起,要保障好用户的流畅性和画质,分发和播放器在一起要保证好低延时和播放的流畅。
一般的流媒体的数据走向、各种请求。但是其中有一些坑我可以跟大家重点说一下,首先看一下直播发起流程,这肯定是由应用向自己的服务端去请求一个推流地址,这个推流地址他就用来向我们的流媒体服务器推,然后我们给它鉴权。
鉴权之后,它可以在参数里选择是不是要录像。如果需要录像截图,或者需要HLS的分发,我们都可以帮他做,做完之后存到我们的存储里,这也是后面会提到的,我们各个业务之间在做隔离、分不同的优先级,这种后端的多媒体的处理尽量都会依赖别的服务,然后就是正常的结束流程。
一般互联网公司做云服务都怎么做?都是给回调,如果这个推流结束了,我来回调业务方,让业务方知道我结束了,你可以做你的逻辑了。
但实际操作中我们遇到了问题,就是业务方的服务器没那么可靠,我们可能过去时间特别久,有延时,有丢,或者他们的服务稳定性我们也确认不了,这其实就是一个双方的耦合了。而且它的服务器,由于是我们来调,它的鉴权功能没有办法做得很复杂,他自己的服务器也存在安全漏洞。如果有人来攻击他的话,他的整个业务流程的状态全是乱的。
在试了几家客户之后,我们就改成另外一种方式,也是大家普遍都接受的,就是由APP和自己的Server发心跳,如果APP的网络不异常的话,它自己结束它的Server肯定是知道的。如果异常的话心跳断了,他也会判断出是结束了的。而且我们这边源站服务也会保证,你5秒钟没有数据就一定是结束的了,我们会把你的流给踢掉,这样就能达到用户的业务状态也是稳定的,我们的流媒体服务也是稳定的,而且耦合也会比较少。
这是我们实际遇到的一个坑,这个其实不难,只是看现在普遍云服务提供商还都是在用回掉的方式,所以我特别提一下另外还有一种可选的方式,效果更好。
播放的流程,播放器会先向他自己的服务请求播放地址,然后来我们这拉流,可以是鉴权也可以不鉴权,取决于它的业务形态。如果拉流失败,我们有一些定制化的操作,他用RTMP来拉流的话,我们会告诉他具体是什么错,包括鉴权失效,鉴权参数错误,还是这个流有问题,我们都会在状态告诉他的。这是之前用户提到的需求,说是播放需要知道哪里出了问题,所以我们尽量把状态码都特别详细的返回给用户。包括我们原站也有查询接口,如果他需要那种统一查询也可以来查。
1、推流端实现方案
推流端设计的原则总结下来就是自适应,推流谁都可以做,开源的也很多。但是为什么有的做得好,有的做得不好呢?就是看自适应做的好不好。
总结下来有三点自适应:
第一是帧率和码率自适应,这是大家都能想到的。我推流,如果网络卡了,我就降点帧率或者降一点码率,把这个事情做好,把流能正常推上去,不要卡顿。也是这张图里画到的,在发送网络的时候,我们做了一个QS模块,我们团队除了做工程化的人之外,还会有四五个博士专门做算法的。
在这里就有一些体现,我们在码率自适应的时候,是直接可以回馈给编码器的,让编码器动态调整自己的码率,尽量保证质量无损,传出来的视频码率下降,视频平滑。帧率的控制就比较简单了,当我们发现网络卡顿了,我们就会反馈给帧率控制模块。
在采集的时候做一些丢弃的操作,目的就是把我们发送的带宽降下来。这个我们是基于TCP做的,肯定没有UDP的效果好,UDP是我们下一步的尝试,现在还没有开始。因为UDP还涉及到源站的一些架构重构,我们还没有来得及做,现在基于TCP的效果其实已经不错了。后面除了这种简单的自适应之外,我们还加了一个算法类的,那个效果就会更明显。
第二种自适应是软硬自适应,这个很好理解,像硬件编码的优点就是手机不烫,缺点一大堆,用MediaRecorder的话,音视频很难同步,用MediaCodec的话,版本兼容有问题,现在还不太好普及。用软编的话码率低,画质好,除了CPU特别烫,别的都是优点。
热门机型有一些低端的,软编受不了的就改成硬编。因为硬编是体力工作,所以适配的机型肯定是有限的
第三个自适应,算法自适应。我们是真正的第一家能够把h.265做成商业化的公司。现在所有的都在提h.265,不知道大家对h.265了不了解,有没有人听说过h.265可以商业化在Web端无插件播放?我们现在做到了在赛扬机器上可以播30FPS的720P视频,在浏览器上不用装任何插件,这是我们持续优化的结果。当然这个不适合移动的场景,是我们在接另外一个场景的时候用到的。
在移动端我们做到了IOS手机720P编码,做到15FPS,然后CPU不会打满,可能是50%到70%之间。之前数据是打满一个核。这是因为我们之前有很多做算法的团队,最开始是做技术授权,后来想在一些产品上落地,移动直播其实是h.265的一个很好的落地的场景,为什么这么说呢?
推流端的任务是把更好的画质推上来,网络有限的情况下,我怎么能推上来更好的画质?h.265相对h.264来说能把带宽省掉30%。30%的概念是在视频点播类的应用里能省点钱,在初创应用来说根本就不在乎,因为主播更贵,谁在乎这样30%的带宽。
但是在移动推流就不一样了,30%是从480P到720P的变化,就是你本来只能推480P上来的画质,经过h.265这种编码之后能推上来720P的,主播的需求就是网络够好,CPU够好,我为什么不推更好的视频上去呢?这就是h.265的一个场景,我用算法的优势,你的机器只要能够让我做到用265来自适应,我就可以推上去更好的画质。
2、分发网络-多集群源站设计
分发网络是躲在很远的一个地方了,我们当时设计的三个原则就是高并发、高可用、系统解耦,前两个很虚了,只要是做系统都会想怎么高并发,怎么高可用,怎么横向扩展最容易。
我们做了一个多源站,相对于很多公司在做单源站的方式,我们就是为了让用户能更好的触达我们的网络。在各个集群、各个城市做了多源站,这样怎么能做到横向的扩容和数据与业务中心的隔离,这种方案并不是很难,用一些存储做好同步其实也做到了。
一些开源服务,也做多分辨率适配,但是它所有的转码调度都是由它的流媒体服务来调起的。包括转码的生命周期也是流媒体服务来控制的,他们都在同级部署。其实这是有很大问题的,多分辨率适配和原画的推送和分发完全不是一个优先级的服务。做系统定级的时候就应该把它们分离开,应该分离在不同的系统来做。
在线转码是一个非常耗CPU的业务。一台现在很高端配置的24核机器,如果我想转一些画质比较好的视频,每个视频转三个分辨率,这样我转八路就把它打满了,这是很耗CPU的。如果我转了没人看,这个CPU就在那耗着,而且这个是不适合和源站混部的一个服务。
转码要和数据离的近,在那个源站集群的同一机房,我们会申请一些转码的资源,然后由核心机房来统一调度。我们把调度和具体的功能分离开,根据你这个流推到哪,我们就就近在哪里转码。转码也加了一些实时转码的策略。
为什么要做在线转码?因为推流端已经是尽最大努力把最好的画质、最高的带宽传上来。但是播放端不一定看得了,这样我们就需要把它转出来,而且h.265虽然好,但是有个最大的问题就是在移动端的浏览器上没有办法播。分享出来的必须是h.264,要不然去微信或者是QQ浏览器,你是看不了的。
我们做了两种策略,一种是有限的机器合理调度。我们的转码系统是个分布式,流水线式的,类似Storm那种系统,但是我们自己做得更适合转码。任务进来之后,我们第一个流程不是转,而是分析,看看你是要转成什么样,你是什么画质,大概会用什么CPU。
如果你的CPU占用很多,我会认为这是一个很难再次被调度的服务,比如你一下进来一个占四个核的转码服务,后来再来一堆占一个核的,肯定是一个核的比较好调度,这个机器资源紧张了,我可以给你调度另外一台机器,或者另外一台机器本来就有些空余,现在剩三个核,我接不了四个核的,我只能先接一个核的,所以我们会按优先级,优先分配高CPU占用的任务,然后才是低CPU占用的任务,在流式系统里,会在预分析之后把不同的任务扔进不同的优先级队列,这个优先级队列就承担着去转不同分辨率视频的职能。
而且在后头如果需要降级容灾的话,也是靠这个优先级队列来解决的,每个用户会有配额。我刚才说24和准24路,其实对于一个云服务公司来说这个量太小了。像我之前在百度做媒体云的时候,每天转码量是有30万,我觉得一个业务做大了,一天30万的转码量是很正常的。
这是考验并发的一个项目,怎么能做到尽量的把CPU打平,因为波峰波谷很明显。像h.265这个场景,我们是做了一套实时转码,有人分享就立刻给你转,让用户一旦开始分享的时候,能达到秒开的作用。但是你不看的时候,我们会有策略尽快帮你停下来。因为这种分享出去的视频并不是一个高并发的业务,有人看我们才给他转是个比较合理的场景。
对于那些低分辨率的现在也在逐步上灰度,不是说所有的你分发了,你发起了,我都给你转,我们会逐渐判断,有人看我们才转,尽量节省系统资源。后面也会考虑存储资源,因为每个机房都会有存储,存储是完全不用CPU的,它保证的是磁盘和IO,和我们完全是资源不复用的,是可以混部的,后面我们会考虑一步一步的混部。
CDN的分发环节,分发环节其实有很多东西是需要播放来配合的,比如说现在推流为了保证画质好,我会增加B帧,加大GOP,这样编码出来的视频质量会变好,代价就是我增加了GOP,那我的延迟就会大,用户一定是从上一个关键帧开始看,这样他看到的可能就是5秒甚至是10秒之前的视频,这个对社交类的移动直播是不可忍受的。既然有这种需求,源站就需要把之前的都保存好。但是怎么能让延时被消化掉,就要靠播放端。
3、播放器端实现方案
这是播放端的实现框图,中间少画了一个地方。这就是个传统的播放器框图,没有体现出我们的核心的技术点,数据从网络接收进来之后,经过RTMP的Demux之后,我们是有一个模块的,这个模块会去判断当前视频是否需要被丢弃,这个原则也和我们接收缓存有关系,我们缓存配的是两秒,如果超过两秒,或者超过某一个其他的阈值的话,我们会开启丢弃的模式。
这个丢弃有多种策略,有的是直接丢掉帧,有的是快进。如果做过播放器就会知道,传统的视频追赶一般都是在视频解码之后来做追赶。解码就意味着会耗CPU,尤其是现在如果我想播720的视频,光是解码就基本上勉强实时的话,根本就没有什么追赶的余地了。
所以我们在算法上做了一些优化,我们拿到这个视频的时候会先去判断它是不是一个可以丢的,如果它是可以丢的,在解码之前我们就丢,但是这样丢会出问题,因为解码器会内部不连续,一旦解码器内部不连续了,它可能会产生黑屏,所以我们即使要丢,也是在解码器里边做了一些定制化的开发,还是把要丢的视频传进去,让它自己来丢,它不去解,这样就能达到更快速的把这个视频丢掉,赶上现在的实际主播的进度。
这样的话,如果我们网络状况很好,不担心以后抖动的话,我们能做到从推流到观看是2秒的延迟,但是一般我们都控制在4秒,就是为了防止抖动产生。
刚才说的是丢的这种逻辑,如果想快进,类似斗鱼那种,在一点进去之后,开始画面是很快过去的,但是没有音频,我们现在在做有音频的方式,视频在快进,音频也在快进,这样的话声音会变调,因为采样率变了。以前在做端的经验的时候,也做过这种变速不变调的算法,很多开源的,改改其实效果都能不错,这种算法只要做好逆向优化,放进来之后,音频也能保证不变调。