nodejs-audio-visualizer 4.2.5 → 4.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +89 -26
- package/dist/index.js.map +1 -1
- package/dist/subtitleConvert.d.ts +3 -0
- package/dist/subtitleConvert.js +62 -0
- package/dist/subtitleConvert.js.map +1 -0
- package/dist/video.d.ts +5 -0
- package/dist/video.js +24 -3
- package/dist/video.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +80 -0
- package/src/index.ts +171 -107
- package/src/subtitleConvert.ts +63 -0
- package/src/tests/config.test.ts +69 -0
- package/src/tests/subtitleConvert.test.ts +23 -0
- package/src/tests/video.test.ts +67 -0
- package/src/video.ts +36 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { lrcToSrt, isLikelyLrc, normalizeInlineSubtitlesToSrt } from '../subtitleConvert';
|
|
3
|
+
|
|
4
|
+
describe('subtitleConvert', function () {
|
|
5
|
+
it('isLikelyLrc', function () {
|
|
6
|
+
expect(isLikelyLrc('[00:01.00]Hello')).equal(true);
|
|
7
|
+
expect(isLikelyLrc('1\n00:00:01,000 --> 00:00:02,000\nSRT')).equal(false);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('lrcToSrt converts basic LRC to SRT', function () {
|
|
11
|
+
const lrc = '[00:01.00]Line one\n[00:03.50]Line two';
|
|
12
|
+
const srt = lrcToSrt(lrc);
|
|
13
|
+
expect(srt).to.include('00:00:01,000 --> 00:00:03,500');
|
|
14
|
+
expect(srt).to.include('Line one');
|
|
15
|
+
expect(srt).to.include('00:00:03,500 -->');
|
|
16
|
+
expect(srt).to.include('Line two');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('normalizeInlineSubtitlesToSrt leaves SRT unchanged', function () {
|
|
20
|
+
const s = '1\n00:00:00,000 --> 00:00:01,000\nX';
|
|
21
|
+
expect(normalizeInlineSubtitlesToSrt(s)).equal(s);
|
|
22
|
+
});
|
|
23
|
+
});
|
package/src/tests/video.test.ts
CHANGED
|
@@ -68,4 +68,71 @@ describe('video', function () {
|
|
|
68
68
|
expect(tIdx).greaterThan(-1);
|
|
69
69
|
expect(spawnArgs[tIdx + 1]).to.equal('15');
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
it('spawnFfmpegVideoWriter adds subtitles filter when subtitle file is provided', function () {
|
|
73
|
+
const childProcessReadableStream = new Readable();
|
|
74
|
+
childProcessReadableStream._read = () => { };
|
|
75
|
+
const childProcessWritableStream = new Writable();
|
|
76
|
+
(<Pipe>childProcessWritableStream.pipe) = () => childProcessWritableStream;
|
|
77
|
+
|
|
78
|
+
childProcessStream.stdin = childProcessWritableStream;
|
|
79
|
+
childProcessStream.stderr = childProcessReadableStream;
|
|
80
|
+
|
|
81
|
+
let spawnArgs: string[] = [];
|
|
82
|
+
const spawnStub = child_process.spawn as SinonStub;
|
|
83
|
+
spawnStub.callsFake((_cmd: string, args: string[]) => {
|
|
84
|
+
spawnArgs = args;
|
|
85
|
+
return childProcessStream as ChildProcessWithoutNullStreams;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
spawnFfmpegVideoWriter({
|
|
89
|
+
audioFilename: 'audio.mp3',
|
|
90
|
+
subtitleFilename: '/tmp/subtitles.srt',
|
|
91
|
+
videoFileName: 'out.mp4',
|
|
92
|
+
fps: 25,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
spawnStub.resetBehavior();
|
|
96
|
+
spawnStub.returns(childProcessStream as ChildProcessWithoutNullStreams);
|
|
97
|
+
|
|
98
|
+
const vfIdx = spawnArgs.indexOf('-vf');
|
|
99
|
+
expect(vfIdx).greaterThan(-1);
|
|
100
|
+
expect(spawnArgs[vfIdx + 1]).to.equal(
|
|
101
|
+
"subtitles='/tmp/subtitles.srt':force_style='Alignment=2'",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('spawnFfmpegVideoWriter passes subtitle Alignment from subtitleAlignmentAss', function () {
|
|
106
|
+
const childProcessReadableStream = new Readable();
|
|
107
|
+
childProcessReadableStream._read = () => { };
|
|
108
|
+
const childProcessWritableStream = new Writable();
|
|
109
|
+
(<Pipe>childProcessWritableStream.pipe) = () => childProcessWritableStream;
|
|
110
|
+
|
|
111
|
+
childProcessStream.stdin = childProcessWritableStream;
|
|
112
|
+
childProcessStream.stderr = childProcessReadableStream;
|
|
113
|
+
|
|
114
|
+
let spawnArgs: string[] = [];
|
|
115
|
+
const spawnStub = child_process.spawn as SinonStub;
|
|
116
|
+
spawnStub.callsFake((_cmd: string, args: string[]) => {
|
|
117
|
+
spawnArgs = args;
|
|
118
|
+
return childProcessStream as ChildProcessWithoutNullStreams;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
spawnFfmpegVideoWriter({
|
|
122
|
+
audioFilename: 'audio.mp3',
|
|
123
|
+
subtitleFilename: '/tmp/subtitles.srt',
|
|
124
|
+
subtitleAlignmentAss: 8,
|
|
125
|
+
videoFileName: 'out.mp4',
|
|
126
|
+
fps: 25,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
spawnStub.resetBehavior();
|
|
130
|
+
spawnStub.returns(childProcessStream as ChildProcessWithoutNullStreams);
|
|
131
|
+
|
|
132
|
+
const vfIdx = spawnArgs.indexOf('-vf');
|
|
133
|
+
expect(vfIdx).greaterThan(-1);
|
|
134
|
+
expect(spawnArgs[vfIdx + 1]).to.equal(
|
|
135
|
+
"subtitles='/tmp/subtitles.srt':force_style='Alignment=8'",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
71
138
|
});
|
package/src/video.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface AudioMuxSegment {
|
|
|
12
12
|
|
|
13
13
|
interface FfmpegVideoWriterConfig {
|
|
14
14
|
audioFilename: string;
|
|
15
|
+
subtitleFilename?: string;
|
|
16
|
+
/** ASS `Alignment` in FFmpeg `subtitles` `force_style` (e.g. 2 bottom, 5 middle, 8 top). Default 2. */
|
|
17
|
+
subtitleAlignmentAss?: number;
|
|
15
18
|
videoFileName: string;
|
|
16
19
|
fps: number;
|
|
17
20
|
crf?: string;
|
|
@@ -24,6 +27,15 @@ interface FfmpegVideoWriterConfig {
|
|
|
24
27
|
audioSegment?: AudioMuxSegment;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
const escapeSubtitleFilterPath = (subtitlePath: string) =>
|
|
31
|
+
subtitlePath
|
|
32
|
+
.replace(/\\/g, '\\\\')
|
|
33
|
+
.replace(/:/g, '\\:')
|
|
34
|
+
.replace(/'/g, "\\'")
|
|
35
|
+
.replace(/,/g, '\\,')
|
|
36
|
+
.replace(/\[/g, '\\[')
|
|
37
|
+
.replace(/\]/g, '\\]');
|
|
38
|
+
|
|
27
39
|
export const spawnFfmpegVideoWriter = (config: FfmpegVideoWriterConfig) => {
|
|
28
40
|
if (!ffmpegPath) {
|
|
29
41
|
throw new Error('ffmpeg path not found');
|
|
@@ -42,7 +54,21 @@ export const spawnFfmpegVideoWriter = (config: FfmpegVideoWriterConfig) => {
|
|
|
42
54
|
'-i', config.audioFilename,
|
|
43
55
|
'-crf', crf,
|
|
44
56
|
'-c:a', 'aac', '-b:a', '384k', '-profile:a', 'aac_low',
|
|
45
|
-
'-c:v', 'libx264',
|
|
57
|
+
'-c:v', 'libx264',
|
|
58
|
+
'-r', `${config.fps}`,
|
|
59
|
+
'-pix_fmt', 'yuv420p',
|
|
60
|
+
'-preset', preset,
|
|
61
|
+
);
|
|
62
|
+
if (config.subtitleFilename) {
|
|
63
|
+
const subPath = escapeSubtitleFilterPath(config.subtitleFilename);
|
|
64
|
+
const alignment = config.subtitleAlignmentAss ?? 2;
|
|
65
|
+
args.push(
|
|
66
|
+
'-vf',
|
|
67
|
+
`subtitles='${subPath}':force_style='Alignment=${alignment}'`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
args.push(
|
|
71
|
+
config.videoFileName,
|
|
46
72
|
'-r', `${config.fps}`,
|
|
47
73
|
'-i', '-'
|
|
48
74
|
);
|
|
@@ -364,6 +390,12 @@ export const writeConcatFile = (
|
|
|
364
390
|
return concatPath;
|
|
365
391
|
};
|
|
366
392
|
|
|
393
|
+
export const writeSubtitlesFile = (subtitles: string): string => {
|
|
394
|
+
const subtitlePath = joinPath(tmpdir(), `av-subtitles-${Date.now()}.srt`);
|
|
395
|
+
writeFileSync(subtitlePath, subtitles, 'utf-8');
|
|
396
|
+
return subtitlePath;
|
|
397
|
+
};
|
|
398
|
+
|
|
367
399
|
export const spawnConcatVideoFrameReader = (config: {
|
|
368
400
|
concatFilePath: string;
|
|
369
401
|
fps: number;
|
|
@@ -398,6 +430,8 @@ export const spawnConcatVideoFrameReader = (config: {
|
|
|
398
430
|
return spawn(ffmpegPath, args);
|
|
399
431
|
};
|
|
400
432
|
|
|
401
|
-
export const
|
|
433
|
+
export const cleanupTempFile = (filePath: string) => {
|
|
402
434
|
try { unlinkSync(filePath); } catch {}
|
|
403
435
|
};
|
|
436
|
+
|
|
437
|
+
export const cleanupConcatFile = cleanupTempFile;
|