mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 12:46:15 -08:00
373 lines
11 KiB
JavaScript
373 lines
11 KiB
JavaScript
import { MP4Clip, Combinator } from '@webav/av-cliper';
|
|
|
|
export default function artplayerProxyWebAV(opt = {}) {
|
|
return (art) => {
|
|
const { option, constructor } = art;
|
|
const { createElement, def } = constructor.utils;
|
|
|
|
const canvas = createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
let audioCtx;
|
|
let gainNode;
|
|
|
|
let clip = null;
|
|
let audioSource = null;
|
|
let intervalId = null;
|
|
let seekTarget = null;
|
|
let lastSeekTime = 0;
|
|
|
|
const state = {
|
|
playing: false,
|
|
duration: 0,
|
|
videoWidth: 0,
|
|
videoHeight: 0,
|
|
currentTime: 0,
|
|
playbackRate: 1,
|
|
paused: true,
|
|
ended: false,
|
|
readyState: 0,
|
|
buffered: 0,
|
|
muted: option.muted,
|
|
volume: option.volume,
|
|
autoplay: option.autoplay,
|
|
};
|
|
|
|
function reset() {
|
|
Object.assign(state, {
|
|
playing: false,
|
|
duration: 0,
|
|
videoWidth: 0,
|
|
videoHeight: 0,
|
|
currentTime: 0,
|
|
playbackRate: 1,
|
|
paused: true,
|
|
ended: false,
|
|
readyState: 0,
|
|
buffered: 0,
|
|
muted: option.muted,
|
|
volume: option.volume,
|
|
autoplay: option.autoplay,
|
|
});
|
|
}
|
|
|
|
function stop() {
|
|
if (intervalId) {
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
if (audioSource) {
|
|
audioSource.stop();
|
|
audioSource = null;
|
|
}
|
|
}
|
|
|
|
function updateVolume() {
|
|
if (gainNode) {
|
|
const effectiveVolume = state.muted ? 0 : state.volume;
|
|
gainNode.gain.setValueAtTime(effectiveVolume, audioCtx.currentTime);
|
|
}
|
|
}
|
|
|
|
async function play() {
|
|
if (!audioCtx) {
|
|
audioCtx = new AudioContext();
|
|
gainNode = audioCtx.createGain();
|
|
gainNode.connect(audioCtx.destination);
|
|
}
|
|
|
|
let curTime = state.currentTime * 1e6;
|
|
let startAt = 0;
|
|
let first = true;
|
|
let lastFrameTime = performance.now();
|
|
|
|
stop();
|
|
updateVolume();
|
|
|
|
async function frameHandler() {
|
|
if (!state.playing) return;
|
|
|
|
const currentFrameTime = performance.now();
|
|
const deltaTime = currentFrameTime - lastFrameTime;
|
|
lastFrameTime = currentFrameTime;
|
|
|
|
if (seekTarget !== null) {
|
|
curTime = seekTarget * 1e6;
|
|
seekTarget = null;
|
|
first = true;
|
|
} else {
|
|
curTime += deltaTime * 1000 * state.playbackRate;
|
|
}
|
|
|
|
state.currentTime = curTime / 1e6;
|
|
|
|
const { state: clipState, video, audio } = await clip.tick(Math.round(curTime));
|
|
|
|
art.emit('video:timeupdate', { type: 'timeupdate' });
|
|
|
|
if (clipState === 'done') {
|
|
stop();
|
|
state.ended = true;
|
|
state.playing = false;
|
|
state.paused = true;
|
|
art.emit('video:ended', { type: 'ended' });
|
|
return;
|
|
}
|
|
|
|
if (video && clipState === 'success') {
|
|
ctx.clearRect(0, 0, state.videoWidth, state.videoHeight);
|
|
ctx.drawImage(video, 0, 0, state.videoWidth, state.videoHeight);
|
|
video.close();
|
|
}
|
|
|
|
if (first) {
|
|
first = false;
|
|
} else if (audio?.[0]?.length) {
|
|
const buf = audioCtx.createBuffer(2, audio[0].length, 48000);
|
|
buf.copyToChannel(audio[0], 0);
|
|
buf.copyToChannel(audio[1], 1);
|
|
audioSource = audioCtx.createBufferSource();
|
|
audioSource.buffer = buf;
|
|
audioSource.connect(gainNode);
|
|
audioSource.playbackRate.setValueAtTime(state.playbackRate, audioCtx.currentTime);
|
|
startAt = Math.max(audioCtx.currentTime, startAt);
|
|
audioSource.start(startAt);
|
|
startAt += buf.duration / state.playbackRate;
|
|
}
|
|
}
|
|
|
|
state.playing = true;
|
|
state.paused = false;
|
|
intervalId = setInterval(frameHandler, 1000 / 60); // 约60fps
|
|
}
|
|
|
|
async function preview(time) {
|
|
const { video } = await clip.tick(time * 1e6);
|
|
if (video) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
video.close();
|
|
}
|
|
}
|
|
|
|
function resize() {
|
|
const player = art.template?.$player;
|
|
if (!player || option.autoSize) return;
|
|
|
|
const aspectRatio = canvas.videoWidth / canvas.videoHeight;
|
|
const containerWidth = player.clientWidth;
|
|
const containerHeight = player.clientHeight;
|
|
const containerRatio = containerWidth / containerHeight;
|
|
|
|
let paddingLeft = 0;
|
|
let paddingTop = 0;
|
|
|
|
if (containerRatio > aspectRatio) {
|
|
const canvasWidth = containerHeight * aspectRatio;
|
|
paddingLeft = (containerWidth - canvasWidth) / 2;
|
|
} else {
|
|
const canvasHeight = containerWidth / aspectRatio;
|
|
paddingTop = (containerHeight - canvasHeight) / 2;
|
|
}
|
|
|
|
Object.assign(canvas.style, {
|
|
padding: `${paddingTop}px ${paddingLeft}px`,
|
|
});
|
|
}
|
|
|
|
async function init() {
|
|
const isSupported = await Combinator.isSupported();
|
|
|
|
if (!isSupported) {
|
|
art.notice.show = 'WebAV is not supported';
|
|
throw new Error('WebAV is not supported');
|
|
}
|
|
|
|
stop();
|
|
reset();
|
|
|
|
if (clip) {
|
|
clip.destroy();
|
|
art.emit('video:abort', { type: 'abort' });
|
|
art.emit('video:emptied', { type: 'emptied' });
|
|
}
|
|
|
|
try {
|
|
await Promise.resolve();
|
|
state.readyState = 1;
|
|
art.emit('video:loadstart', { type: 'loadstart' });
|
|
const response = await fetch(option.url);
|
|
|
|
if (!response.body) {
|
|
throw new Error('No response body');
|
|
}
|
|
|
|
clip = new MP4Clip(response.body, opt);
|
|
} catch (error) {
|
|
state.readyState = 0;
|
|
art.emit('video:error', error);
|
|
throw error;
|
|
}
|
|
|
|
const info = await clip.ready;
|
|
|
|
Object.assign(state, {
|
|
readyState: 4,
|
|
duration: Math.round(info.duration / 1e6),
|
|
videoWidth: info.width,
|
|
videoHeight: info.height,
|
|
});
|
|
|
|
canvas.width = state.videoWidth;
|
|
canvas.height = state.videoHeight;
|
|
await preview(0.1);
|
|
resize();
|
|
|
|
art.emit('video:loadedmetadata', { type: 'loadedmetadata' });
|
|
art.emit('video:durationchange', { type: 'durationchange' });
|
|
art.emit('video:loadeddata', { type: 'loadeddata' });
|
|
art.emit('video:canplay', { type: 'canplay' });
|
|
art.emit('video:canplaythrough', { type: 'canplaythrough' });
|
|
}
|
|
|
|
def(canvas, 'duration', {
|
|
get: () => state.duration,
|
|
});
|
|
|
|
def(canvas, 'videoWidth', {
|
|
get: () => state.videoWidth,
|
|
});
|
|
|
|
def(canvas, 'videoHeight', {
|
|
get: () => state.videoHeight,
|
|
});
|
|
|
|
def(canvas, 'volume', {
|
|
get: () => state.volume,
|
|
set: (val) => {
|
|
state.volume = Math.max(0, Math.min(1, val));
|
|
updateVolume();
|
|
art.emit('video:volumechange', { type: 'volumechange' });
|
|
},
|
|
});
|
|
|
|
def(canvas, 'currentTime', {
|
|
get: () => state.currentTime,
|
|
set: (val) => {
|
|
if (state.readyState < 4) return;
|
|
const newTime = Math.max(0, Math.min(val, state.duration));
|
|
const now = performance.now();
|
|
if (now - lastSeekTime > 16) {
|
|
lastSeekTime = now;
|
|
seekTarget = newTime;
|
|
state.currentTime = newTime;
|
|
if (!state.playing) {
|
|
preview(newTime);
|
|
}
|
|
art.emit('video:timeupdate', { type: 'timeupdate' });
|
|
}
|
|
},
|
|
});
|
|
|
|
def(canvas, 'autoplay', {
|
|
get: () => state.autoplay,
|
|
set: (val) => {
|
|
state.autoplay = val;
|
|
if (val && state.readyState >= 4) {
|
|
canvas.play();
|
|
}
|
|
},
|
|
});
|
|
|
|
def(canvas, 'src', {
|
|
get: () => option.url,
|
|
set: (val) => {
|
|
option.url = val;
|
|
init().then(() => {
|
|
if (option.autoplay) {
|
|
canvas.play();
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
def(canvas, 'playbackRate', {
|
|
get: () => state.playbackRate,
|
|
set: (val) => {
|
|
state.playbackRate = Math.max(0.25, Math.min(2, val));
|
|
if (audioSource) {
|
|
audioSource.playbackRate.setValueAtTime(state.playbackRate, audioCtx.currentTime);
|
|
}
|
|
art.emit('video:ratechange', { type: 'ratechange' });
|
|
},
|
|
});
|
|
|
|
def(canvas, 'playing', {
|
|
get: () => state.playing,
|
|
});
|
|
|
|
def(canvas, 'paused', {
|
|
get: () => state.paused,
|
|
});
|
|
|
|
def(canvas, 'ended', {
|
|
get: () => state.ended,
|
|
});
|
|
|
|
def(canvas, 'readyState', {
|
|
get: () => state.readyState,
|
|
});
|
|
|
|
def(canvas, 'muted', {
|
|
get: () => state.muted,
|
|
set: (val) => {
|
|
state.muted = val;
|
|
updateVolume();
|
|
art.emit('video:volumechange', { type: 'volumechange' });
|
|
},
|
|
});
|
|
|
|
def(canvas, 'buffered', {
|
|
get: () => ({
|
|
start: () => 0,
|
|
end: () => state.buffered,
|
|
length: 1,
|
|
}),
|
|
});
|
|
|
|
def(canvas, 'play', {
|
|
value: async () => {
|
|
if (state.readyState < 4) return false;
|
|
await play();
|
|
art.emit('video:play', { type: 'play' });
|
|
art.emit('video:playing', { type: 'playing' });
|
|
return true;
|
|
},
|
|
});
|
|
|
|
def(canvas, 'pause', {
|
|
value: () => {
|
|
stop();
|
|
state.playing = false;
|
|
state.paused = true;
|
|
art.emit('video:pause', { type: 'pause' });
|
|
},
|
|
});
|
|
|
|
art.on('destroy', () => {
|
|
stop();
|
|
if (clip) {
|
|
clip.destroy();
|
|
}
|
|
});
|
|
|
|
art.on('resize', resize);
|
|
|
|
return canvas;
|
|
};
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window['artplayerProxyWebAV'] = artplayerProxyWebAV;
|
|
}
|