mediasnacks 0.19.0 → 0.20.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/.zsh/completions/_mediasnacks +2 -1
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/avif.js +6 -4
- package/src/avif.test.js +4 -7
- package/src/cli.js +8 -6
- package/src/detectdups.js +30 -18
- package/src/dropdups.js +7 -5
- package/src/edgespic.js +10 -8
- package/src/edgespic.test.js +12 -8
- package/src/fixtures/edgespic/60fps_first.png +0 -0
- package/src/fixtures/edgespic/60fps_last.png +0 -0
- package/src/fixtures/lenna.avif +0 -0
- package/src/framediff.sh +18 -3
- package/src/hev1tohvc1.js +9 -7
- package/src/moov2front.js +9 -6
- package/src/prores.js +9 -10
- package/src/qdir.js +6 -4
- package/src/resize.js +10 -8
- package/src/seqcheck.js +7 -5
- package/src/sqcrop.js +6 -4
- package/src/ssim.js +44 -0
- package/src/utils/ffmpeg.js +3 -4
- package/src/utils/test-utils.js +4 -4
- package/src/vconcat.sh +3 -3
- package/src/vsplit.js +8 -6
- package/src/vtrim.js +7 -5
|
@@ -6,6 +6,7 @@ _mediasnacks_commands=(
|
|
|
6
6
|
|
|
7
7
|
'resize:Resizes videos or images'
|
|
8
8
|
'edgespic:Extracts first and last frames'
|
|
9
|
+
'ssim:Computes similarity of two images'
|
|
9
10
|
'gif:Video to GIF'
|
|
10
11
|
|
|
11
12
|
'detectdups:Detects sequentially duplicate frames in a video'
|
|
@@ -39,7 +40,7 @@ fi
|
|
|
39
40
|
|
|
40
41
|
local cmd="$words[2]"
|
|
41
42
|
case "$cmd" in
|
|
42
|
-
avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores)
|
|
43
|
+
avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores|ssim)
|
|
43
44
|
_files
|
|
44
45
|
;;
|
|
45
46
|
qdir)
|
package/README.md
CHANGED
package/package.json
CHANGED
package/src/avif.js
CHANGED
|
@@ -7,10 +7,12 @@ import { replaceExt, lstat } from './utils/fs-utils.js'
|
|
|
7
7
|
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const MAN = `
|
|
11
|
+
SYNOPSIS
|
|
12
|
+
mediasnacks avif [-y | --overwrite] [--output-dir=<dir>] <images>
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
DESCRIPTION
|
|
15
|
+
Converts images to AVIF.
|
|
14
16
|
`.trim()
|
|
15
17
|
|
|
16
18
|
|
|
@@ -24,7 +26,7 @@ async function main() {
|
|
|
24
26
|
})
|
|
25
27
|
|
|
26
28
|
if (values.help) {
|
|
27
|
-
console.log(
|
|
29
|
+
console.log(MAN)
|
|
28
30
|
process.exit(0)
|
|
29
31
|
}
|
|
30
32
|
|
package/src/avif.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import {
|
|
3
|
+
import { ok } from 'node:assert/strict'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ssim } from './ssim.js'
|
|
6
6
|
import { mkTempDir, cli } from './utils/test-utils.js'
|
|
7
7
|
|
|
8
8
|
const rel = f => join(import.meta.dirname, f)
|
|
@@ -11,9 +11,6 @@ test('PNG to AVIF', async () => {
|
|
|
11
11
|
const tmp = mkTempDir('avif')
|
|
12
12
|
cli('avif', '--output-dir', tmp, rel('fixtures/lenna.png'))
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await videoAttrs(rel('fixtures/lenna.avif')))
|
|
17
|
-
// That's because we use non-deterministic avif.
|
|
18
|
-
// Claude says: avif is deterministic only when it's single-threaded: '-threads 1'
|
|
14
|
+
const similarityScore = await ssim(join(tmp, 'lenna.avif'), rel('fixtures/lenna.avif'))
|
|
15
|
+
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
19
16
|
})
|
package/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ const COMMANDS = {
|
|
|
12
12
|
|
|
13
13
|
resize: ['resize.js', 'Resizes videos or images'],
|
|
14
14
|
edgespic: ['edgespic.js', 'Extracts first and last frames'],
|
|
15
|
+
ssim: ['ssim.js', 'Computes SSIM between two images'],
|
|
15
16
|
gif: ['gif.sh', 'Video to GIF\n'],
|
|
16
17
|
|
|
17
18
|
detectdups: ['detectdups.js', 'Detects duplicate frames in a video'],
|
|
@@ -38,12 +39,13 @@ const COMMANDS = {
|
|
|
38
39
|
curltime: ['curltime.sh', 'Measures request response timings'],
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const
|
|
42
|
-
|
|
42
|
+
const MAN = `
|
|
43
|
+
SYNOPSIS
|
|
44
|
+
mediasnacks <command> <args>
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
COMMANDS
|
|
45
47
|
${Object.entries(COMMANDS).map(([cmd, [, title]]) =>
|
|
46
|
-
`
|
|
48
|
+
` ${styleText('bold', cmd.padEnd(12, ' '))}\t${title}`).join('\n')}
|
|
47
49
|
`.trim()
|
|
48
50
|
|
|
49
51
|
|
|
@@ -54,12 +56,12 @@ if (opt === '-v' || opt === '--version') {
|
|
|
54
56
|
process.exit(0)
|
|
55
57
|
}
|
|
56
58
|
if (opt === '-h' || opt === '--help') {
|
|
57
|
-
console.log(
|
|
59
|
+
console.log(MAN)
|
|
58
60
|
process.exit(0)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
if (!opt) {
|
|
62
|
-
console.log(
|
|
64
|
+
console.log(MAN)
|
|
63
65
|
process.exit(1)
|
|
64
66
|
}
|
|
65
67
|
if (!Object.hasOwn(COMMANDS, opt)) {
|
package/src/detectdups.js
CHANGED
|
@@ -4,16 +4,26 @@ import { parseOptions } from './utils/parseOptions.js'
|
|
|
4
4
|
import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/ffmpeg.js'
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const MAN = `
|
|
8
8
|
Usage: mediasnacks detectdups [options] <video>
|
|
9
9
|
|
|
10
|
-
Detects sequentially duplicate frames in a video and prints their
|
|
10
|
+
Detects sequentially duplicate frames in a video and prints a histogram of their distance.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
EXAMPLES
|
|
13
|
+
Peak at N=2, means that every other frame is repeated, such as in a
|
|
14
|
+
video that was converted from 30 to 60fps without interpolation.
|
|
15
|
+
|
|
16
|
+
Peak at N=6, means that the 6th frame in a sequence is repeated.
|
|
17
|
+
For instance, a video converted from 25 to 30fps, or 50 to 60fps.
|
|
18
|
+
|
|
19
|
+
OPTIONS
|
|
13
20
|
-s, --seek <sec> Video start time for detection
|
|
14
21
|
-d, --duration <sec> Analyze this many seconds of video
|
|
15
22
|
-v, --verbose
|
|
16
23
|
-h, --help
|
|
24
|
+
|
|
25
|
+
SEE ALSO
|
|
26
|
+
mediasnacks framediff
|
|
17
27
|
`.trim()
|
|
18
28
|
|
|
19
29
|
|
|
@@ -27,21 +37,27 @@ async function main() {
|
|
|
27
37
|
})
|
|
28
38
|
|
|
29
39
|
if (values.help) {
|
|
30
|
-
console.log(
|
|
40
|
+
console.log(MAN)
|
|
31
41
|
process.exit(0)
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
if (files.length !== 1)
|
|
35
|
-
throw new Error('One video file must be specified. See mediasnacks detectdups --help')
|
|
45
|
+
throw new Error('Invalid input file. One video file must be specified. See mediasnacks detectdups --help')
|
|
36
46
|
|
|
37
47
|
const v = await videoAttrs(files[0])
|
|
38
48
|
|
|
39
49
|
if (v.codec_type !== 'video')
|
|
40
|
-
throw new Error('
|
|
50
|
+
throw new Error('Invalid input file. Must be a video.')
|
|
41
51
|
|
|
42
52
|
const vDur = Number(v.duration)
|
|
43
|
-
|
|
44
|
-
const
|
|
53
|
+
|
|
54
|
+
const seek = values.seek
|
|
55
|
+
? Number(values.seek)
|
|
56
|
+
: vDur > 60 ? 20 : 0
|
|
57
|
+
|
|
58
|
+
const duration = values.duration
|
|
59
|
+
? Number(values.duration)
|
|
60
|
+
: vDur > 60 ? 20 : vDur
|
|
45
61
|
|
|
46
62
|
if (isNaN(seek) || seek < 0)
|
|
47
63
|
throw new Error(`Invalid --seek value: ${values.seek}`)
|
|
@@ -53,8 +69,8 @@ async function main() {
|
|
|
53
69
|
throw new Error(`Invalid analysis range. Exceeds video duration: ${vDur}`)
|
|
54
70
|
|
|
55
71
|
|
|
56
|
-
const
|
|
57
|
-
analyze(
|
|
72
|
+
const dups = await detectDuplicateFramesNums(files[0], seek, duration)
|
|
73
|
+
analyze(dups, seek, duration)
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
async function detectDuplicateFramesNums(video, seek, duration) {
|
|
@@ -84,22 +100,18 @@ async function detectDuplicateFramesNums(video, seek, duration) {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
function analyze(dup_frames, seek, duration) {
|
|
87
|
-
const dup_distance = []
|
|
88
103
|
const histogram = {}
|
|
89
104
|
for (let i = 1; i < dup_frames.length; i++) {
|
|
90
105
|
const diff = dup_frames[i] - dup_frames[i - 1]
|
|
91
|
-
dup_distance.push(diff)
|
|
92
106
|
histogram[diff] = (histogram[diff] || 0) + 1
|
|
93
107
|
}
|
|
94
|
-
console.log({
|
|
108
|
+
console.log(JSON.stringify({
|
|
95
109
|
analyzed_region: {
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
start_sec: seek,
|
|
111
|
+
end_sec: seek + duration
|
|
98
112
|
},
|
|
99
|
-
dup_frames,
|
|
100
|
-
dup_distance,
|
|
101
113
|
histogram
|
|
102
|
-
})
|
|
114
|
+
}, null, 2))
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
main().catch(err => {
|
package/src/dropdups.js
CHANGED
|
@@ -17,12 +17,14 @@ const PRORES_PROFILES = {
|
|
|
17
17
|
const PROFILE = PRORES_PROFILES.hq
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const MAN = `
|
|
21
|
+
SYNOPSIS
|
|
22
|
+
mediasnacks dropdups [-n <bad-frame-number>] <video>
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
DESCRIPTION
|
|
25
|
+
Removes sequentially duplicate frames and outputs ProRes 422 HQ.
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
OPTIONS
|
|
26
28
|
-n, --bad-frame-number <n> Known frame interval to drop.
|
|
27
29
|
(default: n=0) auto-detects repeated frames (slower)
|
|
28
30
|
Ex.A: Use n=2 when every other frame is repeated.
|
|
@@ -40,7 +42,7 @@ async function main() {
|
|
|
40
42
|
})
|
|
41
43
|
|
|
42
44
|
if (values.help) {
|
|
43
|
-
console.log(
|
|
45
|
+
console.log(MAN)
|
|
44
46
|
process.exit(0)
|
|
45
47
|
}
|
|
46
48
|
|
package/src/edgespic.js
CHANGED
|
@@ -7,15 +7,17 @@ import { parseOptions } from './utils/parseOptions.js'
|
|
|
7
7
|
import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const MAN = `
|
|
11
|
+
SYNOPSIS
|
|
12
|
+
mediasnacks edgespic [--width=<num>] <files>
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
DESCRIPTION
|
|
15
|
+
Extracts the first and last frames from each video and saves them to the 'edgepics/' subfolder.
|
|
16
|
+
--width defaults to 640px and The aspect ratio is preserved.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
EXAMPLES
|
|
19
|
+
mediasnacks edgespic --width 800 *.mov
|
|
20
|
+
mediasnacks edgespic -w 600 'videos/**/*.mp4'
|
|
19
21
|
`.trim()
|
|
20
22
|
|
|
21
23
|
|
|
@@ -28,7 +30,7 @@ async function main() {
|
|
|
28
30
|
})
|
|
29
31
|
|
|
30
32
|
if (values.help) {
|
|
31
|
-
console.log(
|
|
33
|
+
console.log(MAN)
|
|
32
34
|
process.exit(0)
|
|
33
35
|
}
|
|
34
36
|
|
package/src/edgespic.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { ok } from 'node:assert/strict'
|
|
1
2
|
import { join } from 'node:path'
|
|
2
|
-
import { ok, equal } from 'node:assert/strict'
|
|
3
3
|
import { describe, test } from 'node:test'
|
|
4
4
|
import { cpSync, readdirSync, } from 'node:fs'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { ssim } from './ssim.js'
|
|
7
|
+
import { cli, mkTempDir } from './utils/test-utils.js'
|
|
7
8
|
|
|
8
9
|
const rel = f => join(import.meta.dirname, f)
|
|
9
10
|
|
|
@@ -18,15 +19,18 @@ describe('edgespic', () => {
|
|
|
18
19
|
ok(files.length === 2, `Expected 2 PNG files, got ${files.length}`)
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
test('extracts first frame', () => {
|
|
22
|
+
test('extracts first frame', async () => {
|
|
23
|
+
const out = join(tmp, 'edgespic', '60fps_first.png')
|
|
22
24
|
const fixture = rel('fixtures/edgespic/60fps_first.png')
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const similarityScore = await ssim(out, fixture)
|
|
26
|
+
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
25
27
|
})
|
|
26
28
|
|
|
27
|
-
test('extracts last frame', () => {
|
|
29
|
+
test('extracts last frame', async () => {
|
|
30
|
+
const out = join(tmp, 'edgespic', '60fps_last.png')
|
|
28
31
|
const fixture = rel('fixtures/edgespic/60fps_last.png')
|
|
29
|
-
const
|
|
30
|
-
|
|
32
|
+
const similarityScore = await ssim(out, fixture)
|
|
33
|
+
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
31
34
|
})
|
|
32
35
|
})
|
|
36
|
+
|
|
Binary file
|
|
Binary file
|
package/src/fixtures/lenna.avif
CHANGED
|
Binary file
|
package/src/framediff.sh
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
|
5
|
+
/bin/cat << EOF
|
|
6
|
+
SYNOPSIS
|
|
7
|
+
mediasnacks framediff <video>
|
|
8
|
+
|
|
9
|
+
DESCRIPTION
|
|
10
|
+
Runs FFplay with a video filter for diffing adjacent frames. Useful for
|
|
11
|
+
finding duplicate frames, which will show up a a black frame.
|
|
12
|
+
|
|
13
|
+
TIPS
|
|
14
|
+
Hit [s] to step frame-by-frame.
|
|
15
|
+
|
|
16
|
+
SEE ALSO
|
|
17
|
+
mediasnacks detectdups, ffplay(1)
|
|
18
|
+
EOF
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
6
21
|
|
|
7
22
|
ffplay -v error "$1" -vf "
|
|
8
23
|
tblend=all_mode=difference,
|
package/src/hev1tohvc1.js
CHANGED
|
@@ -5,12 +5,14 @@ import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
|
|
|
5
5
|
import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const MAN = `
|
|
9
|
+
SYNOPSIS
|
|
10
|
+
mediasnacks hev1tohvc1 <videos>
|
|
11
|
+
|
|
12
|
+
DESCRIPTION
|
|
13
|
+
This program fixes video thumbnails not rendering in macOS
|
|
14
|
+
Finder, and fixes video not importable in Final Cut Pro. That’s done
|
|
15
|
+
by changing the container’s sample entry code from HEV1 to HVC1.
|
|
14
16
|
`.trim()
|
|
15
17
|
|
|
16
18
|
|
|
@@ -20,7 +22,7 @@ async function main() {
|
|
|
20
22
|
const { files } = await parseOptions()
|
|
21
23
|
|
|
22
24
|
if (!files.length)
|
|
23
|
-
throw new Error(
|
|
25
|
+
throw new Error(MAN)
|
|
24
26
|
|
|
25
27
|
console.log('HEV1 to HVC1…')
|
|
26
28
|
for (const file of files)
|
package/src/moov2front.js
CHANGED
|
@@ -5,12 +5,15 @@ import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
|
|
|
5
5
|
import { parseOptions } from './utils/parseOptions.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const MAN = `
|
|
9
|
+
SYNOPSIS
|
|
10
|
+
mediasnacks moov2front <videos>
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
DESCRIPTION
|
|
13
|
+
Rearranges .mov and .mp4 metadata to the start of the file for fast-start streaming.
|
|
14
|
+
|
|
15
|
+
NOTES
|
|
16
|
+
Files are overwritten.
|
|
14
17
|
`.trim()
|
|
15
18
|
|
|
16
19
|
async function main() {
|
|
@@ -19,7 +22,7 @@ async function main() {
|
|
|
19
22
|
const { files } = await parseOptions()
|
|
20
23
|
|
|
21
24
|
if (!files.length)
|
|
22
|
-
throw new Error(
|
|
25
|
+
throw new Error(MAN)
|
|
23
26
|
|
|
24
27
|
console.log('Optimizing video for progressive download…')
|
|
25
28
|
for (const file of files)
|
package/src/prores.js
CHANGED
|
@@ -14,23 +14,22 @@ const PRORES_PROFILES = {
|
|
|
14
14
|
'4444xq': 5,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const MAN = `
|
|
18
|
+
SYNOPSIS
|
|
19
|
+
mediasnacks prores [options] <video>
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
DESCRIPTION
|
|
22
|
+
Converts a video to ProRes format.
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
<video> Video file to convert
|
|
24
|
-
|
|
25
|
-
Options:
|
|
24
|
+
OPTIONS
|
|
26
25
|
-p, --profile <n> ProRes profile (default: 3 (422 HQ))
|
|
27
26
|
-h, --help Show this help message
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
EXAMPLES
|
|
30
29
|
mediasnacks prores video.mov
|
|
31
30
|
mediasnacks prores --profile 2 video.mov
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
Both output a file named: video.prores.mov
|
|
34
33
|
`.trim()
|
|
35
34
|
|
|
36
35
|
|
|
@@ -43,7 +42,7 @@ async function main() {
|
|
|
43
42
|
})
|
|
44
43
|
|
|
45
44
|
if (values.help) {
|
|
46
|
-
console.log(
|
|
45
|
+
console.log(MAN)
|
|
47
46
|
process.exit(0)
|
|
48
47
|
}
|
|
49
48
|
|
package/src/qdir.js
CHANGED
|
@@ -8,10 +8,12 @@ import { readdir, writeFile, unlink, rename } from 'node:fs/promises'
|
|
|
8
8
|
import { isFile } from './utils/fs-utils.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const MAN = `
|
|
12
|
+
SYNOPSIS
|
|
13
|
+
mediasnacks qdir [folder]
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
DESCRIPTION
|
|
16
|
+
Sequentially runs all *.sh files in a folder (cwd by default).
|
|
15
17
|
`.trim()
|
|
16
18
|
|
|
17
19
|
|
|
@@ -24,7 +26,7 @@ async function main() {
|
|
|
24
26
|
})
|
|
25
27
|
|
|
26
28
|
if (values.help) {
|
|
27
|
-
console.log(
|
|
29
|
+
console.log(MAN)
|
|
28
30
|
process.exit(0)
|
|
29
31
|
}
|
|
30
32
|
|
package/src/resize.js
CHANGED
|
@@ -8,19 +8,21 @@ import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
|
8
8
|
import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
const MAN = `
|
|
12
|
+
SYNOPSIS
|
|
13
|
+
mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--output-dir=<dir>] <files>
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
DESCRIPTION
|
|
16
|
+
Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Example: Overwrites the input file (-y)
|
|
18
|
+
EXAMPLES
|
|
19
|
+
Overwrites the input file (-y)
|
|
18
20
|
mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
Output directory (-o)
|
|
21
23
|
mediasnacks resize --height 240 --output-dir /tmp/out video.mov
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
OPTIONS
|
|
24
26
|
--width and --height are -2 by default:
|
|
25
27
|
-1 = auto-compute while preserving the aspect ratio (may result in an odd number)
|
|
26
28
|
-2 = same as -1 but rounds to the nearest even number
|
|
@@ -39,7 +41,7 @@ async function main() {
|
|
|
39
41
|
})
|
|
40
42
|
|
|
41
43
|
if (values.help) {
|
|
42
|
-
console.log(
|
|
44
|
+
console.log(MAN)
|
|
43
45
|
process.exit(0)
|
|
44
46
|
}
|
|
45
47
|
|
package/src/seqcheck.js
CHANGED
|
@@ -4,12 +4,14 @@ import { parseArgs } from 'node:util'
|
|
|
4
4
|
import { readdirSync } from 'node:fs'
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const MAN = `
|
|
8
|
+
SYNOPSIS
|
|
9
|
+
mediasnacks seqcheck [options] [folder]
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
DESCRIPTION
|
|
12
|
+
Find missing numbered files in a sequence.
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
OPTIONS
|
|
13
15
|
-ld, --left-delimiter <str> Delimiter before the number (default: "_")
|
|
14
16
|
-rd, --right-delimiter <str> Delimiter after the number (default: ".")
|
|
15
17
|
-h, --help
|
|
@@ -27,7 +29,7 @@ function main() {
|
|
|
27
29
|
})
|
|
28
30
|
|
|
29
31
|
if (values.help) {
|
|
30
|
-
console.log(
|
|
32
|
+
console.log(MAN)
|
|
31
33
|
process.exit(0)
|
|
32
34
|
}
|
|
33
35
|
|
package/src/sqcrop.js
CHANGED
|
@@ -8,10 +8,12 @@ import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
|
8
8
|
import { parseOptions } from './utils/parseOptions.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const MAN = `
|
|
12
|
+
SYNOPSIS
|
|
13
|
+
mediasnacks sqcrop [-y | --overwrite] [--output-dir=<dir>] <images>
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
DESCRIPTION
|
|
16
|
+
Square crops images
|
|
15
17
|
`.trim()
|
|
16
18
|
|
|
17
19
|
|
|
@@ -25,7 +27,7 @@ async function main() {
|
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
if (values.help) {
|
|
28
|
-
console.log(
|
|
30
|
+
console.log(MAN)
|
|
29
31
|
process.exit(0)
|
|
30
32
|
}
|
|
31
33
|
|
package/src/ssim.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const MAN = `
|
|
7
|
+
SYNOPSIS
|
|
8
|
+
mediasnacks ssim <img1> <img2>
|
|
9
|
+
|
|
10
|
+
DESCRIPTION
|
|
11
|
+
Computes the Structural Similarity Index (SSIM) between two images using ffmpeg.
|
|
12
|
+
`.trim()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const [img1, img2] = process.argv.slice(2)
|
|
17
|
+
if (!img1 || !img2) {
|
|
18
|
+
console.log(MAN)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const score = await ssim(img1, img2)
|
|
23
|
+
console.log(score)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function ssim(img1, img2) {
|
|
27
|
+
const result = await ffmpeg([
|
|
28
|
+
'-i', img1,
|
|
29
|
+
'-i', img2,
|
|
30
|
+
'-filter_complex', 'ssim',
|
|
31
|
+
'-f', 'null', '-'
|
|
32
|
+
])
|
|
33
|
+
const match = result.stderr.match(/All:([\d.]+)/)
|
|
34
|
+
if (!match)
|
|
35
|
+
throw new Error(`Could not parse SSIM output:\n${result.stderr}`)
|
|
36
|
+
return parseFloat(match[1])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if (import.meta.main)
|
|
41
|
+
main().catch(err => {
|
|
42
|
+
console.error(err.message)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
})
|
package/src/utils/ffmpeg.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
export async function ffmpeg(args) {
|
|
5
|
-
return runSilently('ffmpeg', args)
|
|
6
|
-
}
|
|
7
|
-
|
|
8
4
|
export async function assertUserHasFFmpeg() {
|
|
9
5
|
try {
|
|
10
6
|
await runSilently('ffmpeg', ['-version'])
|
|
@@ -15,6 +11,9 @@ export async function assertUserHasFFmpeg() {
|
|
|
15
11
|
}
|
|
16
12
|
}
|
|
17
13
|
|
|
14
|
+
export async function ffmpeg(args) {
|
|
15
|
+
return runSilently('ffmpeg', args)
|
|
16
|
+
}
|
|
18
17
|
|
|
19
18
|
async function runSilently(program, args) {
|
|
20
19
|
return new Promise((resolve, reject) => {
|
package/src/utils/test-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { join }
|
|
2
|
-
import { tmpdir }
|
|
3
|
-
import { spawnSync }
|
|
4
|
-
import { createHash }
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
5
|
import { mkdtempSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
6
6
|
|
|
7
7
|
const rel = f => join(import.meta.dirname, f)
|
package/src/vconcat.sh
CHANGED
package/src/vsplit.js
CHANGED
|
@@ -9,16 +9,18 @@ import { assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
|
|
|
9
9
|
|
|
10
10
|
// TODO looks like it's missing a frame (perhaps becaue of -c copy)
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const MAN = `
|
|
13
|
+
SYNOPSIS
|
|
14
|
+
mediasnacks vsplit <csv> <video>
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
DESCRIPTION
|
|
17
|
+
Splits a video into multiple clips from CSV timestamps.
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
ARGUMENTS
|
|
18
20
|
<csv> CSV file with start,end columns (in seconds)
|
|
19
21
|
<video> Video file to split
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
EXAMPLE
|
|
22
24
|
mediasnacks vsplit clips.csv video.mov
|
|
23
25
|
|
|
24
26
|
Given clips.csv:
|
|
@@ -39,7 +41,7 @@ async function main() {
|
|
|
39
41
|
})
|
|
40
42
|
|
|
41
43
|
if (values.help) {
|
|
42
|
-
console.log(
|
|
44
|
+
console.log(MAN)
|
|
43
45
|
process.exit(0)
|
|
44
46
|
}
|
|
45
47
|
|
package/src/vtrim.js
CHANGED
|
@@ -5,12 +5,14 @@ import { parseOptions } from './utils/parseOptions.js'
|
|
|
5
5
|
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const MAN = `
|
|
9
|
+
SYNOPSIS
|
|
10
|
+
mediasnacks vtrim [--start <time>] [--end <time>] <video>
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
DESCRIPTION
|
|
13
|
+
Trims a video without re-encoding (fast, but approximate cuts).
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
OPTIONS
|
|
14
16
|
-s, --start <time> Start time (e.g. 10, 00:00:10, 1:23.5). Default: beginning.
|
|
15
17
|
-e, --end <time> End time (e.g. 30, 00:00:30, 2:45.0). Default: end of video.
|
|
16
18
|
-h, --help
|
|
@@ -27,7 +29,7 @@ async function main() {
|
|
|
27
29
|
})
|
|
28
30
|
|
|
29
31
|
if (values.help) {
|
|
30
|
-
console.log(
|
|
32
|
+
console.log(MAN)
|
|
31
33
|
process.exit(0)
|
|
32
34
|
}
|
|
33
35
|
|