Files
web_huangguo_web/app/static/js/pages/play.js
T
fish03andClaude Opus 5.5 bef9f8b4ed Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:43:16 +07:00

1409 lines
52 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());
}
// 复用同一 meta 对象:bindVideoEvents 只绑一次,切集时就地更新字段即可生效。
var playTrackerMeta = {
media_id: "",
video_id: "",
video_title: "",
video_type_id: "playlet",
video_type_name: "短剧",
video_content_type: "playlet",
recommend_trace_id: "",
video_tag_key: "",
video_tag_name: "",
};
var playViewTracked = false;
function syncPlayTrackerMeta() {
var root = document.querySelector("[data-play-page]");
var ds = (root && root.dataset) || {};
var player = document.querySelector("[data-play-player]");
var pds = (player && player.dataset) || {};
var episode = ds.playEpisode || "";
playTrackerMeta.media_id = ds.playletId || "";
playTrackerMeta.video_id = ds.playletId || "";
playTrackerMeta.video_title = ds.playTitle || "";
playTrackerMeta.video_type_id = "playlet";
playTrackerMeta.video_type_name = ds.playTag || "短剧";
playTrackerMeta.video_content_type = "playlet";
playTrackerMeta.recommend_trace_id = "";
playTrackerMeta.video_tag_key = pds.chapterId || "";
playTrackerMeta.video_tag_name = episode ? "第" + episode + "集" : "";
return playTrackerMeta;
}
function bindPlayTracker(dp) {
if (!dp || !window.tracker || typeof window.tracker.bindVideoEvents !== "function") {
return;
}
syncPlayTrackerMeta();
window.tracker.bindVideoEvents(dp, playTrackerMeta);
}
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);
bindPlayTracker(playDp);
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;
}
// 付费/VIP 锁定集:返回 id 以便弹窗
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 () {
markCurrentEpisodeWatched();
var chapterId = findNextChapterId();
setNextCountdownVisible(false, 3);
if (chapterId) switchToChapter(chapterId, { autoplay: true });
});
dp.on("play", function () {
applyPlaySpeed(dp, currentPlaySpeed());
markCurrentEpisodeWatched();
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;
var lastWatchReportKey = "";
function reportChapterWatch() {
if (!currentChapterId || !window.HttpRequest) return;
if (window.CookieUtil && !window.CookieUtil.isLoggedIn()) return;
var key = String(playletId || 0) + ":" + currentChapterId;
if (key === lastWatchReportKey) return;
lastWatchReportKey = key;
var body = { chapterId: currentChapterId };
if (playletId) body.playletId = playletId;
window.HttpRequest.post("/api/playlet/chapter/watch/report", body).catch(
function () {
/* 静默失败:未登录/网络错误不打断播放 */
}
);
}
function markCurrentEpisodeWatched() {
reportChapterWatch();
}
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 resolvePayGate(chapter, el) {
if (chapter) {
if (chapter.canWatch || chapter.isUnlocked) return 0;
if (chapter.needUnlock) return 1;
if (chapter.needVip) return 2;
var payType = Number(chapter.payType || 0);
if (payType === 1 || payType === 2) return payType;
return 0;
}
if (el) {
if (el.getAttribute("data-can-watch") === "1") return 0;
if (el.getAttribute("data-need-unlock") === "1") return 1;
if (el.getAttribute("data-need-vip") === "1") return 2;
if (el.getAttribute("data-coin-dialog-open") != null) return 1;
if (el.getAttribute("data-vip-dialog-open") != null) return 2;
return Number(el.getAttribute("data-pay-type") || 0);
}
return 0;
}
function resolveUnlockCost(chapter, el) {
if (chapter) {
if (Number(chapter.costCoins) > 0) return Number(chapter.costCoins);
if (Number(chapter.singleCoins) > 0) return Number(chapter.singleCoins);
}
if (el) return Number(el.getAttribute("data-single-coins") || 0);
return 0;
}
function openPayForChapter(el, chapter) {
var payGate = resolvePayGate(chapter, el);
if (payGate === 1) {
if (typeof window.openCoinDialog === "function") {
var coinTrigger = document.createElement("button");
coinTrigger.setAttribute("data-coin-dialog-open", "");
coinTrigger.setAttribute(
"data-chapter-id",
String((chapter && chapter.id) || (el && el.getAttribute("data-chapter-id")) || "")
);
coinTrigger.setAttribute(
"data-playlet-id",
String(
(chapter && chapter.playletId) ||
playletId ||
(el && el.getAttribute("data-playlet-id")) ||
""
)
);
coinTrigger.setAttribute(
"data-single-coins",
String(resolveUnlockCost(chapter, el))
);
coinTrigger.style.display = "none";
document.body.appendChild(coinTrigger);
coinTrigger.click();
coinTrigger.remove();
}
return true;
}
if (payGate === 2) {
var vipBtn = document.querySelector("[data-vip-dialog-open]");
if (vipBtn) vipBtn.click();
else if (typeof window.openVipDialog === "function") {
window.openVipDialog();
}
return true;
}
return false;
}
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 coin = el.querySelector(".play-ep-coin");
var lock = el.querySelector(".play-ep-lock");
if (coin) coin.style.display = isActive ? "none" : "";
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 coin = el.querySelector(".play-ep-sheet-coin");
var lock = el.querySelector(".play-ep-sheet-lock");
if (coin) coin.hidden = isActive;
if (lock) lock.hidden = isActive;
});
syncDesktopNextControl();
}
function updateChapterMeta(chapter) {
var ep = Number(chapter.currentEpisode) || 0;
currentEpisode = ep;
currentChapterId = Number(chapter.id) || currentChapterId;
lastWatchReportKey = "";
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-single-coins",
String(resolveUnlockCost(chapter, null))
);
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");
player.setAttribute(
"data-is-unlocked",
chapter.isUnlocked ? "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 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;
var payGate = resolvePayGate(chapter, null);
if (payGate !== 0 && !options.skipPayCheck) {
openPayForChapter(null, chapter);
return;
}
var videoUrl = resolvePlayUrl(chapter.videoUrl || "");
if (!videoUrl) {
if (payGate !== 0) {
openPayForChapter(null, chapter);
return;
}
throw new Error("暂无播放地址");
}
currentChapterId = Number(chapter.id) || chapterId;
updateEpisodeActive(currentChapterId);
updateChapterMeta(chapter);
markCurrentEpisodeWatched();
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);
syncPlayTrackerMeta();
bindPlayTracker(playDp);
} 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 markEpisodeUnlocked(chapterId) {
chapterId = Number(chapterId) || 0;
if (!chapterId) return;
var href = buildPlayHref(chapterId);
document
.querySelectorAll(
'.play-ep[data-chapter-id="' +
chapterId +
'"], .play-ep-sheet-item[data-chapter-id="' +
chapterId +
'"]'
)
.forEach(function (el) {
el.removeAttribute("data-coin-dialog-open");
el.removeAttribute("data-vip-dialog-open");
el.removeAttribute("data-single-coins");
el.setAttribute("data-play-switch", "");
el.setAttribute("href", href || "javascript:;");
var coin = el.querySelector(
".play-ep-coin, .play-ep-sheet-coin"
);
if (coin) coin.remove();
var lock = el.querySelector(
".play-ep-lock, .play-ep-sheet-lock"
);
if (lock) lock.remove();
});
var player = document.querySelector("[data-play-player]");
if (
player &&
Number(player.getAttribute("data-chapter-id") || 0) === chapterId
) {
player.setAttribute("data-pay-type", "0");
player.setAttribute("data-can-watch", "1");
player.setAttribute("data-need-unlock", "0");
player.setAttribute("data-need-vip", "0");
player.setAttribute("data-is-unlocked", "1");
player.removeAttribute("data-single-coins");
}
}
function playAfterChapterUnlock(detail) {
detail = detail || {};
var chapterId = Number(detail.chapterId) || 0;
if (!chapterId) return;
if (detail.playletId) {
playletId = Number(detail.playletId) || playletId;
if (playPage) {
playPage.setAttribute("data-playlet-id", String(playletId));
}
}
markEpisodeUnlocked(chapterId);
switchToChapter(chapterId, {
force: true,
skipPayCheck: true,
autoplay: true,
replace: true,
});
}
window.playAfterChapterUnlock = playAfterChapterUnlock;
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 });
});
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;
switchToChapter(chapterId, {
autoplay: true,
skipHistory: true,
force: true,
});
});
if (currentChapterId) {
history.replaceState(
{ chapterId: currentChapterId },
"",
window.location.href
);
}
}
function maybeOpenPayDialog() {
var player = document.querySelector("[data-play-player]");
if (!player) return;
var hasVideo = !!player.querySelector(".dplayer-video[data-url]");
if (hasVideo) return;
openPayForChapter(player, null);
}
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 trackPlayView() {
if (playViewTracked || !window.tracker || typeof window.tracker.videoEvent !== "function") return;
var meta = syncPlayTrackerMeta();
if (!meta.video_id) return;
playViewTracked = true;
window.tracker.videoEvent({
media_id: meta.media_id,
video_id: meta.video_id,
video_title: meta.video_title,
video_type_id: meta.video_type_id,
video_type_name: meta.video_type_name,
video_content_type: meta.video_content_type,
recommend_trace_id: meta.recommend_trace_id,
video_tag_key: meta.video_tag_key,
video_tag_name: meta.video_tag_name,
video_behavior_key: "video_view",
video_behavior_name: "视频展示",
});
}
function bindTrackingWhenReady(dp) {
function activateTracking() {
bindPlayTracker(dp || playDp);
trackPlayView();
}
activateTracking();
if (!window.__trackerReady) {
window.addEventListener("tracker:ready", activateTracking, { once: true });
}
}
function boot() {
var dp = initDPlayer();
bindH5PlayerUI(dp);
bindSidebarRanges();
bindEpisodeSwitch();
maybeOpenPayDialog();
bindTrackingWhenReady(dp);
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();
}
})();