@twick/video-editor 0.15.28 → 0.15.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/track/audio-waveform.d.ts +6 -0
- package/dist/components/track/timeline-media-strip.d.ts +14 -0
- package/dist/helpers/media-manager/browser-media-manager.d.ts +23 -0
- package/dist/hooks/use-marquee-selection.d.ts +2 -1
- package/dist/hooks/use-timeline-drop.d.ts +2 -1
- package/dist/index.js +703 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +704 -24
- package/dist/index.mjs.map +1 -1
- package/dist/video-editor.css +67 -2
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
5
5
|
import { useLivePlayerContext, PLAYER_STATE, LivePlayer } from "@twick/live-player";
|
|
6
|
-
import { ImageElement as ImageElement$1, AudioElement, VideoElement as VideoElement$1, useTimelineContext, TIMELINE_ACTION, ElementDeserializer, getCurrentElements, CaptionElement as CaptionElement$1,
|
|
6
|
+
import { ImageElement as ImageElement$1, AudioElement, VideoElement as VideoElement$1, useTimelineContext, TIMELINE_ACTION, ElementDeserializer, getCurrentElements, CaptionElement as CaptionElement$1, TIMELINE_ELEMENT_TYPE, canSplitElement, getDecimalNumber, resolveIds, TrackElement, TRACK_TYPES, Track, resolveId, getElementIdsInRange, ValidationError, VALIDATION_ERROR_CODE, formatTimeWithFrames } from "@twick/timeline";
|
|
7
7
|
import React, { useState, useRef, useCallback, useEffect, useMemo, forwardRef, createElement, createContext, useContext, useId, useLayoutEffect, useInsertionEffect, Fragment, Component } from "react";
|
|
8
8
|
import { createPortal } from "react-dom";
|
|
9
9
|
function t(t2, e3, s2) {
|
|
@@ -7166,7 +7166,7 @@ const COLOR_FILTERS = {
|
|
|
7166
7166
|
DRAMATIC: "dramatic",
|
|
7167
7167
|
FADED: "faded"
|
|
7168
7168
|
};
|
|
7169
|
-
class LRUCache {
|
|
7169
|
+
let LRUCache$1 = class LRUCache {
|
|
7170
7170
|
constructor(maxSize = 100) {
|
|
7171
7171
|
if (maxSize <= 0) {
|
|
7172
7172
|
throw new Error("maxSize must be greater than 0");
|
|
@@ -7226,10 +7226,10 @@ class LRUCache {
|
|
|
7226
7226
|
get size() {
|
|
7227
7227
|
return this.cache.size;
|
|
7228
7228
|
}
|
|
7229
|
-
}
|
|
7230
|
-
class VideoFrameExtractor {
|
|
7229
|
+
};
|
|
7230
|
+
let VideoFrameExtractor$1 = class VideoFrameExtractor {
|
|
7231
7231
|
constructor(options = {}) {
|
|
7232
|
-
this.frameCache = new LRUCache(
|
|
7232
|
+
this.frameCache = new LRUCache$1(
|
|
7233
7233
|
options.maxCacheSize ?? 50
|
|
7234
7234
|
);
|
|
7235
7235
|
this.videoElements = /* @__PURE__ */ new Map();
|
|
@@ -7495,16 +7495,16 @@ class VideoFrameExtractor {
|
|
|
7495
7495
|
this.videoElements.clear();
|
|
7496
7496
|
this.frameCache.clear();
|
|
7497
7497
|
}
|
|
7498
|
-
}
|
|
7499
|
-
let defaultExtractor = null;
|
|
7500
|
-
function getDefaultVideoFrameExtractor(options) {
|
|
7501
|
-
if (!defaultExtractor) {
|
|
7502
|
-
defaultExtractor = new VideoFrameExtractor(options);
|
|
7498
|
+
};
|
|
7499
|
+
let defaultExtractor$1 = null;
|
|
7500
|
+
function getDefaultVideoFrameExtractor$1(options) {
|
|
7501
|
+
if (!defaultExtractor$1) {
|
|
7502
|
+
defaultExtractor$1 = new VideoFrameExtractor$1(options);
|
|
7503
7503
|
}
|
|
7504
|
-
return defaultExtractor;
|
|
7504
|
+
return defaultExtractor$1;
|
|
7505
7505
|
}
|
|
7506
|
-
async function getThumbnailCached(videoUrl, seekTime = 0.1, playbackRate) {
|
|
7507
|
-
const extractor = getDefaultVideoFrameExtractor(
|
|
7506
|
+
async function getThumbnailCached$1(videoUrl, seekTime = 0.1, playbackRate) {
|
|
7507
|
+
const extractor = getDefaultVideoFrameExtractor$1(
|
|
7508
7508
|
void 0
|
|
7509
7509
|
);
|
|
7510
7510
|
return extractor.getFrame(videoUrl, seekTime);
|
|
@@ -8012,7 +8012,7 @@ const addVideoElement = async ({
|
|
|
8012
8012
|
}) => {
|
|
8013
8013
|
var _a;
|
|
8014
8014
|
try {
|
|
8015
|
-
const thumbnailUrl = await getThumbnailCached(
|
|
8015
|
+
const thumbnailUrl = await getThumbnailCached$1(
|
|
8016
8016
|
((_a = element == null ? void 0 : element.props) == null ? void 0 : _a.src) || "",
|
|
8017
8017
|
snapTime
|
|
8018
8018
|
);
|
|
@@ -9577,6 +9577,7 @@ function useTimelineDrop({
|
|
|
9577
9577
|
zoomLevel,
|
|
9578
9578
|
labelWidth,
|
|
9579
9579
|
trackHeight,
|
|
9580
|
+
separatorHeight = 0,
|
|
9580
9581
|
/** Width of the track content area (timeline minus labels). Used for accurate time mapping. */
|
|
9581
9582
|
trackContentWidth,
|
|
9582
9583
|
onDrop,
|
|
@@ -9594,7 +9595,9 @@ function useTimelineDrop({
|
|
|
9594
9595
|
const viewportLeft = ((_b = (_a = scrollEl == null ? void 0 : scrollEl.getBoundingClientRect) == null ? void 0 : _a.call(scrollEl)) == null ? void 0 : _b.left) ?? rect.left;
|
|
9595
9596
|
const contentX = clientX - viewportLeft + scrollLeft - labelWidth;
|
|
9596
9597
|
const relY = clientY - rect.top;
|
|
9597
|
-
const
|
|
9598
|
+
const rowHeight = trackHeight + separatorHeight;
|
|
9599
|
+
const yFromFirstTrack = Math.max(0, relY - separatorHeight);
|
|
9600
|
+
const rawTrackIndex = Math.floor(yFromFirstTrack / Math.max(1, rowHeight));
|
|
9598
9601
|
const trackIndex = tracks.length === 0 ? 0 : Math.max(0, Math.min(tracks.length - 1, rawTrackIndex));
|
|
9599
9602
|
const pixelsPerSecond = trackContentWidth != null && trackContentWidth > 0 ? trackContentWidth / duration : 100 * zoomLevel;
|
|
9600
9603
|
const timeSec = Math.max(
|
|
@@ -9609,6 +9612,7 @@ function useTimelineDrop({
|
|
|
9609
9612
|
tracks.length,
|
|
9610
9613
|
labelWidth,
|
|
9611
9614
|
trackHeight,
|
|
9615
|
+
separatorHeight,
|
|
9612
9616
|
zoomLevel,
|
|
9613
9617
|
duration,
|
|
9614
9618
|
trackContentWidth
|
|
@@ -19506,6 +19510,602 @@ const TrackElementContextMenu = ({
|
|
|
19506
19510
|
}
|
|
19507
19511
|
return createPortal(menu, document.body);
|
|
19508
19512
|
};
|
|
19513
|
+
const waveformCache = /* @__PURE__ */ new Map();
|
|
19514
|
+
const buildCacheKey = (src, bucketCount) => `${src}::${bucketCount}`;
|
|
19515
|
+
const getSeed = (src) => {
|
|
19516
|
+
let seed = 2166136261;
|
|
19517
|
+
for (let i2 = 0; i2 < src.length; i2 += 1) {
|
|
19518
|
+
seed ^= src.charCodeAt(i2);
|
|
19519
|
+
seed = Math.imul(seed, 16777619);
|
|
19520
|
+
}
|
|
19521
|
+
return seed >>> 0;
|
|
19522
|
+
};
|
|
19523
|
+
const generateFallbackPeaks = (bucketCount, seed) => {
|
|
19524
|
+
const peaks = new Float32Array(bucketCount);
|
|
19525
|
+
let randomState = seed || 123456789;
|
|
19526
|
+
for (let i2 = 0; i2 < bucketCount; i2 += 1) {
|
|
19527
|
+
randomState = 1103515245 * randomState + 12345 >>> 0;
|
|
19528
|
+
const noise = randomState % 1e3 / 1e3;
|
|
19529
|
+
const shape = 0.35 + 0.65 * Math.abs(Math.sin(i2 * 0.21 + noise * 2.7));
|
|
19530
|
+
peaks[i2] = Math.max(0.08, Math.min(1, shape));
|
|
19531
|
+
}
|
|
19532
|
+
return peaks;
|
|
19533
|
+
};
|
|
19534
|
+
const computePeaks = (channelData, bucketCount) => {
|
|
19535
|
+
const peaks = new Float32Array(bucketCount);
|
|
19536
|
+
const step = Math.max(1, Math.floor(channelData.length / bucketCount));
|
|
19537
|
+
for (let i2 = 0; i2 < bucketCount; i2 += 1) {
|
|
19538
|
+
const start = i2 * step;
|
|
19539
|
+
const end = Math.min(channelData.length, start + step);
|
|
19540
|
+
let peak = 0;
|
|
19541
|
+
for (let j2 = start; j2 < end; j2 += 1) {
|
|
19542
|
+
const sample = Math.abs(channelData[j2]);
|
|
19543
|
+
if (sample > peak) {
|
|
19544
|
+
peak = sample;
|
|
19545
|
+
}
|
|
19546
|
+
}
|
|
19547
|
+
peaks[i2] = peak;
|
|
19548
|
+
}
|
|
19549
|
+
return peaks;
|
|
19550
|
+
};
|
|
19551
|
+
const AudioWaveform = ({ src, widthPx, heightPx, label }) => {
|
|
19552
|
+
const canvasRef = useRef(null);
|
|
19553
|
+
const [peaks, setPeaks] = useState(null);
|
|
19554
|
+
const bucketCount = useMemo(
|
|
19555
|
+
() => Math.max(32, Math.min(2048, Math.floor(widthPx / 3))),
|
|
19556
|
+
[widthPx]
|
|
19557
|
+
);
|
|
19558
|
+
useEffect(() => {
|
|
19559
|
+
let isCancelled = false;
|
|
19560
|
+
const controller = new AbortController();
|
|
19561
|
+
const loadWaveform = async () => {
|
|
19562
|
+
if (!src) {
|
|
19563
|
+
setPeaks(generateFallbackPeaks(bucketCount, 1));
|
|
19564
|
+
return;
|
|
19565
|
+
}
|
|
19566
|
+
const cacheKey = buildCacheKey(src, bucketCount);
|
|
19567
|
+
const cached = waveformCache.get(cacheKey);
|
|
19568
|
+
if (cached) {
|
|
19569
|
+
setPeaks(cached);
|
|
19570
|
+
return;
|
|
19571
|
+
}
|
|
19572
|
+
try {
|
|
19573
|
+
const response = await fetch(src, { signal: controller.signal });
|
|
19574
|
+
if (!response.ok) {
|
|
19575
|
+
throw new Error(`Failed to fetch audio (${response.status})`);
|
|
19576
|
+
}
|
|
19577
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
19578
|
+
const audioCtx = new AudioContext();
|
|
19579
|
+
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer.slice(0));
|
|
19580
|
+
const channelData = audioBuffer.getChannelData(0);
|
|
19581
|
+
const computed = computePeaks(channelData, bucketCount);
|
|
19582
|
+
waveformCache.set(cacheKey, computed);
|
|
19583
|
+
if (!isCancelled) {
|
|
19584
|
+
setPeaks(computed);
|
|
19585
|
+
}
|
|
19586
|
+
await audioCtx.close();
|
|
19587
|
+
} catch {
|
|
19588
|
+
if (!isCancelled) {
|
|
19589
|
+
setPeaks(generateFallbackPeaks(bucketCount, getSeed(src)));
|
|
19590
|
+
}
|
|
19591
|
+
}
|
|
19592
|
+
};
|
|
19593
|
+
loadWaveform();
|
|
19594
|
+
return () => {
|
|
19595
|
+
isCancelled = true;
|
|
19596
|
+
controller.abort();
|
|
19597
|
+
};
|
|
19598
|
+
}, [src, bucketCount]);
|
|
19599
|
+
useEffect(() => {
|
|
19600
|
+
const canvas = canvasRef.current;
|
|
19601
|
+
if (!canvas) {
|
|
19602
|
+
return;
|
|
19603
|
+
}
|
|
19604
|
+
const dpr = window.devicePixelRatio || 1;
|
|
19605
|
+
canvas.width = Math.max(1, Math.floor(widthPx * dpr));
|
|
19606
|
+
canvas.height = Math.max(1, Math.floor(heightPx * dpr));
|
|
19607
|
+
canvas.style.width = `${widthPx}px`;
|
|
19608
|
+
canvas.style.height = `${heightPx}px`;
|
|
19609
|
+
const context = canvas.getContext("2d");
|
|
19610
|
+
if (!context) {
|
|
19611
|
+
return;
|
|
19612
|
+
}
|
|
19613
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
19614
|
+
context.clearRect(0, 0, widthPx, heightPx);
|
|
19615
|
+
const centerY = Math.floor(heightPx / 2) + 0.5;
|
|
19616
|
+
context.strokeStyle = "rgba(255, 255, 255, 0.18)";
|
|
19617
|
+
context.lineWidth = 1;
|
|
19618
|
+
context.beginPath();
|
|
19619
|
+
context.moveTo(0, centerY);
|
|
19620
|
+
context.lineTo(widthPx, centerY);
|
|
19621
|
+
context.stroke();
|
|
19622
|
+
if (!peaks || peaks.length === 0) {
|
|
19623
|
+
return;
|
|
19624
|
+
}
|
|
19625
|
+
const maxHeight = Math.max(6, heightPx - 10);
|
|
19626
|
+
let peakMax = 1e-3;
|
|
19627
|
+
for (let i2 = 0; i2 < peaks.length; i2 += 1) {
|
|
19628
|
+
peakMax = Math.max(peakMax, peaks[i2]);
|
|
19629
|
+
}
|
|
19630
|
+
const gap = 1;
|
|
19631
|
+
const barWidth = Math.max(1, Math.floor(widthPx / peaks.length) - gap);
|
|
19632
|
+
context.fillStyle = "rgba(255, 255, 255, 0.95)";
|
|
19633
|
+
for (let i2 = 0; i2 < peaks.length; i2 += 1) {
|
|
19634
|
+
const normalized = peaks[i2] / peakMax;
|
|
19635
|
+
const waveHeight = Math.max(2, normalized * maxHeight);
|
|
19636
|
+
const x2 = i2 * (barWidth + gap);
|
|
19637
|
+
if (x2 > widthPx) {
|
|
19638
|
+
break;
|
|
19639
|
+
}
|
|
19640
|
+
const y2 = (heightPx - waveHeight) / 2;
|
|
19641
|
+
context.fillRect(x2, y2, barWidth, waveHeight);
|
|
19642
|
+
}
|
|
19643
|
+
}, [widthPx, heightPx, peaks]);
|
|
19644
|
+
return /* @__PURE__ */ jsxs("div", { className: "twick-audio-waveform-root", "aria-label": "Audio waveform", children: [
|
|
19645
|
+
/* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "twick-audio-waveform-canvas" }),
|
|
19646
|
+
label ? /* @__PURE__ */ jsx("span", { className: "twick-audio-waveform-label", children: label }) : null
|
|
19647
|
+
] });
|
|
19648
|
+
};
|
|
19649
|
+
class LRUCache2 {
|
|
19650
|
+
constructor(maxSize = 100) {
|
|
19651
|
+
if (maxSize <= 0) {
|
|
19652
|
+
throw new Error("maxSize must be greater than 0");
|
|
19653
|
+
}
|
|
19654
|
+
this.maxSize = maxSize;
|
|
19655
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
19656
|
+
}
|
|
19657
|
+
/**
|
|
19658
|
+
* Get a value from the cache.
|
|
19659
|
+
* Moves the item to the end (most recently used).
|
|
19660
|
+
*/
|
|
19661
|
+
get(key) {
|
|
19662
|
+
const value = this.cache.get(key);
|
|
19663
|
+
if (value === void 0) {
|
|
19664
|
+
return void 0;
|
|
19665
|
+
}
|
|
19666
|
+
this.cache.delete(key);
|
|
19667
|
+
this.cache.set(key, value);
|
|
19668
|
+
return value;
|
|
19669
|
+
}
|
|
19670
|
+
/**
|
|
19671
|
+
* Set a value in the cache.
|
|
19672
|
+
* If cache is full, removes the least recently used item.
|
|
19673
|
+
*/
|
|
19674
|
+
set(key, value) {
|
|
19675
|
+
if (this.cache.has(key)) {
|
|
19676
|
+
this.cache.delete(key);
|
|
19677
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
19678
|
+
const firstKey = this.cache.keys().next().value;
|
|
19679
|
+
if (firstKey !== void 0) {
|
|
19680
|
+
this.cache.delete(firstKey);
|
|
19681
|
+
}
|
|
19682
|
+
}
|
|
19683
|
+
this.cache.set(key, value);
|
|
19684
|
+
}
|
|
19685
|
+
/**
|
|
19686
|
+
* Check if a key exists in the cache.
|
|
19687
|
+
*/
|
|
19688
|
+
has(key) {
|
|
19689
|
+
return this.cache.has(key);
|
|
19690
|
+
}
|
|
19691
|
+
/**
|
|
19692
|
+
* Delete a key from the cache.
|
|
19693
|
+
*/
|
|
19694
|
+
delete(key) {
|
|
19695
|
+
return this.cache.delete(key);
|
|
19696
|
+
}
|
|
19697
|
+
/**
|
|
19698
|
+
* Clear all entries from the cache.
|
|
19699
|
+
*/
|
|
19700
|
+
clear() {
|
|
19701
|
+
this.cache.clear();
|
|
19702
|
+
}
|
|
19703
|
+
/**
|
|
19704
|
+
* Get the current size of the cache.
|
|
19705
|
+
*/
|
|
19706
|
+
get size() {
|
|
19707
|
+
return this.cache.size;
|
|
19708
|
+
}
|
|
19709
|
+
}
|
|
19710
|
+
class VideoFrameExtractor2 {
|
|
19711
|
+
constructor(options = {}) {
|
|
19712
|
+
this.frameCache = new LRUCache2(
|
|
19713
|
+
options.maxCacheSize ?? 50
|
|
19714
|
+
);
|
|
19715
|
+
this.videoElements = /* @__PURE__ */ new Map();
|
|
19716
|
+
this.maxVideoElements = options.maxVideoElements ?? 5;
|
|
19717
|
+
this.loadTimeout = options.loadTimeout ?? 15e3;
|
|
19718
|
+
this.jpegQuality = options.jpegQuality ?? 0.8;
|
|
19719
|
+
this.playbackRate = options.playbackRate ?? 1;
|
|
19720
|
+
}
|
|
19721
|
+
/**
|
|
19722
|
+
* Get a frame thumbnail from a video at a specific time.
|
|
19723
|
+
* Uses caching and reuses video elements for optimal performance.
|
|
19724
|
+
* Uses 0.1s instead of 0 when seekTime is 0, since frames at t=0 are often blank.
|
|
19725
|
+
*
|
|
19726
|
+
* @param videoUrl - The URL of the video
|
|
19727
|
+
* @param seekTime - The time in seconds to extract the frame (0 is treated as 0.1)
|
|
19728
|
+
* @returns Promise resolving to a thumbnail image URL (data URL or blob URL)
|
|
19729
|
+
*/
|
|
19730
|
+
async getFrame(videoUrl, seekTime = 0.1) {
|
|
19731
|
+
const effectiveSeekTime = seekTime === 0 ? 0.1 : seekTime;
|
|
19732
|
+
const cacheKey = this.getCacheKey(videoUrl, effectiveSeekTime);
|
|
19733
|
+
const cached = this.frameCache.get(cacheKey);
|
|
19734
|
+
if (cached) {
|
|
19735
|
+
return cached;
|
|
19736
|
+
}
|
|
19737
|
+
const videoState = await this.getVideoElement(videoUrl);
|
|
19738
|
+
const thumbnail = await this.extractFrame(videoState.video, effectiveSeekTime);
|
|
19739
|
+
this.frameCache.set(cacheKey, thumbnail);
|
|
19740
|
+
return thumbnail;
|
|
19741
|
+
}
|
|
19742
|
+
/**
|
|
19743
|
+
* Get or create a video element for the given URL.
|
|
19744
|
+
* Reuses existing elements and manages cleanup.
|
|
19745
|
+
*/
|
|
19746
|
+
async getVideoElement(videoUrl) {
|
|
19747
|
+
let videoState = this.videoElements.get(videoUrl);
|
|
19748
|
+
if (videoState && videoState.isReady) {
|
|
19749
|
+
videoState.lastUsed = Date.now();
|
|
19750
|
+
return videoState;
|
|
19751
|
+
}
|
|
19752
|
+
if (videoState && videoState.isLoading && videoState.loadPromise) {
|
|
19753
|
+
await videoState.loadPromise;
|
|
19754
|
+
if (videoState.isReady) {
|
|
19755
|
+
videoState.lastUsed = Date.now();
|
|
19756
|
+
return videoState;
|
|
19757
|
+
}
|
|
19758
|
+
}
|
|
19759
|
+
if (this.videoElements.size >= this.maxVideoElements) {
|
|
19760
|
+
this.cleanupOldVideoElements();
|
|
19761
|
+
}
|
|
19762
|
+
videoState = await this.createVideoElement(videoUrl);
|
|
19763
|
+
this.videoElements.set(videoUrl, videoState);
|
|
19764
|
+
videoState.lastUsed = Date.now();
|
|
19765
|
+
return videoState;
|
|
19766
|
+
}
|
|
19767
|
+
/**
|
|
19768
|
+
* Create and initialize a new video element.
|
|
19769
|
+
*/
|
|
19770
|
+
async createVideoElement(videoUrl) {
|
|
19771
|
+
const video = document.createElement("video");
|
|
19772
|
+
video.crossOrigin = "anonymous";
|
|
19773
|
+
video.muted = true;
|
|
19774
|
+
video.playsInline = true;
|
|
19775
|
+
video.autoplay = false;
|
|
19776
|
+
video.preload = "auto";
|
|
19777
|
+
video.playbackRate = this.playbackRate;
|
|
19778
|
+
video.style.position = "absolute";
|
|
19779
|
+
video.style.left = "-9999px";
|
|
19780
|
+
video.style.top = "-9999px";
|
|
19781
|
+
video.style.width = "1px";
|
|
19782
|
+
video.style.height = "1px";
|
|
19783
|
+
video.style.opacity = "0";
|
|
19784
|
+
video.style.pointerEvents = "none";
|
|
19785
|
+
video.style.zIndex = "-1";
|
|
19786
|
+
const state = {
|
|
19787
|
+
video,
|
|
19788
|
+
isReady: false,
|
|
19789
|
+
isLoading: true,
|
|
19790
|
+
loadPromise: null,
|
|
19791
|
+
lastUsed: Date.now()
|
|
19792
|
+
};
|
|
19793
|
+
state.loadPromise = new Promise((resolve, reject) => {
|
|
19794
|
+
let timeoutId;
|
|
19795
|
+
const cleanup = () => {
|
|
19796
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
19797
|
+
};
|
|
19798
|
+
const handleError = () => {
|
|
19799
|
+
var _a;
|
|
19800
|
+
cleanup();
|
|
19801
|
+
state.isLoading = false;
|
|
19802
|
+
reject(new Error(`Failed to load video: ${((_a = video.error) == null ? void 0 : _a.message) || "Unknown error"}`));
|
|
19803
|
+
};
|
|
19804
|
+
const handleLoadedMetadata = () => {
|
|
19805
|
+
cleanup();
|
|
19806
|
+
state.isReady = true;
|
|
19807
|
+
state.isLoading = false;
|
|
19808
|
+
resolve();
|
|
19809
|
+
};
|
|
19810
|
+
video.addEventListener("error", handleError, { once: true });
|
|
19811
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata, { once: true });
|
|
19812
|
+
timeoutId = window.setTimeout(() => {
|
|
19813
|
+
cleanup();
|
|
19814
|
+
state.isLoading = false;
|
|
19815
|
+
reject(new Error("Video loading timed out"));
|
|
19816
|
+
}, this.loadTimeout);
|
|
19817
|
+
video.src = videoUrl;
|
|
19818
|
+
document.body.appendChild(video);
|
|
19819
|
+
});
|
|
19820
|
+
try {
|
|
19821
|
+
await state.loadPromise;
|
|
19822
|
+
} catch (error) {
|
|
19823
|
+
if (video.parentNode) {
|
|
19824
|
+
video.remove();
|
|
19825
|
+
}
|
|
19826
|
+
throw error;
|
|
19827
|
+
}
|
|
19828
|
+
return state;
|
|
19829
|
+
}
|
|
19830
|
+
/**
|
|
19831
|
+
* Extract a frame from a video at the specified time.
|
|
19832
|
+
*/
|
|
19833
|
+
async extractFrame(video, seekTime) {
|
|
19834
|
+
return new Promise((resolve, reject) => {
|
|
19835
|
+
video.pause();
|
|
19836
|
+
const timeThreshold = 0.1;
|
|
19837
|
+
if (Math.abs(video.currentTime - seekTime) < timeThreshold) {
|
|
19838
|
+
try {
|
|
19839
|
+
const canvas = document.createElement("canvas");
|
|
19840
|
+
const width = video.videoWidth || 640;
|
|
19841
|
+
const height = video.videoHeight || 360;
|
|
19842
|
+
canvas.width = width;
|
|
19843
|
+
canvas.height = height;
|
|
19844
|
+
const ctx = canvas.getContext("2d");
|
|
19845
|
+
if (!ctx) {
|
|
19846
|
+
reject(new Error("Failed to get canvas context"));
|
|
19847
|
+
return;
|
|
19848
|
+
}
|
|
19849
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
19850
|
+
try {
|
|
19851
|
+
const dataUrl = canvas.toDataURL("image/jpeg", this.jpegQuality);
|
|
19852
|
+
resolve(dataUrl);
|
|
19853
|
+
} catch {
|
|
19854
|
+
canvas.toBlob(
|
|
19855
|
+
(blob) => {
|
|
19856
|
+
if (!blob) {
|
|
19857
|
+
reject(new Error("Failed to create Blob"));
|
|
19858
|
+
return;
|
|
19859
|
+
}
|
|
19860
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
19861
|
+
resolve(blobUrl);
|
|
19862
|
+
},
|
|
19863
|
+
"image/jpeg",
|
|
19864
|
+
this.jpegQuality
|
|
19865
|
+
);
|
|
19866
|
+
}
|
|
19867
|
+
return;
|
|
19868
|
+
} catch (err) {
|
|
19869
|
+
reject(new Error(`Error creating thumbnail: ${err}`));
|
|
19870
|
+
return;
|
|
19871
|
+
}
|
|
19872
|
+
}
|
|
19873
|
+
const handleSeeked = () => {
|
|
19874
|
+
try {
|
|
19875
|
+
const canvas = document.createElement("canvas");
|
|
19876
|
+
const width = video.videoWidth || 640;
|
|
19877
|
+
const height = video.videoHeight || 360;
|
|
19878
|
+
canvas.width = width;
|
|
19879
|
+
canvas.height = height;
|
|
19880
|
+
const ctx = canvas.getContext("2d");
|
|
19881
|
+
if (!ctx) {
|
|
19882
|
+
reject(new Error("Failed to get canvas context"));
|
|
19883
|
+
return;
|
|
19884
|
+
}
|
|
19885
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
19886
|
+
try {
|
|
19887
|
+
const dataUrl = canvas.toDataURL("image/jpeg", this.jpegQuality);
|
|
19888
|
+
resolve(dataUrl);
|
|
19889
|
+
} catch {
|
|
19890
|
+
canvas.toBlob(
|
|
19891
|
+
(blob) => {
|
|
19892
|
+
if (!blob) {
|
|
19893
|
+
reject(new Error("Failed to create Blob"));
|
|
19894
|
+
return;
|
|
19895
|
+
}
|
|
19896
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
19897
|
+
resolve(blobUrl);
|
|
19898
|
+
},
|
|
19899
|
+
"image/jpeg",
|
|
19900
|
+
this.jpegQuality
|
|
19901
|
+
);
|
|
19902
|
+
}
|
|
19903
|
+
} catch (err) {
|
|
19904
|
+
reject(new Error(`Error creating thumbnail: ${err}`));
|
|
19905
|
+
}
|
|
19906
|
+
};
|
|
19907
|
+
video.addEventListener("seeked", handleSeeked, { once: true });
|
|
19908
|
+
const playPromise = video.play();
|
|
19909
|
+
if (playPromise !== void 0) {
|
|
19910
|
+
playPromise.then(() => {
|
|
19911
|
+
video.currentTime = seekTime;
|
|
19912
|
+
}).catch(() => {
|
|
19913
|
+
video.currentTime = seekTime;
|
|
19914
|
+
});
|
|
19915
|
+
} else {
|
|
19916
|
+
video.currentTime = seekTime;
|
|
19917
|
+
}
|
|
19918
|
+
});
|
|
19919
|
+
}
|
|
19920
|
+
/**
|
|
19921
|
+
* Generate cache key for a video URL and seek time.
|
|
19922
|
+
*/
|
|
19923
|
+
getCacheKey(videoUrl, seekTime) {
|
|
19924
|
+
const roundedTime = Math.round(seekTime * 100) / 100;
|
|
19925
|
+
return `${videoUrl}:${roundedTime}`;
|
|
19926
|
+
}
|
|
19927
|
+
/**
|
|
19928
|
+
* Cleanup least recently used video elements.
|
|
19929
|
+
*/
|
|
19930
|
+
cleanupOldVideoElements() {
|
|
19931
|
+
if (this.videoElements.size < this.maxVideoElements) {
|
|
19932
|
+
return;
|
|
19933
|
+
}
|
|
19934
|
+
const entries = Array.from(this.videoElements.entries());
|
|
19935
|
+
entries.sort((a2, b2) => a2[1].lastUsed - b2[1].lastUsed);
|
|
19936
|
+
const toRemove = entries.slice(0, entries.length - this.maxVideoElements + 1);
|
|
19937
|
+
for (const [url, state] of toRemove) {
|
|
19938
|
+
if (state.video.parentNode) {
|
|
19939
|
+
state.video.remove();
|
|
19940
|
+
}
|
|
19941
|
+
this.videoElements.delete(url);
|
|
19942
|
+
}
|
|
19943
|
+
}
|
|
19944
|
+
/**
|
|
19945
|
+
* Clear the frame cache.
|
|
19946
|
+
*/
|
|
19947
|
+
clearCache() {
|
|
19948
|
+
this.frameCache.clear();
|
|
19949
|
+
}
|
|
19950
|
+
/**
|
|
19951
|
+
* Remove a specific video element and clear its cached frames.
|
|
19952
|
+
*/
|
|
19953
|
+
removeVideo(videoUrl) {
|
|
19954
|
+
const state = this.videoElements.get(videoUrl);
|
|
19955
|
+
if (state) {
|
|
19956
|
+
if (state.video.parentNode) {
|
|
19957
|
+
state.video.remove();
|
|
19958
|
+
}
|
|
19959
|
+
this.videoElements.delete(videoUrl);
|
|
19960
|
+
}
|
|
19961
|
+
this.frameCache.clear();
|
|
19962
|
+
}
|
|
19963
|
+
/**
|
|
19964
|
+
* Dispose of all video elements and clear caches.
|
|
19965
|
+
* Removes all video elements from the DOM and clears both the frame cache
|
|
19966
|
+
* and video element cache. Call this when the extractor is no longer needed
|
|
19967
|
+
* to prevent memory leaks.
|
|
19968
|
+
*/
|
|
19969
|
+
dispose() {
|
|
19970
|
+
for (const state of this.videoElements.values()) {
|
|
19971
|
+
if (state.video.parentNode) {
|
|
19972
|
+
state.video.remove();
|
|
19973
|
+
}
|
|
19974
|
+
}
|
|
19975
|
+
this.videoElements.clear();
|
|
19976
|
+
this.frameCache.clear();
|
|
19977
|
+
}
|
|
19978
|
+
}
|
|
19979
|
+
let defaultExtractor = null;
|
|
19980
|
+
function getDefaultVideoFrameExtractor(options) {
|
|
19981
|
+
if (!defaultExtractor) {
|
|
19982
|
+
defaultExtractor = new VideoFrameExtractor2(options);
|
|
19983
|
+
}
|
|
19984
|
+
return defaultExtractor;
|
|
19985
|
+
}
|
|
19986
|
+
async function getThumbnailCached(videoUrl, seekTime = 0.1, playbackRate) {
|
|
19987
|
+
const extractor = getDefaultVideoFrameExtractor(
|
|
19988
|
+
void 0
|
|
19989
|
+
);
|
|
19990
|
+
return extractor.getFrame(videoUrl, seekTime);
|
|
19991
|
+
}
|
|
19992
|
+
const MAX_THUMBNAILS_PER_CLIP = 24;
|
|
19993
|
+
const MIN_THUMBNAIL_WIDTH = 40;
|
|
19994
|
+
const MAX_THUMBNAIL_WIDTH = 96;
|
|
19995
|
+
const videoThumbCache = /* @__PURE__ */ new Map();
|
|
19996
|
+
const inFlightVideoThumbs = /* @__PURE__ */ new Map();
|
|
19997
|
+
const getThumbCacheKey = (src, seekTime) => `${src}:${Math.round(seekTime * 100) / 100}`;
|
|
19998
|
+
const getCachedVideoThumbnail = async (src, seekTime) => {
|
|
19999
|
+
const key = getThumbCacheKey(src, seekTime);
|
|
20000
|
+
const cached = videoThumbCache.get(key);
|
|
20001
|
+
if (cached) {
|
|
20002
|
+
return cached;
|
|
20003
|
+
}
|
|
20004
|
+
const inFlight = inFlightVideoThumbs.get(key);
|
|
20005
|
+
if (inFlight) {
|
|
20006
|
+
return inFlight;
|
|
20007
|
+
}
|
|
20008
|
+
const request = getThumbnailCached(src, seekTime).then((thumb) => {
|
|
20009
|
+
videoThumbCache.set(key, thumb);
|
|
20010
|
+
inFlightVideoThumbs.delete(key);
|
|
20011
|
+
return thumb;
|
|
20012
|
+
}).catch(() => {
|
|
20013
|
+
inFlightVideoThumbs.delete(key);
|
|
20014
|
+
throw new Error("Thumbnail extraction failed");
|
|
20015
|
+
});
|
|
20016
|
+
inFlightVideoThumbs.set(key, request);
|
|
20017
|
+
return request;
|
|
20018
|
+
};
|
|
20019
|
+
const runWithConcurrencyLimit = async (tasks, concurrency) => {
|
|
20020
|
+
const active = /* @__PURE__ */ new Set();
|
|
20021
|
+
for (const task of tasks) {
|
|
20022
|
+
const promise = task();
|
|
20023
|
+
active.add(promise);
|
|
20024
|
+
promise.finally(() => active.delete(promise));
|
|
20025
|
+
if (active.size >= concurrency) {
|
|
20026
|
+
await Promise.race(active);
|
|
20027
|
+
}
|
|
20028
|
+
}
|
|
20029
|
+
await Promise.all(active);
|
|
20030
|
+
};
|
|
20031
|
+
const getThumbnailCount = (widthPx, heightPx) => {
|
|
20032
|
+
const targetThumbWidth = Math.max(
|
|
20033
|
+
MIN_THUMBNAIL_WIDTH,
|
|
20034
|
+
Math.min(MAX_THUMBNAIL_WIDTH, Math.round(heightPx * 1.5))
|
|
20035
|
+
);
|
|
20036
|
+
return Math.max(
|
|
20037
|
+
1,
|
|
20038
|
+
Math.min(MAX_THUMBNAILS_PER_CLIP, Math.ceil(widthPx / targetThumbWidth))
|
|
20039
|
+
);
|
|
20040
|
+
};
|
|
20041
|
+
const ImageTimelineStrip = ({ src, widthPx, heightPx }) => {
|
|
20042
|
+
const count = useMemo(() => getThumbnailCount(widthPx, heightPx), [widthPx, heightPx]);
|
|
20043
|
+
return /* @__PURE__ */ jsx("div", { className: "twick-media-strip twick-media-strip-image", "aria-hidden": "true", children: Array.from({ length: count }, (_2, index) => /* @__PURE__ */ jsx(
|
|
20044
|
+
"img",
|
|
20045
|
+
{
|
|
20046
|
+
src,
|
|
20047
|
+
className: "twick-media-strip-thumb",
|
|
20048
|
+
alt: "",
|
|
20049
|
+
draggable: false
|
|
20050
|
+
},
|
|
20051
|
+
`${src}-${index}`
|
|
20052
|
+
)) });
|
|
20053
|
+
};
|
|
20054
|
+
const VideoTimelineStrip = ({
|
|
20055
|
+
src,
|
|
20056
|
+
widthPx,
|
|
20057
|
+
heightPx,
|
|
20058
|
+
durationSec,
|
|
20059
|
+
mediaOffsetSec = 0,
|
|
20060
|
+
playbackRate = 1,
|
|
20061
|
+
mediaDurationSec
|
|
20062
|
+
}) => {
|
|
20063
|
+
const count = useMemo(() => getThumbnailCount(widthPx, heightPx), [widthPx, heightPx]);
|
|
20064
|
+
const [thumbs, setThumbs] = useState({});
|
|
20065
|
+
const slots = useMemo(() => {
|
|
20066
|
+
const timelineDuration = Math.max(1e-3, durationSec);
|
|
20067
|
+
return Array.from({ length: count }, (_2, index) => {
|
|
20068
|
+
const progress2 = count === 1 ? 0 : index / (count - 1);
|
|
20069
|
+
const timelineTime = progress2 * timelineDuration;
|
|
20070
|
+
let mediaTime = Math.max(0, mediaOffsetSec + timelineTime * playbackRate);
|
|
20071
|
+
if (typeof mediaDurationSec === "number" && mediaDurationSec > 0) {
|
|
20072
|
+
mediaTime = Math.min(mediaTime, Math.max(0, mediaDurationSec - 0.05));
|
|
20073
|
+
}
|
|
20074
|
+
return mediaTime;
|
|
20075
|
+
});
|
|
20076
|
+
}, [count, durationSec, mediaOffsetSec, playbackRate, mediaDurationSec]);
|
|
20077
|
+
useEffect(() => {
|
|
20078
|
+
let cancelled = false;
|
|
20079
|
+
const loadThumbs = async () => {
|
|
20080
|
+
const nextThumbs = {};
|
|
20081
|
+
const tasks = slots.map((seekTime, index) => async () => {
|
|
20082
|
+
try {
|
|
20083
|
+
const thumb = await getCachedVideoThumbnail(src, seekTime);
|
|
20084
|
+
nextThumbs[index] = thumb;
|
|
20085
|
+
} catch {
|
|
20086
|
+
}
|
|
20087
|
+
});
|
|
20088
|
+
await runWithConcurrencyLimit(tasks, 3);
|
|
20089
|
+
if (!cancelled) {
|
|
20090
|
+
setThumbs(nextThumbs);
|
|
20091
|
+
}
|
|
20092
|
+
};
|
|
20093
|
+
loadThumbs();
|
|
20094
|
+
return () => {
|
|
20095
|
+
cancelled = true;
|
|
20096
|
+
};
|
|
20097
|
+
}, [src, slots]);
|
|
20098
|
+
return /* @__PURE__ */ jsx("div", { className: "twick-media-strip twick-media-strip-video", "aria-hidden": "true", children: slots.map((_2, index) => /* @__PURE__ */ jsx(
|
|
20099
|
+
"img",
|
|
20100
|
+
{
|
|
20101
|
+
src: thumbs[index],
|
|
20102
|
+
className: "twick-media-strip-thumb",
|
|
20103
|
+
alt: "",
|
|
20104
|
+
draggable: false
|
|
20105
|
+
},
|
|
20106
|
+
`${src}-${index}`
|
|
20107
|
+
)) });
|
|
20108
|
+
};
|
|
19509
20109
|
const TrackElementView = ({
|
|
19510
20110
|
element,
|
|
19511
20111
|
parentWidth,
|
|
@@ -19656,6 +20256,18 @@ const TrackElementView = ({
|
|
|
19656
20256
|
const isSelected = useMemo(() => {
|
|
19657
20257
|
return selectedIds.has(element.getId());
|
|
19658
20258
|
}, [selectedIds, element]);
|
|
20259
|
+
const isAudioElement = element.getType() === TIMELINE_ELEMENT_TYPE.AUDIO;
|
|
20260
|
+
const isVideoElement = element.getType() === TIMELINE_ELEMENT_TYPE.VIDEO;
|
|
20261
|
+
const isImageElement = element.getType() === TIMELINE_ELEMENT_TYPE.IMAGE;
|
|
20262
|
+
const elementLabel = element.getType() === TIMELINE_ELEMENT_TYPE.EFFECT ? ((_b = (_a = element.getProps) == null ? void 0 : _a.call(element)) == null ? void 0 : _b.effectKey) ?? "Effect" : element.getText ? element.getText() : element.getName() || element.getType();
|
|
20263
|
+
const mediaSrc = (isAudioElement || isVideoElement || isImageElement) && element.getSrc ? element.getSrc() : void 0;
|
|
20264
|
+
const mediaOffsetSec = isVideoElement && element.getStartAt ? element.getStartAt() : 0;
|
|
20265
|
+
const playbackRate = isVideoElement && element.getPlaybackRate ? element.getPlaybackRate() : 1;
|
|
20266
|
+
const mediaDurationSec = isVideoElement && element.getMediaDuration ? element.getMediaDuration() : void 0;
|
|
20267
|
+
const elementWidthPx = Math.max(
|
|
20268
|
+
1,
|
|
20269
|
+
(position.end - position.start) / Math.max(duration, MIN_DURATION) * parentWidth
|
|
20270
|
+
);
|
|
19659
20271
|
const hasHandles = (selectedItem == null ? void 0 : selectedItem.getId()) === element.getId();
|
|
19660
20272
|
const contextActionsEnabled = Boolean(
|
|
19661
20273
|
onDeleteElement && onSplitElement && onContextMenuTarget
|
|
@@ -19669,7 +20281,7 @@ const TrackElementView = ({
|
|
|
19669
20281
|
};
|
|
19670
20282
|
const motionProps = {
|
|
19671
20283
|
ref,
|
|
19672
|
-
className: `twick-track-element ${isSelected ? "twick-track-element-selected" : "twick-track-element-default"} ${isDragging2 ? "twick-track-element-dragging" : ""}`,
|
|
20284
|
+
className: `twick-track-element ${isSelected ? "twick-track-element-selected" : "twick-track-element-default"} ${isDragging2 ? "twick-track-element-dragging" : ""} ${isAudioElement ? "twick-track-element-audio" : ""}`,
|
|
19673
20285
|
onMouseDown: (e3) => {
|
|
19674
20286
|
if (e3.target === ref.current) {
|
|
19675
20287
|
setLastPos();
|
|
@@ -19716,7 +20328,39 @@ const TrackElementView = ({
|
|
|
19716
20328
|
className: "twick-track-element-handle twick-track-element-handle-start"
|
|
19717
20329
|
}
|
|
19718
20330
|
) : null,
|
|
19719
|
-
/* @__PURE__ */ jsx(
|
|
20331
|
+
/* @__PURE__ */ jsx(
|
|
20332
|
+
"div",
|
|
20333
|
+
{
|
|
20334
|
+
className: `twick-track-element-content ${isAudioElement ? "twick-track-element-content-audio" : ""}`,
|
|
20335
|
+
children: isAudioElement ? /* @__PURE__ */ jsx(
|
|
20336
|
+
AudioWaveform,
|
|
20337
|
+
{
|
|
20338
|
+
src: mediaSrc,
|
|
20339
|
+
widthPx: elementWidthPx,
|
|
20340
|
+
heightPx: 46,
|
|
20341
|
+
label: elementLabel
|
|
20342
|
+
}
|
|
20343
|
+
) : isVideoElement && mediaSrc ? /* @__PURE__ */ jsx(
|
|
20344
|
+
VideoTimelineStrip,
|
|
20345
|
+
{
|
|
20346
|
+
src: mediaSrc,
|
|
20347
|
+
widthPx: elementWidthPx,
|
|
20348
|
+
heightPx: 46,
|
|
20349
|
+
durationSec: Math.max(0, element.getDuration()),
|
|
20350
|
+
mediaOffsetSec,
|
|
20351
|
+
playbackRate,
|
|
20352
|
+
mediaDurationSec
|
|
20353
|
+
}
|
|
20354
|
+
) : isImageElement && mediaSrc ? /* @__PURE__ */ jsx(
|
|
20355
|
+
ImageTimelineStrip,
|
|
20356
|
+
{
|
|
20357
|
+
src: mediaSrc,
|
|
20358
|
+
widthPx: elementWidthPx,
|
|
20359
|
+
heightPx: 46
|
|
20360
|
+
}
|
|
20361
|
+
) : elementLabel
|
|
20362
|
+
}
|
|
20363
|
+
),
|
|
19720
20364
|
hasHandles ? /* @__PURE__ */ jsx(
|
|
19721
20365
|
"div",
|
|
19722
20366
|
{
|
|
@@ -19856,6 +20500,7 @@ function useMarqueeSelection({
|
|
|
19856
20500
|
labelWidth,
|
|
19857
20501
|
trackCount,
|
|
19858
20502
|
trackHeight,
|
|
20503
|
+
separatorHeight = 2,
|
|
19859
20504
|
tracks,
|
|
19860
20505
|
containerRef,
|
|
19861
20506
|
onMarqueeSelect,
|
|
@@ -19909,7 +20554,7 @@ function useMarqueeSelection({
|
|
|
19909
20554
|
const bottom = Math.max(currentMarquee.startY, currentMarquee.endY);
|
|
19910
20555
|
const startTime = Math.max(0, (left - labelWidth) / pixelsPerSecond);
|
|
19911
20556
|
const endTime = Math.min(duration, (right - labelWidth) / pixelsPerSecond);
|
|
19912
|
-
const rowHeight = trackHeight +
|
|
20557
|
+
const rowHeight = trackHeight + separatorHeight;
|
|
19913
20558
|
const startTrackIdx = Math.max(0, Math.floor(top / rowHeight));
|
|
19914
20559
|
const endTrackIdx = Math.min(
|
|
19915
20560
|
trackCount - 1,
|
|
@@ -19938,6 +20583,7 @@ function useMarqueeSelection({
|
|
|
19938
20583
|
labelWidth,
|
|
19939
20584
|
trackCount,
|
|
19940
20585
|
trackHeight,
|
|
20586
|
+
separatorHeight,
|
|
19941
20587
|
tracks,
|
|
19942
20588
|
onMarqueeSelect,
|
|
19943
20589
|
onEmptyClick,
|
|
@@ -20074,7 +20720,7 @@ function getTrackOrSeparatorAt(clientY, containerTop, trackHeight) {
|
|
|
20074
20720
|
return { type: "separator", separatorIndex: index + 1 };
|
|
20075
20721
|
}
|
|
20076
20722
|
const LABEL_WIDTH = 40;
|
|
20077
|
-
const TRACK_HEIGHT =
|
|
20723
|
+
const TRACK_HEIGHT = 52;
|
|
20078
20724
|
const SEPARATOR_HEIGHT = 6;
|
|
20079
20725
|
function TimelineView({
|
|
20080
20726
|
zoomLevel,
|
|
@@ -20217,6 +20863,7 @@ function TimelineView({
|
|
|
20217
20863
|
labelWidth: LABEL_WIDTH,
|
|
20218
20864
|
trackCount: (tracks == null ? void 0 : tracks.length) ?? 0,
|
|
20219
20865
|
trackHeight: TRACK_HEIGHT,
|
|
20866
|
+
separatorHeight: SEPARATOR_HEIGHT,
|
|
20220
20867
|
tracks: tracks ?? [],
|
|
20221
20868
|
containerRef: timelineContentRef,
|
|
20222
20869
|
onMarqueeSelect,
|
|
@@ -20230,6 +20877,7 @@ function TimelineView({
|
|
|
20230
20877
|
zoomLevel,
|
|
20231
20878
|
labelWidth: LABEL_WIDTH,
|
|
20232
20879
|
trackHeight: TRACK_HEIGHT,
|
|
20880
|
+
separatorHeight: SEPARATOR_HEIGHT,
|
|
20233
20881
|
trackContentWidth: timelineWidth - LABEL_WIDTH,
|
|
20234
20882
|
onDrop: onDropOnTimeline ?? (async () => {
|
|
20235
20883
|
}),
|
|
@@ -20325,7 +20973,7 @@ function TimelineView({
|
|
|
20325
20973
|
style: {
|
|
20326
20974
|
position: "absolute",
|
|
20327
20975
|
left: LABEL_WIDTH + preview.timeSec / duration * (timelineWidth - LABEL_WIDTH),
|
|
20328
|
-
top: preview.trackIndex * TRACK_HEIGHT + 2,
|
|
20976
|
+
top: SEPARATOR_HEIGHT + preview.trackIndex * (TRACK_HEIGHT + SEPARATOR_HEIGHT) + 2,
|
|
20329
20977
|
width: preview.widthPct / 100 * (timelineWidth - LABEL_WIDTH),
|
|
20330
20978
|
height: TRACK_HEIGHT - 4
|
|
20331
20979
|
}
|
|
@@ -21168,11 +21816,13 @@ const useEditorManager = () => {
|
|
|
21168
21816
|
class BaseMediaManager {
|
|
21169
21817
|
}
|
|
21170
21818
|
class BrowserMediaManager extends BaseMediaManager {
|
|
21171
|
-
constructor() {
|
|
21172
|
-
super(
|
|
21173
|
-
__publicField(this, "dbName"
|
|
21174
|
-
__publicField(this, "storeName"
|
|
21819
|
+
constructor(options) {
|
|
21820
|
+
super();
|
|
21821
|
+
__publicField(this, "dbName");
|
|
21822
|
+
__publicField(this, "storeName");
|
|
21175
21823
|
__publicField(this, "db", null);
|
|
21824
|
+
this.dbName = (options == null ? void 0 : options.dbName) ?? "mediaStore";
|
|
21825
|
+
this.storeName = (options == null ? void 0 : options.storeName) ?? "mediaItems";
|
|
21176
21826
|
}
|
|
21177
21827
|
async initDB() {
|
|
21178
21828
|
if (this.db) return this.db;
|
|
@@ -21197,6 +21847,36 @@ class BrowserMediaManager extends BaseMediaManager {
|
|
|
21197
21847
|
const transaction = db.transaction(this.storeName, mode);
|
|
21198
21848
|
return transaction.objectStore(this.storeName);
|
|
21199
21849
|
}
|
|
21850
|
+
/**
|
|
21851
|
+
* Clears all items from the object store within this manager's database.
|
|
21852
|
+
* This is scoped to the manager's dbName/storeName (safe for multi-tenant namespaces).
|
|
21853
|
+
*/
|
|
21854
|
+
async clearStore() {
|
|
21855
|
+
const store = await this.getStore("readwrite");
|
|
21856
|
+
await new Promise((resolve, reject) => {
|
|
21857
|
+
const request = store.clear();
|
|
21858
|
+
request.onsuccess = () => resolve();
|
|
21859
|
+
request.onerror = () => reject(request.error);
|
|
21860
|
+
});
|
|
21861
|
+
}
|
|
21862
|
+
/**
|
|
21863
|
+
* Deletes the entire IndexedDB database used by this manager.
|
|
21864
|
+
* Note: Any other tabs/instances using the same dbName may be affected.
|
|
21865
|
+
*/
|
|
21866
|
+
async dropDatabase() {
|
|
21867
|
+
if (this.db) {
|
|
21868
|
+
this.db.close();
|
|
21869
|
+
this.db = null;
|
|
21870
|
+
}
|
|
21871
|
+
await new Promise((resolve, reject) => {
|
|
21872
|
+
const request = indexedDB.deleteDatabase(this.dbName);
|
|
21873
|
+
request.onsuccess = () => resolve();
|
|
21874
|
+
request.onerror = () => reject(request.error);
|
|
21875
|
+
request.onblocked = () => {
|
|
21876
|
+
reject(new Error(`Failed to delete IndexedDB database "${this.dbName}" (blocked)`));
|
|
21877
|
+
};
|
|
21878
|
+
});
|
|
21879
|
+
}
|
|
21200
21880
|
async convertArrayBufferToBlob(arrayBuffer, type) {
|
|
21201
21881
|
return new Blob([arrayBuffer], { type });
|
|
21202
21882
|
}
|