[关闭]
@liuhui0803 2016-04-14T14:42:45.000000Z 字数 5655 阅读 2737

如何为Mail.Ru云开发视频播放器

未分类


作者:Maxim Andreev
译者:大愚若智
原文链接http://highscalability.com/blog/2016/3/28/how-we-implemented-the-video-player-in-mailru-cloud.html

Maxim Andreev最近为Mail.Ru云增加了视频流播服务。本次开发工作的目标是,将这个新功能打造成全能的“瑞士军刀”,不仅能播放任何格式的视频,而且能支持任何连接到云的设备。上传到云端的视频内容主要可归为下列两个类别:“电影/电视剧”,以及“用户的视频”。后者是指用户使用自己的手机或相机自行拍摄的视频,这些视频在格式和编码方面十分多样。因为一些原因,如果不首先进行一定的标准化处理,通常很难在其他最终用户的设备上播放此类视频,例如缺少必要的解码器,文件体积太大难以下载,或遇到其他障碍。

Maxim Andreev在本文中详细介绍了Mail.Ru云是如何实现视频播放的,以及他们的云播放器“omnivorous”是如何开发,并确保能支持尽可能多的最终用户设备的。

存储和缓存:两种方法

很多服务(例如YouTube、社交网络等)会将用户上传的视频转换为相应格式。视频只有在转换后才能正常播放。Mail.Ru云使用了一种截然不同的方法:会在播放的同时对原始文件进行转换。与一些专门的视频托管网站不同,Mail.Ru云并不会更改原始文件。为什么这样做?Mail.Ru云从本质来说是一种云存储服务,如果用户在下载自己的文件时,发现文件质量被降低,或文件体积有所变化,此时肯定会感觉不爽。另一方面,Mail.Ru云无力承担将所有文件预先转换并存储起来的做法:这样做需要极多的存储空间。同时这种做法意味着会做大量无用功,因为存储的一些文件可能从来不曾被播放过,一次都没有。

即时转换的另一个优势是:假如需要更改转换设置,例如添加一个额外的功能,此时并不需要重新转换老视频(有时甚至无法重新转换,因为原始文件已经没有了)。这种情况下,Mail.Ru云的做法可以自动实现所有必要工作。

工作原理

目前Mail.Ru云的在线视频流播服务使用HLS (HTTP Live Streaming)格式 由Apple开发。HLS的价值在于,每个视频文件都可以拆分为多个碎片(也叫做“媒体切割文件组”),并加入播放列表。每个碎片有固定的名称和时间秒数,例如一段两小时长度的电影,可以拆分为一系列单个时长10秒钟,共720个碎片组成的媒体切割文件组。当用户决定好要从哪一刻开始观看这个视频后,播放器会从已接获的播放列表中请求相应的文件碎片。HLS的优势之一在于,当播放器开始读取文件头的时候,用户无需等待便可开始播放视频(常规情况下,面对一部完整长度的电影和缓慢的移动互联网速度,等待时间可能会十分长) 。

该格式还提供了另一个重要的可能性:自适应流播,这个功能可以根据用户的网速即时更改视频质量。例如,用户一开始使用3G网络观看360p格式的视频,当火车行驶至有LTE信号覆盖的区域后,将可以用720p或1080p质量继续观看。HLS格式实现这一点的方法也非常简单:播放器获得的是“主播放列表”,其中包含针对不同网络带宽提供的备用播放列表。在加载一个碎片后,播放器会评估当前速度,根据评估结果确定下一个碎片的视频质量:持平、降低,或提高。目前可支持240p、360p、480p、720p以及1080p。

后端实现

此处输入图片的描述

Mail.Ru云服务包含三组服务器。第一组为应用程序服务器,负责接受视频流请求:创建HLS播放清单并将其返回给用户,分发转换后的碎片,并设置转换任务。第二组为包含嵌入逻辑的数据库(Tarantool),负责存储视频信息并管理转换队列。第三组为转换器,负责从Tarantool的队列中接收任务,以及将数据库中的任务标记为完成。在接收到某一视频文件碎片的请求后,首先会在数据库中检查某台服务器中是否存在已转换,满足所请求的视频质量要求,可直接使用的碎片文件。此时存在两种情况。

第一种情况:找到了转换后的碎片。这种情况下可以直接将找到的内容返回给用户。如果有人最近请求过该碎片,就会有现成碎片可用。这是第一种级别的缓存,可用于所有转换后的文件。有必要提到一点:此处还使用了另一种级别的缓存,为避免网络接口超负荷工作,可将频繁请求的碎片分散保存到多台服务器中。

第二种情况:没找到转换后的碎片。这种情况会在数据库中创建一个转换任务,并等待任务执行完成。如上文所述,该服务使用Tarantool(一种非常高速的开源NoSQL数据库,可通过Lua编写存储过程)存储视频信息并管理转换队列。应用程序服务器和数据库之间的通信方式如下。一台应用程序服务器发出请求:“我需要movie.mp4这个文件第二个碎片的720p版本;等待时间不能超过4秒,”在4秒钟内,该服务器会收到有关该碎片所在位置的信息,或者收到一条错误信息。因此数据库客户端并不需要考虑自己请求的任务是如何执行的,通常可以立刻得到结果,或者经历一系列复杂的操作:这一过程中可以通过一个非常简单的接口发送请求并接收结果。

该数据库的容错是通过主-副(Master-replica)故障转移实现的。数据库客户端只将请求发往主服务器。如果当前主服务器遇到问题,随后会将某一个副本标记为主服务器,客户端会被重定向至新的主服务器。这样的主-副切换对客户端来说是透明的,因此客户端始终可以联系到主服务器。

除了应用程序服务器,还有什么组件可以充当数据库客户端?还有担任转换器角色的服务器,这些服务器准备好随时开始转换碎片,需要通过参数化的HTTP连接访问原始视频文件。转换器和Tarantool之间的通信与上文提到的应用程序服务器间的接口非常类似。转换器发出请求:“给我一个任务,我可以等待10秒钟,”随后如果在10秒内接到了任务,就可以让一个转换器进入等待状态。为简化客户端到转换器之间任务转发的实现方式,此处在Tarantool内部使用了Lua语言实现的IPC信道。这种信道可供不同请求相互通信。简化后的碎片转换代码范例如下:

function get_part(file_hash, part_number, quality, timeout)
    -- Trying to select the requested fragment
    local t = v.fragments_space.index.main:select(file_hash, part_number, quality)

    -- If it exists — returning immediately
    if t ~= nil then
        return t
    end

    -- Creating a key to identify the requested fragment, and an ipc channel, then writing it
    -- in a table in order to receive a “task completed” notification later
    local table_key = msgpack.encode{file_hash, part_number, quality}
    local ch = fiber.channel(1)
    v.ctable[table_key] = ch

    -- Creating a record about the fragment with the status “want to be converted”
    v.fragments_space:insert(file_hash, part_number, quality, STATUS_QUEUED)

    -- If we have idle workers, let’s notify them about the new task
    if s.waitch:has_readers() then
        s.waitch:put(true, 0)
    end

    -- Waiting for task completion for no more than “timeout” seconds
    local body = ch:get(timeout)

    if body ~= nil then
        if body == false then
            -- Couldn’t complete the task — return error
            return box.tuple.new{RET_ERROR}
        else
            -- Task completed, selecting and returning the result
            return v.fragments_space.index.main:select{file_hash, part_number, quality}
        end
    else
        -- Timeout error is returned
        return box.tuple.new{RET_ERROR}
    end
end

local table_key = msgpack.encode{file_hash, part_number, quality}
v.ctable[table_key]:put(true, 0)

实际使用的代码略微复杂一些:例如,需要考虑到发出请求的同时,碎片可能处于“正在转换”这一状态。借助这样的架构,转换器可以立刻接获新任务,客户端也可以立刻收到任务已完成的通知。这一点非常重要,因为用户看到视频加载进度条的时间越长,在视频开始播放前直接关闭页面的可能性就越高。

正如下列示意图所示,大部分转换任务,以及因此产生的等待时间通常不会超过几秒钟。

此处输入图片的描述

转换

转换任务使用FFmpeg实现,不过所用的是根据需求定制的版本。最初的计划是使用FFmpeg的内建工具进行HLS转换,然而实际运用中遇到了一些问题。如果用FFmpeg将一段20秒的文件转换为HLS,并设置每个碎片时长10秒钟,最终将获得两个文件以及一个播放列表,此时可以正常播放。但如果请求转换同一个文件,首先转换其中的0-至-10秒,随后转换其中的10-至-20秒(运行另一个FFmpeg转换器实例),播放过程中在从一个文件过渡到另一个(大约在播放到第10秒时)的过程中,会听到非常明显的鼠标单击声。为解决这个问题,Maxim花了好几天时间尝试FFmpeg的不同配置选项,但于事无补。因此只能更深入地研究FFmpeg并写了个小补丁。这个补丁可以通过一个命令行参数修复由于音频和视频轨道间差异造成的“点击声”Bug。

此外Maxim还使用了目前FFmpeg本身尚未提供的其他补丁。例如,使用一个补丁解决了MOV文件(由iPhone拍摄的视频)转换速度缓慢这一已知问题。Maxim还使用了一个名为“Aurora”的守护进程控制从数据库获得任务,并启动FFmpeg的过程。“Aurora”守护进程,以及数据库其他方面所用的守护进程,都使用Perl语言编写,可通过异步方式与EV事件循环(Event loop)以及各种实用模块配合使用,例如EV-TarantoolAsync::Chain

有趣的是,Mail.Ru云新增的视频流播服务完全没有添置任何额外的服务器:转换任务(这种任务需要耗费大量资源)直接在存储设备上一个特殊的隔离环境中运行。通过日志和图表可知,现有可用容量是当前负载的数倍。这里有些数据可供参考:自2015年6月视频流播服务正式上线后,用户共请求超过5百万个唯一视频,每分钟观看500–600个唯一文件

前端实现

时至今日,几乎每个人都有一部或两部智能手机,随手为家人和朋友拍摄一段小视频已经成了稀松平常的事情。因此Mail.Ru云已经为这种场景做好了准备:某人从自己的手机或平板上传视频到Mail.Ru云,随后为了释放空间将自己设备上的文件删掉。如果用户想要向别人展示这个视频,此时只需要使用Mail.Ru云应用即可,或者也可以在桌面上启动Web版本的云播放器。用户无需将所有视频剪辑都存储在自己的手机中,但与此同时,依然可以通过任何设备随时访问。通过移动互联网访问时的视频码率会有所降低,同时流量的消耗也会大幅减少。

此外,在通过移动平台播放视频时,还可以直接使用Android和iOS的原生库文件。因此智能手机和平板无需额外安装应用,即可在移动浏览器中播放视频:Mail.Ru云所用的视频格式无需另行开发专门的播放器。与Web版本类似,台式机上也可以激活自适应流播机制,并获得与当前网络带宽相匹配的画面质量。

Mail.Ru云的播放器,与竞争对手播放器最主要的差别之一在于,前者是与用户环境无关的。大部分时候,开发者需要创建两个不同播放器:一个使用Flash界面,另一个(针对原生支持HLS的浏览器,例如Safari)虽然功能完全相同,但却是基于HTML5实现的,并且需要提供相应的界面。但Mail.Ru云只需要一个播放器。他们的目标是能够轻松更改界面,因此播放器在播放视频和音频时的外观非常类似——所有图标、布局等元素都使用HTML5编写。播放器本身与播放视频所用的技术完全无关。

视频的渲染依然使用了Flash,但整个界面都使用HTML编写,这就避免了可能的版本同步问题,因为并不需要专门为特定版本的Flash提供支持。对于HLS的播放,开源库文件已经够用了。但他们也写了一小段代码,主要是为了将HTML5视频元素界面转换为Flash。因此在开发界面的过程中,可以放心假设所有工作都能通过HTML5完成。如果浏览器无法支持这一格式,可以直接使用自行开发的相同界面取代原生的视频元素。

如果用户设备不支持Flash,将借助对HLS的支持使用HTML5方式播放(目前这是在Safari中实现的唯一方法)。HLS可通过原生工具在Android 4.2+和iOS平台播放。如果设备不支持并且无原生格式可用,此时可以让用户将文件下载到本地。


如果你在视频播放器的实现方面有经验,欢迎通过评论来交流:Maxim非常渴望了解你是如何将视频拆分为碎片,在存储和缓存之间进行权衡,以及在实现过程中遇到过哪些挑战。总的来说,共同分享自己的见解吧。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注