mediasnacks 0.18.1 → 0.19.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.
@@ -8,7 +8,8 @@ _mediasnacks_commands=(
8
8
  'edgespic:Extracts first and last frames'
9
9
  'gif:Video to GIF'
10
10
 
11
- 'dropdups:Removes duplicate frames in a video'
11
+ 'detectdups:Detects sequentially duplicate frames in a video'
12
+ 'dropdups:Removes sequentially duplicate frames in a video'
12
13
  'framediff:ffplay with a filter for diffing adjacent frames'
13
14
  'hev1tohvc1:Fixes video thumbnails not rendering in macOS Finder'
14
15
  'moov2front:Rearranges metadata for fast-start streaming'
@@ -38,7 +39,7 @@ fi
38
39
 
39
40
  local cmd="$words[2]"
40
41
  case "$cmd" in
41
- avif|resize|sqcrop|moov2front|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores)
42
+ avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores)
42
43
  _files
43
44
  ;;
44
45
  qdir)
package/README.md CHANGED
@@ -6,13 +6,20 @@ Utilities video and images.
6
6
  ### Install
7
7
  **FFmpeg and Node.js must be installed.**
8
8
 
9
- ```shell
10
- npm install -g mediasnacks --ignore-scripts=true
9
+ ```sh
10
+ npm install -g mediasnacks
11
+ ```
12
+
13
+ Optionally, if you have `ignore-scripts=true` in your `.npmprc`,
14
+ you can install zsh auto-completions with:
15
+ ```sh
16
+ $(npm root -g)/mediasnacks/install-zsh-completions.sh
11
17
  ```
12
18
 
13
19
 
20
+
14
21
  ## Overview
15
- ```shell
22
+ ```sh
16
23
  mediasnacks <command> <args>
17
24
  ```
18
25
 
@@ -27,7 +34,8 @@ mediasnacks <command> <args>
27
34
  - `gif`: Video to GIF
28
35
 
29
36
 
30
- - `dropdups` Removes duplicate frames in a video
37
+ - `detectdups` Detects sequentially duplicate frames in a video
38
+ - `dropdups` Removes sequentially duplicate frames in a video
31
39
  - `framediff`: Plays a video of adjacent frames diff
32
40
  - `hev1tohvc1`: Fixes video thumbnails not rendering in macOS Finder
33
41
  - `moov2front` Rearranges .mov and .mp4 metadata for fast-start streaming
@@ -4,9 +4,6 @@ set -eu
4
4
  # Exit on systems without ZSH
5
5
  zsh=$(command -v zsh) || exit 0
6
6
 
7
- # Exit on non-global (npm -g) installations
8
- [ "${npm_config_global:-}" = "true" ] || exit 0
9
-
10
7
  src="$(cd "$(dirname "$0")" && pwd)/.zsh/completions/_mediasnacks"
11
8
  [ -f "$src" ] || exit 0
12
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "scripts": {
12
12
  "test": "docker run --rm $(docker build -q .)",
13
- "postinstall": "sh install-zsh-completions.sh"
13
+ "postinstall": "sh install-zsh-completions.sh",
14
+ "dev-install": "npm i -g . --ignore-scripts=false"
14
15
  },
15
16
  "files": [
16
17
  "src",
package/src/avif.js CHANGED
@@ -29,7 +29,7 @@ async function main() {
29
29
  }
30
30
 
31
31
  if (!files.length)
32
- throw new Error('No images specified. See npx mediasnacks avif --help')
32
+ throw new Error('No images specified. See mediasnacks avif --help')
33
33
 
34
34
  console.log('AVIF…')
35
35
  for (const file of files)
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ const COMMANDS = {
14
14
  edgespic: ['edgespic.js', 'Extracts first and last frames'],
15
15
  gif: ['gif.sh', 'Video to GIF\n'],
16
16
 
17
+ detectdups: ['detectdups.js', 'Detects duplicate frames in a video'],
17
18
  dropdups: ['dropdups.js', 'Removes duplicate frames in a video'],
18
19
  framediff: ['framediff.sh', 'Plays a video of adjacent frames diff'],
19
20
  hev1tohvc1: ['hev1tohvc1.js', 'Fixes video thumbnails not rendering in macOS Finder '],
@@ -62,7 +63,7 @@ if (!opt) {
62
63
  process.exit(1)
63
64
  }
64
65
  if (!Object.hasOwn(COMMANDS, opt)) {
65
- console.error(`'${opt}' is not a command. See npx mediasnacks --help\n`)
66
+ console.error(`'${opt}' is not a command. See mediasnacks --help\n`)
66
67
  process.exit(1)
67
68
  }
68
69
 
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseOptions } from './utils/parseOptions.js'
4
+ import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/ffmpeg.js'
5
+
6
+
7
+ const USAGE = `
8
+ Usage: mediasnacks detectdups [options] <video>
9
+
10
+ Detects sequentially duplicate frames in a video and prints their frame numbers.
11
+
12
+ Options:
13
+ -s, --seek <sec> Video start time for detection
14
+ -d, --duration <sec> Analyze this many seconds of video
15
+ -v, --verbose
16
+ -h, --help
17
+ `.trim()
18
+
19
+
20
+ async function main() {
21
+ await assertUserHasFFmpeg()
22
+
23
+ const { values, files } = await parseOptions({
24
+ seek: { short: 's', type: 'string', },
25
+ duration: { short: 'd', type: 'string' },
26
+ help: { short: 'h', type: 'boolean' }
27
+ })
28
+
29
+ if (values.help) {
30
+ console.log(USAGE)
31
+ process.exit(0)
32
+ }
33
+
34
+ if (files.length !== 1)
35
+ throw new Error('One video file must be specified. See mediasnacks detectdups --help')
36
+
37
+ const v = await videoAttrs(files[0])
38
+
39
+ if (v.codec_type !== 'video')
40
+ throw new Error('Input file must be a video.')
41
+
42
+ 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)
45
+
46
+ if (isNaN(seek) || seek < 0)
47
+ throw new Error(`Invalid --seek value: ${values.seek}`)
48
+
49
+ if (isNaN(duration) || duration < 1)
50
+ throw new Error(`Invalid --duration value: ${values.duration}`)
51
+
52
+ if ((seek + duration) > vDur)
53
+ throw new Error(`Invalid analysis range. Exceeds video duration: ${vDur}`)
54
+
55
+
56
+ const dupFrames = await detectDuplicateFramesNums(files[0], seek, duration)
57
+ analyze(dupFrames, seek, duration)
58
+ }
59
+
60
+ async function detectDuplicateFramesNums(video, seek, duration) {
61
+ const { stderr } = await ffmpeg([
62
+ '-v', 'info',
63
+ '-stats',
64
+ '-ss', seek,
65
+ '-t', duration,
66
+ '-i', video,
67
+ '-vf', [
68
+ 'scale=320:-1',
69
+ 'tblend=all_mode=difference',
70
+ 'format=gray',
71
+ 'showinfo',
72
+ ].join(','),
73
+ '-f', 'null', '-',
74
+ ])
75
+
76
+ const reBlackFramesNum = /n:\s*(\d+).*?mean:\[0]/
77
+ const dupFrames = []
78
+ for (const line of stderr.split('\n')) {
79
+ const match = line.match(reBlackFramesNum)
80
+ if (match)
81
+ dupFrames.push(Number(match[1]))
82
+ }
83
+ return dupFrames
84
+ }
85
+
86
+ function analyze(dup_frames, seek, duration) {
87
+ const dup_distance = []
88
+ const histogram = {}
89
+ for (let i = 1; i < dup_frames.length; i++) {
90
+ const diff = dup_frames[i] - dup_frames[i - 1]
91
+ dup_distance.push(diff)
92
+ histogram[diff] = (histogram[diff] || 0) + 1
93
+ }
94
+ console.log({
95
+ analyzed_region: {
96
+ start: seek + 's',
97
+ end: (seek + duration) + 's',
98
+ },
99
+ dup_frames,
100
+ dup_distance,
101
+ histogram
102
+ })
103
+ }
104
+
105
+ main().catch(err => {
106
+ console.error(err.message || err)
107
+ process.exit(1)
108
+ })
package/src/dropdups.js CHANGED
@@ -20,7 +20,7 @@ const PROFILE = PRORES_PROFILES.hq
20
20
  const USAGE = `
21
21
  Usage: mediasnacks dropdups [-n <bad-frame-number>] <video>
22
22
 
23
- Removes duplicate frames and outputs ProRes 422 HQ.
23
+ Removes sequentially duplicate frames and outputs ProRes 422 HQ.
24
24
 
25
25
  Options:
26
26
  -n, --bad-frame-number <n> Known frame interval to drop.
@@ -45,7 +45,7 @@ async function main() {
45
45
  }
46
46
 
47
47
  if (!files.length)
48
- throw new Error('No video specified. See npx mediasnacks dropdups --help')
48
+ throw new Error('No video specified. See mediasnacks dropdups --help')
49
49
 
50
50
  let nBadFrame = values['bad-frame-number']
51
51
  if (nBadFrame && !/^\d+$/.test(nBadFrame))
package/src/edgespic.js CHANGED
@@ -14,8 +14,8 @@ Extracts the first and last frames from each video and saves them to the 'edgepi
14
14
  --width defaults to 640px and The aspect ratio is preserved.
15
15
 
16
16
  Example:
17
- npx mediasnacks edgespic --width 800 *.mov
18
- npx mediasnacks edgespic -w 600 'videos/**/*.mp4'
17
+ mediasnacks edgespic --width 800 *.mov
18
+ mediasnacks edgespic -w 600 'videos/**/*.mp4'
19
19
  `.trim()
20
20
 
21
21
 
package/src/prores.js CHANGED
@@ -27,8 +27,8 @@ Options:
27
27
  -h, --help Show this help message
28
28
 
29
29
  Example:
30
- npx mediasnacks prores video.mov
31
- npx mediasnacks prores --profile 2 video.mov
30
+ mediasnacks prores video.mov
31
+ mediasnacks prores --profile 2 video.mov
32
32
 
33
33
  Outputs: video.prores.mov
34
34
  `.trim()
@@ -48,7 +48,7 @@ async function main() {
48
48
  }
49
49
 
50
50
  if (files.length !== 1)
51
- throw new Error('Expected 1 argument: video file. See npx mediasnacks prores --help')
51
+ throw new Error('Expected 1 argument: video file. See mediasnacks prores --help')
52
52
 
53
53
  const videoPath = resolve(files[0])
54
54
 
package/src/resize.js CHANGED
@@ -15,10 +15,10 @@ Usage: mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [-
15
15
  Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
16
16
 
17
17
  Example: Overwrites the input file (-y)
18
- npx mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
18
+ mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
19
19
 
20
20
  Example: Output directory (-o)
21
- npx mediasnacks resize --height 240 --output-dir /tmp/out video.mov
21
+ mediasnacks resize --height 240 --output-dir /tmp/out video.mov
22
22
 
23
23
  Details:
24
24
  --width and --height are -2 by default:
package/src/sqcrop.js CHANGED
@@ -30,7 +30,7 @@ async function main() {
30
30
  }
31
31
 
32
32
  if (!files.length)
33
- throw new Error('No images specified. See npx mediasnacks sqcrop --help')
33
+ throw new Error('No images specified. See mediasnacks sqcrop --help')
34
34
 
35
35
  console.log('Cropping…')
36
36
  for (const file of files)
@@ -141,7 +141,7 @@ export async function videoAttrs(v) {
141
141
  '-of', 'json',
142
142
  v
143
143
  ])
144
- return JSON.parse(stdout).streams[0]
144
+ return JSON.parse(stdout).streams?.[0] || {}
145
145
  }
146
146
 
147
147
 
package/src/vsplit.js CHANGED
@@ -19,7 +19,7 @@ Arguments:
19
19
  <video> Video file to split
20
20
 
21
21
  Example:
22
- npx mediasnacks vsplit clips.csv video.mov
22
+ mediasnacks vsplit clips.csv video.mov
23
23
 
24
24
  Given clips.csv:
25
25
  start,end
@@ -44,7 +44,7 @@ async function main() {
44
44
  }
45
45
 
46
46
  if (files.length !== 2)
47
- throw new Error('Expected 2 arguments: CSV file and video file. See npx mediasnacks vsplit --help')
47
+ throw new Error('Expected 2 arguments: CSV file and video file. See mediasnacks vsplit --help')
48
48
 
49
49
  const [csvPath, videoPath] = files.map(f => resolve(f))
50
50
 
package/src/vtrim.js CHANGED
@@ -32,7 +32,7 @@ async function main() {
32
32
  }
33
33
 
34
34
  if (!files.length)
35
- throw new Error('No video specified. See npx mediasnacks vtrim --help')
35
+ throw new Error('No video specified. See mediasnacks vtrim --help')
36
36
 
37
37
  for (const file of files)
38
38
  await trim(resolve(file), values.start, values.end)