背景
自HEVC等视频协议提出“基于块”(tile-based)的视频格式概念后,在360度视频中基于视点追踪及预测进行多码率分配的方法成为了现实。由于一个完整的360度视频可以轻松达到12K的视频分辨率,大多数视频播放器依赖视点预测算法来估计未来用户的头部运动趋势。如果能成功预测到视点即将经过的下一个位置,播放器就能优先满足该位置所在视频块的视频质量,从而降低了360度视频对网络带宽的需求。
提出方法
在验证平台中,由于整体框架使用DASH协议进行流媒体传输,基于tile的播放模式容易引起画面断层。因此,我们使用多播放器联合的模式,并添加catchup机制来控制各tile同步播放。
【我用的是dash.js v3.2.1,作者只实现了低延迟直播模式下使用catchup机制来维持两个播放器播放同步,之前有发起issue和作者沟通,在确认了没有实现点播模式下多播放器时间同步之后,我决定直接加一个支持点播模式的catchup机制。】
以tile = 6的CMP格式360度视频传输为例,平台首先初始化六个基于dash.js的Mediaplayer,每个Mediaplayer对应一个tile空间,各Mediaplayer按照文件服务器的视频块索引文件进行流媒体数据请求,并通过A-Frame框架进行3D联合渲染执行播放。
以下为catchup机制:平台实时观测各Mediaplayer的播放时间进度,并始终选取播放进度最快的Mediaplayer作为标准播放进度,其他Mediaplayer根据偏移阈值判断是否执行“追赶”;若确认执行,则该Mediaplayer将立刻执行倍速播放,直到与标准播放进度的偏移量小于偏移阈值。通过catchup机制,目前平台上基于tile播放360度视频可以保证各tile进度偏移量在0.02s上下浮动,主观上基本不影响观看体验。
代码实现
以下代码是在dash.js v3.2.1版本的基础上修改的,只提供playbackController.js中的关键修改部分供大家参考,具体还要考虑怎么和平台适配以及各种数据怎么调用等等。
// Change: [PlaybackController.js]
PlaybackController.onPlaybackProgression() { // Consider the situation whatever isDynamic is
if (
//isDynamic &&
_isCatchupEnabled() &&
settings.get().streaming.liveCatchup.playbackRate > 0 &&
!isPaused() &&
!isSeeking()
) {
if (_needToCatchUp()) { // Judge if it needs to catch up according to current live latency
if ($scope !== undefined && $scope.playerCatchUp !== undefined) {
$scope.playerCatchUp[settings.get().count] = true; // $scope contains global variables, $scope.playerCatchUp is for caching catchup state, settings.get().count is for locating which tile is operating
}
startPlaybackCatchUp(); // Begin catching up
} else {
if ($scope !== undefined && $scope.playerCatchUp !== undefined) {
$scope.playerCatchUp[settings.get().count] = false;
}
stopPlaybackCatchUp(); // Stop catching up when the divation is less than the threshold
}
}
};
// ...
PlaybackController.getCurrentLiveLatency() { // Change totally with compatibility for non-isDynamic
if (isNaN(availabilityStartTime)) {
return NaN;
}
let currentTime = getNormalizedTime();
if (isNaN(currentTime) || currentTime === 0) {
return 0;
}
if (!isDynamic && $scope !== undefined && $scope.normalizedTime !== undefined) { // Run this when non-isDynamic
const now = $scope.normalizedTime * 1000 + timelineConverter.getClientTimeOffset() * 1000;
return Math.max(((now - availabilityStartTime - currentTime * 1000) / 1000).toFixed(3), 0); // Return the divation between current tile's timeline and the normalized timeline
}
const now = new Date().getTime() + timelineConverter.getClientTimeOffset() * 1000; // Run this when isDynamic
return Math.max(((now - availabilityStartTime - currentTime * 1000) / 1000).toFixed(3), 0);
};