基于本地代理的边下边播技术分析

2020-12-16 22:04:03

参考地址 基于本地代理的边下边播技术分析

1.边下边播技术介绍

我们熟知的边下边播技术,是迅雷提供的,还有之前的快播、快车等工具,它们使用的技术基本上都是P2P下载技术。 P2P下载技术,本质上它并不是C-S的架构,P2P----> Peer to Peer,实际上它将各个客户端的资源调度起来,给上传资源种子,方便后续的下载者可以快速有效的下载资源,这种方式需要服务器整合各个Client,在有用户需要下载的情况下,服务器能及时调度资源,开始给下载者提供资源信息,保证下载者下载资源越快越好。P2P的下载方式后面我们专门介绍一下。这儿不继续展开了。
对一个普通开发者而言,我不想这么费事,我能访问视频资源服务器,我直接从视频源服务器上下载不行吗?
视频下载和视频播放本来是两件完全不相干的事情,但是也有共通之处:播放视频的同时就是需要请求视频资源的;我们要实现边下边播,那就要在请求完视频资源的时候,传输中的比特流,给播放器送去数据,同时也存在本地,这样才是边下边播。正常情况下,播放的速度肯定不如下载的速度,所以一般是先把视频资源存到本地,然后读取本地视频数据送到播放器中。

2.边下边播技术演进


正常的模型是 播放器 <----> 视频源服务器 模型,播放器请求视频资源,视频源服务器收到了请求,返回相应的数据,播放器播放视频数据,这种情况下,也是可以做边下边播的,但是有限制;限制主要是边下边播的控制逻辑非常复杂,因为边下边播的逻辑和播放器的控制逻辑势必搞在一起,这样不仅从架构上无法区分,而且代码上也不好分开,后续维护的成本比较高;一般情况下不建议这么做;
播放器 <----> 代理服务器 <----> 视频源服务器 :这是提出的一个改进的想法,改进的一个点就是在 播放器 和 视频源服务器之间架了 一个 代理服务器,代理服务器请求 视频源服务器数据,然后返回给播放器,这下就实现了将播放模块与下载模块隔离开来;但是代理服务器是需要服务器配置的,一般公司没必要搞视频源代理服务器,太耗带宽了。
本地代理服务器替代一下这个代理服务器是比较好的一种方法,既可以实现将播放模块和下载模块分层,也可以实现边下边播的功能。

这就是演进到播放器 <----> 本地代理服务器 <----> 视频源服务器,我们本文所讲的 边下边播的技术就是 基于本地代理服务展开的。


3.Socket监听

边下边播是基于本地代理,本地代理的建立依赖于Socket监听,那么什么是Socket?

Socket就是一组API,对TCP/IP协议进行封装的API,可以将Socket理解为处于传输层和应用层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。

Socket通讯流程简化如下:

  • 服务端这边首先创建一个Socket(Socket()),然后绑定IP地址和端口号(Bind()),之后注册监听(Listen()),这样服务端就可以监听指定的Socket地址了;
  • 客户端这边也创建一个Socket(Socket())并打开,然后根据服务器IP地址和端口号向服务器Socket发送连接请求(Connect());
  • 服务器Socket监听到客户端Socket发来的连接请求之后,被动打开,并调用Accept()函数接收请求,这样客户端和服务器之间的连接就建立好了;
  • 成功建立连接之后就可以你侬我侬了,客户端和服务器进行数据交互(Receive()、Send());
  • 在腻歪完之后,各自关闭连接(Close()),交互结束;


我们建立本地代理,服务端的地址就是http://127.0.0.1:port,其他的交互流程都和普通的Socket监听是一样的。值得注意的是,Client端和本地代理的服务端http://127.0.0.1:port进行交互,本地代理的服务端和视频源服务器进行交互的时候,本地代理服务端就作为一个“Client端”存在。这种角色易位我们要能理解。
开启一个ServerSocket监听,需要确定一个本地可用的端口,就是这个端口没有被占用。这儿非常重要,使用一个端口一定要确定一下这个端口是否被占用,一旦被占用,本地代理整个服务就挂掉了,代码逻辑上做好控制即可。

4.边下边播技术点解析


4.1 视频类型

视频类型,我们知道有非分片视频和分片视频区分;像 mp4 mov mkv avi rmvb 这些封装格式都是非分片视频,一般情况下,播放器可以一次请求,后续处理,这些视频最终会存储到一个文件中;像现在mp4的封装格式应用的最广泛,有一个很优秀的开源库:https://github.com/danikula/AndroidVideoCache,主要是针对非分片视频的边下边播来的;

分片视频,就是一个整视频被分为若干个小分片视频,请求的时候不能一次性地请求所有的视频文件;会有一个视频索引文件,至于各个分片要依次请求,这种叫做分片视频。之前我们详细分析过M3U8视频的格式:多媒体文件格式剖析:M3U8篇,还有其中的TS视频格式:多媒体文件格式剖析:TS篇

#EXTM3U#EXT-X-TARGETDURATION:10
#EXTINF:9.009,http://media.example.com/first.ts#EXTINF:9.009,http://media.example.com/second.ts#EXTINF:3.003,http://media.example.com/third.ts#EXT-X-ENDLIST

这个HLS文件中有3个分片ts视频,我们请求的时候,需要一个一个请求,整视频请求数据是一次就可以的,后续使用206分段下载;

实现mp4 等非分片视频的边下边播是可以的,那么HLS分片视频如何实现边下边播呢?

4.2 分片视频如何处理

HLS视频----> HTTP Live Streaming,就是熟知的M3U8视频;
https://tv.youkutv.cc/2019/10/28/6MSVuLec4zbpYFlj/playlist.m3u8

什么样算是HLS类型的视频?

从上面的流程图可以得到如何判断视频是M3U8类型:

  • 简单的判断,url解析出的path后缀名是  .m3u8的就是M3U8类型

  • 通过视频的mimetype来判断:如果发现是下面四种类型,就是M3U8类型的视频,我们就可以按照M3U8解析的规则来解析这个视频url

public static String MIME_TYPE_M3U8_1 = "application/vnd.apple.mpegurl";public static String MIME_TYPE_M3U8_2 = "application/x-mpegurl";public static String MIME_TYPE_M3U8_3 = "vnd.apple.mpegurl";public static String MIME_TYPE_M3U8_4 = "applicationnd.apple.mpegurl";

private boolean isM3U8Mimetype(String mimeType) {    return mimeType.contains(MIME_TYPE_M3U8_1)            || mimeType.contains(MIME_TYPE_M3U8_2)            || mimeType.contains(MIME_TYPE_M3U8_3)            || mimeType.contains(MIME_TYPE_M3U8_4);}
解析的视频url是:
http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.main.m3u8

这个m3u8文件存储信息是:

#EXTM3U#EXT-X-VERSION:3#EXT-X-MEDIA-SEQUENCE:0#EXT-X-TARGETDURATION:11.0#EXTINF:10.12,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_0_10.ts#EXTINF:9.88,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_10_20.ts#EXTINF:10.08,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_20_30.ts#EXTINF:10.28,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_30_40.ts#EXTINF:9.68,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_40_50.ts#EXTINF:10.12,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_50_60.ts#EXTINF:3.04,http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_60_63.ts#EXT-X-ENDLIST

转化成一个本地代理的文件:

#EXTM3U#EXT-X-VERSION:3#EXT-X-MEDIA-SEQUENCE:0#EXT-X-TARGETDURATION:11.0#EXTINF:10.12,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_0_10.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_0.ts#EXTINF:9.88,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_10_20.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_1.ts#EXTINF:10.08,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_20_30.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_2.ts#EXTINF:10.28,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_30_40.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_3.ts#EXTINF:9.68,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_40_50.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_4.ts#EXTINF:10.12,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_50_60.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_5.ts#EXTINF:3.04,http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_60_63.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_6.ts#EXT-X-ENDLIST

请求一个

http://127.0.0.1:3888/http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_0_10.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_0.ts

这个http://127.0.0.1:3888的url会被拦截,直接解析出后续的参数:
http%3A%2F%2Fvideoconverter.vivo.com.cn%2F201706%2F655_1498479540118.mp4.f10_0_10.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_0.ts

对这个url decode 一下:

http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_0_10.ts&jeffmony&/3bfd0b2eec722da9ed67509a9388dbe2/seg_0.ts

我们自己定义的一个隔离字符串 &jeffmony&,这个分隔符将字符串分为两部分:


http://videoconverter.vivo.com.cn/201706/655_1498479540118.mp4.f10_0_10.ts

/3bfd0b2eec722da9ed67509a9388dbe2/seg_0.ts
第一个表示当前分片视频的网络url;第二个表示当前文件本地存储的位置;我们在解析的时候,先判断是否存在本地的分片视频,如果存在,直接读取本地的文件,如果不存在,那要去请求网络的分片url;

最终缓存文件夹下内容如下:
PD1710:/sdcard/Android/data/com.android.media/cache/.local/3bfd0b2eec722da9ed67509a9388dbe2 $ lsproxy.m3u8 remote.m3u8 seg_0.ts seg_1.ts seg_2.ts seg_3.ts seg_4.ts seg_5.ts seg_6.ts video.info
分片视频都下载到了本地;真正下载的逻辑应用不需要介绍了,这个大家直接看代码吧;

4.3 非分片视频分段如何处理

视频播放不是孤立的行为,用户有可能会拖动进度条的,拖动进度条,如何拖动到当前没有下载到的位置,那就必须要从拖动到位置向后重新下载,这个分段缓存片段的管理也是比较重要的;

用户随意拖动进度条,可能会产生若干个分段的缓存块,这些缓存块是不连续的,但是一旦用户拖动进度条到之前的某个位置,下载资源的时候会将各个分段的缓存块连起来,连接起来之后就是一个完整的视频;

不过维护缓存块的逻辑是比较重要的,这儿主要讲解一下思想,具体看下项目代码吧,是按照上面阐述的思想来的。



4.4 边下边播架构图

最后贴一下边下边播的架构图。代码见开源项目:
https://github.com/JeffMony/MediaSDK


  • 2018-12-02 10:54:14

    HTTP长连接、短连接究竟是什么?

    HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。

  • 2018-12-04 15:30:01

    如何在Mac OS X上安装 Ruby运行环境

    ​ 对于新入门的开发者,如何安装 Ruby和Ruby Gems 的运行环境可能会是个问题,本页主要介绍如何用一条靠谱的路子快速安装 Ruby 开发环境。 此安装方法同样适用于产品环境!

  • 2018-12-04 15:31:15

    iOS--Pod install && Pod update

    许多人在最初接触CocoaPods时认为pod install只是在第一次为项目设置CocoaPods时使用,之后都应该使用pod update.看起来是这样,但也不是(But that's not the case at all.)。 这篇文章的目的就是教你啥时候用pod install,啥时候用pod update

  • 2018-12-04 15:33:19

    CocoaPods安装和使用教程

    当你开发iOS应用时,会经常使用到很多第三方开源类库,比如JSONKit,AFNetWorking等等。可能某个类库又用到其他类库,所以要使用它,必须得另外下载其他类库,而其他类库又用到其他类库,“子子孙孙无穷尽也”,这也许是比较特殊的情况。总之小编的意思就是,手动一个个去下载所需类库十分麻烦。另外一种常见情况是,你项目中用到的类库有更新,你必须得重新下载新版本,重新加入到项目中,十分麻烦。如果能有什么工具能解决这些恼人的问题,那将“善莫大焉”。所以,你需要 CocoaPods。

  • 2018-12-04 23:37:37

    pod install 和 pod update

    当我们新建一个Podfile文件运行后,会自动生成一个Podfile.lock文件,Podfile.lock文件里存储着我们已经安装的依赖库(pods)的版本。 当我们第一次运行Podfile时,如果对依赖库不指定版本的话,cocoapods会安装最新的版本,同时将pods的版本记录在Podfile.lock文件中。这个文件会保持对每个pod已安装版本的跟踪,并且锁定这些版本。

  • 2018-12-04 23:40:26

    pod删除已导入的第三方库和移除项目中的cocoapods

    CocoaPods是一个负责管理iOS项目中第三方开源库的工具。CocoaPods的项目源码在Github上管理。在我们有了CocoaPods这个工具之后,只需要将用到的第三方开源库放到一个名为Podfile的文件中,然后在命令行执行$ pod install命令。CocoaPods就会自动将这些第三方开源库的源码下载下来,并且为我的工程设置好相应的系统依赖和编译参数. 但是如果我们导入的某个第三方不适用,或者我们又不想使用该第三方,那我们又该如何将这些相关的东西从我们的项目中清理出去呢?

  • 2018-12-04 23:41:47

    制作自己的Pod库(公有/私有)

    目的:1.管理自己常用的类;2.组件化开发步骤:1.想一个比较酷的名字,在桌面简历文件夹。2.打开terminal,cd到这个文件夹下面,执行pod lib create  xxx(这里我们以JJCategoryKit为例子,下同)命令,如下图。这个过程会问几个问题,根据实际情况输入回答即可。这里我们选择添加demo,结束的时候会自动Lanuch这个app. 作者:深水日月 链接:https://www.jianshu.com/p/ece0b5721461 來源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 2018-12-05 06:08:26

    CocoaPods建立私有仓库 spec repo

    好多项目里都有公共的组件,copy来,copy去很容易出错,而且不容易维护,所以就想到用用cocoapods 建自己的私有库,Carthage用法虽然相对简单,但是它是把公共组件都放在framework里不容易单步调试,所以我还是选择用Cocoapods 来建立私有仓库 参考使用Cocoapods创建私有podspec