mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(ambilight): [PKG-AMBILIGHT-03] split typed lifecycle and recover native canvas sampling
This commit is contained in:
1 parent
43f0e9a8c6
commit
6caee44dbf
25 files changed
+1680
-306
No files matched your search
@@ -4,99 +4,299 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
function createColorSampler(canvas, video, createCanvas) {
|
||||
let context;
|
||||
try {
|
||||
context = canvas.getContext("2d");
|
||||
canvas.width = 3;
|
||||
canvas.height = 3;
|
||||
} catch (error) {
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
throw error;
|
||||
}
|
||||
let destroyed = false;
|
||||
let replaceCanvas = false;
|
||||
function read(active) {
|
||||
if (destroyed || !active())
|
||||
return null;
|
||||
const width = video.videoWidth;
|
||||
const height = video.videoHeight;
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
|
||||
return null;
|
||||
const w = width / 3;
|
||||
const h = height / 3;
|
||||
const colors = [];
|
||||
try {
|
||||
if (replaceCanvas) {
|
||||
const replacement = createCanvas();
|
||||
if (destroyed || !active()) {
|
||||
replacement.width = 0;
|
||||
replacement.height = 0;
|
||||
return null;
|
||||
}
|
||||
canvas = replacement;
|
||||
context = null;
|
||||
context = canvas.getContext("2d");
|
||||
if (destroyed || !active()) {
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
return null;
|
||||
}
|
||||
canvas.width = 3;
|
||||
canvas.height = 3;
|
||||
replaceCanvas = false;
|
||||
}
|
||||
if (!context)
|
||||
return null;
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let column = 0; column < 3; column++) {
|
||||
if (destroyed || !active())
|
||||
return null;
|
||||
context.drawImage(video, column * w, row * h, w, h, 0, 0, 1, 1);
|
||||
if (destroyed || !active())
|
||||
return null;
|
||||
const [r, g, b] = context.getImageData(0, 0, 1, 1).data;
|
||||
colors.push(`rgb(${r}, ${g}, ${b})`);
|
||||
}
|
||||
}
|
||||
return colors;
|
||||
} catch {
|
||||
if (!destroyed) {
|
||||
replaceCanvas = true;
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function destroy() {
|
||||
if (destroyed)
|
||||
return;
|
||||
destroyed = true;
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
}
|
||||
return { read, destroy };
|
||||
}
|
||||
function createFrameLoop(frequency, playing, update) {
|
||||
let state = "stopped";
|
||||
let pending = null;
|
||||
let generation = 0;
|
||||
let inFrame = false;
|
||||
let lastUpdateTime = 0;
|
||||
function schedule() {
|
||||
const current = generation;
|
||||
try {
|
||||
pending = requestAnimationFrame(() => {
|
||||
if (state !== "running" || current !== generation)
|
||||
return;
|
||||
pending = null;
|
||||
tick();
|
||||
});
|
||||
} catch (error) {
|
||||
state = "stopped";
|
||||
generation++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function tick() {
|
||||
const current = generation;
|
||||
const active = () => state === "running" && current === generation;
|
||||
inFrame = true;
|
||||
try {
|
||||
const now = performance.now();
|
||||
if (now - lastUpdateTime < 1e3 / frequency || !playing() || !active())
|
||||
return;
|
||||
lastUpdateTime = now;
|
||||
update(active);
|
||||
} finally {
|
||||
inFrame = false;
|
||||
if (state === "running" && pending === null)
|
||||
schedule();
|
||||
}
|
||||
}
|
||||
function start() {
|
||||
if (state !== "stopped")
|
||||
return;
|
||||
state = "running";
|
||||
generation++;
|
||||
if (!inFrame)
|
||||
tick();
|
||||
}
|
||||
function stop() {
|
||||
if (state === "destroyed")
|
||||
return;
|
||||
state = "stopped";
|
||||
generation++;
|
||||
const frame = pending;
|
||||
pending = null;
|
||||
if (frame !== null)
|
||||
cancelAnimationFrame(frame);
|
||||
}
|
||||
function destroy() {
|
||||
if (state === "destroyed")
|
||||
return;
|
||||
state = "destroyed";
|
||||
generation++;
|
||||
const frame = pending;
|
||||
pending = null;
|
||||
if (frame !== null)
|
||||
cancelAnimationFrame(frame);
|
||||
}
|
||||
return { start, stop, destroy };
|
||||
}
|
||||
function createAmbilightView(utils) {
|
||||
const element = utils.createElement("div");
|
||||
element.innerHTML = Array.from({ length: 9 }).fill("<div></div>").join("");
|
||||
const items = Array.from(element.children);
|
||||
function mount(video, option, active) {
|
||||
if (!active())
|
||||
return;
|
||||
utils.addClass(element, "artplayer-plugin-ambilight");
|
||||
if (!active())
|
||||
return;
|
||||
video.parentNode.insertBefore(element, video);
|
||||
if (!active())
|
||||
return;
|
||||
utils.setStyles(element, { position: "absolute", top: 0, left: 0, zIndex: 9, inset: 0, display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gridTemplateRows: "1fr 1fr 1fr" });
|
||||
for (const item of items) {
|
||||
if (!active())
|
||||
return;
|
||||
utils.setStyles(item, { opacity: option.opacity, filter: `blur(${option.blur})`, transition: `background-color ${option.duration}s ease` });
|
||||
}
|
||||
}
|
||||
function render(colors, active) {
|
||||
for (let index = 0; index < colors.length; index++) {
|
||||
if (!active())
|
||||
return;
|
||||
const item = items[index];
|
||||
const color = colors[index];
|
||||
if (item && color !== void 0)
|
||||
item.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
function destroy() {
|
||||
try {
|
||||
element.parentNode?.removeChild(element);
|
||||
} finally {
|
||||
items.length = 0;
|
||||
}
|
||||
}
|
||||
return { mount, render, destroy };
|
||||
}
|
||||
function artplayerPluginAmbilight(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 {
|
||||
let host = art;
|
||||
let closed = art.isDestroy;
|
||||
let view = null;
|
||||
let sampler = null;
|
||||
let loop = null;
|
||||
const subscriptions = [];
|
||||
const result = {
|
||||
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})`;
|
||||
start() {
|
||||
if (!closed)
|
||||
loop?.start();
|
||||
},
|
||||
stop() {
|
||||
if (!closed)
|
||||
loop?.stop();
|
||||
}
|
||||
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);
|
||||
};
|
||||
};
|
||||
if (closed) {
|
||||
host = null;
|
||||
return result;
|
||||
}
|
||||
function releaseResources() {
|
||||
const releases = [loop?.destroy, sampler?.destroy, view?.destroy];
|
||||
loop = null;
|
||||
sampler = null;
|
||||
view = null;
|
||||
const failures = [];
|
||||
for (const release of releases) {
|
||||
try {
|
||||
release?.();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
const current = host;
|
||||
host = null;
|
||||
const failures = [];
|
||||
for (const [event, callback] of subscriptions.splice(0)) {
|
||||
try {
|
||||
current?.off(event, callback);
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
releaseResources();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
function listen(event, callback) {
|
||||
subscriptions.push([event, callback]);
|
||||
host?.on(event, callback);
|
||||
}
|
||||
try {
|
||||
listen("destroy", destroy);
|
||||
if (closed)
|
||||
return result;
|
||||
const { $video } = art.template;
|
||||
const utils = art.constructor.utils;
|
||||
const { blur = "50px", opacity = 0.5, frequency = 10, duration = 0.3 } = option;
|
||||
if (closed)
|
||||
return result;
|
||||
view = createAmbilightView(utils);
|
||||
if (!closed)
|
||||
view.mount($video, { blur, opacity, duration }, () => !closed);
|
||||
if (closed) {
|
||||
releaseResources();
|
||||
return result;
|
||||
}
|
||||
const canvas = utils.createElement("canvas");
|
||||
if (closed) {
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
return result;
|
||||
}
|
||||
sampler = createColorSampler(canvas, $video, () => utils.createElement("canvas"));
|
||||
if (closed) {
|
||||
releaseResources();
|
||||
return result;
|
||||
}
|
||||
loop = createFrameLoop(frequency, () => Boolean(host?.playing), (valid) => {
|
||||
const colors = sampler?.read(valid);
|
||||
if (colors && valid())
|
||||
view?.render(colors, valid);
|
||||
});
|
||||
listen("ready", result.start);
|
||||
if (closed)
|
||||
releaseResources();
|
||||
} catch (error) {
|
||||
try {
|
||||
destroy();
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
releaseResources();
|
||||
} catch {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
export {
|
||||
|
||||
Reference in new issue
Block a user