From cf8d211eb2f376eca0134691147e71c854747710 Mon Sep 17 00:00:00 2001 From: Brodypen Date: Sat, 28 Feb 2026 02:16:03 -0600 Subject: [PATCH] feat: add the speed to exporter lol --- src/lib/exporter/gifExporter.ts | 3 +- src/lib/exporter/streamingDecoder.ts | 51 +++++++++++++++++++++++----- src/lib/exporter/videoExporter.ts | 3 +- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index 382a010..db7f299 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -124,7 +124,7 @@ export class GifExporter { }); // Calculate effective duration and frame count (excluding trim regions) - const effectiveDuration = this.streamingDecoder.getEffectiveDuration(this.config.trimRegions); + const effectiveDuration = this.streamingDecoder.getEffectiveDuration(this.config.trimRegions, this.config.speedRegions); const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate); // Calculate frame delay in milliseconds (gif.js uses ms) @@ -144,6 +144,7 @@ export class GifExporter { await this.streamingDecoder.decodeAll( this.config.frameRate, this.config.trimRegions, + this.config.speedRegions, async (videoFrame, _exportTimestampUs, sourceTimestampMs) => { if (this.cancelled) { videoFrame.close(); diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index d5610fc..d07e164 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -1,5 +1,5 @@ import { WebDemuxer } from 'web-demuxer'; -import type { TrimRegion } from '@/components/video-editor/types'; +import type { TrimRegion, SpeedRegion } from '@/components/video-editor/types'; export interface DecodedVideoInfo { width: number; @@ -67,6 +67,7 @@ export class StreamingVideoDecoder { async decodeAll( targetFrameRate: number, trimRegions: TrimRegion[] | undefined, + speedRegions: SpeedRegion[] | undefined, onFrame: OnFrameCallback ): Promise { if (!this.demuxer || !this.metadata) { @@ -74,7 +75,10 @@ export class StreamingVideoDecoder { } const decoderConfig = await this.demuxer.getDecoderConfig('video'); - const segments = this.computeSegments(this.metadata.duration, trimRegions); + const segments = this.splitBySpeed( + this.computeSegments(this.metadata.duration, trimRegions), + speedRegions + ); const frameDurationUs = 1_000_000 / targetFrameRate; // Async frame queue — decoder pushes, consumer pulls @@ -218,7 +222,7 @@ export class StreamingVideoDecoder { */ private async deliverSegment( frames: VideoFrame[], - segment: { startSec: number; endSec: number }, + segment: { startSec: number; endSec: number; speed: number }, targetFrameRate: number, frameDurationUs: number, startExportFrameIndex: number, @@ -226,7 +230,9 @@ export class StreamingVideoDecoder { ): Promise { if (frames.length === 0) return startExportFrameIndex; - const segmentFrameCount = Math.ceil((segment.endSec - segment.startSec) * targetFrameRate); + const segmentFrameCount = Math.ceil( + (segment.endSec - segment.startSec) / segment.speed * targetFrameRate + ); let exportFrameIndex = startExportFrameIndex; for (let i = 0; i < segmentFrameCount && !this.cancelled; i++) { @@ -271,12 +277,39 @@ export class StreamingVideoDecoder { return segments; } - getEffectiveDuration(trimRegions?: TrimRegion[]): number { + getEffectiveDuration(trimRegions?: TrimRegion[], speedRegions?: SpeedRegion[]): number { if (!this.metadata) throw new Error('Must call loadMetadata() first'); - const trimmed = (trimRegions || []).reduce( - (sum, r) => sum + (r.endMs - r.startMs) / 1000, 0 - ); - return this.metadata.duration - trimmed; + const trimSegments = this.computeSegments(this.metadata.duration, trimRegions); + const speedSegments = this.splitBySpeed(trimSegments, speedRegions); + return speedSegments.reduce((sum, seg) => sum + (seg.endSec - seg.startSec) / seg.speed, 0); + } + + private splitBySpeed( + segments: Array<{ startSec: number; endSec: number }>, + speedRegions?: SpeedRegion[] + ): Array<{ startSec: number; endSec: number; speed: number }> { + if (!speedRegions || speedRegions.length === 0) + return segments.map(s => ({ ...s, speed: 1 })); + + const result: Array<{ startSec: number; endSec: number; speed: number }> = []; + for (const segment of segments) { + const overlapping = speedRegions + .filter(sr => (sr.startMs / 1000) < segment.endSec && (sr.endMs / 1000) > segment.startSec) + .sort((a, b) => a.startMs - b.startMs); + + if (overlapping.length === 0) { result.push({ ...segment, speed: 1 }); continue; } + + let cursor = segment.startSec; + for (const sr of overlapping) { + const srStart = Math.max(sr.startMs / 1000, segment.startSec); + const srEnd = Math.min(sr.endMs / 1000, segment.endSec); + if (cursor < srStart) result.push({ startSec: cursor, endSec: srStart, speed: 1 }); + result.push({ startSec: srStart, endSec: srEnd, speed: sr.speed }); + cursor = srEnd; + } + if (cursor < segment.endSec) result.push({ startSec: cursor, endSec: segment.endSec, speed: 1 }); + } + return result.filter(s => s.endSec - s.startSec > 0.0001); } cancel(): void { diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 8c513b2..e41bc47 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -83,7 +83,7 @@ export class VideoExporter { await this.muxer.initialize(); // Calculate effective duration and frame count (excluding trim regions) - const effectiveDuration = this.streamingDecoder.getEffectiveDuration(this.config.trimRegions); + const effectiveDuration = this.streamingDecoder.getEffectiveDuration(this.config.trimRegions, this.config.speedRegions); const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate); console.log('[VideoExporter] Original duration:', videoInfo.duration, 's'); @@ -98,6 +98,7 @@ export class VideoExporter { await this.streamingDecoder.decodeAll( this.config.frameRate, this.config.trimRegions, + this.config.speedRegions, async (videoFrame, _exportTimestampUs, sourceTimestampMs) => { if (this.cancelled) { videoFrame.close();