Work notes 2007.03 ~ 2008.09
记录2007.03 ~ 2008.09期间开发基于BitTorrent的P2P VOD系统的一些思考,这个项目使用libtorrent作为P2P模块,但媒体文件分析,回放及与P2P的集成并没有可借鉴的实现,完全靠自己摸索,一个接一个问题的分析和解决。
2016.10.08 评注
libtorrent这个产品至今仍在活跃状态,从2003年开始,到现在2016年,已经13年,开发者Arvid Norberg的毅力和坚持真的很令人敬佩。对于这样一个开源项目,除了热爱,我想不到其他的原因。libtorrent大量使用Boost里的库来实现功能,包括thread, filesystem, asio,因为采用跨平台的库,所以当初需要从windows移植到Linux时只花了一个星期的时间…
现在回头来看,当时(2007)公司的业务是基于Windows Media Server提供视频点播业务(和运营商合作,用户需要付费,包月观看),一台服务器大概能提供300个用户访问,做这个系统的目的是减轻服务器的并发压力,希望能达到一台服务器提供1000个用户访问。这个业务严格来说是不合规的,但中国的国情如此(不管是软件,还是电影,小说,甚至游戏…等等知识产权的东西都是盗版横行),而且那时候监管不严,用BitComet,Vagaa, VeryCD可以搜索,下载大量的AV内容,随着政策和监管严厉,以及QVOD,迅雷看看,PPStream, PPTV等通用的视频客户端的出现,公司业务转向2B方向,与机顶盒厂商合作,提供P2P VOD服务,所以有了移植到Linux的需求。再后来我就离职了。
记得当时还研究过QVOD(虽然当时并不懂得竞争分析,只是单纯的技术分析),发现其下载缓冲速度的确很快,做了抓包分析后,也试图改进我们的播放器,但发现原理并不一样,甚至还构想过“外挂任何一种协议,只要能从该地址取流即可,做到协议无关取流,不只是依赖BT,比如可以从HTTP, MMS, QVOD,只要能从拥有相同内容的地方取得数据即可。”,但终究只是想一想而以,并没有进一步的动作。是什么原因,早已经忘记,也许当时并没有那样的动力,仅仅只是完成任务,老板也并不看重,所以不了了之了吧。
那段时间还是比较开心的,因为有解决问题的成就感,公司小不规范,但也灵活自由,没有那么多冗余的流程以及政治,还有和同一水平的team members(总共也就5个人开发)一起讨论问题的技术氛围(虽然水平不高)。我离开后就没有再联系,直到2015年偶然的机会,听说他们被收购,再后来又成立新公司做智能电视去了。这么多年过去,公司没有长大,但也没有死,而且以前的骨干也还在,也是不容易的事情。
从商业的角度看,视频行业的关键在于内容,技术只是辅助工具,而且资金投入巨大,非小公司玩的起,同时其商业模式变现有限(会员,广告,游戏联运),亏损是常态,比如2005年成立的Youtube至今仍在亏损,优酷也是如此。现在火起来的直播行业,恐怕也会面临这样的局面,但个人看好未来的VR直播,因为视频的核心是优质的内容,VR直播技术提供的在场感,可以解决体育赛事,演唱会,综艺节目,新闻,教育等等因为物理空间限制而无法满足巨量人群到现场享受高质量内容的痛点。观众花很低的价格可以”现场”坐第一排看节目;内容提供商可以卖百倍,千倍的票;直播平台抽取一些提成;三方多赢,皆大欢喜。
2008.08.16
加密下载,解决出现的BUG:
目前的加密策略是,从tracker中得到peerid,根据peerid判断是否是我们自己的客户端,如果是,则使用加密连接,否则使用非加密链接。在测试BTCache时,发现外部tracker基本都没有返回peerid,且如果外部tracker比我们的tracker早返回sender这个peer,就会导致使用非加密连接。通过查看相关文档,及实际测试,发现很多BT程序都已经支持加密,目前考虑向外连接时强制使用加密。
2008.06.10 ~ 2008.07.13
深圳出差一个月,跟进PTV项目,在别人公司,第一次听说员工内部持股制度。
2008.04.21
设计点播的大文件格式,考虑小步迭代的方式逐步的实现。通过逐步实现功能,避免一开始就设计的太复杂,导致陷入设计和编码泥潭。先简单的实现,从实现中获取的经验为进一步设计提供参考和实践支撑,避免了设计和实现的脱节。
针对点播的大文件设计,采用如下逐步设计和实现策略:
1.只考虑大文件中存储一个文件,即事先建立一个大文件,然后将媒体数据存储在此大文件中,验证播放模块是否能正常工作;
2.多个文件连续存放,不考虑拖放造成的数据空白问题,某个文件在下载时,其他的文件能提供上传(空闲时?);
3.考虑拖放时,空白数据的填充,文件的替换,淘汰策略;
P2pvod V2实现纯粹的P2P点播,不考虑所谓的下载。相对于V1,主要的变更在于大文件格式的设计(FileIO),P2P模块的性能提升上。
迅雷看看是使用大量的小文件存储点播的视频,PPS则是用一个大文件来存储。
2008.03.18
P2PVOD考虑使用大文件存储,需要重新考虑架构,最好与现有的接口,功能兼容。最复杂的地方在于文件格式设计。
2008.02.26
公司要将BT下载功能移植到基于MIPS硬件的linux平台下,这次能顺利,在一个星期内就完成,得益于当初架构选择的正确,使用了跨平台的底层BT实现库,libtorrent,
在x86 linux 下编译,然后在mips linux下运行,使用了mips-sde cross compiler
在移植到x86 linux平台时,只修改了很少的代码即可以成功运行起来;(跨平台库的优势充分显现)
在移植到mips linux平台时,设置boost.builid来使用交叉编译环境花了一定的时间,所得经验如下:
一.在x86 linux平台下编译libtorrent
可能需要安装g++
apt-get install g++(ubuntu)
1.下载boost,解压
2.或自己编译,或下载 得到相应的bjam,将bjam拷贝到系统目录(/usr/local/bin)
3.根据当前使用的编译器,修改$(BOOST_ROOT)/tools/build/v2下user-config.jam文件,指定相应的toolset (去掉相应编译器的屏蔽即可(即去掉#))
4.指定Boost.Build v2的目录,设置环境变量,输入如下命令:
export BOOST_ROOT=$(BOOST_ROOT)
export BOOST_BUILD_PATH=$(BOOST_ROOT)/tools/build/v2
5.切换到libtorrent的examples目录,执行如下命令:
bjam link=static release
6.编译出的client_test即是BT客户端
./client_test -i [torrentfile]
在$(BOOST_ROOT)目录下运行下面的命令:
bjam –v2 –with-filesystem –with-thread –with-date_time –with-regex –with-program_options link=static release
二.在x86 linux平台使用cross compiler 来编译libtorrent (target为基于MIPS的linux)
修改boost.build 来使用交叉编译tools-chain,mips-linux-gcc
1.使用BOOST.Build v1
export BOOST_ROOT=$(BOOST_ROOT)
export BOOST_BUILD_PATH=$(BOOST_ROOT)/tools/build/v1
export GCC_ROOT_DIRECTORY=”/usr/local/mipsel-linux”
export GCC_BIN_DIRECTORY=”/usr/local/mipsel-linux/bin”
export GCC_INCLUDE_DIRECTORY=”/usr/local/mipsel-linux/include”
GCC_STDLIB_DIRECTORY=”/usr/local/mipsel-linux/lib”
export GXX=”mipsel-linux-g++”
export GCC=”mipsel-linux-gcc”
设置好上述环境变量即可
要在目标板上运行,需要静态链接runtime库:
bjam “-sBUILD=release
2.使用BOOST.Build v2
export BOOST_ROOT=$(BOOST_ROOT)
export BOOST_BUILD_PATH=$(BOOST_ROOT)/tools/build/v1
修改$(BOOST_ROOT)/tools/build/v2下user-config.jam文件
using gcc : : /usr/local/mipsel-linux/bin/mipsel-linux-g++ ;
要在目标板上运行,需要静态链接runtime库:
bjam link=static runtime-link=static release
2008.02.20
学习,研究某个源代码时,绘制相应的类图/顺序图有助于增强理解,增加学习效率。
2008.02.18
新的一年开始,多看书,主要放在c++ librarys: ACE,BOOST
学习领域,主要放在高性能网络编程,并行,分布式对象计算上,重点关注Erlang
开发实践思考:
- 使用hpp后缀定义C++头文件,与C的h后缀相区别
- 编写“self-documented”的代码(通过使用注释+相应的工具(doxygen,visual assist X)来完成)
- VisualAssistX自动生成符合doxygen规范的注释,doxygen自动生成相应的文档,保持代码和文档的一致
- 使用IDE 助手工具,帮助更好的编写代码,测试等(Visual Assist X, Purify..etc)
- 善于使用library来帮助完成工作(ACE, boost, Cppunit, STL..etc)
- 开始编码时,构建数个开发目录include, lib, src, bin, bind, test, doc
- 在整个开发生命周期中,掌握必要的工具来帮助编码,测试,文档等。
- 版本控制工具:VSS,Subversion
- 以后使用的文档,都保存为.mht格式,可以直接在browser中查看,同时可以用word编辑。
- 在程序中引入crash report tool
- 以后开发使用ATL的程序时,UI的工作使用WTL来完成.
- 工具熟练使用:doxygen, subversion
2007.12.28
P2P点播时,提高用户体验的改进:
使用小piece的种子文件,这样减少下载头./索引的时间;
取消piece校验;(基本上校验时间可以忽略不计)
SDK与MediaHandler之间的通知和交互由piece改为block,因为通常头及索引数据比一个piece 256K小的多,浪费很多时间在下其他的数据上,这样也会损失安全性,因为此时没有做piece完整性校验;
通过测试,发现如下影响性能的问题:
1.在下载到索引的第一个block,写入文件会花费和文件大小等比例的时间(100M的文件,4S左右),导致p2pnetwork堵塞;
2.紧急数据的请求,清除拥有请求范围piece的connection先前的请求队列中非在紧急请求中的piece,cancel下载队列中非在紧急请求范围内的piece,
2007.12.19
研究下BitTorrent Location-aware Protocol
http://wiki.theory.org/BitTorrent_Location-aware_Protocol_1.0_Specification
2007.11.12
经过测试,发现我们的P2PVOD与另一款也是基于BT的VOD产品QVOD相比,在同样的种子及机器网络环境下,下载速度上差距很大,有时别人能达到100-200K,而我们才数K的速度,主要的区别在于QVOD能得到很好的提供较大上传速度的peer。需要进一步调查,以提高我们产品的下载速度。
以前一直以为只要减少缓冲时间即可提升用户体验,现在看来,这个固然是一方面的因素,关键还是在于下载速度,即BT协议的优化上。
QVOD与P2PVOD对BT协议使用的差异:
QVOD 向tracker请求num_want = 200,而p2pVod是50;
经过捕包测试,发现QVOD并未使用BT协议,而是使用自定义的基于HTTP/UDP的协议,使用BT种子文件的目的是想从tracker得到运行同样终端的peer;同时其使用了自定义的类似于DHT的peer发现策略,所有找到的peers 一直会上传数据,形成一个自治的网络。关于QVOD的实现,有个想法就是可以在我们P2PVOD中实现QVOD的协议,从而能从它的网络里得到数据;
考虑:外挂任何一种协议,只要能从该地址取流即可,做到协议无关取流,不只是依赖BT,比如可以从HTTP, MMS, QVOD,只要能从拥有相同内容的地方取得数据即可。
经过测试发现,从tracker上得到的peer,有50%都是无效PEER,现在优化策略如下:
向tracker请求的peers由50增加到200;
缩短向Tracker请求时间,由30分钟缩短为1分钟;
在有紧急数据请求,就是MediaHandler向p2pNet请求下载piece时,p2pNet强制向tracker 请求peers;
增加DHT的支持;
2007.10.22
整个系统完成的差不多了,剩下的主要在完整的测试及各个模块文档,代码的整理等收尾工作;
P2PVOD性能提升,需要在现有的已测试基础上,这样才能形成对比,以验证修改。
因此需要做一个测试计划,尽量保持测试环境的稳定性,以保证性能评估的可靠性及可比性。
2007.09.27
多文件种子点播支持。必须先释放MediaHandler,再创建一个MeidaHandler,而不是仅仅调用SetPlayParam接口,因为如下原因:
如果种子中存在文件A, B,有可能一个piece跨越了AB两个文件。A对应的piece范围为(0-100),B对应的piece范围为(100-200)用户先点播B,这时MediaHandler请求下载piece 100,继续播放一段时间后,P2P会删除piece 100 中A文件的部分。假设此时MediaHandler记录已经下载了 100, 101,102,用户切换为点播A,MediaHandler会认为A文件尾对应的piece 100已经下载,但是其实A对应的尾部文件已经被删除了。这个是主要原因,同时切换了媒体文件,MediaHandler内部的子对象都要释放,不若直接把MediaHandler对象释放。
2007.09.04
“MediaPlayer播放出错”的原因已经找到,因为MediaHandler是由SDK驱动来进行播放进度1秒递增,但是SDK的定时器会间隔2S才调用一次,所以“进度”(比如10)和实际的播放器时间(比如30s)的差距会越来越大,同时缓冲时间是以“进度 ”为基准来计算需要的piece是否已经下载,这样就有可能出现计算的piece已经足够播放,但是实际播放器已经无法播放的情况。解决该问题的方法是,MediaHandler使用自己的线程来驱动,而不依赖于SDK,这样进度和播放时间误差较小,不会出现播放器无法播放的情况;同时因为使用自定义的线程,外部调用改为异步调用,通过为MediaHandlerMgr增加一个线程及消息队列来驱动所有的Handler,
MediaHandler自己创建线程驱动不使用SDK线程,同时暴露给外部的接口都转为异步调用.
2007.08.30
解决两个大问题:
以前的MediaHandler及SDK都是在主窗口线程环境下运行,播放器能正常播放。改为MediaHandler在SDK的线程下运行,会导致播放器不能播放,原因是SDK线程不是窗口线程,导致播放器的消息堆积无法处理,通过在SDK线程的主循环中加入
MSG msg;
if ( 0 != ::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) )
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
如上代码可解决该问题。
播放时拖动过程中有时候会出现“无法找到媒体文件错误的情况”,所以每次播放都需要指定媒体文件路径 ,但是在播放DRM文件时一旦调用SetUrl,会导致拖放时每次都会重新认证,通过设置播放器不自动播放来解决该问题。
使用MFC封装的播放器控件,播放时拖动过程中有时候会出弹出一个错误对话框,该对话框无任何内容,一旦点击,会导致系统挂住,通过使用原始的播放器COM接口调用,不使用MFC封装解决该问题。 使用MediaPlayer的COM接口之后,原先出现的“播放器全屏时按键盘会导致无法退出全屏”的问题不使用钩子自动解决
2007.08.06
这周要解决以下问题:
播放进度和时间的通知;
处理播放速度比下载速度快的问题;
项目管理方面,代码共享机制的实施可以避免人员流动代码的项目风险,同时增加成员对系统的理解和沟通方便,并在单个成员主负责的模块无力解决某个问题时,能有效地参与解决问题。如果没有条件实现代码完全共享,至少每个成员可以根据模块进行代码互串,特别是关键底层模块需要至少两个成员掌握理解,避免做关于该模块的决策时的片面及不完整。
2007.08.04
先前的设计有些问题,创建播放器的任务本来是由MediaHandler来做,但是该任务是在p2p模块的线程里执行,而创建播放器的父窗口是由SDK给的,所以窗口句柄无法在两个线程间共享而导致获取播放器窗口失败。现在的设计改为由SDK来驱动MediaHandler运转,这样就可以创建窗口成功。
现在的进度,可以播放,还需要完善以下 :
进度条和拖放处理;
下载速度比播放速度慢的情况
还有一个情况是,在非完整媒体文件的情况下,MediaPlayer能正常播放,但使用MediaPlayer控件就有一个“错误码 = 0xc00d107d, 错误描述 = 无法创建全局界面表”的情况,需要多点击几个play才能播放。
在ASF规范中,索引对应的时间戳需要间接的通过packetNum来得到,而在我们的应用中,文件是非完整的,因此无法得到索引对应的时间。因此现在的处理是进度条进度和播放时间的显示是分离的,进度条是以索引为总长读,一个刻度是一个索引的偏移;播放时间的显示是通过读取MediaPlayer控件的时间得到。
2007.05.08
经过试验发现,边下载边播放是可行的:下载的非完整的文件直接给player播放,拖放时,经过一定时间(实际实现的时候需要根据带宽等采用一定的策略)的缓冲,再将player由指定时间播放。因此目前最大的风险在于,下载速度要快于播放速度,所有的关键在于此。
因此今后的重点放在libTorrent上,主要还是BOOST库的学习及BT协议的理解。
2007.04.23
查找读sample播放拖放时瓶颈,降低时间延迟,当前是20s,主要时间消耗在IWMWriter:: EndWriting()上。对于当前的拖放的处理(先停止player,Reader, Writer,再从指定位置启动player, Reader, Writer),所消耗时间为:
Player Stop : 125 ms
Media Stop : 16188ms
Player Start: 140ms
Media Start: 266ms
其中,Media Stop的主要开销在IWMWriter:: EndWriting()上,大概16s左右,如果采用当前的广播方式这个时间是少不了的,再加上播放器缓冲时间5s,需要20s左右的响应时间。在实际点播的时候,在这个IWMWriter:: EndWriting()所消耗的16s时间内,可以并行的进行piece下载任务,等需要的blocks和 IWMWriter:: EndWriting()都结束时,即可重新Reader/Writer/Player
对于当前响应时间太长问题的主要解决方法,不使用广播编码再播放的方式,而是得到sample后直接丢给player播放。
http Tracker ,UDP Tracker,MutlTracker
研究UDP Tracker protocol,比http Tracker节省 >50%的带宽
2007.04.17
packet如何得到BT下载后的piece:
通过索引得到Sample所在所在packet Number的文件位置及 packet Count,计算出文件偏移位置范围,用该位置,除去piece长度,计算sample所在piece之间,从而请求相应的piece.
当前只是根据Simple Index Object来拖放
研究libtorrent,实现顺序取piece,开始学习BOOST
2007.04.16
研究使拖动播放正常
把拖放点的第一个包的时间戳做起始时间不可行,因为有可能文件后面的音频包的时间戳比起始时间的要小(设为0就OK了),同时发现从文件开始播放时,不需要设定起始时间,从文件里读出的Presentation Time即可作为WriteStreamSample的参数,不需要减去第一个包的时间。但是从文件的某个packet播放时,需要指定初始时间(问题是如果把第一个包作为初始时间,后续相对媒体的包的时间可能比这个小,当前处理是设为0,是OK的,所以起始sampleTime是必需的)。
拖动处理:
停止当前的player, Writer, reader,重新启动reader, writer, player,从拖放点对应的packet开始播放,是可行的,从拖动开始到显示图像,时间差大概20-25s。
暂停,从指定的POS开始播放,再次播放时,会导致player和Writer里先前缓冲的内容被播放出去,也就是有先前残留的图像播放出来。
出现一个问题,在debug下正常,在Release下会异常,出现NULL指针。是因为使用的自定义的DCLinkedList导致的,改用标准库的list,按packet来播放是不需要缓冲池的。去掉list,搞定。
2007.04.10
Refactoring,消除重复代码;
性能调节,主要是去除不必要的内存分配,拷贝,释放过程;
按照文件中音视频prestation time顺序播放,有问题
按照文件中packet顺序(完整的sample)播放,正常。
增加随机拖动播放的功能
2007.04.09
分析ASF文件读取SAMPLE回放,并可以指定从某个包回放,不使用广播编码回放而是直接在player里回放;
2007.02.28
P2P点播相关讨论:需要考虑的内容
分包 / 格式
播放 / 协议
缓冲 / proxy
tracker
公司目前的P2P直播在peerCast基础上开发的,因此下载研究peerCast, peerCast使用SVN进行版本管理需要使用一个SVN 客户端工具来下载,选择TortoiseSVN作为下载客户端
当前可找到的分析peerCast的BLOG:http://blog.csdn.net/bbisonic/
对于P2P点播,由类似BT下载
看BT协议,讨论P2P点播的相关架构
BitTorrent Specification http://wiki.theory.org/BitTorrentSpecification
一个C++实现的BT 库libTorrent,采用了BOOST,ASIO http://www.rasterbar.com/products/libtorrent/index.html