tav-media
Version:
Cross platform media editing framework
234 lines (233 loc) • 9.53 kB
JavaScript
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;
}
}