diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index be475809..db90b746 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -141,7 +141,7 @@ export function AnnotationSettingsPanel({ return (
-
+
{t("annotations.settings")} @@ -786,6 +786,7 @@ export function AnnotationSettingsPanel({
  • {t("annotations.tipCycleBackward")}
  • +
    -
    ); } diff --git a/src/components/video-editor/audio/clipAudio.ts b/src/components/video-editor/audio/clipAudio.ts index d539ad4b..c6bf7409 100644 --- a/src/components/video-editor/audio/clipAudio.ts +++ b/src/components/video-editor/audio/clipAudio.ts @@ -1,14 +1,13 @@ -import { mapSourceTimeToTimelineTime } from "../types"; +import { getClipSourceEndMs, sortClipRegions } from "../types"; import type { ClipRegion } from "../types"; export function getActiveClipIdAtSourceTime( sourceTimeSeconds: number, clipRegions: ClipRegion[], ): string | null { - const sourceMs = sourceTimeSeconds * 1000; - const timelineMs = mapSourceTimeToTimelineTime(sourceMs, clipRegions); - const activeClip = clipRegions.find( - (clip) => timelineMs >= clip.startMs && timelineMs < clip.endMs, + const sourceMs = Math.round(sourceTimeSeconds * 1000); + const activeClip = sortClipRegions(clipRegions).find( + (clip) => sourceMs >= clip.startMs && sourceMs < getClipSourceEndMs(clip), ); return activeClip?.id ?? null; } diff --git a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts index 2e776eb2..799d6db9 100644 --- a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts +++ b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts @@ -120,7 +120,9 @@ export function useSourceAudioTrackSettings({ [selectedClipId]: { ...prevClip, [id]: { - volume: Math.max(0, Math.min(2, volume)), + volume: Number.isFinite(volume) + ? Math.max(0, Math.min(2, volume)) + : (prevClip[id]?.volume ?? 1), normalize: prevClip[id]?.normalize ?? false, }, }, diff --git a/src/components/video-editor/audio/waveform/WaveformGenerator.ts b/src/components/video-editor/audio/waveform/WaveformGenerator.ts index a51475d8..b44c274f 100644 --- a/src/components/video-editor/audio/waveform/WaveformGenerator.ts +++ b/src/components/video-editor/audio/waveform/WaveformGenerator.ts @@ -8,43 +8,52 @@ export class WaveformGenerator { private peaksCache = new Map(); private pending = new Map>(); private workerRequestSeq = 0; - private workerResolvers = new Map void>(); + private workerResolvers = new Map void; reject: (err: Error) => void }>(); constructor() { this.audioContext = new (window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext)(); this.worker = new WorkerConstructor(); + this.worker.addEventListener( "message", - (event: MessageEvent<{ requestId: number; peaks: Float32Array }>) => { - const { requestId, peaks } = event.data; - const resolve = this.workerResolvers.get(requestId); - if (!resolve) return; + (event: MessageEvent<{ requestId: number; peaks?: Float32Array; error?: string }>) => { + const { requestId, peaks, error } = event.data; + const resolver = this.workerResolvers.get(requestId); + if (!resolver) return; + this.workerResolvers.delete(requestId); - resolve(peaks); + if (error) { + resolver.reject(new Error(error)); + } else if (peaks) { + resolver.resolve(peaks); + } }, ); + + this.worker.addEventListener("error", (error: ErrorEvent) => { + console.error("[WaveformGenerator] Worker fatal error:", error); + const fatalError = error.error ?? new Error(error.message || "Worker crashed"); + + // Reject all pending requests if the worker itself crashes + for (const resolver of this.workerResolvers.values()) { + resolver.reject(fatalError); + } + this.workerResolvers.clear(); + }); } - private computePeaksWithWorker(channelData: Float32Array, samples: number): Promise { + private computePeaksWithWorker(channels: Float32Array[], samples: number): Promise { return new Promise((resolve, reject) => { const requestId = ++this.workerRequestSeq; - const onError = (error: ErrorEvent) => { - this.worker.removeEventListener("error", onError); - this.workerResolvers.delete(requestId); - reject(error.error ?? new Error(error.message)); - }; - this.worker.addEventListener("error", onError, { once: true }); - this.workerResolvers.set(requestId, (peaks) => { - this.worker.removeEventListener("error", onError); - resolve(peaks); - }); + this.workerResolvers.set(requestId, { resolve, reject }); + this.worker.postMessage( { requestId, - channelData, + channels, samples, }, - [channelData.buffer], + channels.map(c => c.buffer), ); }); } @@ -65,8 +74,14 @@ export class WaveformGenerator { const arrayBuffer = await response.arrayBuffer(); const decoded = await this.audioContext.decodeAudioData(arrayBuffer); - const channelData = decoded.getChannelData(0).slice(); - const peaks = await this.computePeaksWithWorker(channelData, peakCount); + + const channels: Float32Array[] = []; + for (let i = 0; i < decoded.numberOfChannels; i++) { + // We slice to transfer the underlying buffer to the worker + channels.push(decoded.getChannelData(i).slice()); + } + + const peaks = await this.computePeaksWithWorker(channels, peakCount); let max = 0; for (let i = 0; i < peaks.length; i++) { diff --git a/src/components/video-editor/audio/waveform/waveform.worker.ts b/src/components/video-editor/audio/waveform/waveform.worker.ts index 1db7b382..f5332b64 100644 --- a/src/components/video-editor/audio/waveform/waveform.worker.ts +++ b/src/components/video-editor/audio/waveform/waveform.worker.ts @@ -1,34 +1,39 @@ self.onmessage = (e: MessageEvent) => { - const { requestId, channelData, samples } = e.data as { + const { requestId, channels, samples } = e.data as { requestId: number; - channelData: Float32Array; + channels: Float32Array[]; samples: number; }; - if (!channelData || samples <= 0) { + if (!channels || channels.length === 0 || samples <= 0) { const empty = new Float32Array(0); (self as any).postMessage({ requestId, peaks: empty }, [empty.buffer]); return; } try { - const step = Math.max(1, Math.floor(channelData.length / samples)); + const firstChannel = channels[0]; + const step = Math.max(1, Math.floor(firstChannel.length / samples)); const result = new Float32Array(samples); for (let i = 0; i < samples; i++) { const start = i * step; - const end = Math.min(start + step, channelData.length); + const end = Math.min(start + step, firstChannel.length); let max = 0; for (let j = start; j < end; j++) { - const val = Math.abs(channelData[j]); - if (val > max) max = val; + for (let c = 0; c < channels.length; c++) { + const val = Math.abs(channels[c][j]); + if (val > max) max = val; + } } result[i] = max; } (self as any).postMessage({ requestId, peaks: result }, [result.buffer]); - } catch { - const empty = new Float32Array(0); - (self as any).postMessage({ requestId, peaks: empty }, [empty.buffer]); + } catch (err) { + (self as any).postMessage({ + requestId, + error: err instanceof Error ? err.message : "Unknown worker error", + }); } };