注:此方案于2015年实现,本文已收录专利,版权所有。另外,部分段落缩减了部分内容。
1. 背景
现今在线教育如此火爆,百花齐放、百家争鸣。对于在线教育一对一或一对多实时语音答疑的产品方案中,有多种方案可以选择,优点和缺点因人因公司而异。
本方案采用语音和笔迹(板书)相结合的方式提供在线答疑服务。
老师通过语音和学生实时通话讲解,并通过老师端的专用答疑本和数码笔为学生进行板书书写,如图1。
答疑本和数码笔是定制专用硬件产品,答疑本上布有专用数据点阵,数码笔通过USB线和电脑相连。书写的过程中,数码笔采集到本子上的点阵位置,并实时传输到PC老师端(图2);
并通过专有服务器将笔迹数据转发传输到学生手机端APP(图3)上进行描绘出来,同时对这一过程进行录播,达到语音笔迹同步播放的效果。
图2、图3是实际答疑过程中的截图,图4是答疑结束后回放的截图。
在线答疑业务中需要对老师和学生答疑过程进行现场还原,需要对这一过程进行录制,并在答疑结束后立即由PC老师端上传文件到云端,以便在PC或APP浏览器端、APP移动端进行回放。一方面以供后期学生对历史答疑记录进行知识点的回放和温故总结;另一方面供内部审核,一是审核当前答疑老师或学生是否进行了违规操作、不当的言语或老师不用心的讲解等,二是审核并且自动或人工检测当前的语音通话质量、笔迹传输是否完整和合格。
一种简单的方案是,在PC老师端由录屏软件录制音频和笔迹,然后压缩编码成视频格式(通用mp4格式)并上传到后端云存储服务。好处是在各类平台(如app或pc端)通过html5 video标签自由回放视频文件。弊端是:资源和带宽浪费,因为视频本身的压缩会很耗时,可能导致PC老师端答疑软件特别卡顿,另外存储文件的大小也很大(比如1个小时的480p/15fps/500Kbps的视频大小可能达到200M),造成本地磁盘和上传带宽压力大大增大,同时如果文件是存储在第三方云存储上的话,价格也会大大增加;另外PC老师端软件窗口大小不能调节变化,因为如果窗口大小变化,录屏下来的视频画面将会不一致,导致用户体验大打折扣。
另外一种方案是,由中转服务器或者从中转服务器引出一路监听者的身份的服务器(录制上传服务器)来录制语音(.mp3)和笔迹(.pen自定义格式)文件,并分开标记上传到云存储服务,在app或pc端通过自定义的播放器来播放。好处是不占用PC老师端的任何额外资源;其次,因为是在服务器端录制,不需要PC老师端上传逻辑,这样答疑完能立即实时生成播放文件链接。弊端是:消耗服务器资源,需要部署多台录制上传服务器专门用来做录制和上传;其次不能保证老师端和学生端的语音和笔迹的完整性,因为中转服务器在不可用的情况下会自动切换为p2p的形式直连,这样是不能录制到语音和笔迹的;另外也不能保证至少一端的语音通话质量(回音、噪音等),因为老师端和学生端的语音需要各端的语音引擎分别处理优化后才能通过外放播放。
综合以上情况,我们可以看出,在线实时答疑录播存在以下难点:
难点1:数据压缩问题。在非wifi情况下的回放答疑历史记录,需要保证在回放体验的基础上将流量消耗降到最低。
难点2:数据完整性问题。由于PC客户端软件可能异常退出,所以对于已经录制的语音和笔记需要保证没有脏数据,播放器也能正常的播放出。
难点3:笔迹和语音同步问题。由于在录制时是分别录制语音和笔迹,同时在PC客户端出现异常退出后恢复时继续上次的语音和笔记录制,所以在播放时需要对两者进行同步匹配,不然就会出现语音和笔迹脱节不同步,从而影响播放体验。
难点4:播放器效率问题。由于在移动端设备方面存在IOS和安卓等平台的差别,而且基于安卓平台,更有各种手机厂商数千款手机产品,每种产品的设备配置和性能参数都不尽相同,所以定制化的播放器(基于html5)需要适配所有这些机型的兼容问题,并且在绘制笔迹时尽量减少cpu的消耗,尤其是在大步长的seek进度条时,绘制效率更需要考虑。
综合以上的讨论和难点,提出了一种更优化可靠的方案,即在PC老师端实时录制老师和学生的语音(.mp3)和笔迹(.pen自定义格式)文件,分别保存在磁盘指定目录,在答疑结束后通过deamon进程循环扫描式上传(断点续传)到云存储服务器,定制化的播放器从云端请求播放语音和笔迹。这样可以保证老师端和学生端的语音质量(本地回音、噪音消除等);不需要额外的服务器资源,节省了成本,完全在PC老师端机器上完成录制和上传;充分压缩录制文件,减少带宽和存储压力;定制化的播放器可以更优的回放语音和笔迹。
2 在线答疑录播流程
2.1 业务流程
图5所示为答疑录播的整体流程,在老师接单并建立了答疑通道后,开始答疑,答疑结束后上传语音和笔迹,并存储和通知后台记录,供播放器回放。
老师和学生建立连接后,老师使用数码笔在答疑笔记本上书写要讲解的内容,实时采样并通过USB线传输给PC老师端;同时采集PC本地音频设备来捕获老师语音。
通过网络中转服务器,老师端将语音和笔迹数据发送给学生端,同时学生端采集手机端语音发送给老师端,PC老师端实时保存老师和学生的语音到mp3文件,保存笔迹到自定义pen格式的文件。
在整个答疑结束后,随机启动的deamon子进程对当前答疑文件通过内网域名断点续传到反向代理服务器,语音和笔迹文件是顺序传输的(即语音文件传输完成后传输笔迹文件),并且进程会自动检测当前上行网络带宽,自动调整上传速率,保证不影响正常答疑。另外此deamon子进程负责纪录对应答疑订单号下的每个文件的上传进度和状态信息到本地数据库,并且循环扫描指定的本地存储目录,保证对所有未完成的文件进行断点续传。
反向代理服务器首先初步验证客户端上传片段请求的合法性,验证成功后通过负载均衡策略匹配到合适的单个业务服务器,否则拒绝此次上传片段请求;业务服务器再进一步验证请求的头部字段、token、文件格式和类型,验证成功后本地存储分片到对应的文件里,否则拒绝此次上传片断请求。
单个片段上传成功后,反向代理服务器返回http 201响应给deamon子进程,直到整个文件全部分段传完后返回http 200响应,失败返回错误响应。这样一个文件就算传输完成,同时deamon子进程删除本地数据库对应的状态纪录和文件,否则本地数据库纪录对应文件上传进度和状态,以便下一次扫描再传输。
当一个文件传输完成后,业务服务器负责将临时存储在反向代理服务器本地磁盘的文件,生成全局唯一的文件名作为key(语音以.mp3为后缀,笔迹以.pen为后缀),通过第三方云服务api上传到云存储里,成功后同时删除本地文件,否则保留。转到第5步返回状态给deamon子进程。
当文件传输到第三方云端服务器后,通知后台web服务器文件上传成功,后台服务纪录对应答疑订单号下的各个文件url到数据库中,同时转到第5步返回状态给deamon子进程。
将指定订单号下的语音和笔迹url传到定制化播放端,播放器解析笔迹数据,将相对于语音的相对时间戳与语音时间轴进行同步化匹配,并最终呈现出来。
2.2 关键技术
2.2.1 笔迹数据高效压缩
对于要在PC端、web端或app端能同时兼容播放渲染笔迹数据,同时为了保证浏览器跨域问题,设计了一种高效通用的笔迹数据包格式(jsonp格式),这样当浏览器读取笔迹文件后可以直接内部解析成json数据。如下图为笔迹数据格式。
其中:
hw_h表示笔迹数据头,hw_b表示笔迹数据体。
hw_h中:此字段存在的目的是为了在播放器进度条上明确标记老师的状态进行打点。比如跳过审题状态,直接到达开始讲解阶段。
s:表示老师的状态(0:审题阶段;1:开始讲解阶段;其他预留)。
t: 表示当前老师状态对应的点的相对时间戳(相对于音频开始时间戳(采集时间)的时间(毫秒))。
hw_b中:
t: 表示当前点相对于音频开始的相对时间戳(采集时间)的时间(毫秒)。
c: 表示笔记相关命令操作,按位操作。第1位:0表示笔书写,1表示笔悬浮;第2-4位:表示笔迹的颜色。
s: 笔迹的分页号。
x1、y1、x2、y2: 表示点的开始和结束。
2.2.2 语音和笔迹完整性录制
当PC老师端录制语音时,老师端和学生端分别经过回音消除、噪声抑制处理之后,对生成出来的PCM信号进行采样、压缩、并实时编码成mp3双声道数据。比如16KHZ 的PCM数据每10ms生成长度为160的采样包。如下图为老师和学生的PCM信号包对齐合并编码包。理论上老师和学生端的语音基本是同步的,如果对于通话过程中某一方语音信号丢失,或者某一端等待另一端PCM信号包超时,那么需要用对应长度的静音包来填充,避免出现语音不同步的情况。
如果PC老师端因为意外情况退出后再恢复时,会接着当前订单的语音继续录制。如下图为意外恢复情况下时间戳校对策略。假设时间戳单位是秒,PC老师端在第5秒的时候意外退出,在第20秒时恢复,在第26秒时答疑结束,那么此时整个答疑时间是11秒(即语音时长)。在第20秒恢复时,获取已经录制的第一阶段mp3时长(5秒),并且seek到最后一个sample位置接着写入语音sample,之后每个笔迹包的相对时间戳都需要减去断开阶段的时长继续录制。
2.2.3 高效定制化播放
设计了一种定制化的播放器,采用html5进行播放和渲染,笔迹(.pen)采用基于canvas的绘制,语音(.mp3)采用audio标签播放。如图9为播放器加载语音和笔迹的流程。
2.2.3.1 自适应分辨率策略
因为笔迹本上的分辨率和要显示播放设备的分辨率不一样,所以需要转换为要播放的设备窗口的分辨率。如图10为自适应选择目标窗口分辨率策略。
因为在手机端缩放时对点的本身分辨率会造成影响,这时需要记录scale 为window.devicePixelRatio,对于每个点坐标e(x,y)做等比例(因子r(x,y))缩放调整,即f(x,y)= e(x,y)r(x,y)scale。
2.2.3.2 绘制笔迹流程
对于笔迹每一页动态创建一个canvas来显示当前页号下所有的点线,通过页号来映射canvas。页面每次只会有一个canvas显示出来,其他的canvas隐藏。如果下一个点的页号和当前绘制的页号不同,则继续创建一个canvas和页号映射关联,并且切换为当前显示的canvas。出于效率考虑,每30个点线stroke一次。
2.2.3.3 seek进度条流程
对于手机端来说,电量和绘制效率是重点要考虑的,尤其对于canvas的seek绘图效率问题更应该重视,当一次性绘制超过上万个点线时,会暂用大量的临时内存,并且cpu瞬时达到峰值,这样会导致app意外退出。
如果在当前页之内seek:向后seek的话就seek到指定位置,绘出当前播放点到指定seek点内的所有当前页号的笔迹点线;如果向前seek则清空当前页号的canvas内的所有笔迹点线,绘出当前页号开始点到指定seek点内的所有当前页号的笔迹点线。
如果在当前页之外seek:清空当前canvas,绘制seek点位置对应的页号的开始点到seek点内的所有当前页号的笔迹点线,同时将seek点对应的页号映射的canvas设置为显示状态。