1028 lines
36 KiB
JavaScript
1028 lines
36 KiB
JavaScript
(function () {
|
|
var DEFAULT_SVGA_SCRIPT =
|
|
'https://cdn.jsdelivr.net/npm/svgaplayerweb@2.3.2/build/svga.min.js';
|
|
var DEFAULT_PAG_BASE = 'https://cdn.jsdelivr.net/npm/libpag@4.5.68/lib/';
|
|
var scriptCache = {};
|
|
var pagInitCache = {};
|
|
var videoLayoutCache = {};
|
|
var svgaVideoItemCache = {};
|
|
var MIN_SPLIT_MATCH_FRAMES = 3;
|
|
var MIN_USEFUL_FRAME_BRIGHTNESS = 0.035;
|
|
var MIN_SPLIT_SATURATION_GAP = 0.08;
|
|
var MAX_SPLIT_MASK_SATURATION = 0.28;
|
|
var MAX_SPLIT_MASK_GRAY_ERROR = 0.06;
|
|
|
|
function extend(target, source) {
|
|
var output = target || {};
|
|
source = source || {};
|
|
Object.keys(source).forEach(function (key) {
|
|
if (source[key] !== undefined) output[key] = source[key];
|
|
});
|
|
return output;
|
|
}
|
|
|
|
function resolveElement(target) {
|
|
if (!target) return null;
|
|
if (typeof target === 'string') return document.querySelector(target);
|
|
return target;
|
|
}
|
|
|
|
function loadScriptOnce(url) {
|
|
if (scriptCache[url]) return scriptCache[url];
|
|
scriptCache[url] = new Promise(function (resolve, reject) {
|
|
var script = document.createElement('script');
|
|
script.src = url;
|
|
script.async = true;
|
|
script.onload = resolve;
|
|
script.onerror = function () {
|
|
reject(new Error('script_load_failed'));
|
|
};
|
|
document.head.appendChild(script);
|
|
});
|
|
return scriptCache[url];
|
|
}
|
|
|
|
function loadSVGAItemOnce(sourceURL, canvas, options) {
|
|
if (svgaVideoItemCache[sourceURL]) return svgaVideoItemCache[sourceURL];
|
|
svgaVideoItemCache[sourceURL] = loadScriptOnce(options.svgaScript)
|
|
.then(function () {
|
|
if (!window.SVGA) throw new Error('svga_unavailable');
|
|
return new Promise(function (resolve, reject) {
|
|
var parser = new window.SVGA.Parser(canvas);
|
|
parser.load(sourceURL, resolve, reject);
|
|
});
|
|
})
|
|
.catch(function (error) {
|
|
delete svgaVideoItemCache[sourceURL];
|
|
throw error;
|
|
});
|
|
return svgaVideoItemCache[sourceURL];
|
|
}
|
|
|
|
function waitForWindowLoad() {
|
|
if (document.readyState === 'complete') return Promise.resolve();
|
|
return new Promise(function (resolve) {
|
|
window.addEventListener('load', resolve, { once: true });
|
|
});
|
|
}
|
|
|
|
function extensionFromURL(url) {
|
|
var clean = String(url || '')
|
|
.split('?')[0]
|
|
.split('#')[0];
|
|
var match = clean.match(/\.([a-z0-9]+)$/i);
|
|
return match ? match[1].toLowerCase() : '';
|
|
}
|
|
|
|
function inferType(url, explicitType) {
|
|
var type = String(explicitType || '').toLowerCase();
|
|
if (type) return type;
|
|
var ext = extensionFromURL(url);
|
|
if (ext === 'mp4' || ext === 'webm' || ext === 'mov') return 'video';
|
|
if (ext === 'svga') return 'svga';
|
|
if (ext === 'pag') return 'pag';
|
|
return '';
|
|
}
|
|
|
|
function sameOrigin(url) {
|
|
try {
|
|
return (
|
|
new URL(url, window.location.href).origin ===
|
|
window.location.origin
|
|
);
|
|
} catch (_) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function proxyBinaryURL(url, type, options) {
|
|
if (options.proxy === false) return url;
|
|
if (type !== 'svga' && type !== 'pag') return url;
|
|
if (sameOrigin(url)) return url;
|
|
if (typeof options.proxyURL === 'function') {
|
|
return options.proxyURL(url, type) || url;
|
|
}
|
|
if (typeof options.proxyURL === 'string' && options.proxyURL) {
|
|
var base = options.proxyURL;
|
|
var separator = base.indexOf('?') >= 0 ? '&' : '?';
|
|
return base + separator + 'url=' + encodeURIComponent(url);
|
|
}
|
|
if (window.HyAppAPI && typeof window.HyAppAPI.buildURL === 'function') {
|
|
return window.HyAppAPI.buildURL('/api/v1/media/effects/proxy', {
|
|
url: url,
|
|
});
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function mediaSize(container, options) {
|
|
return {
|
|
width:
|
|
Number(options.width) ||
|
|
(container && container.clientWidth) ||
|
|
300,
|
|
height:
|
|
Number(options.height) ||
|
|
(container && container.clientHeight) ||
|
|
300,
|
|
};
|
|
}
|
|
|
|
function styleNode(node, options) {
|
|
node.style.display = 'block';
|
|
node.style.width = '100%';
|
|
node.style.height = '100%';
|
|
if (node.tagName === 'VIDEO' || node.tagName === 'CANVAS') {
|
|
node.style.objectFit = options.fit || 'contain';
|
|
}
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function median(values) {
|
|
if (!values.length) return 0;
|
|
var sorted = values.slice().sort(function (a, b) {
|
|
return a - b;
|
|
});
|
|
return sorted[Math.floor(sorted.length / 2)];
|
|
}
|
|
|
|
function fullFrameLayout(video, kind, hasNativeAlpha) {
|
|
var width = Math.max(1, video.videoWidth || 1);
|
|
var height = Math.max(1, video.videoHeight || 1);
|
|
var rect = { x: 0, y: 0, width: 1, height: 1 };
|
|
return {
|
|
kind: kind || 'normal',
|
|
hasAlphaMask: false,
|
|
hasNativeAlpha: !!hasNativeAlpha,
|
|
contentAspectRatio: width / height,
|
|
colorRect: rect,
|
|
alphaRect: rect,
|
|
};
|
|
}
|
|
|
|
function waitForMediaEvent(target, eventName, timeoutMS) {
|
|
return new Promise(function (resolve, reject) {
|
|
var timer = 0;
|
|
function cleanup() {
|
|
window.clearTimeout(timer);
|
|
target.removeEventListener(eventName, handleEvent);
|
|
target.removeEventListener('error', handleError);
|
|
}
|
|
function handleEvent() {
|
|
cleanup();
|
|
resolve();
|
|
}
|
|
function handleError() {
|
|
cleanup();
|
|
reject(new Error('video_' + eventName + '_failed'));
|
|
}
|
|
target.addEventListener(eventName, handleEvent, { once: true });
|
|
target.addEventListener('error', handleError, { once: true });
|
|
timer = window.setTimeout(function () {
|
|
cleanup();
|
|
reject(new Error('video_' + eventName + '_timeout'));
|
|
}, timeoutMS || 4000);
|
|
});
|
|
}
|
|
|
|
function loadVideoMetadata(video) {
|
|
if (video.readyState >= 1 && video.videoWidth && video.videoHeight) {
|
|
return Promise.resolve();
|
|
}
|
|
return waitForMediaEvent(video, 'loadedmetadata', 8000);
|
|
}
|
|
|
|
function seekVideo(video, time) {
|
|
var duration = Number(video.duration || 0);
|
|
var targetTime = duration
|
|
? clamp(time, 0, Math.max(0, duration - 0.05))
|
|
: Math.max(0, time);
|
|
if (Math.abs(video.currentTime - targetTime) < 0.04) {
|
|
return Promise.resolve();
|
|
}
|
|
video.currentTime = targetTime;
|
|
return waitForMediaEvent(video, 'seeked', 2500).catch(function () {});
|
|
}
|
|
|
|
function videoSampleTimes(duration, mode) {
|
|
var base =
|
|
mode === 'packed'
|
|
? [0, 0.5, 1, 2, 3]
|
|
: [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 2.1, 2.4, 2.7, 3];
|
|
var filtered = base.filter(function (time) {
|
|
return !duration || !Number.isFinite(duration) || time <= duration;
|
|
});
|
|
return filtered.length ? filtered : [duration ? duration / 2 : 0];
|
|
}
|
|
|
|
function uniqueTimes(times) {
|
|
var seen = {};
|
|
return times
|
|
.slice()
|
|
.sort(function (a, b) {
|
|
return a - b;
|
|
})
|
|
.filter(function (time) {
|
|
var key = time.toFixed(2);
|
|
if (seen[key]) return false;
|
|
seen[key] = true;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function readVideoFrame(video, maxWidth) {
|
|
var sourceWidth = Math.max(1, video.videoWidth || 1);
|
|
var sourceHeight = Math.max(1, video.videoHeight || 1);
|
|
var sampleWidth = Math.min(sourceWidth, Number(maxWidth) || 480);
|
|
var sampleHeight = Math.max(
|
|
1,
|
|
Math.round((sampleWidth * sourceHeight) / sourceWidth)
|
|
);
|
|
var canvas = document.createElement('canvas');
|
|
var context = canvas.getContext('2d', { willReadFrequently: true });
|
|
canvas.width = sampleWidth;
|
|
canvas.height = sampleHeight;
|
|
context.clearRect(0, 0, sampleWidth, sampleHeight);
|
|
context.drawImage(video, 0, 0, sampleWidth, sampleHeight);
|
|
return {
|
|
width: sampleWidth,
|
|
height: sampleHeight,
|
|
data: context.getImageData(0, 0, sampleWidth, sampleHeight).data,
|
|
};
|
|
}
|
|
|
|
function pixelAt(frame, x, y) {
|
|
var safeX = clamp(Math.round(x), 0, frame.width - 1);
|
|
var safeY = clamp(Math.round(y), 0, frame.height - 1);
|
|
var index = (safeY * frame.width + safeX) * 4;
|
|
return {
|
|
r: frame.data[index],
|
|
g: frame.data[index + 1],
|
|
b: frame.data[index + 2],
|
|
a: frame.data[index + 3],
|
|
};
|
|
}
|
|
|
|
function hasNativeAlpha(frames) {
|
|
for (var index = 0; index < frames.length; index += 1) {
|
|
var data = frames[index].data;
|
|
var transparent = 0;
|
|
var sampled = 0;
|
|
var step = Math.max(4, Math.floor(data.length / (4 * 6000)) * 4);
|
|
for (var offset = 3; offset < data.length; offset += step) {
|
|
sampled += 1;
|
|
if (data[offset] < 245) transparent += 1;
|
|
}
|
|
if (sampled && transparent / sampled > 0.003) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isLikelyAlphaMaskPixel(pixel) {
|
|
var maxChannel = Math.max(pixel.r, pixel.g, pixel.b);
|
|
if (maxChannel < 22) return false;
|
|
var minChannel = Math.min(pixel.r, pixel.g, pixel.b);
|
|
var saturation = maxChannel
|
|
? (maxChannel - minChannel) / maxChannel
|
|
: 0;
|
|
var grayError =
|
|
(Math.abs(pixel.r - pixel.g) +
|
|
Math.abs(pixel.g - pixel.b) +
|
|
Math.abs(pixel.b - pixel.r)) /
|
|
(3 * 255);
|
|
return saturation < 0.16 && grayError < 0.055;
|
|
}
|
|
|
|
function detectPackedAlphaTopRightInFrame(frame) {
|
|
var width = frame.width;
|
|
var height = frame.height;
|
|
if (width < 12 || height < 12) return null;
|
|
|
|
var searchStart = clamp(Math.floor(width * 0.54), 0, width - 1);
|
|
var sampleHeight = clamp(Math.floor(height * 0.58), 1, height);
|
|
var stepY = Math.max(1, Math.floor(sampleHeight / 220));
|
|
var rowSamples = Math.max(1, Math.ceil(sampleHeight / stepY));
|
|
var threshold = Math.max(8, Math.floor(rowSamples * 0.32));
|
|
var grayColumns = [];
|
|
|
|
for (var x = searchStart; x < width; x += 1) {
|
|
var grayCount = 0;
|
|
for (var y = 0; y < sampleHeight; y += stepY) {
|
|
if (isLikelyAlphaMaskPixel(pixelAt(frame, x, y))) {
|
|
grayCount += 1;
|
|
}
|
|
}
|
|
grayColumns[x] = grayCount >= threshold;
|
|
}
|
|
|
|
var minRangeWidth = Math.max(24, Math.floor(width * 0.12));
|
|
var bestStart = -1;
|
|
var bestEnd = -1;
|
|
var cursor = searchStart;
|
|
while (cursor < width) {
|
|
while (cursor < width && !grayColumns[cursor]) cursor += 1;
|
|
if (cursor >= width) break;
|
|
var start = cursor;
|
|
while (cursor < width && grayColumns[cursor]) cursor += 1;
|
|
var end = cursor - 1;
|
|
if (end - start + 1 >= minRangeWidth && end > bestEnd) {
|
|
bestStart = start;
|
|
bestEnd = end;
|
|
}
|
|
}
|
|
if (bestStart < 0 || bestEnd < 0) return null;
|
|
return {
|
|
frameWidth: width,
|
|
frameHeight: height,
|
|
alphaStartX: bestStart,
|
|
alphaEndX: bestEnd,
|
|
};
|
|
}
|
|
|
|
function detectPackedAlphaTopRight(frames) {
|
|
var candidates = frames
|
|
.map(detectPackedAlphaTopRightInFrame)
|
|
.filter(Boolean);
|
|
if (!candidates.length) return null;
|
|
var frameWidth = median(
|
|
candidates.map(function (item) {
|
|
return item.frameWidth;
|
|
})
|
|
);
|
|
var frameHeight = median(
|
|
candidates.map(function (item) {
|
|
return item.frameHeight;
|
|
})
|
|
);
|
|
var alphaStartX = median(
|
|
candidates.map(function (item) {
|
|
return item.alphaStartX;
|
|
})
|
|
);
|
|
var alphaEndX = median(
|
|
candidates.map(function (item) {
|
|
return item.alphaEndX;
|
|
})
|
|
);
|
|
var alphaWidth = alphaEndX - alphaStartX + 1;
|
|
var alphaStartRatio = alphaStartX / frameWidth;
|
|
var alphaWidthRatio = alphaWidth / frameWidth;
|
|
var colorToAlphaRatio = alphaStartX / alphaWidth;
|
|
if (
|
|
alphaStartRatio < 0.58 ||
|
|
alphaStartRatio > 0.78 ||
|
|
alphaWidthRatio < 0.18 ||
|
|
alphaWidthRatio > 0.42 ||
|
|
colorToAlphaRatio < 1.45 ||
|
|
colorToAlphaRatio > 2.65 ||
|
|
alphaEndX < frameWidth * 0.82
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
var colorWidth = clamp(alphaStartX, 1, frameWidth - 1);
|
|
var safeAlphaEndX = clamp(alphaEndX, colorWidth, frameWidth - 1);
|
|
var safeAlphaWidth = Math.max(1, safeAlphaEndX - colorWidth + 1);
|
|
var alphaHeight = clamp(
|
|
Math.round((frameHeight * safeAlphaWidth) / colorWidth),
|
|
1,
|
|
frameHeight
|
|
);
|
|
return {
|
|
kind: 'packedAlphaTopRight',
|
|
hasAlphaMask: true,
|
|
hasNativeAlpha: false,
|
|
contentAspectRatio: colorWidth / frameHeight,
|
|
colorRect: {
|
|
x: 0,
|
|
y: 0,
|
|
width: colorWidth / frameWidth,
|
|
height: 1,
|
|
},
|
|
alphaRect: {
|
|
x: colorWidth / frameWidth,
|
|
y: 0,
|
|
width: safeAlphaWidth / frameWidth,
|
|
height: alphaHeight / frameHeight,
|
|
},
|
|
};
|
|
}
|
|
|
|
function statsForRect(frame, startX, startY, endX, endY) {
|
|
var width = Math.max(1, endX - startX);
|
|
var height = Math.max(1, endY - startY);
|
|
var stepX = Math.max(1, Math.floor(width / 80));
|
|
var stepY = Math.max(1, Math.floor(height / 120));
|
|
var saturationSum = 0;
|
|
var grayErrorSum = 0;
|
|
var brightnessSum = 0;
|
|
var count = 0;
|
|
for (var y = startY; y < endY; y += stepY) {
|
|
for (var x = startX; x < endX; x += stepX) {
|
|
var pixel = pixelAt(frame, x, y);
|
|
var red = pixel.r / 255;
|
|
var green = pixel.g / 255;
|
|
var blue = pixel.b / 255;
|
|
var maxChannel = Math.max(red, green, blue);
|
|
var minChannel = Math.min(red, green, blue);
|
|
saturationSum += maxChannel
|
|
? (maxChannel - minChannel) / maxChannel
|
|
: 0;
|
|
grayErrorSum +=
|
|
(Math.abs(red - green) +
|
|
Math.abs(green - blue) +
|
|
Math.abs(blue - red)) /
|
|
3;
|
|
brightnessSum += red * 0.299 + green * 0.587 + blue * 0.114;
|
|
count += 1;
|
|
}
|
|
}
|
|
if (!count) {
|
|
return { saturation: 0, grayError: 0, brightness: 0 };
|
|
}
|
|
return {
|
|
saturation: saturationSum / count,
|
|
grayError: grayErrorSum / count,
|
|
brightness: brightnessSum / count,
|
|
};
|
|
}
|
|
|
|
function statsByHorizontalHalves(frame) {
|
|
var half = Math.floor(frame.width / 2);
|
|
return [
|
|
statsForRect(frame, 0, 0, half, frame.height),
|
|
statsForRect(frame, half, 0, frame.width, frame.height),
|
|
];
|
|
}
|
|
|
|
function statsByVerticalHalves(frame) {
|
|
var half = Math.floor(frame.height / 2);
|
|
return [
|
|
statsForRect(frame, 0, 0, frame.width, half),
|
|
statsForRect(frame, 0, half, frame.width, frame.height),
|
|
];
|
|
}
|
|
|
|
function addSplitScore(score, first, second) {
|
|
if (
|
|
Math.max(first.brightness, second.brightness) <
|
|
MIN_USEFUL_FRAME_BRIGHTNESS
|
|
) {
|
|
return;
|
|
}
|
|
var alphaStats = first.saturation < second.saturation ? first : second;
|
|
var saturationGap = Math.abs(second.saturation - first.saturation);
|
|
if (
|
|
saturationGap < MIN_SPLIT_SATURATION_GAP ||
|
|
alphaStats.saturation > MAX_SPLIT_MASK_SATURATION ||
|
|
alphaStats.grayError > MAX_SPLIT_MASK_GRAY_ERROR
|
|
) {
|
|
return;
|
|
}
|
|
score.direction += second.saturation - first.saturation;
|
|
score.saturationGap += saturationGap;
|
|
score.maskSaturation += alphaStats.saturation;
|
|
score.maskGrayError += alphaStats.grayError;
|
|
score.frames += 1;
|
|
}
|
|
|
|
function splitCandidateFromScore(orientation, score) {
|
|
if (score.frames < MIN_SPLIT_MATCH_FRAMES) return null;
|
|
var averageSaturationGap = score.saturationGap / score.frames;
|
|
var averageMaskSaturation = score.maskSaturation / score.frames;
|
|
var averageMaskGrayError = score.maskGrayError / score.frames;
|
|
if (
|
|
averageSaturationGap < MIN_SPLIT_SATURATION_GAP ||
|
|
averageMaskSaturation > MAX_SPLIT_MASK_SATURATION ||
|
|
averageMaskGrayError > MAX_SPLIT_MASK_GRAY_ERROR
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
candidate: {
|
|
orientation: orientation,
|
|
alphaFirst: score.direction > 0,
|
|
},
|
|
score: averageSaturationGap,
|
|
};
|
|
}
|
|
|
|
function splitSourceLayout(candidate, video) {
|
|
var left = { x: 0, y: 0, width: 0.5, height: 1 };
|
|
var right = { x: 0.5, y: 0, width: 0.5, height: 1 };
|
|
var top = { x: 0, y: 0, width: 1, height: 0.5 };
|
|
var bottom = { x: 0, y: 0.5, width: 1, height: 0.5 };
|
|
var isHorizontal = candidate.orientation === 'horizontal';
|
|
var first = isHorizontal ? left : top;
|
|
var second = isHorizontal ? right : bottom;
|
|
var width = Math.max(1, video.videoWidth || 1);
|
|
var height = Math.max(1, video.videoHeight || 1);
|
|
return {
|
|
kind: isHorizontal ? 'splitAlphaHorizontal' : 'splitAlphaVertical',
|
|
hasAlphaMask: true,
|
|
hasNativeAlpha: false,
|
|
contentAspectRatio: isHorizontal
|
|
? width / 2 / height
|
|
: width / (height / 2),
|
|
colorRect: candidate.alphaFirst ? second : first,
|
|
alphaRect: candidate.alphaFirst ? first : second,
|
|
};
|
|
}
|
|
|
|
function detectSplitAlpha(frames, video) {
|
|
var horizontalScore = {
|
|
direction: 0,
|
|
saturationGap: 0,
|
|
maskSaturation: 0,
|
|
maskGrayError: 0,
|
|
frames: 0,
|
|
};
|
|
var verticalScore = {
|
|
direction: 0,
|
|
saturationGap: 0,
|
|
maskSaturation: 0,
|
|
maskGrayError: 0,
|
|
frames: 0,
|
|
};
|
|
frames.forEach(function (frame) {
|
|
var horizontal = statsByHorizontalHalves(frame);
|
|
var vertical = statsByVerticalHalves(frame);
|
|
addSplitScore(horizontalScore, horizontal[0], horizontal[1]);
|
|
addSplitScore(verticalScore, vertical[0], vertical[1]);
|
|
});
|
|
var candidates = [
|
|
splitCandidateFromScore('horizontal', horizontalScore),
|
|
splitCandidateFromScore('vertical', verticalScore),
|
|
].filter(Boolean);
|
|
if (!candidates.length) return null;
|
|
candidates.sort(function (a, b) {
|
|
return b.score - a.score;
|
|
});
|
|
return splitSourceLayout(candidates[0].candidate, video);
|
|
}
|
|
|
|
function sampleVideoFrames(video, mode, maxWidth) {
|
|
var times = uniqueTimes(videoSampleTimes(video.duration || 0, mode));
|
|
var frames = [];
|
|
var chain = Promise.resolve();
|
|
times.forEach(function (time) {
|
|
chain = chain
|
|
.then(function () {
|
|
return seekVideo(video, time);
|
|
})
|
|
.then(function () {
|
|
frames.push(readVideoFrame(video, maxWidth));
|
|
});
|
|
});
|
|
return chain.then(function () {
|
|
return frames;
|
|
});
|
|
}
|
|
|
|
function detectVideoLayout(video, url, options) {
|
|
var cacheKey = String(url || '');
|
|
if (videoLayoutCache[cacheKey]) {
|
|
return Promise.resolve(videoLayoutCache[cacheKey]);
|
|
}
|
|
var maxWidth = Number(options.videoDetectionSampleWidth) || 480;
|
|
return loadVideoMetadata(video)
|
|
.then(function () {
|
|
return sampleVideoFrames(video, 'packed', maxWidth);
|
|
})
|
|
.then(function (packedFrames) {
|
|
if (hasNativeAlpha(packedFrames)) {
|
|
return fullFrameLayout(video, 'nativeAlpha', true);
|
|
}
|
|
return (
|
|
detectPackedAlphaTopRight(packedFrames) ||
|
|
sampleVideoFrames(video, 'split', maxWidth).then(
|
|
function (splitFrames) {
|
|
return (
|
|
detectSplitAlpha(splitFrames, video) ||
|
|
fullFrameLayout(video, 'normal', false)
|
|
);
|
|
}
|
|
)
|
|
);
|
|
})
|
|
.then(function (layout) {
|
|
videoLayoutCache[cacheKey] = layout;
|
|
return layout;
|
|
});
|
|
}
|
|
|
|
function rectToPixels(rect, width, height) {
|
|
var x = clamp(Math.round(rect.x * width), 0, width - 1);
|
|
var y = clamp(Math.round(rect.y * height), 0, height - 1);
|
|
var right = clamp(
|
|
Math.round((rect.x + rect.width) * width),
|
|
x + 1,
|
|
width
|
|
);
|
|
var bottom = clamp(
|
|
Math.round((rect.y + rect.height) * height),
|
|
y + 1,
|
|
height
|
|
);
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
width: Math.max(1, right - x),
|
|
height: Math.max(1, bottom - y),
|
|
};
|
|
}
|
|
|
|
function layoutCanvasSize(video, layout, options) {
|
|
var videoWidth = Math.max(1, video.videoWidth || 1);
|
|
var videoHeight = Math.max(1, video.videoHeight || 1);
|
|
var rect = layout.hasAlphaMask
|
|
? rectToPixels(layout.colorRect, videoWidth, videoHeight)
|
|
: { width: videoWidth, height: videoHeight };
|
|
var maxDimension = Number(options.maxVideoCanvasDimension) || 1200;
|
|
var scale = Math.min(
|
|
1,
|
|
maxDimension / Math.max(rect.width, rect.height)
|
|
);
|
|
return {
|
|
width: Math.max(1, Math.round(rect.width * scale)),
|
|
height: Math.max(1, Math.round(rect.height * scale)),
|
|
};
|
|
}
|
|
|
|
function renderMaskedVideoFrame(video, canvas, layout, scratch) {
|
|
var videoWidth = Math.max(1, video.videoWidth || 1);
|
|
var videoHeight = Math.max(1, video.videoHeight || 1);
|
|
var outWidth = canvas.width;
|
|
var outHeight = canvas.height;
|
|
var context = scratch.outputContext;
|
|
context.clearRect(0, 0, outWidth, outHeight);
|
|
|
|
if (!layout.hasAlphaMask) {
|
|
context.drawImage(video, 0, 0, outWidth, outHeight);
|
|
return;
|
|
}
|
|
|
|
var colorRect = rectToPixels(layout.colorRect, videoWidth, videoHeight);
|
|
var alphaRect = rectToPixels(layout.alphaRect, videoWidth, videoHeight);
|
|
scratch.colorCanvas.width = outWidth;
|
|
scratch.colorCanvas.height = outHeight;
|
|
scratch.alphaCanvas.width = outWidth;
|
|
scratch.alphaCanvas.height = outHeight;
|
|
scratch.colorContext.clearRect(0, 0, outWidth, outHeight);
|
|
scratch.alphaContext.clearRect(0, 0, outWidth, outHeight);
|
|
scratch.colorContext.drawImage(
|
|
video,
|
|
colorRect.x,
|
|
colorRect.y,
|
|
colorRect.width,
|
|
colorRect.height,
|
|
0,
|
|
0,
|
|
outWidth,
|
|
outHeight
|
|
);
|
|
scratch.alphaContext.drawImage(
|
|
video,
|
|
alphaRect.x,
|
|
alphaRect.y,
|
|
alphaRect.width,
|
|
alphaRect.height,
|
|
0,
|
|
0,
|
|
outWidth,
|
|
outHeight
|
|
);
|
|
var colorData = scratch.colorContext.getImageData(
|
|
0,
|
|
0,
|
|
outWidth,
|
|
outHeight
|
|
);
|
|
var alphaData = scratch.alphaContext.getImageData(
|
|
0,
|
|
0,
|
|
outWidth,
|
|
outHeight
|
|
);
|
|
for (var offset = 0; offset < colorData.data.length; offset += 4) {
|
|
colorData.data[offset + 3] =
|
|
alphaData.data[offset] * 0.299 +
|
|
alphaData.data[offset + 1] * 0.587 +
|
|
alphaData.data[offset + 2] * 0.114;
|
|
}
|
|
context.putImageData(colorData, 0, 0);
|
|
}
|
|
|
|
function initPAG(libBase) {
|
|
var base = libBase || DEFAULT_PAG_BASE;
|
|
if (pagInitCache[base]) return pagInitCache[base];
|
|
pagInitCache[base] = loadScriptOnce(base + 'libpag.umd.js').then(
|
|
function () {
|
|
if (!window.libpag) throw new Error('pag_unavailable');
|
|
return window.libpag.PAGInit({
|
|
locateFile: function (file) {
|
|
return base + file;
|
|
},
|
|
});
|
|
}
|
|
);
|
|
return pagInitCache[base];
|
|
}
|
|
|
|
function EffectPlayer(target, options) {
|
|
this.container = resolveElement(target);
|
|
this.options = extend(
|
|
{
|
|
activeClass: 'is-active',
|
|
fit: 'contain',
|
|
loop: true,
|
|
muted: true,
|
|
waitForWindowLoad: true,
|
|
svgaScript: DEFAULT_SVGA_SCRIPT,
|
|
pagBase: DEFAULT_PAG_BASE,
|
|
},
|
|
options || {}
|
|
);
|
|
this.url = '';
|
|
this.active = false;
|
|
this.cleanup = null;
|
|
this.current = null;
|
|
this.sequence = 0;
|
|
this.destroyed = false;
|
|
}
|
|
|
|
EffectPlayer.prototype._teardown = function () {
|
|
if (this.cleanup) {
|
|
try {
|
|
this.cleanup();
|
|
} catch (_) {}
|
|
}
|
|
this.cleanup = null;
|
|
this.active = false;
|
|
this.current = null;
|
|
if (this.container) {
|
|
this.container.classList.remove(this.options.activeClass);
|
|
this.container.innerHTML = '';
|
|
}
|
|
};
|
|
|
|
EffectPlayer.prototype._mount = function (node) {
|
|
if (!this.container) throw new Error('container_unavailable');
|
|
this.container.innerHTML = '';
|
|
if (node) this.container.appendChild(node);
|
|
this.container.classList.add(this.options.activeClass);
|
|
this.active = true;
|
|
};
|
|
|
|
EffectPlayer.prototype._isCurrent = function (url) {
|
|
return !this.destroyed && this.url === url;
|
|
};
|
|
|
|
EffectPlayer.prototype._createVideoElement = function (
|
|
url,
|
|
options,
|
|
useCORS
|
|
) {
|
|
var video = document.createElement('video');
|
|
if (useCORS) video.crossOrigin = 'anonymous';
|
|
video.src = url;
|
|
video.autoplay = true;
|
|
video.loop = options.loop !== false;
|
|
video.muted = options.muted !== false;
|
|
video.playsInline = true;
|
|
video.setAttribute('webkit-playsinline', 'true');
|
|
video.preload = options.preload || 'auto';
|
|
return video;
|
|
};
|
|
|
|
EffectPlayer.prototype._playPlainVideo = function (url, options, video) {
|
|
if (!this._isCurrent(url)) return Promise.resolve();
|
|
video = video || this._createVideoElement(url, options, false);
|
|
styleNode(video, options);
|
|
this._mount(video);
|
|
this.cleanup = function () {
|
|
video.pause();
|
|
video.removeAttribute('src');
|
|
video.load();
|
|
};
|
|
return video.play().catch(function () {});
|
|
};
|
|
|
|
EffectPlayer.prototype._playCanvasVideo = function (
|
|
url,
|
|
options,
|
|
video,
|
|
layout
|
|
) {
|
|
var self = this;
|
|
if (!this._isCurrent(url)) return Promise.resolve();
|
|
var size = layoutCanvasSize(video, layout, options);
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = size.width;
|
|
canvas.height = size.height;
|
|
styleNode(canvas, options);
|
|
var scratch = {
|
|
outputContext: canvas.getContext('2d', {
|
|
alpha: true,
|
|
willReadFrequently: !!layout.hasAlphaMask,
|
|
}),
|
|
colorCanvas: document.createElement('canvas'),
|
|
alphaCanvas: document.createElement('canvas'),
|
|
};
|
|
scratch.colorContext = scratch.colorCanvas.getContext('2d', {
|
|
willReadFrequently: true,
|
|
});
|
|
scratch.alphaContext = scratch.alphaCanvas.getContext('2d', {
|
|
willReadFrequently: true,
|
|
});
|
|
var raf = 0;
|
|
function draw() {
|
|
if (!self._isCurrent(url)) return;
|
|
if (video.readyState >= 2) {
|
|
renderMaskedVideoFrame(video, canvas, layout, scratch);
|
|
}
|
|
raf = window.requestAnimationFrame(draw);
|
|
}
|
|
this._mount(canvas);
|
|
this.cleanup = function () {
|
|
window.cancelAnimationFrame(raf);
|
|
video.pause();
|
|
video.removeAttribute('src');
|
|
video.load();
|
|
};
|
|
draw();
|
|
return seekVideo(video, 0).then(function () {
|
|
return video.play().catch(function () {});
|
|
});
|
|
};
|
|
|
|
EffectPlayer.prototype._playVideo = function (url, options) {
|
|
var self = this;
|
|
if (!this._isCurrent(url)) return Promise.resolve();
|
|
if (options.detectTransparentVideo === false) {
|
|
return this._playPlainVideo(url, options);
|
|
}
|
|
var video = this._createVideoElement(url, options, true);
|
|
return detectVideoLayout(video, url, options)
|
|
.then(function (layout) {
|
|
if (!self._isCurrent(url)) return undefined;
|
|
if (!layout.hasAlphaMask && !layout.hasNativeAlpha) {
|
|
return self._playPlainVideo(url, options, video);
|
|
}
|
|
return self._playCanvasVideo(url, options, video, layout);
|
|
})
|
|
.catch(function () {
|
|
if (!self._isCurrent(url)) return undefined;
|
|
return self._playPlainVideo(url, options);
|
|
});
|
|
};
|
|
|
|
EffectPlayer.prototype._playSVGA = function (url, options) {
|
|
var self = this;
|
|
var sourceURL = proxyBinaryURL(url, 'svga', options);
|
|
var size = mediaSize(self.container, options);
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = size.width;
|
|
canvas.height = size.height;
|
|
styleNode(canvas, options);
|
|
self._mount(canvas);
|
|
return loadSVGAItemOnce(sourceURL, canvas, options).then(
|
|
function (videoItem) {
|
|
if (!self._isCurrent(url)) return undefined;
|
|
if (!window.SVGA) throw new Error('svga_unavailable');
|
|
var player = new window.SVGA.Player(canvas);
|
|
self.cleanup = function () {
|
|
if (player.stopAnimation) player.stopAnimation();
|
|
if (player.clear) player.clear();
|
|
};
|
|
player.setVideoItem(videoItem);
|
|
player.startAnimation();
|
|
return undefined;
|
|
}
|
|
);
|
|
};
|
|
|
|
EffectPlayer.prototype._playPAG = function (url, options) {
|
|
var self = this;
|
|
var sourceURL = proxyBinaryURL(url, 'pag', options);
|
|
return initPAG(options.pagBase)
|
|
.then(function (PAG) {
|
|
if (!self._isCurrent(url)) return undefined;
|
|
return fetch(sourceURL).then(function (response) {
|
|
if (!response.ok) throw new Error('pag_fetch_failed');
|
|
return response.arrayBuffer().then(function (buffer) {
|
|
return { PAG: PAG, buffer: buffer };
|
|
});
|
|
});
|
|
})
|
|
.then(function (payload) {
|
|
if (!payload || !self._isCurrent(url)) return undefined;
|
|
var size = mediaSize(self.container, options);
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = size.width;
|
|
canvas.height = size.height;
|
|
styleNode(canvas, options);
|
|
self._mount(canvas);
|
|
return payload.PAG.PAGFile.load(payload.buffer).then(
|
|
function (pagFile) {
|
|
return payload.PAG.PAGView.init(pagFile, canvas, {
|
|
useScale: false,
|
|
});
|
|
}
|
|
);
|
|
})
|
|
.then(function (pagView) {
|
|
if (!self._isCurrent(url)) return undefined;
|
|
if (!pagView) throw new Error('pag_view_unavailable');
|
|
self.cleanup = function () {
|
|
if (pagView.destroy) pagView.destroy();
|
|
};
|
|
return pagView.play();
|
|
});
|
|
};
|
|
|
|
EffectPlayer.prototype._playNow = function (url, options) {
|
|
var type = inferType(url, options.type);
|
|
if (type === 'video' || type === 'mp4') {
|
|
return this._playVideo(url, options);
|
|
}
|
|
if (type === 'svga') return this._playSVGA(url, options);
|
|
if (type === 'pag') return this._playPAG(url, options);
|
|
return Promise.reject(new Error('unsupported_effect_type'));
|
|
};
|
|
|
|
EffectPlayer.prototype.play = function (url, options) {
|
|
var self = this;
|
|
var nextURL = String(url || '').trim();
|
|
var nextOptions = extend(extend({}, this.options), options || {});
|
|
if (!nextURL) {
|
|
this.stop();
|
|
return Promise.resolve();
|
|
}
|
|
if (this.url === nextURL && this.current) return this.current;
|
|
this.stop();
|
|
this.url = nextURL;
|
|
var sequence = this.sequence;
|
|
var run = function () {
|
|
if (self.destroyed || sequence !== self.sequence) {
|
|
return Promise.resolve();
|
|
}
|
|
self.current = Promise.resolve()
|
|
.then(function () {
|
|
return self._playNow(nextURL, nextOptions);
|
|
})
|
|
.catch(function (error) {
|
|
if (sequence === self.sequence) self.stop();
|
|
throw error;
|
|
});
|
|
return self.current;
|
|
};
|
|
if (nextOptions.waitForWindowLoad === false) return run();
|
|
this.current = waitForWindowLoad().then(run);
|
|
return this.current;
|
|
};
|
|
|
|
EffectPlayer.prototype.stop = function () {
|
|
this.sequence += 1;
|
|
this.url = '';
|
|
this._teardown();
|
|
};
|
|
|
|
EffectPlayer.prototype.destroy = function () {
|
|
this.destroyed = true;
|
|
this.stop();
|
|
this.container = null;
|
|
};
|
|
|
|
window.HyAppEffectPlayer = {
|
|
create: function (target, options) {
|
|
return new EffectPlayer(target, options);
|
|
},
|
|
preload: function (url, options) {
|
|
var normalized = extend(
|
|
{
|
|
svgaScript: DEFAULT_SVGA_SCRIPT,
|
|
proxy: true,
|
|
},
|
|
options || {}
|
|
);
|
|
var type = inferType(url, normalized.type);
|
|
if (type !== 'svga') return Promise.resolve();
|
|
var canvas = document.createElement('canvas');
|
|
var sourceURL = proxyBinaryURL(url, 'svga', normalized);
|
|
return loadSVGAItemOnce(sourceURL, canvas, normalized);
|
|
},
|
|
play: function (target, url, options) {
|
|
var player = new EffectPlayer(target, options);
|
|
return player.play(url).then(function () {
|
|
return player;
|
|
});
|
|
},
|
|
inferType: inferType,
|
|
loadScriptOnce: loadScriptOnce,
|
|
};
|
|
})();
|