UNPKG

tav-media

Version:

Cross platform media editing framework

234 lines (233 loc) 9.53 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { VIDEO_DECODE_WAIT_FRAME, VIDEO_PLAYBACK_RATE_MAX, VIDEO_PLAYBACK_RATE_MIN } from '../constant'; import { addListener, removeListener, removeAllListeners } from '../utils/video-listener'; import { IPHONE, IS_WECHAT } from '../utils/ua'; const UHD_RESOLUTION = 3840; // Get video initiated token on Wechat browser. const getWechatNetwork = () => { return new Promise((resolve) => { window.WeixinJSBridge.invoke('getNetworkType', {}, () => { resolve(); }, () => { resolve(); }); }); }; const playVideoElement = (videoElement) => __awaiter(void 0, void 0, void 0, function* () { if (IS_WECHAT && window.WeixinJSBridge) { yield getWechatNetwork(); } try { yield videoElement.play(); } catch (error) { console.error(error.message); throw new Error('Failed to decode video, please play PAG after user gesture. Or your can load a software decoder to decode the video.'); } }); export class VideoReader { constructor(mp4Data, width, height, frameRate, staticTimeRanges) { this.lastVideoTime = -1; this.hadPlay = false; this.lastPrepareTime = []; this.disablePlaybackRate = false; this.videoEl = document.createElement('video'); this.videoEl.style.display = 'none'; this.videoEl.muted = true; this.videoEl.playsInline = true; this.videoEl.load(); addListener(this.videoEl, 'timeupdate', this.onTimeupdate.bind(this)); this.frameRate = frameRate; const blob = new Blob([mp4Data], { type: 'video/mp4' }); this.videoEl.src = URL.createObjectURL(blob); this.staticTimeRanges = new StaticTimeRanges(staticTimeRanges); if (UHD_RESOLUTION < width || UHD_RESOLUTION < height) { this.disablePlaybackRate = true; } } prepare(targetFrame) { return __awaiter(this, void 0, void 0, function* () { if (!this.videoEl) { console.error('Video element is null!'); return false; } this.alignPlaybackRate(targetFrame); const { currentTime } = this.videoEl; const targetTime = targetFrame / this.frameRate; this.lastVideoTime = targetTime; if (currentTime === 0 && targetTime === 0) { if (this.hadPlay) { return true; } else { // Wait for initialization to complete yield playVideoElement(this.videoEl); // Pause video at first frame. yield new Promise((resolve) => { window.requestAnimationFrame(() => { if (!this.videoEl) { console.error('Video Element is null!'); } else { this.videoEl.pause(); this.hadPlay = true; } resolve(); }); }); return true; } } else { if (Math.round(targetTime * this.frameRate) === Math.round(currentTime * this.frameRate)) { // Current frame return true; } else if (this.staticTimeRanges.contains(targetFrame)) { // Static frame return yield this.seek(targetTime, false); } else if (Math.abs(currentTime - targetTime) < (1 / this.frameRate) * VIDEO_DECODE_WAIT_FRAME) { // Within tolerable frame rate deviation if (this.videoEl.paused) { yield playVideoElement(this.videoEl); } return true; } else { // Seek and play return yield this.seek(targetTime); } } }); } renderToTexture(GL, textureID) { var _a; if (!this.videoEl || this.videoEl.readyState < 2) return; const gl = (_a = GL.currentContext) === null || _a === void 0 ? void 0 : _a.GLctx; gl.bindTexture(gl.TEXTURE_2D, GL.textures[textureID]); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.videoEl); } onDestroy() { if (!this.videoEl) { throw new Error('Video element is null!'); } removeAllListeners(this.videoEl, 'playing'); removeAllListeners(this.videoEl, 'timeupdate'); this.videoEl = null; } onTimeupdate() { if (!this.videoEl || this.lastVideoTime < 0) return; const { currentTime } = this.videoEl; if (currentTime - this.lastVideoTime >= (1 / this.frameRate) * VIDEO_DECODE_WAIT_FRAME && !this.videoEl.paused) { this.videoEl.pause(); this.videoEl.currentTime = this.lastVideoTime; } } seek(targetTime, play = true) { return new Promise((resolve) => { let isCallback = false; let timer = null; const canplayCallback = () => __awaiter(this, void 0, void 0, function* () { if (!this.videoEl) { console.error('Video element is null!'); resolve(false); return; } removeListener(this.videoEl, 'seeked', canplayCallback); if (play && this.videoEl.paused) { yield playVideoElement(this.videoEl); } else if (!play && !this.videoEl.paused) { this.videoEl.pause(); } isCallback = true; clearTimeout(timer); timer = null; resolve(true); }); if (!this.videoEl) { console.error('Video element is null!'); resolve(false); return; } addListener(this.videoEl, 'seeked', canplayCallback); this.videoEl.currentTime = targetTime; // Timeout timer = setTimeout(() => { if (!isCallback) { if (!this.videoEl) { console.error('Video element is null!'); resolve(false); return; } else { removeListener(this.videoEl, 'seeked', canplayCallback); if (play && this.videoEl.paused) { playVideoElement(this.videoEl); } else if (!play && !this.videoEl.paused) { this.videoEl.pause(); } resolve(false); } } }, (1000 / this.frameRate) * VIDEO_DECODE_WAIT_FRAME); }); } alignPlaybackRate(targetFrame) { if (!this.videoEl || this.disablePlaybackRate) return; const now = performance.now(); if (this.lastPrepareTime.length === 0) { this.lastPrepareTime.push({ frame: targetFrame, time: now }); return; } if (this.lastPrepareTime[this.lastPrepareTime.length - 1].frame === targetFrame) return; if (targetFrame < this.lastPrepareTime[this.lastPrepareTime.length - 1].frame) { this.lastPrepareTime = []; this.lastPrepareTime.push({ frame: targetFrame, time: now }); return; } if (this.lastPrepareTime.length === 5) { this.lastPrepareTime.shift(); } this.lastPrepareTime.push({ frame: targetFrame, time: now }); const distance = (now - this.lastPrepareTime[0].time) / (targetFrame - this.lastPrepareTime[0].frame); let playbackRate = 1000 / this.frameRate / distance; playbackRate = Math.min(Math.max(playbackRate, VIDEO_PLAYBACK_RATE_MIN), VIDEO_PLAYBACK_RATE_MAX); this.videoEl.playbackRate = playbackRate; } } VideoReader.isIOS = () => { return IPHONE; }; VideoReader.isAndroidMiniprogram = () => { return false; }; export class StaticTimeRanges { constructor(timeRanges) { this.timeRanges = timeRanges; } contains(targetFrame) { if (this.timeRanges.length === 0) return false; for (let timeRange of this.timeRanges) { if (timeRange.start <= targetFrame && targetFrame < timeRange.end) { return true; } } return false; } }