mediasnacks 0.20.0 → 0.21.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.
@@ -29,8 +29,6 @@ _mediasnacks_commands=(
29
29
 
30
30
  'unemoji:Removes emojis from filenames'
31
31
  'rmcover:Removes cover art'
32
-
33
- 'curltime:Measures request response timings'
34
32
  )
35
33
 
36
34
  if (( CURRENT == 2 )); then
package/README.md CHANGED
@@ -60,8 +60,6 @@ mediasnacks <command> <args>
60
60
  - `rmcover`: Removes cover art
61
61
 
62
62
 
63
- - `curltime`: Measures request response timings
64
-
65
63
  ### Globs
66
64
  Glob patterns are expanded by Node.js.
67
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
package/src/cli.js CHANGED
@@ -34,9 +34,7 @@ const COMMANDS = {
34
34
  dlvideo: ['dlvideo.sh', 'yt-dlp best video\n'],
35
35
 
36
36
  unemoji: ['unemoji.sh', 'Removes emojis from filenames'],
37
- rmcover: ['rmcover.sh', 'Removes cover art\n'],
38
-
39
- curltime: ['curltime.sh', 'Measures request response timings'],
37
+ rmcover: ['rmcover.sh', 'Removes cover art'],
40
38
  }
41
39
 
42
40
  const MAN = `
package/src/detectdups.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
4
  import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/ffmpeg.js'
5
5
 
6
+ const STDEV_THRESHOLD = 0.2
6
7
 
7
8
  const MAN = `
8
9
  Usage: mediasnacks detectdups [options] <video>
@@ -70,10 +71,19 @@ async function main() {
70
71
 
71
72
 
72
73
  const dups = await detectDuplicateFramesNums(files[0], seek, duration)
73
- analyze(dups, seek, duration)
74
+ const h = deltaHistogram(dups)
75
+ const report = {
76
+ n: maxFreqKey(h),
77
+ histogram: h,
78
+ analyzed_region: {
79
+ start_sec: seek,
80
+ end_sec: seek + duration
81
+ },
82
+ }
83
+ console.log(JSON.stringify(report, null, 2))
74
84
  }
75
85
 
76
- async function detectDuplicateFramesNums(video, seek, duration) {
86
+ export async function detectDuplicateFramesNums(video, seek, duration) {
77
87
  const { stderr } = await ffmpeg([
78
88
  '-v', 'info',
79
89
  '-stats',
@@ -81,7 +91,6 @@ async function detectDuplicateFramesNums(video, seek, duration) {
81
91
  '-t', duration,
82
92
  '-i', video,
83
93
  '-vf', [
84
- 'scale=320:-1',
85
94
  'tblend=all_mode=difference',
86
95
  'format=gray',
87
96
  'showinfo',
@@ -89,32 +98,48 @@ async function detectDuplicateFramesNums(video, seek, duration) {
89
98
  '-f', 'null', '-',
90
99
  ])
91
100
 
92
- const reBlackFramesNum = /n:\s*(\d+).*?mean:\[0]/
101
+ const reNearBlackFrames = /n:\s*(\d+).*?mean:\[0].*?stdev:\[([0-9.]+)]/
93
102
  const dupFrames = []
94
103
  for (const line of stderr.split('\n')) {
95
- const match = line.match(reBlackFramesNum)
96
- if (match)
97
- dupFrames.push(Number(match[1]))
104
+ const match = line.match(reNearBlackFrames)
105
+ if (match) {
106
+ const stdev = parseFloat(match[2])
107
+ if (stdev <= STDEV_THRESHOLD) {
108
+ const frameNum = parseInt(match[1], 10)
109
+ dupFrames.push(frameNum)
110
+ }
111
+ }
98
112
  }
99
113
  return dupFrames
100
114
  }
101
115
 
102
- function analyze(dup_frames, seek, duration) {
116
+ // This is only good for when there's one repeated frame in a cycle.
117
+ // i.e. it's the wrong approach for e.g. 25 to 60, in which N=2 and N=3
118
+ function deltaHistogram(dups) {
103
119
  const histogram = {}
104
- for (let i = 1; i < dup_frames.length; i++) {
105
- const diff = dup_frames[i] - dup_frames[i - 1]
120
+ for (let i = 1; i < dups.length; i++) {
121
+ const diff = dups[i] - dups[i - 1]
106
122
  histogram[diff] = (histogram[diff] || 0) + 1
107
123
  }
108
- console.log(JSON.stringify({
109
- analyzed_region: {
110
- start_sec: seek,
111
- end_sec: seek + duration
112
- },
113
- histogram
114
- }, null, 2))
124
+ return histogram
125
+ }
126
+
127
+ function maxFreqKey(histogram) {
128
+ let maxKey = null
129
+ let maxVal = -1
130
+ for (const [k, v] of Object.entries(histogram))
131
+ if (v > maxVal) {
132
+ maxVal = v
133
+ maxKey = k
134
+ }
135
+ return maxKey !== null
136
+ ? Number(maxKey)
137
+ : null
115
138
  }
116
139
 
117
- main().catch(err => {
118
- console.error(err.message || err)
119
- process.exit(1)
120
- })
140
+
141
+ if (import.meta.main)
142
+ main().catch(err => {
143
+ console.error(err.message || err)
144
+ process.exit(1)
145
+ })
@@ -0,0 +1,24 @@
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
+
@@ -0,0 +1,71 @@
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
@@ -1,8 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { spawnSync } from 'node:child_process'
4
- import { createHash } from 'node:crypto'
5
- import { mkdtempSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
4
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
6
5
 
7
6
  const rel = f => join(import.meta.dirname, f)
8
7
 
@@ -11,13 +10,7 @@ export function mkTempDir(prefix = 'test-') {
11
10
  }
12
11
 
13
12
  export function cli(...args) {
14
- spawnSync(rel('../cli.js'), args)
15
- }
16
-
17
- export function sha1(filePath) {
18
- return createHash('sha1')
19
- .update(readFileSync(filePath))
20
- .digest('base64')
13
+ return spawnSync(rel('../cli.js'), args)
21
14
  }
22
15
 
23
16
  export function dir(...args) {
package/src/curltime.sh DELETED
@@ -1,14 +0,0 @@
1
- #!/bin/sh
2
-
3
- # https://stackoverflow.com/a/47944496
4
-
5
- curl -so /dev/null -w "\
6
- DNS Lookup %{time_namelookup}
7
- TCP Handshake %{time_connect}
8
- TLS Handshake %{time_appconnect}
9
- Wait %{time_pretransfer}
10
- Redirect %{time_redirect}
11
- First Byte %{time_starttransfer}
12
- ───────────────────────
13
- TOTAL %{time_total}
14
- " "$@"