Skip to content

Commit 3840a96

Browse files
omitsu-devclaude
authored andcommitted
feat: 100 commands milestone! Add oscilloscope, extract-stream, blackdetect (v2.3.0)
🎯 100 command milestone reached! New commands: - oscilloscope: Audio visualization overlay on video - extract-stream: Extract specific stream by index (v:0, a:1, s:0) - blackdetect: Detect black frames/scenes in video https://claude.ai/code/session_01BPZRs1uut8bAVcEQsjWjsw
1 parent 7b42fc6 commit 3840a96

6 files changed

Lines changed: 89 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ jobs:
125125
node bin/ffmpeg-quick.js duration test.mp4 --dry-run
126126
node bin/ffmpeg-quick.js sample-rate test.mp4 44100 --dry-run
127127
node bin/ffmpeg-quick.js bit-depth test.mp4 16 --dry-run
128+
node bin/ffmpeg-quick.js oscilloscope test.mp4 --dry-run
129+
node bin/ffmpeg-quick.js extract-stream test.mp4 v:0 --dry-run
130+
node bin/ffmpeg-quick.js blackdetect test.mp4 --dry-run
128131
129132
- name: Test real encode (compress a generated video)
130133
run: |

bin/ffmpeg-quick.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,16 @@ import { register as multiAudio } from "../src/commands/multi-audio.js";
9898
import { register as duration } from "../src/commands/duration.js";
9999
import { register as sampleRate } from "../src/commands/sample-rate.js";
100100
import { register as bitDepth } from "../src/commands/bit-depth.js";
101+
import { register as oscilloscope } from "../src/commands/oscilloscope.js";
102+
import { register as extractStream } from "../src/commands/extract-stream.js";
103+
import { register as blackdetect } from "../src/commands/blackdetect.js";
101104

102105
const program = new Command();
103106

104107
program
105108
.name("ffmpeg-quick")
106109
.description("Quick FFmpeg presets for common video tasks")
107-
.version("2.2.0");
110+
.version("2.3.0");
108111

109112
compress(program);
110113
gif(program);
@@ -203,5 +206,8 @@ multiAudio(program);
203206
duration(program);
204207
sampleRate(program);
205208
bitDepth(program);
209+
oscilloscope(program);
210+
extractStream(program);
211+
blackdetect(program);
206212

207213
program.parse();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ffmpeg-quick",
3-
"version": "2.2.0",
3+
"version": "2.3.0",
44
"description": "Quick FFmpeg presets for common video tasks / FFmpegプリセットCLI",
55
"type": "module",
66
"bin": {

src/commands/blackdetect.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { run } from "../run.js";
2+
3+
export function register(program) {
4+
program
5+
.command("blackdetect")
6+
.description("Detect black frames / black scenes in video")
7+
.argument("<input>", "Input video file")
8+
.option("--threshold <n>", "Black pixel threshold (0.0-1.0)", "0.98")
9+
.option("--duration <sec>", "Minimum black scene duration", "0.5")
10+
.option("--dry-run", "Print the FFmpeg command without running it")
11+
.action((input, opts) => {
12+
const filter = `blackdetect=d=${opts.duration}:pix_th=${opts.threshold}`;
13+
const args = ["-i", input, "-vf", filter, "-f", "null", "-"];
14+
15+
run(args, { dryRun: opts.dryRun });
16+
});
17+
}

src/commands/extract-stream.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("extract-stream")
7+
.description("Extract a specific stream by index (video, audio, or subtitle)")
8+
.argument("<input>", "Input media file")
9+
.argument("<stream>", "Stream specifier (e.g. v:0, a:1, s:0)")
10+
.option("-o, --output <path>", "Output file path")
11+
.option("--dry-run", "Print the FFmpeg command without running it")
12+
.option("-y", "Overwrite output without asking")
13+
.action((input, stream, opts) => {
14+
const typeMap = { v: ".mp4", a: ".aac", s: ".srt" };
15+
const type = stream.split(":")[0];
16+
const ext = typeMap[type] || "";
17+
const out = opts.output || outputName(input, `stream-${stream.replace(":", "")}`, ext);
18+
const args = ["-i", input, "-map", `0:${stream}`, "-c", "copy"];
19+
20+
if (opts.y) args.push("-y");
21+
args.push(out);
22+
run(args, { dryRun: opts.dryRun });
23+
});
24+
}

src/commands/oscilloscope.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { run } from "../run.js";
2+
import { outputName } from "../utils.js";
3+
4+
export function register(program) {
5+
program
6+
.command("oscilloscope")
7+
.description("Add oscilloscope overlay to video (audio visualization on video)")
8+
.argument("<input>", "Input video file with audio")
9+
.option("--size <n>", "Oscilloscope size (0.0-1.0)", "0.5")
10+
.option("--pos <position>", "Position: top-left, top-right, bottom-left, bottom-right, center", "bottom-right")
11+
.option("-o, --output <path>", "Output file path")
12+
.option("--dry-run", "Print the FFmpeg command without running it")
13+
.option("-y", "Overwrite output without asking")
14+
.action((input, opts) => {
15+
const posMap = {
16+
"top-left": "x=0:y=0",
17+
"top-right": "x=1:y=0",
18+
"bottom-left": "x=0:y=1",
19+
"bottom-right": "x=1:y=1",
20+
"center": "x=0.5:y=0.5",
21+
};
22+
23+
const pos = posMap[opts.pos];
24+
if (!pos) {
25+
console.error(`Error: --pos must be one of: ${Object.keys(posMap).join(", ")}`);
26+
process.exit(1);
27+
}
28+
29+
const filter = `avectorscope=s=320x320:zoom=1.5:draw=line,format=yuva420p[osc];[0:v][osc]overlay=W*0.7:H*0.7`;
30+
const out = opts.output || outputName(input, "oscilloscope");
31+
const args = ["-i", input, "-filter_complex", filter, "-c:a", "copy"];
32+
33+
if (opts.y) args.push("-y");
34+
args.push(out);
35+
run(args, { dryRun: opts.dryRun });
36+
});
37+
}

0 commit comments

Comments
 (0)