@linker-design-plus/timeline-track 1.0.9 → 1.0.11
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 +328 -372
- package/dist/components/interaction/globalPointerDragSession.d.ts +8 -0
- package/dist/components/panel/ClipConfigPanel.d.ts +48 -0
- package/dist/components/panel/TrackInfoPanel.d.ts +33 -0
- package/dist/components/scrollbar/KonvaScrollbarView.d.ts +58 -0
- package/dist/components/timeline/ManagedPlayhead.d.ts +29 -0
- package/dist/components/{Playhead.d.ts → timeline/Playhead.d.ts} +1 -1
- package/dist/components/{Timeline.d.ts → timeline/Timeline.d.ts} +27 -6
- package/dist/components/timeline/TimelineHeaderView.d.ts +45 -0
- package/dist/components/track/Clip.d.ts +16 -0
- package/dist/components/track/Track.d.ts +121 -0
- package/dist/components/track/trackClipLayout.d.ts +23 -0
- package/dist/components/track/trackInteractionState.d.ts +79 -0
- package/dist/core/commands/timelineCommands.d.ts +121 -0
- package/dist/core/controllers/demoPreviewRebuildState.d.ts +54 -0
- package/dist/core/controllers/domPreviewBackend.d.ts +5 -0
- package/dist/core/controllers/index.d.ts +13 -0
- package/dist/core/controllers/previewBackend.d.ts +53 -0
- package/dist/core/controllers/previewBackendSupport.d.ts +2 -0
- package/dist/core/controllers/previewClockController.d.ts +13 -0
- package/dist/core/controllers/previewTransformMath.d.ts +27 -0
- package/dist/core/controllers/previewTransformOverlay.d.ts +45 -0
- package/dist/core/controllers/timelineClipConfigController.d.ts +15 -0
- package/dist/core/controllers/timelineClipEventController.d.ts +32 -0
- package/dist/core/controllers/timelineClipWorkflowController.d.ts +30 -0
- package/dist/core/controllers/timelineDurationController.d.ts +14 -0
- package/dist/core/controllers/timelineEventDispatcher.d.ts +12 -0
- package/dist/core/controllers/timelinePlaybackResolver.d.ts +17 -0
- package/dist/core/controllers/timelinePreviewSession.d.ts +94 -0
- package/dist/core/controllers/timelineSelectionController.d.ts +17 -0
- package/dist/core/controllers/timelineTrackInfoPanelController.d.ts +19 -0
- package/dist/core/controllers/timelineTrackMutationController.d.ts +46 -0
- package/dist/core/facade/timelineManager.d.ts +382 -0
- package/dist/core/{history.d.ts → history/history.d.ts} +16 -14
- package/dist/core/history/index.d.ts +3 -0
- package/dist/core/history/timelineHistoryExecutor.d.ts +23 -0
- package/dist/core/history/timelineHistoryRecorder.d.ts +15 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/timelineManagerDom.d.ts +22 -0
- package/dist/core/layout/timelineTrackLayout.d.ts +10 -0
- package/dist/core/models/clipState.d.ts +3 -0
- package/dist/core/{constants.d.ts → models/constants.d.ts} +4 -0
- package/dist/core/models/index.d.ts +3 -0
- package/dist/core/models/types.d.ts +392 -0
- package/dist/core/presentation/index.d.ts +1 -0
- package/dist/core/presentation/timelinePresentationAdapter.d.ts +22 -0
- package/dist/core/stores/index.d.ts +4 -0
- package/dist/core/stores/playbackStore.d.ts +17 -0
- package/dist/core/stores/selectionStore.d.ts +7 -0
- package/dist/core/stores/timelineStore.d.ts +44 -0
- package/dist/core/stores/viewportStore.d.ts +33 -0
- package/dist/core/testing/konva-test-stub.d.ts +81 -0
- package/dist/core/tracks/index.d.ts +3 -0
- package/dist/core/tracks/timelineTrackBridge.d.ts +37 -0
- package/dist/core/tracks/timelineTrackCollection.d.ts +50 -0
- package/dist/core/tracks/trackManager.d.ts +19 -0
- package/dist/core/utils/mountManager.d.ts +10 -0
- package/dist/index.cjs.js +29 -3
- package/dist/index.d.ts +12 -7
- package/dist/index.es.js +8914 -4353
- package/dist/utils/logging/Logger.d.ts +30 -0
- package/dist/utils/logging/index.d.ts +1 -0
- package/dist/utils/{KonvaUtils.d.ts → rendering/KonvaUtils.d.ts} +3 -43
- package/dist/utils/rendering/clipCoverRenderer.d.ts +23 -0
- package/dist/utils/rendering/clipVisualRenderer.d.ts +5 -0
- package/dist/utils/rendering/index.d.ts +4 -0
- package/dist/utils/rendering/timelineGridDrawing.d.ts +8 -0
- package/dist/utils/time/index.d.ts +1 -0
- package/dist/utils/{timeUtils.d.ts → time/timeUtils.d.ts} +4 -0
- package/package.json +5 -3
- package/dist/components/Clip.d.ts +0 -44
- package/dist/components/VideoTrack.d.ts +0 -126
- package/dist/core/timelineManager.d.ts +0 -212
- package/dist/core/types.d.ts +0 -183
- package/dist/utils/Logger.d.ts +0 -49
package/README.md
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
# @linker-design-plus/timeline-track
|
|
2
2
|
|
|
3
|
-
基于 TypeScript 和 Konva
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
- ✅ 封面系统,支持自定义缩略图提供器
|
|
23
|
-
- ✅ 异步封面加载,支持 Promise 形式的封面获取
|
|
3
|
+
基于 TypeScript 和 Konva 的时间线编辑组件库,提供单一主入口 `TimelineManager`,支持多轨片段编辑、缩放、分割、撤销重做、封面渲染以及多轨音视频预览播放。
|
|
4
|
+
|
|
5
|
+
## 当前状态
|
|
6
|
+
|
|
7
|
+
- 主线重构已完成,代码已收敛到单一轨道实现 `Track`
|
|
8
|
+
- `TimelineManager` 仍是对外 façade,但内部职责已拆分到 store / commands / controller / adapter
|
|
9
|
+
- 工具层和核心层已有回归测试覆盖,当前保持全量自动化回归
|
|
10
|
+
- 交互约束与重构路线分别见 [docs/interaction-model.md](./docs/interaction-model.md) 和 [docs/refactor-roadmap.md](./docs/refactor-roadmap.md)
|
|
11
|
+
|
|
12
|
+
## 能力概览
|
|
13
|
+
|
|
14
|
+
- 多轨时间线编辑,支持视频轨和音频轨
|
|
15
|
+
- 片段拖拽、拉伸、分割、跨轨移动、自动避让重叠
|
|
16
|
+
- 单一选中态,避免多个 clip 同时处于最终选中状态
|
|
17
|
+
- 播放头、滚动、缩放和时间同步
|
|
18
|
+
- 撤销 / 重做 / 历史变更通知
|
|
19
|
+
- 缩略图提供器与异步封面加载
|
|
20
|
+
- 多轨视频层级叠加预览与多轨音频混合播放
|
|
21
|
+
- 预览容器挂载、buffering 状态与预览比例同步
|
|
24
22
|
|
|
25
23
|
## 安装
|
|
26
24
|
|
|
@@ -30,402 +28,360 @@ npm install @linker-design-plus/timeline-track
|
|
|
30
28
|
|
|
31
29
|
## 快速开始
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
31
|
+
```ts
|
|
36
32
|
import { TimelineManager } from '@linker-design-plus/timeline-track';
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
34
|
+
const container = document.getElementById('timeline-container') as HTMLDivElement;
|
|
35
|
+
const preview = document.getElementById('preview-container') as HTMLDivElement;
|
|
36
|
+
|
|
37
|
+
const timeline = new TimelineManager({
|
|
38
|
+
container,
|
|
39
|
+
debug: true,
|
|
40
|
+
zoom: 100,
|
|
41
|
+
currentTime: 0,
|
|
42
|
+
previewBackend: 'dom',
|
|
43
|
+
previewSourceResolver: (clip) => {
|
|
44
|
+
if (clip.src.endsWith('.mp4')) {
|
|
45
|
+
return {
|
|
46
|
+
url: clip.src,
|
|
47
|
+
mimeType: 'video/mp4',
|
|
48
|
+
kind: 'mp4'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (clip.src.endsWith('.m3u8')) {
|
|
53
|
+
return {
|
|
54
|
+
url: clip.src,
|
|
55
|
+
mimeType: 'application/vnd.apple.mpegurl',
|
|
56
|
+
kind: 'hls-fmp4'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
container: timelineContainer,
|
|
45
|
-
debug: true, // 开启调试模式
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
46
62
|
});
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
timelineManager.connectTo(videoElement);
|
|
64
|
+
timeline.attachPreview(preview);
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
const clipId = await timelineManager.addClip({
|
|
66
|
+
await timeline.addClip({
|
|
53
67
|
src: 'sample-video.mp4',
|
|
54
68
|
name: 'Clip 1',
|
|
55
|
-
startTimeAtSource: 0,
|
|
56
|
-
duration: 5000,
|
|
57
|
-
|
|
69
|
+
startTimeAtSource: 0,
|
|
70
|
+
duration: 5000,
|
|
71
|
+
visualTransform: {
|
|
72
|
+
x: 0.5,
|
|
73
|
+
y: 0.5,
|
|
74
|
+
scale: 1
|
|
75
|
+
},
|
|
76
|
+
thumbnails: ['https://example.com/thumb-1.jpg']
|
|
58
77
|
});
|
|
59
78
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// 设置播放倍速
|
|
64
|
-
timelineManager.setSpeed(2); // 2 倍速
|
|
65
|
-
|
|
66
|
-
// 适合缩放(自动调整缩放比例以适应所有片段)
|
|
67
|
-
timelineManager.fitZoom();
|
|
68
|
-
|
|
69
|
-
// 分割当前时间点的片段
|
|
70
|
-
timelineManager.splitCurrentClip();
|
|
71
|
-
|
|
72
|
-
// 移除片段之间的间隙
|
|
73
|
-
timelineManager.removeClipGaps();
|
|
74
|
-
|
|
75
|
-
// 监听历史记录变更事件
|
|
76
|
-
timelineManager.on('history_change', (event, data) => {
|
|
77
|
-
console.log('History changed:', data);
|
|
78
|
-
// 更新撤销/重做按钮状态
|
|
79
|
-
updateUndoRedoButtons(data.canUndo, data.canRedo);
|
|
79
|
+
timeline.on('history_change', (_event, data) => {
|
|
80
|
+
console.log('canUndo', data.canUndo, 'canRedo', data.canRedo);
|
|
80
81
|
});
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
// timelineManager.destroy();
|
|
83
|
+
timeline.play();
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
## 缩略图提供器
|
|
87
|
+
|
|
88
|
+
`ThumbnailProvider` 使用 `getThumbnails(clip)`,返回 `string[]` 或 `Promise<string[]>`。
|
|
89
|
+
音频 clip 默认不会使用缩略图渲染,`thumbnailProvider` 主要作用于视频 clip。
|
|
87
90
|
|
|
88
|
-
```
|
|
89
|
-
import { TimelineManager, ThumbnailProvider } from '@linker-design-plus/timeline-track';
|
|
91
|
+
```ts
|
|
92
|
+
import { TimelineManager, type ThumbnailProvider } from '@linker-design-plus/timeline-track';
|
|
90
93
|
|
|
91
|
-
// 创建缩略图提供器
|
|
92
94
|
const thumbnailProvider: ThumbnailProvider = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// return new Promise((resolve) => {
|
|
99
|
-
// // 模拟异步获取封面
|
|
100
|
-
// setTimeout(() => {
|
|
101
|
-
// resolve(`https://example.com/thumbnails/${clip.id}.jpg`);
|
|
102
|
-
// }, 100);
|
|
103
|
-
// });
|
|
95
|
+
async getThumbnails(clip) {
|
|
96
|
+
return [
|
|
97
|
+
`https://example.com/thumbnails/${clip.id}/0.jpg`,
|
|
98
|
+
`https://example.com/thumbnails/${clip.id}/1.jpg`
|
|
99
|
+
];
|
|
104
100
|
}
|
|
105
101
|
};
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
thumbnailProvider: thumbnailProvider
|
|
103
|
+
const timeline = new TimelineManager({
|
|
104
|
+
container,
|
|
105
|
+
thumbnailProvider
|
|
111
106
|
});
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// 添加片段时,会自动通过提供器获取封面
|
|
117
|
-
const clipId = await timelineManager.addClip({
|
|
118
|
-
src: 'sample-video.mp4',
|
|
119
|
-
name: 'Clip 1',
|
|
120
|
-
duration: 5000
|
|
121
|
-
});
|
|
108
|
+
timeline.setThumbnailProvider(thumbnailProvider);
|
|
109
|
+
await timeline.refreshAllClipThumbnails();
|
|
122
110
|
```
|
|
123
111
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
container: timelineContainer.value,
|
|
191
|
-
debug: true,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// 连接到视频元素
|
|
195
|
-
if (videoElement.value) {
|
|
196
|
-
timelineManager.connectTo(videoElement.value);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 添加事件监听器
|
|
200
|
-
timelineManager.on('time_change', (event, data) => {
|
|
201
|
-
currentTime.value = data.time;
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
timelineManager.on('play_state_change', (event, data) => {
|
|
205
|
-
isPlaying.value = data.playState === 'playing';
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
timelineManager.on('zoom_change', (event, data) => {
|
|
209
|
-
zoom.value = data.zoom;
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
timelineManager.on('history_change', (event, data) => {
|
|
213
|
-
canUndo.value = data.canUndo;
|
|
214
|
-
canRedo.value = data.canRedo;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
timelineManager.on('speed_change', (event, data) => {
|
|
218
|
-
speed.value = data.speed;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// 添加示例片段
|
|
222
|
-
await addSampleClips();
|
|
223
|
-
updateClipCount();
|
|
224
|
-
|
|
225
|
-
// 初始设置
|
|
226
|
-
zoom.value = timelineManager.getZoom();
|
|
227
|
-
speed.value = timelineManager.getSpeed();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// 清理
|
|
231
|
-
onUnmounted(() => {
|
|
232
|
-
timelineManager?.destroy();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// 控制方法
|
|
236
|
-
const togglePlay = () => timelineManager?.togglePlay();
|
|
237
|
-
const undo = () => timelineManager?.undo();
|
|
238
|
-
const redo = () => timelineManager?.redo();
|
|
239
|
-
const fitZoom = () => timelineManager?.fitZoom();
|
|
240
|
-
const removeClipGaps = () => timelineManager?.removeClipGaps();
|
|
241
|
-
|
|
242
|
-
// 添加片段
|
|
243
|
-
const addClip = async () => {
|
|
244
|
-
if (!timelineManager) return;
|
|
245
|
-
|
|
246
|
-
const clipId = await timelineManager.addClip({
|
|
247
|
-
src: 'sample-video.mp4',
|
|
248
|
-
name: `Clip ${Date.now()}`,
|
|
249
|
-
duration: 5000,
|
|
250
|
-
startTimeAtSource: 0
|
|
251
|
-
});
|
|
252
|
-
updateClipCount();
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
// 分割片段
|
|
256
|
-
const splitClip = () => timelineManager?.splitCurrentClip();
|
|
257
|
-
|
|
258
|
-
// 更新片段计数
|
|
259
|
-
const updateClipCount = () => {
|
|
260
|
-
if (timelineManager) {
|
|
261
|
-
clipCount.value = timelineManager.getClips().length;
|
|
262
|
-
}
|
|
263
|
-
};
|
|
112
|
+
## 对外主 API
|
|
113
|
+
|
|
114
|
+
### `TimelineManager`
|
|
115
|
+
|
|
116
|
+
常用方法:
|
|
117
|
+
|
|
118
|
+
- `play()` / `pause()` / `togglePlay()`
|
|
119
|
+
- `setCurrentTime(time)` / `getCurrentTime()`
|
|
120
|
+
- `setZoom(zoom)` / `getZoom()`
|
|
121
|
+
- `setSpeed(speed)` / `getSpeed()`
|
|
122
|
+
- `addClip(config)` / `addClips(configs)`
|
|
123
|
+
- `updateClip(clipId, updates)` / `removeClip(clipId)`
|
|
124
|
+
- `selectClip(clipId)` / `clearSelection()` / `getSelectedClip()`
|
|
125
|
+
- `canSeparateClipAudio(clipId)` / `separateClipAudio(clipId)` / `canRestoreClipAudio(clipId)` / `restoreClipAudio(clipId)`
|
|
126
|
+
- `splitCurrentClip()` / `removeClipGaps()` / `fitZoom()`
|
|
127
|
+
- `undo()` / `redo()` / `clearHistory()`
|
|
128
|
+
- `attachPreview(containerOrConfig)` / `detachPreview()`
|
|
129
|
+
- `getPreviewBackendType()`
|
|
130
|
+
- `getCurrentActiveClips()` / `getActiveClipsAtTime(time)`
|
|
131
|
+
- `getPreviewAspectRatio()` / `setPreviewAspectRatio({ width, height })` / `resetPreviewAspectRatioToAuto()`
|
|
132
|
+
- `exportTimeline()`
|
|
133
|
+
|
|
134
|
+
常用事件:
|
|
135
|
+
|
|
136
|
+
- `time_change`
|
|
137
|
+
- `play_state_change`
|
|
138
|
+
- `speed_change`
|
|
139
|
+
- `zoom_change`
|
|
140
|
+
- `preview_aspect_ratio_change`
|
|
141
|
+
- `clip_added`
|
|
142
|
+
- `clip_removed`
|
|
143
|
+
- `clip_updated`
|
|
144
|
+
- `clip_selected`
|
|
145
|
+
- `selected_clip_change`
|
|
146
|
+
- `history_change`
|
|
147
|
+
- `track_duration_change`
|
|
148
|
+
- `buffering_state_change`
|
|
149
|
+
- `can_play_change`
|
|
150
|
+
- `source_loading_change`
|
|
151
|
+
|
|
152
|
+
## 核心类型
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
interface ClipConfig {
|
|
156
|
+
id?: string;
|
|
157
|
+
type?: 'video' | 'audio';
|
|
158
|
+
externalId?: string;
|
|
159
|
+
src: string;
|
|
160
|
+
name: string;
|
|
161
|
+
isMuted?: boolean;
|
|
162
|
+
startTime?: number;
|
|
163
|
+
duration: number;
|
|
164
|
+
startTimeAtSource?: number;
|
|
165
|
+
endTimeAtSource?: number;
|
|
166
|
+
sourceDuration?: number;
|
|
167
|
+
thumbnails?: string[];
|
|
168
|
+
style?: Record<string, any>;
|
|
169
|
+
visualTransform?: {
|
|
170
|
+
x: number;
|
|
171
|
+
y: number;
|
|
172
|
+
scale: number;
|
|
173
|
+
};
|
|
174
|
+
separatedAudioClipId?: string;
|
|
175
|
+
separatedFromVideoClipId?: string;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
264
178
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
179
|
+
当 `type: 'audio'` 时,clip 会渲染为音频卡片样式:内置音频图标、装饰性波形和文件名,不展示视频缩略图。
|
|
180
|
+
视频 clip 可通过 `visualTransform` 控制预览中的归一化位置和等比缩放,默认值为 `{ x: 0.5, y: 0.5, scale: 1 }`。
|
|
181
|
+
视频 clip 分离原声后会写入 `separatedAudioClipId`,对应音频 clip 会写入 `separatedFromVideoClipId`。
|
|
182
|
+
导出时间线时会包含 `composition`、轨道 `order` / `isMuted`、clip `isMuted` 和视频 clip 的 `visualTransform`,便于后端按当前多轨预览语义做合成。
|
|
183
|
+
|
|
184
|
+
### 预览后端
|
|
185
|
+
|
|
186
|
+
`TimelineManager` 现在支持双预览后端:
|
|
187
|
+
|
|
188
|
+
- `previewBackend: 'dom'`
|
|
189
|
+
当前默认后端,使用多媒体元素叠层完成多轨预览,兼容性最好。
|
|
190
|
+
- `previewBackend: 'canvas'`
|
|
191
|
+
第一阶段的新预览后端,使用 Canvas2D + WebCodecs 进行视频解码与单画布合成。
|
|
192
|
+
- `previewBackend: 'auto'`
|
|
193
|
+
在支持的浏览器中优先使用 `canvas`,否则自动回退到 `dom`。
|
|
194
|
+
|
|
195
|
+
配合 `canvas` / `auto` 后端,建议显式提供 `previewSourceResolver`,让编辑预览使用代理 MP4:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const timeline = new TimelineManager({
|
|
199
|
+
container,
|
|
200
|
+
previewBackend: 'canvas',
|
|
201
|
+
previewSourceResolver: (clip) => {
|
|
202
|
+
if (!clip.src.endsWith('.mp4')) {
|
|
203
|
+
return clip.src.endsWith('.m3u8')
|
|
204
|
+
? {
|
|
205
|
+
url: clip.src,
|
|
206
|
+
mimeType: 'application/vnd.apple.mpegurl',
|
|
207
|
+
kind: 'hls-fmp4'
|
|
208
|
+
}
|
|
209
|
+
: null;
|
|
281
210
|
}
|
|
282
|
-
];
|
|
283
211
|
|
|
284
|
-
|
|
285
|
-
|
|
212
|
+
return {
|
|
213
|
+
url: clip.src,
|
|
214
|
+
mimeType: 'video/mp4',
|
|
215
|
+
kind: 'mp4'
|
|
216
|
+
};
|
|
286
217
|
}
|
|
287
|
-
|
|
288
|
-
timelineManager.clearHistory();
|
|
289
|
-
};
|
|
290
|
-
</script>
|
|
291
|
-
|
|
292
|
-
<style scoped>
|
|
293
|
-
/* 样式省略 */
|
|
294
|
-
</style>
|
|
218
|
+
});
|
|
295
219
|
```
|
|
296
220
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
221
|
+
当前阶段限制:
|
|
222
|
+
|
|
223
|
+
- `canvas` 后端仅面向 Chrome / Edge 桌面端
|
|
224
|
+
- 视频预览源支持 MP4,以及 `EXT-X-MAP + m4s` 形式的 fMP4 HLS
|
|
225
|
+
- 暂不支持基于 TS 分片的 HLS,也不支持 DRM 流
|
|
226
|
+
- Firefox / Safari 会自动回退到 `dom` 后端
|
|
227
|
+
|
|
228
|
+
## 当前代码架构
|
|
229
|
+
|
|
230
|
+
### 入口与 façade
|
|
231
|
+
|
|
232
|
+
- [src/index.ts](./src/index.ts)
|
|
233
|
+
- [src/core/facade/timelineManager.ts](./src/core/facade/timelineManager.ts)
|
|
234
|
+
|
|
235
|
+
### 组件层
|
|
236
|
+
|
|
237
|
+
- [src/components/timeline/Timeline.tsx](./src/components/timeline/Timeline.tsx)
|
|
238
|
+
- [src/components/track/Track.tsx](./src/components/track/Track.tsx)
|
|
239
|
+
- [src/components/track/Clip.tsx](./src/components/track/Clip.tsx)
|
|
240
|
+
- [src/components/timeline/Playhead.tsx](./src/components/timeline/Playhead.tsx)
|
|
241
|
+
- [src/components/panel/TrackInfoPanel.tsx](./src/components/panel/TrackInfoPanel.tsx)
|
|
242
|
+
|
|
243
|
+
### 核心状态与命令
|
|
244
|
+
|
|
245
|
+
- `timelineStore` / `selectionStore` / `playbackStore` / `viewportStore`
|
|
246
|
+
- `timelineCommands`
|
|
247
|
+
- `timelineHistoryRecorder` / `timelineHistoryExecutor`
|
|
248
|
+
|
|
249
|
+
### façade 已拆出的控制器与适配层
|
|
250
|
+
|
|
251
|
+
- `timelineEventDispatcher`
|
|
252
|
+
- `timelineTrackInfoPanelController`
|
|
253
|
+
- `timelinePlaybackResolver`
|
|
254
|
+
- `timelinePreviewSession`
|
|
255
|
+
- `timelineSelectionController`
|
|
256
|
+
- `timelineDurationController`
|
|
257
|
+
- `timelineClipEventController`
|
|
258
|
+
- `timelinePresentationAdapter`
|
|
259
|
+
- `timelineTrackBridge`
|
|
260
|
+
- `timelineTrackCollection`
|
|
261
|
+
- `timelineTrackLayout`
|
|
262
|
+
|
|
263
|
+
### 工具层
|
|
264
|
+
|
|
265
|
+
- `clipVisualRenderer`
|
|
266
|
+
- `clipCoverRenderer`
|
|
267
|
+
- `timelineGridDrawing`
|
|
268
|
+
- `timeUtils`
|
|
269
|
+
- `KonvaUtils`
|
|
270
|
+
|
|
271
|
+
### 目录结构
|
|
272
|
+
|
|
273
|
+
```text
|
|
274
|
+
src/
|
|
275
|
+
components/ Konva 组件和交互 helper
|
|
276
|
+
core/ 状态、命令、controller、façade、类型
|
|
277
|
+
utils/ 渲染与通用工具
|
|
278
|
+
styles/ 样式入口
|
|
279
|
+
index.ts 对外导出
|
|
280
|
+
docs/
|
|
281
|
+
interaction-model.md
|
|
282
|
+
refactor-roadmap.md
|
|
283
|
+
demo/
|
|
284
|
+
App.vue
|
|
285
|
+
main.ts
|
|
305
286
|
```
|
|
306
287
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
| `removeSelectedClip()` | 移除当前选中的片段 | 无 | `boolean`(是否成功) |
|
|
335
|
-
| `splitCurrentClip()` | 分割当前时间点的片段 | 无 | 无 |
|
|
336
|
-
| `removeClipGaps()` | 移除片段之间的间隙 | 无 | 无 |
|
|
337
|
-
| `fitZoom()` | 自动调整缩放比例以适应所有片段 | 无 | 无 |
|
|
338
|
-
| `getClips()` | 获取所有片段 | 无 | `Clip[]` |
|
|
339
|
-
| `getCurrentClip()` | 获取当前时间点所在的片段 | 无 | `Clip` 或 `null` |
|
|
340
|
-
| `getSelectedClip()` | 获取当前选中的片段 | 无 | `Clip` 或 `null` |
|
|
341
|
-
| `getTrackTotalDuration()` | 获取轨道总时长(包含间隙) | 无 | `number` |
|
|
342
|
-
| `exportTimeline()` | 导出时间轴数据 | 无 | `TimelineExportData` |
|
|
343
|
-
| `undo()` | 撤销操作 | 无 | `boolean`(是否成功) |
|
|
344
|
-
| `redo()` | 重做操作 | 无 | `boolean`(是否成功) |
|
|
345
|
-
| `clearHistory()` | 清空历史记录 | 无 | 无 |
|
|
346
|
-
| `connectTo(video)` | 连接到视频元素 | `video`:视频元素 | 无 |
|
|
347
|
-
| `on(event, listener)` | 添加事件监听器 | `event`:事件类型,`listener`:事件监听器 | 无 |
|
|
348
|
-
| `off(event, listener)` | 移除事件监听器 | `event`:事件类型,`listener`:事件监听器 | 无 |
|
|
349
|
-
| `destroy()` | 销毁时间轴管理器 | 无 | 无 |
|
|
350
|
-
|
|
351
|
-
#### 事件
|
|
352
|
-
|
|
353
|
-
| 事件名 | 描述 | 数据 |
|
|
354
|
-
|--------|------|------|
|
|
355
|
-
| `time_change` | 当前时间变化 | `{ time: number }` |
|
|
356
|
-
| `play_state_change` | 播放状态变化 | `{ playState: 'playing' \| 'paused' }` |
|
|
357
|
-
| `speed_change` | 播放倍速变化 | `{ speed: number }` |
|
|
358
|
-
| `clip_added` | 添加片段 | `{ clip: Clip }` |
|
|
359
|
-
| `clip_removed` | 移除片段 | `{ clipId: string }` |
|
|
360
|
-
| `clip_updated` | 更新片段 | `{ clip: Clip }` |
|
|
361
|
-
| `clip_selected` | 选择片段 | `{ clip: Clip }` |
|
|
362
|
-
| `zoom_change` | 缩放比例变化 | `{ zoom: number }` |
|
|
363
|
-
| `history_change` | 历史记录变更 | `{ canUndo: boolean, canRedo: boolean }` |
|
|
364
|
-
| `track_duration_change` | 轨道总时长变化 | `{ duration: number }` |
|
|
365
|
-
| `buffering_state_change` | 视频缓冲状态变化 | `{ isBuffering: boolean }` |
|
|
366
|
-
|
|
367
|
-
### ClipConfig 接口
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
interface ClipConfig {
|
|
371
|
-
src: string; // 视频源 URL
|
|
372
|
-
name?: string; // 片段名称
|
|
373
|
-
duration: number; // 片段持续时间(毫秒)
|
|
374
|
-
startTimeAtSource?: number; // 源视频中的开始时间(毫秒)
|
|
375
|
-
startTime?: number; // 片段在轨道上的起始时间(可选,自动计算)
|
|
376
|
-
thumbnail?: string; // 缩略图 URL
|
|
377
|
-
}
|
|
288
|
+
## 架构图级别说明
|
|
289
|
+
|
|
290
|
+
### 分层关系图
|
|
291
|
+
|
|
292
|
+
```mermaid
|
|
293
|
+
flowchart TB
|
|
294
|
+
API["Public API\nsrc/index.ts"] --> FACADE["TimelineManager façade\nsrc/core/facade/timelineManager.ts"]
|
|
295
|
+
|
|
296
|
+
FACADE --> STORES["Stores\nTimelineStore / PlaybackStore / SelectionStore / ViewportStore"]
|
|
297
|
+
FACADE --> COMMANDS["Commands\nTimelineCommands"]
|
|
298
|
+
FACADE --> CONTROLLERS["Controllers\nclip workflow / track mutation / playback / selection / duration / event dispatcher"]
|
|
299
|
+
FACADE --> HISTORY["History\nHistoryManager / Recorder / Executor"]
|
|
300
|
+
FACADE --> TRACKS["Tracks\nTrackManager / TimelineTrackBridge / TimelineTrackCollection"]
|
|
301
|
+
FACADE --> PRESENTATION["Presentation\nTimelinePresentationAdapter"]
|
|
302
|
+
FACADE --> PREVIEW["Preview Backend\nDomPreviewBackend / CanvasPreviewBackend"]
|
|
303
|
+
FACADE --> COMPONENTS["Konva Views\nTimeline / Track / Clip / Playhead / TrackInfoPanel"]
|
|
304
|
+
|
|
305
|
+
COMPONENTS --> INTERACTION["Interaction\nglobalPointerDragSession / trackInteractionState"]
|
|
306
|
+
COMPONENTS --> UTILS["Utils\ntime / rendering / logging"]
|
|
307
|
+
|
|
308
|
+
TRACKS --> COMPONENTS
|
|
309
|
+
COMMANDS --> STORES
|
|
310
|
+
CONTROLLERS --> STORES
|
|
311
|
+
CONTROLLERS --> HISTORY
|
|
312
|
+
CONTROLLERS --> TRACKS
|
|
313
|
+
PRESENTATION --> COMPONENTS
|
|
314
|
+
PREVIEW --> STORES
|
|
378
315
|
```
|
|
379
316
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
317
|
+
### 运行时职责图
|
|
318
|
+
|
|
319
|
+
```mermaid
|
|
320
|
+
flowchart LR
|
|
321
|
+
USER["用户操作\n拖拽 / 拉伸 / 分割 / 播放"] --> VIEW["组件层\nTrack / Clip / Timeline"]
|
|
322
|
+
VIEW --> BRIDGE["TrackBridge / 回调桥接"]
|
|
323
|
+
BRIDGE --> WORKFLOW["Controllers\n工作流编排"]
|
|
324
|
+
WORKFLOW --> COMMANDS2["Commands\n领域规则与变更计划"]
|
|
325
|
+
COMMANDS2 --> STATE["Stores + TrackCollection\n时间轴状态与轨道集合"]
|
|
326
|
+
WORKFLOW --> HISTORY2["History\n记录 undo / redo"]
|
|
327
|
+
WORKFLOW --> PREVIEW2["Preview Session\n同步多轨预览"]
|
|
328
|
+
STATE --> PRESENTATION2["PresentationAdapter\n把状态转换为渲染输入"]
|
|
329
|
+
PRESENTATION2 --> VIEW
|
|
330
|
+
WORKFLOW --> EVENTS["EventDispatcher\n对外事件通知"]
|
|
386
331
|
```
|
|
387
332
|
|
|
388
|
-
###
|
|
389
|
-
|
|
390
|
-
```bash
|
|
391
|
-
npm run dev
|
|
392
|
-
```
|
|
333
|
+
### 核心分层说明
|
|
393
334
|
|
|
394
|
-
|
|
335
|
+
- `src/index.ts`:对外导出层,暴露 `TimelineManager`、组件、`HistoryManager`、`TrackManager` 与模型类型。
|
|
336
|
+
- `src/core/facade/timelineManager.ts`:系统总入口,负责装配 store、commands、controllers、history、preview backend 与 Konva 视图。
|
|
337
|
+
- `src/core/stores/`:保存当前时间、播放状态、选中态、视口等最小状态源。
|
|
338
|
+
- `src/core/commands/`:封装纯领域规则,例如选中、查找 clip、跨轨移动准备、轨道放置规划。
|
|
339
|
+
- `src/core/controllers/`:把用户行为编排成完整工作流,处理副作用、事件派发、预览同步与轨道/clip 变更。
|
|
340
|
+
- `src/core/tracks/`:管理轨道集合、轨道桥接和轨道级别操作,是 façade 与 `Track` 组件之间的中间层。
|
|
341
|
+
- `src/core/presentation/`:把核心状态适配为视图输入,降低 façade 与 Konva 组件之间的耦合。
|
|
342
|
+
- `src/components/`:Konva 视图层,负责时间轴、轨道、clip、滚动条、播放头和面板渲染。
|
|
343
|
+
- `src/components/interaction/`:处理全局拖拽会话与交互状态机,保证拖拽移动/结束在窗口级别可靠收敛。
|
|
344
|
+
- `src/core/history/`:记录命令结果,提供撤销/重做闭环。
|
|
345
|
+
- `src/core/controllers/*Preview*`:抽象预览后端,支持 `dom` 与 `canvas` 两套多轨预览实现。
|
|
395
346
|
|
|
396
|
-
|
|
397
|
-
npm run build
|
|
398
|
-
```
|
|
347
|
+
### 典型调用链路
|
|
399
348
|
|
|
400
|
-
|
|
349
|
+
#### 1. Clip 拖拽/跨轨移动
|
|
401
350
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
351
|
+
1. 用户在 `Track` / `Clip` 上产生指针操作。
|
|
352
|
+
2. `trackInteractionState` 识别当前操作类型,`globalPointerDragSession` 保证拖拽期间的全局 move/end 监听。
|
|
353
|
+
3. `Track` 通过回调把变更意图交给 `TimelineTrackBridge` 与 façade。
|
|
354
|
+
4. façade 调用 `timelineTrackMutationController`、`timelineClipWorkflowController`、`TimelineCommands` 计算目标轨道、位置与合法性。
|
|
355
|
+
5. 轨道集合与 store 更新后,同步写入 history,并触发重绘、事件通知与预览刷新。
|
|
405
356
|
|
|
406
|
-
|
|
357
|
+
#### 2. 播放与时间推进
|
|
407
358
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
359
|
+
1. `TimelineManager` 持有播放状态与当前时间。
|
|
360
|
+
2. `TimelinePlaybackResolver` 与预览 session 根据当前时间解析 active clips。
|
|
361
|
+
3. `DomPreviewBackend` 或 `CanvasPreviewBackend` 执行多轨音视频预览。
|
|
362
|
+
4. 时间变化通过 `TimelineEventDispatcher` 向外发布 `time_change`、`play_state_change` 等事件。
|
|
412
363
|
|
|
413
|
-
|
|
364
|
+
### 当前架构特点
|
|
414
365
|
|
|
415
|
-
|
|
366
|
+
- 对外仍保持单一 façade:调用方只需面向 `TimelineManager`。
|
|
367
|
+
- 内部已经拆成 store / commands / controllers / tracks / presentation,职责边界比早期单体 manager 更清晰。
|
|
368
|
+
- UI 交互和领域规则分离:交互细节主要留在 `components`,领域决策集中在 `core`。
|
|
369
|
+
- 预览后端可替换:预览能力通过 backend 抽象扩展,而不是直接耦合到时间轴视图。
|
|
370
|
+
- 历史系统贯穿工作流:新增、删除、更新、跨轨移动都能进入 undo / redo 闭环。
|
|
416
371
|
|
|
417
|
-
##
|
|
372
|
+
## 开发
|
|
418
373
|
|
|
419
|
-
|
|
374
|
+
```bash
|
|
375
|
+
pnpm install
|
|
376
|
+
pnpm run dev
|
|
377
|
+
pnpm run build
|
|
378
|
+
pnpm test
|
|
379
|
+
pnpm exec tsc -p tsconfig.json --noEmit
|
|
380
|
+
```
|
|
420
381
|
|
|
421
|
-
##
|
|
382
|
+
## 浏览器支持
|
|
422
383
|
|
|
423
|
-
|
|
424
|
-
-
|
|
425
|
-
-
|
|
426
|
-
-
|
|
427
|
-
- 支持片段拖拽、调整大小、分割
|
|
428
|
-
- 集成操作历史记录系统
|
|
429
|
-
- 支持播放倍速控制
|
|
430
|
-
- 提供完整的外部 API
|
|
431
|
-
- 包含 TypeScript 类型声明
|
|
384
|
+
- Chrome
|
|
385
|
+
- Edge
|
|
386
|
+
- Firefox(`dom` 后端)
|
|
387
|
+
- Safari(`dom` 后端)
|