922 lines
33 KiB
JavaScript
922 lines
33 KiB
JavaScript
/**
|
|
* Web SDK v1.2.0 接入层。
|
|
* 页面/点击/导航沿用 jha-1043 的口径,短剧使用最新版 dramaEvent/dramaStatusChange。
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
var state = {
|
|
initialized: false,
|
|
initializing: false,
|
|
queue: [],
|
|
page: { key: "internal_other", name: "站内其他页面" },
|
|
referrer: { key: "direct", name: "直接访问" },
|
|
userType: "normal",
|
|
pageStartedAt: Date.now(),
|
|
dramaViewSent: false,
|
|
adShown: {},
|
|
};
|
|
|
|
function stringValue(value, fallback) {
|
|
var result = value == null ? "" : String(value).trim();
|
|
return result || (fallback == null ? "" : String(fallback));
|
|
}
|
|
|
|
function integer(value, fallback) {
|
|
var result = parseInt(value, 10);
|
|
return isFinite(result) ? result : (fallback || 0);
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
var result = Number(value);
|
|
if (!isFinite(result)) result = min;
|
|
return Math.max(min, Math.min(max, result));
|
|
}
|
|
|
|
function sdkAvailable() {
|
|
return !!(global.WebSDK && global.WebSDK.track);
|
|
}
|
|
|
|
function logError() {}
|
|
|
|
function invoke(method, payload) {
|
|
if (!state.initialized || !sdkAvailable()) {
|
|
return new Promise(function (resolve) {
|
|
state.queue.push([method, payload, resolve]);
|
|
});
|
|
}
|
|
try {
|
|
var fn = global.WebSDK.track[method];
|
|
if (typeof fn !== "function") {
|
|
var missing = new Error("SDK method does not exist: " + method);
|
|
logError(missing);
|
|
return Promise.resolve({ sent: false, ok: false, reason: "unknown_method" });
|
|
}
|
|
var result = fn(payload);
|
|
return Promise.resolve(result).then(function (response) {
|
|
if (response && response.ok === false) {
|
|
logError(method + " rejected: " + JSON.stringify(response));
|
|
}
|
|
return response;
|
|
}).catch(function (error) {
|
|
logError(error);
|
|
return { sent: false, ok: false, reason: "exception" };
|
|
});
|
|
} catch (error) {
|
|
logError(error);
|
|
return Promise.resolve({ sent: false, ok: false, reason: "exception" });
|
|
}
|
|
}
|
|
|
|
function flushQueue() {
|
|
var pending = state.queue.splice(0, state.queue.length);
|
|
pending.forEach(function (entry) {
|
|
Promise.resolve(invoke(entry[0], entry[1])).then(entry[2]);
|
|
});
|
|
}
|
|
|
|
function currentChannel(value) {
|
|
var channel = stringValue(value);
|
|
if (!channel && global.ChannelAttribution && typeof global.ChannelAttribution.get === "function") {
|
|
channel = stringValue(global.ChannelAttribution.get());
|
|
}
|
|
if (!channel) channel = stringValue(global.__CHANNEL_CODE__, "organic");
|
|
var normalized = channel.toLowerCase();
|
|
if (["self", "natural", "official", "channel", "channel://natural"].indexOf(normalized) >= 0) {
|
|
return "organic";
|
|
}
|
|
if (!channel || channel.length > 50 || !/^[A-Za-z0-9_.-]+$/.test(channel) ||
|
|
["{}", "0", "-", "unknown"].indexOf(normalized) >= 0) {
|
|
return "organic";
|
|
}
|
|
return channel;
|
|
}
|
|
|
|
function normalizeUid(value) {
|
|
var uid = stringValue(value);
|
|
if (uid === "0" || uid.length < 4 || uid.length > 32 || !/^[A-Za-z0-9_]+$/.test(uid) ||
|
|
["u_1001", "stress_user", "u_lab"].indexOf(uid.toLowerCase()) >= 0) return "";
|
|
return uid;
|
|
}
|
|
|
|
function inferPage(path) {
|
|
path = stringValue(path, "/").replace(/\/$/, "") || "/";
|
|
if (path === "/") return { key: "home", name: "首页" };
|
|
var rules = [
|
|
[/^\/series-play(?:\/|$)/, "series_play", "AI成人剧集播放页"],
|
|
[/^\/series(?:\/|$)/, "series", "AI成人剧集"],
|
|
[/^\/play(?:\/|$)/, "play", "短剧播放页"],
|
|
[/^\/detail(?:\/|$)/, "detail", "短剧详情"],
|
|
[/^\/list(?:\/|$)/, "list", "短剧列表"],
|
|
[/^\/rank(?:\/|$)/, "rank", "排行榜"],
|
|
[/^\/topic-detail(?:\/|$)/, "topic_detail", "专题详情"],
|
|
[/^\/topic(?:\/|$)/, "topic", "专题列表"],
|
|
[/^\/tags(?:\/|$)/, "tags", "标签详情"],
|
|
[/^\/search(?:-mobile)?(?:\/|$)/, "search", "搜索结果页"],
|
|
[/^\/history(?:\/|$)/, "history", "观看历史"],
|
|
[/^\/favorites(?:\/|$)/, "favorites", "我的收藏"],
|
|
[/^\/profile(?:\/|$)/, "profile", "个人中心"],
|
|
];
|
|
for (var i = 0; i < rules.length; i += 1) {
|
|
if (rules[i][0].test(path)) return { key: rules[i][1], name: rules[i][2] };
|
|
}
|
|
return { key: "internal_other", name: "站内其他页面" };
|
|
}
|
|
|
|
function readReferrer() {
|
|
try {
|
|
var saved = global.sessionStorage.getItem("__tracker_last_page");
|
|
if (saved) {
|
|
var parsed = JSON.parse(saved);
|
|
if (parsed && parsed.key) return parsed;
|
|
}
|
|
} catch (error) {}
|
|
try {
|
|
if (document.referrer) {
|
|
var url = new URL(document.referrer, global.location.href);
|
|
if (url.host === global.location.host) return inferPage(url.pathname);
|
|
return { key: "external", name: url.host || "外部站点" };
|
|
}
|
|
} catch (error) {}
|
|
return { key: "direct", name: "直接访问" };
|
|
}
|
|
|
|
function rememberPage() {
|
|
try { global.sessionStorage.setItem("__tracker_last_page", JSON.stringify(state.page)); } catch (error) {}
|
|
}
|
|
|
|
function init(options) {
|
|
if (state.initialized || state.initializing) return;
|
|
options = options || {};
|
|
var inferred = inferPage(global.location && global.location.pathname);
|
|
state.page = {
|
|
key: stringValue(options.pageKey, inferred.key),
|
|
name: stringValue(options.pageName, inferred.name),
|
|
};
|
|
state.userType = options.userType === "vip" ? "vip" : "normal";
|
|
state.referrer = readReferrer();
|
|
state.initializing = true;
|
|
|
|
var attempts = 0;
|
|
var timer = global.setInterval(function () {
|
|
attempts += 1;
|
|
if (!sdkAvailable() && attempts < 100) return;
|
|
global.clearInterval(timer);
|
|
if (!sdkAvailable()) {
|
|
state.initializing = false;
|
|
return;
|
|
}
|
|
try {
|
|
var result = global.WebSDK.init({
|
|
appId: stringValue(options.appId),
|
|
uid: normalizeUid(options.uid),
|
|
channel: currentChannel(options.channel),
|
|
encryptedConfig: stringValue(options.encryptedConfig),
|
|
appVersion: stringValue(options.appVersion, "1.0.0"),
|
|
disableAutoEvents: ["app_page_view", "page_click"],
|
|
});
|
|
Promise.resolve(result).then(function () {
|
|
state.initialized = true;
|
|
state.initializing = false;
|
|
__commonFieldsReady = null;
|
|
loadCommonFields();
|
|
flushQueue();
|
|
onReady();
|
|
}).catch(function (error) {
|
|
state.initializing = false;
|
|
logError(error);
|
|
});
|
|
} catch (error) {
|
|
state.initializing = false;
|
|
logError(error);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function whenDOMReady(callback) {
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", callback, { once: true });
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
function trackPageView(extra) {
|
|
extra = extra || {};
|
|
invoke("appPageView", {
|
|
user_type: stringValue(extra.user_type, state.userType),
|
|
page_key: state.page.key,
|
|
page_name: state.page.name,
|
|
referrer_page_key: state.referrer.key,
|
|
referrer_page_name: state.referrer.name,
|
|
current_page_key: state.page.key,
|
|
current_page_name: state.page.name,
|
|
page_load_time: pageLoadDuration(),
|
|
recommend_trace_id: stringValue(extra.recommend_trace_id),
|
|
});
|
|
rememberPage();
|
|
}
|
|
|
|
function pageLoadDuration() {
|
|
try {
|
|
if (global.performance && typeof global.performance.getEntriesByType === "function") {
|
|
var entries = global.performance.getEntriesByType("navigation");
|
|
if (entries && entries[0]) {
|
|
var navigation = entries[0];
|
|
var end = navigation.loadEventEnd || navigation.domContentLoadedEventEnd || global.performance.now();
|
|
if (isFinite(end)) return Math.max(0, Math.round(end));
|
|
}
|
|
}
|
|
if (global.performance && typeof global.performance.now === "function") {
|
|
return Math.max(0, Math.round(global.performance.now()));
|
|
}
|
|
} catch (error) {}
|
|
return Math.max(0, Date.now() - state.pageStartedAt);
|
|
}
|
|
|
|
function trackPageClick(event) {
|
|
var doc = document.documentElement || {};
|
|
var viewportWidth = global.innerWidth || doc.clientWidth || 0;
|
|
var viewportHeight = global.innerHeight || doc.clientHeight || 0;
|
|
var x = integer(event.clientX, 0);
|
|
var y = integer(event.clientY, 0);
|
|
invoke("pageClick", {
|
|
page_key: state.page.key,
|
|
page_name: state.page.name,
|
|
click_page_x: x,
|
|
click_page_y: y,
|
|
click_x_percent: viewportWidth ? Math.round(clamp(x / viewportWidth * 100, 0, 100)) : 0,
|
|
click_y_percent: viewportHeight ? Math.round(clamp(y / viewportHeight * 100, 0, 100)) : 0,
|
|
screen_width: integer(global.screen && global.screen.width, 0),
|
|
screen_height: integer(global.screen && global.screen.height, 0),
|
|
recommend_trace_id: "",
|
|
});
|
|
}
|
|
|
|
function trackNavigation(anchor) {
|
|
if (!anchor || (!anchor.hasAttribute("data-navigation-key") && !anchor.hasAttribute("data-nav-key")) ||
|
|
!anchor.getAttribute("href")) return;
|
|
var href = anchor.getAttribute("href");
|
|
if (!href || href.charAt(0) === "#" || /^javascript:/i.test(href)) return;
|
|
var label = stringValue(anchor.getAttribute("aria-label") || anchor.textContent).replace(/\s+/g, " ");
|
|
var navigationKey = stringValue(
|
|
anchor.getAttribute("data-navigation-key") || anchor.getAttribute("data-nav-key")
|
|
);
|
|
if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(navigationKey)) {
|
|
navigationKey = "";
|
|
try {
|
|
var url = new URL(href, global.location.href);
|
|
if (url.origin !== global.location.origin) {
|
|
navigationKey = "nav_external";
|
|
} else {
|
|
var targetPage = inferPage(url.pathname);
|
|
if (targetPage.key !== "internal_other") {
|
|
navigationKey = "nav_" + targetPage.key;
|
|
} else {
|
|
var firstSegment = stringValue(url.pathname.split("/").filter(Boolean)[0])
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "_")
|
|
.replace(/^_+|_+$/g, "")
|
|
.slice(0, 48);
|
|
navigationKey = firstSegment ? "nav_" + firstSegment : "nav_home";
|
|
}
|
|
}
|
|
} catch (error) {
|
|
navigationKey = "nav_other";
|
|
}
|
|
}
|
|
invoke("navigation", {
|
|
navigation_key: navigationKey.toLowerCase(),
|
|
navigation_name: stringValue(
|
|
anchor.getAttribute("data-navigation-name") || anchor.getAttribute("data-nav-name"),
|
|
label || navigationKey
|
|
).slice(0, 255),
|
|
});
|
|
}
|
|
|
|
function adPayload(node) {
|
|
var id = stringValue(node && node.getAttribute("data-ad-id"));
|
|
if (!id) return null;
|
|
var adType = stringValue(node.getAttribute("data-ad-type"), "banner");
|
|
var allSlots = document.querySelectorAll('[data-track-ad="external"][data-ad-id]');
|
|
var slotPosition = Array.prototype.indexOf.call(allSlots, node) + 1;
|
|
if (slotPosition <= 0) slotPosition = 1;
|
|
var fallbackSlotKey = state.page.key + "_content_banner_" + slotPosition;
|
|
var fallbackSlotName = state.page.name + "内容横幅第" + slotPosition + "位";
|
|
if (adType === "interstitial") {
|
|
fallbackSlotKey = state.page.key + "_popup_" + slotPosition;
|
|
fallbackSlotName = state.page.name + "弹窗广告位";
|
|
}
|
|
return {
|
|
page_key: state.page.key,
|
|
page_name: state.page.name,
|
|
ad_slot_key: stringValue(
|
|
node.getAttribute("data-ad-slot-key"),
|
|
fallbackSlotKey
|
|
),
|
|
ad_slot_name: stringValue(
|
|
node.getAttribute("data-ad-slot-name"),
|
|
fallbackSlotName
|
|
),
|
|
ad_id: id,
|
|
creative_id: stringValue(node.getAttribute("data-creative-id")),
|
|
ad_type: adType,
|
|
};
|
|
}
|
|
|
|
function bindAds() {
|
|
var selector = '[data-track-ad="external"][data-ad-id]';
|
|
var nodes = document.querySelectorAll(selector);
|
|
function show(node) {
|
|
trackExternalAd(node, "show");
|
|
}
|
|
if (global.IntersectionObserver) {
|
|
var observer = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
show(entry.target);
|
|
observer.unobserve(entry.target);
|
|
});
|
|
}, { threshold: 0.1 });
|
|
function observe(node) {
|
|
if (!node || node.__trackerAdObserved) return;
|
|
node.__trackerAdObserved = true;
|
|
observer.observe(node);
|
|
}
|
|
Array.prototype.forEach.call(nodes, observe);
|
|
if (global.MutationObserver) {
|
|
new MutationObserver(function (records) {
|
|
records.forEach(function (record) {
|
|
Array.prototype.forEach.call(record.addedNodes || [], function (node) {
|
|
if (!node || node.nodeType !== 1) return;
|
|
if (node.matches && node.matches(selector)) observe(node);
|
|
if (node.querySelectorAll) {
|
|
Array.prototype.forEach.call(node.querySelectorAll(selector), observe);
|
|
}
|
|
});
|
|
});
|
|
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
}
|
|
} else {
|
|
Array.prototype.forEach.call(nodes, show);
|
|
}
|
|
}
|
|
|
|
function trackExternalAd(node, eventType) {
|
|
if (!node || (eventType !== "show" && eventType !== "click")) return;
|
|
var payload = adPayload(node);
|
|
if (!payload) return;
|
|
var key = payload.ad_slot_key + "|" + payload.ad_id;
|
|
if (!state.adShown[key]) {
|
|
state.adShown[key] = true;
|
|
invoke("adImpression", payload);
|
|
}
|
|
if (eventType === "click" && node.getAttribute("data-ad-clickable") === "1") {
|
|
return invoke("adClick", payload);
|
|
}
|
|
}
|
|
|
|
function bindExternalAdEvents() {
|
|
document.addEventListener("external-ad:track", function (event) {
|
|
var detail = event && event.detail ? event.detail : {};
|
|
trackExternalAd(detail.element, stringValue(detail.eventType));
|
|
});
|
|
}
|
|
|
|
function trackAdvertising(node, eventType) {
|
|
if (!node || ["show", "click", "close"].indexOf(eventType) < 0) return;
|
|
invoke("advertising", {
|
|
event_type: eventType,
|
|
advertising_key: stringValue(node.getAttribute("data-advertising-key"), "dialog_ad"),
|
|
advertising_name: stringValue(node.getAttribute("data-advertising-name"), "弹窗广告"),
|
|
advertising_id: stringValue(node.getAttribute("data-advertising-id") || node.getAttribute("data-dialog-ad-id")),
|
|
});
|
|
}
|
|
|
|
function bindAdvertisingEvents() {
|
|
document.addEventListener("dialog-ad:track", function (event) {
|
|
var detail = event && event.detail ? event.detail : {};
|
|
var node = detail.element;
|
|
if (!node || !node.getAttribute) return;
|
|
trackAdvertising(node, stringValue(detail.eventType));
|
|
});
|
|
}
|
|
|
|
function dramaRoot() {
|
|
return document.querySelector("[data-track-drama]");
|
|
}
|
|
|
|
function dramaContext(root) {
|
|
root = root || dramaRoot();
|
|
if (!root) return null;
|
|
var data = root.dataset || {};
|
|
var dramaID = stringValue(data.dramaId);
|
|
var videoID = stringValue(data.videoId);
|
|
if (!dramaID || !videoID) return null;
|
|
return {
|
|
media_id: stringValue(data.mediaId),
|
|
drama_id: dramaID,
|
|
drama_title: stringValue(data.dramaTitle, "未命名短剧"),
|
|
episode: Math.max(1, integer(data.episode, 1)),
|
|
total_episode: Math.max(1, integer(data.totalEpisode, 1)),
|
|
video_id: videoID,
|
|
drama_type_id: stringValue(data.dramaTypeId, "default"),
|
|
drama_type_name: stringValue(data.dramaTypeName, "默认分类"),
|
|
drama_content_type: "short_drama",
|
|
recommend_trace_id: stringValue(data.recommendTraceId),
|
|
drama_tag_key: stringValue(data.dramaTagKey, "default"),
|
|
drama_tag_name: stringValue(data.dramaTagName, "默认标签"),
|
|
drama_duration: Math.max(0, integer(data.dramaDuration, 0)),
|
|
};
|
|
}
|
|
|
|
var dramaBehaviorNames = {
|
|
ep_view: "展示",
|
|
ep_play: "播放",
|
|
ep_pause: "暂停",
|
|
ep_forward: "快进",
|
|
ep_rewind: "快退",
|
|
ep_complete: "播放完成",
|
|
ep_share: "分享",
|
|
};
|
|
|
|
function videoPlaybackState(video) {
|
|
if (!video) return null;
|
|
if (!video.__trackerPlaybackState) {
|
|
video.__trackerPlaybackState = {
|
|
startedAt: 0,
|
|
elapsedMs: 0,
|
|
seekFrom: null,
|
|
lastPosition: Number(video.currentTime) || 0,
|
|
};
|
|
}
|
|
return video.__trackerPlaybackState;
|
|
}
|
|
|
|
function beginPlayback(video) {
|
|
var playback = videoPlaybackState(video);
|
|
if (playback && !playback.startedAt) playback.startedAt = Date.now();
|
|
}
|
|
|
|
function settlePlayback(video) {
|
|
var playback = videoPlaybackState(video);
|
|
if (!playback || !playback.startedAt) return;
|
|
playback.elapsedMs += Math.max(0, Date.now() - playback.startedAt);
|
|
playback.startedAt = 0;
|
|
}
|
|
|
|
function playbackSeconds(video, keepDuration) {
|
|
var playback = videoPlaybackState(video);
|
|
if (!playback) return 0;
|
|
var now = Date.now();
|
|
var activeMs = playback.startedAt ? Math.max(0, now - playback.startedAt) : 0;
|
|
var seconds = Math.max(0, Math.round((playback.elapsedMs + activeMs) / 1000));
|
|
if (!keepDuration) {
|
|
playback.elapsedMs = 0;
|
|
if (playback.startedAt) playback.startedAt = now;
|
|
}
|
|
return seconds;
|
|
}
|
|
|
|
function videoMetrics(video, context, keepDuration) {
|
|
var duration = video && isFinite(video.duration) ? Math.round(video.duration) : context.drama_duration;
|
|
var current = video && isFinite(video.currentTime) ? Math.round(video.currentTime) : 0;
|
|
return {
|
|
duration: Math.max(0, duration),
|
|
current: Math.max(0, current),
|
|
played: playbackSeconds(video, keepDuration),
|
|
progress: duration > 0 ? Math.round(clamp(current / duration * 100, 0, 100)) : 0,
|
|
speed: Math.max(1, Math.round(((video && video.playbackRate) || 1) * 100)),
|
|
};
|
|
}
|
|
|
|
function trackDramaEvent(behaviorKey, video, options) {
|
|
var context = dramaContext();
|
|
if (!context) return;
|
|
options = options || {};
|
|
var metrics = videoMetrics(video, context, options.keepPlaybackDuration === true);
|
|
context.drama_duration = metrics.duration;
|
|
context.play_duration = metrics.played;
|
|
context.play_progress = options.forceProgress != null
|
|
? Math.round(clamp(options.forceProgress, 0, 100))
|
|
: behaviorKey === "ep_complete" ? 100 : metrics.progress;
|
|
context.play_speed = metrics.speed;
|
|
context.play_behavior_key = behaviorKey;
|
|
context.play_behavior_name = dramaBehaviorNames[behaviorKey] || behaviorKey;
|
|
return invoke("dramaEvent", context);
|
|
}
|
|
|
|
function trackDramaStatusChange(targetEpisode, targetVideoID, behaviorOverride) {
|
|
var context = dramaContext();
|
|
if (!context) return;
|
|
var nextEpisode = Math.max(1, integer(targetEpisode, context.episode));
|
|
var videoID = stringValue(targetVideoID, context.video_id);
|
|
var behavior = stringValue(behaviorOverride);
|
|
var names = {
|
|
ep_auto_play: "自动播放",
|
|
ep_previous: "上一集",
|
|
ep_next: "下一集",
|
|
ep_switch: "切换剧集",
|
|
ep_unlock: "解锁",
|
|
};
|
|
if (!behavior) {
|
|
if (nextEpisode === context.episode && videoID === context.video_id) return;
|
|
behavior = "ep_switch";
|
|
if (nextEpisode === context.episode + 1) behavior = "ep_next";
|
|
else if (nextEpisode === context.episode - 1) behavior = "ep_previous";
|
|
}
|
|
if (!names[behavior]) return;
|
|
return invoke("dramaStatusChange", {
|
|
media_id: context.media_id,
|
|
drama_id: context.drama_id,
|
|
drama_title: context.drama_title,
|
|
episode: nextEpisode,
|
|
video_id: videoID,
|
|
pre_episode: context.episode,
|
|
drama_behavior_key: behavior,
|
|
drama_behavior_name: names[behavior],
|
|
});
|
|
}
|
|
|
|
function trackDramaUnlock() {
|
|
var context = dramaContext();
|
|
if (!context) return Promise.resolve({ sent: false, ok: false, reason: "no_drama_context" });
|
|
return trackDramaStatusChange(context.episode, context.video_id, "ep_unlock");
|
|
}
|
|
|
|
function isMainDramaVideo(root, video) {
|
|
var mode = video && video.__trackerDramaMode;
|
|
if (!mode && root) {
|
|
mode = root.getAttribute("data-play-mode") === "preview" ? "preview" : "main";
|
|
}
|
|
return !!(
|
|
root &&
|
|
video &&
|
|
video.tagName === "VIDEO" &&
|
|
root.contains(video) &&
|
|
mode !== "preview"
|
|
);
|
|
}
|
|
|
|
function rememberDramaVideoMode(root, video) {
|
|
if (!root || !video || video.tagName !== "VIDEO" || !root.contains(video)) return;
|
|
video.__trackerDramaMode = root.getAttribute("data-play-mode") === "preview" ? "preview" : "main";
|
|
}
|
|
|
|
function prepareDramaModeChange(nextMode) {
|
|
var root = dramaRoot();
|
|
var video = root && root.querySelector("video");
|
|
if (!root || !video) return;
|
|
var previousMode = video.__trackerDramaMode ||
|
|
(root.getAttribute("data-play-mode") === "preview" ? "preview" : "main");
|
|
nextMode = nextMode === "preview" ? "preview" : "main";
|
|
if (previousMode === nextMode) return;
|
|
|
|
if (previousMode === "main") {
|
|
var playback = video.__trackerPlaybackState;
|
|
var wasPlaying = !!(playback && (playback.startedAt || playback.elapsedMs > 0));
|
|
settlePlayback(video);
|
|
if (wasPlaying) trackDramaEvent("ep_pause", video);
|
|
}
|
|
video.__trackerPlaybackState = null;
|
|
video.__trackerDramaMode = nextMode;
|
|
}
|
|
|
|
function bindDramaEvents() {
|
|
var root = dramaRoot();
|
|
if (!root) return;
|
|
if (!state.dramaViewSent) {
|
|
state.dramaViewSent = true;
|
|
trackDramaEvent("ep_view", null);
|
|
}
|
|
document.addEventListener("play", function (event) {
|
|
rememberDramaVideoMode(root, event.target);
|
|
if (isMainDramaVideo(root, event.target)) {
|
|
trackDramaEvent("ep_play", event.target);
|
|
}
|
|
}, true);
|
|
document.addEventListener("playing", function (event) {
|
|
rememberDramaVideoMode(root, event.target);
|
|
if (isMainDramaVideo(root, event.target)) beginPlayback(event.target);
|
|
}, true);
|
|
document.addEventListener("loadedmetadata", function (event) {
|
|
rememberDramaVideoMode(root, event.target);
|
|
}, true);
|
|
document.addEventListener("waiting", function (event) {
|
|
if (isMainDramaVideo(root, event.target)) settlePlayback(event.target);
|
|
}, true);
|
|
document.addEventListener("pause", function (event) {
|
|
if (isMainDramaVideo(root, event.target) && !event.target.ended) {
|
|
var playback = event.target.__trackerPlaybackState;
|
|
var wasPlaying = !!(playback && (playback.startedAt || playback.elapsedMs > 0));
|
|
settlePlayback(event.target);
|
|
if (event.target.seeking || !wasPlaying) return;
|
|
trackDramaEvent("ep_pause", event.target);
|
|
}
|
|
}, true);
|
|
document.addEventListener("timeupdate", function (event) {
|
|
if (!isMainDramaVideo(root, event.target) || event.target.seeking) return;
|
|
videoPlaybackState(event.target).lastPosition = Number(event.target.currentTime) || 0;
|
|
}, true);
|
|
document.addEventListener("seeking", function (event) {
|
|
if (!isMainDramaVideo(root, event.target)) return;
|
|
var playback = videoPlaybackState(event.target);
|
|
if (playback.seekFrom != null) return;
|
|
settlePlayback(event.target);
|
|
playback.seekFrom = playback.lastPosition;
|
|
}, true);
|
|
document.addEventListener("seeked", function (event) {
|
|
if (!isMainDramaVideo(root, event.target)) return;
|
|
var playback = videoPlaybackState(event.target);
|
|
if (playback.seekFrom == null) return;
|
|
var from = playback.seekFrom;
|
|
var target = Number(event.target.currentTime) || 0;
|
|
playback.seekFrom = null;
|
|
playback.lastPosition = target;
|
|
if (Math.abs(target - from) >= 0.5) {
|
|
trackDramaEvent(target > from ? "ep_forward" : "ep_rewind", event.target);
|
|
}
|
|
if (!event.target.paused && !event.target.ended) beginPlayback(event.target);
|
|
}, true);
|
|
document.addEventListener("ended", function (event) {
|
|
if (isMainDramaVideo(root, event.target)) {
|
|
settlePlayback(event.target);
|
|
trackDramaEvent("ep_complete", event.target, { keepPlaybackDuration: true });
|
|
trackDramaEvent("ep_play", event.target, { forceProgress: 100 });
|
|
}
|
|
}, true);
|
|
|
|
function reportPendingDramaProgress() {
|
|
var video = root.querySelector("video");
|
|
if (!isMainDramaVideo(root, video)) return false;
|
|
var playback = video.__trackerPlaybackState;
|
|
if (!playback || (!playback.startedAt && playback.elapsedMs <= 0)) return false;
|
|
var pendingMs = playback.elapsedMs +
|
|
(playback.startedAt ? Math.max(0, Date.now() - playback.startedAt) : 0);
|
|
if (pendingMs < 500) return false;
|
|
settlePlayback(video);
|
|
trackDramaEvent("ep_play", video);
|
|
return true;
|
|
}
|
|
|
|
function flushSDKAfterEnqueue() {
|
|
if (!global.WebSDK || typeof global.WebSDK.flush !== "function") return;
|
|
function flush() {
|
|
Promise.resolve(global.WebSDK.flush()).catch(logError);
|
|
}
|
|
Promise.resolve().then(function () {
|
|
return Promise.resolve();
|
|
}).then(flush);
|
|
global.setTimeout(flush, 50);
|
|
global.setTimeout(flush, 200);
|
|
}
|
|
|
|
document.addEventListener("visibilitychange", function () {
|
|
var video = root.querySelector("video");
|
|
if (document.visibilityState === "hidden") {
|
|
if (reportPendingDramaProgress()) flushSDKAfterEnqueue();
|
|
if (isMainDramaVideo(root, video) && !video.paused && !video.ended) beginPlayback(video);
|
|
return;
|
|
}
|
|
if (document.visibilityState === "visible" && isMainDramaVideo(root, video) &&
|
|
!video.paused && !video.ended) {
|
|
beginPlayback(video);
|
|
}
|
|
});
|
|
global.addEventListener("beforeunload", function () {
|
|
if (reportPendingDramaProgress()) flushSDKAfterEnqueue();
|
|
});
|
|
global.addEventListener("pagehide", function () {
|
|
if (reportPendingDramaProgress()) flushSDKAfterEnqueue();
|
|
});
|
|
}
|
|
|
|
function bindSearchEvents() {
|
|
var container = document.querySelector("[data-search-results]");
|
|
if (!container) return;
|
|
var keyword = stringValue(container.getAttribute("data-search-keyword"));
|
|
if (!keyword) return;
|
|
var grid = container.querySelector("[data-search-grid]");
|
|
var cards = grid ? Array.prototype.slice.call(grid.querySelectorAll("[data-search-item-id]")) : [];
|
|
var resultCount = Math.max(0, integer(container.getAttribute("data-search-result-count"), cards.length));
|
|
var pageNum = Math.max(1, integer(container.getAttribute("data-search-page"), 1));
|
|
var pageSize = Math.max(1, integer(container.getAttribute("data-search-page-size"), cards.length || 1));
|
|
if (pageNum === 1) {
|
|
invoke("keywordSearch", {
|
|
keyword: keyword,
|
|
search_result_count: resultCount,
|
|
search_content_type: "",
|
|
search_trace_id: "",
|
|
search_id: "",
|
|
});
|
|
}
|
|
if (!grid) return;
|
|
grid.addEventListener("click", function (event) {
|
|
var target = event.target && event.target.closest ? event.target : null;
|
|
var resultLink = target && target.closest("[data-search-result-click]");
|
|
var card = resultLink && resultLink.closest("[data-search-item-id]");
|
|
if (!card || !grid.contains(card)) return;
|
|
var position = cards.indexOf(card);
|
|
if (position < 0) return;
|
|
invoke("keywordClick", {
|
|
keyword: keyword,
|
|
click_item_id: stringValue(card.getAttribute("data-search-item-id")),
|
|
click_item_type_key: "short_drama",
|
|
click_item_type_name: "短剧",
|
|
click_position: (pageNum - 1) * pageSize + position + 1,
|
|
search_trace_id: "",
|
|
});
|
|
}, true);
|
|
}
|
|
|
|
function bindClicks() {
|
|
document.addEventListener("click", function (event) {
|
|
var target = event.target && event.target.closest ? event.target : null;
|
|
if (!target) return;
|
|
var popupClose = target.closest(
|
|
"[data-dialog-ad-close],[data-ad-close],[data-register-close]," +
|
|
"[data-download-close],[data-login-close],[data-recharge-close]," +
|
|
"[data-play-coin-close],[data-setting-password-close],[data-drawer-close]"
|
|
);
|
|
if (!popupClose) trackPageClick(event);
|
|
var switcher = target.closest("[data-drama-switch]");
|
|
if (switcher) {
|
|
trackDramaStatusChange(switcher.getAttribute("data-target-episode"), switcher.getAttribute("data-target-video-id"));
|
|
}
|
|
if (target.closest("[data-share-page]") && dramaRoot()) {
|
|
var root = dramaRoot();
|
|
var video = root.querySelector("video");
|
|
trackDramaEvent("ep_share", isMainDramaVideo(root, video) ? video : null);
|
|
}
|
|
var ad = target.closest('[data-track-ad="external"][data-ad-clickable="1"][data-ad-id]');
|
|
if (ad) {
|
|
trackExternalAd(ad, "click");
|
|
}
|
|
trackNavigation(target.closest("a[data-navigation-key],a[data-nav-key]"));
|
|
}, true);
|
|
}
|
|
|
|
function onReady() {
|
|
whenDOMReady(function () {
|
|
Promise.resolve(loadCommonFields()).then(function () {
|
|
trackPageView();
|
|
});
|
|
bindClicks();
|
|
bindAds();
|
|
bindExternalAdEvents();
|
|
bindAdvertisingEvents();
|
|
bindDramaEvents();
|
|
bindSearchEvents();
|
|
try {
|
|
global.dispatchEvent(new CustomEvent("tracker:ready", { detail: state.page }));
|
|
} catch (error) {}
|
|
});
|
|
}
|
|
|
|
|
|
var COMMON_FIELD_KEYS = [
|
|
"sid",
|
|
"device_id",
|
|
"device",
|
|
"device_fingerprint",
|
|
"fp_version",
|
|
];
|
|
var __commonFields = null;
|
|
var __commonFieldsReady = null;
|
|
|
|
function emptyCommonFields() {
|
|
var out = {};
|
|
for (var i = 0; i < COMMON_FIELD_KEYS.length; i++) {
|
|
out[COMMON_FIELD_KEYS[i]] = "";
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function normalizeCommonFields(fields) {
|
|
var out = emptyCommonFields();
|
|
fields = fields || {};
|
|
for (var i = 0; i < COMMON_FIELD_KEYS.length; i++) {
|
|
var k = COMMON_FIELD_KEYS[i];
|
|
if (fields[k] != null) out[k] = String(fields[k]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function publishCommonFields(fields) {
|
|
__commonFields = normalizeCommonFields(fields);
|
|
try {
|
|
global.__trackerCommonFields = __commonFields;
|
|
} catch (e) {}
|
|
try {
|
|
if (typeof CustomEvent === "function") {
|
|
global.dispatchEvent(
|
|
new CustomEvent("tracker:common-fields", { detail: __commonFields })
|
|
);
|
|
}
|
|
} catch (e) {}
|
|
return __commonFields;
|
|
}
|
|
|
|
function loadCommonFields() {
|
|
if (__commonFieldsReady) return __commonFieldsReady;
|
|
__commonFieldsReady = new Promise(function (resolve) {
|
|
function finish(fields) {
|
|
resolve(publishCommonFields(fields));
|
|
}
|
|
if (!global.WebSDK || typeof global.WebSDK.getCommonFields !== "function") {
|
|
finish(emptyCommonFields());
|
|
return;
|
|
}
|
|
try {
|
|
Promise.resolve(global.WebSDK.getCommonFields(COMMON_FIELD_KEYS.slice()))
|
|
.then(function (fields) {
|
|
finish(fields || emptyCommonFields());
|
|
})
|
|
.catch(function (err) {
|
|
logError(err);
|
|
finish(emptyCommonFields());
|
|
});
|
|
} catch (err) {
|
|
logError(err);
|
|
finish(emptyCommonFields());
|
|
}
|
|
});
|
|
return __commonFieldsReady;
|
|
}
|
|
|
|
function getCommonFieldsCached(keys) {
|
|
return loadCommonFields().then(function (all) {
|
|
if (!Array.isArray(keys) || !keys.length) return all;
|
|
var out = {};
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = String(keys[i] || "").trim();
|
|
if (k) out[k] = Object.prototype.hasOwnProperty.call(all, k) ? all[k] : "";
|
|
}
|
|
return out;
|
|
});
|
|
}
|
|
|
|
function getCommonFieldCached(key) {
|
|
var k = String(key || "").trim();
|
|
if (!k) return Promise.resolve("");
|
|
return getCommonFieldsCached([k]).then(function (m) {
|
|
return m[k] || "";
|
|
});
|
|
}
|
|
|
|
function encodeSDKCommonHeader(fields) {
|
|
var payload = normalizeCommonFields(fields);
|
|
try {
|
|
return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
|
|
} catch (e) {
|
|
logError(e);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function getSDKCommonHeader() {
|
|
return loadCommonFields().then(function (fields) {
|
|
return encodeSDKCommonHeader(fields);
|
|
});
|
|
}
|
|
|
|
|
|
global.trackerInit = init;
|
|
global.tracker = {
|
|
init: init,
|
|
pageView: trackPageView,
|
|
dramaEvent: trackDramaEvent,
|
|
dramaStatusChange: trackDramaStatusChange,
|
|
dramaUnlock: trackDramaUnlock,
|
|
prepareDramaModeChange: prepareDramaModeChange,
|
|
advertising: function (payload) { return invoke("advertising", payload || {}); },
|
|
externalAd: trackExternalAd,
|
|
getDramaContext: function () {
|
|
var context = dramaContext();
|
|
return context ? Object.assign({}, context) : {};
|
|
},
|
|
keywordSearch: function (payload) { return invoke("keywordSearch", payload || {}); },
|
|
keywordClick: function (payload) { return invoke("keywordClick", payload || {}); },
|
|
getCommonFields: getCommonFieldsCached,
|
|
getCommonField: getCommonFieldCached,
|
|
getCommonFieldsSync: function () { return __commonFields; },
|
|
encodeSDKCommonHeader: encodeSDKCommonHeader,
|
|
getSDKCommonHeader: getSDKCommonHeader,
|
|
flush: function () {
|
|
if (!global.WebSDK || typeof global.WebSDK.flush !== "function") return Promise.resolve();
|
|
return Promise.resolve(global.WebSDK.flush()).catch(logError);
|
|
},
|
|
setUid: function (uid) {
|
|
if (global.WebSDK && global.WebSDK.setUid) global.WebSDK.setUid(normalizeUid(uid));
|
|
},
|
|
setChannel: function (channel) {
|
|
if (global.WebSDK && global.WebSDK.setChannel) global.WebSDK.setChannel(currentChannel(channel));
|
|
},
|
|
setContext: function (context) {
|
|
if (global.WebSDK && global.WebSDK.setContext) global.WebSDK.setContext(context || {});
|
|
},
|
|
};
|
|
|
|
init(global.__TRACKER_BOOT__ || {});
|
|
})(window);
|