mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 02:21:20 -08:00
feat: batch video upscaling with folder upload, directory save, and result comparison (#1051)
* feat: support batch video upscaling with folder upload and directory save - Add multi-file selection and entire folder upload (webkitdirectory) - Support drag & drop of both files and folders via FileSystemEntry API - Add save directory picker using File System Access API (showDirectoryPicker) - Process videos sequentially with per-file progress and status tracking - Add right-side result list with click-to-compare functionality - Add per-video duration and total batch duration display - Add retry button for failed items with error detail display - Remove auto-play on comparison viewer load - Fix worker error handling: handle "error" command to reject stale promises - Fix event listener accumulation in video comparison (AbortController) - Fix XSS risk by escaping HTML in all innerHTML injections - Fix URL revocation timing in download to prevent large file truncation - Extract writeToSaveDirectory and truncateName helpers to reduce duplication - Add Usage Guide panel explaining single/multiple/folder upload options Files changed: docs/upscaler/index.html (+1110/-181) docs/upscaler/upscaler.js (+110) * fix: prevent auto-play and resource consumption during upscaling - Set preload="metadata" on preview and comparison video elements - Explicitly pause preview video after setting src in showVideoPreview - Remove preview video seek sync from batch processing progress callbacks (was causing continuous decode during upscaling, wasting GPU resources) - Remove preview video seek sync from retry progress callback - All videos now require manual play; no background resource usage * fix: improve upscaled video quality with higher bitrate and codec support - Raise base bitrate from 2.5 Mbps (25e5) to 8 Mbps (8e6) for standard and firefox processors, preventing quality loss when source videos have high bitrates (e.g. 720p @ 16.7 Mbps was producing 1440p @ 10 Mbps, smaller than the original file) - Double the bitrate for mediabunny processor (X.n1 * 2) to account for the 4x pixel increase from 2x upscaling - Add H.264 Level 5.1 (avc1.640033) and Level 5.2 (avc1.640034) codecs to the fallback lists, since Level 4.0 (avc1.640028, ~24.6 Mbps) cannot support the higher bitrates for 1440p+ outputs - Add latencyMode: "quality" to VideoEncoder configs for both standard and firefox processors, prioritizing encoding quality over latency (suitable for offline processing) * feat: add video data comparison panel for original vs upscaled metrics - Add a comparison data table below the file info bar showing 6 metrics: Resolution, File Size, Duration, Est. Bitrate, Pixels, Process Time - Display per-metric change percentage with color-coded indicators: green (↑) for improvements, red (↓) for regressions, gray for neutral - Estimated bitrate calculated from file size × 8 / video duration - Table renders after both original and upscaled video metadata loads - Auto-hide panel on reset * fix: make video data comparison panel full-width and responsive - Move comparison data table from inside the left column to the full-width area below the left/right layout, so it is no longer constrained by the comparison viewer column width - Remove table-fixed colgroup and overflow-x-auto that caused horizontal scrolling on smaller screens - Table now uses w-full auto-layout, displaying all 4 columns (Metric, Original, Upscaled, Change) without requiring horizontal scrolling --------- Co-authored-by: rionzhao <rionzhao@tencent.com>
This commit is contained in:
commit
0896841d23
538 files changed
+379715
No files matched your search
@@ -0,0 +1,246 @@
|
||||
var artplayerPluginAds = (function() {
|
||||
"use strict";
|
||||
const style = ".artplayer-plugin-ads {\n position: absolute;\n z-index: 150;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-size: 13px;\n line-height: 1;\n color: #fff;\n background-color: #000;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-html {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-video {\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer {\n display: none;\n position: absolute;\n top: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control {\n display: none;\n position: absolute;\n bottom: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg {\n width: 20px;\n height: 20px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n}\n";
|
||||
function checkVersion(art) {
|
||||
const {
|
||||
version,
|
||||
utils: { errorHandle }
|
||||
} = art.constructor;
|
||||
const arr = version.split(".").map(Number);
|
||||
const major = arr[0];
|
||||
const minor = arr[1] / 100;
|
||||
errorHandle(
|
||||
major + minor >= 5,
|
||||
`Artplayer.js@${version} is not compatible the artplayerPluginAds@${artplayerPluginAds2.version}. Please update it to version Artplayer.js@5.x.x`
|
||||
);
|
||||
}
|
||||
function artplayerPluginAds2(option) {
|
||||
return (art) => {
|
||||
checkVersion(art);
|
||||
const {
|
||||
template: { $player },
|
||||
icons: { volume, volumeClose, fullscreenOn, fullscreenOff, loading },
|
||||
constructor: {
|
||||
validator,
|
||||
utils: { query, append, setStyle }
|
||||
}
|
||||
} = art;
|
||||
option = validator(
|
||||
{
|
||||
html: "",
|
||||
video: "",
|
||||
url: "",
|
||||
playDuration: 5,
|
||||
totalDuration: 10,
|
||||
muted: false,
|
||||
i18n: {
|
||||
close: "关闭广告",
|
||||
countdown: "%s秒",
|
||||
detail: "查看详情",
|
||||
canBeClosed: "%s秒后可关闭广告"
|
||||
},
|
||||
...option
|
||||
},
|
||||
{
|
||||
html: "?string",
|
||||
video: "?string",
|
||||
url: "?string",
|
||||
playDuration: "number",
|
||||
totalDuration: "number",
|
||||
muted: "?boolean",
|
||||
i18n: {
|
||||
close: "string",
|
||||
countdown: "string",
|
||||
detail: "string",
|
||||
canBeClosed: "string"
|
||||
}
|
||||
}
|
||||
);
|
||||
let $ads = null;
|
||||
let $timer = null;
|
||||
let $close = null;
|
||||
let $countdown = null;
|
||||
let $control = null;
|
||||
let $loading = null;
|
||||
let time = 0;
|
||||
let timer = null;
|
||||
let isEnd = false;
|
||||
let isInit = false;
|
||||
let isCanClose = false;
|
||||
function getI18n(val, str) {
|
||||
return str.replace("%s", val);
|
||||
}
|
||||
function skip() {
|
||||
isEnd = true;
|
||||
art.play();
|
||||
if (option.video)
|
||||
$ads.pause();
|
||||
setStyle(art.template.$ads, "display", "none");
|
||||
art.emit("artplayerPluginAds:skip", option);
|
||||
}
|
||||
function play() {
|
||||
if (isEnd)
|
||||
return;
|
||||
timer = setTimeout(() => {
|
||||
time += 1;
|
||||
const playDuration = option.playDuration - time;
|
||||
if (playDuration >= 1) {
|
||||
$close.innerHTML = getI18n(playDuration, option.i18n.canBeClosed);
|
||||
} else {
|
||||
$close.innerHTML = option.i18n.close;
|
||||
if (!isCanClose) {
|
||||
isCanClose = true;
|
||||
}
|
||||
}
|
||||
$countdown.innerHTML = getI18n(option.totalDuration - time, option.i18n.countdown);
|
||||
if (time >= option.totalDuration) {
|
||||
skip();
|
||||
} else {
|
||||
play();
|
||||
}
|
||||
}, 1e3);
|
||||
}
|
||||
function pause() {
|
||||
if (isEnd)
|
||||
return;
|
||||
clearTimeout(timer);
|
||||
}
|
||||
function show() {
|
||||
art.template.$ads = append($player, '<div class="artplayer-plugin-ads"></div>');
|
||||
$ads = append(
|
||||
art.template.$ads,
|
||||
option.video ? `<video class="artplayer-plugin-ads-video" src="${option.video}" loop playsInline></video>` : `<div class="artplayer-plugin-ads-html">${option.html}</div>`
|
||||
);
|
||||
$loading = append(art.template.$ads, '<div class="artplayer-plugin-ads-loading"></div>');
|
||||
append($loading, loading);
|
||||
$timer = append(
|
||||
art.template.$ads,
|
||||
`<div class="artplayer-plugin-ads-timer">
|
||||
<div class="artplayer-plugin-ads-close">${option.playDuration <= 0 ? option.i18n.close : getI18n(option.playDuration, option.i18n.canBeClosed)}</div>
|
||||
<div class="artplayer-plugin-ads-countdown">${getI18n(
|
||||
option.totalDuration,
|
||||
option.i18n.countdown
|
||||
)}</div>
|
||||
</div>`
|
||||
);
|
||||
$close = query(".artplayer-plugin-ads-close", $timer);
|
||||
$countdown = query(".artplayer-plugin-ads-countdown", $timer);
|
||||
if (option.playDuration >= option.totalDuration) {
|
||||
setStyle($close, "display", "none");
|
||||
}
|
||||
art.proxy($close, "click", () => {
|
||||
if (isCanClose) {
|
||||
skip();
|
||||
}
|
||||
});
|
||||
$control = append(
|
||||
art.template.$ads,
|
||||
`<div class="artplayer-plugin-ads-control">
|
||||
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
|
||||
<div class="artplayer-plugin-ads-muted"></div>
|
||||
<div class="artplayer-plugin-ads-fullscreen"></div>
|
||||
</div>`
|
||||
);
|
||||
const $detail = query(".artplayer-plugin-ads-detail", $control);
|
||||
const $muted = query(".artplayer-plugin-ads-muted", $control);
|
||||
const $fullscreen = query(".artplayer-plugin-ads-fullscreen", $control);
|
||||
if (option.url) {
|
||||
art.proxy($detail, "click", () => {
|
||||
window.open(option.url);
|
||||
art.emit("artplayerPluginAds:click", option);
|
||||
});
|
||||
} else {
|
||||
setStyle($detail, "display", "none");
|
||||
}
|
||||
if (option.video) {
|
||||
const $volume = append($muted, volume);
|
||||
const $volumeClose = append($muted, volumeClose);
|
||||
setStyle($volumeClose, "display", "none");
|
||||
if (option.muted) {
|
||||
$ads.muted = true;
|
||||
setStyle($volume, "display", "none");
|
||||
setStyle($volumeClose, "display", "inline-flex");
|
||||
}
|
||||
art.proxy($muted, "click", () => {
|
||||
$ads.muted = !$ads.muted;
|
||||
if ($ads.muted) {
|
||||
setStyle($volume, "display", "none");
|
||||
setStyle($volumeClose, "display", "inline-flex");
|
||||
} else {
|
||||
setStyle($volume, "display", "inline-flex");
|
||||
setStyle($volumeClose, "display", "none");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStyle($muted, "display", "none");
|
||||
}
|
||||
const $fullscreenOn = append($fullscreen, fullscreenOn);
|
||||
const $fullscreenOff = append($fullscreen, fullscreenOff);
|
||||
setStyle($fullscreenOff, "display", "none");
|
||||
art.proxy($fullscreen, "click", () => {
|
||||
art.fullscreen = !art.fullscreen;
|
||||
if (art.fullscreen) {
|
||||
setStyle($fullscreenOn, "display", "inline-flex");
|
||||
setStyle($fullscreenOff, "display", "none");
|
||||
} else {
|
||||
setStyle($fullscreenOn, "display", "none");
|
||||
setStyle($fullscreenOff, "display", "inline-flex");
|
||||
}
|
||||
});
|
||||
art.proxy($ads, "click", () => {
|
||||
if (option.url)
|
||||
window.open(option.url);
|
||||
art.emit("artplayerPluginAds:click", option);
|
||||
});
|
||||
}
|
||||
function init() {
|
||||
if (isInit)
|
||||
return;
|
||||
isInit = true;
|
||||
show();
|
||||
art.pause();
|
||||
if (option.video) {
|
||||
art.proxy($ads, "error", skip);
|
||||
art.proxy($ads, "loadedmetadata", () => {
|
||||
play();
|
||||
$ads.play();
|
||||
setStyle($timer, "display", "flex");
|
||||
setStyle($control, "display", "flex");
|
||||
setStyle($loading, "display", "none");
|
||||
});
|
||||
} else {
|
||||
play();
|
||||
setStyle($timer, "display", "flex");
|
||||
setStyle($control, "display", "flex");
|
||||
setStyle($loading, "display", "none");
|
||||
}
|
||||
art.on("document:visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
pause();
|
||||
} else {
|
||||
play();
|
||||
}
|
||||
});
|
||||
}
|
||||
art.on("ready", () => {
|
||||
art.once("play", init);
|
||||
art.once("video:playing", init);
|
||||
});
|
||||
return {
|
||||
name: "artplayerPluginAds",
|
||||
skip,
|
||||
pause,
|
||||
play
|
||||
};
|
||||
};
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
if (!document.getElementById("artplayer-plugin-ads")) {
|
||||
const $style = document.createElement("style");
|
||||
$style.id = "artplayer-plugin-ads";
|
||||
$style.textContent = style;
|
||||
document.head.appendChild($style);
|
||||
}
|
||||
}
|
||||
return artplayerPluginAds2;
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
var artplayerPluginAmbilight = (function() {
|
||||
"use strict";
|
||||
function artplayerPluginAmbilight2(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template;
|
||||
const { createElement, addClass, setStyles } = art.constructor.utils;
|
||||
const { blur = "50px", opacity = 0.5, frequency = 10, duration = 0.3 } = option;
|
||||
const $ambilight = createElement("div");
|
||||
const gridItems = createGridItems($ambilight);
|
||||
setupAmbilight($ambilight, $video);
|
||||
setupGridItems(gridItems, blur, opacity, duration);
|
||||
const updateColors = createColorUpdater($video, gridItems, frequency);
|
||||
let animationFrameId = null;
|
||||
function start() {
|
||||
if (!animationFrameId) {
|
||||
updateColors();
|
||||
}
|
||||
}
|
||||
function stop() {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
}
|
||||
art.on("ready", start);
|
||||
art.on("destroy", stop);
|
||||
return {
|
||||
name: "artplayerPluginAmbilight",
|
||||
start,
|
||||
stop
|
||||
};
|
||||
function createGridItems($ambilight2) {
|
||||
$ambilight2.innerHTML = Array.from({ length: 9 }).fill("<div></div>").join("");
|
||||
return Array.from($ambilight2.children);
|
||||
}
|
||||
function setupAmbilight($ambilight2, $video2) {
|
||||
addClass($ambilight2, "artplayer-plugin-ambilight");
|
||||
$video2.parentNode.insertBefore($ambilight2, $video2);
|
||||
setStyles($ambilight2, {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 9,
|
||||
inset: 0,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr 1fr",
|
||||
gridTemplateRows: "1fr 1fr 1fr"
|
||||
});
|
||||
}
|
||||
function setupGridItems(gridItems2, blur2, opacity2, duration2) {
|
||||
gridItems2.forEach(($item) => {
|
||||
setStyles($item, {
|
||||
opacity: opacity2,
|
||||
filter: `blur(${blur2})`,
|
||||
transition: `background-color ${duration2}s ease`
|
||||
});
|
||||
});
|
||||
}
|
||||
function createColorUpdater($video2, gridItems2, frequency2) {
|
||||
const canvas = createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = 3;
|
||||
canvas.height = 3;
|
||||
function getAverageColor(x, y, w, h) {
|
||||
ctx.drawImage($video2, x, y, w, h, 0, 0, 1, 1);
|
||||
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
let lastUpdateTime = 0;
|
||||
return function updateColors2() {
|
||||
const now = performance.now();
|
||||
if (now - lastUpdateTime < 1e3 / frequency2 || !art.playing) {
|
||||
animationFrameId = requestAnimationFrame(updateColors2);
|
||||
return;
|
||||
}
|
||||
lastUpdateTime = now;
|
||||
const w = $video2.videoWidth / 3;
|
||||
const h = $video2.videoHeight / 3;
|
||||
const colors = [
|
||||
[0, 0],
|
||||
[w, 0],
|
||||
[2 * w, 0],
|
||||
[0, h],
|
||||
[w, h],
|
||||
[2 * w, h],
|
||||
[0, 2 * h],
|
||||
[w, 2 * h],
|
||||
[2 * w, 2 * h]
|
||||
].map(([x, y]) => getAverageColor(x, y, w, h));
|
||||
gridItems2.forEach(($item, index) => {
|
||||
$item.style.backgroundColor = colors[index];
|
||||
});
|
||||
animationFrameId = requestAnimationFrame(updateColors2);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
return artplayerPluginAmbilight2;
|
||||
})();
|
||||
@@ -0,0 +1,247 @@
|
||||
var artplayerPluginAsr = (function() {
|
||||
"use strict";
|
||||
const style = ".art-video-player .art-layer-asr {\n pointer-events: none !important;\n position: absolute;\n z-index: 150;\n inset: 0px;\n display: flex;\n flex-direction: column;\n justify-content: end;\n padding: 0 2%;\n font-size: 1rem;\n gap: var(--art-subtitle-gap);\n padding-bottom: var(--art-subtitle-bottom);\n transition: padding-bottom var(--art-transition-duration) ease;\n text-shadow: var(--art-subtitle-border) 1px 0 1px, var(--art-subtitle-border) 0 1px 1px, var(--art-subtitle-border) -1px 0 1px, var(--art-subtitle-border) 0 -1px 1px, var(--art-subtitle-border) 1px 1px 1px, var(--art-subtitle-border) -1px -1px 1px, var(--art-subtitle-border) 1px -1px 1px, var(--art-subtitle-border) -1px 1px 1px;\n}\n.art-video-player.art-control-show .art-layer-asr {\n padding-bottom: calc(var(--art-control-height) + var(--art-subtitle-bottom));\n}\n";
|
||||
function artplayerPluginAsr2(option = {}) {
|
||||
const {
|
||||
length = 3,
|
||||
interval = 100,
|
||||
sampleRate = 16e3,
|
||||
autoHideTimeout = 1e4,
|
||||
onAudioChunk = () => null
|
||||
} = option;
|
||||
return (art) => {
|
||||
let started = false;
|
||||
let audioCtx = null;
|
||||
let sourceNode = null;
|
||||
let recorderNode = null;
|
||||
let gainNode = null;
|
||||
let bufferChunks = [];
|
||||
let timer = null;
|
||||
let workletLoaded = false;
|
||||
let hideTimer = null;
|
||||
let mediaStream = null;
|
||||
let mediaStreamSource = null;
|
||||
const $asr = art.layers.add({
|
||||
name: "asr",
|
||||
html: ""
|
||||
});
|
||||
function splitByPunctuation(text) {
|
||||
return text.split(/(?<=[、。!?!?.])\s*/u).map((s) => s.trim()).filter(Boolean).slice(-length);
|
||||
}
|
||||
function hide() {
|
||||
$asr.style.display = "none";
|
||||
}
|
||||
function append(subtitle) {
|
||||
if (typeof subtitle !== "string")
|
||||
return;
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(hide, autoHideTimeout);
|
||||
$asr.style.display = "";
|
||||
$asr.innerHTML = splitByPunctuation(subtitle).map((line) => `<div class="art-asr-line">${line}</div>`).join("");
|
||||
}
|
||||
const recorderProcessorCode = `
|
||||
class RecorderProcessor extends AudioWorkletProcessor {
|
||||
process(inputs) {
|
||||
const input = inputs[0];
|
||||
if (input && input[0]) {
|
||||
this.port.postMessage(input[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
registerProcessor('recorder-processor', RecorderProcessor);
|
||||
`;
|
||||
const createWorkletBlobUrl = () => {
|
||||
const blob = new Blob([recorderProcessorCode], { type: "application/javascript" });
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
function floatTo16BitPCM(float32Array) {
|
||||
const len = float32Array.length;
|
||||
const buffer = new ArrayBuffer(len * 2);
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const s = Math.max(-1, Math.min(1, float32Array[i]));
|
||||
view.setInt16(i * 2, s < 0 ? s * 32768 : s * 32767, true);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
function pcmToWav(pcmBuffer, sampleRate2) {
|
||||
const pcmLength = pcmBuffer.byteLength;
|
||||
const wavBuffer = new ArrayBuffer(44 + pcmLength);
|
||||
const view = new DataView(wavBuffer);
|
||||
const writeString = (offset, str) => {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
view.setUint8(offset + i, str.charCodeAt(i));
|
||||
}
|
||||
};
|
||||
writeString(0, "RIFF");
|
||||
view.setUint32(4, 36 + pcmLength, true);
|
||||
writeString(8, "WAVE");
|
||||
writeString(12, "fmt ");
|
||||
view.setUint32(16, 16, true);
|
||||
view.setUint16(20, 1, true);
|
||||
view.setUint16(22, 1, true);
|
||||
view.setUint32(24, sampleRate2, true);
|
||||
view.setUint32(28, sampleRate2 * 2, true);
|
||||
view.setUint16(32, 2, true);
|
||||
view.setUint16(34, 16, true);
|
||||
writeString(36, "data");
|
||||
view.setUint32(40, pcmLength, true);
|
||||
new Uint8Array(wavBuffer).set(new Uint8Array(pcmBuffer), 44);
|
||||
return wavBuffer;
|
||||
}
|
||||
async function setupAudioContext() {
|
||||
if (audioCtx)
|
||||
return audioCtx;
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate });
|
||||
if (audioCtx.state === "suspended")
|
||||
await audioCtx.resume();
|
||||
return audioCtx;
|
||||
}
|
||||
async function setupAudioSource() {
|
||||
if (!sourceNode && art.video) {
|
||||
try {
|
||||
sourceNode = audioCtx.createMediaElementSource(art.video);
|
||||
return sourceNode;
|
||||
} catch (err) {
|
||||
console.warn("[artplayerPluginAsr] Direct connection failed:", err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!mediaStream) {
|
||||
mediaStream = await captureAudioFromVideo(art.video);
|
||||
}
|
||||
if (mediaStream) {
|
||||
mediaStreamSource = audioCtx.createMediaStreamSource(mediaStream);
|
||||
return mediaStreamSource;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[artplayerPluginAsr] MediaStream fallback failed:", err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function captureAudioFromVideo(videoElement) {
|
||||
try {
|
||||
if (videoElement.captureStream)
|
||||
return videoElement.captureStream();
|
||||
if (videoElement.mozCaptureStream)
|
||||
return videoElement.mozCaptureStream();
|
||||
console.warn("[artplayerPluginAsr] captureStream not supported");
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn("[artplayerPluginAsr] Error capturing stream:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function startCapture() {
|
||||
if (started)
|
||||
return;
|
||||
try {
|
||||
await setupAudioContext();
|
||||
const audioSource = await setupAudioSource();
|
||||
if (!audioSource)
|
||||
throw new Error("Could not establish audio source");
|
||||
if (!workletLoaded) {
|
||||
const blobUrl = createWorkletBlobUrl();
|
||||
await audioCtx.audioWorklet.addModule(blobUrl);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
workletLoaded = true;
|
||||
}
|
||||
gainNode = audioCtx.createGain();
|
||||
gainNode.gain.value = 1;
|
||||
recorderNode = new AudioWorkletNode(audioCtx, "recorder-processor");
|
||||
recorderNode.port.onmessage = (event) => {
|
||||
bufferChunks.push(new Float32Array(event.data));
|
||||
};
|
||||
audioSource.connect(recorderNode);
|
||||
audioSource.connect(gainNode);
|
||||
gainNode.connect(audioCtx.destination);
|
||||
const CHUNK_SAMPLES = sampleRate * (interval / 1e3);
|
||||
timer = setInterval(async () => {
|
||||
if (bufferChunks.length === 0)
|
||||
return;
|
||||
let accumulated = new Float32Array(0);
|
||||
while (accumulated.length < CHUNK_SAMPLES && bufferChunks.length) {
|
||||
const next = bufferChunks.shift();
|
||||
const tmp = new Float32Array(accumulated.length + next.length);
|
||||
tmp.set(accumulated, 0);
|
||||
tmp.set(next, accumulated.length);
|
||||
accumulated = tmp;
|
||||
}
|
||||
if (accumulated.length < CHUNK_SAMPLES)
|
||||
return;
|
||||
const chunkToSend = accumulated.slice(0, CHUNK_SAMPLES);
|
||||
const pcm = floatTo16BitPCM(chunkToSend);
|
||||
const wav = pcmToWav(pcm, sampleRate);
|
||||
const subtitle = await onAudioChunk({ pcm, wav });
|
||||
append(subtitle);
|
||||
}, interval);
|
||||
started = true;
|
||||
} catch (err) {
|
||||
console.error("[artplayerPluginAsr] Initialization failed:", err);
|
||||
await stopCapture();
|
||||
}
|
||||
}
|
||||
async function stopCapture() {
|
||||
if (!started)
|
||||
return;
|
||||
started = false;
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
if (recorderNode) {
|
||||
recorderNode.disconnect();
|
||||
recorderNode.port.onmessage = null;
|
||||
recorderNode = null;
|
||||
}
|
||||
if (gainNode) {
|
||||
gainNode.disconnect();
|
||||
gainNode = null;
|
||||
}
|
||||
bufferChunks = [];
|
||||
}
|
||||
async function destroy() {
|
||||
await stopCapture();
|
||||
if (mediaStreamSource) {
|
||||
mediaStreamSource.disconnect();
|
||||
mediaStreamSource = null;
|
||||
}
|
||||
if (sourceNode) {
|
||||
sourceNode.disconnect();
|
||||
sourceNode = null;
|
||||
}
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach((track) => track.stop());
|
||||
mediaStream = null;
|
||||
}
|
||||
if (audioCtx) {
|
||||
await audioCtx.close();
|
||||
audioCtx = null;
|
||||
}
|
||||
workletLoaded = false;
|
||||
}
|
||||
art.on("video:volumechange", () => {
|
||||
if (gainNode) {
|
||||
gainNode.gain.value = art.volume;
|
||||
}
|
||||
});
|
||||
art.on("play", startCapture);
|
||||
art.on("pause", stopCapture);
|
||||
art.on("destroy", destroy);
|
||||
return {
|
||||
name: "artplayerPluginAsr",
|
||||
stop: destroy,
|
||||
hide,
|
||||
append
|
||||
};
|
||||
};
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
if (!document.getElementById("artplayer-plugin-asr")) {
|
||||
const $style = document.createElement("style");
|
||||
$style.id = "artplayer-plugin-asr";
|
||||
$style.textContent = style;
|
||||
document.head.appendChild($style);
|
||||
}
|
||||
}
|
||||
return artplayerPluginAsr2;
|
||||
})();
|
||||
@@ -0,0 +1,87 @@
|
||||
var artplayerPluginAudioTrack = (function() {
|
||||
"use strict";
|
||||
function artplayerPluginAudioTrack2(option) {
|
||||
return (art) => {
|
||||
let { url, offset = 0, sync = 0.3 } = option;
|
||||
const audio = new Audio();
|
||||
audio.preload = "auto";
|
||||
if (url) {
|
||||
audio.src = url;
|
||||
}
|
||||
function syncAudio() {
|
||||
if (!art.video || !url)
|
||||
return;
|
||||
const videoTime = art.currentTime;
|
||||
const targetTime = videoTime + offset;
|
||||
if (Math.abs(audio.currentTime - targetTime) > sync) {
|
||||
audio.currentTime = targetTime;
|
||||
}
|
||||
}
|
||||
art.on("play", () => {
|
||||
if (!url)
|
||||
return;
|
||||
syncAudio();
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
art.on("pause", () => {
|
||||
audio.pause();
|
||||
});
|
||||
art.on("seek", () => {
|
||||
syncAudio();
|
||||
});
|
||||
art.on("video:timeupdate", () => {
|
||||
if (art.playing) {
|
||||
syncAudio();
|
||||
}
|
||||
});
|
||||
art.on("video:ratechange", () => {
|
||||
audio.playbackRate = art.video.playbackRate;
|
||||
});
|
||||
art.on("video:volumechange", () => {
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
});
|
||||
art.on("video:waiting", () => {
|
||||
audio.pause();
|
||||
});
|
||||
art.on("video:playing", () => {
|
||||
if (url && art.playing) {
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("destroy", () => {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
audio.load();
|
||||
});
|
||||
function update(newOption) {
|
||||
if (newOption.url && newOption.url !== url) {
|
||||
url = newOption.url;
|
||||
audio.src = url;
|
||||
if (art.playing) {
|
||||
audio.play().catch((err) => console.warn(err));
|
||||
}
|
||||
}
|
||||
if (newOption.offset !== void 0) {
|
||||
offset = newOption.offset;
|
||||
}
|
||||
if (newOption.sync !== void 0) {
|
||||
sync = newOption.sync;
|
||||
}
|
||||
}
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
audio.playbackRate = art.video?.playbackRate || 1;
|
||||
return {
|
||||
name: "artplayerPluginAudioTrack",
|
||||
audio,
|
||||
update
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginAudioTrack2;
|
||||
})();
|
||||
@@ -0,0 +1,58 @@
|
||||
var artplayerPluginAutoThumbnail = (function() {
|
||||
"use strict";
|
||||
function create({ url, width, number }, callback) {
|
||||
const video = document.createElement("video");
|
||||
video.crossOrigin = "anonymous";
|
||||
video.src = url;
|
||||
video.onloadedmetadata = () => {
|
||||
const duration = video.duration;
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const height = Math.floor(width * video.videoHeight / video.videoWidth);
|
||||
canvas.width = width * 10;
|
||||
canvas.height = height * Math.ceil(number / 10);
|
||||
let blobUrl = null;
|
||||
function seekAndDraw(index) {
|
||||
canvas.toBlob((blob) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
blobUrl = URL.createObjectURL(blob);
|
||||
callback({
|
||||
url: blobUrl,
|
||||
height
|
||||
});
|
||||
}, "image/jpeg");
|
||||
if (index >= number)
|
||||
return;
|
||||
video.currentTime = duration * index / number;
|
||||
video.onseeked = () => {
|
||||
ctx.drawImage(video, index % 10 * width, Math.floor(index / 10) * height, width, height);
|
||||
seekAndDraw(index + 1);
|
||||
};
|
||||
}
|
||||
seekAndDraw(0);
|
||||
};
|
||||
}
|
||||
function artplayerPluginAutoThumbnail2(option) {
|
||||
return async (art) => {
|
||||
art.on("video:loadedmetadata", () => {
|
||||
const url = option.url || art.option.url;
|
||||
const width = option.width || 160;
|
||||
const number = option.number || 100;
|
||||
const scale = option.scale || 1;
|
||||
create({ url, width, number }, (config) => {
|
||||
art.thumbnails = {
|
||||
...config,
|
||||
column: 10,
|
||||
number,
|
||||
width,
|
||||
scale
|
||||
};
|
||||
});
|
||||
});
|
||||
return {
|
||||
name: "artplayerPluginAutoThumbnail"
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginAutoThumbnail2;
|
||||
})();
|
||||
@@ -0,0 +1,154 @@
|
||||
var artplayerPluginChapter = (function() {
|
||||
"use strict";
|
||||
const style = ".artplayer-plugin-chapter .art-control-progress-inner {\n height: 100% !important;\n background-color: transparent !important;\n}\n.artplayer-plugin-chapter .art-control-progress-inner > .art-progress-hover,\n.artplayer-plugin-chapter .art-control-progress-inner > .art-progress-loaded,\n.artplayer-plugin-chapter .art-control-progress-inner > .art-progress-played {\n display: none !important;\n}\n.artplayer-plugin-chapter .art-control-thumbnails {\n bottom: calc(var(--art-bottom-gap) + 64px) !important;\n}\n.artplayer-plugin-chapter .art-chapters {\n position: absolute;\n z-index: 0;\n inset: 0;\n display: flex;\n align-items: center;\n gap: 4px;\n height: 100%;\n transform: scaleY(1.25);\n}\n.artplayer-plugin-chapter .art-chapters .art-chapter {\n display: flex;\n align-items: center;\n height: 100%;\n}\n.artplayer-plugin-chapter .art-chapters .art-chapter .art-chapter-inner {\n position: relative;\n cursor: pointer;\n width: 100%;\n height: 50%;\n border-radius: 10px;\n overflow: hidden;\n transition: height var(--art-transition-duration) ease;\n background-color: var(--art-progress-color);\n}\n.artplayer-plugin-chapter .art-chapters .art-chapter:hover .art-chapter-inner {\n height: 100%;\n}\n.artplayer-plugin-chapter .art-chapter-title {\n transform-origin: bottom center;\n transform: scale(0.5);\n opacity: 0;\n position: absolute;\n z-index: 70;\n top: -50px;\n left: 0;\n padding: 3px 5px;\n line-height: 1;\n font-size: 14px;\n border-radius: var(--art-border-radius);\n white-space: nowrap;\n background-color: var(--art-tip-background);\n transition: transform var(--art-transition-duration) ease, opacity var(--art-transition-duration) ease;\n}\n.artplayer-plugin-chapter.art-progress-hover .art-chapter-title {\n transform: scale(1);\n opacity: 1;\n}\n";
|
||||
function artplayerPluginChapter2(option = {}) {
|
||||
return (art) => {
|
||||
const { $player } = art.template;
|
||||
const { setStyle, append, clamp, query, addClass, removeClass } = art.constructor.utils;
|
||||
const html = `
|
||||
<div class="art-chapter">
|
||||
<div class="art-chapter-inner">
|
||||
<div class="art-progress-hover"></div>
|
||||
<div class="art-progress-loaded"></div>
|
||||
<div class="art-progress-played"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
let $chapters = [];
|
||||
const $inner = art.query(".art-control-progress-inner");
|
||||
const $control = append($inner, '<div class="art-chapters"></div>');
|
||||
const $title = append($inner, '<div class="art-chapter-title"></div>');
|
||||
function showTitle({ $chapter, width }) {
|
||||
const title = $chapter.dataset.title.trim();
|
||||
if (title) {
|
||||
$title.textContent = title;
|
||||
const titleWidth = $title.clientWidth;
|
||||
if (width <= titleWidth / 2) {
|
||||
setStyle($title, "left", 0);
|
||||
} else if (width > $inner.clientWidth - titleWidth / 2) {
|
||||
setStyle($title, "left", `${$inner.clientWidth - titleWidth}px`);
|
||||
} else {
|
||||
setStyle($title, "left", `${width - titleWidth / 2}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
function update(chapters = []) {
|
||||
$chapters = [];
|
||||
$control.textContent = "";
|
||||
removeClass($player, "artplayer-plugin-chapter");
|
||||
if (!Array.isArray(chapters))
|
||||
return;
|
||||
if (!chapters.length)
|
||||
return;
|
||||
if (!art.duration)
|
||||
return;
|
||||
chapters = chapters.sort((a, b) => a.start - b.start);
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const chapter = chapters[i];
|
||||
const nextChapter = chapters[i + 1];
|
||||
if (chapter.end === Infinity) {
|
||||
chapter.end = art.duration;
|
||||
}
|
||||
if (typeof chapter.start !== "number" || typeof chapter.end !== "number" || typeof chapter.title !== "string") {
|
||||
throw new TypeError("Illegal chapter data type");
|
||||
}
|
||||
if (chapter.start < 0 || chapter.end > art.duration || chapter.start >= chapter.end) {
|
||||
throw new Error("Illegal chapter time point");
|
||||
}
|
||||
if (nextChapter && chapter.end > nextChapter.start) {
|
||||
throw new Error("Illegal chapter time point");
|
||||
}
|
||||
}
|
||||
if (chapters[0].start > 0) {
|
||||
chapters.unshift({ start: 0, end: chapters[0].start, title: "" });
|
||||
}
|
||||
if (chapters[chapters.length - 1].end < art.duration) {
|
||||
chapters.push({ start: chapters[chapters.length - 1].end, end: art.duration, title: "" });
|
||||
}
|
||||
for (let i = 0; i < chapters.length - 1; i++) {
|
||||
if (chapters[i].end !== chapters[i + 1].start) {
|
||||
chapters.splice(i + 1, 0, {
|
||||
start: chapters[i].end,
|
||||
end: chapters[i + 1].start,
|
||||
title: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
$chapters = chapters.map((chapter) => {
|
||||
const $chapter = append($control, html);
|
||||
const start = clamp(chapter.start, 0, art.duration);
|
||||
const end = clamp(chapter.end, 0, art.duration);
|
||||
const duration = end - start;
|
||||
const percentage = duration / art.duration;
|
||||
$chapter.dataset.start = start;
|
||||
$chapter.dataset.end = end;
|
||||
$chapter.dataset.duration = duration;
|
||||
$chapter.dataset.title = chapter.title.trim();
|
||||
$chapter.style.width = `${percentage * 100}%`;
|
||||
return {
|
||||
$chapter,
|
||||
$hover: query(".art-progress-hover", $chapter),
|
||||
$loaded: query(".art-progress-loaded", $chapter),
|
||||
$played: query(".art-progress-played", $chapter)
|
||||
};
|
||||
});
|
||||
addClass($player, "artplayer-plugin-chapter");
|
||||
art.emit("setBar", "loaded", art.loaded || 0);
|
||||
}
|
||||
art.on("setBar", (type, percentage) => {
|
||||
if (!$chapters.length)
|
||||
return;
|
||||
for (let i = 0; i < $chapters.length; i++) {
|
||||
const { $chapter, $loaded, $played, $hover } = $chapters[i];
|
||||
const $target = {
|
||||
hover: $hover,
|
||||
loaded: $loaded,
|
||||
played: $played
|
||||
}[type];
|
||||
if (!$target)
|
||||
return;
|
||||
const width = $control.clientWidth * percentage;
|
||||
const currentTime = art.duration * percentage;
|
||||
const duration = Number.parseFloat($chapter.dataset.duration);
|
||||
const start = Number.parseFloat($chapter.dataset.start);
|
||||
const end = Number.parseFloat($chapter.dataset.end);
|
||||
if (currentTime < start) {
|
||||
setStyle($target, "width", 0);
|
||||
}
|
||||
if (currentTime > end) {
|
||||
setStyle($target, "width", "100%");
|
||||
}
|
||||
if (currentTime >= start && currentTime <= end) {
|
||||
const percentage2 = (currentTime - start) / duration;
|
||||
setStyle($target, "width", `${percentage2 * 100}%`);
|
||||
if (type === "hover" && width > 0) {
|
||||
showTitle({ $chapter, width });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
art.once("video:loadedmetadata", () => update(option.chapters));
|
||||
return {
|
||||
name: "artplayerPluginChapter",
|
||||
update: ({ chapters }) => update(chapters)
|
||||
};
|
||||
};
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-chapter";
|
||||
let $style = document.getElementById(id);
|
||||
if (!$style) {
|
||||
$style = document.createElement("style");
|
||||
$style.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.head.appendChild($style);
|
||||
});
|
||||
} else {
|
||||
(document.head || document.documentElement).appendChild($style);
|
||||
}
|
||||
}
|
||||
$style.textContent = style;
|
||||
}
|
||||
return artplayerPluginChapter2;
|
||||
})();
|
||||
@@ -0,0 +1,171 @@
|
||||
var artplayerPluginChromecast = (function() {
|
||||
"use strict";
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
function getMimeType(url) {
|
||||
const extension = url.split("?")[0].split("#")[0].split(".").pop().toLowerCase();
|
||||
const mimeTypes = {
|
||||
mp4: "video/mp4",
|
||||
webm: "video/webm",
|
||||
ogg: "video/ogg",
|
||||
ogv: "video/ogg",
|
||||
mp3: "audio/mp3",
|
||||
wav: "audio/wav",
|
||||
flv: "video/x-flv",
|
||||
mov: "video/quicktime",
|
||||
avi: "video/x-msvideo",
|
||||
wmv: "video/x-ms-wmv",
|
||||
mpd: "application/dash+xml",
|
||||
m3u8: "application/x-mpegURL"
|
||||
};
|
||||
return mimeTypes[extension] || "application/octet-stream";
|
||||
}
|
||||
function artplayerPluginChromecast2(option) {
|
||||
const DEFAULT_ICON = `<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M512 96H64v99c-13-2-26.4-3-40-3H0V96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H288V456c0-13.6-1-27-3-40H512V96zM24 224c128.1 0 232 103.9 232 232c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-101.6-82.4-184-184-184c-13.3 0-24-10.7-24-24s10.7-24 24-24zm8 192a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM0 344c0-13.3 10.7-24 24-24c75.1 0 136 60.9 136 136c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-48.6-39.4-88-88-88c-13.3 0-24-10.7-24-24z"/></svg>`;
|
||||
const DEFAULT_SDK = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
||||
let isCastInitialized = false;
|
||||
let castSession = null;
|
||||
let castState = null;
|
||||
const updateCastButton = (state) => {
|
||||
const button = document.querySelector(".art-icon-cast");
|
||||
if (button) {
|
||||
switch (state) {
|
||||
case "connected":
|
||||
button.style.color = "red";
|
||||
break;
|
||||
case "connecting":
|
||||
case "disconnecting":
|
||||
button.style.color = "orange";
|
||||
break;
|
||||
case "disconnected":
|
||||
default:
|
||||
button.style.color = "white";
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const initializeCastApi = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.__onGCastApiAvailable = (isAvailable) => {
|
||||
if (isAvailable) {
|
||||
const context = window.cast.framework.CastContext.getInstance();
|
||||
context.setOptions({
|
||||
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
|
||||
});
|
||||
context.addEventListener(
|
||||
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
(event) => {
|
||||
const SessionState = window.cast.framework.SessionState;
|
||||
castState = event.sessionState;
|
||||
castSession = event.session;
|
||||
switch (event.sessionState) {
|
||||
case SessionState.NO_SESSION:
|
||||
option.onStateChange?.("disconnected");
|
||||
updateCastButton("disconnected");
|
||||
break;
|
||||
case SessionState.SESSION_STARTING:
|
||||
option.onStateChange?.("connecting");
|
||||
updateCastButton("connecting");
|
||||
break;
|
||||
case SessionState.SESSION_STARTED:
|
||||
option.onStateChange?.("connected");
|
||||
updateCastButton("connected");
|
||||
break;
|
||||
case SessionState.SESSION_ENDING:
|
||||
option.onStateChange?.("disconnecting");
|
||||
updateCastButton("disconnecting");
|
||||
break;
|
||||
case SessionState.SESSION_RESUMED:
|
||||
option.onStateChange?.("connected");
|
||||
updateCastButton("connected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
context.addEventListener(window.cast.framework.CastContextEventType.CAST_STATE_CHANGED, (event) => {
|
||||
const CastState = window.cast.framework.CastState;
|
||||
switch (event.castState) {
|
||||
case CastState.NO_DEVICES_AVAILABLE:
|
||||
option.onCastAvailable?.(false);
|
||||
break;
|
||||
case CastState.NOT_CONNECTED:
|
||||
option.onCastAvailable?.(true);
|
||||
break;
|
||||
case CastState.CONNECTING:
|
||||
case CastState.CONNECTED:
|
||||
option.onCastAvailable?.(true);
|
||||
break;
|
||||
}
|
||||
});
|
||||
isCastInitialized = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Cast API is not available"));
|
||||
}
|
||||
};
|
||||
if (!window.chrome || !window.chrome.cast) {
|
||||
loadScript(option.sdk || DEFAULT_SDK).catch(reject);
|
||||
}
|
||||
});
|
||||
};
|
||||
const castVideo = (art, session) => {
|
||||
const url = option.url || art.option.url;
|
||||
const mediaInfo = new window.chrome.cast.media.MediaInfo(url, option.mimeType || getMimeType(url));
|
||||
const request = new window.chrome.cast.media.LoadRequest(mediaInfo);
|
||||
session.loadMedia(request).then(() => {
|
||||
art.notice.show = "Casting started";
|
||||
option.onCastStart?.();
|
||||
}).catch((error) => {
|
||||
art.notice.show = "Error casting media";
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
return async (art) => {
|
||||
art.controls.add({
|
||||
name: "chromecast",
|
||||
position: "right",
|
||||
tooltip: "Chromecast",
|
||||
html: `<i class="art-icon art-icon-cast">${option.icon || DEFAULT_ICON}</i>`,
|
||||
click: async () => {
|
||||
if (!isCastInitialized) {
|
||||
try {
|
||||
await initializeCastApi();
|
||||
} catch (error) {
|
||||
art.notice.show = "Failed to initialize Cast API";
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const context = window.cast.framework.CastContext.getInstance();
|
||||
if (castSession) {
|
||||
castVideo(art, castSession);
|
||||
} else {
|
||||
try {
|
||||
const session = await context.requestSession();
|
||||
castVideo(art, session);
|
||||
} catch (error) {
|
||||
art.notice.show = "Error connecting to cast session";
|
||||
option.onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
name: "artplayerPluginChromecast",
|
||||
getCastState: () => castState,
|
||||
isCasting: () => castSession !== null
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginChromecast2;
|
||||
})();
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,164 @@
|
||||
var artplayerPluginDashControl = (function() {
|
||||
"use strict";
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
return array.filter((item) => {
|
||||
const key = item[property];
|
||||
if (key === void 0) {
|
||||
return true;
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1);
|
||||
});
|
||||
}
|
||||
function artplayerPluginDashControl2(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template;
|
||||
const { errorHandle } = art.constructor.utils;
|
||||
function updateQuality(dash) {
|
||||
const qualities = dash.getRepresentationsByType("video");
|
||||
if (!qualities || !qualities.length)
|
||||
return;
|
||||
const config = option.quality || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Quality";
|
||||
const getName = config.getName || ((level) => `${level.height}p`);
|
||||
const currentRepresentation = dash.getCurrentRepresentationForType("video");
|
||||
const currentId = currentRepresentation && currentRepresentation.id;
|
||||
const currentAuto = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
|
||||
const defaultHtml = !currentAuto && currentRepresentation ? getName(currentRepresentation) : auto;
|
||||
const selector = uniqBy(
|
||||
qualities.map((item, index) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: index,
|
||||
id: item.id,
|
||||
default: !currentAuto && !!currentId && item.id === currentId
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
).sort((a, b) => b.value - a.value);
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: "auto",
|
||||
default: currentAuto
|
||||
});
|
||||
const onSelect = (item) => {
|
||||
if (item.value === "auto") {
|
||||
dash.updateSettings({
|
||||
streaming: {
|
||||
abr: {
|
||||
autoSwitchBitrate: {
|
||||
video: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dash.updateSettings({
|
||||
streaming: {
|
||||
abr: {
|
||||
autoSwitchBitrate: {
|
||||
video: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
dash.setRepresentationForTypeById("video", item.id);
|
||||
}
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
if (config.control)
|
||||
art.controls.check(item);
|
||||
if (config.setting)
|
||||
art.setting.check(item);
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "dash-quality",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "dash-quality",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
function updateAudio(dash) {
|
||||
const audioTracks = dash.getTracksFor("audio");
|
||||
if (!audioTracks || !audioTracks.length)
|
||||
return;
|
||||
const config = option.audio || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Audio";
|
||||
const getName = config.getName || ((track) => track.lang || track.id);
|
||||
const currentTrack = dash.getCurrentTrackFor("audio") || audioTracks[0];
|
||||
const defaultHtml = currentTrack ? getName(currentTrack) : auto;
|
||||
const selector = uniqBy(
|
||||
audioTracks.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item,
|
||||
default: currentTrack === item
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
);
|
||||
const onSelect = (item) => {
|
||||
dash.setCurrentTrack(item.value);
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
if (config.control)
|
||||
art.controls.check(item);
|
||||
if (config.setting)
|
||||
art.setting.check(item);
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "dash-audio",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "dash-audio",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
function update() {
|
||||
errorHandle(art.dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
|
||||
updateQuality(art.dash);
|
||||
updateAudio(art.dash);
|
||||
}
|
||||
art.on("ready", update);
|
||||
art.on("restart", update);
|
||||
return {
|
||||
name: "artplayerPluginDashControl",
|
||||
update
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginDashControl2;
|
||||
})();
|
||||
@@ -0,0 +1,189 @@
|
||||
var artplayerPluginDocumentPip = (function() {
|
||||
"use strict";
|
||||
const style = ".artplayer-document-pip.art-video-player {\n width: 100% !important;\n height: 100% !important;\n}\n";
|
||||
function artplayerPluginDocumentPip2(userOptions = {}) {
|
||||
const options = {
|
||||
width: 480,
|
||||
height: 270,
|
||||
fallbackToVideoPiP: true,
|
||||
placeholder: `Playing in Document Picture-in-Picture`,
|
||||
...userOptions
|
||||
};
|
||||
return (art) => {
|
||||
const isSupported = "documentPictureInPicture" in window && typeof window.documentPictureInPicture?.requestWindow === "function";
|
||||
const { proxy, icons, i18n, template: { $player }, constructor: { utils: { append, tooltip, addClass, removeClass, sleep } } } = art;
|
||||
const state = {
|
||||
win: null,
|
||||
originalParent: null,
|
||||
originalNext: null,
|
||||
currentDoc: null,
|
||||
placeholder: null
|
||||
};
|
||||
function copyStylesTo(targetDoc) {
|
||||
const base = targetDoc.createElement("style");
|
||||
base.textContent = `
|
||||
html, body { margin:0; padding:0; width:100%; height:100%; background:#000; overflow:hidden; }
|
||||
#__art_dpip_root { position:absolute; inset:0; display:flex; }
|
||||
#__art_dpip_root > * { width:100% !important; height:100% !important; }
|
||||
`;
|
||||
targetDoc.head.appendChild(base);
|
||||
if (!targetDoc.querySelector('meta[name="viewport"]')) {
|
||||
const meta = targetDoc.createElement("meta");
|
||||
meta.name = "viewport";
|
||||
meta.content = "width=device-width,initial-scale=1";
|
||||
targetDoc.head.appendChild(meta);
|
||||
}
|
||||
try {
|
||||
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||
const clone = targetDoc.createElement("link");
|
||||
clone.rel = "stylesheet";
|
||||
clone.href = link.href;
|
||||
if (link.media)
|
||||
clone.media = link.media;
|
||||
if (link.crossOrigin)
|
||||
clone.crossOrigin = link.crossOrigin;
|
||||
if (link.referrerPolicy)
|
||||
clone.referrerPolicy = link.referrerPolicy;
|
||||
targetDoc.head.appendChild(clone);
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
document.querySelectorAll("style").forEach((style2) => {
|
||||
const clone = targetDoc.createElement("style");
|
||||
clone.textContent = style2.textContent;
|
||||
targetDoc.head.appendChild(clone);
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
async function open() {
|
||||
if (!isSupported && options.fallbackToVideoPiP) {
|
||||
art.pip = true;
|
||||
console.warn("[artplayer-plugin-document-pip] Document Picture-in-Picture is not supported, falling back to Video Picture-in-Picture");
|
||||
return;
|
||||
}
|
||||
if (state.win)
|
||||
return;
|
||||
try {
|
||||
const pipWin = await window.documentPictureInPicture.requestWindow({
|
||||
width: options.width,
|
||||
height: options.height
|
||||
});
|
||||
state.win = pipWin;
|
||||
state.currentDoc = pipWin.document;
|
||||
state.placeholder = document.createElement("div");
|
||||
state.placeholder.className = "artplayer-document-pip-placeholder";
|
||||
state.placeholder.style.cssText = `display:flex;justify-content:center;align-items:center;width:100%;height:100%;`;
|
||||
state.placeholder.textContent = options.placeholder;
|
||||
state.originalParent = $player.parentNode;
|
||||
state.originalNext = $player.nextSibling;
|
||||
state.originalParent.insertBefore(state.placeholder, state.originalNext);
|
||||
const root = pipWin.document.createElement("div");
|
||||
root.id = "__art_dpip_root";
|
||||
pipWin.document.body.appendChild(root);
|
||||
copyStylesTo(pipWin.document);
|
||||
const adopted = pipWin.document.adoptNode($player);
|
||||
root.appendChild(adopted);
|
||||
state.currentDoc = pipWin.document;
|
||||
const onResize = () => art.resize?.();
|
||||
const onClose = () => close();
|
||||
pipWin.addEventListener("resize", onResize);
|
||||
pipWin.addEventListener("pagehide", onClose);
|
||||
pipWin.addEventListener("unload", onClose);
|
||||
state.cleanup = () => {
|
||||
pipWin.removeEventListener("resize", onResize);
|
||||
pipWin.removeEventListener("pagehide", onClose);
|
||||
pipWin.removeEventListener("unload", onClose);
|
||||
};
|
||||
addClass($player, "artplayer-document-pip");
|
||||
art.events.bindGlobalEvents?.();
|
||||
art.emit("document-pip", true);
|
||||
await sleep(100);
|
||||
art.emit("resize");
|
||||
} catch (err) {
|
||||
art.notice.show = "Document Picture-in-Picture open failed";
|
||||
console.warn("[artplayer-plugin-document-pip] open failed:", err);
|
||||
}
|
||||
}
|
||||
function restoreToOriginalDocument() {
|
||||
if (!state.placeholder)
|
||||
return;
|
||||
const backDoc = state.placeholder.ownerDocument || document;
|
||||
const adoptedBack = backDoc.adoptNode($player);
|
||||
state.originalParent.insertBefore(adoptedBack, state.placeholder);
|
||||
state.placeholder.remove();
|
||||
state.placeholder = null;
|
||||
state.currentDoc = backDoc;
|
||||
}
|
||||
async function close() {
|
||||
if (!state.win)
|
||||
return;
|
||||
try {
|
||||
state.cleanup?.();
|
||||
restoreToOriginalDocument();
|
||||
state.win.close();
|
||||
state.win = null;
|
||||
removeClass($player, "artplayer-document-pip");
|
||||
art.events.bindGlobalEvents?.();
|
||||
art.emit("document-pip", false);
|
||||
await sleep(100);
|
||||
art.emit("resize");
|
||||
} catch (err) {
|
||||
art.notice.show = "Document Picture-in-Picture close failed";
|
||||
console.warn("[artplayer-plugin-document-pip] close failed:", err);
|
||||
}
|
||||
}
|
||||
function toggle() {
|
||||
if (state.win) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
art.controls.add({
|
||||
name: "document-pip",
|
||||
position: "right",
|
||||
index: 40,
|
||||
tooltip: art.i18n.get("PIP Mode"),
|
||||
mounted: ($control) => {
|
||||
append($control, icons.pip);
|
||||
proxy($control, "click", toggle);
|
||||
art.on("document-pip", (value) => {
|
||||
tooltip($control, i18n.get(value ? "Exit PIP Mode" : "PIP Mode"));
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("destroy", close);
|
||||
return {
|
||||
name: "artplayerPluginDocumentPip",
|
||||
get isSupported() {
|
||||
return isSupported;
|
||||
},
|
||||
get isActive() {
|
||||
return !!state.win;
|
||||
},
|
||||
open,
|
||||
close,
|
||||
toggle
|
||||
};
|
||||
};
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-document-pip";
|
||||
let $style = document.getElementById(id);
|
||||
if (!$style) {
|
||||
$style = document.createElement("style");
|
||||
$style.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.head.appendChild($style);
|
||||
});
|
||||
} else {
|
||||
(document.head || document.documentElement).appendChild($style);
|
||||
}
|
||||
}
|
||||
$style.textContent = style;
|
||||
}
|
||||
return artplayerPluginDocumentPip2;
|
||||
})();
|
||||
@@ -0,0 +1,138 @@
|
||||
var artplayerPluginHlsControl = (function() {
|
||||
"use strict";
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
return array.filter((item) => {
|
||||
const key = item[property];
|
||||
if (key === void 0) {
|
||||
return true;
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1);
|
||||
});
|
||||
}
|
||||
function artplayerPluginHlsControl2(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template;
|
||||
const { errorHandle } = art.constructor.utils;
|
||||
function updateQuality(hls) {
|
||||
if (!hls.levels.length)
|
||||
return;
|
||||
const config = option.quality || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Quality";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const defaultLevel = hls.levels[hls.currentLevel];
|
||||
const defaultHtml = defaultLevel ? getName(defaultLevel) : auto;
|
||||
const selector = uniqBy(
|
||||
hls.levels.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: index,
|
||||
default: hls.currentLevel === index
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
).sort((a, b) => b.value - a.value);
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: -1,
|
||||
default: hls.currentLevel === -1
|
||||
});
|
||||
const onSelect = (item) => {
|
||||
hls.currentLevel = item.value;
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
if (config.control)
|
||||
art.controls.check(item);
|
||||
if (config.setting)
|
||||
art.setting.check(item);
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "hls-quality",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "hls-quality",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
function updateAudio(hls) {
|
||||
if (!hls.audioTracks.length)
|
||||
return;
|
||||
const config = option.audio || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Audio";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const defaultTrack = hls.audioTracks[hls.audioTrack];
|
||||
const defaultHtml = defaultTrack ? getName(defaultTrack) : auto;
|
||||
const selector = uniqBy(
|
||||
hls.audioTracks.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: item.id,
|
||||
default: hls.audioTrack === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
);
|
||||
const onSelect = (item) => {
|
||||
hls.audioTrack = item.value;
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
if (config.control)
|
||||
art.controls.check(item);
|
||||
if (config.setting)
|
||||
art.setting.check(item);
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "hls-audio",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "hls-audio",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
function update() {
|
||||
errorHandle(art.hls?.media === $video, 'Cannot find instance of HLS from "art.hls"');
|
||||
updateQuality(art.hls);
|
||||
updateAudio(art.hls);
|
||||
}
|
||||
art.on("ready", update);
|
||||
art.on("restart", update);
|
||||
return {
|
||||
name: "artplayerPluginHlsControl",
|
||||
update
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginHlsControl2;
|
||||
})();
|
||||
@@ -0,0 +1,132 @@
|
||||
var artplayerPluginIframe = (function() {
|
||||
"use strict";
|
||||
class ArtplayerToolIframe {
|
||||
static get iframe() {
|
||||
return window.top !== window;
|
||||
}
|
||||
static postMessage({ type, data, id = 0 }) {
|
||||
if (!ArtplayerToolIframe.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');
|
||||
}
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
static async onMessage(event) {
|
||||
if (!ArtplayerToolIframe.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');
|
||||
}
|
||||
const { type, data, id } = event.data;
|
||||
switch (type) {
|
||||
case "commit":
|
||||
try {
|
||||
if (data.match(/\bresolve\((.*?)\)/)) {
|
||||
const string = `return new Promise(function(resolve){
|
||||
${data}
|
||||
})`;
|
||||
const result = await new Function(string)();
|
||||
ArtplayerToolIframe.postMessage({ type: "response", data: result, id });
|
||||
} else {
|
||||
const result = new Function(data)();
|
||||
ArtplayerToolIframe.postMessage({ type: "response", data: result, id });
|
||||
}
|
||||
} catch (error) {
|
||||
ArtplayerToolIframe.postMessage({ type: "error", data: error.message, id });
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
static inject() {
|
||||
if (!ArtplayerToolIframe.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');
|
||||
}
|
||||
ArtplayerToolIframe.postMessage({ type: "inject" });
|
||||
window.addEventListener("message", ArtplayerToolIframe.onMessage);
|
||||
}
|
||||
constructor({ iframe, url }) {
|
||||
if (iframe instanceof HTMLIFrameElement === false) {
|
||||
throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');
|
||||
}
|
||||
if (typeof url !== "string") {
|
||||
throw new TypeError('"option.url" needs to be a string');
|
||||
}
|
||||
this.url = url;
|
||||
this.$iframe = iframe;
|
||||
this.promises = {};
|
||||
this.injected = false;
|
||||
this.destroyed = false;
|
||||
this.messageCallback = () => null;
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
window.addEventListener("message", this.onMessage);
|
||||
this.$iframe.src = this.url;
|
||||
}
|
||||
onMessage(event) {
|
||||
const { type, data, id } = event.data;
|
||||
switch (type) {
|
||||
case "inject":
|
||||
this.injected = true;
|
||||
break;
|
||||
}
|
||||
if (this.promises[id]) {
|
||||
if (type === "error") {
|
||||
this.promises[id].reject(new Error(data));
|
||||
} else {
|
||||
this.promises[id].resove(data);
|
||||
}
|
||||
delete this.promises[id];
|
||||
}
|
||||
if (this.messageCallback) {
|
||||
this.messageCallback({ type, data });
|
||||
}
|
||||
}
|
||||
postMessage({ type, data }) {
|
||||
return new Promise((resove, reject) => {
|
||||
(function loop() {
|
||||
if (this.destroyed) {
|
||||
reject(new Error("The instance has been destroyed"));
|
||||
} else {
|
||||
if (this.injected) {
|
||||
const id = Date.now();
|
||||
this.promises[id] = { resove, reject };
|
||||
this.$iframe.contentWindow.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id
|
||||
},
|
||||
"*"
|
||||
);
|
||||
} else {
|
||||
setTimeout(loop.bind(this), 200);
|
||||
}
|
||||
}
|
||||
}).call(this);
|
||||
});
|
||||
}
|
||||
commit(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError('"commit.callback" needs to be a function');
|
||||
}
|
||||
const callbackString = callback.toString();
|
||||
const bodyString = callbackString.substring(callbackString.indexOf("{") + 1, callbackString.lastIndexOf("}"));
|
||||
return this.postMessage({ type: "commit", data: bodyString });
|
||||
}
|
||||
message(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError('"message.callback" needs to be a function');
|
||||
}
|
||||
this.messageCallback = callback;
|
||||
}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
window.removeEventListener("message", this.onMessage);
|
||||
}
|
||||
}
|
||||
return ArtplayerToolIframe;
|
||||
})();
|
||||
@@ -0,0 +1,573 @@
|
||||
var artplayerPluginJassub = (function() {
|
||||
"use strict";var __defProp = Object.defineProperty;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||||
|
||||
typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype && (HTMLVideoElement.prototype._rvfcpolyfillmap = {}, HTMLVideoElement.prototype.requestVideoFrameCallback = function(c) {
|
||||
const e = performance.now(), t = this.getVideoPlaybackQuality(), s = this.mozPresentedFrames || this.mozPaintedFrames || t.totalVideoFrames - t.droppedVideoFrames, r = (n, a) => {
|
||||
const i = this.getVideoPlaybackQuality(), h = this.mozPresentedFrames || this.mozPaintedFrames || i.totalVideoFrames - i.droppedVideoFrames;
|
||||
if (h > s) {
|
||||
const d = this.mozFrameDelay || i.totalFrameDelay - t.totalFrameDelay || 0, l = a - n;
|
||||
c(a, {
|
||||
presentationTime: a + d * 1e3,
|
||||
expectedDisplayTime: a + l,
|
||||
width: this.videoWidth,
|
||||
height: this.videoHeight,
|
||||
mediaTime: Math.max(0, this.currentTime || 0) + l / 1e3,
|
||||
presentedFrames: h,
|
||||
processingDuration: d
|
||||
}), delete this._rvfcpolyfillmap[e];
|
||||
} else
|
||||
this._rvfcpolyfillmap[e] = requestAnimationFrame((d) => r(a, d));
|
||||
};
|
||||
return this._rvfcpolyfillmap[e] = requestAnimationFrame((n) => r(e, n)), e;
|
||||
}, HTMLVideoElement.prototype.cancelVideoFrameCallback = function(c) {
|
||||
cancelAnimationFrame(this._rvfcpolyfillmap[c]), delete this._rvfcpolyfillmap[c];
|
||||
});
|
||||
const _ = {
|
||||
bt709: "BT709",
|
||||
// these might not be exactly correct? oops?
|
||||
bt470bg: "BT601",
|
||||
// alias BT.601 PAL... whats the difference?
|
||||
smpte170m: "BT601"
|
||||
// alias BT.601 NTSC... whats the difference?
|
||||
}, f = {
|
||||
BT601: {
|
||||
BT709: "1.0863 -0.0723 -0.014 0 0 0.0965 0.8451 0.0584 0 0 -0.0141 -0.0277 1.0418"
|
||||
},
|
||||
BT709: {
|
||||
BT601: "0.9137 0.0784 0.0079 0 0 -0.1049 1.1722 -0.0671 0 0 0.0096 0.0322 0.9582"
|
||||
},
|
||||
FCC: {
|
||||
BT709: "1.0873 -0.0736 -0.0137 0 0 0.0974 0.8494 0.0531 0 0 -0.0127 -0.0251 1.0378",
|
||||
BT601: "1.001 -0.0008 -0.0002 0 0 0.0009 1.005 -0.006 0 0 0.0013 0.0027 0.996"
|
||||
},
|
||||
SMPTE240M: {
|
||||
BT709: "0.9993 0.0006 0.0001 0 0 -0.0004 0.9812 0.0192 0 0 -0.0034 -0.0114 1.0148",
|
||||
BT601: "0.913 0.0774 0.0096 0 0 -0.1051 1.1508 -0.0456 0 0 0.0063 0.0207 0.973"
|
||||
}
|
||||
};
|
||||
const _o = class _o extends EventTarget {
|
||||
/**
|
||||
* @param {Object} options Settings object.
|
||||
* @param {HTMLVideoElement} options.video Video to use as target for rendering and event listeners. Optional if canvas is specified instead.
|
||||
* @param {HTMLCanvasElement} [options.canvas=HTMLCanvasElement] Canvas to use for manual handling. Not required if video is specified.
|
||||
* @param {'js'|'wasm'} [options.blendMode='js'] Which image blending mode to use. WASM will perform better on lower end devices, JS will perform better if the device and browser supports hardware acceleration.
|
||||
* @param {Boolean} [options.asyncRender=true] Whether or not to use async rendering, which offloads the CPU by creating image bitmaps on the GPU.
|
||||
* @param {Boolean} [options.offscreenRender=true] Whether or not to render things fully on the worker, greatly reduces CPU usage.
|
||||
* @param {Boolean} [options.onDemandRender=true] Whether or not to render subtitles as the video player decodes renders, rather than predicting which frame the player is on using events.
|
||||
* @param {Number} [options.targetFps=24] Target FPS to render subtitles at. Ignored when onDemandRender is enabled.
|
||||
* @param {Number} [options.timeOffset=0] Subtitle time offset in seconds.
|
||||
* @param {Boolean} [options.debug=false] Whether or not to print debug information.
|
||||
* @param {Number} [options.prescaleFactor=1.0] Scale down (< 1.0) the subtitles canvas to improve performance at the expense of quality, or scale it up (> 1.0).
|
||||
* @param {Number} [options.prescaleHeightLimit=1080] The height in pixels beyond which the subtitles canvas won't be prescaled.
|
||||
* @param {Number} [options.maxRenderHeight=0] The maximum rendering height in pixels of the subtitles canvas. Beyond this subtitles will be upscaled by the browser.
|
||||
* @param {Boolean} [options.dropAllAnimations=false] Attempt to discard all animated tags. Enabling this may severly mangle complex subtitles and should only be considered as an last ditch effort of uncertain success for hardware otherwise incapable of displaing anything. Will not reliably work with manually edited or allocated events.
|
||||
* @param {Boolean} [options.dropAllBlur=false] The holy grail of performance gains. If heavy TS lags a lot, disabling this will make it ~x10 faster. This drops blur from all added subtitle tracks making most text and backgrounds look sharper, this is way less intrusive than dropping all animations, while still offering major performance gains.
|
||||
* @param {String} [options.workerUrl='jassub-worker.js'] The URL of the worker.
|
||||
* @param {String} [options.wasmUrl='jassub-worker.wasm'] The URL of the worker WASM.
|
||||
* @param {String} [options.legacyWasmUrl='jassub-worker.wasm.js'] The URL of the worker WASM. Only loaded if the browser doesn't support WASM.
|
||||
* @param {String} options.modernWasmUrl The URL of the modern worker WASM. This includes faster ASM instructions, but is only supported by newer browsers, disabled if the URL isn't defined.
|
||||
* @param {String} [options.subUrl=options.subContent] The URL of the subtitle file to play.
|
||||
* @param {String} [options.subContent=options.subUrl] The content of the subtitle file to play.
|
||||
* @param {String[]|Uint8Array[]} [options.fonts] An array of links or Uint8Arrays to the fonts used in the subtitle. If Uint8Array is used the array is copied, not referenced. This forces all the fonts in this array to be loaded by the renderer, regardless of if they are used.
|
||||
* @param {Object} [options.availableFonts={'liberation sans': './default.woff2'}] Object with all available fonts - Key is font family in lower case, value is link or Uint8Array: { arial: '/font1.ttf' }. These fonts are selectively loaded if detected as used in the current subtitle track.
|
||||
* @param {String} [options.fallbackFont='liberation sans'] The font family key of the fallback font in availableFonts to use if the other font for the style is missing special glyphs or unicode.
|
||||
* @param {Boolean} [options.useLocalFonts=false] If the Local Font Access API is enabled [chrome://flags/#font-access], the library will query for permissions to use local fonts and use them if any are missing. The permission can be queried beforehand using navigator.permissions.request({ name: 'local-fonts' }).
|
||||
* @param {Number} [options.libassMemoryLimit] libass bitmap cache memory limit in MiB (approximate).
|
||||
* @param {Number} [options.libassGlyphLimit] libass glyph cache memory limit in MiB (approximate).
|
||||
*/
|
||||
constructor(e) {
|
||||
if (super(), !globalThis.Worker)
|
||||
throw this.destroy("Worker not supported");
|
||||
if (!e)
|
||||
throw this.destroy("No options provided");
|
||||
this._loaded = /** @type {Promise<void>} */
|
||||
new Promise((s) => {
|
||||
this._init = s;
|
||||
});
|
||||
const t = _o._test();
|
||||
if (this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && (e.onDemandRender ?? true), this._offscreenRender = "transferControlToOffscreen" in HTMLCanvasElement.prototype && !e.canvas && (e.offscreenRender ?? true), this.timeOffset = e.timeOffset || 0, this._video = e.video, this._videoHeight = 0, this._videoWidth = 0, this._videoColorSpace = null, this._canvas = e.canvas, this._video && !this._canvas)
|
||||
this._canvasParent = document.createElement("div"), this._canvasParent.className = "JASSUB", this._canvasParent.style.position = "relative", this._canvas = this._createCanvas(), this._video.insertAdjacentElement("afterend", this._canvasParent);
|
||||
else if (!this._canvas)
|
||||
throw this.destroy("Don't know where to render: you should give video or canvas in options.");
|
||||
if (this._bufferCanvas = document.createElement("canvas"), this._bufferCtx = this._bufferCanvas.getContext("2d"), !this._bufferCtx)
|
||||
throw this.destroy("Canvas rendering not supported");
|
||||
this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = this.setRate.bind(this), this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = false, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
|
||||
this._worker.postMessage({
|
||||
target: "init",
|
||||
wasmUrl: _o._supportsSIMD && e.modernWasmUrl ? e.modernWasmUrl : e.wasmUrl ?? "jassub-worker.wasm",
|
||||
legacyWasmUrl: e.legacyWasmUrl ?? "jassub-worker.wasm.js",
|
||||
asyncRender: typeof createImageBitmap < "u" && (e.asyncRender ?? true),
|
||||
onDemandRender: this._onDemandRender,
|
||||
width: this._canvasctrl.width || 0,
|
||||
height: this._canvasctrl.height || 0,
|
||||
blendMode: e.blendMode || "js",
|
||||
subUrl: e.subUrl,
|
||||
subContent: e.subContent || null,
|
||||
fonts: e.fonts || [],
|
||||
availableFonts: e.availableFonts || { "liberation sans": "./default.woff2" },
|
||||
fallbackFont: e.fallbackFont || "liberation sans",
|
||||
debug: this.debug,
|
||||
targetFps: e.targetFps || 24,
|
||||
dropAllAnimations: e.dropAllAnimations,
|
||||
dropAllBlur: e.dropAllBlur,
|
||||
libassMemoryLimit: e.libassMemoryLimit || 0,
|
||||
libassGlyphLimit: e.libassGlyphLimit || 0,
|
||||
// @ts-ignore
|
||||
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? true),
|
||||
hasBitmapBug: _o._hasBitmapBug
|
||||
}), this._offscreenRender === true && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
|
||||
});
|
||||
}
|
||||
_createCanvas() {
|
||||
return this._canvas = document.createElement("canvas"), this._canvas.style.display = "block", this._canvas.style.position = "absolute", this._canvas.style.pointerEvents = "none", this._canvasParent.appendChild(this._canvas), this._canvas;
|
||||
}
|
||||
static _testSIMD() {
|
||||
if (_o._supportsSIMD === null)
|
||||
try {
|
||||
_o._supportsSIMD = WebAssembly.validate(Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11));
|
||||
} catch {
|
||||
_o._supportsSIMD = false;
|
||||
}
|
||||
}
|
||||
static async _testImageBugs() {
|
||||
if (_o._hasBitmapBug !== null)
|
||||
return;
|
||||
const e = document.createElement("canvas"), t = e.getContext("2d", { willReadFrequently: true });
|
||||
if (!t)
|
||||
throw new Error("Canvas rendering not supported");
|
||||
if (typeof ImageData.prototype.constructor == "function")
|
||||
try {
|
||||
new ImageData(new Uint8ClampedArray([0, 0, 0, 0]), 1, 1);
|
||||
} catch {
|
||||
console.log("Detected that ImageData is not constructable despite browser saying so"), self.ImageData = function(h, d, l) {
|
||||
const m = t.createImageData(d, l);
|
||||
return h && m.data.set(h), m;
|
||||
};
|
||||
}
|
||||
const s = document.createElement("canvas"), r = s.getContext("2d", { willReadFrequently: true });
|
||||
if (!r)
|
||||
throw new Error("Canvas rendering not supported");
|
||||
e.width = s.width = 1, e.height = s.height = 1, t.clearRect(0, 0, 1, 1), r.clearRect(0, 0, 1, 1);
|
||||
const n = r.getImageData(0, 0, 1, 1).data;
|
||||
t.putImageData(new ImageData(new Uint8ClampedArray([0, 255, 0, 0]), 1, 1), 0, 0), r.drawImage(e, 0, 0);
|
||||
const a = r.getImageData(0, 0, 1, 1).data;
|
||||
if (_o._hasAlphaBug = n[1] !== a[1], _o._hasAlphaBug && console.log("Detected a browser having issue with transparent pixels, applying workaround"), typeof createImageBitmap < "u") {
|
||||
const i = new Uint8ClampedArray([255, 0, 255, 0, 255]).subarray(1, 5);
|
||||
r.drawImage(await createImageBitmap(new ImageData(i, 1)), 0, 0);
|
||||
const { data: h } = r.getImageData(0, 0, 1, 1);
|
||||
_o._hasBitmapBug = false;
|
||||
for (const [d, l] of h.entries())
|
||||
if (Math.abs(i[d] - l) > 15) {
|
||||
_o._hasBitmapBug = true, console.log("Detected a browser having issue with partial bitmaps, applying workaround");
|
||||
break;
|
||||
}
|
||||
} else
|
||||
_o._hasBitmapBug = false;
|
||||
e.remove(), s.remove();
|
||||
}
|
||||
static async _test() {
|
||||
_o._testSIMD(), await _o._testImageBugs();
|
||||
}
|
||||
/**
|
||||
* Resize the canvas to given parameters. Auto-generated if values are ommited.
|
||||
* @param {Number} [width=0]
|
||||
* @param {Number} [height=0]
|
||||
* @param {Number} [top=0]
|
||||
* @param {Number} [left=0]
|
||||
* @param {Boolean} [force=false]
|
||||
*/
|
||||
resize(e = 0, t = 0, s = 0, r = 0, n = this._video?.paused) {
|
||||
if ((!e || !t) && this._video) {
|
||||
const a = this._getVideoPosition();
|
||||
let i = null;
|
||||
if (this._videoWidth) {
|
||||
const h = this._video.videoWidth / this._videoWidth, d = this._video.videoHeight / this._videoHeight;
|
||||
i = this._computeCanvasSize((a.width || 0) / h, (a.height || 0) / d);
|
||||
} else
|
||||
i = this._computeCanvasSize(a.width || 0, a.height || 0);
|
||||
e = i.width, t = i.height, this._canvasParent && (s = a.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top), r = a.x), this._canvas.style.width = a.width + "px", this._canvas.style.height = a.height + "px";
|
||||
}
|
||||
this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === false ? this.busy = true : n = false, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video.videoWidth, videoHeight: this._videoHeight || this._video.videoHeight, force: n });
|
||||
}
|
||||
_getVideoPosition(e = this._video.videoWidth, t = this._video.videoHeight) {
|
||||
const s = e / t, { offsetWidth: r, offsetHeight: n } = this._video, a = r / n;
|
||||
e = r, t = n, a > s ? e = Math.floor(n * s) : t = Math.floor(r / s);
|
||||
const i = (r - e) / 2, h = (n - t) / 2;
|
||||
return { width: e, height: t, x: i, y: h };
|
||||
}
|
||||
_computeCanvasSize(e = 0, t = 0) {
|
||||
const s = this.prescaleFactor <= 0 ? 1 : this.prescaleFactor, r = self.devicePixelRatio || 1;
|
||||
if (t <= 0 || e <= 0)
|
||||
e = 0, t = 0;
|
||||
else {
|
||||
const n = s < 1 ? -1 : 1;
|
||||
let a = t * r;
|
||||
n * a * s <= n * this.prescaleHeightLimit ? a *= s : n * a < n * this.prescaleHeightLimit && (a = this.prescaleHeightLimit), this.maxRenderHeight > 0 && a > this.maxRenderHeight && (a = this.maxRenderHeight), e *= a / t, t = a;
|
||||
}
|
||||
return { width: e, height: t };
|
||||
}
|
||||
_timeupdate({ type: e }) {
|
||||
const s = {
|
||||
seeking: true,
|
||||
waiting: true,
|
||||
playing: false
|
||||
}[e];
|
||||
s != null && (this._playstate = s), this.setCurrentTime(this._video.paused || this._playstate, this._video.currentTime + this.timeOffset);
|
||||
}
|
||||
/**
|
||||
* Change the video to use as target for event listeners.
|
||||
* @param {HTMLVideoElement} video
|
||||
*/
|
||||
setVideo(e) {
|
||||
e instanceof HTMLVideoElement ? (this._removeListeners(), this._video = e, this._onDemandRender ? this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)) : (this._playstate = e.paused, e.addEventListener("timeupdate", this._boundTimeUpdate, false), e.addEventListener("progress", this._boundTimeUpdate, false), e.addEventListener("waiting", this._boundTimeUpdate, false), e.addEventListener("seeking", this._boundTimeUpdate, false), e.addEventListener("playing", this._boundTimeUpdate, false), e.addEventListener("ratechange", this._boundSetRate, false), e.addEventListener("resize", this._boundResize, false)), "VideoFrame" in window && (e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, false), e.readyState > 2 && this._updateColorSpace()), e.videoWidth > 0 && this.resize(), typeof ResizeObserver < "u" && (this._ro || (this._ro = new ResizeObserver(() => this.resize())), this._ro.observe(e))) : this._error("Video element invalid!");
|
||||
}
|
||||
runBenchmark() {
|
||||
this.sendMessage("runBenchmark");
|
||||
}
|
||||
/**
|
||||
* Overwrites the current subtitle content.
|
||||
* @param {String} url URL to load subtitles from.
|
||||
*/
|
||||
setTrackByUrl(e) {
|
||||
this.sendMessage("setTrackByUrl", { url: e }), this._reAttachOffscreen(), this._ctx && (this._ctx.filter = "none");
|
||||
}
|
||||
/**
|
||||
* Overwrites the current subtitle content.
|
||||
* @param {String} content Content of the ASS file.
|
||||
*/
|
||||
setTrack(e) {
|
||||
this.sendMessage("setTrack", { content: e }), this._reAttachOffscreen(), this._ctx && (this._ctx.filter = "none");
|
||||
}
|
||||
/**
|
||||
* Free currently used subtitle track.
|
||||
*/
|
||||
freeTrack() {
|
||||
this.sendMessage("freeTrack");
|
||||
}
|
||||
/**
|
||||
* Sets the playback state of the media.
|
||||
* @param {Boolean} isPaused Pause/Play subtitle playback.
|
||||
*/
|
||||
setIsPaused(e) {
|
||||
this.sendMessage("video", { isPaused: e });
|
||||
}
|
||||
/**
|
||||
* Sets the playback rate of the media [speed multiplier].
|
||||
* @param {Number} rate Playback rate.
|
||||
*/
|
||||
setRate(e) {
|
||||
this.sendMessage("video", { rate: e });
|
||||
}
|
||||
/**
|
||||
* Sets the current time, playback state and rate of the subtitles.
|
||||
* @param {Boolean} [isPaused] Pause/Play subtitle playback.
|
||||
* @param {Number} [currentTime] Time in seconds.
|
||||
* @param {Number} [rate] Playback rate.
|
||||
*/
|
||||
setCurrentTime(e, t, s) {
|
||||
this.sendMessage("video", { isPaused: e, currentTime: t, rate: s, colorSpace: this._videoColorSpace });
|
||||
}
|
||||
/**
|
||||
* @typedef {Object} ASS_Event
|
||||
* @property {Number} Start Start Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will appear onscreen. Note that there is a single digit for the hours!
|
||||
* @property {Number} Duration End Time of the Event, in 0:00:00:00 format ie. Hrs:Mins:Secs:hundredths. This is the time elapsed during script playback at which the text will disappear offscreen. Note that there is a single digit for the hours!
|
||||
* @property {String} Style Style name. If it is "Default", then your own *Default style will be subtituted.
|
||||
* @property {String} Name Character name. This is the name of the character who speaks the dialogue. It is for information only, to make the script is easier to follow when editing/timing.
|
||||
* @property {Number} MarginL 4-figure Left Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
|
||||
* @property {Number} MarginR 4-figure Right Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
|
||||
* @property {Number} MarginV 4-figure Bottom Margin override. The values are in pixels. All zeroes means the default margins defined by the style are used.
|
||||
* @property {String} Effect Transition Effect. This is either empty, or contains information for one of the three transition effects implemented in SSA v4.x
|
||||
* @property {String} Text Subtitle Text. This is the actual text which will be displayed as a subtitle onscreen. Everything after the 9th comma is treated as the subtitle text, so it can include commas.
|
||||
* @property {Number} ReadOrder Number in order of which to read this event.
|
||||
* @property {Number} Layer Z-index overlap in which to render this event.
|
||||
* @property {Number} _index (Internal) index of the event.
|
||||
*/
|
||||
/**
|
||||
* Create a new ASS event directly.
|
||||
* @param {ASS_Event} event
|
||||
*/
|
||||
createEvent(e) {
|
||||
this.sendMessage("createEvent", { event: e });
|
||||
}
|
||||
/**
|
||||
* Overwrite the data of the event with the specified index.
|
||||
* @param {ASS_Event} event
|
||||
* @param {Number} index
|
||||
*/
|
||||
setEvent(e, t) {
|
||||
this.sendMessage("setEvent", { event: e, index: t });
|
||||
}
|
||||
/**
|
||||
* Remove the event with the specified index.
|
||||
* @param {Number} index
|
||||
*/
|
||||
removeEvent(e) {
|
||||
this.sendMessage("removeEvent", { index: e });
|
||||
}
|
||||
/**
|
||||
* Get all ASS events.
|
||||
* @param {function(Error|null, ASS_Event): void} callback Function to callback when worker returns the events.
|
||||
*/
|
||||
getEvents(e) {
|
||||
this._fetchFromWorker({
|
||||
target: "getEvents"
|
||||
}, (t, { events: s }) => {
|
||||
e(t, s);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Set a style override.
|
||||
* @param {ASS_Style} style
|
||||
*/
|
||||
styleOverride(e) {
|
||||
this.sendMessage("styleOverride", { style: e });
|
||||
}
|
||||
/**
|
||||
* Disable style override.
|
||||
*/
|
||||
disableStyleOverride() {
|
||||
this.sendMessage("disableStyleOverride");
|
||||
}
|
||||
/**
|
||||
* @typedef {Object} ASS_Style
|
||||
* @property {String} Name The name of the Style. Case sensitive. Cannot include commas.
|
||||
* @property {String} FontName The fontname as used by Windows. Case-sensitive.
|
||||
* @property {Number} FontSize Font size.
|
||||
* @property {Number} PrimaryColour A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR
|
||||
* @property {Number} SecondaryColour A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR
|
||||
* @property {Number} OutlineColour A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR
|
||||
* @property {Number} BackColour This is the colour of the subtitle outline or shadow, if these are used. A long integer BGR (blue-green-red) value. ie. the byte order in the hexadecimal equivelent of this number is BBGGRR.
|
||||
* @property {Number} Bold This defines whether text is bold (true) or not (false). -1 is True, 0 is False. This is independant of the Italic attribute - you can have have text which is both bold and italic.
|
||||
* @property {Number} Italic Italic. This defines whether text is italic (true) or not (false). -1 is True, 0 is False. This is independant of the bold attribute - you can have have text which is both bold and italic.
|
||||
* @property {Number} Underline -1 or 0
|
||||
* @property {Number} StrikeOut -1 or 0
|
||||
* @property {Number} ScaleX Modifies the width of the font. [percent]
|
||||
* @property {Number} ScaleY Modifies the height of the font. [percent]
|
||||
* @property {Number} Spacing Extra space between characters. [pixels]
|
||||
* @property {Number} Angle The origin of the rotation is defined by the alignment. Can be a floating point number. [degrees]
|
||||
* @property {Number} BorderStyle 1=Outline + drop shadow, 3=Opaque box
|
||||
* @property {Number} Outline If BorderStyle is 1, then this specifies the width of the outline around the text, in pixels. Values may be 0, 1, 2, 3 or 4.
|
||||
* @property {Number} Shadow If BorderStyle is 1, then this specifies the depth of the drop shadow behind the text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in addition to an outline - SSA will force an outline of 1 pixel if no outline width is given.
|
||||
* @property {Number} Alignment This sets how text is "justified" within the Left/Right onscreen margins, and also the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value for a "Toptitle". Add 8 to the value for a "Midtitle". eg. 5 = left-justified toptitle
|
||||
* @property {Number} MarginL This defines the Left Margin in pixels. It is the distance from the left-hand edge of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed.
|
||||
* @property {Number} MarginR This defines the Right Margin in pixels. It is the distance from the right-hand edge of the screen. The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed.
|
||||
* @property {Number} MarginV This defines the vertical Left Margin in pixels. For a subtitle, it is the distance from the bottom of the screen. For a toptitle, it is the distance from the top of the screen. For a midtitle, the value is ignored - the text will be vertically centred.
|
||||
* @property {Number} Encoding This specifies the font character set or encoding and on multi-lingual Windows installations it provides access to characters used in multiple than one languages. It is usually 0 (zero) for English (Western, ANSI) Windows.
|
||||
* @property {Number} treat_fontname_as_pattern
|
||||
* @property {Number} Blur
|
||||
* @property {Number} Justify
|
||||
*/
|
||||
/**
|
||||
* Create a new ASS style directly.
|
||||
* @param {ASS_Style} style
|
||||
*/
|
||||
createStyle(e) {
|
||||
this.sendMessage("createStyle", { style: e });
|
||||
}
|
||||
/**
|
||||
* Overwrite the data of the style with the specified index.
|
||||
* @param {ASS_Style} style
|
||||
* @param {Number} index
|
||||
*/
|
||||
setStyle(e, t) {
|
||||
this.sendMessage("setStyle", { style: e, index: t });
|
||||
}
|
||||
/**
|
||||
* Remove the style with the specified index.
|
||||
* @param {Number} index
|
||||
*/
|
||||
removeStyle(e) {
|
||||
this.sendMessage("removeStyle", { index: e });
|
||||
}
|
||||
/**
|
||||
* Get all ASS styles.
|
||||
* @param {function(Error|null, ASS_Style): void} callback Function to callback when worker returns the styles.
|
||||
*/
|
||||
getStyles(e) {
|
||||
this._fetchFromWorker({
|
||||
target: "getStyles"
|
||||
}, (t, { styles: s }) => {
|
||||
e(t, s);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Adds a font to the renderer.
|
||||
* @param {String|Uint8Array} font Font to add.
|
||||
*/
|
||||
addFont(e) {
|
||||
this.sendMessage("addFont", { font: e });
|
||||
}
|
||||
/**
|
||||
* Changes the font family of the default font, this font needs to be previously added via addFont or fonts array on construction.
|
||||
* @param {String} font Font family to change to.
|
||||
*/
|
||||
setDefaultFont(e) {
|
||||
this.sendMessage("defaultFont", { font: e });
|
||||
}
|
||||
_sendLocalFont(e) {
|
||||
try {
|
||||
queryLocalFonts().then((t) => {
|
||||
const s = t?.find((r) => r.fullName.toLowerCase() === e);
|
||||
s && s.blob().then((r) => {
|
||||
r.arrayBuffer().then((n) => {
|
||||
this.addFont(new Uint8Array(n));
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (t) {
|
||||
console.warn("Local fonts API:", t);
|
||||
}
|
||||
}
|
||||
_getLocalFont({ font: e }) {
|
||||
try {
|
||||
navigator?.permissions?.query ? navigator.permissions.query({ name: "local-fonts" }).then((t) => {
|
||||
t.state === "granted" && this._sendLocalFont(e);
|
||||
}) : this._sendLocalFont(e);
|
||||
} catch (t) {
|
||||
console.warn("Local fonts API:", t);
|
||||
}
|
||||
}
|
||||
_unbusy() {
|
||||
this._lastDemandTime ? this._demandRender(this._lastDemandTime) : this.busy = false;
|
||||
}
|
||||
_handleRVFC(e, { mediaTime: t, width: s, height: r }) {
|
||||
if (this._destroyed)
|
||||
return null;
|
||||
this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = true, this._demandRender({ mediaTime: t, width: s, height: r })), this._video.requestVideoFrameCallback(this._handleRVFC.bind(this));
|
||||
}
|
||||
_demandRender({ mediaTime: e, width: t, height: s }) {
|
||||
this._lastDemandTime = null, (t !== this._videoWidth || s !== this._videoHeight) && (this._videoWidth = t, this._videoHeight = s, this.resize()), this.sendMessage("demand", { time: e + this.timeOffset });
|
||||
}
|
||||
// if we're using offscreen render, we can't use ctx filters, so we can't use a transfered canvas
|
||||
_detachOffscreen() {
|
||||
if (!this._offscreenRender || this._ctx)
|
||||
return null;
|
||||
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas, this._ctx = this._canvasctrl.getContext("2d"), this.sendMessage("detachOffscreen"), this.busy = false, this.resize(0, 0, 0, 0, true);
|
||||
}
|
||||
// if the video or track changed, we need to re-attach the offscreen canvas
|
||||
_reAttachOffscreen() {
|
||||
if (!this._offscreenRender || !this._ctx)
|
||||
return null;
|
||||
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = false, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, true);
|
||||
}
|
||||
_updateColorSpace() {
|
||||
this._video.requestVideoFrameCallback(() => {
|
||||
try {
|
||||
const e = new VideoFrame(this._video);
|
||||
this._videoColorSpace = _[e.colorSpace.matrix], e.close(), this.sendMessage("getColorSpace");
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Veryify the color spaces for subtitles and videos, then apply filters to correct the color of subtitles.
|
||||
* @param {Object} options
|
||||
* @param {String} options.subtitleColorSpace Subtitle color space. One of: BT601 BT709 SMPTE240M FCC
|
||||
* @param {String=} options.videoColorSpace Video color space. One of: BT601 BT709
|
||||
*/
|
||||
_verifyColorSpace({ subtitleColorSpace: e, videoColorSpace: t = this._videoColorSpace }) {
|
||||
!e || !t || e !== t && (this._detachOffscreen(), this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${f[e][t]} 0 0 0 0 0 1 0'/></filter></svg>#f")`);
|
||||
}
|
||||
_render({ images: e, asyncRender: t, times: s, width: r, height: n, colorSpace: a }) {
|
||||
this._unbusy(), this.debug && (s.IPCTime = Date.now() - s.JSRenderTime), (this._canvasctrl.width !== r || this._canvasctrl.height !== n) && (this._canvasctrl.width = r, this._canvasctrl.height = n, this._verifyColorSpace({ subtitleColorSpace: a })), this._ctx.clearRect(0, 0, this._canvasctrl.width, this._canvasctrl.height);
|
||||
for (const i of e)
|
||||
i.image && (t ? (this._ctx.drawImage(i.image, i.x, i.y), i.image.close()) : (this._bufferCanvas.width = i.w, this._bufferCanvas.height = i.h, this._bufferCtx.putImageData(new ImageData(this._fixAlpha(new Uint8ClampedArray(i.image)), i.w, i.h), 0, 0), this._ctx.drawImage(this._bufferCanvas, i.x, i.y)));
|
||||
if (this.debug) {
|
||||
s.JSRenderTime = Date.now() - s.JSRenderTime - s.IPCTime;
|
||||
let i = 0;
|
||||
const h = s.bitmaps || e.length;
|
||||
delete s.bitmaps;
|
||||
for (const d in s)
|
||||
i += s[d];
|
||||
console.log("Bitmaps: " + h + " Total: " + (i | 0) + "ms", s);
|
||||
}
|
||||
}
|
||||
_fixAlpha(e) {
|
||||
if (_o._hasAlphaBug)
|
||||
for (let t = 3; t < e.length; t += 4)
|
||||
e[t] = e[t] > 1 ? e[t] : 1;
|
||||
return e;
|
||||
}
|
||||
_ready() {
|
||||
this._init(), this.dispatchEvent(new CustomEvent("ready"));
|
||||
}
|
||||
/**
|
||||
* Send data and execute function in the worker.
|
||||
* @param {String} target Target function.
|
||||
* @param {Object} [data] Data for function.
|
||||
* @param {Transferable[]} [transferable] Array of transferables.
|
||||
*/
|
||||
async sendMessage(e, t = {}, s) {
|
||||
await this._loaded, s ? this._worker.postMessage({
|
||||
target: e,
|
||||
transferable: s,
|
||||
...t
|
||||
}, [...s]) : this._worker.postMessage({
|
||||
target: e,
|
||||
...t
|
||||
});
|
||||
}
|
||||
_fetchFromWorker(e, t) {
|
||||
try {
|
||||
const s = e.target, r = setTimeout(() => {
|
||||
a(new Error("Error: Timeout while try to fetch " + s));
|
||||
}, 5e3), n = ({ data: i }) => {
|
||||
i.target === s && (t(null, i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r));
|
||||
}, a = (i) => {
|
||||
t(i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r);
|
||||
};
|
||||
this._worker.addEventListener("message", n), this._worker.addEventListener("error", a), this._worker.postMessage(e);
|
||||
} catch (s) {
|
||||
this._error(s);
|
||||
}
|
||||
}
|
||||
_console({ content: e, command: t }) {
|
||||
console[t].apply(console, JSON.parse(e));
|
||||
}
|
||||
_onmessage({ data: e }) {
|
||||
this["_" + e.target] && this["_" + e.target](e);
|
||||
}
|
||||
_error(e) {
|
||||
const t = e instanceof Error ? e : e instanceof ErrorEvent ? e.error : new Error(e), s = e instanceof Event ? new ErrorEvent(e.type, e) : new ErrorEvent("error", { error: t });
|
||||
return this.dispatchEvent(s), console.error(t), t;
|
||||
}
|
||||
_removeListeners() {
|
||||
this._video && (this._ro && this._ro.unobserve(this._video), this._ctx && (this._ctx.filter = "none"), this._video.removeEventListener("timeupdate", this._boundTimeUpdate), this._video.removeEventListener("progress", this._boundTimeUpdate), this._video.removeEventListener("waiting", this._boundTimeUpdate), this._video.removeEventListener("seeking", this._boundTimeUpdate), this._video.removeEventListener("playing", this._boundTimeUpdate), this._video.removeEventListener("ratechange", this._boundSetRate), this._video.removeEventListener("resize", this._boundResize), this._video.removeEventListener("loadedmetadata", this._boundUpdateColorSpace));
|
||||
}
|
||||
/**
|
||||
* Destroy the object, worker, listeners and all data.
|
||||
* @param {String|Error} [err] Error to throw when destroying.
|
||||
*/
|
||||
destroy(e) {
|
||||
return e && (e = this._error(e)), this._video && this._canvasParent && this._video.parentNode?.removeChild(this._canvasParent), this._destroyed = true, this._removeListeners(), this.sendMessage("destroy"), this._worker?.terminate(), e;
|
||||
}
|
||||
};
|
||||
// test support for WASM, ImageData, alphaBug, but only once, on init so it doesn't run when first running the page
|
||||
/** @type {boolean|null} */
|
||||
__publicField(_o, "_supportsSIMD", null);
|
||||
/** @type {boolean|null} */
|
||||
__publicField(_o, "_hasAlphaBug", null);
|
||||
/** @type {boolean|null} */
|
||||
__publicField(_o, "_hasBitmapBug", null);
|
||||
let o = _o;
|
||||
function artplayerPluginJassub2(option) {
|
||||
return (art) => {
|
||||
const instance = new o({
|
||||
video: art.video,
|
||||
...option
|
||||
});
|
||||
instance._canvasParent.style.zIndex = 20;
|
||||
art.on("destroy", () => {
|
||||
instance.destroy();
|
||||
});
|
||||
return {
|
||||
name: "artplayerPluginJassub",
|
||||
instance
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginJassub2;
|
||||
})();
|
||||
@@ -0,0 +1,829 @@
|
||||
var artplayerPluginMultipleSubtitles = (function() {
|
||||
"use strict";
|
||||
var defaultCueSettings = {
|
||||
direction: "horizontal",
|
||||
snapToLines: true,
|
||||
linePosition: "auto",
|
||||
lineAlign: "start",
|
||||
textPosition: "auto",
|
||||
positionAlign: "auto",
|
||||
size: 100,
|
||||
alignment: "center"
|
||||
};
|
||||
var WebVTTParser = function(entities) {
|
||||
if (!entities) {
|
||||
entities = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&lrm": "",
|
||||
"&rlm": "",
|
||||
" ": " "
|
||||
};
|
||||
}
|
||||
this.entities = entities;
|
||||
this.parse = function(input, mode) {
|
||||
input = input.replace(/\0/g, "�");
|
||||
var NEWLINE = /\r\n|\r|\n/, startTime = Date.now(), linePos = 0, lines = input.split(NEWLINE), alreadyCollected = false, styles = [], cues = [], errors = [];
|
||||
function err(message, col) {
|
||||
errors.push({ message, line: linePos + 1, col });
|
||||
}
|
||||
var line = lines[linePos], lineLength = line.length, signature = "WEBVTT", bom = 0, signature_length = signature.length;
|
||||
if (line[0] === "\uFEFF") {
|
||||
bom = 1;
|
||||
signature_length += 1;
|
||||
}
|
||||
if (lineLength < signature_length || line.indexOf(signature) !== 0 + bom || lineLength > signature_length && line[signature_length] !== " " && line[signature_length] !== " ") {
|
||||
err('No valid signature. (File needs to start with "WEBVTT".)');
|
||||
}
|
||||
linePos++;
|
||||
while (lines[linePos] != "" && lines[linePos] != void 0) {
|
||||
err("No blank line after the signature.");
|
||||
if (lines[linePos].indexOf("-->") != -1) {
|
||||
alreadyCollected = true;
|
||||
break;
|
||||
}
|
||||
linePos++;
|
||||
}
|
||||
while (lines[linePos] != void 0) {
|
||||
var cue;
|
||||
while (!alreadyCollected && lines[linePos] == "") {
|
||||
linePos++;
|
||||
}
|
||||
if (!alreadyCollected && lines[linePos] == void 0) break;
|
||||
cue = Object.assign({}, defaultCueSettings, {
|
||||
id: "",
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
pauseOnExit: false,
|
||||
direction: "horizontal",
|
||||
snapToLines: true,
|
||||
linePosition: "auto",
|
||||
lineAlign: "start",
|
||||
textPosition: "auto",
|
||||
positionAlign: "auto",
|
||||
size: 100,
|
||||
alignment: "center",
|
||||
text: "",
|
||||
tree: null
|
||||
});
|
||||
var parseTimings = true;
|
||||
if (lines[linePos].indexOf("-->") == -1) {
|
||||
cue.id = lines[linePos];
|
||||
if (/^NOTE($|[ \t])/.test(cue.id)) {
|
||||
linePos++;
|
||||
while (lines[linePos] != "" && lines[linePos] != void 0) {
|
||||
if (lines[linePos].indexOf("-->") != -1) err("Cannot have timestamp in a comment.");
|
||||
linePos++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (/^STYLE($|[ \t])/.test(cue.id)) {
|
||||
var style = [];
|
||||
var invalid = false;
|
||||
linePos++;
|
||||
while (lines[linePos] != "" && lines[linePos] != void 0) {
|
||||
if (lines[linePos].indexOf("-->") != -1) {
|
||||
err("Cannot have timestamp in a style block.");
|
||||
invalid = true;
|
||||
}
|
||||
style.push(lines[linePos]);
|
||||
linePos++;
|
||||
}
|
||||
if (cues.length) {
|
||||
err("Style blocks cannot appear after the first cue.");
|
||||
continue;
|
||||
}
|
||||
if (!invalid) {
|
||||
styles.push(style.join("\n"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
linePos++;
|
||||
if (lines[linePos] == "" || lines[linePos] == void 0) {
|
||||
err("Cue identifier cannot be standalone.");
|
||||
continue;
|
||||
}
|
||||
if (lines[linePos].indexOf("-->") == -1) {
|
||||
parseTimings = false;
|
||||
err("Cue identifier needs to be followed by timestamp.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
alreadyCollected = false;
|
||||
var timings = new WebVTTCueTimingsAndSettingsParser(lines[linePos], err);
|
||||
var previousCueStart = 0;
|
||||
if (cues.length > 0) {
|
||||
previousCueStart = cues[cues.length - 1].startTime;
|
||||
}
|
||||
if (parseTimings && !timings.parse(cue, previousCueStart)) {
|
||||
cue = null;
|
||||
linePos++;
|
||||
while (lines[linePos] != "" && lines[linePos] != void 0) {
|
||||
if (lines[linePos].indexOf("-->") != -1) {
|
||||
alreadyCollected = true;
|
||||
break;
|
||||
}
|
||||
linePos++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
linePos++;
|
||||
while (lines[linePos] != "" && lines[linePos] != void 0) {
|
||||
if (lines[linePos].indexOf("-->") != -1) {
|
||||
err("Blank line missing before cue.");
|
||||
alreadyCollected = true;
|
||||
break;
|
||||
}
|
||||
if (cue.text != "") cue.text += "\n";
|
||||
cue.text += lines[linePos];
|
||||
linePos++;
|
||||
}
|
||||
var cuetextparser = new WebVTTCueTextParser(cue.text, err, mode, entities);
|
||||
cue.tree = cuetextparser.parse(cue.startTime, cue.endTime);
|
||||
cues.push(cue);
|
||||
}
|
||||
cues.sort(function(a, b) {
|
||||
if (a.startTime < b.startTime) return -1;
|
||||
if (a.startTime > b.startTime) return 1;
|
||||
if (a.endTime > b.endTime) return -1;
|
||||
if (a.endTime < b.endTime) return 1;
|
||||
return 0;
|
||||
});
|
||||
return { cues, errors, time: Date.now() - startTime, styles };
|
||||
};
|
||||
};
|
||||
var WebVTTCueTimingsAndSettingsParser = function(line, errorHandler) {
|
||||
var SPACE = /[\u0020\t\f]/, NOSPACE = /[^\u0020\t\f]/, line = line, pos = 0, err = function(message) {
|
||||
errorHandler(message, pos + 1);
|
||||
};
|
||||
function skip(pattern) {
|
||||
while (line[pos] != void 0 && pattern.test(line[pos])) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
function collect(pattern) {
|
||||
var str = "";
|
||||
while (line[pos] != void 0 && pattern.test(line[pos])) {
|
||||
str += line[pos];
|
||||
pos++;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function timestamp() {
|
||||
var units = "minutes", val1, val2, val3, val4;
|
||||
if (line[pos] == void 0) {
|
||||
err("No timestamp found.");
|
||||
return;
|
||||
}
|
||||
if (!/\d/.test(line[pos])) {
|
||||
err("Timestamp must start with a character in the range 0-9.");
|
||||
return;
|
||||
}
|
||||
val1 = collect(/\d/);
|
||||
if (val1.length > 2 || parseInt(val1, 10) > 59) {
|
||||
units = "hours";
|
||||
}
|
||||
if (line[pos] != ":") {
|
||||
err("No time unit separator found.");
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
val2 = collect(/\d/);
|
||||
if (val2.length != 2) {
|
||||
err("Must be exactly two digits.");
|
||||
return;
|
||||
}
|
||||
if (units == "hours" || line[pos] == ":") {
|
||||
if (line[pos] != ":") {
|
||||
err("No seconds found or minutes is greater than 59.");
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
val3 = collect(/\d/);
|
||||
if (val3.length != 2) {
|
||||
err("Must be exactly two digits.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (val1.length != 2) {
|
||||
err("Must be exactly two digits.");
|
||||
return;
|
||||
}
|
||||
val3 = val2;
|
||||
val2 = val1;
|
||||
val1 = "0";
|
||||
}
|
||||
if (line[pos] != ".") {
|
||||
err('No decimal separator (".") found.');
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
val4 = collect(/\d/);
|
||||
if (val4.length != 3) {
|
||||
err("Milliseconds must be given in three digits.");
|
||||
return;
|
||||
}
|
||||
if (parseInt(val2, 10) > 59) {
|
||||
err("You cannot have more than 59 minutes.");
|
||||
return;
|
||||
}
|
||||
if (parseInt(val3, 10) > 59) {
|
||||
err("You cannot have more than 59 seconds.");
|
||||
return;
|
||||
}
|
||||
return parseInt(val1, 10) * 60 * 60 + parseInt(val2, 10) * 60 + parseInt(val3, 10) + parseInt(val4, 10) / 1e3;
|
||||
}
|
||||
function parseSettings(input, cue) {
|
||||
var settings = input.split(SPACE), seen = [];
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i] == "") continue;
|
||||
var index = settings[i].indexOf(":"), setting = settings[i].slice(0, index), value = settings[i].slice(index + 1);
|
||||
if (seen.indexOf(setting) != -1) {
|
||||
err("Duplicate setting.");
|
||||
}
|
||||
seen.push(setting);
|
||||
if (value == "") {
|
||||
err("No value for setting defined.");
|
||||
return;
|
||||
}
|
||||
if (setting == "vertical") {
|
||||
if (value != "rl" && value != "lr") {
|
||||
err("Writing direction can only be set to 'rl' or 'rl'.");
|
||||
continue;
|
||||
}
|
||||
cue.direction = value;
|
||||
} else if (setting == "line") {
|
||||
if (/,/.test(value)) {
|
||||
var comp = value.split(",");
|
||||
value = comp[0];
|
||||
var lineAlign = comp[1];
|
||||
}
|
||||
if (!/^[-\d](\d*)(\.\d+)?%?$/.test(value)) {
|
||||
err("Line position takes a number or percentage.");
|
||||
continue;
|
||||
}
|
||||
if (value.indexOf("-", 1) != -1) {
|
||||
err("Line position can only have '-' at the start.");
|
||||
continue;
|
||||
}
|
||||
if (value.indexOf("%") != -1 && value.indexOf("%") != value.length - 1) {
|
||||
err("Line position can only have '%' at the end.");
|
||||
continue;
|
||||
}
|
||||
if (value[0] == "-" && value[value.length - 1] == "%") {
|
||||
err("Line position cannot be a negative percentage.");
|
||||
continue;
|
||||
}
|
||||
var numVal = value;
|
||||
var isPercent = false;
|
||||
if (value[value.length - 1] == "%") {
|
||||
isPercent = true;
|
||||
numVal = value.slice(0, value.length - 1);
|
||||
if (parseInt(value, 10) > 100) {
|
||||
err("Line position cannot be >100%.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (numVal === "" || isNaN(numVal) || !isFinite(numVal)) {
|
||||
err("Line position needs to be a number");
|
||||
continue;
|
||||
}
|
||||
if (lineAlign !== void 0) {
|
||||
if (!["start", "center", "end"].includes(lineAlign)) {
|
||||
err("Line alignment needs to be one of start, center or end");
|
||||
continue;
|
||||
}
|
||||
cue.lineAlign = lineAlign;
|
||||
}
|
||||
cue.snapToLines = !isPercent;
|
||||
cue.linePosition = parseFloat(numVal);
|
||||
if (parseFloat(numVal).toString() !== numVal) {
|
||||
cue.nonSerializable = true;
|
||||
}
|
||||
} else if (setting == "position") {
|
||||
if (/,/.test(value)) {
|
||||
var comp = value.split(",");
|
||||
value = comp[0];
|
||||
var positionAlign = comp[1];
|
||||
}
|
||||
if (value[value.length - 1] != "%") {
|
||||
err("Text position must be a percentage.");
|
||||
continue;
|
||||
}
|
||||
if (parseInt(value, 10) > 100 || parseInt(value, 10) < 0) {
|
||||
err("Text position needs to be between 0 and 100%.");
|
||||
continue;
|
||||
}
|
||||
numVal = value.slice(0, value.length - 1);
|
||||
if (numVal === "" || isNaN(numVal) || !isFinite(numVal)) {
|
||||
err("Line position needs to be a number");
|
||||
continue;
|
||||
}
|
||||
if (positionAlign !== void 0) {
|
||||
if (!["line-left", "center", "line-right"].includes(positionAlign)) {
|
||||
err("Position alignment needs to be one of line-left, center or line-right");
|
||||
continue;
|
||||
}
|
||||
cue.positionAlign = positionAlign;
|
||||
}
|
||||
cue.textPosition = parseFloat(numVal);
|
||||
} else if (setting == "size") {
|
||||
if (value[value.length - 1] != "%") {
|
||||
err("Size must be a percentage.");
|
||||
continue;
|
||||
}
|
||||
if (parseInt(value, 10) > 100) {
|
||||
err("Size cannot be >100%.");
|
||||
continue;
|
||||
}
|
||||
var size = value.slice(0, value.length - 1);
|
||||
if (size === void 0 || size === "" || isNaN(size)) {
|
||||
err("Size needs to be a number");
|
||||
size = 100;
|
||||
continue;
|
||||
} else {
|
||||
size = parseFloat(size);
|
||||
if (size < 0 || size > 100) {
|
||||
err("Size needs to be between 0 and 100%.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
cue.size = size;
|
||||
} else if (setting == "align") {
|
||||
var alignValues = ["start", "center", "end", "left", "right"];
|
||||
if (alignValues.indexOf(value) == -1) {
|
||||
err("Alignment can only be set to one of " + alignValues.join(", ") + ".");
|
||||
continue;
|
||||
}
|
||||
cue.alignment = value;
|
||||
} else {
|
||||
err("Invalid setting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
this.parse = function(cue, previousCueStart) {
|
||||
skip(SPACE);
|
||||
cue.startTime = timestamp();
|
||||
if (cue.startTime == void 0) {
|
||||
return;
|
||||
}
|
||||
if (cue.startTime < previousCueStart) {
|
||||
err("Start timestamp is not greater than or equal to start timestamp of previous cue.");
|
||||
}
|
||||
if (NOSPACE.test(line[pos])) {
|
||||
err("Timestamp not separated from '-->' by whitespace.");
|
||||
}
|
||||
skip(SPACE);
|
||||
if (line[pos] != "-") {
|
||||
err("No valid timestamp separator found.");
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
if (line[pos] != "-") {
|
||||
err("No valid timestamp separator found.");
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
if (line[pos] != ">") {
|
||||
err("No valid timestamp separator found.");
|
||||
return;
|
||||
}
|
||||
pos++;
|
||||
if (NOSPACE.test(line[pos])) {
|
||||
err("'-->' not separated from timestamp by whitespace.");
|
||||
}
|
||||
skip(SPACE);
|
||||
cue.endTime = timestamp();
|
||||
if (cue.endTime == void 0) {
|
||||
return;
|
||||
}
|
||||
if (cue.endTime <= cue.startTime) {
|
||||
err("End timestamp is not greater than start timestamp.");
|
||||
}
|
||||
if (NOSPACE.test(line[pos])) ;
|
||||
skip(SPACE);
|
||||
parseSettings(line.substring(pos), cue);
|
||||
return true;
|
||||
};
|
||||
this.parseTimestamp = function() {
|
||||
var ts = timestamp();
|
||||
if (line[pos] != void 0) {
|
||||
err("Timestamp must not have trailing characters.");
|
||||
return;
|
||||
}
|
||||
return ts;
|
||||
};
|
||||
};
|
||||
var WebVTTCueTextParser = function(line, errorHandler, mode, entities) {
|
||||
this.entities = entities;
|
||||
var self = this;
|
||||
var line = line, pos = 0, err = function(message) {
|
||||
if (mode == "metadata") return;
|
||||
errorHandler(message, pos + 1);
|
||||
};
|
||||
this.parse = function(cueStart, cueEnd) {
|
||||
function removeCycles(tree) {
|
||||
const cyclelessTree = { ...tree };
|
||||
if (tree.children) {
|
||||
cyclelessTree.children = tree.children.map(removeCycles);
|
||||
}
|
||||
if (cyclelessTree.parent) {
|
||||
delete cyclelessTree.parent;
|
||||
}
|
||||
return cyclelessTree;
|
||||
}
|
||||
var result = { children: [] }, current = result, timestamps = [];
|
||||
function attach(token2) {
|
||||
current.children.push({
|
||||
type: "object",
|
||||
name: token2[1],
|
||||
classes: token2[2],
|
||||
children: [],
|
||||
parent: current
|
||||
});
|
||||
current = current.children[current.children.length - 1];
|
||||
}
|
||||
function inScope(name2) {
|
||||
var node = current;
|
||||
while (node) {
|
||||
if (node.name == name2) return true;
|
||||
node = node.parent;
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (line[pos] != void 0) {
|
||||
var token = nextToken();
|
||||
if (token[0] == "text") {
|
||||
current.children.push({ type: "text", value: token[1], parent: current });
|
||||
} else if (token[0] == "start tag") {
|
||||
if (mode == "chapters") err("Start tags not allowed in chapter title text.");
|
||||
var name = token[1];
|
||||
if (name != "v" && name != "lang" && token[3] != "") {
|
||||
err("Only <v> and <lang> can have an annotation.");
|
||||
}
|
||||
if (name == "c" || name == "i" || name == "b" || name == "u" || name == "ruby") {
|
||||
attach(token);
|
||||
} else if (name == "rt" && current.name == "ruby") {
|
||||
attach(token);
|
||||
} else if (name == "v") {
|
||||
if (inScope("v")) {
|
||||
err("<v> cannot be nested inside itself.");
|
||||
}
|
||||
attach(token);
|
||||
current.value = token[3];
|
||||
if (!token[3]) {
|
||||
err("<v> requires an annotation.");
|
||||
}
|
||||
} else if (name == "lang") {
|
||||
attach(token);
|
||||
current.value = token[3];
|
||||
} else {
|
||||
err("Incorrect start tag.");
|
||||
}
|
||||
} else if (token[0] == "end tag") {
|
||||
if (mode == "chapters") err("End tags not allowed in chapter title text.");
|
||||
if (token[1] == current.name) {
|
||||
current = current.parent;
|
||||
} else if (token[1] == "ruby" && current.name == "rt") {
|
||||
current = current.parent.parent;
|
||||
} else {
|
||||
err("Incorrect end tag.");
|
||||
}
|
||||
} else if (token[0] == "timestamp") {
|
||||
if (mode == "chapters") err("Timestamp not allowed in chapter title text.");
|
||||
var timings = new WebVTTCueTimingsAndSettingsParser(token[1], err), timestamp = timings.parseTimestamp();
|
||||
if (timestamp != void 0) {
|
||||
if (timestamp <= cueStart || timestamp >= cueEnd) {
|
||||
err("Timestamp must be between start timestamp and end timestamp.");
|
||||
}
|
||||
if (timestamps.length > 0 && timestamps[timestamps.length - 1] >= timestamp) {
|
||||
err("Timestamp must be greater than any previous timestamp.");
|
||||
}
|
||||
current.children.push({ type: "timestamp", value: timestamp, parent: current });
|
||||
timestamps.push(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (current.parent) {
|
||||
if (current.name != "v") {
|
||||
err("Required end tag missing.");
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return removeCycles(result);
|
||||
};
|
||||
function nextToken() {
|
||||
var state = "data", result = "", buffer = "", classes = [];
|
||||
while (line[pos - 1] != void 0 || pos == 0) {
|
||||
var c = line[pos];
|
||||
if (state == "data") {
|
||||
if (c == "&") {
|
||||
buffer = c;
|
||||
state = "escape";
|
||||
} else if (c == "<" && result == "") {
|
||||
state = "tag";
|
||||
} else if (c == "<" || c == void 0) {
|
||||
return ["text", result];
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
} else if (state == "escape") {
|
||||
if (c == "<" || c == void 0) {
|
||||
err("Incorrect escape.");
|
||||
let m;
|
||||
if (m = buffer.match(/^&#([0-9]+)$/)) {
|
||||
result += String.fromCharCode(m[1]);
|
||||
} else {
|
||||
if (self.entities[buffer]) {
|
||||
result += self.entities[buffer];
|
||||
} else {
|
||||
result += buffer;
|
||||
}
|
||||
}
|
||||
return ["text", result];
|
||||
} else if (c == "&") {
|
||||
err("Incorrect escape.");
|
||||
result += buffer;
|
||||
buffer = c;
|
||||
} else if (/[a-z#0-9]/i.test(c)) {
|
||||
buffer += c;
|
||||
} else if (c == ";") {
|
||||
let m;
|
||||
if (m = buffer.match(/^&#(x?[0-9]+)$/)) {
|
||||
result += String.fromCharCode("0" + m[1]);
|
||||
} else if (self.entities[buffer + c]) {
|
||||
result += self.entities[buffer + c];
|
||||
} else if (m = Object.keys(entities).find((n) => buffer.startsWith(n))) {
|
||||
result += self.entities[m] + buffer.slice(m.length) + c;
|
||||
} else {
|
||||
err("Incorrect escape.");
|
||||
result += buffer + ";";
|
||||
}
|
||||
state = "data";
|
||||
} else {
|
||||
err("Incorrect escape.");
|
||||
result += buffer + c;
|
||||
state = "data";
|
||||
}
|
||||
} else if (state == "tag") {
|
||||
if (c == " " || c == "\n" || c == "\f" || c == " ") {
|
||||
state = "start tag annotation";
|
||||
} else if (c == ".") {
|
||||
state = "start tag class";
|
||||
} else if (c == "/") {
|
||||
state = "end tag";
|
||||
} else if (/\d/.test(c)) {
|
||||
result = c;
|
||||
state = "timestamp tag";
|
||||
} else if (c == ">" || c == void 0) {
|
||||
if (c == ">") {
|
||||
pos++;
|
||||
}
|
||||
return ["start tag", "", [], ""];
|
||||
} else {
|
||||
result = c;
|
||||
state = "start tag";
|
||||
}
|
||||
} else if (state == "start tag") {
|
||||
if (c == " " || c == "\f" || c == " ") {
|
||||
state = "start tag annotation";
|
||||
} else if (c == "\n") {
|
||||
buffer = c;
|
||||
state = "start tag annotation";
|
||||
} else if (c == ".") {
|
||||
state = "start tag class";
|
||||
} else if (c == ">" || c == void 0) {
|
||||
if (c == ">") {
|
||||
pos++;
|
||||
}
|
||||
return ["start tag", result, [], ""];
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
} else if (state == "start tag class") {
|
||||
if (c == " " || c == "\f" || c == " ") {
|
||||
if (buffer) {
|
||||
classes.push(buffer);
|
||||
}
|
||||
buffer = "";
|
||||
state = "start tag annotation";
|
||||
} else if (c == "\n") {
|
||||
if (buffer) {
|
||||
classes.push(buffer);
|
||||
}
|
||||
buffer = c;
|
||||
state = "start tag annotation";
|
||||
} else if (c == ".") {
|
||||
if (buffer) {
|
||||
classes.push(buffer);
|
||||
}
|
||||
buffer = "";
|
||||
} else if (c == ">" || c == void 0) {
|
||||
if (c == ">") {
|
||||
pos++;
|
||||
}
|
||||
if (buffer) {
|
||||
classes.push(buffer);
|
||||
}
|
||||
return ["start tag", result, classes, ""];
|
||||
} else {
|
||||
buffer += c;
|
||||
}
|
||||
} else if (state == "start tag annotation") {
|
||||
if (c == ">" || c == void 0) {
|
||||
if (c == ">") {
|
||||
pos++;
|
||||
}
|
||||
buffer = buffer.split(/[\u0020\t\f\r\n]+/).filter(function(item) {
|
||||
if (item) return true;
|
||||
}).join(" ");
|
||||
return ["start tag", result, classes, buffer];
|
||||
} else {
|
||||
buffer += c;
|
||||
}
|
||||
} else if (state == "end tag") {
|
||||
if (c == ">" || c == void 0) {
|
||||
if (c == ">") {
|
||||
pos++;
|
||||
}
|
||||
return ["end tag", result];
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
} else if (state == "timestamp tag") {
|
||||
if (c == ">" || c == void 0) {
|
||||
if (c == ">") {
|
||||
pos++;
|
||||
}
|
||||
return ["timestamp", result];
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
} else {
|
||||
err("Never happens.");
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
};
|
||||
var WebVTTSerializer = function() {
|
||||
function serializeTimestamp(seconds) {
|
||||
const ms = ("00" + (seconds - Math.floor(seconds)).toFixed(3) * 1e3).slice(-3);
|
||||
let h = 0, m = 0, s = 0;
|
||||
if (seconds >= 3600) {
|
||||
h = Math.floor(seconds / 3600);
|
||||
}
|
||||
m = Math.floor((seconds - 3600 * h) / 60);
|
||||
s = Math.floor(seconds - 3600 * h - 60 * m);
|
||||
return (h ? h + ":" : "") + ("" + m).padStart(2, "0") + ":" + ("" + s).padStart(2, "0") + "." + ms;
|
||||
}
|
||||
function serializeCueSettings(cue) {
|
||||
var result = "";
|
||||
const nonDefaultSettings = Object.keys(defaultCueSettings).filter((s) => cue[s] !== defaultCueSettings[s]);
|
||||
if (nonDefaultSettings.includes("direction")) {
|
||||
result += " vertical:" + cue.direction;
|
||||
}
|
||||
if (nonDefaultSettings.includes("alignment")) {
|
||||
result += " align:" + cue.alignment;
|
||||
}
|
||||
if (nonDefaultSettings.includes("size")) {
|
||||
result += " size:" + cue.size + "%";
|
||||
}
|
||||
if (nonDefaultSettings.includes("lineAlign") || nonDefaultSettings.includes("linePosition")) {
|
||||
result += " line:" + cue.linePosition + (cue.snapToLines ? "" : "%") + (cue.lineAlign && cue.lineAlign != defaultCueSettings.lineAlign ? "," + cue.lineAlign : "");
|
||||
}
|
||||
if (nonDefaultSettings.includes("textPosition") || nonDefaultSettings.includes("positionAlign")) {
|
||||
result += " position:" + cue.textPosition + "%" + (cue.positionAlign && cue.positionAlign !== defaultCueSettings.positionAlign ? "," + cue.positionAlign : "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function serializeTree(tree) {
|
||||
var result = "";
|
||||
for (var i = 0; i < tree.length; i++) {
|
||||
var node = tree[i];
|
||||
if (node.type == "text") {
|
||||
result += node.value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
} else if (node.type == "object") {
|
||||
result += "<" + node.name;
|
||||
if (node.classes) {
|
||||
for (var y = 0; y < node.classes.length; y++) {
|
||||
result += "." + node.classes[y];
|
||||
}
|
||||
}
|
||||
if (node.value) {
|
||||
result += " " + node.value;
|
||||
}
|
||||
result += ">";
|
||||
if (node.children) result += serializeTree(node.children);
|
||||
result += "</" + node.name + ">";
|
||||
} else if (node.type == "timestamp") {
|
||||
result += "<" + serializeTimestamp(node.value) + ">";
|
||||
} else {
|
||||
result += "<" + node.value + ">";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function serializeCue(cue) {
|
||||
return (cue.id !== void 0 ? cue.id + "\n" : "") + serializeTimestamp(cue.startTime) + " --> " + serializeTimestamp(cue.endTime) + serializeCueSettings(cue) + "\n" + serializeTree(cue.tree.children) + "\n\n";
|
||||
}
|
||||
function serializeStyle(style) {
|
||||
return "STYLE\n" + style + "\n\n";
|
||||
}
|
||||
this.serialize = function(cues, styles) {
|
||||
var result = "WEBVTT\n\n";
|
||||
if (styles) {
|
||||
for (var i = 0; i < styles.length; i++) {
|
||||
result += serializeStyle(styles[i]);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < cues.length; i++) {
|
||||
result += serializeCue(cues[i]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
async function loadVtt(option, { getExt, srtToVtt, assToVtt }) {
|
||||
const response = await fetch(option.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const decoder = new TextDecoder(option.encoding || "utf-8");
|
||||
const text = decoder.decode(buffer);
|
||||
switch (option.type || getExt(option.url)) {
|
||||
case "srt": {
|
||||
return srtToVtt(text);
|
||||
}
|
||||
case "ass": {
|
||||
return assToVtt(text);
|
||||
}
|
||||
case "vtt": {
|
||||
return text;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function mergeTrees(trees) {
|
||||
const parser = new WebVTTParser();
|
||||
const result = parser.parse("", "metadata");
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
const tree = trees[i];
|
||||
if (!tree.updated) {
|
||||
tree.updated = true;
|
||||
for (let j = 0; j < tree.cues.length; j++) {
|
||||
const cue = tree.cues[j];
|
||||
for (let k = 0; k < cue.tree.children.length; k++) {
|
||||
const children = cue.tree.children[k];
|
||||
children.value = `<div class="art-subtitle-${tree.name}">${children.value}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
result.cues.push(...tree.cues);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function artplayerPluginMultipleSubtitles2({ subtitles = [] }) {
|
||||
return async (art) => {
|
||||
const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils;
|
||||
const parser = new WebVTTParser();
|
||||
const seri = new WebVTTSerializer();
|
||||
const vtts = await Promise.all(
|
||||
subtitles.map((option) => {
|
||||
return loadVtt(option, { getExt, srtToVtt, assToVtt });
|
||||
})
|
||||
);
|
||||
const trees = vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, "metadata");
|
||||
tree.url = subtitles[index].url;
|
||||
tree.name = subtitles[index].name;
|
||||
return tree;
|
||||
});
|
||||
let lastUrl = "";
|
||||
function setTracks(trees2 = []) {
|
||||
const tree = mergeTrees(trees2);
|
||||
const vtt = seri.serialize(tree.cues);
|
||||
URL.revokeObjectURL(lastUrl);
|
||||
const url = URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
||||
lastUrl = url;
|
||||
art.option.subtitle.escape = false;
|
||||
art.subtitle.init({
|
||||
...art.option.subtitle,
|
||||
url,
|
||||
type: "vtt",
|
||||
onVttLoad: unescape
|
||||
});
|
||||
}
|
||||
setTracks(trees);
|
||||
return {
|
||||
name: "multipleSubtitles",
|
||||
tracks(names = []) {
|
||||
return setTracks(names.map((name) => trees.find((tree) => tree.name === name)));
|
||||
},
|
||||
reset() {
|
||||
return setTracks(trees);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginMultipleSubtitles2;
|
||||
})();
|
||||
@@ -0,0 +1,547 @@
|
||||
var artplayerPluginVast = (function() {
|
||||
"use strict";
|
||||
var t = null, i = function() {
|
||||
t = null;
|
||||
}, e = function() {
|
||||
var e2 = window;
|
||||
return e2.google && e2.google.ima ? Promise.resolve(e2.google.ima) : t || ((t = new Promise(function(t2, i2) {
|
||||
var e3 = document.createElement("script");
|
||||
e3.async = true, e3.src = "https://imasdk.googleapis.com/js/sdkloader/ima3.js", e3.onload = t2, e3.onerror = i2, document.body.appendChild(e3);
|
||||
}).then(function() {
|
||||
return e2.google.ima;
|
||||
})).then(i).catch(i), t);
|
||||
};
|
||||
function n(t2, i2) {
|
||||
for (var e2 = 0; e2 < i2.length; e2++) {
|
||||
var n2 = i2[e2];
|
||||
n2.enumerable = n2.enumerable || false, n2.configurable = true, "value" in n2 && (n2.writable = true), Object.defineProperty(t2, n2.key, n2);
|
||||
}
|
||||
}
|
||||
function s(t2, i2, e2) {
|
||||
return i2 && n(t2.prototype, i2), t2;
|
||||
}
|
||||
function h() {
|
||||
return h = Object.assign || function(t2) {
|
||||
for (var i2 = 1; i2 < arguments.length; i2++) {
|
||||
var e2 = arguments[i2];
|
||||
for (var n2 in e2) Object.prototype.hasOwnProperty.call(e2, n2) && (t2[n2] = e2[n2]);
|
||||
}
|
||||
return t2;
|
||||
}, h.apply(this, arguments);
|
||||
}
|
||||
function r(t2, i2) {
|
||||
t2.prototype = Object.create(i2.prototype), t2.prototype.constructor = t2, a(t2, i2);
|
||||
}
|
||||
function o(t2) {
|
||||
return o = Object.setPrototypeOf ? Object.getPrototypeOf : function(t3) {
|
||||
return t3.__proto__ || Object.getPrototypeOf(t3);
|
||||
}, o(t2);
|
||||
}
|
||||
function a(t2, i2) {
|
||||
return a = Object.setPrototypeOf || function(t3, i3) {
|
||||
return t3.__proto__ = i3, t3;
|
||||
}, a(t2, i2);
|
||||
}
|
||||
function u() {
|
||||
if ("undefined" == typeof Reflect || !Reflect.construct) return false;
|
||||
if (Reflect.construct.sham) return false;
|
||||
if ("function" == typeof Proxy) return true;
|
||||
try {
|
||||
return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {
|
||||
})), true;
|
||||
} catch (t2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function c(t2, i2, e2) {
|
||||
return c = u() ? Reflect.construct : function(t3, i3, e3) {
|
||||
var n2 = [null];
|
||||
n2.push.apply(n2, i3);
|
||||
var s2 = new (Function.bind.apply(t3, n2))();
|
||||
return e3 && a(s2, e3.prototype), s2;
|
||||
}, c.apply(null, arguments);
|
||||
}
|
||||
function d(t2) {
|
||||
var i2 = "function" == typeof Map ? /* @__PURE__ */ new Map() : void 0;
|
||||
return d = function(t3) {
|
||||
if (null === t3 || -1 === Function.toString.call(t3).indexOf("[native code]")) return t3;
|
||||
if ("function" != typeof t3) throw new TypeError("Super expression must either be null or a function");
|
||||
if (void 0 !== i2) {
|
||||
if (i2.has(t3)) return i2.get(t3);
|
||||
i2.set(t3, e2);
|
||||
}
|
||||
function e2() {
|
||||
return c(t3, arguments, o(this).constructor);
|
||||
}
|
||||
return e2.prototype = Object.create(t3.prototype, { constructor: { value: e2, enumerable: false, writable: true, configurable: true } }), a(e2, t3);
|
||||
}, d(t2);
|
||||
}
|
||||
function l(t2) {
|
||||
if (void 0 === t2) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
||||
return t2;
|
||||
}
|
||||
function f(t2, i2) {
|
||||
(null == i2 || i2 > t2.length) && (i2 = t2.length);
|
||||
for (var e2 = 0, n2 = new Array(i2); e2 < i2; e2++) n2[e2] = t2[e2];
|
||||
return n2;
|
||||
}
|
||||
var v = 0;
|
||||
function b(t2) {
|
||||
return "__private_" + v++ + "_" + t2;
|
||||
}
|
||||
function w(t2, i2) {
|
||||
if (!Object.prototype.hasOwnProperty.call(t2, i2)) throw new TypeError("attempted to use private field on non-instance");
|
||||
return t2;
|
||||
}
|
||||
var m = {};
|
||||
m.CustomEvent = "function" == typeof CustomEvent ? CustomEvent : (function(t2) {
|
||||
return i2[t2] = new i2("").constructor[t2], i2;
|
||||
function i2(t3, i3) {
|
||||
i3 || (i3 = {});
|
||||
var e2 = document.createEvent("CustomEvent");
|
||||
return e2.initCustomEvent(t3, !!i3.bubbles, !!i3.cancelable, i3.detail), e2;
|
||||
}
|
||||
})("prototype");
|
||||
var p, y, O = m.CustomEvent, j = b("mediaElement"), g = b("currentTime"), A = b("enabled"), k = (function() {
|
||||
function t2(t3) {
|
||||
Object.defineProperty(this, j, { writable: true, value: void 0 }), Object.defineProperty(this, g, { writable: true, value: void 0 }), Object.defineProperty(this, A, { writable: true, value: void 0 }), this.seeking = void 0, w(this, j)[j] = t3, w(this, g)[g] = 0, w(this, A)[A] = false, this.seeking = false, this.t = this.t.bind(this), this.i = this.i.bind(this), this.h = this.h.bind(this), this.enable();
|
||||
}
|
||||
var i2 = t2.prototype;
|
||||
return i2.i = function() {
|
||||
this.seeking = true;
|
||||
}, i2.h = function() {
|
||||
this.seeking = false;
|
||||
}, i2.t = function() {
|
||||
var t3, i3;
|
||||
this.seeking || null != (t3 = w(this, j)[j]) && t3.paused || (w(this, g)[g] = null == (i3 = w(this, j)[j]) ? void 0 : i3.currentTime);
|
||||
}, i2.enable = function() {
|
||||
var t3, i3, e2;
|
||||
w(this, A)[A] || (null == (t3 = w(this, j)[j]) || t3.addEventListener("seeking", this.i), null == (i3 = w(this, j)[j]) || i3.addEventListener("seeked", this.h), null == (e2 = w(this, j)[j]) || e2.addEventListener("timeupdate", this.t), w(this, A)[A] = true);
|
||||
}, i2.disable = function() {
|
||||
var t3, i3, e2;
|
||||
w(this, A)[A] && (null == (t3 = w(this, j)[j]) || t3.removeEventListener("seeking", this.i), null == (i3 = w(this, j)[j]) || i3.removeEventListener("seeked", this.h), null == (e2 = w(this, j)[j]) || e2.removeEventListener("timeupdate", this.t), w(this, A)[A] = false);
|
||||
}, i2.play = function() {
|
||||
var t3 = this;
|
||||
return new Promise(function(i3) {
|
||||
var e2;
|
||||
i3(null == (e2 = w(t3, j)[j]) ? void 0 : e2.play());
|
||||
});
|
||||
}, i2.pause = function() {
|
||||
var t3;
|
||||
null == (t3 = w(this, j)[j]) || t3.pause();
|
||||
}, i2.reset = function() {
|
||||
w(this, g)[g] = 0, w(this, A)[A] = false, this.seeking = false, this.enable();
|
||||
}, i2.destroy = function() {
|
||||
w(this, g)[g] = 0, w(this, A)[A] = false, this.seeking = false, this.disable(), w(this, j)[j] = void 0;
|
||||
}, s(t2, [{ key: "enabled", get: function() {
|
||||
return w(this, A)[A];
|
||||
} }, { key: "currentTime", get: function() {
|
||||
return w(this, g)[g];
|
||||
} }, { key: "duration", get: function() {
|
||||
var t3;
|
||||
return null == (t3 = w(this, j)[j]) ? void 0 : t3.duration;
|
||||
} }, { key: "muted", get: function() {
|
||||
var t3;
|
||||
return null == (t3 = w(this, j)[j]) ? void 0 : t3.muted;
|
||||
} }, { key: "volume", get: function() {
|
||||
var t3;
|
||||
return null == (t3 = w(this, j)[j]) ? void 0 : t3.volume;
|
||||
} }]), t2;
|
||||
})(), C = (function() {
|
||||
function t2() {
|
||||
this.delegate = document.createDocumentFragment();
|
||||
}
|
||||
var i2 = t2.prototype;
|
||||
return i2.addEventListener = function() {
|
||||
this.delegate.addEventListener.apply(this.delegate, [].slice.call(arguments));
|
||||
}, i2.dispatchEvent = function() {
|
||||
return this.delegate.dispatchEvent.apply(this.delegate, [].slice.call(arguments));
|
||||
}, i2.removeEventListener = function() {
|
||||
return this.delegate.removeEventListener.apply(this.delegate, [].slice.call(arguments));
|
||||
}, t2;
|
||||
})(), P = ["abort", "canplay", "canplaythrough", "durationchange", "emptied", "ended", "error", "loadeddata", "loadedmetadata", "loadstart", "pause", "play", "playing", "progress", "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", "volumechange", "waiting"];
|
||||
!(function(t2) {
|
||||
t2.MEDIA_START = "MediaStart", t2.MEDIA_IMPRESSION = "MediaImpression", t2.MEDIA_STOP = "MediaStop", t2.MEDIA_CUE_POINTS_CHANGE = "MediaCuePointsChange", t2.MEDIA_RESUMED = "MediaResumed";
|
||||
})(p || (p = {})), (function(t2) {
|
||||
t2.AD_ERROR = "AdError", t2.AD_BUFFERING = "AdBuffering", t2.LOADED = "AdLoaded", t2.IMPRESSION = "AdImpression", t2.STARTED = "AdStarted", t2.FIRST_QUARTILE = "AdFirstQuartile", t2.MIDPOINT = "AdMidpoint", t2.THIRD_QUARTILE = "AdThirdQuartile", t2.AD_PROGRESS = "AdProgress", t2.COMPLETE = "AdComplete", t2.CLICK = "AdClick", t2.PAUSED = "AdPaused", t2.RESUMED = "AdResumed", t2.SKIPPED = "AdSkipped", t2.SKIPPABLE_STATE_CHANGED = "AdSkippableStateChanged", t2.VOLUME_CHANGED = "AdVolumeChanged", t2.VOLUME_MUTED = "AdMuted", t2.AD_METADATA = "AdMetadata", t2.AD_BREAK_READY = "AdBreakReady", t2.CONTENT_PAUSE_REQUESTED = "AdContentPauseRequested", t2.CONTENT_RESUME_REQUESTED = "AdContentResumeRequested", t2.ALL_ADS_COMPLETED = "AdAllAdsCompleted", t2.DURATION_CHANGE = "AdDurationChange", t2.INTERACTION = "AdInteraction", t2.LINEAR_CHANGED = "AdLinearChanged", t2.LOG = "AdLog", t2.USER_CLOSE = "AdUserClose", t2.AD_CAN_PLAY = "AdCanPlay", t2.EXPANDED_CHANGED = "AdExpandedChanged", t2.VIEWABLE_IMPRESSION = "AdViewableImpression";
|
||||
})(y || (y = {}));
|
||||
var R = h({}, y, p), E = function() {
|
||||
this.disableCustomPlaybackForIOS10Plus = false, this.autoResize = true, this.clickTrackingElement = void 0;
|
||||
}, T = (function(t2) {
|
||||
function i2() {
|
||||
var i3;
|
||||
return (i3 = t2.call.apply(t2, [this].concat([].slice.call(arguments))) || this).errorCode = void 0, i3.innerError = void 0, i3.type = void 0, i3.vastErrorCode = void 0, i3;
|
||||
}
|
||||
return r(i2, t2), i2;
|
||||
})(d(Error));
|
||||
T.ERROR_CODE_ADS_MANAGER_LOADED_TIMEOUT = 9e3, T.ERROR_CODE_REQUEST_ADS_TIMEOUT = 9001;
|
||||
var S = b("mediaElement"), M = b("adElement"), x = b("customPlayhead"), I = b("adsRenderingSettings"), B = b("ima"), L = b("adDisplayContainer"), D = b("adsManager"), W = b("width"), _ = b("height"), q = b("adsLoader"), F = b("playerOptions"), N = b("resizeObserver"), V = b("currentAd"), G = b("loadedAd"), Q = b("mediaStartTriggered"), U = b("mediaImpressionTriggered"), z = b("mediaInActivation"), Z = b("customPlaybackTimeAdjustedOnEnded"), $ = b("cuePoints"), H = b("adCurrentTime"), J = b("adDuration"), K = b("startAdCallback"), X = b("adsManagerLoadedTimeout"), Y = b("requestAdsTimeout"), tt = b("wasExternallyPaused"), it = b("lastNonZeroAdVolume"), et = b("activatePromise"), nt = (function(t2) {
|
||||
function i2(i3, e3, n2, s2, h2) {
|
||||
var r2;
|
||||
void 0 === s2 && (s2 = new i3.AdsRenderingSettings()), void 0 === h2 && (h2 = new E()), r2 = t2.call(this) || this, Object.defineProperty(l(r2), S, { writable: true, value: void 0 }), Object.defineProperty(l(r2), M, { writable: true, value: void 0 }), Object.defineProperty(l(r2), x, { writable: true, value: void 0 }), Object.defineProperty(l(r2), I, { writable: true, value: void 0 }), Object.defineProperty(l(r2), B, { writable: true, value: void 0 }), Object.defineProperty(l(r2), L, { writable: true, value: void 0 }), Object.defineProperty(l(r2), D, { writable: true, value: void 0 }), Object.defineProperty(l(r2), W, { writable: true, value: void 0 }), Object.defineProperty(l(r2), _, { writable: true, value: void 0 }), Object.defineProperty(l(r2), q, { writable: true, value: void 0 }), Object.defineProperty(l(r2), F, { writable: true, value: void 0 }), Object.defineProperty(l(r2), N, { writable: true, value: void 0 }), Object.defineProperty(l(r2), V, { writable: true, value: void 0 }), Object.defineProperty(l(r2), G, { writable: true, value: void 0 }), Object.defineProperty(l(r2), Q, { writable: true, value: false }), Object.defineProperty(l(r2), U, { writable: true, value: false }), Object.defineProperty(l(r2), z, { writable: true, value: false }), Object.defineProperty(l(r2), Z, { writable: true, value: false }), Object.defineProperty(l(r2), $, { writable: true, value: [] }), Object.defineProperty(l(r2), H, { writable: true, value: void 0 }), Object.defineProperty(l(r2), J, { writable: true, value: void 0 }), Object.defineProperty(l(r2), K, { writable: true, value: void 0 }), Object.defineProperty(l(r2), X, { writable: true, value: void 0 }), Object.defineProperty(l(r2), Y, { writable: true, value: void 0 }), Object.defineProperty(l(r2), tt, { writable: true, value: false }), Object.defineProperty(l(r2), it, { writable: true, value: 1 }), Object.defineProperty(l(r2), et, { writable: true, value: Promise.resolve() }), w(l(r2), S)[S] = e3, w(l(r2), M)[M] = n2, w(l(r2), B)[B] = i3, w(l(r2), F)[F] = h2, w(l(r2), I)[I] = s2, w(l(r2), I)[I].restoreCustomPlaybackStateOnAdBreakComplete = true, h2.disableCustomPlaybackForIOS10Plus && !w(l(r2), S)[S].hasAttribute("playsinline") && w(l(r2), S)[S].setAttribute("playsinline", ""), w(l(r2), B)[B].settings.setDisableCustomPlaybackForIOS10Plus(h2.disableCustomPlaybackForIOS10Plus), w(l(r2), x)[x] = new k(w(l(r2), S)[S]), r2.o = r2.o.bind(l(r2)), P.forEach(function(t3) {
|
||||
w(l(r2), S)[S].addEventListener(t3, r2.o);
|
||||
}), r2.u = r2.u.bind(l(r2)), r2.l = r2.l.bind(l(r2));
|
||||
var o2 = w(l(r2), S)[S], a2 = o2.offsetHeight, u2 = o2.offsetWidth;
|
||||
return w(l(r2), W)[W] = u2, w(l(r2), _)[_] = a2, h2.autoResize && window.ResizeObserver && (w(l(r2), N)[N] = new window.ResizeObserver(function(t3) {
|
||||
return r2.v(t3);
|
||||
}), w(l(r2), N)[N].observe(w(l(r2), S)[S])), r2;
|
||||
}
|
||||
r(i2, t2);
|
||||
var e2 = i2.prototype;
|
||||
return e2.activate = function() {
|
||||
var t3 = this;
|
||||
if (!w(this, Q)[Q] && !w(this, z)[z] && (w(this, z)[z] = true, w(this, S)[S].paused)) {
|
||||
var i3 = function() {
|
||||
return w(t3, S)[S].pause(), new Promise(function(i4) {
|
||||
setTimeout(function() {
|
||||
w(t3, z)[z] = false, i4();
|
||||
}, 1);
|
||||
});
|
||||
};
|
||||
w(this, et)[et] = new Promise(function(i4) {
|
||||
return i4(w(t3, S)[S].play());
|
||||
}).then(i3).catch(i3);
|
||||
}
|
||||
}, e2.playAds = function(t3) {
|
||||
this.m(t3);
|
||||
}, e2.loadAds = function(t3, i3) {
|
||||
this.m(t3, false, i3);
|
||||
}, e2.p = function() {
|
||||
var t3 = this;
|
||||
return w(this, et)[et].then(function() {
|
||||
return new Promise(function(i3) {
|
||||
return i3(w(t3, S)[S].play());
|
||||
});
|
||||
});
|
||||
}, e2.m = function(t3, i3, e3) {
|
||||
var n2 = this;
|
||||
void 0 === i3 && (i3 = true), this.reset(), this.O(), w(this, B)[B].settings.setAutoPlayAdBreaks(i3), w(this, S)[S].ended && (w(this, x)[x].reset(), w(this, S)[S].currentTime = 0), w(this, K)[K] = e3, t3.linearAdSlotWidth = w(this, W)[W], t3.linearAdSlotHeight = w(this, _)[_], t3.nonLinearAdSlotWidth = w(this, W)[W], t3.nonLinearAdSlotHeight = w(this, _)[_], null == t3.contentDuration && (t3.contentDuration = -3), w(this, X)[X] = window.setTimeout(function() {
|
||||
var t4 = new T("No adsManagerLoadedEvent within 5000ms.");
|
||||
t4.errorCode = T.ERROR_CODE_ADS_MANAGER_LOADED_TIMEOUT, n2.j(t4);
|
||||
}, 5e3), w(this, Y)[Y] = window.setTimeout(function() {
|
||||
var t4 = new T("No response for ads-request within 10000ms.");
|
||||
t4.errorCode = T.ERROR_CODE_REQUEST_ADS_TIMEOUT, n2.j(t4);
|
||||
}, 1e4), w(this, q)[q].requestAds(t3);
|
||||
}, e2.O = function() {
|
||||
w(this, L)[L] = new (w(this, B))[B].AdDisplayContainer(w(this, M)[M], w(this, F)[F].disableCustomPlaybackForIOS10Plus ? void 0 : w(this, S)[S], w(this, F)[F].clickTrackingElement), w(this, q)[q] = new (w(this, B))[B].AdsLoader(w(this, L)[L]), w(this, q)[q].addEventListener(w(this, B)[B].AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, this.u, false), w(this, q)[q].addEventListener(w(this, B)[B].AdErrorEvent.Type.AD_ERROR, this.l, false);
|
||||
}, e2.skipAd = function() {
|
||||
w(this, D)[D] && w(this, D)[D].skip();
|
||||
}, e2.discardAdBreak = function() {
|
||||
w(this, D)[D] && w(this, D)[D].discardAdBreak();
|
||||
}, e2.play = function() {
|
||||
var t3 = this;
|
||||
w(this, tt)[tt] = false, !w(this, x)[x].enabled && w(this, D)[D] ? w(this, D)[D].resume() : w(this, X)[X] || this.p().then(function() {
|
||||
}).catch(function() {
|
||||
t3.dispatchEvent(new O("pause"));
|
||||
});
|
||||
}, e2.pause = function() {
|
||||
w(this, tt)[tt] = true, !w(this, x)[x].enabled && w(this, D)[D] ? w(this, D)[D].pause() : w(this, S)[S].pause();
|
||||
}, e2.g = function(t3) {
|
||||
w(this, $)[$] = [].concat(t3), this.dispatchEvent(new O(R.MEDIA_CUE_POINTS_CHANGE, { detail: { cuePoints: [].concat(w(this, $)[$]) } }));
|
||||
}, e2.A = function(t3) {
|
||||
var i3 = this.cuePoints.indexOf(t3);
|
||||
i3 > -1 && (w(this, $)[$].splice(i3, 1), this.g(w(this, $)[$]));
|
||||
}, e2.resizeAd = function(t3, i3) {
|
||||
w(this, W)[W] = t3, w(this, _)[_] = i3, w(this, D)[D] && w(this, D)[D].resize(t3, i3, this.k()), w(this, M)[M].style.width = t3 + "px", w(this, M)[M].style.height = i3 + "px";
|
||||
}, e2.reset = function(t3) {
|
||||
void 0 === t3 && (t3 = false), t3 && (w(this, U)[U] = false, w(this, Q)[Q] = false, w(this, Z)[Z] = false), this.C(), w(this, $)[$] = [], w(this, tt)[tt] = false, w(this, K)[K] = void 0, w(this, D)[D] && w(this, D)[D].destroy(), w(this, D)[D] = void 0, w(this, B)[B].settings.setAutoPlayAdBreaks(true), t3 && this.P(), w(this, M)[M].style.display = "none";
|
||||
}, e2.P = function() {
|
||||
w(this, q)[q] && (w(this, q)[q].removeEventListener(w(this, B)[B].AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, this.u, false), w(this, q)[q].removeEventListener(w(this, B)[B].AdErrorEvent.Type.AD_ERROR, this.l, false), w(this, q)[q].destroy()), w(this, L)[L] && w(this, L)[L].destroy();
|
||||
}, e2.destroy = function() {
|
||||
var t3, i3, e3, n2, s2 = this;
|
||||
this.reset(true), w(this, x)[x].destroy(), P.forEach(function(t4) {
|
||||
w(s2, S)[S].removeEventListener(t4, s2.o);
|
||||
}), null == (t3 = w(this, q)[q]) || t3.removeEventListener(w(this, B)[B].AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, this.u, false), null == (i3 = w(this, L)[L]) || i3.destroy(), null == (e3 = w(this, q)[q]) || e3.destroy(), w(this, U)[U] = false, w(this, Z)[Z] = false, w(this, Q)[Q] = false, null == (n2 = w(this, N)[N]) || n2.disconnect();
|
||||
}, e2.isCustomPlaybackUsed = function() {
|
||||
return false === w(this, B)[B].settings.getDisableCustomPlaybackForIOS10Plus() && !w(this, M)[M].querySelector("video");
|
||||
}, e2.C = function() {
|
||||
window.clearTimeout(w(this, X)[X]), window.clearTimeout(w(this, Y)[Y]), w(this, X)[X] = void 0, w(this, Y)[Y] = void 0, w(this, V)[V] = void 0, w(this, H)[H] = void 0, w(this, J)[J] = void 0, w(this, M)[M].style.display = "none", w(this, M)[M].classList.remove("nonlinear"), w(this, D)[D] && this.R();
|
||||
}, e2.o = function(t3) {
|
||||
var i3 = this;
|
||||
if (w(this, x)[x].enabled || "volumechange" === t3.type) {
|
||||
if ("timeupdate" === t3.type) {
|
||||
if (w(this, D)[D]) {
|
||||
var e3 = w(this, D)[D].getCuePoints().filter(function(t4) {
|
||||
return t4 >= 0 && t4 < w(i3, x)[x].currentTime;
|
||||
}).pop();
|
||||
this.A(e3);
|
||||
}
|
||||
!w(this, U)[U] && w(this, Q)[Q] && w(this, S)[S].currentTime >= 0.5 && (this.dispatchEvent(new O(R.MEDIA_IMPRESSION)), w(this, U)[U] = true);
|
||||
}
|
||||
if ("play" !== t3.type || w(this, Q)[Q] || w(this, z)[z] || (this.dispatchEvent(new O(R.MEDIA_START)), w(this, Q)[Q] = true), "ended" === t3.type && (this.isCustomPlaybackUsed() && w(this, S)[S].currentTime === w(this, S)[S].duration && w(this, $)[$].indexOf(-1) > -1 && (w(this, S)[S].currentTime = w(this, S)[S].duration - 1e-5, w(this, Z)[Z] = true), w(this, q)[q].contentComplete(), w(this, D)[D] || this.T()), !window.ResizeObserver && w(this, F)[F].autoResize && "loadedmetadata" === t3.type) {
|
||||
var n2 = w(this, S)[S], s2 = n2.offsetHeight, h2 = n2.offsetWidth;
|
||||
w(this, W)[W] = h2, w(this, _)[_] = s2, this.R();
|
||||
}
|
||||
w(this, z)[z] && "volumechange" !== t3.type || this.dispatchEvent(new O(t3.type));
|
||||
}
|
||||
}, e2.S = function(t3) {
|
||||
var i3 = this, e3 = w(this, B)[B].AdEvent;
|
||||
switch (t3.type) {
|
||||
case e3.Type.LOADED:
|
||||
var n2 = t3.getAd();
|
||||
w(this, K)[K] && 0 === w(this, $)[$].length ? w(this, K)[K]({ ad: n2, start: function() {
|
||||
i3.M(), w(i3, K)[K] = void 0;
|
||||
}, startWithoutReset: function() {
|
||||
i3.M();
|
||||
} }) : w(this, G)[G] = n2;
|
||||
break;
|
||||
case e3.Type.AD_BREAK_READY:
|
||||
this.C(), w(this, K)[K] ? (w(this, K)[K]({ ad: w(this, G)[G], adBreakTime: t3.getAdData().adBreakTime, start: function() {
|
||||
i3.M(), w(i3, K)[K] = void 0;
|
||||
}, startWithoutReset: function() {
|
||||
i3.M();
|
||||
} }), w(this, G)[G] = void 0) : this.M();
|
||||
break;
|
||||
case e3.Type.STARTED:
|
||||
var s2 = w(this, V)[V] = t3.getAd();
|
||||
s2.getAdPodInfo().getAdPosition() > 1 && w(this, D)[D].setVolume(w(this, D)[D].getVolume()), w(this, M)[M].classList.remove("nonlinear"), this.R(), s2.isLinear() ? (w(this, x)[x].disable(), w(this, J)[J] = s2.getDuration(), w(this, H)[H] = 0) : (w(this, M)[M].classList.add("nonlinear"), this.I()), w(this, M)[M].style.display = "", w(this, tt)[tt] && (w(this, tt)[tt] = false, w(this, D)[D].pause());
|
||||
break;
|
||||
case e3.Type.ALL_ADS_COMPLETED:
|
||||
if (w(this, Z)[Z]) return;
|
||||
this.isCustomPlaybackUsed() && Boolean(w(this, V)[V]) && -1 !== w(this, V)[V].getAdPodInfo().getTimeOffset() && this.I(), this.reset();
|
||||
break;
|
||||
case e3.Type.CONTENT_PAUSE_REQUESTED:
|
||||
this.C(), w(this, V)[V] = t3.getAd(), w(this, M)[M].style.display = "", w(this, S)[S].pause(), this.R(), w(this, V)[V] && this.A(w(this, V)[V].getAdPodInfo().getTimeOffset()), w(this, D)[D].setVolume(w(this, S)[S].muted ? 0 : w(this, S)[S].volume), w(this, x)[x].disable(), w(this, J)[J] = w(this, V)[V].getDuration(), w(this, H)[H] = 0;
|
||||
break;
|
||||
case e3.Type.CONTENT_RESUME_REQUESTED:
|
||||
var h2 = Boolean(w(this, V)[V]);
|
||||
if (h2) {
|
||||
var r2 = w(this, D)[D].getVolume();
|
||||
w(this, S)[S].muted = 0 === r2, w(this, S)[S].volume = w(this, it)[it];
|
||||
}
|
||||
w(this, Z)[Z] && (w(this, S)[S].currentTime = w(this, S)[S].duration + 1, w(this, Z)[Z] = false), w(this, S)[S].ended ? (this.reset(), this.T()) : this.C(), h2 && (w(this, tt)[tt] = false, this.I());
|
||||
break;
|
||||
case e3.Type.AD_METADATA:
|
||||
this.g(w(this, D)[D].getCuePoints()), -1 === w(this, $)[$].indexOf(0) && (w(this, K)[K] ? w(this, K)[K]({ start: function() {
|
||||
i3.I(), w(i3, K)[K] = void 0;
|
||||
}, startWithoutReset: function() {
|
||||
i3.I();
|
||||
} }) : this.I());
|
||||
break;
|
||||
case e3.Type.AD_PROGRESS:
|
||||
var o2 = t3.getAdData();
|
||||
w(this, H)[H] = o2.currentTime, w(this, J)[J] = o2.duration;
|
||||
break;
|
||||
case e3.Type.LOG:
|
||||
var a2 = t3.getAdData();
|
||||
w(this, K)[K] ? w(this, K)[K]({ start: function() {
|
||||
i3.I(), w(i3, K)[K] = void 0;
|
||||
}, startWithoutReset: function() {
|
||||
i3.I();
|
||||
} }) : a2.adError && !w(this, V)[V] && this.I();
|
||||
break;
|
||||
case e3.Type.VOLUME_CHANGED:
|
||||
var u2 = w(this, D)[D].getVolume();
|
||||
u2 > 0 && (w(this, it)[it] = u2);
|
||||
}
|
||||
}, e2.l = function(t3) {
|
||||
this.j(this.B(t3));
|
||||
}, e2.u = function(t3) {
|
||||
var i3 = this, e3 = w(this, B)[B], n2 = e3.AdEvent, s2 = e3.AdErrorEvent.Type.AD_ERROR;
|
||||
window.clearTimeout(w(this, X)[X]);
|
||||
var h2 = w(this, D)[D] = t3.getAdsManager(w(this, x)[x], w(this, I)[I]);
|
||||
Object.keys(n2.Type).forEach(function(t4) {
|
||||
h2.addEventListener(n2.Type[t4], function(e4) {
|
||||
if (i3.S(e4), R[t4]) {
|
||||
var n3 = ["LOG", "AD_PROGRESS"].indexOf(t4) > -1;
|
||||
i3.dispatchEvent(new O(R[t4], { detail: { ad: e4.getAd() || w(i3, V)[V], adData: n3 ? e4.getAdData() : {} } }));
|
||||
}
|
||||
});
|
||||
}), h2.addEventListener(s2, function(t4) {
|
||||
return i3.j(i3.B(t4));
|
||||
});
|
||||
try {
|
||||
var r2;
|
||||
h2.init(w(this, W)[W], w(this, _)[_], this.k()), h2.setVolume(w(this, S)[S].muted ? 0 : w(this, S)[S].volume), null == (r2 = w(this, L)[L]) || r2.initialize(), w(this, K)[K] || this.M();
|
||||
} catch (t4) {
|
||||
this.j(new T(t4.message));
|
||||
}
|
||||
}, e2.M = function() {
|
||||
if (w(this, Q)[Q] || (this.dispatchEvent(new O(R.MEDIA_START)), w(this, Q)[Q] = true), w(this, D)[D]) try {
|
||||
w(this, D)[D].start();
|
||||
} catch (t3) {
|
||||
this.j(new T(t3.message));
|
||||
}
|
||||
else this.I();
|
||||
}, e2.T = function() {
|
||||
var t3 = this;
|
||||
setTimeout(function() {
|
||||
w(t3, U)[U] = false, w(t3, Q)[Q] = false, w(t3, Z)[Z] = false, t3.dispatchEvent(new O(R.MEDIA_STOP));
|
||||
}, 1);
|
||||
}, e2.v = function(t3) {
|
||||
for (var i3, e3 = (function(t4, i4) {
|
||||
var e4 = "undefined" != typeof Symbol && t4[Symbol.iterator] || t4["@@iterator"];
|
||||
if (e4) return (e4 = e4.call(t4)).next.bind(e4);
|
||||
if (Array.isArray(t4) || (e4 = (function(t5, i5) {
|
||||
if (t5) {
|
||||
if ("string" == typeof t5) return f(t5, i5);
|
||||
var e5 = Object.prototype.toString.call(t5).slice(8, -1);
|
||||
return "Object" === e5 && t5.constructor && (e5 = t5.constructor.name), "Map" === e5 || "Set" === e5 ? Array.from(t5) : "Arguments" === e5 || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e5) ? f(t5, i5) : void 0;
|
||||
}
|
||||
})(t4))) {
|
||||
e4 && (t4 = e4);
|
||||
var n3 = 0;
|
||||
return function() {
|
||||
return n3 >= t4.length ? { done: true } : { done: false, value: t4[n3++] };
|
||||
};
|
||||
}
|
||||
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
||||
})(t3); !(i3 = e3()).done; ) {
|
||||
var n2 = i3.value;
|
||||
n2.contentBoxSize && 1 === n2.contentBoxSize.length ? (w(this, W)[W] = n2.contentBoxSize[0].inlineSize, w(this, _)[_] = n2.contentBoxSize[0].blockSize) : n2.contentBoxSize && n2.contentBoxSize.inlineSize ? (w(this, W)[W] = n2.contentBoxSize.inlineSize, w(this, _)[_] = n2.contentBoxSize.blockSize) : (w(this, W)[W] = n2.contentRect.width, w(this, _)[_] = n2.contentRect.height);
|
||||
}
|
||||
this.R();
|
||||
}, e2.R = function() {
|
||||
if (w(this, F)[F].autoResize && w(this, D)[D]) {
|
||||
var t3 = w(this, V)[V], i3 = this.k();
|
||||
t3 && !t3.isLinear() ? t3 && (t3.getWidth() > w(this, W)[W] || t3.getHeight() > w(this, _)[_] ? this.resizeAd(w(this, W)[W], w(this, _)[_]) : (w(this, D)[D].resize(t3.getWidth(), t3.getHeight() + 20, i3), w(this, M)[M].style.width = t3.getWidth() + "px", w(this, M)[M].style.height = t3.getHeight() + 20 + "px")) : this.resizeAd(w(this, W)[W], w(this, _)[_]);
|
||||
}
|
||||
}, e2.k = function() {
|
||||
return document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement || w(this, S)[S].webkitDisplayingFullscreen ? w(this, B)[B].ViewMode.FULLSCREEN : w(this, B)[B].ViewMode.NORMAL;
|
||||
}, e2.I = function() {
|
||||
var t3 = this;
|
||||
w(this, M)[M].style.display = "none", w(this, S)[S].ended || (w(this, x)[x].enable(), w(this, tt)[tt] ? (w(this, S)[S].pause(), this.dispatchEvent(new O("pause"))) : (this.p().then(function() {
|
||||
}).catch(function() {
|
||||
t3.dispatchEvent(new O("pause"));
|
||||
}), this.dispatchEvent(new O("play")), this.dispatchEvent(new O(R.MEDIA_RESUMED))), w(this, tt)[tt] = false);
|
||||
}, e2.B = function(t3) {
|
||||
var i3 = t3.getError(), e3 = new T(i3.getMessage());
|
||||
return e3.type = i3.getType(), e3.errorCode = i3.getErrorCode(), e3.vastErrorCode = i3.getVastErrorCode && i3.getVastErrorCode(), e3.innerError = i3.getInnerError(), e3;
|
||||
}, e2.j = function(t3) {
|
||||
var i3 = this;
|
||||
this.dispatchEvent(new O(R.AD_ERROR, { detail: { error: t3 } })), this.C(), w(this, K)[K] ? w(this, K)[K]({ start: function() {
|
||||
i3.I(), w(i3, K)[K] = void 0;
|
||||
}, startWithoutReset: function() {
|
||||
i3.I();
|
||||
} }) : this.I();
|
||||
}, s(i2, [{ key: "volume", get: function() {
|
||||
return !w(this, x)[x].enabled && w(this, D)[D] ? w(this, D)[D].getVolume() : w(this, S)[S].volume;
|
||||
}, set: function(t3) {
|
||||
!w(this, x)[x].enabled && w(this, D)[D] && w(this, D)[D].setVolume(t3), w(this, S)[S].volume = t3;
|
||||
} }, { key: "muted", get: function() {
|
||||
return !w(this, x)[x].enabled && w(this, D)[D] ? 0 === w(this, D)[D].getVolume() : w(this, S)[S].muted;
|
||||
}, set: function(t3) {
|
||||
!w(this, x)[x].enabled && w(this, D)[D] && w(this, D)[D].setVolume(t3 ? 0 : w(this, it)[it]), w(this, S)[S].muted = t3;
|
||||
} }, { key: "currentTime", get: function() {
|
||||
return void 0 !== w(this, H)[H] ? w(this, H)[H] : w(this, S)[S].currentTime;
|
||||
}, set: function(t3) {
|
||||
w(this, x)[x].enabled && (w(this, S)[S].currentTime = t3);
|
||||
} }, { key: "duration", get: function() {
|
||||
return void 0 !== w(this, J)[J] ? w(this, J)[J] : w(this, S)[S].duration;
|
||||
} }, { key: "cuePoints", get: function() {
|
||||
return [].concat(w(this, $)[$]);
|
||||
} }]), i2;
|
||||
})(C);
|
||||
function artplayerPluginVast2(callback) {
|
||||
return async (art) => {
|
||||
const { template, constructor } = art;
|
||||
const { createElement, setStyles } = constructor.utils;
|
||||
const { $video, $player } = template;
|
||||
await e();
|
||||
const google = window.google;
|
||||
const ima = google.ima;
|
||||
const adsRenderingSettings = new ima.AdsRenderingSettings();
|
||||
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
|
||||
adsRenderingSettings.enablePreloading = true;
|
||||
const playerOptions = new E();
|
||||
let isAdPlaying = false;
|
||||
let imaPlayer = null;
|
||||
let $container = null;
|
||||
function createContainer() {
|
||||
const container = createElement("div");
|
||||
const id = `art-vast-${Date.now()}`;
|
||||
container.id = id;
|
||||
setStyles(container, {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "150",
|
||||
backgroundColor: "black",
|
||||
display: "none",
|
||||
pointerEvents: "auto"
|
||||
});
|
||||
return container;
|
||||
}
|
||||
function initPlayer() {
|
||||
if (imaPlayer)
|
||||
return imaPlayer;
|
||||
$container = createContainer();
|
||||
$player.appendChild($container);
|
||||
imaPlayer = new nt(ima, $video, $container, adsRenderingSettings, playerOptions);
|
||||
imaPlayer.addEventListener("AdContentPauseRequested", () => {
|
||||
isAdPlaying = true;
|
||||
$container.style.display = "block";
|
||||
});
|
||||
imaPlayer.addEventListener("AdContentResumeRequested", () => {
|
||||
isAdPlaying = false;
|
||||
$container.style.display = "none";
|
||||
});
|
||||
imaPlayer.addEventListener("AdStarted", () => {
|
||||
isAdPlaying = true;
|
||||
$container.style.display = "block";
|
||||
});
|
||||
imaPlayer.addEventListener("AdError", (event) => {
|
||||
console.error("VAST Ad Error:", event.detail);
|
||||
isAdPlaying = false;
|
||||
$container.style.display = "none";
|
||||
});
|
||||
return imaPlayer;
|
||||
}
|
||||
function destroyPlayer() {
|
||||
if (imaPlayer?.destroy)
|
||||
imaPlayer.destroy();
|
||||
if ($container?.parentNode)
|
||||
$container.parentNode.removeChild($container);
|
||||
$container = null;
|
||||
imaPlayer = null;
|
||||
}
|
||||
function playUrl(url, config = {}) {
|
||||
if (isAdPlaying)
|
||||
return;
|
||||
if (!imaPlayer)
|
||||
initPlayer();
|
||||
const request = new ima.AdsRequest();
|
||||
request.adTagUrl = url;
|
||||
for (const key in config) {
|
||||
request[key] = config[key];
|
||||
}
|
||||
imaPlayer.playAds(request);
|
||||
}
|
||||
function playRes(res, config = {}) {
|
||||
if (isAdPlaying)
|
||||
return;
|
||||
if (!imaPlayer)
|
||||
initPlayer();
|
||||
const request = new ima.AdsRequest();
|
||||
request.adsResponse = res;
|
||||
for (const key in config) {
|
||||
request[key] = config[key];
|
||||
}
|
||||
imaPlayer.playAds(request);
|
||||
}
|
||||
if (typeof callback === "function") {
|
||||
await callback({
|
||||
art,
|
||||
playUrl,
|
||||
playRes,
|
||||
init: initPlayer,
|
||||
ima,
|
||||
adsRenderingSettings,
|
||||
playerOptions,
|
||||
get imaPlayer() {
|
||||
return imaPlayer;
|
||||
},
|
||||
get container() {
|
||||
return $container;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
name: "artplayerPluginVast",
|
||||
destroy: destroyPlayer
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginVast2;
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
var artplayerPluginVttThumbnail = (function() {
|
||||
"use strict";
|
||||
function padEnd(str, targetLength, padString) {
|
||||
if (str.length > targetLength) {
|
||||
return String(str);
|
||||
} else {
|
||||
targetLength = targetLength - str.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length);
|
||||
}
|
||||
return String(str) + padString.slice(0, targetLength);
|
||||
}
|
||||
}
|
||||
function t2d(time) {
|
||||
const arr = time.split(".");
|
||||
const left = arr[0].split(":") || [];
|
||||
const right = padEnd(arr[1] || "0", 3, "0");
|
||||
const ms = Number(right) / 1e3;
|
||||
const h = Number(left[left.length - 3] || 0) * 3600;
|
||||
const m = Number(left[left.length - 2] || 0) * 60;
|
||||
const s = Number(left[left.length - 1] || 0);
|
||||
return h + m + s + ms;
|
||||
}
|
||||
async function getVttArray(vttUrl = "") {
|
||||
const vttString = await (await fetch(vttUrl)).text();
|
||||
const lines = vttString.split(/[\n\r]/g).filter((item) => item.trim());
|
||||
const vttArray = [];
|
||||
for (let i = 1; i < lines.length; i += 2) {
|
||||
const time = lines[i];
|
||||
const text = lines[i + 1];
|
||||
if (!text.trim())
|
||||
continue;
|
||||
const timeReg = /((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/;
|
||||
const timeMatch = time.match(timeReg);
|
||||
const textReg = /(.*)#(\w{4})=(.*)/;
|
||||
const textMatch = text.match(textReg);
|
||||
const start = Math.floor(t2d(timeMatch[1]));
|
||||
const end = Math.floor(t2d(timeMatch[2]));
|
||||
let url = textMatch[1];
|
||||
const isAbsoluteUrl = /^\/|(?:https?|ftp|file):\/\//i.test(url);
|
||||
if (!isAbsoluteUrl) {
|
||||
const urlArr = vttUrl.split("/");
|
||||
urlArr.pop();
|
||||
urlArr.push(url);
|
||||
url = urlArr.join("/");
|
||||
}
|
||||
const result = { start, end, url };
|
||||
const keys = textMatch[2].split("");
|
||||
const values = textMatch[3].split(",");
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
result[keys[j]] = values[j];
|
||||
}
|
||||
vttArray.push(result);
|
||||
}
|
||||
return vttArray;
|
||||
}
|
||||
function artplayerPluginVttThumbnail2(option) {
|
||||
return async (art) => {
|
||||
const {
|
||||
constructor: {
|
||||
utils: { setStyle, isMobile, addClass }
|
||||
},
|
||||
template: { $progress }
|
||||
} = art;
|
||||
let timer = null;
|
||||
const thumbnails = await getVttArray(option.vtt);
|
||||
function showThumbnails($control, find, width) {
|
||||
setStyle($control, "backgroundImage", `url(${find.url})`);
|
||||
setStyle($control, "height", `${find.h}px`);
|
||||
setStyle($control, "width", `${find.w}px`);
|
||||
setStyle($control, "backgroundPosition", `-${find.x}px -${find.y}px`);
|
||||
if (width <= find.w / 2) {
|
||||
setStyle($control, "left", 0);
|
||||
} else if (width > $progress.clientWidth - find.w / 2) {
|
||||
setStyle($control, "left", `${$progress.clientWidth - find.w}px`);
|
||||
} else {
|
||||
setStyle($control, "left", `${width - find.w / 2}px`);
|
||||
}
|
||||
}
|
||||
art.controls.add({
|
||||
name: "vtt-thumbnail",
|
||||
position: "top",
|
||||
index: 20,
|
||||
style: option.style || {},
|
||||
mounted($control) {
|
||||
addClass($control, "art-control-thumbnails");
|
||||
art.on("setBar", async (type, percentage, event) => {
|
||||
const isMobileDroging = type === "played" && event && isMobile;
|
||||
if (type === "hover" || isMobileDroging) {
|
||||
const width = $progress.clientWidth * percentage;
|
||||
const second = percentage * art.duration;
|
||||
setStyle($control, "display", "flex");
|
||||
const find = thumbnails.find((item) => second >= item.start && second <= item.end);
|
||||
if (!find)
|
||||
return setStyle($control, "display", "none");
|
||||
if (width > 0 && width < $progress.clientWidth) {
|
||||
showThumbnails($control, find, width);
|
||||
} else {
|
||||
if (!isMobile) {
|
||||
setStyle($control, "display", "none");
|
||||
}
|
||||
}
|
||||
if (isMobileDroging) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
setStyle($control, "display", "none");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
name: "artplayerPluginVttThumbnail"
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginVttThumbnail2;
|
||||
})();
|
||||
@@ -0,0 +1,719 @@
|
||||
var artplayerPluginWebsr = (function() {
|
||||
"use strict";var __defProp = Object.defineProperty;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||||
|
||||
const _Upscaler = class _Upscaler {
|
||||
static isSupported() {
|
||||
try {
|
||||
const hasWorker = typeof Worker !== "undefined";
|
||||
const hasOffscreen = typeof OffscreenCanvas !== "undefined" || typeof document !== "undefined" && !!document.createElement("canvas").transferControlToOffscreen;
|
||||
const hasBlob = typeof Blob !== "undefined";
|
||||
return hasWorker && hasOffscreen && hasBlob;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static isVideoSupported() {
|
||||
try {
|
||||
return typeof VideoEncoder !== "undefined" && typeof VideoDecoder !== "undefined";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
constructor(options = {}) {
|
||||
const weightsBaseUrl = options.weightsBaseUrl || "/weights";
|
||||
this.networks = options.networks || {
|
||||
small: {
|
||||
name: "anime4k/cnn-2x-l",
|
||||
weightsUrl: `${weightsBaseUrl}/cnn-8.json`
|
||||
},
|
||||
medium: {
|
||||
name: "anime4k/cnn-2x-16",
|
||||
weightsUrl: `${weightsBaseUrl}/cnn-16.json`
|
||||
},
|
||||
large: {
|
||||
name: "anime4k/cnn-2x-28",
|
||||
weightsUrl: `${weightsBaseUrl}/cnn-28.json`
|
||||
}
|
||||
};
|
||||
this.weightsBaseUrl = weightsBaseUrl;
|
||||
this.networkSize = options.networkSize || "medium";
|
||||
this.workerUrl = options.workerUrl || "/worker/main.js";
|
||||
this.timeouts = {
|
||||
..._Upscaler.DEFAULT_TIMEOUTS,
|
||||
...options.timeouts || {}
|
||||
};
|
||||
this.delays = { ..._Upscaler.DEFAULT_DELAYS, ...options.delays || {} };
|
||||
this.imageScale = typeof options.imageScale === "number" && options.imageScale > 0 ? options.imageScale : 2;
|
||||
this.videoScale = typeof options.videoScale === "number" && options.videoScale > 0 ? options.videoScale : 2;
|
||||
this.weightsCache = /* @__PURE__ */ new Map();
|
||||
this.workerInstance = null;
|
||||
this.messageHandlers = {};
|
||||
this.progressCallback = null;
|
||||
this.processingType = null;
|
||||
this.realtimeLoopId = null;
|
||||
this.realtimeState = null;
|
||||
this.init();
|
||||
}
|
||||
init({ prewarm = true } = {}) {
|
||||
if (!_Upscaler.isSupported()) {
|
||||
throw new Error("Upscaler is not supported in this environment");
|
||||
}
|
||||
if (prewarm) {
|
||||
this.getWorker().postMessage({ cmd: "isSupported" });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
getWorker() {
|
||||
if (!this.workerInstance) {
|
||||
this.workerInstance = new Worker(this.workerUrl);
|
||||
this.workerInstance.onmessage = (event) => this.handleWorkerMessage(event);
|
||||
}
|
||||
return this.workerInstance;
|
||||
}
|
||||
static extractProgressValue(data) {
|
||||
const value = data.data ?? data.progress ?? data.value ?? data.percentage;
|
||||
return typeof value === "number" ? Math.min(100, Math.max(0, Math.round(value))) : null;
|
||||
}
|
||||
handleBlobResponse(data) {
|
||||
const blobType = this.processingType === "video" ? "videoBlob" : "imageBlob";
|
||||
const handler = this.messageHandlers[blobType];
|
||||
if (data.data instanceof Blob && handler) {
|
||||
handler({ [blobType]: data.data });
|
||||
}
|
||||
}
|
||||
requestBlob() {
|
||||
const cmd = this.processingType === "video" ? "getVideoBlob" : "getImageBlob";
|
||||
this.getWorker().postMessage({ cmd });
|
||||
}
|
||||
handleWorkerMessage(event) {
|
||||
const { data } = event;
|
||||
if (!data.cmd)
|
||||
return;
|
||||
const { cmd } = data;
|
||||
if (cmd === "progress") {
|
||||
const progress = _Upscaler.extractProgressValue(data);
|
||||
if (progress !== null && this.progressCallback) {
|
||||
this.progressCallback(progress);
|
||||
}
|
||||
if (this.messageHandlers.progress)
|
||||
this.messageHandlers.progress(data);
|
||||
} else if (cmd === "finished") {
|
||||
if (data.data instanceof Blob) {
|
||||
this.handleBlobResponse(data);
|
||||
} else {
|
||||
this.requestBlob();
|
||||
}
|
||||
} else if (this.messageHandlers[cmd]) {
|
||||
this.messageHandlers[cmd](data);
|
||||
}
|
||||
}
|
||||
async loadWeights(networkSize) {
|
||||
this.validateNetworkSize(networkSize);
|
||||
if (this.weightsCache.has(networkSize)) {
|
||||
return this.weightsCache.get(networkSize);
|
||||
}
|
||||
const network = this.networks[networkSize];
|
||||
const response = await fetch(network.weightsUrl);
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch weights: ${response.statusText}`);
|
||||
const weights = await response.json();
|
||||
this.weightsCache.set(networkSize, weights);
|
||||
return weights;
|
||||
}
|
||||
static loadImageMetadata(arrayBuffer, mimeType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([arrayBuffer], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
resolve({ width: img.width, height: img.height });
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error("Failed to load image"));
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
static loadVideoMetadata(file, timeoutMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const video = document.createElement("video");
|
||||
video.src = url;
|
||||
const timeout = setTimeout(() => reject(new Error("Timeout")), timeoutMs);
|
||||
video.onloadedmetadata = () => {
|
||||
clearTimeout(timeout);
|
||||
URL.revokeObjectURL(url);
|
||||
resolve({
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
duration: video.duration
|
||||
});
|
||||
};
|
||||
video.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error("Failed to load video"));
|
||||
};
|
||||
});
|
||||
}
|
||||
createOffscreenCanvas(width, height, scale = 2) {
|
||||
const factor = typeof scale === "number" && scale > 0 ? scale : 1;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width * factor;
|
||||
canvas.height = height * factor;
|
||||
if (typeof canvas.transferControlToOffscreen !== "function") {
|
||||
throw new TypeError("OffscreenCanvas is not supported in this environment");
|
||||
}
|
||||
return canvas.transferControlToOffscreen();
|
||||
}
|
||||
createBlobPromise(blobType, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (this.messageHandlers[blobType]) {
|
||||
delete this.messageHandlers[blobType];
|
||||
reject(new Error(`${blobType} processing timeout`));
|
||||
}
|
||||
}, timeout);
|
||||
this.messageHandlers[blobType] = (msg) => {
|
||||
delete this.messageHandlers[blobType];
|
||||
clearTimeout(timeoutId);
|
||||
const blob = msg[blobType] || msg.data;
|
||||
if (blob instanceof Blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error(`Invalid ${blobType} format`));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
validateNetworkSize(size) {
|
||||
if (!this.networks[size]) {
|
||||
throw new Error(
|
||||
`Invalid networkSize: use one of ${Object.keys(this.networks).join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
async upscaleImage(imageFile, networkSize, scaleOverride) {
|
||||
if (!imageFile)
|
||||
throw new Error("Image file is required");
|
||||
const size = networkSize || this.networkSize;
|
||||
this.validateNetworkSize(size);
|
||||
this.processingType = "image";
|
||||
const arrayBuffer = await imageFile.arrayBuffer();
|
||||
const mimeType = imageFile.type || "image/jpeg";
|
||||
const { width, height } = await _Upscaler.loadImageMetadata(
|
||||
arrayBuffer,
|
||||
mimeType
|
||||
);
|
||||
const weights = await this.loadWeights(size);
|
||||
const network = this.networks[size];
|
||||
const scale = typeof scaleOverride === "number" && scaleOverride > 0 ? scaleOverride : this.imageScale;
|
||||
const [canvasUp, canvasOrig] = [
|
||||
this.createOffscreenCanvas(width, height, scale),
|
||||
this.createOffscreenCanvas(width, height, 1)
|
||||
];
|
||||
const blobPromise = this.createBlobPromise(
|
||||
"imageBlob",
|
||||
this.timeouts.IMAGE
|
||||
);
|
||||
this.getWorker().postMessage(
|
||||
{
|
||||
cmd: "init",
|
||||
data: {
|
||||
imageArrayBuffer: arrayBuffer,
|
||||
imageMimeType: mimeType,
|
||||
upscaled: canvasUp,
|
||||
original: canvasOrig,
|
||||
resolution: { width, height },
|
||||
network_name: network.name,
|
||||
weights
|
||||
}
|
||||
},
|
||||
[canvasUp, canvasOrig]
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.getWorker().postMessage({
|
||||
cmd: "network",
|
||||
data: {
|
||||
name: network.name,
|
||||
imageArrayBuffer: arrayBuffer,
|
||||
imageMimeType: mimeType,
|
||||
weights
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.getWorker().postMessage({
|
||||
cmd: "getImageBlob",
|
||||
data: { imageArrayBuffer: arrayBuffer, imageMimeType: mimeType }
|
||||
});
|
||||
}, this.delays.NETWORK);
|
||||
}, this.delays.INIT);
|
||||
return blobPromise;
|
||||
}
|
||||
async upscaleVideo(videoFile, networkSize, scaleOverride) {
|
||||
if (!videoFile)
|
||||
throw new Error("Video file is required");
|
||||
const size = networkSize || this.networkSize;
|
||||
this.validateNetworkSize(size);
|
||||
if (!_Upscaler.isVideoSupported()) {
|
||||
throw new Error("WebCodecs API not supported. Requires Chrome 94+");
|
||||
}
|
||||
this.processingType = "video";
|
||||
const { width, height, duration } = await _Upscaler.loadVideoMetadata(
|
||||
videoFile,
|
||||
this.timeouts.METADATA
|
||||
);
|
||||
const weights = await this.loadWeights(size);
|
||||
const network = this.networks[size];
|
||||
const scale = typeof scaleOverride === "number" && scaleOverride > 0 ? scaleOverride : this.videoScale;
|
||||
const [canvasOut, canvasIn] = [
|
||||
this.createOffscreenCanvas(width, height, scale),
|
||||
this.createOffscreenCanvas(width, height, 1)
|
||||
];
|
||||
const blobPromise = this.createBlobPromise(
|
||||
"videoBlob",
|
||||
this.timeouts.VIDEO
|
||||
);
|
||||
this.getWorker().postMessage(
|
||||
{
|
||||
cmd: "process",
|
||||
file: videoFile,
|
||||
fileSize: videoFile.size,
|
||||
duration,
|
||||
adjustedResolution: {
|
||||
adjustedInputWidth: width,
|
||||
adjustedInputHeight: height,
|
||||
adjustedOutputWidth: width * scale,
|
||||
adjustedOutputHeight: height * scale
|
||||
},
|
||||
upscaled: canvasOut,
|
||||
original: canvasIn,
|
||||
weights,
|
||||
network_name: network.name,
|
||||
skipDemuxProgress: true
|
||||
},
|
||||
[canvasOut, canvasIn]
|
||||
);
|
||||
return blobPromise;
|
||||
}
|
||||
async startRealtimeUpscale(videoElement, canvasElement, networkSize, scaleOverride) {
|
||||
if (!videoElement || !canvasElement) {
|
||||
throw new Error("videoElement and canvasElement are required");
|
||||
}
|
||||
const size = networkSize || this.networkSize;
|
||||
this.validateNetworkSize(size);
|
||||
const network = this.networks[size];
|
||||
const weights = await this.loadWeights(size);
|
||||
const scale = typeof scaleOverride === "number" && scaleOverride > 0 ? scaleOverride : this.videoScale;
|
||||
const ensureMetadata = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (videoElement.readyState >= 1) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let onLoaded, onError;
|
||||
const cleanup = () => {
|
||||
videoElement.removeEventListener("loadedmetadata", onLoaded);
|
||||
videoElement.removeEventListener("error", onError);
|
||||
};
|
||||
onLoaded = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
onError = () => {
|
||||
cleanup();
|
||||
reject(new Error("Failed to load video metadata"));
|
||||
};
|
||||
videoElement.addEventListener("loadedmetadata", onLoaded);
|
||||
videoElement.addEventListener("error", onError);
|
||||
});
|
||||
};
|
||||
await ensureMetadata();
|
||||
const width = videoElement.videoWidth;
|
||||
const height = videoElement.videoHeight;
|
||||
if (!width || !height) {
|
||||
throw new Error("Invalid video dimensions");
|
||||
}
|
||||
const firstInit = !canvasElement._upscalerTransferred;
|
||||
let offscreen = null;
|
||||
if (firstInit) {
|
||||
canvasElement.width = width * scale;
|
||||
canvasElement.height = height * scale;
|
||||
if (typeof canvasElement.transferControlToOffscreen !== "function") {
|
||||
throw new TypeError("OffscreenCanvas is not supported in this environment");
|
||||
}
|
||||
offscreen = canvasElement.transferControlToOffscreen();
|
||||
canvasElement._upscalerTransferred = true;
|
||||
}
|
||||
const captureCanvas = document.createElement("canvas");
|
||||
captureCanvas.width = width;
|
||||
captureCanvas.height = height;
|
||||
const captureCtx = captureCanvas.getContext("2d");
|
||||
if (this.realtimeLoopId) {
|
||||
cancelAnimationFrame(this.realtimeLoopId);
|
||||
this.realtimeLoopId = null;
|
||||
}
|
||||
const initPayload = {
|
||||
cmd: "realtimeInit",
|
||||
data: {
|
||||
upscaled: firstInit ? offscreen : null,
|
||||
resolution: {
|
||||
width,
|
||||
height,
|
||||
scale,
|
||||
outputWidth: width * scale,
|
||||
outputHeight: height * scale
|
||||
},
|
||||
network_name: network.name,
|
||||
weights
|
||||
}
|
||||
};
|
||||
if (firstInit && offscreen) {
|
||||
this.getWorker().postMessage(initPayload, [offscreen]);
|
||||
} else {
|
||||
this.getWorker().postMessage(initPayload);
|
||||
}
|
||||
this.realtimeState = {
|
||||
running: true,
|
||||
video: videoElement,
|
||||
canvas: canvasElement,
|
||||
scale,
|
||||
busy: false,
|
||||
frameIndex: 0,
|
||||
captureCanvas,
|
||||
captureCtx
|
||||
};
|
||||
const loop = async () => {
|
||||
if (!this.realtimeState || !this.realtimeState.running)
|
||||
return;
|
||||
const state = this.realtimeState;
|
||||
const v = state.video;
|
||||
const haveCurrentData = v.HAVE_CURRENT_DATA || 2;
|
||||
if (!v.paused && !v.ended && !state.busy && v.readyState >= haveCurrentData && v.videoWidth > 0 && v.videoHeight > 0) {
|
||||
state.busy = true;
|
||||
try {
|
||||
const captureCanvas2 = state.captureCanvas;
|
||||
const captureCtx2 = state.captureCtx;
|
||||
if (!captureCanvas2 || !captureCtx2) {
|
||||
throw new Error("Missing capture canvas");
|
||||
}
|
||||
captureCtx2.drawImage(v, 0, 0, captureCanvas2.width, captureCanvas2.height);
|
||||
const frame = await createImageBitmap(captureCanvas2);
|
||||
state.frameIndex += 1;
|
||||
this.getWorker().postMessage(
|
||||
{
|
||||
cmd: "realtimeFrame",
|
||||
frame
|
||||
},
|
||||
[frame]
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("realtimeFrame error", e);
|
||||
} finally {
|
||||
if (this.realtimeState) {
|
||||
this.realtimeState.busy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.realtimeLoopId = requestAnimationFrame(loop);
|
||||
};
|
||||
this.realtimeLoopId = requestAnimationFrame(loop);
|
||||
}
|
||||
stopRealtimeUpscale() {
|
||||
if (this.realtimeLoopId) {
|
||||
cancelAnimationFrame(this.realtimeLoopId);
|
||||
this.realtimeLoopId = null;
|
||||
}
|
||||
if (this.realtimeState) {
|
||||
this.realtimeState.running = false;
|
||||
this.realtimeState = null;
|
||||
}
|
||||
}
|
||||
async downloadUpscaled(file, networkSize, filename = null) {
|
||||
const isVideo = file.type.startsWith("video");
|
||||
const size = networkSize || this.networkSize;
|
||||
const blob = isVideo ? await this.upscaleVideo(file, size) : await this.upscaleImage(file, size);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename || `upscaled_${Date.now()}.${isVideo ? "mp4" : "png"}`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return blob;
|
||||
}
|
||||
getSupportedNetworks() {
|
||||
return Object.keys(this.networks);
|
||||
}
|
||||
static async getGPUCapability() {
|
||||
try {
|
||||
if (navigator.gpu) {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (adapter) {
|
||||
const buffer = adapter.limits.maxStorageBufferBindingSize / 512;
|
||||
const texture = 8192 * 8192;
|
||||
return Math.min(buffer, texture);
|
||||
}
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
const gl = canvas.getContext("webgl2");
|
||||
if (gl)
|
||||
return gl.getParameter(gl.MAX_TEXTURE_SIZE) ** 2;
|
||||
return 8388608;
|
||||
} catch {
|
||||
return 8388608;
|
||||
}
|
||||
}
|
||||
static getBackendType() {
|
||||
try {
|
||||
if (navigator.gpu)
|
||||
return "webgpu";
|
||||
if (document.createElement("canvas").getContext("webgl2"))
|
||||
return "webgl";
|
||||
return "unknown";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
getAppState() {
|
||||
return {
|
||||
backend: _Upscaler.getBackendType(),
|
||||
isProcessing: false,
|
||||
progress: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
onProgress(callback) {
|
||||
if (typeof callback === "function") {
|
||||
this.progressCallback = callback;
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
this.stopRealtimeUpscale();
|
||||
if (this.workerInstance) {
|
||||
this.workerInstance.terminate();
|
||||
this.workerInstance = null;
|
||||
}
|
||||
this.messageHandlers = {};
|
||||
this.progressCallback = null;
|
||||
this.processingType = null;
|
||||
this.weightsCache.clear();
|
||||
}
|
||||
};
|
||||
__publicField(_Upscaler, "DEFAULT_TIMEOUTS", { IMAGE: 3e5, VIDEO: 36e5, METADATA: 1e4 });
|
||||
__publicField(_Upscaler, "DEFAULT_DELAYS", { INIT: 500, NETWORK: 300 });
|
||||
let Upscaler = _Upscaler;
|
||||
function artplayerPluginWebsr2(option = {
|
||||
networkSize: "medium",
|
||||
compare: false,
|
||||
weightsBaseUrl: "/weights",
|
||||
workerUrl: "/worker/main.js",
|
||||
videoScale: 2
|
||||
}) {
|
||||
return (art) => {
|
||||
const { $video, $player } = art.template;
|
||||
const $canvas = document.createElement("canvas");
|
||||
$player.appendChild($canvas);
|
||||
$canvas.style.position = "absolute";
|
||||
$canvas.style.zIndex = "11";
|
||||
$canvas.style.pointerEvents = "none";
|
||||
$canvas.style.top = "50%";
|
||||
$canvas.style.left = "50%";
|
||||
$canvas.style.transform = "translate(-50%, -50%)";
|
||||
$canvas.style.opacity = "0";
|
||||
$canvas.style.transition = "opacity 0.2s ease";
|
||||
function calcCanvasSize() {
|
||||
const videoElement = art.video;
|
||||
const containerWidth = $player.offsetWidth || 640;
|
||||
const containerHeight = $player.offsetHeight || 360;
|
||||
const videoWidth = videoElement.videoWidth || 640;
|
||||
const videoHeight = videoElement.videoHeight || 360;
|
||||
const aspectRatio = videoWidth / videoHeight;
|
||||
let displayWidth2 = containerWidth;
|
||||
let displayHeight2 = containerHeight;
|
||||
if (containerWidth / containerHeight > aspectRatio) {
|
||||
displayWidth2 = containerHeight * aspectRatio;
|
||||
} else {
|
||||
displayHeight2 = containerWidth / aspectRatio;
|
||||
}
|
||||
return { displayWidth: displayWidth2, displayHeight: displayHeight2 };
|
||||
}
|
||||
if (!Upscaler || !Upscaler.isSupported || !Upscaler.isSupported()) {
|
||||
console.warn("Upscaler is not supported in this environment");
|
||||
return;
|
||||
}
|
||||
const upscaler = new Upscaler({
|
||||
networkSize: option.networkSize || "medium",
|
||||
weightsBaseUrl: option.weightsBaseUrl || "/weights",
|
||||
workerUrl: option.workerUrl || "/worker/main.js",
|
||||
videoScale: option.videoScale || 2
|
||||
});
|
||||
let realtimeStarted = false;
|
||||
async function startRealtime() {
|
||||
if (realtimeStarted)
|
||||
return;
|
||||
realtimeStarted = true;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
if ($video.readyState >= 1) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let onLoaded, onError;
|
||||
const cleanup = () => {
|
||||
$video.removeEventListener("loadedmetadata", onLoaded);
|
||||
$video.removeEventListener("error", onError);
|
||||
};
|
||||
onLoaded = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
onError = () => {
|
||||
cleanup();
|
||||
reject(new Error("Failed to load video metadata"));
|
||||
};
|
||||
$video.addEventListener("loadedmetadata", onLoaded);
|
||||
$video.addEventListener("error", onError);
|
||||
});
|
||||
await upscaler.startRealtimeUpscale(
|
||||
$video,
|
||||
$canvas,
|
||||
option.networkSize || upscaler.networkSize
|
||||
);
|
||||
requestAnimationFrame(() => {
|
||||
$canvas.style.opacity = "1";
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"artplayer-plugin-upscaler: failed to start realtime",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
const { displayWidth, displayHeight } = calcCanvasSize();
|
||||
$canvas.style.width = `${displayWidth}px`;
|
||||
$canvas.style.height = `${displayHeight}px`;
|
||||
let comparePosition = 50;
|
||||
let isDragging = false;
|
||||
let compareEnabled = !!option.compare;
|
||||
const $handler = document.createElement("div");
|
||||
$handler.style.position = "absolute";
|
||||
$handler.style.width = "3px";
|
||||
$handler.style.backgroundColor = "rgba(255, 255, 255, 0.75)";
|
||||
$handler.style.cursor = "ew-resize";
|
||||
$handler.style.zIndex = "12";
|
||||
$handler.style.pointerEvents = "auto";
|
||||
$handler.style.display = compareEnabled ? "block" : "none";
|
||||
$handler.style.boxShadow = "0 0 2px rgba(0, 0, 0, 0.1)";
|
||||
if (compareEnabled) {
|
||||
$player.appendChild($handler);
|
||||
$handler.addEventListener("mousedown", handleMouseDown);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
updateCompareMask();
|
||||
}
|
||||
function updateCompareMask() {
|
||||
if (compareEnabled) {
|
||||
const { displayWidth: displayWidth2, displayHeight: displayHeight2 } = calcCanvasSize();
|
||||
const gradient = `linear-gradient(to right, transparent 0%, transparent ${comparePosition}%, black ${comparePosition}%, black 100%)`;
|
||||
$canvas.style.maskImage = gradient;
|
||||
const containerWidth = $player.offsetWidth || 640;
|
||||
const containerHeight = $player.offsetHeight || 360;
|
||||
const canvasLeft = (containerWidth - displayWidth2) / 2;
|
||||
const canvasTop = (containerHeight - displayHeight2) / 2;
|
||||
const handlerX = canvasLeft + displayWidth2 * comparePosition / 100;
|
||||
$handler.style.top = `${canvasTop}px`;
|
||||
$handler.style.left = `${handlerX}px`;
|
||||
$handler.style.height = `${displayHeight2}px`;
|
||||
$handler.style.transform = "translateX(-50%)";
|
||||
} else {
|
||||
$canvas.style.maskImage = "none";
|
||||
}
|
||||
}
|
||||
function handleMouseDown(_e) {
|
||||
if (compareEnabled) {
|
||||
isDragging = true;
|
||||
}
|
||||
}
|
||||
function handleMouseMove(e) {
|
||||
if (compareEnabled && isDragging) {
|
||||
const rect = $player.getBoundingClientRect();
|
||||
const { displayWidth: displayWidth2 } = calcCanvasSize();
|
||||
const containerWidth = $player.offsetWidth || 640;
|
||||
const canvasLeft = (containerWidth - displayWidth2) / 2;
|
||||
const x = e.clientX - rect.left;
|
||||
const relativeX = x - canvasLeft;
|
||||
comparePosition = relativeX / displayWidth2 * 100;
|
||||
comparePosition = Math.max(0, Math.min(100, comparePosition));
|
||||
updateCompareMask();
|
||||
}
|
||||
}
|
||||
function handleMouseUp(_e) {
|
||||
isDragging = false;
|
||||
}
|
||||
art.on("destroy", () => {
|
||||
if (upscaler && typeof upscaler.dispose === "function") {
|
||||
upscaler.dispose();
|
||||
}
|
||||
$handler.removeEventListener("mousedown", handleMouseDown);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
});
|
||||
art.on("resize", () => {
|
||||
const { displayWidth: displayWidth2, displayHeight: displayHeight2 } = calcCanvasSize();
|
||||
$canvas.style.width = `${displayWidth2}px`;
|
||||
$canvas.style.height = `${displayHeight2}px`;
|
||||
updateCompareMask();
|
||||
});
|
||||
art.on("play", () => {
|
||||
startRealtime();
|
||||
});
|
||||
function update(newOption = {}) {
|
||||
if (!newOption || typeof newOption !== "object")
|
||||
return;
|
||||
if (Object.prototype.hasOwnProperty.call(newOption, "compare")) {
|
||||
const nextCompare = !!newOption.compare;
|
||||
option.compare = nextCompare;
|
||||
compareEnabled = nextCompare;
|
||||
if (compareEnabled) {
|
||||
if (!$handler.parentNode) {
|
||||
$player.appendChild($handler);
|
||||
}
|
||||
$handler.style.display = "block";
|
||||
$handler.addEventListener("mousedown", handleMouseDown);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
updateCompareMask();
|
||||
} else {
|
||||
$canvas.style.maskImage = "none";
|
||||
$handler.style.display = "none";
|
||||
$handler.removeEventListener("mousedown", handleMouseDown);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
}
|
||||
if (newOption.networkSize) {
|
||||
const nextSize = newOption.networkSize;
|
||||
option.networkSize = nextSize;
|
||||
if (upscaler && typeof upscaler.stopRealtimeUpscale === "function") {
|
||||
upscaler.stopRealtimeUpscale();
|
||||
}
|
||||
realtimeStarted = false;
|
||||
if (!art.paused) {
|
||||
startRealtime();
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: "artplayerPluginWebsr",
|
||||
upscaler,
|
||||
update
|
||||
};
|
||||
};
|
||||
}
|
||||
return artplayerPluginWebsr2;
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
var artplayerProxyCanvas = (function() {
|
||||
"use strict";
|
||||
function artplayerProxyCanvas2(callback) {
|
||||
return (art) => {
|
||||
const { constructor } = art;
|
||||
const { createElement } = constructor.utils;
|
||||
const canvas = createElement("canvas");
|
||||
const video = createElement("video");
|
||||
const ctx = canvas.getContext("2d");
|
||||
let animationId = null;
|
||||
const originalCanvasMethods = {};
|
||||
for (const prop in canvas) {
|
||||
if (typeof canvas[prop] === "function") {
|
||||
originalCanvasMethods[prop] = canvas[prop].bind(canvas);
|
||||
}
|
||||
}
|
||||
for (const prop in video) {
|
||||
if (!(prop in canvas)) {
|
||||
Object.defineProperty(canvas, prop, {
|
||||
get() {
|
||||
const value = video[prop];
|
||||
return typeof value === "function" ? value.bind(video) : value;
|
||||
},
|
||||
set(value) {
|
||||
video[prop] = value;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const prop in originalCanvasMethods) {
|
||||
canvas[prop] = function(...args) {
|
||||
if (prop in originalCanvasMethods) {
|
||||
return originalCanvasMethods[prop](...args);
|
||||
}
|
||||
return video[prop](...args);
|
||||
};
|
||||
}
|
||||
setupEventListeners();
|
||||
setupArtPlayerEvents();
|
||||
return canvas;
|
||||
function setupEventListeners() {
|
||||
const { events } = constructor.config;
|
||||
setTimeout(() => {
|
||||
events.forEach((event) => {
|
||||
art.proxy(video, event, (event2) => {
|
||||
art.emit(`video:${event2.type}`, event2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
async function draw() {
|
||||
try {
|
||||
if (typeof createImageBitmap !== "undefined") {
|
||||
const bitmap = await createImageBitmap(video);
|
||||
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
bitmap.close();
|
||||
} else {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
callback && callback(ctx, video);
|
||||
art.emit("artplayerProxyCanvas:draw", ctx, video);
|
||||
} catch (error) {
|
||||
art.emit("artplayerProxyCanvas:error", error);
|
||||
}
|
||||
}
|
||||
async function animation() {
|
||||
await draw();
|
||||
animationId = requestAnimationFrame(animation);
|
||||
}
|
||||
function resize() {
|
||||
const player = art.template?.$player;
|
||||
if (!player || art.option.autoSize)
|
||||
return;
|
||||
const aspectRatio = video.videoWidth / video.videoHeight;
|
||||
const containerWidth = player.clientWidth;
|
||||
const containerHeight = player.clientHeight;
|
||||
const containerRatio = containerWidth / containerHeight;
|
||||
let canvasWidth, canvasHeight;
|
||||
if (containerRatio > aspectRatio) {
|
||||
canvasHeight = containerHeight;
|
||||
canvasWidth = canvasHeight * aspectRatio;
|
||||
} else {
|
||||
canvasWidth = containerWidth;
|
||||
canvasHeight = canvasWidth / aspectRatio;
|
||||
}
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
const paddingLeft = (containerWidth - canvasWidth) / 2;
|
||||
const paddingTop = (containerHeight - canvasHeight) / 2;
|
||||
Object.assign(canvas.style, {
|
||||
padding: `${paddingTop}px ${paddingLeft}px`
|
||||
});
|
||||
}
|
||||
function setupArtPlayerEvents() {
|
||||
art.on("video:loadedmetadata", () => {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
});
|
||||
art.on("video:play", () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
animation();
|
||||
});
|
||||
art.on("video:pause", () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
});
|
||||
art.on("resize", () => {
|
||||
resize();
|
||||
draw();
|
||||
});
|
||||
art.on("destroy", () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
return artplayerProxyCanvas2;
|
||||
})();
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,132 @@
|
||||
var ArtplayerToolIframe = (function() {
|
||||
"use strict";
|
||||
class ArtplayerToolIframe2 {
|
||||
static get iframe() {
|
||||
return window.top !== window;
|
||||
}
|
||||
static postMessage({ type, data, id = 0 }) {
|
||||
if (!ArtplayerToolIframe2.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');
|
||||
}
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
static async onMessage(event) {
|
||||
if (!ArtplayerToolIframe2.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');
|
||||
}
|
||||
const { type, data, id } = event.data;
|
||||
switch (type) {
|
||||
case "commit":
|
||||
try {
|
||||
if (data.match(/\bresolve\((.*?)\)/)) {
|
||||
const string = `return new Promise(function(resolve){
|
||||
${data}
|
||||
})`;
|
||||
const result = await new Function(string)();
|
||||
ArtplayerToolIframe2.postMessage({ type: "response", data: result, id });
|
||||
} else {
|
||||
const result = new Function(data)();
|
||||
ArtplayerToolIframe2.postMessage({ type: "response", data: result, id });
|
||||
}
|
||||
} catch (error) {
|
||||
ArtplayerToolIframe2.postMessage({ type: "error", data: error.message, id });
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
static inject() {
|
||||
if (!ArtplayerToolIframe2.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');
|
||||
}
|
||||
ArtplayerToolIframe2.postMessage({ type: "inject" });
|
||||
window.addEventListener("message", ArtplayerToolIframe2.onMessage);
|
||||
}
|
||||
constructor({ iframe, url }) {
|
||||
if (iframe instanceof HTMLIFrameElement === false) {
|
||||
throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');
|
||||
}
|
||||
if (typeof url !== "string") {
|
||||
throw new TypeError('"option.url" needs to be a string');
|
||||
}
|
||||
this.url = url;
|
||||
this.$iframe = iframe;
|
||||
this.promises = {};
|
||||
this.injected = false;
|
||||
this.destroyed = false;
|
||||
this.messageCallback = () => null;
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
window.addEventListener("message", this.onMessage);
|
||||
this.$iframe.src = this.url;
|
||||
}
|
||||
onMessage(event) {
|
||||
const { type, data, id } = event.data;
|
||||
switch (type) {
|
||||
case "inject":
|
||||
this.injected = true;
|
||||
break;
|
||||
}
|
||||
if (this.promises[id]) {
|
||||
if (type === "error") {
|
||||
this.promises[id].reject(new Error(data));
|
||||
} else {
|
||||
this.promises[id].resove(data);
|
||||
}
|
||||
delete this.promises[id];
|
||||
}
|
||||
if (this.messageCallback) {
|
||||
this.messageCallback({ type, data });
|
||||
}
|
||||
}
|
||||
postMessage({ type, data }) {
|
||||
return new Promise((resove, reject) => {
|
||||
(function loop() {
|
||||
if (this.destroyed) {
|
||||
reject(new Error("The instance has been destroyed"));
|
||||
} else {
|
||||
if (this.injected) {
|
||||
const id = Date.now();
|
||||
this.promises[id] = { resove, reject };
|
||||
this.$iframe.contentWindow.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id
|
||||
},
|
||||
"*"
|
||||
);
|
||||
} else {
|
||||
setTimeout(loop.bind(this), 200);
|
||||
}
|
||||
}
|
||||
}).call(this);
|
||||
});
|
||||
}
|
||||
commit(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError('"commit.callback" needs to be a function');
|
||||
}
|
||||
const callbackString = callback.toString();
|
||||
const bodyString = callbackString.substring(callbackString.indexOf("{") + 1, callbackString.lastIndexOf("}"));
|
||||
return this.postMessage({ type: "commit", data: bodyString });
|
||||
}
|
||||
message(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError('"message.callback" needs to be a function');
|
||||
}
|
||||
this.messageCallback = callback;
|
||||
}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
window.removeEventListener("message", this.onMessage);
|
||||
}
|
||||
}
|
||||
return ArtplayerToolIframe2;
|
||||
})();
|
||||
@@ -0,0 +1,259 @@
|
||||
var ArtplayerToolThumbnail = (function() {
|
||||
"use strict";
|
||||
class Emitter {
|
||||
on(name, fn, ctx) {
|
||||
const e = this.e || (this.e = {});
|
||||
(e[name] || (e[name] = [])).push({ fn, ctx });
|
||||
return this;
|
||||
}
|
||||
once(name, fn, ctx) {
|
||||
const self = this;
|
||||
function listener(...args) {
|
||||
self.off(name, listener);
|
||||
fn.apply(ctx, args);
|
||||
}
|
||||
listener._ = fn;
|
||||
return this.on(name, listener, ctx);
|
||||
}
|
||||
emit(name, ...data) {
|
||||
const evtArr = ((this.e || (this.e = {}))[name] || []).slice();
|
||||
for (let i = 0; i < evtArr.length; i += 1) {
|
||||
evtArr[i].fn.apply(evtArr[i].ctx, data);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
off(name, callback) {
|
||||
const e = this.e || (this.e = {});
|
||||
const evts = e[name];
|
||||
const liveEvents = [];
|
||||
if (evts && callback) {
|
||||
for (let i = 0, len = evts.length; i < len; i += 1) {
|
||||
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
|
||||
liveEvents.push(evts[i]);
|
||||
}
|
||||
}
|
||||
if (liveEvents.length) {
|
||||
e[name] = liveEvents;
|
||||
} else {
|
||||
delete e[name];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function runPromisesInSeries(ps) {
|
||||
return ps.reduce((p, next) => p.then(next), Promise.resolve());
|
||||
}
|
||||
function getFileName(name) {
|
||||
const nameArray = name.split(".");
|
||||
nameArray.pop();
|
||||
return nameArray.join(".");
|
||||
}
|
||||
function clamp(num, a, b) {
|
||||
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
}
|
||||
class ArtplayerToolThumbnail2 extends Emitter {
|
||||
constructor(option = {}) {
|
||||
super();
|
||||
this.processing = false;
|
||||
this.option = {};
|
||||
this.setup(Object.assign({}, ArtplayerToolThumbnail2.DEFAULTS, option));
|
||||
this.video = ArtplayerToolThumbnail2.creatVideo();
|
||||
this.duration = 0;
|
||||
this.inputChange = this.inputChange.bind(this);
|
||||
this.ondrop = this.ondrop.bind(this);
|
||||
this.option.fileInput.addEventListener("change", this.inputChange);
|
||||
this.option.fileInput.addEventListener("dragover", ArtplayerToolThumbnail2.ondragover);
|
||||
this.option.fileInput.addEventListener("drop", ArtplayerToolThumbnail2.ondrop);
|
||||
}
|
||||
static get DEFAULTS() {
|
||||
return {
|
||||
number: 60,
|
||||
width: 160,
|
||||
height: 90,
|
||||
column: 10,
|
||||
begin: 0,
|
||||
end: Number.NaN
|
||||
};
|
||||
}
|
||||
static ondragover(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
ondrop(event) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
this.loadVideo(file);
|
||||
}
|
||||
setup(option = {}) {
|
||||
this.option = Object.assign({}, this.option, option);
|
||||
const { fileInput, number, width, column } = this.option;
|
||||
this.errorHandle(fileInput instanceof Element, "The 'fileInput' is not a Element");
|
||||
if (!(fileInput.tagName === "INPUT" && fileInput.type === "file")) {
|
||||
fileInput.style.position = "relative";
|
||||
const newFileInput = document.createElement("input");
|
||||
newFileInput.type = "file";
|
||||
newFileInput.style.position = "absolute";
|
||||
newFileInput.style.width = "100%";
|
||||
newFileInput.style.height = "100%";
|
||||
newFileInput.style.left = "0";
|
||||
newFileInput.style.top = "0";
|
||||
newFileInput.style.right = "0";
|
||||
newFileInput.style.bottom = "0";
|
||||
newFileInput.style.opacity = "0";
|
||||
fileInput.appendChild(newFileInput);
|
||||
this.option.fileInput = newFileInput;
|
||||
}
|
||||
["number", "width", "column", "begin", "end"].forEach((item) => {
|
||||
this.errorHandle(typeof this.option[item] === "number", `The '${item}' is not a number`);
|
||||
});
|
||||
this.option.number = clamp(number, 10, 1e3);
|
||||
this.option.width = clamp(width, 10, 1e3);
|
||||
this.option.column = clamp(column, 1, 1e3);
|
||||
return this;
|
||||
}
|
||||
static creatVideo() {
|
||||
const video = document.createElement("video");
|
||||
video.style.position = "absolute";
|
||||
video.style.top = "-9999px";
|
||||
video.style.left = "-9999px";
|
||||
video.muted = true;
|
||||
video.controls = true;
|
||||
document.body.appendChild(video);
|
||||
return video;
|
||||
}
|
||||
inputChange(event) {
|
||||
const file = this.option.fileInput.files[0];
|
||||
this.loadVideo(file);
|
||||
event.target.value = "";
|
||||
}
|
||||
loadVideo(file) {
|
||||
if (file) {
|
||||
const canPlayType = this.video.canPlayType(file.type);
|
||||
this.errorHandle(
|
||||
canPlayType === "maybe" || canPlayType === "probably",
|
||||
`Playback of this file format is not supported: ${file.type}`
|
||||
);
|
||||
const videoUrl = URL.createObjectURL(file);
|
||||
this.videoUrl = videoUrl;
|
||||
this.file = file;
|
||||
this.emit("file", this.file);
|
||||
this.video.src = videoUrl;
|
||||
this.emit("video", this.video);
|
||||
}
|
||||
}
|
||||
start() {
|
||||
if (!this.video.duration)
|
||||
return sleep(1e3).then(() => this.start());
|
||||
const { width, number, begin, end } = this.option;
|
||||
const height = this.video.videoHeight / this.video.videoWidth * width;
|
||||
this.option.height = height;
|
||||
this.option.begin = clamp(begin, 0, this.video.duration);
|
||||
this.option.end = clamp(end || this.video.duration, begin, this.video.duration);
|
||||
this.errorHandle(this.option.end > this.option.begin, `End time must be greater than the start time`);
|
||||
this.duration = this.option.end - this.option.begin;
|
||||
this.density = number / this.duration;
|
||||
this.errorHandle(this.file && this.video, "Please select the video file first");
|
||||
this.errorHandle(!this.processing, "There is currently a task in progress, please wait a moment...");
|
||||
this.errorHandle(this.density <= 1, `The preview density cannot be greater than 1, but got ${this.density}`);
|
||||
const screenshotDate = this.creatScreenshotDate();
|
||||
const canvas = this.creatCanvas();
|
||||
const context2D = canvas.getContext("2d");
|
||||
this.emit("canvas", canvas);
|
||||
const promiseList = screenshotDate.map((item, index) => () => {
|
||||
return new Promise((resolve) => {
|
||||
this.video.oncanplay = () => {
|
||||
context2D.drawImage(this.video, item.x, item.y, width, height);
|
||||
canvas.toBlob((blob) => {
|
||||
if (this.thumbnailUrl) {
|
||||
URL.revokeObjectURL(this.thumbnailUrl);
|
||||
}
|
||||
this.thumbnailUrl = URL.createObjectURL(blob);
|
||||
this.emit("update", this.thumbnailUrl, (index + 1) / number);
|
||||
this.video.oncanplay = null;
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
this.video.currentTime = item.time;
|
||||
});
|
||||
});
|
||||
this.processing = true;
|
||||
return runPromisesInSeries(promiseList).then(() => {
|
||||
this.processing = false;
|
||||
this.emit("done");
|
||||
}).catch((err) => {
|
||||
this.processing = false;
|
||||
this.emit("error", err.message);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
creatScreenshotDate() {
|
||||
const { number, width, height, column, begin } = this.option;
|
||||
const timeGap = this.duration / number;
|
||||
const timePoints = [begin + timeGap];
|
||||
while (timePoints.length < number) {
|
||||
const last = timePoints[timePoints.length - 1];
|
||||
timePoints.push(last + timeGap);
|
||||
}
|
||||
return timePoints.map((item, index) => ({
|
||||
time: item - timeGap / 2,
|
||||
x: index % column * width,
|
||||
y: Math.floor(index / column) * height
|
||||
}));
|
||||
}
|
||||
creatCanvas() {
|
||||
const { number, width, height, column } = this.option;
|
||||
const canvas = document.createElement("canvas");
|
||||
const context2D = canvas.getContext("2d");
|
||||
canvas.width = width * column;
|
||||
canvas.height = Math.ceil(number / column) * height + 30;
|
||||
context2D.fillStyle = "black";
|
||||
context2D.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context2D.font = "14px Georgia";
|
||||
context2D.fillStyle = "#fff";
|
||||
context2D.fillText(
|
||||
`From: https://artplayer.org/, Number: ${number}, Width: ${width}, Height: ${height}, Column: ${column}`,
|
||||
10,
|
||||
canvas.height - 11
|
||||
);
|
||||
return canvas;
|
||||
}
|
||||
download() {
|
||||
this.errorHandle(
|
||||
this.file && this.thumbnailUrl,
|
||||
"Download does not seem to be ready, please create preview first"
|
||||
);
|
||||
this.errorHandle(!this.processing, "There is currently a task in progress, please wait a moment...");
|
||||
const elink = document.createElement("a");
|
||||
const name = `${getFileName(this.file.name)}.png`;
|
||||
elink.download = name;
|
||||
elink.href = this.thumbnailUrl;
|
||||
document.body.appendChild(elink);
|
||||
elink.click();
|
||||
document.body.removeChild(elink);
|
||||
this.emit("download", name);
|
||||
return this;
|
||||
}
|
||||
errorHandle(condition, msg) {
|
||||
if (!condition) {
|
||||
this.emit("error", msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
this.option.fileInput.removeEventListener("change", this.inputChange);
|
||||
this.option.fileInput.removeEventListener("dragover", ArtplayerToolThumbnail2.ondragover);
|
||||
this.option.fileInput.removeEventListener("drop", ArtplayerToolThumbnail2.ondrop);
|
||||
document.body.removeChild(this.video);
|
||||
if (this.videoUrl) {
|
||||
URL.revokeObjectURL(this.videoUrl);
|
||||
}
|
||||
if (this.thumbnailUrl) {
|
||||
URL.revokeObjectURL(this.thumbnailUrl);
|
||||
}
|
||||
this.emit("destroy");
|
||||
}
|
||||
}
|
||||
return ArtplayerToolThumbnail2;
|
||||
})();
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user