import type {SignalValue, SimpleSignal} from '@revideo/core'; import { DependencyContext, PlaybackState, clamp, isReactive, useLogger, useThread, } from '@revideo/core'; import {computed, initial, nodeName, signal} from '../decorators'; import type {RectProps} from './Rect'; import {Rect} from './Rect'; export interface MediaProps extends RectProps { src?: SignalValue; loop?: SignalValue; playbackRate?: number; volume?: number; time?: SignalValue; play?: boolean; awaitCanPlay?: SignalValue; allowVolumeAmplificationInPreview?: SignalValue; } const reactivePlaybackRate = ` The \`playbackRate\` of a \`Video\` cannot be reactive. Make sure to use a concrete value and not a function: \`\`\`ts wrong video.playbackRate(() => 7); \`\`\` \`\`\`ts correct video.playbackRate(7); \`\`\` If you're using a signal, extract its value before passing it to the property: \`\`\`ts wrong video.playbackRate(mySignal); \`\`\` \`\`\`ts correct video.playbackRate(mySignal()); \`\`\` `; @nodeName('Media') export abstract class Media extends Rect { @signal() public declare readonly src: SimpleSignal; @initial(false) @signal() public declare readonly loop: SimpleSignal; @initial(1) @signal() public declare readonly playbackRate: SimpleSignal; @initial(0) @signal() protected declare readonly time: SimpleSignal; @initial(false) @signal() protected declare readonly playing: SimpleSignal; @initial(true) @signal() protected declare readonly awaitCanPlay: SimpleSignal; @initial(false) @signal() protected declare readonly allowVolumeAmplificationInPreview: SimpleSignal< boolean, this >; protected declare volume: number; protected static readonly amplificationPool: Record< string, { audioContext: AudioContext; sourceNode: MediaElementAudioSourceNode; gainNode: GainNode; } > = {}; protected lastTime = -1; public constructor(props: MediaProps) { super(props); if (!this.awaitCanPlay()) { this.scheduleSeek(this.time()); } if (props.play) { this.play(); } this.volume = props.volume ?? 1; this.setVolume(this.volume); } public isPlaying(): boolean { return this.playing(); } public getCurrentTime(): number { return this.clampTime(this.time()); } public getDuration(): number { return this.mediaElement().duration; } public getVolume(): number { return this.volume; } public getUrl(): string { return this.mediaElement().src; } public override dispose() { this.pause(); this.remove(); super.dispose(); } @computed() public override completion(): number { return this.clampTime(this.time()) / this.getDuration(); } protected abstract mediaElement(): HTMLMediaElement; protected abstract seekedMedia(): HTMLMediaElement; protected abstract fastSeekedMedia(): HTMLMediaElement; protected abstract override draw( context: CanvasRenderingContext2D, ): Promise; protected setCurrentTime(value: number) { const media = this.mediaElement(); if (media.readyState < 2) return; media.currentTime = value; this.lastTime = value; if (media.seeking) { DependencyContext.collectPromise( new Promise(resolve => { const listener = () => { resolve(); media.removeEventListener('seeked', listener); }; media.addEventListener('seeked', listener); }), ); } } public setVolume(volume: number) { if (volume < 0) { console.warn( `volumes cannot be negative - the value will be clamped to 0.`, ); } const media = this.mediaElement(); media.volume = Math.min(Math.max(volume, 0), 1); if (volume > 1) { if (this.allowVolumeAmplificationInPreview()) { this.amplify(media, volume); return; } console.warn( `you have set the volume of node ${this.key} to ${volume} - your video will be exported with the correct volume, but the browser does not support volumes higher than 1 by default. To enable volume amplification in the preview, set the "allowVolumeAmplificationInPreview" of your