@linker-design-plus/timeline-track
v1.0.10
Published
A TypeScript-based video editing library with timeline and track functionality
Readme
@linker-design-plus/timeline-track
基于 TypeScript 和 Konva.js 的高性能视频编辑时间线库,支持拖拽、缩放、分割等核心功能,集成了完整的操作历史记录和日志系统。
核心功能
- ✅ 交互式时间线,支持水平缩放(鼠标滚轮/触摸手势)
- ✅ 视频轨道系统,支持多个片段的拖拽、调整大小、分割功能
- ✅ 播放头指针,用于精确时间指示
- ✅ 操作历史记录,支持撤销/重做功能
- ✅ Canvas-based 渲染,确保高性能
- ✅ 外部 API,用于片段管理、时间同步和播放控制
- ✅ 片段碰撞检测,防止重叠
- ✅ 片段边界限制,确保在轨道范围内
- ✅ 原视频长度边界限制,确保剪辑不超出原视频范围
- ✅ 日志系统,支持调试模式开关
- ✅ 间隙移除功能,自动删除片段之间的间隙
- ✅ 批量片段更新,提高性能
- ✅ 历史记录变更通知事件,用于外部应用调整撤销/重做按钮状态
- ✅ 播放倍速控制,支持 0.1x 到 10x 的播放速度
- ✅ 轨道总时长计算,包含片段间隙
- ✅ 封面系统,支持自定义缩略图提供器
- ✅ 异步封面加载,支持 Promise 形式的封面获取
安装
npm install @linker-design-plus/timeline-track快速开始
基本用法
import { TimelineManager } from '@linker-design-plus/timeline-track';
// 获取容器元素
const timelineContainer = document.getElementById('timeline-container');
const videoElement = document.getElementById('video-element') as HTMLVideoElement;
// 创建 TimelineManager 实例
const timelineManager = new TimelineManager({
container: timelineContainer,
debug: true, // 开启调试模式
});
// 连接到视频元素(可选)
timelineManager.connectTo(videoElement);
// 添加片段
const clipId = await timelineManager.addClip({
src: 'sample-video.mp4',
name: 'Clip 1',
startTimeAtSource: 0, // 源视频中的开始时间(毫秒)
duration: 5000, // 片段持续时间(毫秒)
thumbnail: 'https://example.com/thumbnail1.jpg' // 可选:直接提供封面图片
});
// 开始播放
timelineManager.play();
// 设置播放倍速
timelineManager.setSpeed(2); // 2 倍速
// 适合缩放(自动调整缩放比例以适应所有片段)
timelineManager.fitZoom();
// 分割当前时间点的片段
timelineManager.splitCurrentClip();
// 移除片段之间的间隙
timelineManager.removeClipGaps();
// 监听历史记录变更事件
timelineManager.on('history_change', (event, data) => {
console.log('History changed:', data);
// 更新撤销/重做按钮状态
updateUndoRedoButtons(data.canUndo, data.canRedo);
});
// 销毁时间轴管理器
// timelineManager.destroy();封面系统用法
import { TimelineManager, ThumbnailProvider } from '@linker-design-plus/timeline-track';
// 创建缩略图提供器
const thumbnailProvider: ThumbnailProvider = {
getThumbnail(clip) {
// 同步获取封面
return `https://example.com/thumbnails/${clip.id}.jpg`;
// 或异步获取封面
// return new Promise((resolve) => {
// // 模拟异步获取封面
// setTimeout(() => {
// resolve(`https://example.com/thumbnails/${clip.id}.jpg`);
// }, 100);
// });
}
};
// 创建 TimelineManager 实例时设置缩略图提供器
const timelineManager = new TimelineManager({
container: timelineContainer,
thumbnailProvider: thumbnailProvider
});
// 或动态设置缩略图提供器
timelineManager.setThumbnailProvider(thumbnailProvider);
// 添加片段时,会自动通过提供器获取封面
const clipId = await timelineManager.addClip({
src: 'sample-video.mp4',
name: 'Clip 1',
duration: 5000
});Vue 3 集成
<template>
<div class="app">
<div class="video-container">
<video ref="videoElement" class="video" controls playsinline></video>
</div>
<div class="controls">
<button @click="togglePlay">{{ isPlaying ? 'Pause' : 'Play' }}</button>
<button @click="fitZoom">适合缩放</button>
<button @click="removeClipGaps">移除间隙</button>
<button @click="undo" :disabled="!canUndo">Undo</button>
<button @click="redo" :disabled="!canRedo">Redo</button>
<button @click="addClip">Add Clip</button>
<button @click="splitClip">Split Clip</button>
</div>
<div class="timeline-container" ref="timelineContainer"></div>
<div class="info-panel">
<div>Current Time: {{ formattedTime }}</div>
<div>Zoom: {{ zoom }} px/s</div>
<div>Clips: {{ clipCount }}</div>
<div>Status: {{ isPlaying ? 'Playing' : 'Paused' }}</div>
<div>Speed: {{ speed }}x</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { TimelineManager } from '@linker-design-plus/timeline-track';
// 容器引用
const timelineContainer = ref<HTMLDivElement | null>(null);
const videoElement = ref<HTMLVideoElement | null>(null);
// 状态
const isPlaying = ref(false);
const currentTime = ref(0);
const zoom = ref(100);
const clipCount = ref(0);
const canUndo = ref(false);
const canRedo = ref(false);
const speed = ref(1.0);
// TimelineManager 实例
let timelineManager: TimelineManager | null = null;
// 格式化时间显示
const formattedTime = computed(() => {
const totalSeconds = Math.floor(currentTime.value / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
});
// 初始化
onMounted(async () => {
if (!timelineContainer.value) return;
// 创建 TimelineManager 实例
timelineManager = new TimelineManager({
container: timelineContainer.value,
debug: true,
});
// 连接到视频元素
if (videoElement.value) {
timelineManager.connectTo(videoElement.value);
}
// 添加事件监听器
timelineManager.on('time_change', (event, data) => {
currentTime.value = data.time;
});
timelineManager.on('play_state_change', (event, data) => {
isPlaying.value = data.playState === 'playing';
});
timelineManager.on('zoom_change', (event, data) => {
zoom.value = data.zoom;
});
timelineManager.on('history_change', (event, data) => {
canUndo.value = data.canUndo;
canRedo.value = data.canRedo;
});
timelineManager.on('speed_change', (event, data) => {
speed.value = data.speed;
});
// 添加示例片段
await addSampleClips();
updateClipCount();
// 初始设置
zoom.value = timelineManager.getZoom();
speed.value = timelineManager.getSpeed();
});
// 清理
onUnmounted(() => {
timelineManager?.destroy();
});
// 控制方法
const togglePlay = () => timelineManager?.togglePlay();
const undo = () => timelineManager?.undo();
const redo = () => timelineManager?.redo();
const fitZoom = () => timelineManager?.fitZoom();
const removeClipGaps = () => timelineManager?.removeClipGaps();
// 添加片段
const addClip = async () => {
if (!timelineManager) return;
const clipId = await timelineManager.addClip({
src: 'sample-video.mp4',
name: `Clip ${Date.now()}`,
duration: 5000,
startTimeAtSource: 0
});
updateClipCount();
};
// 分割片段
const splitClip = () => timelineManager?.splitCurrentClip();
// 更新片段计数
const updateClipCount = () => {
if (timelineManager) {
clipCount.value = timelineManager.getClips().length;
}
};
// 添加示例片段
const addSampleClips = async () => {
if (!timelineManager) return;
const clips = [
{
src: 'sample1.mp4',
name: 'Clip 1',
startTimeAtSource: 0,
duration: 5000
},
{
src: 'sample2.mp4',
name: 'Clip 2',
startTimeAtSource: 0,
duration: 8000
}
];
for (const clip of clips) {
await timelineManager.addClip(clip);
}
timelineManager.clearHistory();
};
</script>
<style scoped>
/* 样式省略 */
</style>API 文档
TimelineManager
构造函数
new TimelineManager(config?: Partial<TimelineConfig>)参数:
config:配置选项container:容器元素debug:是否开启调试模式,默认 falseduration:总时长(毫秒),默认 3600000zoom:缩放比例(像素/秒),默认 100currentTime:初始当前时间(毫秒),默认 0playState:初始播放状态,默认 'paused'speed:初始播放倍速,默认 1.0thumbnailProvider:缩略图提供器,用于获取片段封面
核心方法
| 方法名 | 描述 | 参数 | 返回值 |
|--------|------|------|--------|
| play() | 开始播放 | 无 | 无 |
| pause() | 暂停播放 | 无 | 无 |
| togglePlay() | 切换播放状态 | 无 | 无 |
| setCurrentTime(time) | 设置当前时间 | time:时间(毫秒) | 无 |
| getCurrentTime() | 获取当前时间 | 无 | number |
| setZoom(zoom) | 设置缩放比例 | zoom:缩放比例(像素/秒) | 无 |
| getZoom() | 获取缩放比例 | 无 | number |
| setSpeed(speed) | 设置播放倍速 | speed:播放倍速 | 无 |
| getSpeed() | 获取当前播放倍速 | 无 | number |
| setThumbnailProvider(provider) | 设置缩略图提供器 | provider:缩略图提供器 | 无 |
| addClip(clipConfig) | 添加片段 | clipConfig:片段配置 | Promise<string>(片段 ID) |
| removeClip(clipId) | 移除片段 | clipId:片段 ID | 无 |
| removeSelectedClip() | 移除当前选中的片段 | 无 | boolean(是否成功) |
| splitCurrentClip() | 分割当前时间点的片段 | 无 | 无 |
| removeClipGaps() | 移除片段之间的间隙 | 无 | 无 |
| fitZoom() | 自动调整缩放比例以适应所有片段 | 无 | 无 |
| getClips() | 获取所有片段 | 无 | Clip[] |
| getCurrentClip() | 获取当前时间点所在的片段 | 无 | Clip 或 null |
| getSelectedClip() | 获取当前选中的片段 | 无 | Clip 或 null |
| getTrackTotalDuration() | 获取轨道总时长(包含间隙) | 无 | number |
| exportTimeline() | 导出时间轴数据 | 无 | TimelineExportData |
| undo() | 撤销操作 | 无 | boolean(是否成功) |
| redo() | 重做操作 | 无 | boolean(是否成功) |
| clearHistory() | 清空历史记录 | 无 | 无 |
| connectTo(video) | 连接到视频元素 | video:视频元素 | 无 |
| on(event, listener) | 添加事件监听器 | event:事件类型,listener:事件监听器 | 无 |
| off(event, listener) | 移除事件监听器 | event:事件类型,listener:事件监听器 | 无 |
| destroy() | 销毁时间轴管理器 | 无 | 无 |
交互事件分层规范
- 鼠标交互(时间轴拖拽、底部滑块拖拽、片段拖拽)采用统一分层策略,详见:
docs/interaction-model.md
- 该文档用于约束后续重构,避免出现“移出画布中断拖拽”或“回到画布瞬移”等回归问题。
事件
| 事件名 | 描述 | 数据 |
|--------|------|------|
| time_change | 当前时间变化 | { time: number } |
| play_state_change | 播放状态变化 | { playState: 'playing' \| 'paused' } |
| speed_change | 播放倍速变化 | { speed: number } |
| clip_added | 添加片段 | { clip: Clip } |
| clip_removed | 移除片段 | { clipId: string } |
| clip_updated | 更新片段 | { clip: Clip } |
| clip_selected | 选择片段 | { clip: Clip } |
| selected_clip_change | 选中片段变化(订阅后会立即回调当前状态) | { clip: Clip \| null, hasSelectedClip: boolean } |
| zoom_change | 缩放比例变化 | { zoom: number } |
| history_change | 历史记录变更 | { canUndo: boolean, canRedo: boolean } |
| track_duration_change | 轨道总时长变化 | { duration: number } |
| buffering_state_change | 视频缓冲状态变化 | { isBuffering: boolean } |
| can_play_change | 是否可播放状态变化 | { canPlay: boolean } |
| source_loading_change | 视频源加载状态变化 | { isLoading: boolean, pending: number } |
ClipConfig 接口
interface ClipConfig {
src: string; // 视频源 URL
name?: string; // 片段名称
duration: number; // 片段持续时间(毫秒)
startTimeAtSource?: number; // 源视频中的开始时间(毫秒)
startTime?: number; // 片段在轨道上的起始时间(可选,自动计算)
thumbnail?: string; // 缩略图 URL
}开发指南
安装依赖
npm install开发模式
npm run dev构建
npm run build预览构建结果
npm run preview浏览器兼容性
- Chrome (推荐)
- Firefox
- Safari
- Edge
许可证
MIT
贡献
欢迎提交 Issue 和 Pull Request!
更新日志
v1.0.0-beta.1
- 首次发布
- 基于 TypeScript 和 Konva.js 构建
- 实现核心视频编辑时间线功能
- 支持片段拖拽、调整大小、分割
- 集成操作历史记录系统
- 支持播放倍速控制
- 提供完整的外部 API
- 包含 TypeScript 类型声明
