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.
@@ -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
@@ -31,6 +31,7 @@ mediasnacks <command> <args>
31
31
 
32
32
  - `resize` Resizes videos or images
33
33
  - `edgespic` Extracts first and last frames
34
+ - `ssim` Computes similarity of two images
34
35
  - `gif`: Video to GIF
35
36
 
36
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
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 USAGE = `
11
- Usage: mediasnacks avif [-y | --overwrite] [--output-dir=<dir>] <images>
10
+ const MAN = `
11
+ SYNOPSIS
12
+ mediasnacks avif [-y | --overwrite] [--output-dir=<dir>] <images>
12
13
 
13
- Converts images to AVIF.
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(USAGE)
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 { deepEqual } from 'node:assert/strict'
3
+ import { ok } from 'node:assert/strict'
4
4
 
5
- import { videoAttrs } from './utils/ffmpeg.js'
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
- deepEqual(
15
- await videoAttrs(join(tmp, 'lenna.avif')),
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 USAGE = `
42
- Usage: mediasnacks <command> <args>
42
+ const MAN = `
43
+ SYNOPSIS
44
+ mediasnacks <command> <args>
43
45
 
44
- Commands:
46
+ COMMANDS
45
47
  ${Object.entries(COMMANDS).map(([cmd, [, title]]) =>
46
- ` ${styleText('bold', cmd.padEnd(12, ' '))}\t${title}`).join('\n')}
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(USAGE)
59
+ console.log(MAN)
58
60
  process.exit(0)
59
61
  }
60
62
 
61
63
  if (!opt) {
62
- console.log(USAGE)
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 USAGE = `
7
+ const MAN = `
8
8
  Usage: mediasnacks detectdups [options] <video>
9
9
 
10
- Detects sequentially duplicate frames in a video and prints their frame numbers.
10
+ Detects sequentially duplicate frames in a video and prints a histogram of their distance.
11
11
 
12
- Options:
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(USAGE)
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('Input file must be a video.')
50
+ throw new Error('Invalid input file. Must be a video.')
41
51
 
42
52
  const vDur = Number(v.duration)
43
- const seek = Number(values.seek) || (vDur > 60 ? 20 : 0)
44
- const duration = Number(values.duration) || (vDur > 60 ? 20 : vDur)
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 dupFrames = await detectDuplicateFramesNums(files[0], seek, duration)
57
- analyze(dupFrames, seek, duration)
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
- start: seek + 's',
97
- end: (seek + duration) + 's',
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 USAGE = `
21
- Usage: mediasnacks dropdups [-n <bad-frame-number>] <video>
20
+ const MAN = `
21
+ SYNOPSIS
22
+ mediasnacks dropdups [-n <bad-frame-number>] <video>
22
23
 
23
- Removes sequentially duplicate frames and outputs ProRes 422 HQ.
24
+ DESCRIPTION
25
+ Removes sequentially duplicate frames and outputs ProRes 422 HQ.
24
26
 
25
- Options:
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(USAGE)
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 USAGE = `
11
- Usage: mediasnacks edgespic [--width=<num>] <files>
10
+ const MAN = `
11
+ SYNOPSIS
12
+ mediasnacks edgespic [--width=<num>] <files>
12
13
 
13
- Extracts the first and last frames from each video and saves them to the 'edgepics/' subfolder.
14
- --width defaults to 640px and The aspect ratio is preserved.
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
- Example:
17
- mediasnacks edgespic --width 800 *.mov
18
- mediasnacks edgespic -w 600 'videos/**/*.mp4'
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(USAGE)
33
+ console.log(MAN)
32
34
  process.exit(0)
33
35
  }
34
36
 
@@ -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 { cli, mkTempDir, sha1 } from './utils/test-utils.js'
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 generated = join(tmp, 'edgespic', '60fps_first.png')
24
- equal(sha1(generated), sha1(fixture))
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 generated = join(tmp, 'edgespic', '60fps_last.png')
30
- equal(sha1(generated), sha1(fixture))
32
+ const similarityScore = await ssim(out, fixture)
33
+ ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
31
34
  })
32
35
  })
36
+
Binary file
package/src/framediff.sh CHANGED
@@ -1,8 +1,23 @@
1
1
  #!/bin/sh
2
2
 
3
- # Plays a video with a filter for diffing adjacent frames.
4
- # I use this for finding repeated frames. For example, you’ll see
5
- # a black frame if two consecutive frames are almost similar.
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 USAGE = `
9
- Usage: mediasnacks hev1tohvc1 <videos>
10
-
11
- This program fixes video thumbnails not rendering in macOS
12
- Finder, and fixes video not importable in Final Cut Pro. That’s done
13
- by changing the container’s sample entry code from HEV1 to HVC1.
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(USAGE)
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 USAGE = `
9
- Usage: mediasnacks moov2front <videos>
8
+ const MAN = `
9
+ SYNOPSIS
10
+ mediasnacks moov2front <videos>
10
11
 
11
- Rearranges .mov and .mp4 metadata to the start of the file for fast-start streaming.
12
-
13
- Files are overwritten.
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(USAGE)
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 USAGE = `
18
- Usage: mediasnacks prores [options] <video>
17
+ const MAN = `
18
+ SYNOPSIS
19
+ mediasnacks prores [options] <video>
19
20
 
20
- Converts a video to ProRes format.
21
+ DESCRIPTION
22
+ Converts a video to ProRes format.
21
23
 
22
- Arguments:
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
- Example:
28
+ EXAMPLES
30
29
  mediasnacks prores video.mov
31
30
  mediasnacks prores --profile 2 video.mov
32
31
 
33
- Outputs: video.prores.mov
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(USAGE)
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 USAGE = `
12
- Usage: mediasnacks qdir [folder]
11
+ const MAN = `
12
+ SYNOPSIS
13
+ mediasnacks qdir [folder]
13
14
 
14
- Sequentially runs all *.sh files in a folder (cwd by default).
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(USAGE)
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
- const USAGE = `
13
- Usage: mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--output-dir=<dir>] <files>
15
+ DESCRIPTION
16
+ Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
14
17
 
15
- Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
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
- Example: Output directory (-o)
22
+ Output directory (-o)
21
23
  mediasnacks resize --height 240 --output-dir /tmp/out video.mov
22
24
 
23
- Details:
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(USAGE)
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 USAGE = `
8
- Usage: mediasnacks seqcheck [options] [folder]
7
+ const MAN = `
8
+ SYNOPSIS
9
+ mediasnacks seqcheck [options] [folder]
9
10
 
10
- Find missing numbered files in a sequence.
11
+ DESCRIPTION
12
+ Find missing numbered files in a sequence.
11
13
 
12
- Options:
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(USAGE)
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 USAGE = `
12
- Usage: mediasnacks sqcrop [-y | --overwrite] [--output-dir=<dir>] <images>
11
+ const MAN = `
12
+ SYNOPSIS
13
+ mediasnacks sqcrop [-y | --overwrite] [--output-dir=<dir>] <images>
13
14
 
14
- Square crops images
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(USAGE)
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
+ })
@@ -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) => {
@@ -1,7 +1,7 @@
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'
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
@@ -2,9 +2,9 @@
2
2
 
3
3
  if [ "$#" -lt 2 ]; then
4
4
  cat << EOF
5
- Usage:
6
- $(basename "$0") vid1.mov vid2.mov [...]
7
- $(basename "$0") *.mp4
5
+ EXAMPLES
6
+ $(basename "$0") vid1.mov vid2.mov [...]
7
+ $(basename "$0") *.mp4
8
8
  EOF
9
9
  exit 1
10
10
  fi
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 USAGE = `
13
- Usage: mediasnacks vsplit <csv> <video>
12
+ const MAN = `
13
+ SYNOPSIS
14
+ mediasnacks vsplit <csv> <video>
14
15
 
15
- Splits a video into multiple clips from CSV timestamps.
16
+ DESCRIPTION
17
+ Splits a video into multiple clips from CSV timestamps.
16
18
 
17
- Arguments:
19
+ ARGUMENTS
18
20
  <csv> CSV file with start,end columns (in seconds)
19
21
  <video> Video file to split
20
22
 
21
- Example:
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(USAGE)
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 USAGE = `
9
- Usage: mediasnacks vtrim [--start <time>] [--end <time>] <video>
8
+ const MAN = `
9
+ SYNOPSIS
10
+ mediasnacks vtrim [--start <time>] [--end <time>] <video>
10
11
 
11
- Trims a video without re-encoding (fast, but approximate cuts).
12
+ DESCRIPTION
13
+ Trims a video without re-encoding (fast, but approximate cuts).
12
14
 
13
- Options:
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(USAGE)
32
+ console.log(MAN)
31
33
  process.exit(0)
32
34
  }
33
35