283 lines
9.9 KiB
JavaScript
283 lines
9.9 KiB
JavaScript
/**
|
|
* AKSRT Video Player
|
|
*
|
|
* Lightweight HTML5 video player wrapper with support for:
|
|
* - Standard MP4/WebM playback
|
|
* - HLS streaming (lazy-loaded)
|
|
* - FLV streaming (lazy-loaded)
|
|
* - Auto-quality selection
|
|
* - Keyboard shortcuts
|
|
*
|
|
* Usage:
|
|
* <div class="aksrt-video" data-aksrt-video data-src="video.mp4" data-poster="thumb.jpg"></div>
|
|
* <div class="aksrt-video" data-aksrt-video data-src="stream.m3u8" data-type="hls"></div>
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var STYLE_ID = 'aksrt-video-player-style';
|
|
var HLS_SRC = 'https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js';
|
|
var FLV_SRC = 'https://cdn.jsdelivr.net/npm/flv.js@1/dist/flv.min.js';
|
|
|
|
function injectStyle() {
|
|
if (document.getElementById(STYLE_ID)) return;
|
|
var style = document.createElement('style');
|
|
style.id = STYLE_ID;
|
|
style.textContent =
|
|
'.aksrt-video{position:relative;background:#000;border-radius:12px;overflow:hidden;cursor:pointer;}' +
|
|
// 组件容器(文章封面比例盒)有确定高度:视频铺满并居中裁切,对齐单图封面;
|
|
// 容器无确定高度时 height:100% 退化为 auto,行为与原 height:auto 一致。
|
|
'.aksrt-video video{display:block;width:100%;height:100%;object-fit:cover;}' +
|
|
'.aksrt-video__overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none;}' +
|
|
'.aksrt-video.is-playing .aksrt-video__overlay{opacity:0;}' +
|
|
'.aksrt-video__play{width:60px;height:60px;border-radius:50%;background:rgba(255,255,255,0.9);display:flex;align-items:center;justify-content:center;font-size:24px;color:#1a1a2e;box-shadow:0 4px 20px rgba(0,0,0,0.3);transition:transform 0.2s;}' +
|
|
'.aksrt-video:hover .aksrt-video__play{transform:scale(1.1);}' +
|
|
'.aksrt-video__badge{position:absolute;top:8px;right:8px;background:rgba(0,0,0,0.6);color:#fff;font-size:11px;padding:2px 8px;border-radius:4px;backdrop-filter:blur(4px);}' +
|
|
'.aksrt-video.is-playing .aksrt-video__badge{display:none;}';
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function detectType(src) {
|
|
if (!src) return 'mp4';
|
|
if (/\.m3u8(\?|$)/i.test(src)) return 'hls';
|
|
if (/\.flv(\?|$)/i.test(src)) return 'flv';
|
|
return 'mp4';
|
|
}
|
|
|
|
function loadScript(src) {
|
|
return new Promise(function (resolve, reject) {
|
|
var script = document.createElement('script');
|
|
script.src = src;
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
function AksrtVideoPlayer(element) {
|
|
this.el = typeof element === 'string' ? document.querySelector(element) : element;
|
|
if (!this.el || this.el.dataset.aksrtVideoBound) return;
|
|
this.el.dataset.aksrtVideoBound = '1';
|
|
this.el._aksrtPlayer = this;
|
|
|
|
this.src = this.el.dataset.src || '';
|
|
this.poster = this.el.dataset.poster || '';
|
|
this.type = this.el.dataset.type || detectType(this.src);
|
|
this.autoplay = this.el.dataset.autoplay === 'true';
|
|
this.loop = this.el.dataset.loop === 'true';
|
|
this.muted = this.el.dataset.muted === 'true' || this.autoplay;
|
|
this.init();
|
|
}
|
|
|
|
AksrtVideoPlayer.prototype.init = function () {
|
|
var self = this;
|
|
|
|
// Create video element
|
|
this.video = document.createElement('video');
|
|
this.video.controls = true;
|
|
this.video.playsInline = true;
|
|
this.video.preload = this.autoplay ? 'auto' : 'metadata';
|
|
if (this.loop) this.video.loop = true;
|
|
if (this.muted) this.video.muted = true;
|
|
if (this.poster) this.video.poster = this.poster;
|
|
this.video.style.cssText = 'display:block;width:100%;height:100%;object-fit:contain;max-height:none;';
|
|
|
|
// Play overlay
|
|
this.overlay = document.createElement('div');
|
|
this.overlay.className = 'aksrt-video__overlay';
|
|
this.overlay.innerHTML = '<div class="aksrt-video__play"><i class="fa fas fa-play" style="margin-left:3px;"></i></div>';
|
|
|
|
// Badge
|
|
if (this.type === 'hls') {
|
|
var badge = document.createElement('div');
|
|
badge.className = 'aksrt-video__badge';
|
|
badge.textContent = 'HLS';
|
|
this.el.appendChild(badge);
|
|
}
|
|
|
|
this.el.appendChild(this.video);
|
|
this.el.appendChild(this.overlay);
|
|
|
|
// Events
|
|
this.el.addEventListener('click', function (e) {
|
|
if (e.target.closest('video') && self.video.controls) return;
|
|
self.toggle();
|
|
});
|
|
|
|
this.video.addEventListener('play', function () {
|
|
self.el.classList.add('is-playing');
|
|
});
|
|
|
|
this.video.addEventListener('pause', function () {
|
|
self.el.classList.remove('is-playing');
|
|
});
|
|
|
|
this.video.addEventListener('ended', function () {
|
|
self.el.classList.remove('is-playing');
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
this.video.addEventListener('keydown', function (e) {
|
|
switch (e.key) {
|
|
case ' ': e.preventDefault(); self.toggle(); break;
|
|
case 'ArrowLeft': self.video.currentTime -= 5; break;
|
|
case 'ArrowRight': self.video.currentTime += 5; break;
|
|
case 'f': self.toggleFullscreen(); break;
|
|
case 'm': self.video.muted = !self.video.muted; break;
|
|
}
|
|
});
|
|
|
|
// Load source
|
|
this.loadSource();
|
|
};
|
|
|
|
AksrtVideoPlayer.prototype.loadSource = function () {
|
|
var self = this;
|
|
|
|
if (this.type === 'hls') {
|
|
if (window.Hls && Hls.isSupported()) {
|
|
this.hls = new Hls();
|
|
this.hls.loadSource(this.src);
|
|
this.hls.attachMedia(this.video);
|
|
} else if (this.video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
// Safari native HLS
|
|
this.video.src = this.src;
|
|
} else {
|
|
// Load hls.js dynamically
|
|
loadScript(HLS_SRC).then(function () {
|
|
if (window.Hls && Hls.isSupported()) {
|
|
self.hls = new Hls();
|
|
self.hls.loadSource(self.src);
|
|
self.hls.attachMedia(self.video);
|
|
}
|
|
}).catch(function () { /* CDN unreachable: leave the player sourceless */ });
|
|
}
|
|
} else if (this.type === 'flv') {
|
|
loadScript(FLV_SRC).then(function () {
|
|
if (window.flvjs && flvjs.isSupported()) {
|
|
self.flvPlayer = flvjs.createPlayer({ type: 'flv', url: self.src });
|
|
self.flvPlayer.attachMediaElement(self.video);
|
|
self.flvPlayer.load();
|
|
}
|
|
}).catch(function () { /* CDN unreachable: leave the player sourceless */ });
|
|
} else {
|
|
this.video.src = this.src;
|
|
}
|
|
|
|
if (this.autoplay) {
|
|
this.video.play().catch(function () { /* autoplay blocked */ });
|
|
}
|
|
};
|
|
|
|
AksrtVideoPlayer.prototype.hasSource = function () {
|
|
return Boolean(this.video.currentSrc || this.video.src || this.hls || this.flvPlayer);
|
|
};
|
|
|
|
AksrtVideoPlayer.prototype.toggle = function () {
|
|
if (!this.hasSource()) return;
|
|
if (this.video.paused) {
|
|
this.video.play().catch(function () { /* source failed or autoplay blocked */ });
|
|
} else {
|
|
this.video.pause();
|
|
}
|
|
};
|
|
|
|
AksrtVideoPlayer.prototype.toggleFullscreen = function () {
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen();
|
|
} else if (this.el.requestFullscreen) {
|
|
this.el.requestFullscreen();
|
|
}
|
|
};
|
|
|
|
AksrtVideoPlayer.prototype.destroy = function () {
|
|
if (this.hls) { this.hls.destroy(); this.hls = null; }
|
|
if (this.flvPlayer) { this.flvPlayer.destroy(); this.flvPlayer = null; }
|
|
this.video.src = '';
|
|
this.el.innerHTML = '';
|
|
delete this.el.dataset.aksrtVideoBound;
|
|
};
|
|
|
|
/**
|
|
* 切换视频源(剧集切换)
|
|
* @param {string} src - 新视频地址
|
|
* @param {string} [type] - 可选类型 (mp4/hls/flv)
|
|
* @param {string} [poster] - 可选封面
|
|
*/
|
|
AksrtVideoPlayer.prototype.switchSource = function (src, type, poster) {
|
|
if (!src) return;
|
|
|
|
// 清理旧播放器
|
|
if (this.hls) { this.hls.destroy(); this.hls = null; }
|
|
if (this.flvPlayer) { this.flvPlayer.destroy(); this.flvPlayer = null; }
|
|
|
|
this.src = src;
|
|
this.type = type || detectType(src);
|
|
if (poster) {
|
|
this.poster = poster;
|
|
this.video.poster = poster;
|
|
}
|
|
|
|
this.video.src = '';
|
|
this.loadSource();
|
|
};
|
|
|
|
// ── 剧集切换按钮事件委托(iframe 剧集由 runtime main 的 .switch-iframe 链路处理)──
|
|
document.addEventListener('click', function (e) {
|
|
var switchBtn = e.target.closest('.switch-video');
|
|
if (!switchBtn) return;
|
|
|
|
var container = switchBtn.closest('.aksrt-video') || switchBtn.closest('[data-aksrt-video]');
|
|
// Single cover: episodes sit in .single-video-footer; player is sibling under .single-video
|
|
if (!container) {
|
|
var singleVideo = switchBtn.closest('.single-video, .single-head-cover');
|
|
if (singleVideo) {
|
|
container = singleVideo.querySelector('.aksrt-video, [data-aksrt-video]');
|
|
}
|
|
}
|
|
if (!container) return;
|
|
|
|
var src = switchBtn.dataset.src || switchBtn.getAttribute('video-url') || switchBtn.getAttribute('href');
|
|
if (!src || !/^https?:/i.test(src)) return;
|
|
|
|
e.preventDefault();
|
|
|
|
// 检查是否已有播放器实例
|
|
if (container._aksrtPlayer) {
|
|
container._aksrtPlayer.switchSource(src, switchBtn.dataset.type);
|
|
} else {
|
|
container.dataset.src = src;
|
|
if (switchBtn.dataset.type) container.dataset.type = switchBtn.dataset.type;
|
|
new AksrtVideoPlayer(container);
|
|
}
|
|
|
|
// 高亮当前集
|
|
var scope = switchBtn.closest('.featured-video-episode') || (container.parentElement || document);
|
|
var siblings = scope.querySelectorAll('.switch-video');
|
|
siblings.forEach(function (btn) {
|
|
btn.classList.remove('is-active');
|
|
btn.classList.remove('active');
|
|
});
|
|
switchBtn.classList.add('is-active');
|
|
switchBtn.classList.add('active');
|
|
});
|
|
|
|
// ── Public API ──
|
|
|
|
window.AksrtVideoPlayer = AksrtVideoPlayer;
|
|
|
|
// ── Auto-init ──
|
|
|
|
function init() {
|
|
injectStyle();
|
|
document.querySelectorAll('[data-aksrt-video]').forEach(function (el) {
|
|
new AksrtVideoPlayer(el);
|
|
});
|
|
}
|
|
|
|
jQuery(document).off('auto_fun.aksrt-video-player').on('auto_fun.aksrt-video-player', function () { init(document); });
|
|
})();
|