@linker-design-plus/timeline-track 2.1.1 → 2.1.3

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/README.md CHANGED
@@ -135,6 +135,7 @@ pnpm install
135
135
  pnpm dev
136
136
  pnpm build
137
137
  pnpm test
138
+ pnpm harness:test
138
139
  pnpm exec tsc -p tsconfig.json --noEmit
139
140
  ```
140
141
 
@@ -146,3 +147,4 @@ pnpm exec tsc -p tsconfig.json --noEmit
146
147
  - [docs/resource-cache.md](./docs/resource-cache.md): 预览资源缓存的能力边界与配置
147
148
  - [docs/interaction-model.md](./docs/interaction-model.md): 指针交互分层和拖拽约束
148
149
  - [docs/preview-playback-recovery-flow.md](./docs/preview-playback-recovery-flow.md): 预览媒体时钟驱动播放、buffering 阻塞与恢复续播流程
150
+ - [docs/testing-harness-roadmap.md](./docs/testing-harness-roadmap.md): Harness Engineering 导向的测试与诊断重构方案
@@ -12,6 +12,7 @@ export { TimelinePreviewRuntimeController } from './timelinePreviewRuntimeContro
12
12
  export { TimelinePreviewStateController, type TimelinePendingPreviewState } from './timelinePreviewStateController';
13
13
  export { TimelineKeyboardShortcutsController, type TimelineKeyboardShortcutsControllerCallbacks } from './timelineKeyboardShortcutsController';
14
14
  export * from './timelineSelectionController';
15
+ export { TimelineStructureSession, type TimelineStructureSessionSummary } from './timelineStructureSession';
15
16
  export * from './timelineTrackMutationController';
16
17
  export { TimelineClipConfigController } from './timelineClipConfigController';
17
18
  export { TimelineTrackInfoPanelController } from './timelineTrackInfoPanelController';
@@ -9,6 +9,7 @@ export interface TimelinePreviewSyncPayload {
9
9
  autoAspectRatioClip: ActiveClipPlaybackInfo | null;
10
10
  currentTime: TimeMs;
11
11
  playState: PlayState;
12
+ requestedPlayState?: PlayState;
12
13
  speed: number;
13
14
  interactionMode?: 'steady' | 'seek' | 'scrub';
14
15
  primarySelectedClipId?: string | null;
@@ -11,7 +11,10 @@ export interface ResolvedPlaybackPlan {
11
11
  nextClips: ActiveClipPlaybackInfo[];
12
12
  firstVideoClip: ActiveClipPlaybackInfo | null;
13
13
  }
14
+ export interface ResolvePlaybackPlanOptions {
15
+ includeEndedClipsAtTime?: boolean;
16
+ }
14
17
  export declare class TimelinePlaybackResolver {
15
18
  resolveActiveClipsAtTime(tracks: PlaybackTrackSnapshot[], time: TimeMs): ActiveClipPlaybackInfo[];
16
- resolvePlaybackPlan(tracks: PlaybackTrackSnapshot[], time: TimeMs): ResolvedPlaybackPlan;
19
+ resolvePlaybackPlan(tracks: PlaybackTrackSnapshot[], time: TimeMs, options?: ResolvePlaybackPlanOptions): ResolvedPlaybackPlan;
17
20
  }
@@ -35,6 +35,7 @@ export interface TimelinePreviewSyncPayload {
35
35
  autoAspectRatioClip: ActiveClipPlaybackInfo | null;
36
36
  currentTime: TimeMs;
37
37
  playState: PlayState;
38
+ requestedPlayState?: PlayState;
38
39
  speed: number;
39
40
  interactionMode?: 'steady' | 'seek' | 'scrub';
40
41
  primarySelectedClipId?: string | null;
@@ -146,6 +147,8 @@ export declare class TimelinePreviewSession {
146
147
  private resetSlotPlaybackProgressProbe;
147
148
  private updateSlotPlaybackProgressState;
148
149
  private maybeResumeRequestedPlayback;
150
+ private canRuntimeCheckRetryBufferedPlayback;
151
+ private shouldClearRecoveredBufferingSignal;
149
152
  private preparePreloadSlot;
150
153
  private setSlotVisible;
151
154
  private getAudioContext;
@@ -201,6 +204,7 @@ export declare class TimelinePreviewSession {
201
204
  private ensureTextPreviewFontReady;
202
205
  private resetTextPreviewFontState;
203
206
  private refreshSlotVisualLayout;
207
+ private syncSlotFailureVisualState;
204
208
  private buildSelectedOverlayState;
205
209
  private getFrameSize;
206
210
  private getDocument;
@@ -0,0 +1,66 @@
1
+ import type { Clip, Track, TrackRestoreAnchor } from '../models/types';
2
+ import type { TimelineTrackCollection, TimelineTrackCollectionItem } from '../tracks/timelineTrackCollection';
3
+ import type { TimelineHistoryExecutionTarget } from '../history/timelineHistoryExecutor';
4
+ export interface TimelineStructureSessionEvent {
5
+ type: 'clip_added' | 'clip_removed' | 'clip_updated';
6
+ payload: Record<string, unknown>;
7
+ }
8
+ export interface TimelineStructureSessionSummary {
9
+ cleanupEmptyTracks: boolean;
10
+ durationDirty: boolean;
11
+ canPlayDirty: boolean;
12
+ selectionDirty: boolean;
13
+ trackLayoutDirty: boolean;
14
+ trackInfoDirty: boolean;
15
+ stageDirty: boolean;
16
+ previewDirty: boolean;
17
+ events: TimelineStructureSessionEvent[];
18
+ }
19
+ interface TimelineStructureSessionOptions {
20
+ trackCollection: TimelineTrackCollection;
21
+ ensureTrackFromHistorySnapshot: (trackSnapshot: Track, restoreAnchor?: TrackRestoreAnchor | null) => string | null;
22
+ removeClipGaps: () => void;
23
+ loadClipThumbnails?: (clip: Clip) => Promise<boolean> | void;
24
+ addClipToTrack?: (trackId: string, clip: Clip) => boolean;
25
+ removeClip?: (clipId: string) => void;
26
+ removeClipFromTrack?: (trackId: string, clipId: string) => boolean;
27
+ updateClip?: (clipId: string, updates: Partial<Clip>) => void;
28
+ moveClipToTrack?: (clip: Clip, sourceTrackId: string, targetTrackId: string) => boolean;
29
+ getClips?: () => Clip[];
30
+ findTrackById?: (trackId: string) => TimelineTrackCollectionItem | null;
31
+ findTrackByClipId?: (clipId: string) => TimelineTrackCollectionItem | null;
32
+ getDefaultTrack?: () => TimelineTrackCollectionItem | null;
33
+ onCommit: (summary: TimelineStructureSessionSummary) => void;
34
+ }
35
+ export declare class TimelineStructureSession {
36
+ private readonly options;
37
+ private cleanupEmptyTracks;
38
+ private durationDirty;
39
+ private canPlayDirty;
40
+ private selectionDirty;
41
+ private trackLayoutDirty;
42
+ private trackInfoDirty;
43
+ private stageDirty;
44
+ private previewDirty;
45
+ private committed;
46
+ private readonly events;
47
+ constructor(options: TimelineStructureSessionOptions);
48
+ addClipToTrack(trackId: string, clip: Clip): boolean;
49
+ removeClip(clipId: string): void;
50
+ removeClipFromTrack(trackId: string, clipId: string): boolean;
51
+ updateClip(clipId: string, updates: Partial<Clip>): void;
52
+ moveClipToTrack(clipId: string, targetTrackId: string): boolean;
53
+ ensureTrackFromHistorySnapshot(trackSnapshot: Track, restoreAnchor?: TrackRestoreAnchor | null): string | null;
54
+ removeClipGaps(): void;
55
+ getClips(): Clip[];
56
+ findTrackByClipId(clipId: string): TimelineTrackCollectionItem | null;
57
+ getDefaultTrack(): TimelineTrackCollectionItem | null;
58
+ queueEvent(type: TimelineStructureSessionEvent['type'], payload: Record<string, unknown>): void;
59
+ markStructureMutation(): void;
60
+ markClipStateMutation(): void;
61
+ markTrackLayoutMutation(): void;
62
+ commit(): void;
63
+ createHistoryExecutionTarget(): TimelineHistoryExecutionTarget;
64
+ private resolveTrackId;
65
+ }
66
+ export {};
@@ -49,7 +49,6 @@ export declare class TimelineManager {
49
49
  private sourceLoadingCount;
50
50
  private timelineStore?;
51
51
  private timelineCommands?;
52
- private timelineHistoryExecutor?;
53
52
  private timelineHistoryRecorder?;
54
53
  private timelinePresentationAdapter?;
55
54
  private timelineTrackBridge?;
@@ -74,7 +73,10 @@ export declare class TimelineManager {
74
73
  private splitOperationDepth;
75
74
  private readonly diagnostics;
76
75
  private previewRuntimeState;
76
+ private failedPreviewClipIds?;
77
77
  private previewActiveClipIds;
78
+ private previewClockSeekAlignmentTargetTime;
79
+ private previewClockSeekAlignmentDeadlineMs;
78
80
  private playbackAttemptId;
79
81
  private lastPreviewSyncedPlayState;
80
82
  private lastPreviewSyncSignature;
@@ -84,6 +86,9 @@ export declare class TimelineManager {
84
86
  private voiceLinkedTextRegenerationTimer;
85
87
  private pendingVoiceLinkedTextRegeneration;
86
88
  private historyRecordingSuppressionDepth;
89
+ private activeStructureSession;
90
+ private structureSessionDepth;
91
+ private preserveEmptyTracksDepth;
87
92
  constructor(config?: Partial<TimelineConfig>);
88
93
  private createPlaybackAttemptId;
89
94
  private refreshPlaybackAttempt;
@@ -119,7 +124,10 @@ export declare class TimelineManager {
119
124
  private getEventDispatcher;
120
125
  private getDefaultTrackForHistory;
121
126
  private getTimelineHistoryRecorder;
122
- private getTimelineHistoryExecutor;
127
+ private withStructureSession;
128
+ private getActiveStructureSession;
129
+ private withPreservedEmptyTracks;
130
+ private applyStructureSessionSummary;
123
131
  private withHistoryBoundary;
124
132
  private shouldSkipHistoryRecording;
125
133
  private withHistoryTransaction;
@@ -156,6 +164,7 @@ export declare class TimelineManager {
156
164
  private getTracksSortedByOrder;
157
165
  private getPlaybackTracksSnapshot;
158
166
  private buildPlaybackPlan;
167
+ private buildPreviewPlaybackPlan;
159
168
  private playbackPlanHasActiveVideoClip;
160
169
  private hasActiveTimelineVideoClip;
161
170
  private getPreviewAutoAspectRatioClipOrderMap;
@@ -166,11 +175,13 @@ export declare class TimelineManager {
166
175
  private shouldSkipSteadyPlaybackPreviewSync;
167
176
  private syncPreviewSession;
168
177
  private resolvePreviewSyncPlayState;
178
+ private resolvePreviewRequestedPlayState;
169
179
  private syncPreviewPlaybackStateIfNeeded;
170
180
  private shouldUsePreviewClockPlayback;
171
181
  private handlePreviewClockStateChange;
172
182
  private isPreviewClockForCurrentActiveClip;
173
183
  private shouldCommitUnavailablePreviewClock;
184
+ private shouldIgnorePreviewClockUntilSeekTargetAligned;
174
185
  private commitPlaybackTimeFromPreviewClock;
175
186
  private syncPreviewAfterPreviewClockCommitIfNeeded;
176
187
  private resumeWallClockPlaybackIfPreviewClockUnavailable;
@@ -183,7 +194,7 @@ export declare class TimelineManager {
183
194
  private retryPendingPreview;
184
195
  private handlePreviewBackendRuntimeError;
185
196
  init(container: HTMLElement): void;
186
- createTrack(type: TrackType, name?: string, insertionPlacement?: TrackInsertionPlacement, referenceTrackId?: string, roleOverride?: TrackEntity['role']): string;
197
+ createTrack(type: TrackType, name?: string, insertionPlacement?: TrackInsertionPlacement, referenceTrackId?: string, roleOverride?: TrackEntity['role'], explicitTrackId?: string): string;
187
198
  removeTrack(trackId: string): boolean;
188
199
  renameTrack(trackId: string, newName: string): boolean;
189
200
  getTracks(): any[];
@@ -193,6 +204,7 @@ export declare class TimelineManager {
193
204
  private calculateTrackY;
194
205
  private calculateTotalHeight;
195
206
  private updateAllTrackPositions;
207
+ private removeEmptyTracksFromStructure;
196
208
  private cleanupEmptyTracks;
197
209
  private clearAllTrackDropPreviews;
198
210
  private showClipDropPreview;
@@ -315,6 +327,10 @@ export declare class TimelineManager {
315
327
  splitClip(clipId: string, time: TimeMs): void;
316
328
  splitCurrentClip(): void;
317
329
  getClips(): Clip[];
330
+ private syncTimelineClipMediaStateFromPreview;
331
+ private applyTimelineClipMediaStatus;
332
+ private clearTimelineClipMediaStatus;
333
+ private getFailedPreviewClipIds;
318
334
  private getExportComposition;
319
335
  private getExportCoverUrl;
320
336
  exportTimeline(): TimelineExportData;
@@ -592,7 +608,6 @@ export declare class TimelineManager {
592
608
  private isPointOnClip;
593
609
  private getClipsIntersectingBox;
594
610
  private resolveTrackRenderHeight;
595
- private addClipToTrack;
596
611
  private cloneTrackSnapshot;
597
612
  private ensureTrackFromHistorySnapshot;
598
613
  private getTrackRestoreAnchor;
@@ -1,4 +1,4 @@
1
- import { Action, Clip, ClipStateUpdate, CompoundAction, HistoryState, MoveClipBetweenTracksAction, MoveClipAction, MultiClipUpdateAction, RemoveClipAction, RemoveGapsAction, RestoreClipAudioAction, ResizeClipAction, SeparateClipAudioAction, SetTimeAction, SplitClipAction, Track, TrackRestoreAnchor, UpdateClipAction, AddClipAction } from '../models/types';
1
+ import { Action, Clip, ClipStateUpdate, CompoundAction, HistoryState, MoveClipBetweenTracksAction, MoveClipAction, MultiClipUpdateAction, RemoveClipAction, RemoveGapsAction, RestoreClipAudioAction, ResizeClipAction, SeparateClipAudioAction, SetTimeAction, SplitClipAction, Track, TrackRestoreAnchor, TrackHistoryReference, UpdateClipAction, AddClipAction } from '../models/types';
2
2
  export declare class HistoryManager {
3
3
  private state;
4
4
  private maxHistorySize;
@@ -50,7 +50,7 @@ export declare class HistoryManager {
50
50
  /**
51
51
  * 创建分割片段操作
52
52
  */
53
- createSplitClipAction(originalClip: Clip, clip1: Clip, clip2: Clip): SplitClipAction;
53
+ createSplitClipAction(originalClip: Clip, clip1: Clip, clip2: Clip, sourceTrackReference?: Partial<TrackHistoryReference>): SplitClipAction;
54
54
  /**
55
55
  * 创建移动片段操作
56
56
  */
@@ -74,7 +74,7 @@ export declare class HistoryManager {
74
74
  /**
75
75
  * 创建跨轨道移动片段操作
76
76
  */
77
- createMoveClipBetweenTracksAction(clipId: string, sourceTrackId: string, targetTrackId: string, clipBefore: Clip, clipAfter: Clip, sourceTrackSnapshot: Track | null, targetTrackSnapshot: Track | null): MoveClipBetweenTracksAction;
77
+ createMoveClipBetweenTracksAction(clipId: string, sourceTrackId: string, targetTrackId: string, clipBefore: Clip, clipAfter: Clip, sourceTrackSnapshot: Track | null, targetTrackSnapshot: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null, targetTrackRestoreAnchor?: TrackRestoreAnchor | null): MoveClipBetweenTracksAction;
78
78
  createSeparateClipAudioAction(videoClipBefore: Clip, videoClipAfter: Clip, audioClip: Clip | null, audioTrackId: string | null): SeparateClipAudioAction;
79
79
  createRestoreClipAudioAction(videoClipBefore: Clip, videoClipAfter: Clip, audioClip: Clip | null, audioTrackId: string | null): RestoreClipAudioAction;
80
80
  createCompoundAction(actions: Action[], label?: string): CompoundAction;
@@ -13,6 +13,7 @@ export interface TimelineHistoryExecutionTarget {
13
13
  findTrackByClipId(clipId: string): TimelineHistoryExecutionTrack | null;
14
14
  getDefaultTrack(): TimelineHistoryExecutionTrack | null;
15
15
  loadClipThumbnails?(clip: Clip): Promise<boolean> | void;
16
+ withClipRemovalBatch?<T>(callback: () => T): T;
16
17
  }
17
18
  export declare class TimelineHistoryExecutor {
18
19
  private readonly target;
@@ -22,8 +23,11 @@ export declare class TimelineHistoryExecutor {
22
23
  private executeUndoInternal;
23
24
  private executeRedoInternal;
24
25
  private restoreMovedClip;
26
+ private withClipRemovalBatch;
25
27
  private normalizeRemoveClipActionData;
26
28
  private normalizeAddClipActionData;
29
+ private normalizeSplitClipActionData;
30
+ private normalizeMoveClipBetweenTracksActionData;
27
31
  private resolveTrackIdForHistorySnapshot;
28
32
  private resolveRestoreAnchor;
29
33
  }
@@ -9,9 +9,9 @@ export declare class TimelineHistoryRecorder {
9
9
  createRemoveClipAction(clip: Clip, sourceTrackId?: string | null, sourceTrackSnapshot?: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
10
10
  recordRemoveClip(clip: Clip, sourceTrackId?: string | null, sourceTrackSnapshot?: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
11
11
  recordClipUpdate(clip: Clip, originalClip?: Clip, clipUpdates?: ClipStateUpdate[]): Action | null;
12
- recordSplitClip(clip1: Clip, clip2: Clip): Action;
12
+ recordSplitClip(clip1: Clip, clip2: Clip, sourceTrackId?: string | null, sourceTrackSnapshot?: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
13
13
  recordRemoveGaps(clipsBefore: Clip[]): Action | null;
14
- recordMoveClipBetweenTracks(clipId: string, sourceTrackId: string, targetTrackId: string, clipBefore: Clip, clipAfter: Clip, sourceTrackSnapshot: Track | null, targetTrackSnapshot: Track | null): Action;
14
+ recordMoveClipBetweenTracks(clipId: string, sourceTrackId: string, targetTrackId: string, clipBefore: Clip, clipAfter: Clip, sourceTrackSnapshot: Track | null, targetTrackSnapshot: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null, targetTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
15
15
  recordSeparateClipAudio(videoClipBefore: Clip, videoClipAfter: Clip, audioClip: Clip | null, audioTrackId: string | null): Action;
16
16
  recordRestoreClipAudio(videoClipBefore: Clip, videoClipAfter: Clip, audioClip: Clip | null, audioTrackId: string | null): Action;
17
17
  createSeparateClipAudioAction(videoClipBefore: Clip, videoClipAfter: Clip, audioClip: Clip | null, audioTrackId: string | null): Action;
@@ -76,6 +76,7 @@ export interface GenerateVoiceResult {
76
76
  voiceId: string;
77
77
  voiceName: string;
78
78
  }
79
+ export type ClipMediaStatus = 'ready' | 'failed';
79
80
  export interface ClipConfigVoicePanelOptions {
80
81
  voiceCatalog?: VoiceOption[];
81
82
  generateVoiceBatch?: (requests: GenerateVoiceRequest[]) => Promise<GenerateVoiceResult[]>;
@@ -105,6 +106,8 @@ export interface ClipConfig {
105
106
  ttsVoiceId?: string;
106
107
  ttsVoiceName?: string;
107
108
  ttsFollowTextUpdates?: boolean;
109
+ mediaStatus?: ClipMediaStatus;
110
+ mediaStatusMessage?: string | null;
108
111
  }
109
112
  export interface ClipEntity {
110
113
  id: string;
@@ -131,6 +134,8 @@ export interface ClipEntity {
131
134
  ttsVoiceId?: string;
132
135
  ttsVoiceName?: string;
133
136
  ttsFollowTextUpdates?: boolean;
137
+ mediaStatus?: ClipMediaStatus;
138
+ mediaStatusMessage?: string | null;
134
139
  }
135
140
  export interface ClipViewState {
136
141
  isDragging: boolean;
@@ -297,6 +302,11 @@ export interface TrackRestoreAnchor {
297
302
  previousTrackId: string | null;
298
303
  nextTrackId: string | null;
299
304
  }
305
+ export interface TrackHistoryReference {
306
+ trackId: string | null;
307
+ trackSnapshot: Track | null;
308
+ trackRestoreAnchor: TrackRestoreAnchor | null;
309
+ }
300
310
  export interface UpdateClipAction {
301
311
  type: 'update_clip';
302
312
  data: {
@@ -312,6 +322,9 @@ export interface SplitClipAction {
312
322
  originalClip: Clip;
313
323
  clip1: Clip;
314
324
  clip2: Clip;
325
+ sourceTrackId?: string | null;
326
+ sourceTrackSnapshot?: Track | null;
327
+ sourceTrackRestoreAnchor?: TrackRestoreAnchor | null;
315
328
  };
316
329
  timestamp: number;
317
330
  }
@@ -367,6 +380,8 @@ export interface MoveClipBetweenTracksAction {
367
380
  clipAfter: Clip;
368
381
  sourceTrackSnapshot: Track | null;
369
382
  targetTrackSnapshot: Track | null;
383
+ sourceTrackRestoreAnchor?: TrackRestoreAnchor | null;
384
+ targetTrackRestoreAnchor?: TrackRestoreAnchor | null;
370
385
  };
371
386
  timestamp: number;
372
387
  }
@@ -10,6 +10,7 @@ export declare const SHARED_COLOR_TOKENS: {
10
10
  readonly videoClipBackground: "#1e5679";
11
11
  readonly textClipBackground: "#604c32";
12
12
  readonly audioClipBackground: "#254428";
13
+ readonly mediaClipFailedBackground: "#670014";
13
14
  readonly selectedTrackSurface: "#292929";
14
15
  readonly borderStrong: "#333333";
15
16
  readonly borderMedium: "#444444";
@@ -125,6 +126,8 @@ export declare const PLAYHEAD_DEFAULT_COLORS: {
125
126
  export declare const PREVIEW_PENDING_OVERLAY_COLORS: {
126
127
  readonly background: "rgba(0, 0, 0, 0.56)";
127
128
  readonly text: "#ffffff";
129
+ readonly failedBackground: "#670014";
130
+ readonly failedText: "#ffffff";
128
131
  readonly actionBackground: "#4c8dff";
129
132
  readonly actionText: "#ffffff";
130
133
  };
@@ -3,7 +3,7 @@ export declare class TrackManager {
3
3
  private tracks;
4
4
  private trackCounter;
5
5
  constructor(initialTracks?: Track[]);
6
- createTrack(type: TrackType, name?: string, insertionPlacement?: TrackInsertionPlacement, referenceTrackId?: string, roleOverride?: Track['role']): Track;
6
+ createTrack(type: TrackType, name?: string, insertionPlacement?: TrackInsertionPlacement, referenceTrackId?: string, roleOverride?: Track['role'], explicitTrackId?: string): Track;
7
7
  removeTrack(trackId: string): boolean;
8
8
  renameTrack(trackId: string, newName: string): boolean;
9
9
  getTracks(): Track[];