mediasnacks 0.22.1 → 0.22.3

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/dropdups.js +18 -13
  3. package/src/edgespic.js +4 -2
  4. package/src/flattendir.sh +20 -3
  5. package/src/framediff.sh +17 -3
  6. package/src/moov2front.js +1 -1
  7. package/src/play.js +24 -25
  8. package/src/prores.js +2 -4
  9. package/src/random.js +1 -1
  10. package/src/resize.js +0 -1
  11. package/src/seqcheck.js +0 -1
  12. package/src/ssim.js +0 -1
  13. package/src/utils/fs-utils.js +11 -1
  14. package/src/vconcat.sh +27 -3
  15. package/src/vdiff.sh +22 -3
  16. package/src/avif.test.js +0 -16
  17. package/src/detectdups.test.js +0 -24
  18. package/src/edgespic.test.js +0 -36
  19. package/src/fixtures/60fps.csv +0 -7
  20. package/src/fixtures/60fps.mp4 +0 -0
  21. package/src/fixtures/big-buck-bunny/bbb_24_to_25fps_dup.mp4 +0 -0
  22. package/src/fixtures/big-buck-bunny/bbb_24_to_30fps_dup.mp4 +0 -0
  23. package/src/fixtures/big-buck-bunny/bbb_24_to_48fps_dup.mp4 +0 -0
  24. package/src/fixtures/big-buck-bunny/bbb_24fps_no_dups.mp4 +0 -0
  25. package/src/fixtures/big-buck-bunny/bbb_25_to_30fps_dup.mp4 +0 -0
  26. package/src/fixtures/big-buck-bunny/bbb_25_to_50fps_dup.mp4 +0 -0
  27. package/src/fixtures/big-buck-bunny/bbb_25_to_60fps_dup.mp4 +0 -0
  28. package/src/fixtures/big-buck-bunny/bbb_25fps_no_dups.mp4 +0 -0
  29. package/src/fixtures/big-buck-bunny/generate.md +0 -71
  30. package/src/fixtures/edgespic/60fps_first.png +0 -0
  31. package/src/fixtures/edgespic/60fps_last.png +0 -0
  32. package/src/fixtures/lenna.avif +0 -0
  33. package/src/fixtures/lenna.png +0 -0
  34. package/src/fixtures/qdir-jobs/job1_good.sh +0 -1
  35. package/src/fixtures/qdir-jobs/job2_bad.sh +0 -1
  36. package/src/fixtures/qdir-jobs/job3_good.sh +0 -2
  37. package/src/fixtures/qdir-jobs/job4_bad.sh +0 -1
  38. package/src/flattendir.test.js +0 -36
  39. package/src/qdir.test.js +0 -24
  40. package/src/seqcheck.test.js +0 -27
  41. package/src/utils/fs-utils.test.js +0 -21
  42. package/src/utils/parseOptions.test.js +0 -60
  43. package/src/vconcat.test.js +0 -23
  44. package/src/vsplit.test.js +0 -32
  45. package/src/vtrim.test.js +0 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.22.1",
3
+ "version": "0.22.3",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
package/src/dropdups.js CHANGED
@@ -11,17 +11,22 @@ const PROFILE = PRORES_PROFILES.hq
11
11
 
12
12
  const HELP = `
13
13
  SYNOPSIS
14
- mediasnacks dropdups [-n <bad-frame-number>] <video>
14
+ mediasnacks dropdups [-n <dup-frame-num>] <video>
15
15
 
16
16
  DESCRIPTION
17
17
  Removes sequentially duplicate frames and outputs ProRes 422 HQ.
18
18
 
19
19
  OPTIONS
20
- -n, --bad-frame-number <n> Known frame interval to drop.
21
- (default: n=0) auto-detects repeated frames (slower)
22
- Ex.A: Use n=2 when every other frame is repeated.
23
- Ex.B: Use n=6 if e.g., a 25 fps got upped to 30 fps without interpolation.
20
+ -n, --dup-frame-num <n> Known frame interval to drop.
21
+ Default: n=0, which auto-detects repeated frames (slower)
24
22
  -h, --help
23
+
24
+ EXAMPLES
25
+ Use n=2 when every other frame is repeated:
26
+ mediasnacks dropdups -n2 vid.mov
27
+
28
+ Use n=6 if e.g., a 25 fps got upped to 30 fps without interpolation.
29
+ mediasnacks dropdups -n6 vid.mov
25
30
  `.trim()
26
31
 
27
32
 
@@ -29,7 +34,7 @@ async function main() {
29
34
  await assertUserHasFFmpeg()
30
35
 
31
36
  const { values, files } = await parseOptions({
32
- 'bad-frame-number': { short: 'n', type: 'string', default: '' },
37
+ 'dup-frame-num': { short: 'n', type: 'string', default: '' },
33
38
  help: { short: 'h', type: 'boolean' },
34
39
  })
35
40
 
@@ -41,23 +46,23 @@ async function main() {
41
46
  if (!files.length)
42
47
  throw new Error('No video specified. See mediasnacks dropdups --help')
43
48
 
44
- let nBadFrame = values['bad-frame-number']
45
- if (nBadFrame && !/^\d+$/.test(nBadFrame))
46
- throw new Error('Invalid --bad-frame-number. It must be a positive integer.')
49
+ let dupFrameNum = values['dup-frame-num']
50
+ if (dupFrameNum && !Number.isInteger(+dupFrameNum))
51
+ throw new Error('Invalid -n. It must be a positive integer.')
47
52
 
48
53
  console.log('Dropping Duplicate Frames…')
49
54
  for (const file of files)
50
- await dropdups(resolve(file), nBadFrame)
55
+ await dropdups(resolve(file), dupFrameNum)
51
56
  }
52
57
 
53
- async function dropdups(video, nBadFrame) {
58
+ async function dropdups(video, dupFrameNum) {
54
59
  await run('ffmpeg', [
55
60
  '-v', 'error',
56
61
  '-stats',
57
62
  '-an',
58
63
  '-i', video,
59
- '-vf', nBadFrame
60
- ? `decimate=cycle=${nBadFrame}`
64
+ '-vf', dupFrameNum
65
+ ? `decimate=cycle=${dupFrameNum}`
61
66
  : 'mpdecimate,setpts=N/FRAME_RATE/TB',
62
67
  '-fps_mode', 'cfr',
63
68
  '-c:v', 'prores_ks',
package/src/edgespic.js CHANGED
@@ -13,10 +13,12 @@ SYNOPSIS
13
13
 
14
14
  DESCRIPTION
15
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.
16
+
17
+ OPTIONS
18
+ -w, --width Default:640 The aspect ratio is preserved.
17
19
 
18
20
  EXAMPLES
19
- mediasnacks edgespic --width 800 *.mov
21
+ mediasnacks edgespic -w 800 *.mov
20
22
  mediasnacks edgespic -w 600 'videos/**/*.mp4'
21
23
  `.trim()
22
24
 
package/src/flattendir.sh CHANGED
@@ -1,8 +1,25 @@
1
1
  #!/bin/sh
2
2
 
3
- # Moves unique files to the top dir and deletes empty dirs
4
- # Usage: mediasnacks flattendir [folder]
5
- # Default: current working directory
3
+ help() {
4
+ /bin/cat << EOF
5
+ SYNOPSIS
6
+ mediasnacks flattendir [folder]
7
+
8
+ DESCRIPTION
9
+ Moves unique files from subdirectories into the top-level folder, then
10
+ deletes empty directories. Defaults to the current working directory.
11
+ EOF
12
+ }
13
+
14
+ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
15
+ help
16
+ exit 0
17
+ fi
18
+
19
+ if [ $# -gt 0 ] && [ ! -d "$1" ]; then
20
+ help
21
+ exit 1
22
+ fi
6
23
 
7
24
  DIR="${1:-$(pwd)}"
8
25
 
package/src/framediff.sh CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/bin/sh
2
2
 
3
3
 
4
- if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
5
- /bin/cat << EOF
4
+ help() {
5
+ /bin/cat << EOF
6
6
  SYNOPSIS
7
7
  mediasnacks framediff <video>
8
8
 
9
9
  DESCRIPTION
10
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.
11
+ finding duplicate frames, which will show up as a black frame.
12
12
 
13
13
  TIPS
14
14
  Hit [s] to step frame-by-frame.
@@ -16,10 +16,24 @@ TIPS
16
16
  SEE ALSO
17
17
  mediasnacks detectdups, ffplay(1)
18
18
  EOF
19
+ }
20
+
21
+ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
22
+ help
19
23
  exit 0
20
24
  fi
21
25
 
26
+ if [ ! -f "$1" ]; then
27
+ help
28
+ exit 1
29
+ fi
30
+
22
31
  ffplay -v error "$1" -vf "
23
32
  tblend=all_mode=difference,
24
33
  format=gray
25
34
  "
35
+
36
+ # Not rendering the frame number:
37
+ # 'drawtext=text=%{n}:x=20:y=20:fontcolor=white:fontsize=48'
38
+ # …because in homebrew ffmpeg 8 is not compiled with the req libs:
39
+ # https://ayosec.github.io/ffmpeg-filters-docs/8.0/Filters/Video/drawtext.html
package/src/moov2front.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
4
3
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
4
  import { parseOptions } from './utils/parseOptions.js'
@@ -20,6 +19,7 @@ NOTES
20
19
  Files are overwritten.
21
20
  `.trim()
22
21
 
22
+
23
23
  async function main() {
24
24
  await assertUserHasFFmpeg()
25
25
 
package/src/play.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { join } from 'node:path'
3
2
  import { spawn } from 'node:child_process'
4
- import { readdirSync } from 'node:fs'
5
3
  import { parseOptions } from './utils/parseOptions.js'
4
+ import { findFiles } from './utils/fs-utils.js'
6
5
 
7
6
 
8
7
  const HELP = `
9
8
  SYNOPSIS
10
- mediasnacks play [--no-recursive] [-h | --help] [query ...]
9
+ mediasnacks play [--no-recursive] [query ...]
11
10
 
12
11
  DESCRIPTION
13
12
  Plays a filtered playlist with mpv.
@@ -29,33 +28,33 @@ async function main() {
29
28
  process.exit(0)
30
29
  }
31
30
 
32
- const pattern = positionals.length
33
- ? positionals.join('|')
34
- : ''
35
- const files = findFiles('.', new RegExp(pattern, 'i'), values.recursive)
31
+ const files = findFiles({
32
+ dir: '.',
33
+ regex: new RegExp(positionals.join('|'), 'i'),
34
+ recursive: values.recursive,
35
+ ignoredDirs: ['.fcpbundle/']
36
+ })
36
37
 
37
- if (!files.length) {
38
- console.error('No matching files found.')
39
- process.exit(0)
40
- }
38
+ if (!files.length)
39
+ throw new Error('No matching files found.')
40
+
41
+ play(files)
42
+ }
41
43
 
42
- const child = spawn('mpv', ['--playlist=-'], {
44
+ function play(files) {
45
+ const mpv = spawn('mpv', ['--playlist=-'], {
43
46
  detached: true,
44
47
  stdio: ['pipe', 'ignore', 'ignore']
45
48
  })
46
- child.stdin.end(files.join('\n'))
47
- child.unref()
48
- }
49
-
50
- function findFiles(dir, regex, recursive = true) {
51
- const IGNORED_DIRS = ['.fcpbundle/']
52
- return readdirSync(dir, { withFileTypes: true, recursive })
53
- .filter(entry =>
54
- entry.isFile()
55
- && !entry.name.startsWith('.')
56
- && !IGNORED_DIRS.some(d => entry.parentPath.includes(d))
57
- && regex.test(entry.name))
58
- .map(entry => join(entry.parentPath, entry.name))
49
+ mpv.stdin.end(files.join('\n'))
50
+ mpv.unref()
51
+
52
+ mpv.on('error', function (err) {
53
+ if (err.code === 'ENOENT')
54
+ console.error('Error: MPV is not installed')
55
+ else
56
+ console.log(err)
57
+ })
59
58
  }
60
59
 
61
60
  main().catch(err => {
package/src/prores.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { resolve, parse, join } from 'node:path'
4
-
5
3
  import { parseOptions } from './utils/parseOptions.js'
6
4
  import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
7
5
 
@@ -23,7 +21,7 @@ DESCRIPTION
23
21
 
24
22
  OPTIONS
25
23
  -p, --profile <n> ProRes profile (default: 3 (422 HQ))
26
- -h, --help Show this help message
24
+ -h, --help
27
25
 
28
26
  EXAMPLES
29
27
  mediasnacks prores video.mov
@@ -37,8 +35,8 @@ async function main() {
37
35
  await assertUserHasFFmpeg()
38
36
 
39
37
  const { values, files } = await parseOptions({
40
- help: { short: 'h', type: 'boolean' },
41
38
  profile: { short: 'p', type: 'string', default: String(PRORES_PROFILES.hq) },
39
+ help: { short: 'h', type: 'boolean' },
42
40
  })
43
41
 
44
42
  if (values.help) {
package/src/random.js CHANGED
@@ -7,7 +7,7 @@ import { parseOptions } from './utils/parseOptions.js'
7
7
 
8
8
  const HELP = `
9
9
  SYNOPSIS
10
- mediasnacks random [-r | --recursive] [-h | --help]
10
+ mediasnacks random [-r | --recursive]
11
11
 
12
12
  DESCRIPTION
13
13
  Opens a random file in the current working directory
package/src/resize.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { join } from 'node:path'
4
3
  import { rename } from 'node:fs/promises'
5
4
 
package/src/seqcheck.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { parseArgs } from 'node:util'
4
3
  import { readdirSync } from 'node:fs'
5
4
 
package/src/ssim.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { ffmpeg } from './utils/subprocess.js'
4
3
 
5
4
 
@@ -1,5 +1,5 @@
1
- import { lstatSync } from 'node:fs'
2
1
  import { randomUUID } from 'node:crypto'
2
+ import { lstatSync, readdirSync } from 'node:fs'
3
3
  import { mkdir, unlink, rename } from 'node:fs/promises'
4
4
  import { dirname, extname, join } from 'node:path'
5
5
 
@@ -33,3 +33,13 @@ export async function mkDir(path) {
33
33
  throw err
34
34
  }
35
35
  }
36
+
37
+ export function findFiles({ dir, regex, recursive, ignoredDirs }) {
38
+ return readdirSync(dir, { withFileTypes: true, recursive })
39
+ .filter(entry =>
40
+ entry.isFile()
41
+ && !entry.name.startsWith('.')
42
+ && !ignoredDirs.some(d => entry.parentPath.includes(d))
43
+ && regex.test(entry.name))
44
+ .map(entry => join(entry.parentPath, entry.name))
45
+ }
package/src/vconcat.sh CHANGED
@@ -1,14 +1,38 @@
1
1
  #!/bin/sh
2
2
 
3
- if [ "$#" -lt 2 ]; then
3
+
4
+ help() {
4
5
  cat << EOF
6
+ SYNOPSIS
7
+ mediasnacks vconcat <video1> <video2> …
8
+
9
+ DESCRIPTION
10
+ Concatenates video files using FFmpeg's without re-encoding.
11
+ All videos must have compatible codecs and resolutions.
12
+
5
13
  EXAMPLES
6
- $(basename "$0") vid1.mov vid2.mov [...]
7
- $(basename "$0") *.mp4
14
+ mediasnacks vconcat vid1.mov vid2.mov
15
+ mediasnacks vconcat *.mp4
8
16
  EOF
17
+ }
18
+
19
+ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
20
+ help
21
+ exit 0
22
+ fi
23
+
24
+ if [ "$#" -lt 2 ]; then
25
+ help
9
26
  exit 1
10
27
  fi
11
28
 
29
+ for arg in "$@"; do
30
+ if [ ! -f "$arg" ]; then
31
+ help
32
+ exit 1
33
+ fi
34
+ done
35
+
12
36
  list_file=$(mktemp -p .)
13
37
  for file in "$@"; do
14
38
  fname=$(printf '%s' "$file" | sed "s/'/'\\\\''/g") # Escape single quotes
package/src/vdiff.sh CHANGED
@@ -1,10 +1,29 @@
1
1
  #!/bin/sh
2
2
 
3
- # Diffs two video files
4
- # The videos must have the same resolution and ideally the same framerate.
3
+
4
+ help() {
5
+ /bin/cat << EOF
6
+ SYNOPSIS
7
+ mediasnacks vdiff <video1> <video2>
8
+
9
+ DESCRIPTION
10
+ Diffs two video files using FFplay with a blend filter. Videos must have
11
+ the same resolution and ideally the same framerate.
12
+ EOF
13
+ }
14
+
15
+ if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
16
+ help
17
+ exit 0
18
+ fi
19
+
20
+ if [ $# -lt 2 ] || [ ! -f "$1" ] || [ ! -f "$2" ]; then
21
+ help
22
+ exit 1
23
+ fi
24
+
5
25
  video1="$1"
6
26
  video2="$2"
7
-
8
27
  ffprobe -v error "$video1" || exit 1
9
28
  ffprobe -v error "$video2" || exit 1
10
29
 
package/src/avif.test.js DELETED
@@ -1,16 +0,0 @@
1
- import { join } from 'node:path'
2
- import { test } from 'node:test'
3
- import { ok } from 'node:assert/strict'
4
-
5
- import { ssim } from './ssim.js'
6
- import { mkTempDir, cli } from './utils/test-utils.js'
7
-
8
- const rel = f => join(import.meta.dirname, f)
9
-
10
- test('PNG to AVIF', async () => {
11
- const tmp = mkTempDir('avif')
12
- cli('avif', '--output-dir', tmp, rel('fixtures/lenna.png'))
13
-
14
- const similarityScore = await ssim(join(tmp, 'lenna.avif'), rel('fixtures/lenna.avif'))
15
- ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
16
- })
@@ -1,24 +0,0 @@
1
- import { test } from 'node:test'
2
- import { join } from 'node:path'
3
- import { equal } from 'node:assert/strict'
4
- import { cli } from './utils/test-utils.js'
5
-
6
- const rel = f => join(import.meta.dirname, f)
7
-
8
- function detect(video) {
9
- const { stdout } = cli('detectdups', rel(video))
10
- return JSON.parse(stdout).n
11
- }
12
-
13
- test('no dups', () => equal(detect('fixtures/big-buck-bunny/bbb_24fps_no_dups.mp4'), null))
14
-
15
- // These fixtures are badly retimed (non-interpolated, just duplicating a frame)
16
-
17
- test('24 to 48 (has dup at n=2)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_48fps_dup.mp4'), 2))
18
- test('25 to 50 (has dup at n=2)', () => equal(detect('fixtures/big-buck-bunny/bbb_25_to_50fps_dup.mp4'), 2))
19
-
20
- test('24 to 30 (has dup at n=5)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_30fps_dup.mp4'), 5))
21
- test('25 to 30 (has dup at n=6)', () => equal(detect('fixtures/big-buck-bunny/bbb_25_to_30fps_dup.mp4'), 6))
22
-
23
- test('24 to 25 (has dup at n=25)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_25fps_dup.mp4'), 25))
24
-
@@ -1,36 +0,0 @@
1
- import { ok } from 'node:assert/strict'
2
- import { join } from 'node:path'
3
- import { describe, test } from 'node:test'
4
- import { cpSync, readdirSync, } from 'node:fs'
5
-
6
- import { ssim } from './ssim.js'
7
- import { cli, mkTempDir } from './utils/test-utils.js'
8
-
9
- const rel = f => join(import.meta.dirname, f)
10
-
11
- describe('edgespic', () => {
12
- const tmp = mkTempDir('edgespic')
13
- const inputFile = join(tmp, '60fps.mp4')
14
- cpSync(rel('fixtures/60fps.mp4'), inputFile)
15
- cli('edgespic', inputFile)
16
-
17
- test('creates output directory', () => {
18
- const files = readdirSync(join(tmp, 'edgespic'))
19
- ok(files.length === 2, `Expected 2 PNG files, got ${files.length}`)
20
- })
21
-
22
- test('extracts first frame', async () => {
23
- const out = join(tmp, 'edgespic', '60fps_first.png')
24
- const fixture = rel('fixtures/edgespic/60fps_first.png')
25
- const similarityScore = await ssim(out, fixture)
26
- ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
27
- })
28
-
29
- test('extracts last frame', async () => {
30
- const out = join(tmp, 'edgespic', '60fps_last.png')
31
- const fixture = rel('fixtures/edgespic/60fps_last.png')
32
- const similarityScore = await ssim(out, fixture)
33
- ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
34
- })
35
- })
36
-
@@ -1,7 +0,0 @@
1
- start,end
2
- 0,5
3
- 5,10
4
- 10,15
5
- 15,20
6
- 20,25
7
- 25,30
Binary file
@@ -1,71 +0,0 @@
1
- # Generating Fixtures
2
-
3
-
4
- ## 1. Download a video (Big Buck Bunny)
5
-
6
- ```sh
7
- curl https://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_1080p_h264.mov -o bbb_full_24fps.mov
8
- ```
9
-
10
- ## 2. Extract a scene
11
- Using the third scene because it's complex enough to cover many edge cases.
12
- It has a bird flapping its wings with motion blur. Also, animated text titles,
13
- and fairly static frames after the title ends.
14
-
15
- The scene is 1080p, 24fps, 7.28sec, h.264.
16
- ```sh
17
- brew tap ericfortis/fcpscene
18
- brew install fcpscene
19
- fcpscene -m files bbb_full_24fps.mov
20
-
21
- cp bbb/bbb_full_24fps_003.mov ./bbb_24fps_no_dups.mov
22
- rm -rf bbb
23
- rm bbb_full_24fps.mov
24
- ```
25
-
26
- ## 3. Re-encode the scene 24fps
27
- This way all videos will share the same encoding, and no audio.
28
-
29
- ```sh
30
- ffmpeg -i bbb_24fps_no_dups.mov \
31
- -c:v libx264 -crf 18 -preset slow \
32
- -an \
33
- bbb_24fps_no_dups.mp4
34
- ```
35
-
36
- ## 4. Retime (no dups) speed stretch to 25fps
37
- For a good (no dups) 25fps, retiming by speeding it up.
38
- ```sh
39
- ffmpeg -i bbb_24fps_no_dups.mp4 \
40
- -vf "setpts=24/25*PTS" \
41
- -r 25 \
42
- bbb_25fps_no_dups.mp4
43
- ```
44
-
45
-
46
- ## 5. Retime by inserting duplicates (no interpolation)
47
- ```sh
48
- for TARGET_FPS in 48 30 25; do
49
- ffmpeg -i bbb_24fps_no_dups.mp4 \
50
- -vf fps=$TARGET_FPS \
51
- -c:v libx264 -crf 18 -preset slow \
52
- -an \
53
- "bbb_24_to_${TARGET_FPS}fps_dup.mp4"
54
- done
55
-
56
- for TARGET_FPS in 60 50 30; do
57
- ffmpeg -i bbb_25fps_no_dups.mp4 \
58
- -vf fps=$TARGET_FPS \
59
- -c:v libx264 -crf 18 -preset slow \
60
- -an \
61
- "bbb_25_to_${TARGET_FPS}fps_dup.mp4"
62
- done
63
- ```
64
- Counting the cycle from 1 (not from 0):
65
-
66
- - 24 to 48 (inserts dup at n=2) 0101
67
- - 25 to 50 (inserts dup at n=2) 0101
68
- - 24 to 30 (inserts dup at n=5) 0000100001
69
- - 25 to 30 (inserts dup at n=6) 000001000001
70
- - 24 to 25 (inserts dup at n=25) (0*24)1
71
- - 25 to 60 (inserts dup at n=2 and n=3) 01011
Binary file
Binary file
@@ -1 +0,0 @@
1
- exit 0
@@ -1 +0,0 @@
1
- exit 1
@@ -1,2 +0,0 @@
1
- sleep 0.5
2
- exit 0
@@ -1 +0,0 @@
1
- exit 1
@@ -1,36 +0,0 @@
1
- import { test } from 'node:test'
2
- import { deepEqual } from 'node:assert/strict'
3
- import { readdirSync } from 'node:fs'
4
-
5
- import { mkTempDir, cli, dir, touch } from './utils/test-utils.js'
6
-
7
- test('flattendir moves files to top level and deletes empty dirs', () => {
8
- const tmp = mkTempDir('flattendir')
9
- dir(tmp, 'dir1', 'dir1-1')
10
- dir(tmp, 'dir2')
11
- touch(tmp, 'file1.txt')
12
- touch(tmp, 'dir1', 'file2.txt')
13
- touch(tmp, 'dir1', 'dir1-1', 'file3.txt')
14
- touch(tmp, 'dir1', '.DS_Store')
15
-
16
- cli('flattendir', tmp)
17
- deepEqual(readdirSync(tmp).sort(), [
18
- 'file1.txt',
19
- 'file2.txt',
20
- 'file3.txt'
21
- ])
22
- })
23
-
24
- test('flattendir does not move files if filename collision occurs', () => {
25
- const tmp = mkTempDir('flattendir-collision')
26
- dir(tmp, 'dir1')
27
- touch(tmp, 'file1.txt')
28
- touch(tmp, 'dir1', 'file1.txt')
29
-
30
- cli('flattendir', tmp)
31
- deepEqual(readdirSync(tmp, { recursive: true }).sort(), [
32
- 'dir1',
33
- 'dir1/file1.txt',
34
- 'file1.txt'
35
- ])
36
- })
package/src/qdir.test.js DELETED
@@ -1,24 +0,0 @@
1
- import { test } from 'node:test'
2
- import { join } from 'node:path'
3
- import { equal, deepEqual } from 'node:assert/strict'
4
- import { cpSync, readdirSync } from 'node:fs'
5
-
6
- import { qdir } from './qdir.js'
7
- import { mkTempDir } from './utils/test-utils.js'
8
-
9
- const rel = f => join(import.meta.dirname, f)
10
-
11
- test('qdir-jobs get renamed and failed have their exit status code', async () => {
12
- const tmp = mkTempDir('qdir')
13
- cpSync(rel('fixtures/qdir-jobs'), tmp, { recursive: true })
14
-
15
- const err = await qdir(tmp, 0.2)
16
- equal(err, null)
17
-
18
- deepEqual(readdirSync(tmp).sort(), [
19
- 'job1_good.sh.done',
20
- 'job2_bad.sh.failed.1',
21
- 'job3_good.sh.done',
22
- 'job4_bad.sh.failed.1'
23
- ])
24
- })
@@ -1,27 +0,0 @@
1
- import { test } from 'node:test'
2
- import { deepEqual } from 'node:assert/strict'
3
- import { extractSeqNums, findMissingNumbers } from './seqcheck.js'
4
-
5
-
6
- test('extractSeqNums extracts sequence numbers from filenames', () => {
7
- const filenames = [
8
- 'video-111_001.mov',
9
- 'video-111_002.mov',
10
- 'video-111_004.mov',
11
- 'bad.mov',
12
- 'bad_too_a39.mov',
13
- ]
14
- deepEqual(extractSeqNums(filenames, '_', '.'), [1, 2, 4])
15
- })
16
-
17
-
18
- test('findMissingNumbers ', () => {
19
- test('finds gaps in a sequence', () =>
20
- deepEqual(findMissingNumbers([1, 2, 4, 5, 8]), [3, 6, 7]))
21
-
22
- test('returns empty array for empty input', () =>
23
- deepEqual(findMissingNumbers([]), []))
24
-
25
- test('returns empty array when there are no gaps', () =>
26
- deepEqual(findMissingNumbers([10, 11, 12]), []))
27
- })
@@ -1,21 +0,0 @@
1
- import { equal } from 'node:assert/strict'
2
- import test, { describe } from 'node:test'
3
- import { replaceExt } from './fs-utils.js'
4
-
5
-
6
- describe('replaceExt', () => {
7
- test('replaces a simple extension', () =>
8
- equal(replaceExt('file.txt', 'md'), 'file.md'))
9
-
10
- test('replaces a multi-part extension', () =>
11
- equal(replaceExt('archive.tar.gz', 'zip'), 'archive.tar.zip'))
12
-
13
- test('adds extension when none exists', () =>
14
- equal(replaceExt('README', 'md'), 'README.md'))
15
-
16
- test('handles empty filename', () =>
17
- equal(replaceExt('', 'ext'), '.ext'))
18
-
19
- test('handles dot-files', () =>
20
- equal(replaceExt('.env', 'txt'), '.env.txt'))
21
- })
@@ -1,60 +0,0 @@
1
- import { join } from 'node:path'
2
- import { tmpdir } from 'node:os'
3
- import { equal, deepEqual } from 'node:assert/strict'
4
- import { mkdtemp, writeFile, rm } from 'node:fs/promises'
5
- import { test, describe, before, after } from 'node:test'
6
-
7
- import { parseOptions } from './parseOptions.js'
8
-
9
-
10
- describe('parseOptions', () => {
11
- let testDir
12
- let inTmpDir = f => join(testDir, f)
13
- const testFiles = ['file1.png', 'file2.png', 'file3.png']
14
-
15
- before(async () => {
16
- testDir = await mkdtemp(join(tmpdir(), 'parse-args-'))
17
- for (const file of testFiles)
18
- await writeFile(inTmpDir(file), '')
19
- })
20
-
21
- after(() => rm(testDir, { recursive: true }))
22
-
23
- test('parses args and globs files', async () => {
24
- const { values, positionals, files } = await parseOptions({
25
- 'output-dir': { type: 'string' }
26
- }, {
27
- args: ['--output-dir', '/tmp', inTmpDir('file[12].png')],
28
- })
29
- equal(values['output-dir'], '/tmp')
30
- deepEqual(positionals, [inTmpDir('file[12].png')])
31
- deepEqual(files, [
32
- inTmpDir('file1.png'),
33
- inTmpDir('file2.png')
34
- ])
35
- })
36
-
37
- test('respects verbatim tokens', async () => {
38
- const literal0 = 'literal-file[98].png'
39
- const literal1 = 'literal-file[99].png'
40
- const { files } = await parseOptions({}, {
41
- args: [inTmpDir('file[12].png'), '--', literal0, literal1]
42
- })
43
- deepEqual(files, [
44
- inTmpDir('file1.png'),
45
- inTmpDir('file2.png'),
46
- literal0,
47
- literal1,
48
- ])
49
- })
50
-
51
- test('empty files array when no positionals', async () => {
52
- const { files, values } = await parseOptions({
53
- foo: { type: 'boolean' }
54
- }, {
55
- args: ['--foo'],
56
- })
57
- equal(values.foo, true)
58
- deepEqual(files, [])
59
- })
60
- })
@@ -1,23 +0,0 @@
1
- import { ok } from 'node:assert/strict'
2
- import { join } from 'node:path'
3
- import { test } from 'node:test'
4
- import { cpSync } from 'node:fs'
5
-
6
- import { videoAttrs } from './utils/subprocess.js'
7
- import { mkTempDir, cli } from './utils/test-utils.js'
8
-
9
- const rel = f => join(import.meta.dirname, f)
10
-
11
- test('vconcat concatenates videos with single quotes in filenames', async () => {
12
- const tmp = mkTempDir('vconcat')
13
-
14
- const file1 = join(tmp, `video'1.mp4`)
15
- const file2 = join(tmp, `video'2.mp4`)
16
- cpSync(rel('fixtures/60fps.mp4'), file1)
17
- cpSync(rel('fixtures/60fps.mp4'), file2)
18
-
19
- cli('vconcat', file1, file2)
20
-
21
- const { duration } = await videoAttrs(join(tmp, `video'1.concat.mp4`))
22
- ok(parseFloat(duration) === 60, `Duration should be 60s, got ${duration}s`)
23
- })
@@ -1,32 +0,0 @@
1
- import { ok } from 'node:assert/strict'
2
- import { join } from 'node:path'
3
- import { describe, test } from 'node:test'
4
- import { cpSync, readdirSync } from 'node:fs'
5
-
6
- import { videoAttrs } from './utils/subprocess.js'
7
- import { mkTempDir, cli } from './utils/test-utils.js'
8
-
9
-
10
- const rel = f => join(import.meta.dirname, f)
11
-
12
- describe('vsplit splits video into multiple clips from CSV', () => {
13
- const tmp = mkTempDir('vsplit')
14
-
15
- const csvFile = join(tmp, '60fps.csv')
16
- const inputFile = join(tmp, '60fps.mp4')
17
-
18
- cpSync(rel('fixtures/60fps.csv'), csvFile)
19
- cpSync(rel('fixtures/60fps.mp4'), inputFile)
20
- cli('vsplit', csvFile, inputFile)
21
-
22
- test('all 6 clips were created', () => {
23
- const files = readdirSync(tmp).filter(f => f.startsWith('60fps_'))
24
- ok(files.length === 6, `Expected 6 clips, got ${files.length}`)
25
- })
26
-
27
- test('first clip has correct duration (5 seconds)', async () => {
28
- const { duration } = await videoAttrs(join(tmp, '60fps_1.mp4'))
29
- const EPSILON = 0.05
30
- ok(Math.abs(parseFloat(duration) - 5) < EPSILON, `Duration should be 5s, got ${duration}s`)
31
- })
32
- })
package/src/vtrim.test.js DELETED
@@ -1,40 +0,0 @@
1
- import { ok } from 'node:assert/strict'
2
- import { join } from 'node:path'
3
- import { cpSync } from 'node:fs'
4
- import { describe, test } from 'node:test'
5
-
6
- import { videoAttrs } from './utils/subprocess.js'
7
- import { mkTempDir, cli } from './utils/test-utils.js'
8
-
9
- const rel = f => join(import.meta.dirname, f)
10
-
11
-
12
- describe('vtrim', () => {
13
- function almostEqual(actual, expected) {
14
- const EPSILON = 0.05
15
- ok(Math.abs(parseFloat(actual) - expected) < EPSILON,
16
- `Duration should be around ${expected}s, got ${actual}s`)
17
- }
18
-
19
- const tmp = mkTempDir('vtrim')
20
- const inputFile = join(tmp, '60fps.mp4')
21
- cpSync(rel('fixtures/60fps.mp4'), inputFile)
22
-
23
- test('from start to end time', async () => {
24
- cli('vtrim', '--start', 5, '--end', 11, inputFile)
25
- const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
26
- almostEqual(duration, 6)
27
- })
28
-
29
- test('start time only', async () => {
30
- cli('vtrim', '--start', 5, inputFile)
31
- const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
32
- almostEqual(duration, 25)
33
- })
34
-
35
- test('end time only', async () => {
36
- cli('vtrim', '--end', 11, inputFile)
37
- const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
38
- almostEqual(duration, 11)
39
- })
40
- })