// ==UserScript==
// @name Jellyfin 沉浸式播放控制:Trickplay 预览 + 键盘/触控增强
// @namespace https://github.com/guiyuanyuanbao/Jellyfin-betterJellyfinWebPlayer-extension
// @version 1.4
// @description 为 Jellyfin 提供沉浸式播放控制体验:滑动进度时显示影片预览图 (Trickplay);键盘右键短按快进 10 秒 / 长按 2 倍速;移动端支持长按倍速、水平滑动调节进度、双击播放暂停;倍速显示呼吸灯动画,滑动显示时间预览和自定义进度条;智能 OSD 隐藏,打造无干扰观影环境。
// @author guiyuanyuanbao
// @license MIT
// @match *://*/*/web/index.html
// @match *://*/web/index.html
// @match *://*/*/web/
// @match *://*/web/
// @run-at document-idle
// @grant none
// @supportURL https://github.com/guiyuanyuanbao/Jellyfin-betterJellyfinWebPlayer-extension/issues
// @homepageURL https://github.com/guiyuanyuanbao/Jellyfin-betterJellyfinWebPlayer-extension
// ==/UserScript==
(function() {
'use strict';
console.log('[InjectScript] Jellyfin speed control script loaded');
const LONG_PRESS_MS = 300;
const SHORT_SEEK_S = 10;
let pressTimer = null;
let originalRate = 1;
let isFast = false;
let osdBottomElement = null; // 添加OSD元素引用
let headerElement = null; // 添加頭部元素引用
// --- Trickplay 相關變數 ---
let trickplayData = null;
let lastVideoSrc = null; // 用於追蹤影片來源是否變更
function getVideo() {
return document.querySelector('video');
}
// --- 全新改寫:獲取 Trickplay 資訊 (兼容直接播放和轉碼) ---
async function getTrickplayInfo() {
console.log('[Inject] 正在嘗試獲取 Trickplay 資訊...');
const vid = getVideo();
if (!vid) return null;
// --- 方案 A: 嘗試從 URL 直接解析 (適用於直接播放) ---
if (vid.src && !vid.src.startsWith('blob:')) {
try {
const url = new URL(vid.src);
const apiKey = url.searchParams.get('api_key');
const match = url.pathname.match(/\/Videos\/([a-f0-9]+)\//);
if (apiKey && match && match[1]) {
const info = {
itemId: match[1],
apiKey: apiKey,
baseUrl: `${url.protocol}//${url.host}`
};
console.log('[Inject] 成功從 URL 獲取資訊 (方案A):', info);
return info;
}
} catch (e) {
console.log('[Inject] 從 URL 解析失敗,將嘗試方案B。');
}
}
// --- 方案 B: 從 API 獲取 (適用於轉碼 'blob:' URL 或方案A失敗時) ---
console.log('[Inject] URL 為 blob 或解析失敗,正在啟動 API 方案 (方案B)...');
try {
if (typeof ApiClient === 'undefined') {
console.log('[Inject] 找不到 ApiClient。');
return null;
}
const apiKey = ApiClient.accessToken();
const baseUrl = ApiClient.serverAddress();
const deviceId = ApiClient.deviceId();
const sessionUrl = `${baseUrl}/Sessions`;
const response = await fetch(sessionUrl, {
headers: { 'X-Emby-Token': apiKey }
});
if (!response.ok) throw new Error(`API 請求失敗,狀態: ${response.status}`);
const sessions = await response.json();
// 精準查找與當前裝置 ID 匹配,並且正在播放內容的 session
const currentSession = sessions.find(s => s.DeviceId === deviceId && s.NowPlayingItem);
if (currentSession) {
const info = {
itemId: currentSession.NowPlayingItem.Id,
apiKey: apiKey,
baseUrl: baseUrl
};
console.log('[Inject] 成功從 Sessions API 獲取資訊 (方案B):', info);
return info;
}
} catch (error) {
console.error('[Inject] 透過 API 獲取資訊時出錯:', error);
return null;
}
console.log('[Inject] 方案A和B均失敗,無法獲取 Trickplay 資訊。');
return null;
}
// --- 獲取並解析 M3U8 檔案 ---
async function setupTrickplay() {
console.log('[Inject] 正在設定 Trickplay...');
// getTrickplayInfo 現在是異步的,需要 await
const trickplayInfo = await getTrickplayInfo();
if (!trickplayInfo) {
console.log('[Inject] 無法獲取 Trickplay 資訊,設定中止。');
trickplayData = null;
return;
}
const m3u8Url = `${trickplayInfo.baseUrl}/Videos/${trickplayInfo.itemId}/Trickplay/320/tiles.m3u8?api_key=${trickplayInfo.apiKey}`;
try {
const response = await fetch(m3u8Url);
if (!response.ok) throw new Error(`HTTP 錯誤!狀態: ${response.status}`);
const m3u8Text = await response.text();
const baseImageUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf('/') + 1);
console.log('[Inject] 推導出的圖片基礎路徑:', baseImageUrl);
const lines = m3u8Text.split('\n');
const tilesLine = lines.find(line => line.startsWith('#EXT-X-TILES'));
if (!tilesLine) throw new Error('在 M3U8 中找不到 #EXT-X-TILES 標籤。');
const resolutionMatch = tilesLine.match(/RESOLUTION=(\d+)x(\d+)/);
const layoutMatch = tilesLine.match(/LAYOUT=(\d+)x(\d+)/);
const durationMatch = tilesLine.match(/DURATION=([\d.]+)/);
if (!resolutionMatch || !layoutMatch || !durationMatch) {
throw new Error('無法解析 #EXT-X-TILES 標籤的屬性。');
}
const images = lines.filter(line => line.includes('.jpg'));
trickplayData = {
width: parseInt(resolutionMatch[1], 10),
height: parseInt(resolutionMatch[2], 10),
cols: parseInt(layoutMatch[1], 10),
rows: parseInt(layoutMatch[2], 10),
interval: parseFloat(durationMatch[1]),
images: images,
thumbsPerImage: parseInt(layoutMatch[1], 10) * parseInt(layoutMatch[2], 10),
baseImageUrl: baseImageUrl
};
console.log('[Inject] Trickplay 數據已成功載入:', trickplayData);
} catch (error) {
console.error('[Inject] 獲取或解析 Trickplay M3U8 失敗:', error);
trickplayData = null;
}
}
// --- 更新並顯示 Trickplay 預覽 ---
function updateTrickplay(time) {
if (!trickplayData || !getVideo()?.duration) return;
const previewEl = document.getElementById('trickplay-preview');
if (!previewEl) return;
const timePerImage = trickplayData.thumbsPerImage * trickplayData.interval;
const imageIndex = Math.floor(time / timePerImage);
const timeInImage = time % timePerImage;
const thumbIndex = Math.floor(timeInImage / trickplayData.interval);
if (imageIndex >= trickplayData.images.length) return;
const imageName = trickplayData.images[imageIndex];
const imageUrl = `${trickplayData.baseImageUrl}${imageName}`;
const col = thumbIndex % trickplayData.cols;
const row = Math.floor(thumbIndex / trickplayData.cols);
const bgPosX = -col * trickplayData.width;
const bgPosY = -row * trickplayData.height;
previewEl.style.width = `${trickplayData.width}px`;
previewEl.style.height = `${trickplayData.height}px`;
// 添加对于油猴的兼容性支持
let backgroundImage = "url(" + imageUrl + ")";
previewEl.style.backgroundImage = `${backgroundImage}`;
previewEl.style.backgroundPosition = `${bgPosX}px ${bgPosY}px`;
previewEl.style.display = 'block';
}
// --- 隱藏 Trickplay 預覽 ---
function hideTrickplay() {
const previewEl = document.getElementById('trickplay-preview');
if (previewEl) {
previewEl.style.display = 'none';
}
}
function injectStyles() {
const css = `
#speed-overlay {
border-radius: 8px;
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 4px 8px;
font-size: 12px;
display: none;
align-items: center;
z-index: 9999;
pointer-events: none;
}
@keyframes breathe {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
#speed-overlay .tri {
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 8px solid #fff;
margin-right: 4px;
animation: breathe 1.5s infinite ease-in-out;
}
#speed-overlay .tri:nth-child(2) {
animation-delay: 0.5s;
}
#speed-overlay .tri:nth-child(3) {
animation-delay: 1s;
}
#time-overlay {
border-radius: 12px;
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 8px 16px;
font-size: 16px;
font-weight: bold;
display: none;
z-index: 10001;
pointer-events: none;
text-align: center;
}
#custom-progress-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(130deg,#a95bc2,#00a4db);
z-index: 9997;
display: none;
pointer-events: none;
transition: none;
transform-origin: left center;
border-radius: 0 2px 0 0;
}
#custom-progress-bar::before {
content: '';
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.3);
z-index: -1;
}
#custom-progress-bar::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 100%;
background: rgba(255, 255, 255, 0.8);
border-radius: 0 2px 0 0;
}
#trickplay-preview {
position: fixed;
bottom: 20px;
left: 20px;
display: none;
border: 2px solid rgba(255, 255, 255, 0.7);
border-radius: 4px;
background-color: #000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
background-repeat: no-repeat;
z-index: 10000;
pointer-events: none;
}
/* 原生進度條樣式 - 與自訂進度條顏色保持一致 */
.mdl-slider-background-lower {
background: linear-gradient(130deg,#a95bc2,#00a4db) !important;
}
.osdPositionSlider::-webkit-slider-thumb {
background: #00a4db !important;
border: 2px solid #ffffff !important;
box-shadow: 0 0 6px rgba(0, 164, 219, 0.5) !important;
}
.osdPositionSlider::-moz-range-thumb {
background: #00a4db !important;
border: 2px solid #ffffff !important;
box-shadow: 0 0 6px rgba(0, 164, 219, 0.5) !important;
}
/* 圖示OSD進度條樣式 - 與其他進度條顏色保持一致 */
.iconOsdProgressInner {
background: linear-gradient(130deg,#a95bc2,#00a4db) !important;
}
.iconOsdProgressOuter {
background: rgba(255, 255, 255, 0.3) !important;
}
/* 腳本專屬的隱藏CSS類 */
.script-osd-hidden {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
}
/* MDL載入動畫漸變樣式 - 與進度條顏色保持一致 */
.mdl-spinner__circle {
border: none !important;
background: conic-gradient(
from 0deg,
#a95bc2 0deg,
#9456b5 60deg,
#7c6bc9 120deg,
#00a4db 180deg,
#a95bc2 240deg,
transparent 240deg,
transparent 360deg
) !important;
border-radius: 50% !important;
position: relative !important;
mask: radial-gradient(circle at center, transparent 44%, black 45%, black 47%, transparent 48%) !important;
-webkit-mask: radial-gradient(circle at center, transparent 44%, black 45%, black 47%, transparent 48%) !important;
filter: drop-shadow(0 0 3px rgba(169, 91, 194, 0.3)) !important;
}
.mdl-spinner__circle::before {
display: none !important;
}
.mdl-spinner__circleLeft,
.mdl-spinner__circleRight {
border: none !important;
background: transparent !important;
}
/* 移除單獨的層級樣式,使用統一的漸變圓環 */
.mdl-spinner__layer-1 .mdl-spinner__circle,
.mdl-spinner__layer-2 .mdl-spinner__circle,
.mdl-spinner__layer-3 .mdl-spinner__circle,
.mdl-spinner__layer-4 .mdl-spinner__circle {
border: none !important;
background: conic-gradient(
from 0deg,
#a95bc2 0deg,
#9456b5 60deg,
#7c6bc9 120deg,
#00a4db 180deg,
#a95bc2 240deg,
transparent 240deg,
transparent 360deg
) !important;
mask: radial-gradient(circle at center, transparent 44%, black 45%, black 47%, transparent 48%) !important;
-webkit-mask: radial-gradient(circle at center, transparent 44%, black 45%, black 47%, transparent 48%) !important;
filter: drop-shadow(0 0 3px rgba(169, 91, 194, 0.3)) !important;
}
/* 確保spinner圓形完整顯示 */
.mdl-spinner__circle-clipper .mdl-spinner__circle {
border: none !important;
background: conic-gradient(
from 0deg,
#a95bc2 0deg,
#9456b5 60deg,
#7c6bc9 120deg,
#00a4db 180deg,
#a95bc2 240deg,
transparent 240deg,
transparent 360deg
) !important;
border-radius: 50% !important;
mask: radial-gradient(circle at center, transparent 44%, black 45%, black 47%, transparent 48%) !important;
-webkit-mask: radial-gradient(circle at center, transparent 44%, black 45%, black 47%, transparent 48%) !important;
filter: drop-shadow(0 0 3px rgba(169, 91, 194, 0.3)) !important;
}
/* 增強動畫的流暢性 */
.mdl-spinner {
animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1) !important;
}
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}
function createOverlay(container) {
const ov = document.createElement('div');
ov.id = 'speed-overlay';
container.style.position = container.style.position || 'relative';
container.appendChild(ov);
const timeOv = document.createElement('div');
timeOv.id = 'time-overlay';
document.body.appendChild(timeOv);
const progressBar = document.createElement('div');
progressBar.id = 'custom-progress-bar';
document.body.appendChild(progressBar);
const trickplayPreview = document.createElement('div');
trickplayPreview.id = 'trickplay-preview';
document.body.appendChild(trickplayPreview);
return ov;
}
function showOverlay(rate) {
overlay.innerHTML = `
<span class="tri"></span>
<span class="tri"></span>
<span class="tri"></span>
<span class="text">倍速播放中 ×${rate.toFixed(1)}</span>
`;
overlay.style.display = 'flex';
}
function hideOverlay() {
overlay.style.display = 'none';
}
function showTimeOverlay(currentTime, duration) {
const timeOverlay = document.getElementById('time-overlay');
if (timeOverlay) {
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
timeOverlay.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
timeOverlay.style.display = 'block';
}
}
function hideTimeOverlay() {
const timeOverlay = document.getElementById('time-overlay');
if (timeOverlay) {
timeOverlay.style.display = 'none';
}
}
function hideOSDControls() {
if (osdBottomElement) {
if (!osdBottomElement.classList.contains('videoOsdBottom-hidden')) {
osdBottomElement.classList.add('videoOsdBottom-hidden');
}
if (!osdBottomElement.classList.contains('hide')) {
osdBottomElement.classList.add('hide');
}
if (!osdBottomElement.classList.contains('script-osd-hidden')) {
osdBottomElement.classList.add('script-osd-hidden');
}
}
if (headerElement) {
if (!headerElement.classList.contains('osdHeader-hidden')) {
headerElement.classList.add('osdHeader-hidden');
}
if (!headerElement.classList.contains('hide')) {
headerElement.classList.add('hide');
}
if (!headerElement.classList.contains('script-osd-hidden')) {
headerElement.classList.add('script-osd-hidden');
}
}
}
function showOSDControls() {
if (osdBottomElement) {
if (osdBottomElement.classList.contains('videoOsdBottom-hidden')) {
osdBottomElement.classList.remove('videoOsdBottom-hidden');
}
if (osdBottomElement.classList.contains('hide')) {
osdBottomElement.classList.remove('hide');
}
if (osdBottomElement.classList.contains('script-osd-hidden')) {
osdBottomElement.classList.remove('script-osd-hidden');
}
}
if (headerElement) {
if (headerElement.classList.contains('osdHeader-hidden')) {
headerElement.classList.remove('osdHeader-hidden');
}
if (headerElement.classList.contains('hide')) {
headerElement.classList.remove('hide');
}
if (headerElement.classList.contains('script-osd-hidden')) {
headerElement.classList.remove('script-osd-hidden');
}
}
}
function goFast() {
const vid = getVideo();
if (!vid || isFast) return;
originalRate = vid.playbackRate || 1;
const newRate = originalRate * 2;
vid.playbackRate = newRate;
isFast = true;
showOverlay(newRate);
}
function restore() {
const vid = getVideo();
if (!vid || !isFast) return;
vid.playbackRate = originalRate;
isFast = false;
hideOverlay();
}
function seekShort() {
const vid = getVideo();
if (!vid) return;
vid.currentTime = Math.min(vid.duration, vid.currentTime + SHORT_SEEK_S);
}
function findOSDElement() {
const view = document.querySelector('div[data-type="video-osd"]') || document;
osdBottomElement = view.querySelector('.videoOsdBottom-maincontrols');
headerElement = document.querySelector('.skinHeader');
}
function removePlayControlClasses() {
const playControlButtons = document.querySelector('.osdPlayControlButtons');
if (playControlButtons) {
playControlButtons.classList.remove('flex', 'align-items-center', 'flex-direction-row', 'osdPlayControlButtons');
}
}
function togglePlay() {
const vid = getVideo();
if (!vid) return;
if (vid.paused) {
vid.play();
findOSDElement();
hideOSDControls();
} else {
vid.pause();
findOSDElement();
showOSDControls();
}
}
function toggleOSDVisibility() {
findOSDElement();
if (osdBottomElement && osdBottomElement.classList.contains('script-osd-hidden')) {
showOSDControls();
} else {
hideOSDControls();
}
}
function isVideoOsdPageActive() {
const videoOsdPage = document.getElementById('videoOsdPage');
if (!videoOsdPage) return false;
const hasVideoOsdType = videoOsdPage.getAttribute('data-type') === 'video-osd';
const hasVideo = !!getVideo();
return hasVideoOsdType && hasVideo;
}
function isOSDControlElement(target) {
if (osdBottomElement && (osdBottomElement.contains(target) || target === osdBottomElement)) return true;
if (headerElement && (headerElement.contains(target) || target === headerElement)) return true;
// 选集插件兼容性支持
let episodeSidebarElement = document.querySelector('.episodeSidebar');
if (episodeSidebarElement && (episodeSidebarElement.contains(target) || target === episodeSidebarElement)) return true;
// 弹幕插件兼容性支持
let danmakuSidebarElement = document.querySelector('.danmakuSidebar');
if (danmakuSidebarElement && (danmakuSidebarElement.contains(target) || target === danmakuSidebarElement)) return true;
let danmakuSelectDialogElement = document.querySelector('.selectDialog');
if (danmakuSelectDialogElement && (danmakuSelectDialogElement.contains(target) || target === danmakuSelectDialogElement)) return true;
let danmakuInputDialogElement = document.querySelector('.inputDialog');
if (danmakuInputDialogElement && (danmakuInputDialogElement.contains(target) || target === danmakuInputDialogElement)) return true;
const controlSelectors = ['.btnPause', '.btnPlay', '.btnStop', '.btnNext', '.btnPrevious', '.osdPositionSlider', '.mdl-slider', '.volumeSlider', '.btnVolume', '.btnMute', '.btnSubtitles', '.btnAudio', '.btnFullscreen', '.btnExitFullscreen', '.btnSettings', '.osdPlayControlButtons', '.videoOsdBottom-maincontrols', '#debugInfo', '.debug-close-btn', 'button', 'input[type="range"]', '.slider', '[role="button"]'];
for (const selector of controlSelectors) {
if (target.closest(selector)) return true;
}
return false;
}
function updateCustomProgressBar(currentTime, duration) {
const customProgressBar = document.getElementById('custom-progress-bar');
if (customProgressBar && duration > 0) {
const percentage = (currentTime / duration) * 100;
customProgressBar.style.width = percentage + '%';
}
}
function showCustomProgressBar() {
const customProgressBar = document.getElementById('custom-progress-bar');
if (customProgressBar) customProgressBar.style.display = 'block';
findOSDElement();
hideOSDControls();
}
function hideCustomProgressBar() {
const customProgressBar = document.getElementById('custom-progress-bar');
if (customProgressBar) customProgressBar.style.display = 'none';
}
function init() {
console.log('[Inject] init handlers');
const container = document.querySelector('#root') || document.body;
injectStyles();
window.overlay = createOverlay(container);
findOSDElement();
removePlayControlClasses();
const observer = new MutationObserver(() => {
removePlayControlClasses();
});
observer.observe(document.body, { childList: true, subtree: true });
let lastTapTime = 0;
const doubleTapDelay = 500;
let startX = 0, startY = 0, startVideoTime = 0, startTarget = null, isLongPressing = false, isSliding = false, slideThreshold = 20, slideTimer = null, previewTime = 0;
let keyActive = false;
container.addEventListener('keydown', e => {
if (e.code === 'ArrowRight') {
if (!keyActive) {
keyActive = true;
pressTimer = setTimeout(goFast, LONG_PRESS_MS);
}
e.preventDefault(); e.stopImmediatePropagation();
}
}, true);
container.addEventListener('keyup', e => {
if (e.code === 'ArrowRight') {
clearTimeout(pressTimer);
if (!isFast) seekShort(); else restore();
keyActive = false;
e.preventDefault(); e.stopImmediatePropagation();
}
}, true);
container.addEventListener('touchstart', e => {
if (!isVideoOsdPageActive()) return;
const vid = getVideo();
if (vid && vid.src && vid.src !== lastVideoSrc) {
lastVideoSrc = vid.src;
trickplayData = null;
setTimeout(setupTrickplay, 500);
}
startTarget = e.target;
if (isOSDControlElement(startTarget)) return;
if (e.touches.length === 1) {
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
startVideoTime = vid ? vid.currentTime : 0;
isLongPressing = false;
isSliding = false;
pressTimer = setTimeout(() => {
findOSDElement();
goFast();
hideOSDControls();
isLongPressing = true;
}, LONG_PRESS_MS);
}
});
container.addEventListener('touchmove', e => {
if (!isVideoOsdPageActive() || (startTarget && isOSDControlElement(startTarget))) return;
if (e.touches.length === 1 && !isLongPressing) {
const touch = e.touches[0];
const deltaX = touch.clientX - startX;
const deltaY = touch.clientY - startY;
if (Math.abs(deltaX) > slideThreshold && Math.abs(deltaX) > Math.abs(deltaY)) {
if (!isSliding) {
clearTimeout(pressTimer);
isSliding = true;
showCustomProgressBar();
}
findOSDElement();
hideOSDControls();
const vid = getVideo();
if (vid && vid.duration) {
const timeChange = (deltaX / 100) * 10;
previewTime = Math.max(0, Math.min(vid.duration, startVideoTime + timeChange));
showTimeOverlay(previewTime, vid.duration);
updateCustomProgressBar(previewTime, vid.duration);
updateTrickplay(previewTime);
clearTimeout(slideTimer);
}
e.preventDefault();
}
}
});
container.addEventListener('touchend', (e) => {
if (!isVideoOsdPageActive()) return;
clearTimeout(pressTimer);
if (isSliding) {
hideTrickplay();
const vid = getVideo();
if (vid) vid.currentTime = previewTime;
slideTimer = setTimeout(() => {
hideTimeOverlay();
hideCustomProgressBar();
findOSDElement();
hideOSDControls();
}, 1000);
isSliding = false;
return;
}
if (isLongPressing) {
restore();
findOSDElement();
hideOSDControls();
isLongPressing = false;
return;
}
const currentTime = new Date().getTime();
const tapLength = currentTime - lastTapTime;
if (tapLength < doubleTapDelay && tapLength > 0) {
if (!isOSDControlElement(startTarget)) togglePlay();
e.preventDefault();
} else {
if (!isFast) {
if (!isOSDControlElement(startTarget)) toggleOSDVisibility();
} else {
restore();
findOSDElement();
hideOSDControls();
}
}
lastTapTime = currentTime;
});
container.addEventListener('touchcancel', () => {
if (!isVideoOsdPageActive()) return;
clearTimeout(pressTimer);
clearTimeout(slideTimer);
if (isLongPressing) {
restore();
isLongPressing = false;
}
if (isSliding) {
hideTimeOverlay();
hideCustomProgressBar();
hideTrickplay();
isSliding = false;
}
});
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();