cerevox 2.43.0 → 3.0.0-alpha.1

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.
Files changed (39) hide show
  1. package/dist/core/ai.d.ts +26 -3
  2. package/dist/core/ai.d.ts.map +1 -1
  3. package/dist/core/ai.js +15 -8
  4. package/dist/core/ai.js.map +1 -1
  5. package/dist/mcp/servers/prompts/actions/storyboard_optimization.md +5 -5
  6. package/dist/{utils/videoproject-schema.json → mcp/servers/prompts/draft_content-schema.json} +3 -3
  7. package/dist/mcp/servers/prompts/rules/creative-ad.md +6 -6
  8. package/dist/mcp/servers/prompts/rules/expert.md +25 -25
  9. package/dist/mcp/servers/prompts/rules/freeform.md +2 -3
  10. package/dist/mcp/servers/prompts/rules/general-video.md +10 -10
  11. package/dist/mcp/servers/prompts/rules/material-creation.md +2 -2
  12. package/dist/mcp/servers/prompts/rules/music-video.md +3 -3
  13. package/dist/mcp/servers/prompts/rules/stage-play.md +4 -4
  14. package/dist/mcp/servers/prompts/rules/story-telling.md +8 -8
  15. package/dist/mcp/servers/prompts/skills/storyboard/storyboard-optimization-skill.md +5 -5
  16. package/dist/mcp/servers/prompts/skills/video/continuity-techniques.md +1 -1
  17. package/dist/mcp/servers/prompts/skills/workflows/general-video.md +10 -10
  18. package/dist/mcp/servers/prompts/skills/workflows/music-video.md +3 -3
  19. package/dist/mcp/servers/prompts/zerocut-core.md +26 -27
  20. package/dist/mcp/servers/zerocut.d.ts.map +1 -1
  21. package/dist/mcp/servers/zerocut.js +383 -427
  22. package/dist/mcp/servers/zerocut.js.map +1 -1
  23. package/dist/utils/coze.d.ts +1 -0
  24. package/dist/utils/coze.d.ts.map +1 -1
  25. package/dist/utils/coze.js +19 -0
  26. package/dist/utils/coze.js.map +1 -1
  27. package/package.json +2 -2
  28. package/dist/timeline-editor/index.d.ts +0 -42
  29. package/dist/timeline-editor/index.d.ts.map +0 -1
  30. package/dist/timeline-editor/index.js +0 -82
  31. package/dist/timeline-editor/index.js.map +0 -1
  32. package/dist/timeline-editor/public/app.js +0 -2086
  33. package/dist/timeline-editor/public/index.html +0 -141
  34. package/dist/timeline-editor/public/style.css +0 -695
  35. package/dist/timeline-editor/server.d.ts +0 -137
  36. package/dist/timeline-editor/server.d.ts.map +0 -1
  37. package/dist/timeline-editor/server.js +0 -418
  38. package/dist/timeline-editor/server.js.map +0 -1
  39. /package/dist/{utils → mcp/servers/prompts}/storyboard-schema.json +0 -0
@@ -1,2086 +0,0 @@
1
- /* eslint-env browser */
2
- class TimelineEditor {
3
- constructor() {
4
- this.assets = {
5
- video: [],
6
- audio: [],
7
- image: [],
8
- text: [],
9
- };
10
- this.tracks = {
11
- video: [],
12
- audio: [],
13
- bgm: [],
14
- voice: [],
15
- subtitle: [],
16
- };
17
- this.subtitles = []; // 字幕数据
18
- this.selectedAsset = null;
19
- this.selectedClip = null;
20
- this.currentTime = 0;
21
- this.totalDuration = 0;
22
- this.zoomLevel = 1;
23
- this.isPlaying = false;
24
- this.pixelsPerSecond = 50; // Base pixels per second for timeline
25
- this.currentClipIndex = 0; // 当前播放的clip索引
26
- this.playbackClips = []; // 按时间顺序排列的视频clips
27
- this.animationFrameId = null; // 用于requestAnimationFrame
28
-
29
- // 音频播放相关状态
30
- this.audioPlayers = {}; // { clipId: HTMLAudioElement }
31
- this.audioTimers = []; // 保存所有定时器(开始/停止)以便清理
32
-
33
- this.init();
34
- }
35
-
36
- async init() {
37
- this.setupEventListeners();
38
- await this.loadDraftData();
39
- this.renderAssets();
40
- this.renderTimeline();
41
- this.updateTimelineRuler();
42
- this.updatePlaybackIndicator(0);
43
- }
44
-
45
- setupEventListeners() {
46
- // Header controls
47
- document
48
- .getElementById('updateProject')
49
- ?.addEventListener('click', () => this.updateProject());
50
-
51
- // Asset search
52
- document
53
- .getElementById('assetSearch')
54
- ?.addEventListener('input', e => this.searchAssets(e.target.value));
55
-
56
- // Category headers for collapsing/expanding
57
- document.querySelectorAll('.category-header').forEach(header => {
58
- header.addEventListener('click', () => this.toggleCategory(header));
59
- });
60
-
61
- // Video preview controls
62
- document
63
- .getElementById('playPauseBtn')
64
- ?.addEventListener('click', () => this.togglePlayback());
65
-
66
- // Timeline controls
67
- document
68
- .getElementById('zoomIn')
69
- ?.addEventListener('click', () => this.zoomTimeline(1.2));
70
- document
71
- .getElementById('zoomOut')
72
- ?.addEventListener('click', () => this.zoomTimeline(0.8));
73
-
74
- // Track drag and drop
75
- this.setupTrackDragDrop();
76
-
77
- // Timeline ruler click to seek
78
- this.setupTimelineRulerClick();
79
-
80
- // Timeline scroll synchronization
81
- this.setupTimelineScrollSync();
82
-
83
- // Video playback events
84
- this.setupVideoEvents();
85
-
86
- // Keyboard shortcuts
87
- document.addEventListener('keydown', e => this.handleKeyboard(e));
88
- }
89
-
90
- async loadDraftData() {
91
- try {
92
- // 加载draft数据
93
- const response = await fetch('/api/draft');
94
- if (!response.ok) {
95
- console.warn('No draft file found, using empty project');
96
- this.initEmptyProject();
97
- return;
98
- }
99
-
100
- const draftData = await response.json();
101
- console.log('Loaded draft data:', draftData);
102
-
103
- // 设置项目信息
104
- this.projectName = draftData.project?.name || 'Untitled Project';
105
- this.projectId = draftData.project?.id || 'untitled';
106
-
107
- // 加载素材
108
- this.loadAssetsFromDraft(draftData.assets || []);
109
-
110
- // 加载时间轴轨道
111
- this.loadTracksFromDraft(draftData.timeline?.tracks || []);
112
-
113
- // 加载字幕数据
114
- this.loadSubtitlesFromDraft(draftData.subtitles || []);
115
-
116
- // 重置音频播放(确保新项目或草稿加载后清理旧的音频)
117
- this.resetAudioPlayback();
118
-
119
- // 计算总时长
120
- this.calculateTotalDuration();
121
-
122
- console.log('Draft loaded successfully');
123
- } catch (error) {
124
- console.error('Failed to load draft:', error);
125
- this.initEmptyProject();
126
- }
127
- }
128
-
129
- loadAssetsFromDraft(assets) {
130
- // 清空现有素材
131
- this.assets = {
132
- video: [],
133
- audio: [],
134
- image: [],
135
- text: [],
136
- };
137
-
138
- // 按类型分类素材
139
- assets.forEach(asset => {
140
- const assetItem = {
141
- id: asset.id,
142
- name: asset.uri.split('/').pop() || asset.id,
143
- duration: (asset.durationMs || 0) / 1000,
144
- type: asset.type,
145
- url: `/static/${asset.uri}`, // 修正URL路径
146
- uri: asset.uri,
147
- fps: asset.fps,
148
- resolution: asset.resolution,
149
- };
150
-
151
- switch (asset.type) {
152
- case 'video':
153
- this.assets.video.push(assetItem);
154
- break;
155
- case 'audio':
156
- this.assets.audio.push(assetItem);
157
- break;
158
- case 'image':
159
- this.assets.image.push(assetItem);
160
- break;
161
- default:
162
- console.warn('Unknown asset type:', asset.type);
163
- }
164
- });
165
-
166
- console.log('Loaded assets:', this.assets);
167
- }
168
-
169
- loadTracksFromDraft(tracks) {
170
- // 清空现有轨道
171
- this.tracks = {
172
- video: [],
173
- audio: [],
174
- bgm: [],
175
- };
176
-
177
- // 标记是否已创建主视频轨道
178
- let mainVideoTrackCreated = false;
179
- // 标记是否已创建背景音乐轨道
180
- let bgmTrackCreated = false;
181
-
182
- tracks.forEach(track => {
183
- const trackData = {
184
- id: track.id,
185
- type: track.type,
186
- clips: track.clips.map(clip => ({
187
- ...clip,
188
- startTime: clip.startMs / 1000,
189
- duration: clip.durationMs / 1000,
190
- inPoint: clip.inMs / 1000,
191
- })),
192
- };
193
-
194
- // 根据轨道类型分配到对应的轨道组
195
- switch (track.type) {
196
- case 'video':
197
- // 只创建一个主视频轨道,忽略其他video tracks
198
- if (!mainVideoTrackCreated) {
199
- trackData.name = '主视频轨道';
200
- this.tracks.video.push(trackData);
201
- mainVideoTrackCreated = true;
202
- console.log('Created main video track:', trackData.id);
203
- } else {
204
- console.log('Ignored additional video track:', trackData.id);
205
- }
206
- break;
207
- case 'audio': {
208
- // 根据轨道ID或素材URI判断音频类型
209
- const firstClip = trackData.clips[0];
210
- let isBgmTrack = false;
211
-
212
- // 检查轨道ID是否包含bgm
213
- if (track.id.toLowerCase().includes('bgm')) {
214
- isBgmTrack = true;
215
- } else if (firstClip) {
216
- // 检查素材URI是否包含bgm
217
- const asset = this.findAssetById(firstClip.assetId);
218
- if (
219
- asset &&
220
- (asset.uri.includes('bgm') || asset.id.includes('bgm'))
221
- ) {
222
- isBgmTrack = true;
223
- }
224
- }
225
-
226
- if (isBgmTrack && !bgmTrackCreated) {
227
- // 第一个包含bgm的轨道作为背景音乐
228
- trackData.name = '背景音乐';
229
- this.tracks.bgm.push(trackData);
230
- bgmTrackCreated = true;
231
- console.log('Created BGM track:', trackData.id);
232
- } else if (!isBgmTrack) {
233
- // 其他音频轨道
234
- trackData.name = `音频轨道 ${this.tracks.audio.length + 1}`;
235
- this.tracks.audio.push(trackData);
236
- console.log('Created audio track:', trackData.id);
237
- } else {
238
- console.log('Ignored additional BGM track:', trackData.id);
239
- }
240
- break;
241
- }
242
- case 'subtitle':
243
- // 暂时忽略字幕轨道
244
- console.log('Ignored subtitle track:', trackData.id);
245
- break;
246
- default:
247
- console.warn('Unknown track type:', track.type);
248
- }
249
- });
250
-
251
- console.log('Loaded tracks:', this.tracks);
252
- }
253
-
254
- loadSubtitlesFromDraft(subtitles) {
255
- // 清空现有字幕
256
- this.subtitles = [];
257
-
258
- // 加载字幕数据
259
- subtitles.forEach(subtitle => {
260
- this.subtitles.push({
261
- id: subtitle.id,
262
- text: subtitle.text,
263
- startMs: subtitle.startMs,
264
- endMs: subtitle.endMs,
265
- style: subtitle.style || {},
266
- });
267
- });
268
-
269
- console.log('Loaded subtitles:', this.subtitles);
270
- }
271
-
272
- calculateTotalDuration() {
273
- let maxDuration = 0;
274
-
275
- // 遍历所有轨道的所有片段,找到最大结束时间
276
- Object.values(this.tracks).forEach(trackGroup => {
277
- trackGroup.forEach(track => {
278
- track.clips.forEach(clip => {
279
- const endTime = (clip.startMs + clip.durationMs) / 1000;
280
- maxDuration = Math.max(maxDuration, endTime);
281
- });
282
- });
283
- });
284
-
285
- this.totalDuration = maxDuration;
286
- console.log('Total duration calculated:', this.totalDuration, 'seconds');
287
- }
288
-
289
- initEmptyProject() {
290
- this.projectName = 'New Project';
291
- this.projectId = 'new-project';
292
- this.assets = {
293
- video: [],
294
- audio: [],
295
- image: [],
296
- text: [],
297
- };
298
- this.tracks = {
299
- video: [],
300
- audio: [],
301
- bgm: [],
302
- voice: [],
303
- subtitle: [],
304
- };
305
- this.totalDuration = 0;
306
- }
307
-
308
- renderAssets() {
309
- // Render video assets
310
- const videoList = document.getElementById('videoAssets');
311
- if (videoList) {
312
- videoList.innerHTML = '';
313
- this.assets.video.forEach(asset => {
314
- const li = this.createAssetElement(asset);
315
- videoList.appendChild(li);
316
- });
317
- }
318
-
319
- // Render audio assets
320
- const audioList = document.getElementById('audioAssets');
321
- if (audioList) {
322
- audioList.innerHTML = '';
323
- this.assets.audio.forEach(asset => {
324
- const li = this.createAssetElement(asset);
325
- audioList.appendChild(li);
326
- });
327
- }
328
-
329
- // Render image assets
330
- const imageList = document.getElementById('imageAssets');
331
- if (imageList) {
332
- imageList.innerHTML = '';
333
- this.assets.image.forEach(asset => {
334
- const li = this.createAssetElement(asset);
335
- imageList.appendChild(li);
336
- });
337
- }
338
-
339
- // Render text assets
340
- const textList = document.getElementById('textAssets');
341
- if (textList) {
342
- textList.innerHTML = '';
343
- this.assets.text.forEach(asset => {
344
- const li = this.createAssetElement(asset);
345
- textList.appendChild(li);
346
- });
347
- }
348
-
349
- // Auto-select the first video asset if available
350
- if (this.assets.video.length > 0 && !this.selectedAsset) {
351
- this.selectAsset(this.assets.video[0]);
352
- }
353
- }
354
-
355
- createAssetElement(asset) {
356
- const li = document.createElement('li');
357
- li.className = 'asset-item';
358
- li.draggable = true;
359
- li.dataset.assetId = asset.id;
360
- li.dataset.assetType = asset.type;
361
-
362
- const icon = this.getAssetIcon(asset.type);
363
- const duration =
364
- asset.duration > 0 ? ` (${this.formatTime(asset.duration)})` : '';
365
-
366
- li.innerHTML = `
367
- <span class="asset-icon">${icon}</span>
368
- <span class="asset-name">${asset.name}${duration}</span>
369
- `;
370
-
371
- // Asset selection
372
- li.addEventListener('click', () => this.selectAsset(asset));
373
-
374
- // Drag start
375
- li.addEventListener('dragstart', e => {
376
- e.dataTransfer.setData(
377
- 'text/plain',
378
- JSON.stringify({
379
- assetId: asset.id,
380
- assetType: asset.type,
381
- })
382
- );
383
- });
384
-
385
- return li;
386
- }
387
-
388
- getAssetIcon(type) {
389
- const icons = {
390
- video: '🎬',
391
- audio: '🎵',
392
- image: '🖼️',
393
- text: '📝',
394
- };
395
- return icons[type] || '📄';
396
- }
397
-
398
- selectAsset(asset) {
399
- // Remove previous selection
400
- document.querySelectorAll('.asset-item.selected').forEach(item => {
401
- item.classList.remove('selected');
402
- });
403
-
404
- // Add selection to current asset
405
- const assetElement = document.querySelector(
406
- `[data-asset-id="${asset.id}"]`
407
- );
408
- if (assetElement) {
409
- assetElement.classList.add('selected');
410
- }
411
-
412
- // Clear clip selection when selecting asset
413
- this.selectedClip = null;
414
- document.querySelectorAll('.timeline-clip.selected').forEach(clipEl => {
415
- clipEl.classList.remove('selected');
416
- });
417
-
418
- this.selectedAsset = asset;
419
-
420
- // Update video preview for video assets
421
- const video = document.getElementById('previewVideo');
422
- const placeholder = document.getElementById('videoPlaceholder');
423
-
424
- if (asset.type === 'video' && video && placeholder) {
425
- // 清除之前的事件监听器,避免意外触发
426
- video.onloadeddata = null;
427
- video.onended = null;
428
- video.ontimeupdate = null;
429
-
430
- video.src = `/static/${asset.uri}`;
431
- video.style.display = 'block';
432
- placeholder.style.display = 'none';
433
-
434
- // 停止当前播放,切换到选中的资源
435
- if (this.isPlaying) {
436
- this.stopPlayback();
437
- }
438
- } else if (video && placeholder) {
439
- video.style.display = 'none';
440
- placeholder.style.display = 'flex';
441
- }
442
-
443
- this.updatePropertiesPanel();
444
- }
445
-
446
- renderTimeline() {
447
- // Clear existing clips and tracks
448
- document.querySelectorAll('.timeline-clip').forEach(clip => clip.remove());
449
-
450
- // 重置播放clips列表
451
- this.playbackClips = [];
452
-
453
- // 动态渲染轨道
454
- this.renderDynamicTracks();
455
-
456
- // 按开始时间排序播放列表
457
- this.playbackClips.sort((a, b) => a.startTime - b.startTime);
458
-
459
- this.updateTimelineInfo();
460
- // 只有在播放状态下才更新播放指示器,避免暂停时重置位置
461
- if (this.isPlaying) {
462
- this.updatePlaybackIndicator();
463
- }
464
- }
465
-
466
- renderDynamicTracks() {
467
- const tracksContainer = document.getElementById('timelineTracks');
468
- if (!tracksContainer) return;
469
-
470
- // 清空现有轨道
471
- tracksContainer.innerHTML = '';
472
-
473
- // 1. 渲染主视频轨道
474
- if (this.tracks.video && this.tracks.video.length > 0) {
475
- const videoTrack = this.tracks.video[0];
476
- this.createTrackElement(
477
- tracksContainer,
478
- 'video',
479
- '主视频轨道',
480
- videoTrack
481
- );
482
- }
483
-
484
- // 2. 渲染背景音乐轨道
485
- if (this.tracks.bgm && this.tracks.bgm.length > 0) {
486
- const bgmTrack = this.tracks.bgm[0];
487
- this.createTrackElement(tracksContainer, 'bgm', '背景音乐', bgmTrack);
488
- }
489
-
490
- // 3. 渲染音频轨道
491
- if (this.tracks.audio && this.tracks.audio.length > 0) {
492
- this.tracks.audio.forEach((audioTrack, index) => {
493
- const trackName = audioTrack.name || `音频轨道 ${index + 1}`;
494
- this.createTrackElement(
495
- tracksContainer,
496
- 'audio',
497
- trackName,
498
- audioTrack
499
- );
500
- });
501
- }
502
-
503
- // 4. 渲染字幕轨道
504
- if (this.subtitles && this.subtitles.length > 0) {
505
- this.createSubtitleTrackElement(tracksContainer);
506
- }
507
- }
508
-
509
- createTrackElement(container, trackType, trackName, trackData) {
510
- // 创建轨道容器
511
- const trackContainer = document.createElement('div');
512
- trackContainer.className = 'track-container';
513
-
514
- // 创建轨道头部
515
- const trackHeader = document.createElement('div');
516
- trackHeader.className = 'track-header';
517
- trackHeader.textContent = trackName;
518
-
519
- // 创建轨道内容区域
520
- const trackContent = document.createElement('div');
521
- trackContent.className = 'track-content';
522
- trackContent.id = `${trackType}Track_${trackData.id}`;
523
- trackContent.dataset.trackType = trackType;
524
- trackContent.dataset.trackId = trackData.id;
525
-
526
- // 添加拖拽支持
527
- trackContent.addEventListener('dragover', e => {
528
- e.preventDefault();
529
- e.dataTransfer.dropEffect = 'copy';
530
- });
531
-
532
- trackContent.addEventListener('drop', e => {
533
- e.preventDefault();
534
- const assetId = e.dataTransfer.getData('text/plain');
535
- const rect = trackContent.getBoundingClientRect();
536
- const dropTime =
537
- (e.clientX - rect.left) / (this.pixelsPerSecond * this.zoomLevel);
538
- this.addClipToTrack(assetId, trackType, dropTime);
539
- });
540
-
541
- // 渲染轨道中的片段
542
- if (trackData.clips) {
543
- trackData.clips.forEach(clip => {
544
- const clipElement = this.createClipElement(clip, trackType);
545
- if (clipElement) {
546
- trackContent.appendChild(clipElement);
547
- }
548
-
549
- // 如果是视频类型的clip,添加到播放列表
550
- if (trackType === 'video') {
551
- const asset = this.findAssetById(clip.assetId);
552
- if (asset) {
553
- this.playbackClips.push({
554
- clip: clip,
555
- asset: asset,
556
- startTime: clip.startTime || 0,
557
- duration: clip.duration || 0, // 使用clip的duration
558
- });
559
- }
560
- }
561
- });
562
- }
563
-
564
- // 组装轨道元素
565
- trackContainer.appendChild(trackHeader);
566
- trackContainer.appendChild(trackContent);
567
- container.appendChild(trackContainer);
568
- }
569
-
570
- createClipElement(clip, trackType) {
571
- const asset = this.findAssetById(clip.assetId);
572
- if (!asset) return null;
573
-
574
- const clipDiv = document.createElement('div');
575
- clipDiv.className = `timeline-clip ${trackType}`;
576
- clipDiv.dataset.clipId = clip.id;
577
- clipDiv.dataset.trackType = trackType;
578
-
579
- const leftPos = clip.startTime * this.pixelsPerSecond * this.zoomLevel;
580
- const width = clip.duration * this.pixelsPerSecond * this.zoomLevel;
581
-
582
- clipDiv.style.left = `${leftPos}px`;
583
- clipDiv.style.width = `${width}px`;
584
- clipDiv.textContent = asset.name;
585
-
586
- // Clip selection
587
- clipDiv.addEventListener('click', e => {
588
- e.stopPropagation();
589
- this.selectClip(clip);
590
- });
591
-
592
- // Make clips draggable within tracks
593
- clipDiv.draggable = true;
594
- clipDiv.addEventListener('dragstart', e => {
595
- e.dataTransfer.setData(
596
- 'text/plain',
597
- JSON.stringify({
598
- clipId: clip.id,
599
- trackType: trackType,
600
- })
601
- );
602
- });
603
-
604
- return clipDiv;
605
- }
606
-
607
- selectClip(clip) {
608
- // Remove previous selection
609
- document.querySelectorAll('.timeline-clip.selected').forEach(clipEl => {
610
- clipEl.classList.remove('selected');
611
- });
612
-
613
- // Add selection to current clip
614
- const clipElement = document.querySelector(`[data-clip-id="${clip.id}"]`);
615
- if (clipElement) {
616
- clipElement.classList.add('selected');
617
- }
618
-
619
- // Clear asset selection when selecting clip
620
- this.selectedAsset = null;
621
- document.querySelectorAll('.asset-item.selected').forEach(item => {
622
- item.classList.remove('selected');
623
- });
624
-
625
- this.selectedClip = clip;
626
- this.updatePropertiesPanel();
627
- }
628
-
629
- createSubtitleTrackElement(container) {
630
- // 创建字幕轨道容器
631
- const trackContainer = document.createElement('div');
632
- trackContainer.className = 'track-container';
633
-
634
- // 创建轨道头部
635
- const trackHeader = document.createElement('div');
636
- trackHeader.className = 'track-header';
637
- trackHeader.textContent = '字幕轨道';
638
-
639
- // 创建轨道内容区域
640
- const trackContent = document.createElement('div');
641
- trackContent.className = 'track-content';
642
- trackContent.id = 'subtitleTrack';
643
- trackContent.dataset.trackType = 'subtitle';
644
-
645
- // 渲染字幕片段
646
- this.subtitles.forEach(subtitle => {
647
- const subtitleElement = this.createSubtitleClipElement(subtitle);
648
- if (subtitleElement) {
649
- trackContent.appendChild(subtitleElement);
650
- }
651
- });
652
-
653
- // 组装轨道元素
654
- trackContainer.appendChild(trackHeader);
655
- trackContainer.appendChild(trackContent);
656
- container.appendChild(trackContainer);
657
- }
658
-
659
- createSubtitleClipElement(subtitle) {
660
- const clipDiv = document.createElement('div');
661
- clipDiv.className = 'timeline-clip subtitle';
662
- clipDiv.dataset.clipId = subtitle.id;
663
- clipDiv.dataset.trackType = 'subtitle';
664
-
665
- // 计算位置和宽度(字幕使用毫秒,需要转换为秒)
666
- const startTime = subtitle.startMs / 1000;
667
- const endTime = subtitle.endMs / 1000;
668
- const duration = endTime - startTime;
669
-
670
- const leftPos = startTime * this.pixelsPerSecond * this.zoomLevel;
671
- const width = duration * this.pixelsPerSecond * this.zoomLevel;
672
-
673
- clipDiv.style.left = `${leftPos}px`;
674
- clipDiv.style.width = `${width}px`;
675
- clipDiv.textContent = subtitle.text;
676
-
677
- // 字幕片段选择
678
- clipDiv.addEventListener('click', e => {
679
- e.stopPropagation();
680
- this.selectSubtitleClip(subtitle);
681
- });
682
-
683
- return clipDiv;
684
- }
685
-
686
- selectSubtitleClip(subtitle) {
687
- // 移除之前的选择
688
- document.querySelectorAll('.timeline-clip.selected').forEach(clipEl => {
689
- clipEl.classList.remove('selected');
690
- });
691
-
692
- // 选择当前字幕片段
693
- const clipElement = document.querySelector(
694
- `[data-clip-id="${subtitle.id}"]`
695
- );
696
- if (clipElement) {
697
- clipElement.classList.add('selected');
698
- }
699
-
700
- // 清除资源选择
701
- this.selectedAsset = null;
702
- document.querySelectorAll('.asset-item.selected').forEach(item => {
703
- item.classList.remove('selected');
704
- });
705
-
706
- this.selectedClip = null;
707
- this.selectedSubtitle = subtitle;
708
- this.updatePropertiesPanel();
709
- }
710
-
711
- setupTrackDragDrop() {
712
- document.querySelectorAll('.track-content').forEach(track => {
713
- track.addEventListener('dragover', e => {
714
- e.preventDefault();
715
- track.classList.add('drag-over');
716
- });
717
-
718
- track.addEventListener('dragleave', () => {
719
- track.classList.remove('drag-over');
720
- });
721
-
722
- track.addEventListener('drop', e => {
723
- e.preventDefault();
724
- track.classList.remove('drag-over');
725
-
726
- const data = JSON.parse(e.dataTransfer.getData('text/plain'));
727
- const rect = track.getBoundingClientRect();
728
-
729
- // 获取当前滚动偏移量,因为指示器使用了 translateX 同步滚动
730
- const timelineTracks = document.querySelector('.timeline-tracks');
731
- const scrollLeft = timelineTracks ? timelineTracks.scrollLeft : 0;
732
-
733
- // 计算拖拽位置时需要加上滚动偏移量
734
- const dropX = e.clientX - rect.left + scrollLeft;
735
- const dropTime = dropX / (this.pixelsPerSecond * this.zoomLevel);
736
-
737
- if (data.assetId) {
738
- // Dropping an asset from the asset panel
739
- this.addClipToTrack(data.assetId, track.dataset.trackType, dropTime);
740
- } else if (data.clipId) {
741
- // Moving an existing clip
742
- this.moveClip(data.clipId, track.dataset.trackType, dropTime);
743
- }
744
- });
745
- });
746
- }
747
-
748
- addClipToTrack(assetId, trackType, startTime) {
749
- const asset = this.findAssetById(assetId);
750
- if (!asset) return;
751
-
752
- // Validate track type compatibility
753
- if (!this.isAssetCompatibleWithTrack(asset.type, trackType)) {
754
- alert(`无法将${asset.type}素材添加到${trackType}轨道`);
755
- return;
756
- }
757
-
758
- const newClip = {
759
- id: `clip_${Date.now()}`,
760
- assetId: assetId,
761
- startTime: Math.max(0, startTime),
762
- duration: asset.duration || 5, // Default 5 seconds for images/text
763
- trackType: trackType,
764
- };
765
-
766
- if (!this.tracks[trackType]) {
767
- this.tracks[trackType] = [];
768
- }
769
-
770
- this.tracks[trackType].push(newClip);
771
- this.renderTimeline();
772
- this.selectClip(newClip);
773
- }
774
-
775
- moveClip(clipId, newTrackType, newStartTime) {
776
- // Find and remove clip from current track
777
- let clip = null;
778
- Object.keys(this.tracks).forEach(trackType => {
779
- const index = this.tracks[trackType].findIndex(c => c.id === clipId);
780
- if (index !== -1) {
781
- clip = this.tracks[trackType].splice(index, 1)[0];
782
- }
783
- });
784
-
785
- if (!clip) return;
786
-
787
- // Update clip properties
788
- clip.trackType = newTrackType;
789
- clip.startTime = Math.max(0, newStartTime);
790
-
791
- // Add to new track
792
- if (!this.tracks[newTrackType]) {
793
- this.tracks[newTrackType] = [];
794
- }
795
-
796
- this.tracks[newTrackType].push(clip);
797
- this.renderTimeline();
798
- this.selectClip(clip);
799
- }
800
-
801
- isAssetCompatibleWithTrack(assetType, trackType) {
802
- const compatibility = {
803
- video: ['video'],
804
- audio: ['audio', 'bgm', 'voice'],
805
- image: ['video'],
806
- text: ['subtitle'],
807
- };
808
-
809
- return compatibility[assetType]?.includes(trackType) || false;
810
- }
811
-
812
- updateTimelineRuler() {
813
- const ruler = document.getElementById('timelineRuler');
814
- if (!ruler) return;
815
-
816
- ruler.innerHTML = '';
817
-
818
- // 创建一个容器来包含所有标尺标记,这样可以使用 translateX 移动
819
- const rulerContent = document.createElement('div');
820
- rulerContent.className = 'ruler-content';
821
- rulerContent.style.position = 'relative';
822
- rulerContent.style.width = 'max-content';
823
-
824
- const timelineWidth =
825
- this.totalDuration * this.pixelsPerSecond * this.zoomLevel;
826
- const interval = this.zoomLevel >= 1 ? 10 : 30; // seconds between markers
827
-
828
- for (let time = 0; time <= this.totalDuration; time += interval) {
829
- const marker = document.createElement('div');
830
- marker.style.left = `${time * this.pixelsPerSecond * this.zoomLevel}px`;
831
- marker.textContent = this.formatTime(time);
832
- rulerContent.appendChild(marker);
833
- }
834
-
835
- ruler.appendChild(rulerContent);
836
- }
837
-
838
- updateTimelineInfo() {
839
- const info = document.getElementById('timelineInfo');
840
- if (info) {
841
- info.textContent = `缩放: ${Math.round(this.zoomLevel * 100)}% | 总时长: ${this.formatTime(this.totalDuration)}`;
842
- }
843
- }
844
-
845
- zoomTimeline(factor) {
846
- this.zoomLevel = Math.max(0.1, Math.min(5, this.zoomLevel * factor));
847
- this.renderTimeline();
848
- this.updateTimelineRuler();
849
- this.updateTimelineInfo();
850
- // 只有在播放状态下才更新播放指示器位置,避免暂停时重置位置
851
- if (this.isPlaying) {
852
- this.updatePlaybackIndicator();
853
- }
854
- }
855
-
856
- togglePlayback() {
857
- const btn = document.getElementById('playPauseBtn');
858
- const video = document.getElementById('previewVideo');
859
-
860
- if (this.isPlaying) {
861
- // 暂停播放,保存当前播放位置
862
- this.isPlaying = false;
863
- this.stopPlaybackIndicatorAnimation();
864
-
865
- // 保存当前时间轴位置
866
- console.log('Pausing - currentClipIndex:', this.currentClipIndex);
867
- console.log('Pausing - playbackClips length:', this.playbackClips.length);
868
- console.log(
869
- 'Pausing - video currentTime:',
870
- video ? video.currentTime : 'no video'
871
- );
872
-
873
- if (
874
- this.currentClipIndex < this.playbackClips.length &&
875
- this.playbackClips[this.currentClipIndex]
876
- ) {
877
- const currentClip = this.playbackClips[this.currentClipIndex];
878
- const videoCurrentTime = video ? video.currentTime || 0 : 0;
879
- this.currentTime = currentClip.startTime + videoCurrentTime;
880
- console.log(
881
- 'Saved currentTime:',
882
- this.currentTime,
883
- '(startTime:',
884
- currentClip.startTime,
885
- '+ videoTime:',
886
- videoCurrentTime,
887
- ')'
888
- );
889
- } else {
890
- console.log('Cannot save position - invalid clip index or no clips');
891
- this.currentTime = 0;
892
- }
893
-
894
- if (btn) btn.textContent = '▶️';
895
- if (video && !video.paused) {
896
- video.pause();
897
- }
898
- // 同步暂停音频播放
899
- this.pauseAudioPlayback();
900
- } else {
901
- // 继续播放
902
- this.isPlaying = true;
903
- if (btn) btn.textContent = '⏸️';
904
-
905
- console.log('Resuming playback with currentTime:', this.currentTime);
906
- // 从当前位置继续播放
907
- this.resumePlayback();
908
- }
909
- }
910
-
911
- // 从当前位置继续播放
912
- resumePlayback() {
913
- if (this.playbackClips.length === 0) {
914
- console.log('没有可播放的视频片段');
915
- return;
916
- }
917
-
918
- // 如果还没有开始播放过,从头开始
919
- if (this.currentClipIndex === undefined || this.currentClipIndex < 0) {
920
- this.startTimelinePlayback();
921
- return;
922
- }
923
-
924
- // 从保存的位置继续播放
925
- this.resumeFromCurrentPosition();
926
- }
927
-
928
- startTimelinePlayback() {
929
- if (this.playbackClips.length === 0) {
930
- console.log('没有可播放的视频片段');
931
- return;
932
- }
933
-
934
- // 重置播放索引到开头
935
- this.currentClipIndex = 0;
936
- // 重置当前时间到开头
937
- this.currentTime = 0;
938
-
939
- // 开始动画循环更新播放指示器
940
- this.startPlaybackIndicatorAnimation();
941
-
942
- // 按时间轴调度音频播放(从时间0开始)
943
- this.startAudioPlaybackAtTime(0);
944
-
945
- this.playCurrentClip();
946
- }
947
-
948
- resumeFromCurrentPosition() {
949
- console.log(
950
- 'resumeFromCurrentPosition called, currentTime:',
951
- this.currentTime
952
- );
953
-
954
- if (!this.currentTime || this.currentTime === 0) {
955
- // 如果没有保存的位置,从头开始
956
- console.log('No saved position, starting from beginning');
957
- // 从头开始时需要调度音频播放
958
- this.startAudioPlaybackAtTime(0);
959
- this.playCurrentClip();
960
- return;
961
- }
962
-
963
- // 根据保存的时间位置找到对应的片段和片段内的时间
964
- let accumulatedTime = 0;
965
- let targetClipIndex = 0;
966
- let targetClipTime = 0;
967
-
968
- console.log(
969
- 'Searching for target clip, total clips:',
970
- this.playbackClips.length
971
- );
972
-
973
- for (let i = 0; i < this.playbackClips.length; i++) {
974
- const clip = this.playbackClips[i];
975
- const clipDuration = clip.duration;
976
-
977
- console.log(
978
- `Clip ${i}: startTime=${clip.startTime}, duration=${clipDuration}, accumulatedTime=${accumulatedTime}`
979
- );
980
-
981
- if (accumulatedTime + clipDuration > this.currentTime) {
982
- // 找到了目标片段
983
- targetClipIndex = i;
984
- targetClipTime = this.currentTime - accumulatedTime;
985
- console.log(
986
- `Found target clip ${targetClipIndex}, targetClipTime=${targetClipTime}`
987
- );
988
- break;
989
- }
990
-
991
- accumulatedTime += clipDuration;
992
- }
993
-
994
- // 设置当前片段索引
995
- this.currentClipIndex = targetClipIndex;
996
- console.log('Set currentClipIndex to:', this.currentClipIndex);
997
-
998
- // 播放目标片段并设置到指定时间
999
- const currentPlaybackItem = this.playbackClips[this.currentClipIndex];
1000
- const video = document.getElementById('previewVideo');
1001
-
1002
- if (video && currentPlaybackItem) {
1003
- // 临时停止播放指示器更新,避免闪烁
1004
- const wasPlaying = this.isPlaying;
1005
- this.isPlaying = false;
1006
-
1007
- // 清除之前的事件监听器,避免意外触发
1008
- video.onloadeddata = null;
1009
- video.onended = null;
1010
- video.ontimeupdate = null;
1011
-
1012
- // 设置视频源
1013
- video.src = `/static/${currentPlaybackItem.asset.uri}`;
1014
- console.log('Set video source to:', video.src);
1015
-
1016
- // 监听视频加载完成事件
1017
- video.onloadeddata = () => {
1018
- if (wasPlaying) {
1019
- // 设置到指定时间点
1020
- console.log('Setting video currentTime to:', targetClipTime);
1021
- video.currentTime = targetClipTime;
1022
-
1023
- // 恢复播放状态
1024
- this.isPlaying = true;
1025
-
1026
- // 重新启动requestAnimationFrame动画
1027
- this.startPlaybackIndicatorAnimation();
1028
-
1029
- // 根据保存的时间位置调度音频播放
1030
- this.startAudioPlaybackAtTime(this.currentTime);
1031
-
1032
- video.play().catch(err => {
1033
- console.error('播放视频失败:', err);
1034
- this.playNextClip();
1035
- });
1036
- }
1037
- };
1038
-
1039
- // 监听视频时间更新事件
1040
- video.ontimeupdate = () => {
1041
- this.updateTimeDisplay();
1042
- };
1043
-
1044
- // 监听视频播放结束事件
1045
- video.onended = () => {
1046
- console.log(
1047
- '视频播放结束,当前片段索引:',
1048
- this.currentClipIndex,
1049
- '总片段数:',
1050
- this.playbackClips.length
1051
- );
1052
- this.playNextClip();
1053
- };
1054
-
1055
- // 如果视频已经加载,直接设置时间并播放
1056
- if (video.readyState >= 2 && wasPlaying) {
1057
- console.log(
1058
- 'Video already loaded, setting currentTime to:',
1059
- targetClipTime
1060
- );
1061
- video.currentTime = targetClipTime;
1062
-
1063
- // 恢复播放状态
1064
- this.isPlaying = true;
1065
-
1066
- // 重新启动requestAnimationFrame动画
1067
- this.startPlaybackIndicatorAnimation();
1068
-
1069
- // 根据保存的时间位置调度音频播放
1070
- this.startAudioPlaybackAtTime(this.currentTime);
1071
-
1072
- video.play().catch(err => {
1073
- console.error('播放视频失败:', err);
1074
- this.playNextClip();
1075
- });
1076
- }
1077
- }
1078
- }
1079
-
1080
- playCurrentClip() {
1081
- console.log('playCurrentClip调用,索引:', this.currentClipIndex);
1082
- if (this.currentClipIndex >= this.playbackClips.length) {
1083
- console.log('播放完成,重新从头开始循环播放');
1084
- // 播放完成,重新从头开始循环播放
1085
- this.currentClipIndex = 0;
1086
- this.currentTime = 0;
1087
- // 重新调度整条时间轴的音频播放
1088
- this.startAudioPlaybackAtTime(0);
1089
- }
1090
-
1091
- const currentPlaybackItem = this.playbackClips[this.currentClipIndex];
1092
- const video = document.getElementById('previewVideo');
1093
-
1094
- console.log('当前播放项目:', currentPlaybackItem);
1095
-
1096
- if (video && currentPlaybackItem) {
1097
- // 清除之前的事件监听器,避免重复绑定
1098
- video.onloadeddata = null;
1099
- video.ontimeupdate = null;
1100
- video.onended = null;
1101
-
1102
- // 设置视频源
1103
- video.src = `/static/${currentPlaybackItem.asset.uri}`;
1104
- console.log('设置视频源:', video.src);
1105
-
1106
- // 监听视频加载完成事件
1107
- video.onloadeddata = () => {
1108
- console.log(
1109
- '视频加载完成,持续时间:',
1110
- video.duration,
1111
- '当前时间:',
1112
- video.currentTime
1113
- );
1114
-
1115
- // 计算播放速度:asset.duration / clip.duration
1116
- const assetDuration =
1117
- currentPlaybackItem.asset.duration || video.duration;
1118
- const clipDuration = currentPlaybackItem.duration;
1119
- const playbackRate = assetDuration / clipDuration;
1120
-
1121
- console.log(
1122
- `设置播放速度: asset.duration=${assetDuration}, clip.duration=${clipDuration}, playbackRate=${playbackRate}`
1123
- );
1124
- video.playbackRate = playbackRate;
1125
-
1126
- if (this.isPlaying) {
1127
- // 更新当前时间为该片段的开始时间
1128
- this.currentTime = currentPlaybackItem.startTime;
1129
-
1130
- // 启动requestAnimationFrame动画
1131
- this.startPlaybackIndicatorAnimation();
1132
- // 开始按时间轴调度音频播放
1133
- // 注意:首次播放时已在startTimelinePlayback里调度,这里不重复调度,避免中途切片时重置音频
1134
-
1135
- video.play().catch(err => {
1136
- console.error('播放视频失败:', err);
1137
- this.playNextClip();
1138
- });
1139
- }
1140
- };
1141
-
1142
- // 监听视频时间更新事件 - 移除直接更新,改用requestAnimationFrame
1143
- video.ontimeupdate = () => {
1144
- this.updateTimeDisplay();
1145
- };
1146
-
1147
- // 监听视频播放结束事件
1148
- video.onended = () => {
1149
- this.playNextClip();
1150
- };
1151
-
1152
- // 如果视频已经加载,直接播放
1153
- if (video.readyState >= 2 && this.isPlaying) {
1154
- // 计算播放速度:asset.duration / clip.duration
1155
- const assetDuration =
1156
- currentPlaybackItem.asset.duration || video.duration;
1157
- const clipDuration = currentPlaybackItem.duration;
1158
- const playbackRate = assetDuration / clipDuration;
1159
-
1160
- console.log(
1161
- `设置播放速度: asset.duration=${assetDuration}, clip.duration=${clipDuration}, playbackRate=${playbackRate}`
1162
- );
1163
- video.playbackRate = playbackRate;
1164
-
1165
- // 更新当前时间为该片段的开始时间
1166
- this.currentTime = currentPlaybackItem.startTime;
1167
-
1168
- // 启动requestAnimationFrame动画
1169
- this.startPlaybackIndicatorAnimation();
1170
- // 开始按时间轴调度音频播放(避免与startTimelinePlayback重复调用,这里不额外调度)
1171
-
1172
- video.play().catch(err => {
1173
- console.error('播放视频失败:', err);
1174
- this.playNextClip();
1175
- });
1176
- }
1177
- }
1178
- }
1179
-
1180
- playNextClip() {
1181
- console.log(
1182
- 'playNextClip调用,当前索引:',
1183
- this.currentClipIndex,
1184
- '总片段数:',
1185
- this.playbackClips.length
1186
- );
1187
- if (this.isPlaying) {
1188
- this.currentClipIndex++;
1189
- console.log('播放下一个片段,新索引:', this.currentClipIndex);
1190
- this.playCurrentClip();
1191
- }
1192
- }
1193
-
1194
- stopPlayback() {
1195
- this.isPlaying = false;
1196
- // 不重置currentClipIndex和currentTime,保持当前位置
1197
- // this.currentClipIndex = 0;
1198
-
1199
- // 停止播放指示器动画循环
1200
- this.stopPlaybackIndicatorAnimation();
1201
-
1202
- const btn = document.getElementById('playPauseBtn');
1203
- const video = document.getElementById('previewVideo');
1204
- // 不隐藏播放指示器,保持在当前位置
1205
- // const indicator = document.getElementById('playbackIndicator');
1206
-
1207
- if (btn) btn.textContent = '▶️';
1208
- if (video && !video.paused) {
1209
- video.pause();
1210
- }
1211
- // 保持播放指示器显示在当前位置
1212
- // if (indicator) {
1213
- // indicator.style.display = 'none';
1214
- // }
1215
- }
1216
-
1217
- toggleCategory(header) {
1218
- const category = header.parentElement;
1219
- const assetList = category.querySelector('.asset-list');
1220
-
1221
- if (assetList.style.display === 'none') {
1222
- assetList.style.display = 'block';
1223
- header.style.opacity = '1';
1224
- } else {
1225
- assetList.style.display = 'none';
1226
- header.style.opacity = '0.7';
1227
- }
1228
- }
1229
-
1230
- searchAssets(query) {
1231
- const searchTerm = query.toLowerCase();
1232
-
1233
- document.querySelectorAll('.asset-item').forEach(item => {
1234
- const name = item.querySelector('.asset-name').textContent.toLowerCase();
1235
- if (name.includes(searchTerm)) {
1236
- item.style.display = 'flex';
1237
- } else {
1238
- item.style.display = 'none';
1239
- }
1240
- });
1241
- }
1242
-
1243
- updatePropertiesPanel() {
1244
- const content = document.getElementById('propertiesContent');
1245
- if (!content) return;
1246
-
1247
- if (this.selectedClip) {
1248
- content.innerHTML = this.generateClipProperties(this.selectedClip);
1249
- } else if (this.selectedSubtitle) {
1250
- content.innerHTML = this.generateSubtitleProperties(
1251
- this.selectedSubtitle
1252
- );
1253
- } else if (this.selectedAsset) {
1254
- content.innerHTML = this.generateAssetProperties(this.selectedAsset);
1255
- } else {
1256
- content.innerHTML =
1257
- '<div class="no-selection">选择素材或轨道元素以编辑属性</div>';
1258
- }
1259
- }
1260
-
1261
- generateClipProperties(clip) {
1262
- const asset = this.findAssetById(clip.assetId);
1263
- if (!asset) return '';
1264
-
1265
- return `
1266
- <div class="property-group">
1267
- <h4>基本属性</h4>
1268
- <div class="property-item">
1269
- <label class="property-label">名称</label>
1270
- <input type="text" class="property-input" value="${asset.name}" readonly>
1271
- </div>
1272
- <div class="property-item">
1273
- <label class="property-label">轨道类型</label>
1274
- <input type="text" class="property-input" value="${clip.trackType || '未知'}" readonly>
1275
- </div>
1276
- <div class="property-item">
1277
- <label class="property-label">开始时间 (秒)</label>
1278
- <input type="number" class="property-input" value="${clip.startTime}"
1279
- onchange="timelineEditor.updateClipProperty('${clip.id}', 'startTime', this.value)">
1280
- </div>
1281
- <div class="property-item">
1282
- <label class="property-label">持续时间 (秒)</label>
1283
- <input type="number" class="property-input" value="${clip.duration}"
1284
- onchange="timelineEditor.updateClipProperty('${clip.id}', 'duration', this.value)">
1285
- </div>
1286
- </div>
1287
- `;
1288
- }
1289
-
1290
- generateSubtitleProperties(subtitle) {
1291
- return `
1292
- <div class="property-group">
1293
- <h4>字幕属性</h4>
1294
- <div class="property-item">
1295
- <label class="property-label">文本内容</label>
1296
- <textarea class="property-input" rows="3"
1297
- onchange="timelineEditor.updateSubtitleProperty('${subtitle.id}', 'text', this.value)">${subtitle.text}</textarea>
1298
- </div>
1299
- <div class="property-item">
1300
- <label class="property-label">开始时间 (秒)</label>
1301
- <input type="number" class="property-input" value="${subtitle.startMs / 1000}" step="0.1"
1302
- onchange="timelineEditor.updateSubtitleProperty('${subtitle.id}', 'startMs', this.value * 1000)">
1303
- </div>
1304
- <div class="property-item">
1305
- <label class="property-label">结束时间 (秒)</label>
1306
- <input type="number" class="property-input" value="${subtitle.endMs / 1000}" step="0.1"
1307
- onchange="timelineEditor.updateSubtitleProperty('${subtitle.id}', 'endMs', this.value * 1000)">
1308
- </div>
1309
- <div class="property-item">
1310
- <label class="property-label">持续时间 (秒)</label>
1311
- <input type="text" class="property-input" value="${(subtitle.endMs - subtitle.startMs) / 1000}" readonly>
1312
- </div>
1313
- </div>
1314
-
1315
- <div class="property-group">
1316
- <h4>样式属性</h4>
1317
- <div class="property-item">
1318
- <label class="property-label">字体大小</label>
1319
- <input type="number" class="property-input" value="${subtitle.style?.fontSize || 16}"
1320
- onchange="timelineEditor.updateSubtitleStyle('${subtitle.id}', 'fontSize', this.value)">
1321
- </div>
1322
- <div class="property-item">
1323
- <label class="property-label">字体颜色</label>
1324
- <input type="color" class="property-input" value="${subtitle.style?.color || '#ffffff'}"
1325
- onchange="timelineEditor.updateSubtitleStyle('${subtitle.id}', 'color', this.value)">
1326
- </div>
1327
- </div>
1328
- `;
1329
- }
1330
-
1331
- generateAssetProperties(asset) {
1332
- return `
1333
- <div class="property-group">
1334
- <h4>素材信息</h4>
1335
- <div class="property-item">
1336
- <label class="property-label">名称</label>
1337
- <input type="text" class="property-input" value="${asset.name}" readonly>
1338
- </div>
1339
- <div class="property-item">
1340
- <label class="property-label">类型</label>
1341
- <input type="text" class="property-input" value="${asset.type}" readonly>
1342
- </div>
1343
- <div class="property-item">
1344
- <label class="property-label">时长</label>
1345
- <input type="text" class="property-input" value="${this.formatTime(asset.duration)}" readonly>
1346
- </div>
1347
- </div>
1348
- `;
1349
- }
1350
-
1351
- updateClipProperty(clipId, property, value) {
1352
- console.log('updateClipProperty called:', clipId, property, value);
1353
- // Find the clip in all tracks
1354
- Object.keys(this.tracks).forEach(trackType => {
1355
- const tracks = this.tracks[trackType];
1356
- let clip = null;
1357
- for (const track of tracks) {
1358
- if (track.clips.find(c => c.id === clipId)) {
1359
- clip = track.clips.find(c => c.id === clipId);
1360
- break;
1361
- }
1362
- }
1363
- if (clip) {
1364
- console.log('Found clip, updating property:', clip, property, value);
1365
- const numValue = parseFloat(value) || 0;
1366
- clip[property] = numValue;
1367
-
1368
- // 同步更新相关字段
1369
- if (property === 'duration') {
1370
- // 当更新 duration (秒) 时,同步更新 durationMs (毫秒)
1371
- clip.durationMs = numValue * 1000;
1372
- console.log('Synced durationMs:', clip.durationMs);
1373
- } else if (property === 'startTime') {
1374
- // 当更新 startTime (秒) 时,同步更新 startMs (毫秒)
1375
- clip.startMs = numValue * 1000;
1376
- console.log('Synced startMs:', clip.startMs);
1377
- } else if (property === 'inPoint') {
1378
- // 当更新 inPoint (秒) 时,同步更新 inMs (毫秒)
1379
- clip.inMs = numValue * 1000;
1380
- console.log('Synced inMs:', clip.inMs);
1381
- }
1382
-
1383
- console.log('Calling renderTimeline...');
1384
- this.renderTimeline();
1385
- this.updatePropertiesPanel(); // 更新属性面板以显示新的属性值
1386
- }
1387
- });
1388
- }
1389
-
1390
- updateSubtitleProperty(subtitleId, property, value) {
1391
- const subtitle = this.subtitles.find(s => s.id === subtitleId);
1392
- if (subtitle) {
1393
- if (property === 'startMs' || property === 'endMs') {
1394
- subtitle[property] = parseFloat(value) || 0;
1395
- } else {
1396
- subtitle[property] = value;
1397
- }
1398
- this.renderTimeline();
1399
- this.updatePropertiesPanel(); // 更新属性面板以显示新的持续时间
1400
- }
1401
- }
1402
-
1403
- updateSubtitleStyle(subtitleId, styleProperty, value) {
1404
- const subtitle = this.subtitles.find(s => s.id === subtitleId);
1405
- if (subtitle) {
1406
- if (!subtitle.style) {
1407
- subtitle.style = {};
1408
- }
1409
- if (styleProperty === 'fontSize') {
1410
- subtitle.style[styleProperty] = parseFloat(value) || 16;
1411
- } else {
1412
- subtitle.style[styleProperty] = value;
1413
- }
1414
- this.renderTimeline();
1415
- }
1416
- }
1417
-
1418
- findAssetById(assetId) {
1419
- for (const type of Object.keys(this.assets)) {
1420
- const asset = this.assets[type].find(a => a.id === assetId);
1421
- if (asset) return asset;
1422
- }
1423
- return null;
1424
- }
1425
-
1426
- // ===== 音频播放相关方法 =====
1427
- getAllAudioClipsWithAssets() {
1428
- const result = [];
1429
- const collect = (trackGroup = []) => {
1430
- trackGroup.forEach(track => {
1431
- track.clips.forEach(clip => {
1432
- const asset = this.findAssetById(clip.assetId);
1433
- if (asset) {
1434
- result.push({ clip, asset, trackType: track.type || 'audio' });
1435
- }
1436
- });
1437
- });
1438
- };
1439
-
1440
- collect(this.tracks.audio);
1441
- collect(this.tracks.bgm);
1442
- // 如果存在语音轨,可一并处理
1443
- if (this.tracks.voice && this.tracks.voice.length) {
1444
- collect(this.tracks.voice);
1445
- }
1446
-
1447
- return result;
1448
- }
1449
-
1450
- resetAudioPlayback() {
1451
- // 清除所有定时器
1452
- if (this.audioTimers && this.audioTimers.length) {
1453
- this.audioTimers.forEach(tid => clearTimeout(tid));
1454
- }
1455
- this.audioTimers = [];
1456
-
1457
- // 停止并移除所有音频元素
1458
- if (this.audioPlayers) {
1459
- Object.values(this.audioPlayers).forEach(audio => {
1460
- try {
1461
- audio.pause();
1462
- } catch (e) {
1463
- // 避免空的 catch 语句,记录调试信息
1464
- console.warn('resetAudioPlayback: pause() 失败', e);
1465
- }
1466
- try {
1467
- audio.src = '';
1468
- } catch (e) {
1469
- console.warn('resetAudioPlayback: 清空 src 失败', e);
1470
- }
1471
- try {
1472
- audio.remove();
1473
- } catch (e) {
1474
- console.warn('resetAudioPlayback: remove() 失败', e);
1475
- }
1476
- });
1477
- }
1478
- this.audioPlayers = {};
1479
- }
1480
-
1481
- pauseAudioPlayback() {
1482
- // 为了简化恢复逻辑,这里直接重置,恢复时重新根据时间轴进行调度
1483
- this.resetAudioPlayback();
1484
- }
1485
-
1486
- startAudioPlaybackAtTime(timelineTimeSec) {
1487
- // 每次重新调度前,先清理旧的音频和定时器
1488
- this.resetAudioPlayback();
1489
-
1490
- const audioClips = this.getAllAudioClipsWithAssets();
1491
- const container =
1492
- document.querySelector('.video-container') || document.body;
1493
-
1494
- audioClips.forEach(({ clip, asset }) => {
1495
- const clipStart = clip.startTime; // 秒
1496
- const clipDuration = clip.duration; // 秒
1497
- const clipEnd = clipStart + clipDuration;
1498
-
1499
- // 如果当前时间在片段结束之后,跳过
1500
- if (timelineTimeSec >= clipEnd) {
1501
- return;
1502
- }
1503
-
1504
- const startDelaySec = clipStart - timelineTimeSec; // 负数表示立即开始且已过开头
1505
- const offsetSec = Math.max(0, timelineTimeSec - clipStart); // 立即开始时从片段内的偏移
1506
-
1507
- const scheduleStart = () => {
1508
- const remainingSec = clipDuration - offsetSec;
1509
- if (remainingSec <= 0) return; // 无需播放
1510
-
1511
- const audio = document.createElement('audio');
1512
- audio.src = `/static/${asset.uri}`;
1513
- audio.preload = 'auto';
1514
- audio.style.display = 'none';
1515
- audio.dataset.clipId = clip.id;
1516
- container.appendChild(audio);
1517
-
1518
- // 设置起始播放位置(素材内时间=入点+片段内偏移)
1519
- const playStartInAsset = (clip.inPoint || 0) + offsetSec;
1520
- const setAndPlay = () => {
1521
- try {
1522
- audio.currentTime = playStartInAsset;
1523
- } catch (e) {
1524
- // 某些浏览器在未加载metadata前设置currentTime会报错
1525
- }
1526
- // 开始播放
1527
- audio.play().catch(err => {
1528
- console.error('音频播放失败:', err);
1529
- });
1530
-
1531
- // 添加时间更新监听器,确保音频在clip.duration时长后停止
1532
- audio.addEventListener('timeupdate', () => {
1533
- const currentAssetTime = audio.currentTime;
1534
- const playedDuration = currentAssetTime - playStartInAsset;
1535
-
1536
- // 如果播放时长超过了clip的duration,立即停止
1537
- if (playedDuration >= remainingSec) {
1538
- console.log(`音频 ${clip.id} 达到clip时长限制,停止播放`);
1539
- audio.pause();
1540
- audio.currentTime = 0;
1541
- }
1542
- });
1543
- };
1544
-
1545
- // 等待元数据加载后设置currentTime并播放
1546
- audio.addEventListener('loadedmetadata', setAndPlay, { once: true });
1547
- // 如果已经可以播放,直接尝试
1548
- audio.addEventListener('canplay', setAndPlay, { once: true });
1549
-
1550
- // 记录播放器
1551
- this.audioPlayers[clip.id] = audio;
1552
-
1553
- // 定时停止此音频
1554
- const stopDelayMs = remainingSec * 1000;
1555
- const stopTimer = setTimeout(
1556
- () => this.stopAudioForClip(clip.id),
1557
- stopDelayMs
1558
- );
1559
- this.audioTimers.push(stopTimer);
1560
- };
1561
-
1562
- if (startDelaySec <= 0) {
1563
- // 立即开始(从偏移位置)
1564
- scheduleStart();
1565
- } else {
1566
- // 延时开始
1567
- const startTimer = setTimeout(scheduleStart, startDelaySec * 1000);
1568
- this.audioTimers.push(startTimer);
1569
- }
1570
- });
1571
- }
1572
-
1573
- stopAudioForClip(clipId) {
1574
- const audio = this.audioPlayers[clipId];
1575
- if (audio) {
1576
- try {
1577
- audio.pause();
1578
- } catch (e) {
1579
- console.warn('stopAudioForClip: pause() 失败', e);
1580
- }
1581
- try {
1582
- audio.src = '';
1583
- } catch (e) {
1584
- console.warn('stopAudioForClip: 清空 src 失败', e);
1585
- }
1586
- try {
1587
- audio.remove();
1588
- } catch (e) {
1589
- console.warn('stopAudioForClip: remove() 失败', e);
1590
- }
1591
- delete this.audioPlayers[clipId];
1592
- }
1593
- }
1594
-
1595
- getTotalDuration() {
1596
- let maxDuration = 0;
1597
-
1598
- // 遍历所有轨道的所有片段,找到最大的结束时间
1599
- Object.values(this.tracks).forEach(trackGroup => {
1600
- trackGroup.forEach(track => {
1601
- track.clips.forEach(clip => {
1602
- const endTime = (clip.startMs + clip.durationMs) / 1000;
1603
- maxDuration = Math.max(maxDuration, endTime);
1604
- });
1605
- });
1606
- });
1607
-
1608
- // 如果没有片段,返回默认时长60秒
1609
- return maxDuration > 0 ? maxDuration : 60;
1610
- }
1611
-
1612
- formatTime(seconds) {
1613
- const mins = Math.floor(seconds / 60);
1614
- const secs = Math.floor(seconds % 60);
1615
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
1616
- }
1617
-
1618
- updateTimeDisplay() {
1619
- const video = document.getElementById('previewVideo');
1620
- const timeDisplay = document.getElementById('timeDisplay');
1621
-
1622
- if (video && timeDisplay) {
1623
- // 计算整个时间轴的总时长
1624
- const totalTimelineDuration = this.totalDuration;
1625
-
1626
- // 计算当前在整个时间轴中的绝对位置
1627
- let currentTimelinePosition = 0;
1628
- if (this.isPlaying && this.currentClipIndex < this.playbackClips.length) {
1629
- const currentClip = this.playbackClips[this.currentClipIndex];
1630
- const videoCurrentTime = video.currentTime || 0;
1631
- currentTimelinePosition = currentClip.startTime + videoCurrentTime;
1632
- } else if (!this.isPlaying) {
1633
- // 暂停状态下,使用保存的当前时间位置
1634
- currentTimelinePosition = this.currentTime || 0;
1635
- }
1636
-
1637
- timeDisplay.textContent = `${this.formatTime(currentTimelinePosition)} / ${this.formatTime(totalTimelineDuration)}`;
1638
-
1639
- // 只在播放状态下更新播放指示器位置,避免暂停时重置
1640
- if (this.isPlaying) {
1641
- this.updatePlaybackIndicator(currentTimelinePosition);
1642
- }
1643
- }
1644
- }
1645
-
1646
- // 使用requestAnimationFrame平滑更新播放指示器
1647
- startPlaybackIndicatorAnimation() {
1648
- // 如果已经有动画在运行,先停止它
1649
- if (this.animationFrameId) {
1650
- cancelAnimationFrame(this.animationFrameId);
1651
- }
1652
-
1653
- const animate = () => {
1654
- if (this.isPlaying) {
1655
- // 计算当前在时间轴上的绝对位置
1656
- const video = document.getElementById('previewVideo');
1657
- if (video && this.playbackClips[this.currentClipIndex]) {
1658
- const currentClip = this.playbackClips[this.currentClipIndex];
1659
- const timelinePosition = currentClip.startTime + video.currentTime;
1660
- this.currentTime = timelinePosition;
1661
- this.updatePlaybackIndicator(timelinePosition);
1662
- }
1663
-
1664
- // 继续下一帧动画
1665
- this.animationFrameId = requestAnimationFrame(animate);
1666
- }
1667
- };
1668
-
1669
- // 开始动画循环
1670
- this.animationFrameId = requestAnimationFrame(animate);
1671
- }
1672
-
1673
- // 停止播放指示器动画
1674
- stopPlaybackIndicatorAnimation() {
1675
- if (this.animationFrameId) {
1676
- cancelAnimationFrame(this.animationFrameId);
1677
- this.animationFrameId = null;
1678
- }
1679
- }
1680
-
1681
- updatePlaybackIndicator(currentTimelinePosition = null) {
1682
- // 等待DOM完全加载
1683
- if (document.readyState !== 'complete') {
1684
- setTimeout(
1685
- () => this.updatePlaybackIndicator(currentTimelinePosition),
1686
- 100
1687
- );
1688
- return;
1689
- }
1690
-
1691
- const indicator = document.getElementById('playbackIndicator');
1692
- if (!indicator) {
1693
- console.warn(
1694
- 'Playback indicator element not found, DOM may not be ready'
1695
- );
1696
- // 尝试创建指示器元素
1697
- this.createPlaybackIndicator();
1698
- return;
1699
- }
1700
-
1701
- // 如果没有传入位置参数,使用当前时间
1702
- const position =
1703
- currentTimelinePosition !== null
1704
- ? currentTimelinePosition
1705
- : this.currentTime;
1706
-
1707
- // 显示播放指示器
1708
- indicator.style.display = 'block';
1709
-
1710
- // 计算指示器在时间轴上的像素位置
1711
- // 由于timeline-ruler有120px的左边距,指示器位置不需要额外偏移
1712
- // 因为指示器是timeline-ruler的子元素,会自动跟随父元素的偏移
1713
- const pixelPosition = position * this.pixelsPerSecond * this.zoomLevel;
1714
- indicator.style.left = `${pixelPosition}px`;
1715
-
1716
- // 更新字幕显示
1717
- this.updateSubtitleDisplay(position);
1718
-
1719
- // console.log(
1720
- // `Playback indicator updated: position=${position}, pixelPosition=${pixelPosition}`
1721
- // );
1722
- }
1723
-
1724
- createPlaybackIndicator() {
1725
- const timelineRuler = document.getElementById('timelineRuler');
1726
- if (!timelineRuler) {
1727
- console.error(
1728
- 'Timeline ruler not found, cannot create playback indicator'
1729
- );
1730
- return;
1731
- }
1732
-
1733
- // 检查是否已存在
1734
- let indicator = document.getElementById('playbackIndicator');
1735
- if (indicator) {
1736
- return;
1737
- }
1738
-
1739
- // 创建播放指示器元素
1740
- indicator = document.createElement('div');
1741
- indicator.id = 'playbackIndicator';
1742
- indicator.className = 'playback-indicator';
1743
- timelineRuler.appendChild(indicator);
1744
-
1745
- console.log('Playback indicator created dynamically');
1746
- }
1747
-
1748
- handleKeyboard(e) {
1749
- if (e.code === 'Space') {
1750
- e.preventDefault();
1751
- this.togglePlayback();
1752
- } else if (e.code === 'Delete' && this.selectedClip) {
1753
- this.deleteSelectedClip();
1754
- }
1755
- }
1756
-
1757
- deleteSelectedClip() {
1758
- if (!this.selectedClip) return;
1759
-
1760
- Object.keys(this.tracks).forEach(trackType => {
1761
- const index = this.tracks[trackType].findIndex(
1762
- c => c.id === this.selectedClip.id
1763
- );
1764
- if (index !== -1) {
1765
- this.tracks[trackType].splice(index, 1);
1766
- }
1767
- });
1768
-
1769
- this.selectedClip = null;
1770
- this.renderTimeline();
1771
- this.updatePropertiesPanel();
1772
- }
1773
-
1774
- // 过滤掉额外添加的字段,只保留原始数据
1775
- filterClips(clips) {
1776
- if (!clips || !Array.isArray(clips)) {
1777
- return [];
1778
- }
1779
-
1780
- return clips.map(clip => {
1781
- // 创建新对象,排除额外添加的字段
1782
- const { startTime, duration, inPoint, ...originalClip } = clip;
1783
- return originalClip;
1784
- });
1785
- }
1786
-
1787
- // Project management methods
1788
- async updateProject() {
1789
- try {
1790
- // 将app.js的tracks格式转换为draft_content.json的格式
1791
- const convertedTracks = [];
1792
-
1793
- // 转换video tracks
1794
- if (this.tracks.video && this.tracks.video.length > 0) {
1795
- this.tracks.video.forEach((track, index) => {
1796
- convertedTracks.push({
1797
- id: track.id || `video-track-${index + 1}`,
1798
- type: 'video',
1799
- clips: this.filterClips(track.clips) || [],
1800
- });
1801
- });
1802
- }
1803
-
1804
- // 转换audio tracks
1805
- if (this.tracks.audio && this.tracks.audio.length > 0) {
1806
- this.tracks.audio.forEach((track, index) => {
1807
- convertedTracks.push({
1808
- id: track.id || `audio-track-${index + 1}`,
1809
- type: 'audio',
1810
- clips: this.filterClips(track.clips) || [],
1811
- });
1812
- });
1813
- }
1814
-
1815
- // 转换bgm tracks
1816
- if (this.tracks.bgm && this.tracks.bgm.length > 0) {
1817
- this.tracks.bgm.forEach((track, index) => {
1818
- convertedTracks.push({
1819
- id: track.id || `bgm-track-${index + 1}`,
1820
- type: 'audio',
1821
- clips: this.filterClips(track.clips) || [],
1822
- });
1823
- });
1824
- }
1825
-
1826
- // 将app.js的assets格式转换为draft_content.json的格式
1827
- const convertedAssets = [];
1828
-
1829
- // 转换video assets
1830
- if (this.assets.video && this.assets.video.length > 0) {
1831
- this.assets.video.forEach(asset => {
1832
- convertedAssets.push({
1833
- id: asset.id,
1834
- type: 'video',
1835
- uri: asset.uri,
1836
- durationMs: Math.round(asset.duration * 1000),
1837
- });
1838
- });
1839
- }
1840
-
1841
- // 转换audio assets
1842
- if (this.assets.audio && this.assets.audio.length > 0) {
1843
- this.assets.audio.forEach(asset => {
1844
- convertedAssets.push({
1845
- id: asset.id,
1846
- type: 'audio',
1847
- uri: asset.uri,
1848
- durationMs: Math.round(asset.duration * 1000),
1849
- });
1850
- });
1851
- }
1852
-
1853
- // 转换image assets
1854
- if (this.assets.image && this.assets.image.length > 0) {
1855
- this.assets.image.forEach(asset => {
1856
- convertedAssets.push({
1857
- id: asset.id,
1858
- type: 'image',
1859
- uri: asset.uri,
1860
- durationMs: Math.round(asset.duration * 1000),
1861
- });
1862
- });
1863
- }
1864
-
1865
- // 收集当前的项目数据
1866
- const projectData = {
1867
- assets: convertedAssets,
1868
- tracks: convertedTracks,
1869
- totalDuration: this.totalDuration,
1870
- subtitles: this.subtitles || [],
1871
- };
1872
-
1873
- // 发送更新请求到服务器
1874
- const response = await fetch('/api/update-draft', {
1875
- method: 'POST',
1876
- headers: {
1877
- 'Content-Type': 'application/json',
1878
- },
1879
- body: JSON.stringify(projectData),
1880
- });
1881
-
1882
- if (response.ok) {
1883
- alert('项目已成功更新到 draft_content.json');
1884
- } else {
1885
- const error = await response.text();
1886
- alert('更新失败: ' + error);
1887
- }
1888
- } catch (error) {
1889
- console.error('更新项目时出错:', error);
1890
- alert('更新失败: ' + error.message);
1891
- }
1892
- }
1893
-
1894
- setupTimelineRulerClick() {
1895
- const timelineRuler = document.querySelector('.timeline-ruler');
1896
- if (!timelineRuler) return;
1897
-
1898
- timelineRuler.addEventListener('click', e => {
1899
- const timelineTracks = document.querySelector('.timeline-tracks');
1900
- if (!timelineTracks) return;
1901
-
1902
- const rect = timelineRuler.getBoundingClientRect();
1903
- const scrollLeft = timelineTracks.scrollLeft;
1904
-
1905
- // 考虑滚动偏移量的鼠标位置
1906
- const x = e.clientX - rect.left + scrollLeft;
1907
- const clampedX = Math.max(0, Math.min(x, rect.width + scrollLeft));
1908
-
1909
- // 计算对应的时间位置
1910
- const timePosition = clampedX / this.pixelsPerSecond;
1911
-
1912
- // 如果正在播放,先点击播放按钮暂停播放,然后延迟跳转
1913
- if (this.isPlaying) {
1914
- this.togglePlayback(); // 模拟点击播放按钮暂停
1915
- setTimeout(() => {
1916
- this.seekToTimelinePosition(timePosition);
1917
- }, 100);
1918
- } else {
1919
- // 如果没有播放,直接跳转
1920
- this.seekToTimelinePosition(timePosition);
1921
- }
1922
- });
1923
- }
1924
-
1925
- seekToTimelinePosition(timelinePosition) {
1926
- // 更新当前时间位置
1927
- this.currentTime = timelinePosition;
1928
-
1929
- // 找到在指定时间位置的视频片段
1930
- for (
1931
- let trackIndex = 0;
1932
- trackIndex < this.tracks.video.length;
1933
- trackIndex++
1934
- ) {
1935
- const track = this.tracks.video[trackIndex];
1936
- for (let clipIndex = 0; clipIndex < track.clips.length; clipIndex++) {
1937
- const clip = track.clips[clipIndex];
1938
- if (
1939
- timelinePosition >= clip.startTime &&
1940
- timelinePosition < clip.startTime + clip.duration
1941
- ) {
1942
- // 计算在该片段内的相对时间
1943
- const relativeTime = timelinePosition - clip.startTime + clip.inPoint;
1944
-
1945
- // 更新当前片段索引
1946
- this.currentClipIndex = clipIndex;
1947
-
1948
- // 根据assetId找到对应的asset
1949
- const asset = this.findAssetById(clip.assetId);
1950
- if (asset) {
1951
- // 选择该片段并跳转到相应时间
1952
- this.selectAsset(asset);
1953
- const video = document.getElementById('previewVideo');
1954
- if (video) {
1955
- // 确保视频已加载后再设置时间
1956
- const setVideoTime = () => {
1957
- video.currentTime = relativeTime;
1958
- // 强制更新视频帧显示
1959
- video.pause();
1960
- video.currentTime = relativeTime;
1961
- // 拖拽进度到新位置时,暂停当前音频,等待用户继续播放
1962
- this.pauseAudioPlayback();
1963
- };
1964
-
1965
- if (video.readyState >= 2) {
1966
- // 视频已加载足够数据
1967
- setVideoTime();
1968
- } else {
1969
- // 等待视频加载
1970
- video.addEventListener('loadeddata', setVideoTime, {
1971
- once: true,
1972
- });
1973
- }
1974
- }
1975
- }
1976
-
1977
- // 更新播放指示器位置和时间显示
1978
- this.updatePlaybackIndicator(timelinePosition);
1979
- this.updateTimeDisplay();
1980
- return;
1981
- }
1982
- }
1983
- }
1984
-
1985
- // 如果没有找到对应的视频片段,但仍在时间轴范围内
1986
- if (timelinePosition >= 0 && timelinePosition <= this.getTotalDuration()) {
1987
- // 更新播放指示器位置和时间显示
1988
- this.updatePlaybackIndicator(timelinePosition);
1989
- this.updateTimeDisplay();
1990
- }
1991
- }
1992
-
1993
- setupVideoEvents() {
1994
- const video = document.getElementById('previewVideo');
1995
- if (!video) return;
1996
-
1997
- // 监听视频时间更新事件,只更新时间显示
1998
- video.addEventListener('timeupdate', () => {
1999
- // 只更新时间显示,不更新播放指示器位置
2000
- // 播放指示器位置由requestAnimationFrame动画循环控制
2001
- this.updateTimeDisplay();
2002
- });
2003
-
2004
- // 监听视频播放结束事件
2005
- video.addEventListener('ended', () => {
2006
- this.playNextClip();
2007
- });
2008
-
2009
- // 监听视频加载完成事件
2010
- video.addEventListener('loadeddata', () => {
2011
- if (this.isPlaying) {
2012
- video.play().catch(err => {
2013
- console.error('Failed to play video:', err);
2014
- });
2015
- }
2016
- });
2017
- }
2018
-
2019
- setupTimelineScrollSync() {
2020
- const timelineRuler = document.querySelector('.timeline-ruler');
2021
- const timelineTracks = document.querySelector('.timeline-tracks');
2022
-
2023
- if (timelineRuler && timelineTracks) {
2024
- // 监听轨道区域的横向滚动,使用 translateX 同步时间轴标尺
2025
- timelineTracks.addEventListener('scroll', () => {
2026
- const scrollLeft = timelineTracks.scrollLeft;
2027
- // 使用 translateX 来移动 ruler 内容,而不是依赖 scrollLeft
2028
- const rulerContent =
2029
- timelineRuler.querySelector('.ruler-content') || timelineRuler;
2030
- rulerContent.style.transform = `translateX(-${scrollLeft}px)`;
2031
-
2032
- const playbackIndicator = document.getElementById('playbackIndicator');
2033
- if (playbackIndicator) {
2034
- // 同步播放指示器位置
2035
- playbackIndicator.style.transform = `translateX(-${scrollLeft}px)`;
2036
- }
2037
-
2038
- // 只有在播放状态下才更新播放指示器的位置,避免暂停时重置位置
2039
- if (this.isPlaying) {
2040
- this.updatePlaybackIndicator();
2041
- }
2042
- });
2043
- }
2044
- }
2045
-
2046
- updateSubtitleDisplay(currentTimeSeconds) {
2047
- const subtitleText = document.getElementById('subtitleText');
2048
- if (!subtitleText) return;
2049
-
2050
- // 将当前时间转换为毫秒
2051
- const currentTimeMs = currentTimeSeconds * 1000;
2052
-
2053
- // 查找当前时间应该显示的字幕
2054
- const currentSubtitle = this.subtitles.find(subtitle => {
2055
- return (
2056
- currentTimeMs >= subtitle.startMs && currentTimeMs <= subtitle.endMs
2057
- );
2058
- });
2059
-
2060
- if (currentSubtitle) {
2061
- // 显示字幕
2062
- subtitleText.textContent = currentSubtitle.text;
2063
- subtitleText.style.display = 'block';
2064
-
2065
- // 应用字幕样式
2066
- if (currentSubtitle.style) {
2067
- if (currentSubtitle.style.fontSize) {
2068
- subtitleText.style.fontSize =
2069
- currentSubtitle.style.fontSize / 2 + 'px';
2070
- }
2071
- if (currentSubtitle.style.color) {
2072
- subtitleText.style.color = currentSubtitle.style.color;
2073
- }
2074
- }
2075
- } else {
2076
- // 隐藏字幕
2077
- subtitleText.style.display = 'none';
2078
- }
2079
- }
2080
- }
2081
-
2082
- // Initialize the timeline editor when the page loads
2083
- let timelineEditor;
2084
- document.addEventListener('DOMContentLoaded', () => {
2085
- timelineEditor = new TimelineEditor();
2086
- });