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.
@@ -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
+ });
@@ -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', '-r', `${config.fps}`, '-pix_fmt', 'yuv420p', '-preset', preset, config.videoFileName,
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 cleanupConcatFile = (filePath: string) => {
433
+ export const cleanupTempFile = (filePath: string) => {
402
434
  try { unlinkSync(filePath); } catch {}
403
435
  };
436
+
437
+ export const cleanupConcatFile = cleanupTempFile;