Files
web_huangguo_web/app/static/js/pages/play.js
T
2026-09-23 16:50:34 +07:00

1197 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
function isHttpOrHttps(url) {
return /^https?:\/\//i.test(url || "");
}
// 与 1028-91ly lieyanDetail 一致:
// - 绝对地址(http/https)直接播放
// - 相对路径走同域 /h5/m3u8/ 代理(含 ?ticket= 等查询参数)
function resolvePlayUrl(videoUrl) {
if (!videoUrl) return "";
var raw = String(videoUrl).trim();
if (!raw) return "";
if (isHttpOrHttps(raw)) return raw;
var path = raw.replace(/^\/+/, "");
if (path.indexOf("h5/m3u8/") === 0) {
return "/" + path;
}
return "/h5/m3u8/" + path;
}
function supportsH265Playback() {
var video = document.createElement("video");
if (!video || typeof video.canPlayType !== "function") {
return false;
}
var mimeTypes = [
'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/mp4; codecs="hvc1.1.6.L93.B0"',
'application/vnd.apple.mpegurl; codecs="hev1.1.6.L93.B0"',
'application/vnd.apple.mpegurl; codecs="hvc1.1.6.L93.B0"',
'application/x-mpegURL; codecs="hev1.1.6.L93.B0"',
'application/x-mpegURL; codecs="hvc1.1.6.L93.B0"',
];
for (var i = 0; i < mimeTypes.length; i += 1) {
var result = video.canPlayType(mimeTypes[i]);
if (result === "probably" || result === "maybe") {
return true;
}
}
if (
window.MediaSource &&
typeof window.MediaSource.isTypeSupported === "function"
) {
return (
window.MediaSource.isTypeSupported(
'video/mp4; codecs="hev1.1.6.L93.B0"'
) ||
window.MediaSource.isTypeSupported(
'video/mp4; codecs="hvc1.1.6.L93.B0"'
)
);
}
return false;
}
function choosePlaybackSource(defaultUrl, h265Url, canPlayH265) {
var normalizedDefaultUrl = resolvePlayUrl(defaultUrl);
var normalizedH265Url = resolvePlayUrl(h265Url);
var useH265 = !!(canPlayH265 && normalizedH265Url);
return {
defaultUrl: normalizedDefaultUrl,
h265Url: normalizedH265Url,
initialUrl: useH265 ? normalizedH265Url : normalizedDefaultUrl,
usingH265: useH265,
};
}
var HLS_CONFIG = {
maxBufferLength: 30,
maxMaxBufferLength: 60,
liveSyncDurationCount: 3,
fragLoadingTimeOut: 20000,
manifestLoadingTimeOut: 10000,
};
function isAppleNativeHlsBrowser() {
var nav = window.navigator || {};
var ua = nav.userAgent || "";
var isIOS =
/iPhone|iPad|iPod/i.test(ua) ||
(/Macintosh/i.test(ua) && Number(nav.maxTouchPoints) > 1);
var isSafari =
/Safari/i.test(ua) &&
!/Chrome|Chromium|CriOS|FxiOS|EdgiOS|OPiOS|Android/i.test(ua);
if (!isIOS && !isSafari) return false;
var video = document.createElement("video");
return !!(
video &&
typeof video.canPlayType === "function" &&
(video.canPlayType("application/vnd.apple.mpegurl") ||
video.canPlayType("application/x-mpegURL"))
);
}
function canUseHlsJs() {
// Safari 已有稳定的原生 HLS。若同时让 Hls.js 通过 MSE 接管,
// DPlayer 切源时先写入 video.src,WebKit 会启动原生加载器,随后
// Hls.js 又 attach MediaSource,两套加载器竞争会导致切集卡死。
if (isAppleNativeHlsBrowser()) return false;
return (
typeof window.Hls === "function" &&
typeof window.Hls.isSupported === "function" &&
window.Hls.isSupported()
);
}
function attachHlsJs(video, player) {
if (!canUseHlsJs()) return;
if (
player.plugins &&
player.plugins.hls &&
typeof player.plugins.hls.destroy === "function"
) {
player.plugins.hls.destroy();
}
var hls = new window.Hls(HLS_CONFIG);
player.plugins = player.plugins || {};
player.plugins.hls = hls;
hls.loadSource(video.src);
hls.attachMedia(video);
}
function buildVideoOption(url, pic) {
var option = {
url: url,
type: "hls",
};
if (canUseHlsJs()) {
// Chrome 会把原生 HLS 能力报告为 "maybe",但实际无法播放部分
// AES-128 HLS;使用自定义类型可绕过 DPlayer 的原生能力误判。
option.type = "hlsjs";
option.customType = {
hlsjs: attachHlsJs,
};
}
if (pic) option.pic = pic;
return option;
}
// Safari 的原生 HLS 在同一个 video 元素上直接改 src 时,旧资源的
// MediaPlayer 状态有概率不会释放,后续 play() 会一直停在 loading。
// DPlayer.switchVideo() 没有 remove src + load 的清理步骤,因此切集前
// 先显式终止旧资源,切换后再让 WebKit 重新装载新的 manifest。
function switchPlayerVideo(dp, videoOption) {
if (!dp || typeof dp.switchVideo !== "function") return;
var video = getVideoElement(dp);
var usesNativeHls = videoOption && videoOption.type === "hls";
if (isAppleNativeHlsBrowser() && usesNativeHls && video) {
try {
video.pause();
video.removeAttribute("src");
video.load();
} catch (e) {}
}
dp.switchVideo(videoOption);
if (isAppleNativeHlsBrowser() && usesNativeHls && video) {
try {
video.load();
} catch (e2) {}
}
}
function resumePlayerAfterSwitch(dp) {
var video = getVideoElement(dp);
if (!video) return;
var settled = false;
function showManualPlayButton() {
var poster = document.querySelector("[data-play-poster]");
var playBtn = document.querySelector("[data-play-poster-play]");
if (poster) poster.classList.remove("play-player-cover--hidden");
if (playBtn) playBtn.style.display = "";
}
function attemptPlay() {
if (settled || video !== getVideoElement(dp)) return;
var result;
try {
// 直接调用原生 play(),让 DPlayer 通过 video 的 play 事件同步
// UI;也避免 DPlayer 吞掉 iOS 的 Promise 拒绝原因。
result = video.play();
} catch (e) {
showManualPlayButton();
return;
}
if (result && typeof result.then === "function") {
result
.then(function () {
settled = true;
hidePosterOverlay();
})
.catch(function (err) {
// canplay 前的 AbortError 属于切源过程,事件到达后再试;
// NotAllowedError 则保留可点击播放入口,避免页面看似卡死。
if (err && err.name === "NotAllowedError") {
settled = true;
showManualPlayButton();
}
});
} else if (!video.paused) {
settled = true;
hidePosterOverlay();
}
}
video.addEventListener("loadedmetadata", attemptPlay, { once: true });
video.addEventListener("canplay", attemptPlay, { once: true });
attemptPlay();
}
function bindPlayerPoster(dp, player) {
var poster = player && player.querySelector("[data-play-poster]");
var playBtn = player && player.querySelector("[data-play-poster-play]");
if (!dp || typeof dp.on !== "function") {
return;
}
function hideOverlay() {
if (poster) {
poster.classList.add("play-player-cover--hidden");
}
if (playBtn) {
playBtn.style.display = "none";
}
}
dp.on("play", hideOverlay);
if (playBtn) {
playBtn.addEventListener("click", function () {
dp.play();
});
}
}
function getVideoElement(dp) {
if (dp && dp.video && typeof dp.video.addEventListener === "function") {
return dp.video;
}
return null;
}
function bindH265Fallback(dp, source) {
if (!dp || !source || !source.usingH265 || !source.defaultUrl) {
return;
}
var downgraded = false;
var videoEl = getVideoElement(dp);
function downgrade(reason) {
if (downgraded) return;
downgraded = true;
try {
dp.switchVideo(
buildVideoOption(
source.defaultUrl,
dp.video && dp.video.poster ? dp.video.poster : ""
)
);
} catch (e) {
if (videoEl) {
videoEl.src = source.defaultUrl;
if (typeof videoEl.load === "function") {
videoEl.load();
}
}
}
}
if (typeof dp.on === "function") {
dp.on("error", function (event) {
downgrade(event && event.type ? event.type : "dplayer-error");
});
}
if (videoEl) {
videoEl.addEventListener("error", function () {
var mediaError = videoEl.error;
var reason =
mediaError && mediaError.code
? "media-error-" + mediaError.code
: "media-error";
downgrade(reason);
});
}
if (
dp.plugins &&
dp.plugins.hls &&
window.Hls &&
window.Hls.Events &&
window.Hls.Events.ERROR
) {
dp.plugins.hls.on(window.Hls.Events.ERROR, function (_, data) {
if (data && data.fatal) {
downgrade(data.details || data.type || "hls-fatal-error");
}
});
}
}
var playDp = null;
var PLAY_SPEED_RATES = [0.75, 1, 1.25, 1.5, 2, 3];
var PLAY_SPEED_DEFAULT = 1;
var playSpeedIndex = PLAY_SPEED_RATES.indexOf(PLAY_SPEED_DEFAULT);
if (playSpeedIndex < 0) playSpeedIndex = 1;
function formatPlaySpeed(rate) {
var n = Number(rate);
if (!n || Number.isNaN(n)) n = 1;
if (Number.isInteger(n)) return n.toFixed(1) + "x";
return String(n) + "x";
}
function currentPlaySpeed() {
return PLAY_SPEED_RATES[playSpeedIndex] || PLAY_SPEED_DEFAULT;
}
function applyPlaySpeed(dp, rate) {
if (!dp) return;
var value = Number(rate) || PLAY_SPEED_DEFAULT;
try {
if (typeof dp.speed === "function") {
dp.speed(value);
}
} catch (e) {}
try {
if (dp.video) {
dp.video.playbackRate = value;
}
} catch (e2) {}
var speedLabel = document.querySelector("[data-play-speed-label]");
if (speedLabel) speedLabel.textContent = formatPlaySpeed(value);
syncSpeedMenuActive();
}
function syncSpeedMenuActive() {
var menu = document.querySelector("[data-play-speed-menu]");
if (!menu) return;
var current = currentPlaySpeed();
menu.querySelectorAll("[data-play-speed-option]").forEach(function (btn) {
var rate = Number(btn.getAttribute("data-play-speed-option"));
var active = rate === current;
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-selected", active ? "true" : "false");
var check = btn.querySelector("[data-play-speed-check]");
if (check) check.hidden = !active;
});
}
function setSpeedSheetOpen(open) {
var btn = document.querySelector("[data-play-speed]");
if (open) {
document.body.classList.remove("play-ep-sheet-open");
document.body.classList.remove("play-comment-sheet-open");
document.body.classList.add("play-speed-sheet-open");
if (btn) btn.setAttribute("aria-expanded", "true");
syncSpeedMenuActive();
} else {
document.body.classList.remove("play-speed-sheet-open");
if (btn) btn.setAttribute("aria-expanded", "false");
}
}
function bindSpeedMenu(dp) {
var btn = document.querySelector("[data-play-speed]");
var menu = document.querySelector("[data-play-speed-menu]");
var sheet = document.querySelector("[data-play-speed-sheet]");
if (!btn || !menu) return;
if (!menu.getAttribute("data-speed-menu-ready")) {
menu.setAttribute("data-speed-menu-ready", "1");
var displayRates = PLAY_SPEED_RATES.slice().reverse();
menu.innerHTML = displayRates
.map(function (rate) {
var index = PLAY_SPEED_RATES.indexOf(rate);
var label = formatPlaySpeed(rate);
if (rate === PLAY_SPEED_DEFAULT) {
label += "(默认)";
}
return (
'<button class="play-speed-sheet-option" type="button" role="option" data-play-speed-option="' +
rate +
'" data-play-speed-index="' +
index +
'">' +
'<span class="play-speed-sheet-option-label">' +
label +
"</span>" +
'<img class="play-speed-sheet-check" src="/static/images/check_pink.svg" alt="已选择" data-play-speed-check hidden />' +
"</button>"
);
})
.join("");
}
if (!btn.getAttribute("data-speed-bound")) {
btn.setAttribute("data-speed-bound", "1");
btn.addEventListener("click", function (e) {
e.stopPropagation();
var open = !document.body.classList.contains("play-speed-sheet-open");
setSpeedSheetOpen(open);
});
menu.addEventListener("click", function (e) {
var option =
e.target && e.target.closest
? e.target.closest("[data-play-speed-option]")
: null;
if (!option || !menu.contains(option)) return;
e.stopPropagation();
var index = Number(option.getAttribute("data-play-speed-index"));
if (Number.isNaN(index) || index < 0 || index >= PLAY_SPEED_RATES.length) {
return;
}
playSpeedIndex = index;
applyPlaySpeed(playDp || dp, currentPlaySpeed());
setSpeedSheetOpen(false);
});
if (sheet) {
sheet.querySelectorAll("[data-play-speed-close]").forEach(function (closeBtn) {
closeBtn.addEventListener("click", function () {
setSpeedSheetOpen(false);
});
});
}
}
applyPlaySpeed(dp, currentPlaySpeed());
}
function nextEpisodeElement() {
var active = document.querySelector(".play-ep-grid .play-ep--active");
if (!active) return null;
var next = active.nextElementSibling;
while (next) {
if (next.matches && next.matches(".play-ep")) return next;
next = next.nextElementSibling;
}
return null;
}
function syncDesktopNextControl() {
var button = document.querySelector("[data-player-next]");
if (!button) return;
var disabled = !nextEpisodeElement();
button.disabled = disabled;
button.setAttribute("aria-disabled", disabled ? "true" : "false");
}
function formatDesktopPlayerTime(value) {
var seconds = Number(value);
if (!Number.isFinite(seconds) || seconds < 0) seconds = 0;
seconds = Math.floor(seconds);
var hours = Math.floor(seconds / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
var rest = seconds % 60;
var minuteText = String(minutes).padStart(2, "0");
var secondText = String(rest).padStart(2, "0");
if (hours > 0) {
return String(hours).padStart(2, "0") + ":" + minuteText + ":" + secondText;
}
return minuteText + ":" + secondText;
}
function setupDesktopPlayerControls(dp) {
var player = document.querySelector(".play-player .dplayer");
if (!player) return;
var left = player.querySelector(".dplayer-icons-left");
var right = player.querySelector(".dplayer-icons-right");
var playButton = left && left.querySelector(".dplayer-play-icon");
var volume = left && left.querySelector(".dplayer-volume");
var fullscreen = right && right.querySelector(".dplayer-full");
var time = left && left.querySelector(".dplayer-time");
if (!left || !right || !playButton) return;
if (!left.querySelector("[data-player-next]")) {
var nextButton = document.createElement("button");
nextButton.className = "dplayer-icon play-player-next";
nextButton.type = "button";
nextButton.setAttribute("data-player-next", "");
nextButton.setAttribute("aria-label", "播放下一集");
nextButton.innerHTML =
'<img src="/static/images/figma-play/control-next.svg" alt="" aria-hidden="true">';
nextButton.addEventListener("click", function () {
var next = nextEpisodeElement();
if (next) next.click();
});
playButton.insertAdjacentElement("afterend", nextButton);
}
if (volume && fullscreen) {
right.insertBefore(volume, fullscreen);
}
var settingButton = right.querySelector(".dplayer-setting-icon");
if (settingButton && !settingButton.querySelector(".play-player-speed-label")) {
var speedLabel = document.createElement("span");
speedLabel.className = "play-player-speed-label";
speedLabel.textContent = "倍速";
settingButton.appendChild(speedLabel);
}
if (time) {
Array.prototype.slice.call(time.childNodes).forEach(function (node) {
if (node.nodeType !== 3) return;
if (node.nodeValue.indexOf("/") >= 0) node.nodeValue = "/";
else node.nodeValue = "";
});
}
function syncTime() {
if (!dp || !dp.video) return;
var current = player.querySelector(".dplayer-ptime");
var duration = player.querySelector(".dplayer-dtime");
if (current) current.textContent = formatDesktopPlayerTime(dp.video.currentTime);
if (duration) duration.textContent = formatDesktopPlayerTime(dp.video.duration);
}
syncDesktopNextControl();
syncTime();
if (dp && typeof dp.on === "function") {
dp.on("timeupdate", syncTime);
dp.on("loadedmetadata", syncTime);
dp.on("durationchange", syncTime);
}
}
function initDPlayer() {
if (typeof DPlayer === "undefined") return null;
var container = document.querySelector(".dplayer-video");
if (!container) return null;
var source = choosePlaybackSource(
container.dataset.url || "",
container.dataset.h265Url || "",
supportsH265Playback()
);
if (!source.initialUrl) return null;
var videoOption = buildVideoOption(
source.initialUrl,
container.dataset.cover || ""
);
try {
playDp = new DPlayer({
container: container,
autoplay: false,
preload: "metadata",
screenshot: false,
airplay: false,
chromecast: false,
hotkey: true,
mutex: true,
contextmenu: [],
video: videoOption,
});
var player = container.closest(".play-player");
bindPlayerPoster(playDp, player);
bindH265Fallback(playDp, source);
setupDesktopPlayerControls(playDp);
return playDp;
} catch (e) {
return null;
}
}
function bindH5PlayerUI(dp) {
bindSpeedMenu(dp);
var moreBtn = document.querySelector("[data-play-intro-more]");
var introEl = document.querySelector("[data-play-intro]");
if (moreBtn && introEl) {
function syncIntroMore() {
var expanded = introEl.classList.contains("is-expanded");
if (expanded) {
moreBtn.hidden = false;
moreBtn.style.visibility = "";
moreBtn.textContent = "收起";
return;
}
// Reserve button width, then detect clamp overflow.
moreBtn.hidden = false;
moreBtn.style.visibility = "hidden";
moreBtn.textContent = "展开";
void introEl.offsetHeight;
var overflowing = introEl.scrollHeight > introEl.clientHeight + 1;
moreBtn.style.visibility = "";
moreBtn.hidden = !overflowing;
}
moreBtn.addEventListener("click", function () {
var expanded = introEl.classList.toggle("is-expanded");
moreBtn.textContent = expanded ? "收起" : "展开";
if (!expanded) syncIntroMore();
});
syncIntroMore();
window.addEventListener("resize", syncIntroMore);
}
var clearBtn = document.querySelector("[data-play-clear]");
if (clearBtn) {
var clearIcon = clearBtn.querySelector("[data-play-clear-icon]");
clearBtn.addEventListener("click", function () {
var focusing = document.body.classList.toggle("play-h5-focus");
clearBtn.setAttribute("aria-label", focusing ? "取消清屏" : "清屏");
if (clearIcon) {
clearIcon.src = focusing
? "/static/images/full_screen_close.svg"
: "/static/images/full_screen.svg";
}
});
}
var nextBar = document.querySelector("[data-play-next]");
var nextCountdown = document.querySelector("[data-play-next-countdown]");
function findNextChapterId() {
var active =
document.querySelector(".play-ep--active") ||
document.querySelector(".play-ep-sheet-item--active");
if (!active) return 0;
var next = active.nextElementSibling;
while (next) {
if (
next.matches &&
(next.matches(".play-ep") || next.matches(".play-ep-sheet-item"))
) {
var id = Number(next.getAttribute("data-chapter-id") || 0);
if (id > 0 && next.getAttribute("data-play-switch") != null) {
return id;
}
// 付费锁定集:返回 id 以便引导下载 APP
if (id > 0) return id;
}
next = next.nextElementSibling;
}
return 0;
}
function setNextCountdownVisible(visible, seconds) {
if (!nextBar) return;
nextBar.hidden = !visible;
document.body.classList.toggle("play-next-countdown-visible", !!visible);
if (visible && nextCountdown) {
nextCountdown.textContent = "倒计时" + seconds + "s";
}
}
function updateNextCountdown() {
var chapterId = findNextChapterId();
var video = dp && dp.video;
var duration = video ? Number(video.duration) : 0;
var currentTime = video ? Number(video.currentTime) : 0;
var remaining = duration - currentTime;
var shouldShow =
chapterId > 0 &&
Number.isFinite(duration) &&
duration > 0 &&
Number.isFinite(remaining) &&
remaining > 0 &&
remaining <= 3;
setNextCountdownVisible(
shouldShow,
shouldShow ? Math.max(1, Math.ceil(remaining)) : 3
);
}
if (dp && typeof dp.on === "function") {
dp.on("timeupdate", updateNextCountdown);
dp.on("ended", function () {
var chapterId = findNextChapterId();
setNextCountdownVisible(false, 3);
if (!chapterId) return;
if (isLockedEpisode(chapterId)) {
openDownloadApp();
return;
}
switchToChapter(chapterId, { autoplay: true });
});
dp.on("play", function () {
applyPlaySpeed(dp, currentPlaySpeed());
updateNextCountdown();
});
dp.on("seeked", updateNextCountdown);
dp.on("loadedmetadata", function () {
setNextCountdownVisible(false, 3);
});
}
}
var playPage = document.querySelector(".play-page");
var playletId = Number(
(playPage && playPage.getAttribute("data-playlet-id")) || 0
);
var playletSlug =
(playPage && playPage.getAttribute("data-playlet-slug")) || "";
var playTitle =
(playPage && playPage.getAttribute("data-play-title")) || "";
var playCover =
(playPage && playPage.getAttribute("data-play-cover")) || "";
var playDesc =
(playPage && playPage.getAttribute("data-play-desc")) || "";
var playTag =
(playPage && playPage.getAttribute("data-play-tag")) || "短剧";
var playFeatured =
(playPage && playPage.getAttribute("data-play-featured")) === "1";
var playIsNew =
(playPage && playPage.getAttribute("data-play-new")) === "1";
var playTotalEpisode = Number(
(playPage && playPage.getAttribute("data-play-total-episode")) || 0
);
var currentChapterId = Number(
(document.querySelector("[data-play-player]") &&
document
.querySelector("[data-play-player]")
.getAttribute("data-chapter-id")) ||
0
);
var currentEpisode = Number(
(playPage && playPage.getAttribute("data-play-episode")) || 0
);
var switchingChapter = false;
function padEpisode(n) {
var num = Number(n) || 0;
return num < 10 ? "0" + num : String(num);
}
function buildPlayHref(chapterId) {
var key = playletSlug || (playletId ? String(playletId) : "");
if (!key) return "/play/";
return "/play/" + encodeURIComponent(key) + "/" + chapterId + "/";
}
function isChapterLocked(chapter) {
if (!chapter) return false;
if (chapter.canWatch || chapter.isUnlocked) return false;
if (chapter.needUnlock || chapter.needVip) return true;
// 与服务端 PlayletChapterData.AllowWatch 保持一致:仅免费集可直接播放。
return Number(chapter.payType || 0) !== 0;
}
function isLockedEpisode(chapterId) {
chapterId = Number(chapterId) || 0;
if (!chapterId) return false;
return !!document.querySelector(
'[data-play-locked][data-chapter-id="' + chapterId + '"]'
);
}
// 付费剧集统一引导下载 APP(移动端跳转下载地址,PC 弹出下载二维码)。
function openDownloadApp() {
if (typeof window.openDownloadApp === "function") {
return window.openDownloadApp();
}
return Promise.resolve();
}
function updateEpisodeActive(chapterId) {
document.querySelectorAll(".play-ep").forEach(function (el) {
var isActive = Number(el.getAttribute("data-chapter-id") || 0) === chapterId;
el.classList.toggle("play-ep--active", isActive);
var lock = el.querySelector(".play-ep-lock");
if (lock) lock.style.display = isActive ? "none" : "";
});
document.querySelectorAll(".play-ep-sheet-item").forEach(function (el) {
var isActive =
Number(el.getAttribute("data-chapter-id") || 0) === chapterId;
el.classList.toggle("play-ep-sheet-item--active", isActive);
var lock = el.querySelector(".play-ep-sheet-lock");
if (lock) lock.hidden = isActive;
});
syncDesktopNextControl();
}
function updateChapterMeta(chapter) {
var ep = Number(chapter.currentEpisode) || 0;
currentEpisode = ep;
currentChapterId = Number(chapter.id) || currentChapterId;
if (playPage) {
playPage.setAttribute("data-play-episode", String(ep));
}
var h5Ep = document.querySelector("[data-play-h5-ep]");
if (h5Ep) h5Ep.textContent = ep > 0 ? "第" + ep + "集" : "播放";
var infoEp = document.querySelector("[data-play-info-ep]");
if (infoEp) infoEp.textContent = ep > 0 ? "第 " + ep + " 集" : "—";
var infoSub = document.querySelector("[data-play-info-sub]");
if (infoSub) {
infoSub.textContent = playTitle
? playTitle + " - 第" + padEpisode(ep) + "集"
: ep > 0
? "第" + padEpisode(ep) + "集"
: "—";
}
document.title = playTitle
? playTitle + " - 第" + ep + "集"
: "播放";
var player = document.querySelector("[data-play-player]");
if (player) {
player.setAttribute("data-chapter-id", String(chapter.id || ""));
player.setAttribute("data-playlet-id", String(playletId || chapter.playletId || ""));
player.setAttribute("data-pay-type", String(chapter.payType || 0));
player.setAttribute(
"data-can-watch",
chapter.canWatch || chapter.isUnlocked ? "1" : "0"
);
player.setAttribute(
"data-need-unlock",
chapter.needUnlock ? "1" : "0"
);
player.setAttribute("data-need-vip", chapter.needVip ? "1" : "0");
}
var commentCount = document.querySelector("[data-play-comment-count]");
if (commentCount) {
commentCount.textContent = String(chapter.countComment || 0);
}
document.querySelectorAll(".detail-comments").forEach(function (section) {
section.setAttribute(
"data-comment-object-id",
String(chapter.id || "")
);
section.dispatchEvent(new CustomEvent("play:reload-comments"));
});
}
function hidePosterOverlay() {
var poster = document.querySelector("[data-play-poster]");
var playBtn = document.querySelector("[data-play-poster-play]");
if (poster) poster.classList.add("play-player-cover--hidden");
if (playBtn) playBtn.style.display = "none";
}
function ensurePlayerContainer(videoUrl) {
var player = document.querySelector("[data-play-player]");
if (!player) return null;
var container = player.querySelector(".dplayer-video");
if (container) {
container.setAttribute("data-url", videoUrl || "");
if (playCover) container.setAttribute("data-cover", playCover);
return container;
}
container = document.createElement("div");
container.className = "dplayer-video";
container.setAttribute("data-url", videoUrl || "");
if (playCover) container.setAttribute("data-cover", playCover);
player.insertBefore(container, player.firstChild);
var lockedCta = player.querySelector("[data-play-locked-cta]");
if (lockedCta) lockedCta.remove();
var playBtn = player.querySelector("[data-play-poster-play]");
if (!playBtn) {
playBtn = document.createElement("button");
playBtn.className = "play-player-center";
playBtn.type = "button";
playBtn.setAttribute("aria-label", "播放");
playBtn.setAttribute("data-play-poster-play", "");
playBtn.innerHTML = '<img src="/static/images/figma-play/control-center-play.svg" alt="播放" />';
player.appendChild(playBtn);
}
return container;
}
function switchToChapter(chapterId, options) {
options = options || {};
chapterId = Number(chapterId) || 0;
if (!chapterId || !window.HttpRequest) return Promise.resolve();
if (switchingChapter) return Promise.resolve();
if (chapterId === currentChapterId && playDp && !options.force) {
if (options.autoplay !== false) {
try {
playDp.play();
} catch (e) {}
}
return Promise.resolve();
}
switchingChapter = true;
return window.HttpRequest.get("/api/playlet/chapter/detail", {
params: {
chapterId: chapterId,
playletId: playletId || undefined,
playletSlug: playletSlug || undefined,
},
})
.then(function (res) {
if (!res || res.code !== 200 || !res.data) {
throw new Error((res && (res.tip || res.msg)) || "加载失败");
}
var chapter = res.data;
if (isChapterLocked(chapter)) {
openDownloadApp();
return;
}
var videoUrl = resolvePlayUrl(chapter.videoUrl || "");
if (!videoUrl) {
throw new Error("暂无播放地址");
}
currentChapterId = Number(chapter.id) || chapterId;
updateEpisodeActive(currentChapterId);
updateChapterMeta(chapter);
if (!options.skipHistory) {
var href = buildPlayHref(currentChapterId);
var state = { chapterId: currentChapterId };
if (options.replace) {
history.replaceState(state, "", href);
} else {
history.pushState(state, "", href);
}
}
var videoOption = buildVideoOption(videoUrl, playCover);
if (playDp && typeof playDp.switchVideo === "function") {
switchPlayerVideo(playDp, videoOption);
} else {
ensurePlayerContainer(chapter.videoUrl || "");
playDp = initDPlayer();
bindH5PlayerUI(playDp);
}
if (options.autoplay !== false && playDp) {
resumePlayerAfterSwitch(playDp);
applyPlaySpeed(playDp, currentPlaySpeed());
} else {
applyPlaySpeed(playDp, currentPlaySpeed());
}
document.body.classList.remove("play-ep-sheet-open");
var sheetMask = document.querySelector(".play-h5-sheet-mask");
if (sheetMask) sheetMask.style.display = "";
var epSheet = document.querySelector("[data-play-ep-sheet]");
if (epSheet) epSheet.style.display = "";
})
.catch(function (err) {
toast((err && err.message) || "切换剧集失败", "error");
})
.finally(function () {
switchingChapter = false;
});
}
function bindEpisodeSwitch() {
document.addEventListener("click", function (e) {
var el =
e.target && e.target.closest
? e.target.closest("[data-play-switch]")
: null;
if (!el) return;
var chapterId = Number(el.getAttribute("data-chapter-id") || 0);
if (!chapterId) return;
e.preventDefault();
switchToChapter(chapterId, { autoplay: true });
});
document.addEventListener("click", function (e) {
var el =
e.target && e.target.closest
? e.target.closest("[data-play-locked], [data-play-download-app]")
: null;
if (!el) return;
e.preventDefault();
openDownloadApp();
});
window.addEventListener("popstate", function (e) {
var match = String(window.location.pathname || "").match(/\/play\/[^/]+\/(\d+)\/?$/);
var chapterId =
(e.state && e.state.chapterId) ||
Number((match && match[1]) || 0);
if (!chapterId) return;
if (isLockedEpisode(chapterId)) {
// 回退到付费集:交给服务端渲染下载 APP 引导,避免自动跳转。
window.location.reload();
return;
}
switchToChapter(chapterId, {
autoplay: true,
skipHistory: true,
force: true,
});
});
if (currentChapterId) {
history.replaceState(
{ chapterId: currentChapterId },
"",
window.location.href
);
}
}
function bindSidebarRanges() {
var ranges = document.querySelectorAll("[data-play-ep-ranges] .play-ep-range");
var episodes = document.querySelectorAll("[data-play-ep-grid] .play-ep");
if (!ranges.length || !episodes.length) return;
function applyRange(start, end) {
episodes.forEach(function (el) {
var n = Number(el.getAttribute("data-ep"));
var inRange = !Number.isNaN(n) && n >= start && n <= end;
el.classList.toggle("is-range-hidden", !inRange);
});
}
function activate(btn) {
ranges.forEach(function (item) {
item.classList.toggle("play-ep-range--active", item === btn);
});
applyRange(Number(btn.dataset.rangeStart), Number(btn.dataset.rangeEnd));
}
ranges.forEach(function (btn) {
btn.addEventListener("click", function () {
activate(btn);
});
});
var active = document.querySelector(".play-ep-range--active") || ranges[0];
activate(active);
}
var openBtns = document.querySelectorAll("[data-play-ep-open]");
var closeBtns = document.querySelectorAll("[data-play-ep-close]");
var sheet = document.querySelector("[data-play-ep-sheet]");
var mask = document.querySelector(".play-h5-sheet-mask");
var commentOpenBtns = document.querySelectorAll("[data-play-comment-open]");
var commentCloseBtns = document.querySelectorAll("[data-play-comment-close]");
var commentSheet = document.querySelector("[data-play-comment-sheet]");
function openEpSheet() {
document.body.classList.remove("play-comment-sheet-open");
document.body.classList.remove("play-speed-sheet-open");
document.body.classList.add("play-ep-sheet-open");
}
function closeEpSheet() {
document.body.classList.remove("play-ep-sheet-open");
}
function openCommentSheet() {
document.body.classList.remove("play-ep-sheet-open");
document.body.classList.remove("play-speed-sheet-open");
document.body.classList.add("play-comment-sheet-open");
var section = document.querySelector(".play-comment-sheet-body");
if (section) {
section.dispatchEvent(new CustomEvent("play:reload-comments"));
}
}
function closeCommentSheet() {
document.body.classList.remove("play-comment-sheet-open");
}
function closeSpeedSheet() {
document.body.classList.remove("play-speed-sheet-open");
var btn = document.querySelector("[data-play-speed]");
if (btn) btn.setAttribute("aria-expanded", "false");
}
function closeAllSheets() {
closeEpSheet();
closeCommentSheet();
closeSpeedSheet();
}
if (sheet) {
openBtns.forEach(function (btn) {
btn.addEventListener("click", openEpSheet);
});
closeBtns.forEach(function (btn) {
btn.addEventListener("click", closeEpSheet);
});
var tabs = sheet.querySelectorAll(".play-ep-sheet-tab");
var epsPanel = sheet.querySelector('[data-ep-panel="eps"]');
var morePanel = sheet.querySelector('[data-ep-panel="more"]');
var epItems = sheet.querySelectorAll(".play-ep-sheet-item");
function applyRange(start, end) {
epItems.forEach(function (el) {
var n = Number(el.getAttribute("data-ep"));
var inRange = !Number.isNaN(n) && n >= start && n <= end;
el.classList.toggle("is-range-hidden", !inRange);
});
}
function activateTab(btn) {
var tab = btn.getAttribute("data-ep-tab");
if (tab === "more") {
tabs.forEach(function (item) {
item.classList.toggle(
"play-ep-sheet-tab--active",
item.getAttribute("data-ep-tab") === "more"
);
});
if (epsPanel) epsPanel.classList.add("is-hidden");
if (morePanel) morePanel.classList.remove("is-hidden");
return;
}
tabs.forEach(function (item) {
item.classList.toggle("play-ep-sheet-tab--active", item === btn);
});
if (epsPanel) epsPanel.classList.remove("is-hidden");
if (morePanel) morePanel.classList.add("is-hidden");
if (btn.dataset.rangeStart) {
applyRange(Number(btn.dataset.rangeStart), Number(btn.dataset.rangeEnd));
return;
}
var rangeTab =
sheet.querySelector('.play-ep-sheet-tab[data-ep-tab="eps"][data-range-start].play-ep-sheet-tab--active') ||
sheet.querySelector('.play-ep-sheet-tab[data-ep-tab="eps"][data-range-start]');
if (rangeTab) {
applyRange(Number(rangeTab.dataset.rangeStart), Number(rangeTab.dataset.rangeEnd));
}
}
tabs.forEach(function (btn) {
btn.addEventListener("click", function () {
activateTab(btn);
});
});
var activeTab =
sheet.querySelector('.play-ep-sheet-tab[data-ep-tab="eps"][data-range-start].play-ep-sheet-tab--active') ||
sheet.querySelector(".play-ep-sheet-tab--active") ||
tabs[0];
if (activeTab) activateTab(activeTab);
}
if (commentSheet) {
commentOpenBtns.forEach(function (btn) {
btn.addEventListener("click", openCommentSheet);
});
commentCloseBtns.forEach(function (btn) {
btn.addEventListener("click", closeCommentSheet);
});
}
if (mask) {
mask.addEventListener("click", closeAllSheets);
}
function toast(message, type) {
if (typeof window.showToast === "function") {
window.showToast(message, type);
return;
}
window.alert(message);
}
function boot() {
var dp = initDPlayer();
bindH5PlayerUI(dp);
bindSidebarRanges();
bindEpisodeSwitch();
if (typeof window.initLazyImages === "function") {
window.initLazyImages(document.querySelector(".play-page") || document);
}
}
function bootWhenReady() {
if (typeof DPlayer === "undefined") {
window.addEventListener("load", bootWhenReady, { once: true });
return;
}
boot();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bootWhenReady);
} else {
bootWhenReady();
}
})();