audio-video-sync 0.1.0
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/LICENSE +8 -0
- package/README.md +250 -0
- package/README.zh-CN.md +278 -0
- package/dist/audio.d.ts +40 -0
- package/dist/fft.d.ts +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.esm.js +475 -0
- package/dist/index.js +490 -0
- package/dist/sync.d.ts +87 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright © 2026 EuanTop
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# audio-video-sync
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/audio-video-sync)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**English** | [中文版](./README.zh-CN.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
🙆♀️ Multi-camera recording alignment timeline tool, **a must-have for research!**
|
|
12
|
+
|
|
13
|
+
📱 Aligns via audio, bypassing the additional processing inconsistencies of video source data on Apple, Android, and Xiaomi devices, as well as the inherent inaccuracies of the recording equipment's system time.
|
|
14
|
+
|
|
15
|
+
✨ Multi-camera video synchronization using audio cross-correlation. Automatically align multiple video timelines by analyzing audio waveforms with millisecond precision.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- 🎯 **High Precision** - Millisecond-level sync accuracy (±2ms)
|
|
21
|
+
- ⚡ **High Performance** - FFT-accelerated cross-correlation algorithm
|
|
22
|
+
- 🌐 **Browser Native** - Based on FFmpeg.wasm, no server required
|
|
23
|
+
- 📦 **Zero Config** - Works out of the box with automatic audio extraction
|
|
24
|
+
- 🔧 **Flexible** - Supports custom FFmpeg instances and parameters
|
|
25
|
+
- 📝 **TypeScript** - Full type definitions included
|
|
26
|
+
|
|
27
|
+
## How it Works
|
|
28
|
+
|
|
29
|
+
Multi-camera recordings may have inaccurate creation_time metadata, but the ambient sound is consistent across all recordings. By performing cross-correlation analysis on audio waveforms, we can precisely calculate the time offset between videos.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Video A audio: ──────█████████████──────────
|
|
33
|
+
Video B audio: ────────────█████████████────
|
|
34
|
+
↑
|
|
35
|
+
offset Δt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
<!-- ## Requirements (uncertain)
|
|
39
|
+
|
|
40
|
+
- **Node.js**: >= 16.0.0
|
|
41
|
+
- **Browser**: Must support SharedArrayBuffer (Chrome 92+, Firefox 79+, Safari 15.2+)
|
|
42
|
+
- **HTTPS**: Required for SharedArrayBuffer in production -->
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install audio-video-sync @ffmpeg/ffmpeg @ffmpeg/util
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
### Clone and Setup
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/EuanTop/audio-video-sync.git
|
|
56
|
+
cd audio-video-sync
|
|
57
|
+
npm install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Build from Source
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Build the package
|
|
64
|
+
npm run build
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Test the Build
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Open test.html in browser to test the built package
|
|
71
|
+
# npx serve -p 3333
|
|
72
|
+
open test.html
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Scripts
|
|
76
|
+
|
|
77
|
+
- `npm run build` - Build the package for distribution
|
|
78
|
+
- `npm run test` - Run tests (placeholder)
|
|
79
|
+
- `npm run prepublishOnly` - Automatically builds before publishing
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
### Basic Usage
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import { syncVideos } from 'audio-video-sync';
|
|
87
|
+
|
|
88
|
+
const result = await syncVideos([
|
|
89
|
+
{ file: video1File, id: 'cam1' },
|
|
90
|
+
{ file: video2File, id: 'cam2' },
|
|
91
|
+
{ file: video3File, id: 'cam3' },
|
|
92
|
+
{ file: video4File, id: 'cam4' }
|
|
93
|
+
], {
|
|
94
|
+
referenceIndex: 0, // Use first video as reference
|
|
95
|
+
sampleRate: 16000, // Sample rate
|
|
96
|
+
maxDuration: 60 // Only analyze first 60 seconds
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(result);
|
|
100
|
+
// {
|
|
101
|
+
// referenceId: 'cam1',
|
|
102
|
+
// results: [
|
|
103
|
+
// { id: 'cam1', offsetSeconds: 0, confidence: 1 },
|
|
104
|
+
// { id: 'cam2', offsetSeconds: 0.523, confidence: 0.89 },
|
|
105
|
+
// { id: 'cam3', offsetSeconds: -0.127, confidence: 0.92 },
|
|
106
|
+
// { id: 'cam4', offsetSeconds: 1.234, confidence: 0.85 }
|
|
107
|
+
// ],
|
|
108
|
+
// success: true
|
|
109
|
+
// }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Using Existing FFmpeg Instance
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
116
|
+
import { AudioVideoSync } from 'audio-video-sync';
|
|
117
|
+
|
|
118
|
+
const ffmpeg = new FFmpeg();
|
|
119
|
+
await ffmpeg.load();
|
|
120
|
+
|
|
121
|
+
const sync = new AudioVideoSync(ffmpeg);
|
|
122
|
+
const result = await sync.syncVideos(videos);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Calculate Offset Between Two Videos
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
import { createSync } from 'audio-video-sync';
|
|
129
|
+
|
|
130
|
+
const sync = createSync();
|
|
131
|
+
const { offsetSeconds, confidence } = await sync.calculateOffset(
|
|
132
|
+
referenceVideoFile,
|
|
133
|
+
targetVideoFile
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
console.log(`Target video offset: ${offsetSeconds} seconds`);
|
|
137
|
+
console.log(`Confidence: ${(confidence * 100).toFixed(1)}%`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### With Progress Callback
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
const result = await syncVideos(videos, {
|
|
144
|
+
onProgress: (stage, progress) => {
|
|
145
|
+
if (stage === 'extracting') {
|
|
146
|
+
console.log(`Extracting audio: ${(progress * 100).toFixed(0)}%`);
|
|
147
|
+
} else if (stage === 'correlating') {
|
|
148
|
+
console.log(`Computing correlation: ${(progress * 100).toFixed(0)}%`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## API
|
|
155
|
+
|
|
156
|
+
### syncVideos(videos, options)
|
|
157
|
+
|
|
158
|
+
Synchronize multiple video files.
|
|
159
|
+
|
|
160
|
+
**Parameters:**
|
|
161
|
+
- `videos`: `VideoInput[]` - Array of video inputs
|
|
162
|
+
- `file`: `File | Blob` - Video file
|
|
163
|
+
- `id`: `string` (optional) - Video identifier
|
|
164
|
+
- `originalStartTime`: `Date` (optional) - Original start time
|
|
165
|
+
- `options`: `SyncOptions` (optional)
|
|
166
|
+
- `referenceIndex`: `number` - Reference video index, default 0
|
|
167
|
+
- `sampleRate`: `number` - Sample rate, default 16000
|
|
168
|
+
- `maxDuration`: `number` - Max analysis duration (seconds), default 60
|
|
169
|
+
- `minConfidence`: `number` - Min confidence threshold, default 0.3
|
|
170
|
+
- `onProgress`: `(stage, progress) => void` - Progress callback
|
|
171
|
+
|
|
172
|
+
**Returns:** `Promise<MultiSyncResult>`
|
|
173
|
+
|
|
174
|
+
### AudioVideoSync
|
|
175
|
+
|
|
176
|
+
Synchronizer class with FFmpeg instance reuse support.
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
const sync = new AudioVideoSync(ffmpeg?);
|
|
180
|
+
await sync.load();
|
|
181
|
+
const result = await sync.syncVideos(videos, options);
|
|
182
|
+
const offset = await sync.calculateOffset(refVideo, targetVideo);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Low-level API
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
import {
|
|
189
|
+
extractAudio, // Extract audio from video
|
|
190
|
+
crossCorrelate, // Compute cross-correlation
|
|
191
|
+
findPeakOffset, // Find peak offset
|
|
192
|
+
calculateConfidence // Calculate confidence score
|
|
193
|
+
} from 'audio-video-sync';
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Types
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface VideoInput {
|
|
200
|
+
file: File | Blob;
|
|
201
|
+
id?: string;
|
|
202
|
+
originalStartTime?: Date;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface SyncResult {
|
|
206
|
+
id: string;
|
|
207
|
+
offsetSeconds: number;
|
|
208
|
+
offsetSamples: number;
|
|
209
|
+
confidence: number;
|
|
210
|
+
correctedStartTime: Date | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface MultiSyncResult {
|
|
214
|
+
referenceId: string;
|
|
215
|
+
results: SyncResult[];
|
|
216
|
+
sampleRate: number;
|
|
217
|
+
success: boolean;
|
|
218
|
+
error?: string;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Accuracy Comparison
|
|
223
|
+
|
|
224
|
+
| Method | Accuracy |
|
|
225
|
+
|--------|----------|
|
|
226
|
+
| creation_time | ±seconds |
|
|
227
|
+
| File timestamp | ±100ms |
|
|
228
|
+
| Audio cross-correlation | **±2ms** |
|
|
229
|
+
|
|
230
|
+
## Notes
|
|
231
|
+
|
|
232
|
+
1. **Audio Quality**: Ensure videos have clear ambient sound; silent videos cannot be synced
|
|
233
|
+
2. **Sample Rate**: 16000 Hz is sufficient for sync; higher rates increase computation
|
|
234
|
+
3. **Analysis Duration**: Usually 30-60 seconds is enough; no need to process entire video
|
|
235
|
+
4. **Memory Usage**: For long videos, limit `maxDuration` to control memory usage
|
|
236
|
+
5. **Browser Compatibility**: Requires SharedArrayBuffer support
|
|
237
|
+
|
|
238
|
+
## Tech Stack
|
|
239
|
+
|
|
240
|
+
- FFmpeg.wasm - Video decoding and audio extraction
|
|
241
|
+
- FFT (Fast Fourier Transform) - Frequency domain cross-correlation
|
|
242
|
+
- TypeScript - Type safety
|
|
243
|
+
|
|
244
|
+
## Contributing
|
|
245
|
+
|
|
246
|
+
Issues and Pull Requests are welcome!
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
[MIT](LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# audio-video-sync
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/audio-video-sync)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
[English](./README.md) | **中文版**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
🙆♀️ 多机位录影对齐时间轴工具,**科研常备!**
|
|
11
|
+
📱 通过音频对齐,绕开视频源数据在不同系统(苹果、安卓和小米)不一致的额外处理,以及录像设备系统时间本身就不正确的问题。
|
|
12
|
+
🌟 基于音频互相关的多机位视频同步库。使用 FFT 加速的互相关算法,通过分析音频波形自动对齐多个视频的时间轴,精度可达毫秒级。
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## 特性
|
|
16
|
+
|
|
17
|
+
- 🎯 **高精度** - 毫秒级同步精度(±2ms)
|
|
18
|
+
- ⚡ **高性能** - FFT 加速的互相关算法
|
|
19
|
+
- 🌐 **浏览器原生** - 基于 FFmpeg.wasm,无需服务端
|
|
20
|
+
- 📦 **零配置** - 开箱即用,自动处理音频提取
|
|
21
|
+
- 🔧 **灵活** - 支持自定义 FFmpeg 实例和参数
|
|
22
|
+
- 📝 **TypeScript** - 完整的类型定义
|
|
23
|
+
|
|
24
|
+
## 原理
|
|
25
|
+
|
|
26
|
+
多机位录制的视频虽然 creation_time 可能不准确,但录制的环境声音是一致的。通过对音频波形进行互相关分析,可以精确计算出各视频之间的时间偏移。
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
视频A音频: ──────█████████████──────────
|
|
30
|
+
视频B音频: ────────────█████████████────
|
|
31
|
+
↑
|
|
32
|
+
偏移量 Δt
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
<!-- ## 环境要求 (不确定)
|
|
36
|
+
|
|
37
|
+
- **Node.js**: >= 16.0.0
|
|
38
|
+
- **浏览器**: 需要支持 SharedArrayBuffer(Chrome 92+、Firefox 79+、Safari 15.2+)
|
|
39
|
+
- **HTTPS**: 生产环境需要 HTTPS 才能使用 SharedArrayBuffer -->
|
|
40
|
+
|
|
41
|
+
## 安装
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install audio-video-sync @ffmpeg/ffmpeg @ffmpeg/util
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 开发
|
|
48
|
+
|
|
49
|
+
### 克隆和设置
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/EuanTop/audio-video-sync.git
|
|
53
|
+
cd audio-video-sync
|
|
54
|
+
npm install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 从源码构建
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 构建包
|
|
61
|
+
npm run build
|
|
62
|
+
|
|
63
|
+
# 这会生成:
|
|
64
|
+
# - dist/index.js (CommonJS 格式)
|
|
65
|
+
# - dist/index.esm.js (ES Module 格式)
|
|
66
|
+
# - dist/index.d.ts (TypeScript 类型定义)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 测试构建结果
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 在浏览器中打开 test.html 来测试构建的包
|
|
73
|
+
open test.html
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 脚本命令
|
|
77
|
+
|
|
78
|
+
- `npm run build` - 构建用于发布的包
|
|
79
|
+
- `npm run test` - 运行测试(占位符)
|
|
80
|
+
- `npm run prepublishOnly` - 发布前自动构建
|
|
81
|
+
|
|
82
|
+
## 快速开始
|
|
83
|
+
|
|
84
|
+
### 基本用法
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
import { syncVideos } from 'audio-video-sync';
|
|
88
|
+
|
|
89
|
+
// 同步多个视频文件
|
|
90
|
+
const result = await syncVideos([
|
|
91
|
+
{ file: video1File, id: 'cam1' },
|
|
92
|
+
{ file: video2File, id: 'cam2' },
|
|
93
|
+
{ file: video3File, id: 'cam3' },
|
|
94
|
+
{ file: video4File, id: 'cam4' }
|
|
95
|
+
], {
|
|
96
|
+
referenceIndex: 0, // 以第一个视频为参考
|
|
97
|
+
sampleRate: 16000, // 采样率
|
|
98
|
+
maxDuration: 60 // 只分析前60秒
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log(result);
|
|
102
|
+
// {
|
|
103
|
+
// referenceId: 'cam1',
|
|
104
|
+
// results: [
|
|
105
|
+
// { id: 'cam1', offsetSeconds: 0, confidence: 1 },
|
|
106
|
+
// { id: 'cam2', offsetSeconds: 0.523, confidence: 0.89 },
|
|
107
|
+
// { id: 'cam3', offsetSeconds: -0.127, confidence: 0.92 },
|
|
108
|
+
// { id: 'cam4', offsetSeconds: 1.234, confidence: 0.85 }
|
|
109
|
+
// ],
|
|
110
|
+
// success: true
|
|
111
|
+
// }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 使用已有的 FFmpeg 实例
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
118
|
+
import { AudioVideoSync } from 'audio-video-sync';
|
|
119
|
+
|
|
120
|
+
// 如果你的项目已经有 FFmpeg 实例
|
|
121
|
+
const ffmpeg = new FFmpeg();
|
|
122
|
+
await ffmpeg.load();
|
|
123
|
+
|
|
124
|
+
const sync = new AudioVideoSync(ffmpeg);
|
|
125
|
+
const result = await sync.syncVideos(videos);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 计算两个视频的偏移
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
import { createSync } from 'audio-video-sync';
|
|
132
|
+
|
|
133
|
+
const sync = createSync();
|
|
134
|
+
const { offsetSeconds, confidence } = await sync.calculateOffset(
|
|
135
|
+
referenceVideoFile,
|
|
136
|
+
targetVideoFile
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
console.log(`目标视频相对参考视频偏移: ${offsetSeconds} 秒`);
|
|
140
|
+
console.log(`置信度: ${(confidence * 100).toFixed(1)}%`);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 带进度回调
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const result = await syncVideos(videos, {
|
|
147
|
+
onProgress: (stage, progress) => {
|
|
148
|
+
if (stage === 'extracting') {
|
|
149
|
+
console.log(`提取音频: ${(progress * 100).toFixed(0)}%`);
|
|
150
|
+
} else if (stage === 'correlating') {
|
|
151
|
+
console.log(`计算相关: ${(progress * 100).toFixed(0)}%`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 与 RFID 数据对齐
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
import { syncVideos } from 'audio-video-sync';
|
|
161
|
+
|
|
162
|
+
// 假设 cam1 的时间与 RFID 数据是对齐的
|
|
163
|
+
const videos = [
|
|
164
|
+
{
|
|
165
|
+
file: cam1File,
|
|
166
|
+
id: 'cam1',
|
|
167
|
+
originalStartTime: new Date('2024-01-11T10:00:00') // RFID 对齐的时间
|
|
168
|
+
},
|
|
169
|
+
{ file: cam2File, id: 'cam2' },
|
|
170
|
+
{ file: cam3File, id: 'cam3' },
|
|
171
|
+
{ file: cam4File, id: 'cam4' }
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const result = await syncVideos(videos, { referenceIndex: 0 });
|
|
175
|
+
|
|
176
|
+
// 所有视频都会得到校正后的开始时间
|
|
177
|
+
result.results.forEach(r => {
|
|
178
|
+
console.log(`${r.id}: 校正后开始时间 = ${r.correctedStartTime}`);
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## API
|
|
183
|
+
|
|
184
|
+
### syncVideos(videos, options)
|
|
185
|
+
|
|
186
|
+
同步多个视频文件。
|
|
187
|
+
|
|
188
|
+
**参数:**
|
|
189
|
+
- `videos`: `VideoInput[]` - 视频输入数组
|
|
190
|
+
- `file`: `File | Blob` - 视频文件
|
|
191
|
+
- `id`: `string` (可选) - 视频标识
|
|
192
|
+
- `originalStartTime`: `Date` (可选) - 原始开始时间
|
|
193
|
+
- `options`: `SyncOptions` (可选)
|
|
194
|
+
- `referenceIndex`: `number` - 参考视频索引,默认 0
|
|
195
|
+
- `sampleRate`: `number` - 采样率,默认 16000
|
|
196
|
+
- `maxDuration`: `number` - 最大分析时长(秒),默认 60
|
|
197
|
+
- `minConfidence`: `number` - 最小置信度阈值,默认 0.3
|
|
198
|
+
- `onProgress`: `(stage, progress) => void` - 进度回调
|
|
199
|
+
|
|
200
|
+
**返回:** `Promise<MultiSyncResult>`
|
|
201
|
+
|
|
202
|
+
### AudioVideoSync
|
|
203
|
+
|
|
204
|
+
同步器类,支持复用 FFmpeg 实例。
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
const sync = new AudioVideoSync(ffmpeg?);
|
|
208
|
+
await sync.load();
|
|
209
|
+
const result = await sync.syncVideos(videos, options);
|
|
210
|
+
const offset = await sync.calculateOffset(refVideo, targetVideo);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 底层 API
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
import {
|
|
217
|
+
extractAudio, // 从视频提取音频
|
|
218
|
+
crossCorrelate, // 计算互相关
|
|
219
|
+
findPeakOffset, // 找到峰值偏移
|
|
220
|
+
calculateConfidence // 计算置信度
|
|
221
|
+
} from 'audio-video-sync';
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 类型定义
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
interface VideoInput {
|
|
228
|
+
file: File | Blob;
|
|
229
|
+
id?: string;
|
|
230
|
+
originalStartTime?: Date;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface SyncResult {
|
|
234
|
+
id: string;
|
|
235
|
+
offsetSeconds: number;
|
|
236
|
+
offsetSamples: number;
|
|
237
|
+
confidence: number;
|
|
238
|
+
correctedStartTime: Date | null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface MultiSyncResult {
|
|
242
|
+
referenceId: string;
|
|
243
|
+
results: SyncResult[];
|
|
244
|
+
sampleRate: number;
|
|
245
|
+
success: boolean;
|
|
246
|
+
error?: string;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 精度对比
|
|
251
|
+
|
|
252
|
+
| 方法 | 精度 |
|
|
253
|
+
|------|------|
|
|
254
|
+
| creation_time | ±秒级 |
|
|
255
|
+
| 文件时间戳 | ±百毫秒 |
|
|
256
|
+
| 音频互相关 | **±2ms** |
|
|
257
|
+
|
|
258
|
+
## 注意事项
|
|
259
|
+
|
|
260
|
+
1. **音频质量**: 确保视频有清晰的环境音,纯静音视频无法同步
|
|
261
|
+
2. **采样率**: 16000 Hz 足够用于同步,更高采样率会增加计算量
|
|
262
|
+
3. **分析时长**: 通常分析前 30-60 秒就足够,不需要处理整个视频
|
|
263
|
+
4. **内存占用**: 长视频建议限制 `maxDuration` 以控制内存使用
|
|
264
|
+
5. **浏览器兼容**: 需要支持 SharedArrayBuffer 的浏览器环境
|
|
265
|
+
|
|
266
|
+
## 技术栈
|
|
267
|
+
|
|
268
|
+
- FFmpeg.wasm - 视频解码和音频提取
|
|
269
|
+
- FFT (Fast Fourier Transform) - 频域互相关计算
|
|
270
|
+
- TypeScript - 类型安全
|
|
271
|
+
|
|
272
|
+
## 贡献
|
|
273
|
+
|
|
274
|
+
欢迎提交 Issue 和 Pull Request!
|
|
275
|
+
|
|
276
|
+
## 许可证
|
|
277
|
+
|
|
278
|
+
[MIT](LICENSE)
|
package/dist/audio.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 音频提取模块
|
|
3
|
+
* 使用 FFmpeg.wasm 从视频文件中提取音频 PCM 数据
|
|
4
|
+
*/
|
|
5
|
+
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
6
|
+
export interface AudioData {
|
|
7
|
+
samples: Float32Array;
|
|
8
|
+
sampleRate: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
channels: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ExtractOptions {
|
|
13
|
+
/** 目标采样率,默认 16000 Hz(足够用于同步,且计算快) */
|
|
14
|
+
sampleRate?: number;
|
|
15
|
+
/** 是否转为单声道,默认 true */
|
|
16
|
+
mono?: boolean;
|
|
17
|
+
/** 只提取前 N 秒用于同步(节省内存和计算),默认 60 秒 */
|
|
18
|
+
maxDuration?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 从视频文件提取音频 PCM 数据
|
|
22
|
+
*
|
|
23
|
+
* @param ffmpeg 已加载的 FFmpeg 实例
|
|
24
|
+
* @param videoFile 视频文件(File 或 Blob)
|
|
25
|
+
* @param options 提取选项
|
|
26
|
+
* @returns 音频 PCM 数据
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractAudio(ffmpeg: FFmpeg, videoFile: File | Blob, options?: ExtractOptions): Promise<AudioData>;
|
|
29
|
+
/**
|
|
30
|
+
* 从 AudioBuffer 获取 PCM 数据(用于 Web Audio API)
|
|
31
|
+
*/
|
|
32
|
+
export declare function audioBufferToFloat32(audioBuffer: AudioBuffer): Float32Array;
|
|
33
|
+
/**
|
|
34
|
+
* 对音频数据进行降采样
|
|
35
|
+
*/
|
|
36
|
+
export declare function downsample(samples: Float32Array, fromRate: number, toRate: number): Float32Array;
|
|
37
|
+
/**
|
|
38
|
+
* 对音频应用简单的预处理(去直流偏移、归一化)
|
|
39
|
+
*/
|
|
40
|
+
export declare function preprocessAudio(samples: Float32Array): Float32Array;
|
package/dist/fft.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FFT (Fast Fourier Transform) 实现
|
|
3
|
+
* 用于音频信号的频域分析和互相关计算
|
|
4
|
+
*/
|
|
5
|
+
export type Complex = [number, number];
|
|
6
|
+
/**
|
|
7
|
+
* 将数组填充到 2 的幂次长度
|
|
8
|
+
*/
|
|
9
|
+
export declare function padToPowerOfTwo(arr: Float32Array, targetLength: number): Float32Array;
|
|
10
|
+
/**
|
|
11
|
+
* 计算下一个 2 的幂次
|
|
12
|
+
*/
|
|
13
|
+
export declare function nextPowerOfTwo(n: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Cooley-Tukey FFT 算法
|
|
16
|
+
*/
|
|
17
|
+
export declare function fft(input: Float32Array): Complex[];
|
|
18
|
+
/**
|
|
19
|
+
* 逆 FFT
|
|
20
|
+
*/
|
|
21
|
+
export declare function ifft(input: Complex[]): Complex[];
|
|
22
|
+
/**
|
|
23
|
+
* 复数输入的 FFT
|
|
24
|
+
*/
|
|
25
|
+
export declare function fftComplex(input: Complex[]): Complex[];
|
|
26
|
+
/**
|
|
27
|
+
* 计算两个信号的互相关
|
|
28
|
+
* 使用 FFT 加速: corr(a,b) = IFFT(FFT(a) * conj(FFT(b)))
|
|
29
|
+
*
|
|
30
|
+
* @param signalA 参考信号
|
|
31
|
+
* @param signalB 待对齐信号
|
|
32
|
+
* @returns 互相关结果数组
|
|
33
|
+
*/
|
|
34
|
+
export declare function crossCorrelate(signalA: Float32Array, signalB: Float32Array): Float32Array;
|
|
35
|
+
/**
|
|
36
|
+
* 从互相关结果中找到最大峰值位置
|
|
37
|
+
*
|
|
38
|
+
* @param correlation 互相关结果
|
|
39
|
+
* @param signalBLength 信号 B 的原始长度
|
|
40
|
+
* @returns 偏移量(正值表示 B 相对于 A 延迟,负值表示 B 领先)
|
|
41
|
+
*/
|
|
42
|
+
export declare function findPeakOffset(correlation: Float32Array, signalBLength: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* 计算互相关的置信度(归一化相关系数)
|
|
45
|
+
*/
|
|
46
|
+
export declare function calculateConfidence(signalA: Float32Array, signalB: Float32Array, correlation: Float32Array, peakIndex: number): number;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audio-video-sync
|
|
3
|
+
*
|
|
4
|
+
* 基于音频互相关的多机位视频同步库
|
|
5
|
+
* Multi-camera video synchronization using audio cross-correlation
|
|
6
|
+
*/
|
|
7
|
+
export { AudioVideoSync, createSync, syncVideos, type VideoInput, type SyncResult, type MultiSyncResult, type SyncOptions } from './sync';
|
|
8
|
+
export { extractAudio, preprocessAudio, downsample, audioBufferToFloat32, type AudioData, type ExtractOptions } from './audio';
|
|
9
|
+
export { fft, ifft, crossCorrelate, findPeakOffset, calculateConfidence, nextPowerOfTwo, padToPowerOfTwo, type Complex } from './fft';
|