UNPKG

tav-media

Version:

Cross platform media editing framework

334 lines (333 loc) 14.2 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()); }); }; /* eslint-disable no-param-reassign */ import { Http } from '../io/http'; import { tav } from '../tav'; import { movieAudioProcessorCode } from './audio-worklet/movie-audio-processor'; import { setMediaSource } from './media-element-utils'; const DEFAULT_SAMPLE_RATE = 44100; export class MovieAudioReader { constructor(path) { this.contentTime = 0; this.released = false; this.inputChannelCount = 2; this.targetBitDepth = 16; this.inputSampleRate = DEFAULT_SAMPLE_RATE; this.audioSampleCount = 0; this.silenceAudioSampleCount = 1024; this.maxOutSampleCount = 1024; this.adjustRange = 0.1; this.audioData = []; this.playbackRate = 1; this.paused = false; this.audioPaused = true; this.audioIndex = -1; this.type = 1; this.audioContextResume = false; this.audioInitializationSuccess = false; this.lastPostAudioTime = 0; this.path = path; this.targetBitDepth = 16; this.silenceAudioSampleCount = 1024; this.maxOutSampleCount = 1024; this.audioData = []; this.audioSampleCount = 0; this.lastPostAudioTime = 0; MovieAudioReader.audioReaders.push(this); this.audioPromise = new Promise((resolve) => { MovieAudioReader.preload(this.path, true).then((preloadContext) => { this.preloadContext = preloadContext; this.audioContext = preloadContext.audioContext; this.resume(resolve); }); }); } static preload(path, useNow = false) { return __awaiter(this, void 0, void 0, function* () { if (!MovieAudioReader.audioContext) { if (!MovieAudioReader.audioProcessorLoaded) { const blob = new Blob([movieAudioProcessorCode], { type: 'text/javascript' }); this.processorCodePath = URL.createObjectURL(blob); MovieAudioReader.audioProcessorLoaded = true; } const audioContext = new AudioContext({ sampleRate: DEFAULT_SAMPLE_RATE }); yield audioContext.audioWorklet.addModule(this.processorCodePath); const audioWorkletNode = new AudioWorkletNode(audioContext, 'movie-audio-processor', { numberOfInputs: MovieAudioReader.maxAudioInputs }); MovieAudioReader.availableAudioIndexes = Array.from(new Array(MovieAudioReader.maxAudioInputs).keys()); audioWorkletNode.port.onmessage = (e) => { MovieAudioReader.audioReaders.forEach((reader) => { const audioData = e.data.outDataList[reader.audioIndex]; if (!audioData) return; if (reader.audioPaused || reader.audioEl.readyState < 1 || reader.audioEl.ended) { // console.log('audio send data when paused: ', reader.audioIndex, reader.path); return; } const dataArray = Array.from(new Int16Array(audioData.bytes)); if (reader.lastPostAudioTime === reader.audioEl.currentTime && Math.max(...dataArray) === 0 && Math.min(...dataArray) === 0) { // console.log(`audio send silence data, index: ${reader.audioIndex}, time: ${reader.audioEl.currentTime}`); return; } reader.lastPostAudioTime = reader.audioEl.currentTime; reader.audioData.push(...dataArray); reader.audioSampleCount += audioData.sampleCount; reader.inputChannelCount = audioData.channelCount; }); }; const context = { audioContext, audioWorkletNode, audioEl: null, }; MovieAudioReader.audioContext = context; } let audioElement = MovieAudioReader.audioElements[path]; MovieAudioReader.audioElements[path] = null; if (!audioElement) { audioElement = MovieAudioReader.createAudioElement(path); } if (!useNow) { yield MovieAudioReader.audioContext.audioContext.suspend(); MovieAudioReader.audioElements[path] = audioElement; } return Object.assign(Object.assign({}, MovieAudioReader.audioContext), { audioEl: audioElement }); }); } static createAudioElement(path) { const audioEl = document.createElement('audio'); audioEl.preload = 'auto'; audioEl.crossOrigin = 'anonymous'; const src = Http.getUrl(path); setMediaSource(audioEl, src); const source = MovieAudioReader.audioContext.audioContext.createMediaElementSource(audioEl); const audioElement = { audioEl, source, initializationSuccess: true, }; audioEl.onerror = () => { audioElement.initializationSuccess = false; }; return audioElement; } static pauseAll() { MovieAudioReader.pauseTimeout = setTimeout(() => { var _a; (_a = MovieAudioReader.audioContext) === null || _a === void 0 ? void 0 : _a.audioWorkletNode.port.postMessage({ status: 'pause' }); }, 10 * 1000); } static playAll() { var _a; if (MovieAudioReader.pauseTimeout) clearTimeout(MovieAudioReader.pauseTimeout); (_a = MovieAudioReader.audioContext) === null || _a === void 0 ? void 0 : _a.audioWorkletNode.port.postMessage({ status: 'play' }); } static MakeFromPath(path) { return new MovieAudioReader(path); } static MakeFromBytes(bytesOffset, length) { const module = tav; const arrayBuffer = new Uint8Array(module.HEAPU8.buffer, bytesOffset, length); const newArrayBuffer = arrayBuffer.slice(0, length); const blob = new Blob([newArrayBuffer.buffer]); const path = URL.createObjectURL(blob); return new MovieAudioReader(path); } render() { if (this.released) return; requestAnimationFrame(() => this.render()); if (!this.audioEl.paused && this.audioEl.currentTime - this.contentTime > 2 * this.adjustRange * this.playbackRate) { if (this.audioInitializationSuccess) this.pauseAudio(); return; } if (this.audioEl.paused && this.audioEl.currentTime - this.contentTime <= this.adjustRange * this.playbackRate) { if (this.audioInitializationSuccess) this.playAudio(); } } readNextSample() { const module = tav; let numSamples = Math.min(this.audioSampleCount, this.maxOutSampleCount); const byteCount = Math.ceil(numSamples * this.inputChannelCount); let outData = []; if (this.audioData.length === 0 || this.paused) { outData = Array.from(new Int16Array(new Array(this.silenceAudioSampleCount * this.inputChannelCount))); numSamples = this.silenceAudioSampleCount; } else { outData = this.audioData.splice(0, byteCount); this.audioSampleCount -= numSamples; } this.contentTime += (numSamples / this.inputSampleRate) * this.playbackRate; const audioBuffer = new Int16Array(outData); const audioUnit8Buffer = new Uint8Array(audioBuffer.buffer); const numBytes = audioUnit8Buffer.byteLength * Uint8Array.BYTES_PER_ELEMENT; const dataPtr = module._malloc(numBytes); if (audioBuffer.length > 0) { this.lastBufferData = dataPtr; } const dataOnHeap = new Uint8Array(module.HEAPU8.buffer, dataPtr, numBytes); dataOnHeap.set(audioUnit8Buffer); return { bytes: dataOnHeap.byteOffset, length: dataOnHeap.length, sampleRate: this.inputSampleRate, channels: this.inputChannelCount, outputSamplesCount: numSamples, }; } decodeAudio() { return this.audioPromise; } setOptions(options) { if (!this.audioContextResume) { this.resume(() => { }); return; } if (options.playbackRate) { if (this.playbackRate !== options.playbackRate) { this.playbackRate = options.playbackRate; if (options.playbackRate > 4 || options.playbackRate < 0.1) { console.warn(`Invalid audio playback rate ${options.playbackRate.toFixed(2)}! Please set playback rate between 0.1 and 4.0`); } const playbackRate = Math.max(Math.min(this.playbackRate, 4), 0.1); this.audioEl.playbackRate = playbackRate; } if (this.audioEl.muted) { this.audioEl.muted = false; } } else { this.audioEl.muted = true; } } freeBuffer() { if (!this.lastBufferData) { return; } const module = tav; module._free(this.lastBufferData); this.lastBufferData = null; } pause() { return new Promise((resolve) => { this.seekTo(this.contentTime * 1000000, true); this.paused = true; resolve(true); }); } continue() { this.paused = false; return Promise.resolve(true); } seekTo(contentTime, forceSeek = false) { var _a; if (!this.audioContextResume) { return; } if ((_a = this.audioEl) === null || _a === void 0 ? void 0 : _a.paused) { if (this.audioInitializationSuccess) this.playAudio(); } const targetTime = contentTime / 1000000; if (Math.abs(targetTime - this.contentTime) < this.adjustRange * this.playbackRate && !forceSeek) { return; } this.audioData = []; this.audioSampleCount = 0; this.contentTime = targetTime; if (this.audioEl) this.audioEl.currentTime = targetTime; } release() { MovieAudioReader.audioReaders.splice(MovieAudioReader.audioReaders.indexOf(this), 1); this.freeBuffer(); this.released = true; this.audioData = []; this.audioSampleCount = 0; this.contentTime = 0; if (!this.audioEl) return; this.audioEl.currentTime = 0; this.pauseAudio(); this.audioWorkletNode.port.postMessage({ operation: 'disconnect', index: this.audioIndex }); this.source.disconnect(); MovieAudioReader.availableAudioIndexes.push(this.audioIndex); this.audioIndex = -1; if (!MovieAudioReader.audioElements[this.path]) { MovieAudioReader.audioElements[this.path] = { audioEl: this.audioEl, source: this.source, initializationSuccess: true, }; } else { this.audioEl.remove(); } } playAudio() { this.audioEl.play() .then(() => { this.audioPaused = false; }) .catch(() => { }); } pauseAudio() { var _a; (_a = this.audioEl) === null || _a === void 0 ? void 0 : _a.pause(); this.audioPaused = true; } resume(resolve) { let isCallback = false; this.audioContext.resume().then(() => { if (this.audioContextResume) { return; } isCallback = true; this.audioContextResume = true; this.audioEl = this.preloadContext.audioEl.audioEl; this.audioInitializationSuccess = this.preloadContext.audioEl.initializationSuccess; this.source = this.preloadContext.audioEl.source; this.audioWorkletNode = this.preloadContext.audioWorkletNode; if (MovieAudioReader.availableAudioIndexes.length === 0) { console.warn(`too many audios at same time, maximum ${MovieAudioReader.maxAudioInputs}!`); this.audioIndex = 0; } else { this.audioIndex = MovieAudioReader.availableAudioIndexes.shift(); } this.source.connect(this.audioWorkletNode, 0, this.audioIndex); this.audioWorkletNode.port.postMessage({ operation: 'connect', index: this.audioIndex }); this.inputSampleRate = this.audioContext.sampleRate; this.inputChannelCount = this.source.channelCount; const bitDepth = this.audioWorkletNode.parameters.get('bitDepth'); bitDepth.setValueAtTime(this.targetBitDepth, 0); this.audioEl.currentTime = 0; this.audioEl.playbackRate = this.playbackRate; if (this.audioInitializationSuccess) this.playAudio(); requestAnimationFrame(() => this.render()); resolve(this); }); setTimeout(() => { if (!isCallback) { resolve(this); } }, 16); } } MovieAudioReader.audioProcessorLoaded = false; MovieAudioReader.maxAudioInputs = 100; MovieAudioReader.audioElements = {}; MovieAudioReader.audioReaders = [];