mediasnacks 0.30.3 → 0.32.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/README.md CHANGED
@@ -31,6 +31,7 @@ mediasnacks <command> <args>
31
31
  - `countframes` Counts frames in a video
32
32
  - `ssim` Computes similarity of two images
33
33
  - `gif`: Video to GIF
34
+ - `info`: Prints video attributes
34
35
 
35
36
 
36
37
  - `detectdups` Detects sequentially duplicate frames in a video
package/index.js CHANGED
@@ -21,3 +21,4 @@ export { unemoji } from './src/unemoji.js'
21
21
  export { vsplit } from './src/vsplit.js'
22
22
  export { vtrim } from './src/vtrim.js'
23
23
  export { base64 } from './src/base64.js'
24
+ export { infoSummary } from './src/info.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.30.3",
3
+ "version": "0.32.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
@@ -1,7 +1,7 @@
1
1
  import { join, basename, dirname } from 'node:path'
2
2
  import { parseOptions } from './utils/parseOptions.js'
3
3
  import { replaceExt, lstat } from './utils/fs-utils.js'
4
- import { ffmpeg } from './utils/subprocess.js'
4
+ import { ffmpeg } from './utils/ffmpeg.js'
5
5
 
6
6
 
7
7
  const HELP = `
package/src/cli.js CHANGED
@@ -17,7 +17,8 @@ const COMMANDS = {
17
17
  frameseq: ['./frameseq.js', 'Converts video to sequence of PNGs'],
18
18
  countframes: ['./countframes.js', 'Counts frames in a video'],
19
19
  ssim: ['./ssim.js', 'Computes SSIM between two images'],
20
- gif: ['./gif.js', 'Video to GIF\n'],
20
+ gif: ['./gif.js', 'Video to GIF'],
21
+ info: ['./info.js', 'Prints video stream attributes\n'],
21
22
 
22
23
  detectdups: ['./detectdups.js', 'Detects duplicate frames in a video'],
23
24
  dropdups: ['./dropdups.js', 'Removes duplicate frames in a video'],
package/src/detectdups.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parseOptions } from './utils/parseOptions.js'
2
- import { ffmpeg } from './utils/subprocess.js'
3
2
  import { videoAttrs } from './utils/videoAttrs.js'
3
+ import { ffmpeg } from './utils/ffmpeg.js'
4
4
 
5
5
  const STDEV_THRESHOLD = 0.2
6
6
 
package/src/dropdups.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { resolve, parse, format } from 'node:path'
2
- import { parseOptions } from './utils/parseOptions.js'
3
- import { ffmpeg, run } from './utils/subprocess.js'
4
2
  import { ProresProfiles } from './prores.js'
3
+ import { parseOptions } from './utils/parseOptions.js'
4
+ import { ffmpegWithProgress } from './utils/ffmpeg.js'
5
+ import { infoSummary } from './info.js'
5
6
 
6
7
 
7
8
  const PROFILE = ProresProfiles.default
@@ -15,7 +16,7 @@ DESCRIPTION
15
16
 
16
17
  OPTIONS
17
18
  -n, --dup-frame-num <n> Known frame interval to drop.
18
- Default: n=0, which auto-detects repeated frames (slower)
19
+ Default: n=0, which auto-detects repeated frames (slower)
19
20
 
20
21
  EXAMPLES
21
22
  Use n=2 when every other frame is repeated:
@@ -38,14 +39,14 @@ export default async function main() {
38
39
  if (dupFrameNum && !Number.isInteger(+dupFrameNum))
39
40
  throw usage('Invalid -n. It must be a positive integer.')
40
41
 
41
- for (const file of files)
42
+ for (const file of files) {
43
+ console.log(await infoSummary(file))
42
44
  await dropdups(resolve(file), dupFrameNum)
45
+ }
43
46
  }
44
47
 
45
48
  export async function dropdups(video, dupFrameNum) {
46
- await run('ffmpeg', [
47
- '-v', 'error',
48
- '-stats',
49
+ await ffmpegWithProgress(video, [
49
50
  '-an',
50
51
  '-i', video,
51
52
  '-vf', dupFrameNum
package/src/edgespic.js CHANGED
@@ -3,7 +3,7 @@ import { basename, extname, join, parse } from 'node:path'
3
3
  import { mkDir } from './utils/fs-utils.js'
4
4
  import { videoAttrs } from './utils/videoAttrs.js'
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
- import { ffmpeg } from './utils/subprocess.js'
6
+ import { ffmpeg } from './utils/ffmpeg.js'
7
7
 
8
8
 
9
9
  const WIDTH = 640
package/src/frameseq.js CHANGED
@@ -2,7 +2,7 @@ import { basename, extname, join, parse } from 'node:path'
2
2
 
3
3
  import { mkDir } from './utils/fs-utils.js'
4
4
  import { parseOptions } from './utils/parseOptions.js'
5
- import { ffmpeg } from './utils/subprocess.js'
5
+ import { ffmpeg } from './utils/ffmpeg.js'
6
6
  import { countframes } from './countframes.js'
7
7
 
8
8
 
package/src/hev1tohvc1.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseOptions } from './utils/parseOptions.js'
2
2
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
3
- import { ffmpeg } from './utils/subprocess.js'
4
3
  import { videoAttrs } from './utils/videoAttrs.js'
4
+ import { ffmpeg } from './utils/ffmpeg.js'
5
5
 
6
6
 
7
7
  const HELP = `
package/src/info.js ADDED
@@ -0,0 +1,63 @@
1
+ import { parseOptions } from './utils/parseOptions.js'
2
+ import { videoAttrs } from './utils/videoAttrs.js'
3
+ import { formatSeconds, cleanDecimals } from './utils/formatSeconds.js'
4
+
5
+
6
+ const HELP = `
7
+ SYNOPSIS
8
+ mediasnacks info [-a | --all] <file>
9
+
10
+ DESCRIPTION
11
+ Prints all available attributes for the primary video stream from ffprobe.
12
+
13
+ OPTIONS
14
+ -a, --all Prints all attributes as JSON
15
+ `
16
+
17
+ export default async function main() {
18
+ const { values, files, usage } = await parseOptions(HELP, {
19
+ all: { short: 'a', type: 'boolean' }
20
+ })
21
+
22
+ const video = files[0]
23
+ if (!video) throw usage('No video file specified')
24
+
25
+ if (values.all)
26
+ console.log(JSON.stringify(await videoAttrs(video), '', 2))
27
+ else
28
+ console.log(await infoSummary(video))
29
+ }
30
+
31
+
32
+ export async function infoSummary(video) {
33
+ const v = await videoAttrs(video)
34
+ return [
35
+ `${v.width}x${v.height}`,
36
+ `${fps(v.r_frame_rate)}fps`,
37
+ formatSeconds(v.duration),
38
+ prettyCodecName(v.codec_name)
39
+ ].join(' ')
40
+ }
41
+
42
+ function fps(rFrameRate) {
43
+ const [num, den] = rFrameRate.split('/').map(Number)
44
+ return cleanDecimals((num / den).toFixed(2))
45
+ }
46
+
47
+ function prettyCodecName(codec) {
48
+ // ffmpeg -codecs | grep '^...V'
49
+ return {
50
+ 'dnxhd': 'DNxHD',
51
+ 'dvvideo': 'DV (Digital Video)',
52
+ 'h264': 'H.264',
53
+ 'hevc': 'H.265',
54
+ 'jpeg2000': 'JPEG 2000',
55
+ 'mpeg4': 'MPEG-4 Part 2',
56
+ 'prores': 'ProRes',
57
+ 'qtrle': 'QuickTime RLE',
58
+ 'rawvideo': 'Uncompressed',
59
+ }[codec] || codec
60
+ }
61
+
62
+
63
+
package/src/moov2front.js CHANGED
@@ -1,6 +1,6 @@
1
- import { ffmpeg } from './utils/subprocess.js'
2
1
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
3
2
  import { parseOptions } from './utils/parseOptions.js'
3
+ import { ffmpeg } from './utils/ffmpeg.js'
4
4
 
5
5
 
6
6
  const HELP = `
package/src/prores.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { resolve, parse, join } from 'node:path'
2
2
  import { parseOptions } from './utils/parseOptions.js'
3
- import { run } from './utils/subprocess.js'
3
+ import { ffmpegWithProgress } from './utils/ffmpeg.js'
4
+ import { infoSummary } from './info.js'
4
5
 
5
6
 
6
7
  // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
@@ -68,13 +69,12 @@ export default async function main() {
68
69
  const output = join(dir, `${name}.prores.mov`)
69
70
 
70
71
  const { profile, start, end } = values
72
+ console.log(await infoSummary(video))
71
73
  await prores({ video, profile, start, end, output })
72
74
  }
73
75
 
74
76
  export async function prores({ video, profile, start, end, output }) {
75
- await run('ffmpeg', [
76
- '-v', 'error',
77
- '-stats',
77
+ await ffmpegWithProgress(video, [
78
78
  start ? ['-ss', start] : [],
79
79
  end ? ['-to', end] : [],
80
80
  '-i', video,
package/src/resize.js CHANGED
@@ -3,8 +3,8 @@ import { rename } from 'node:fs/promises'
3
3
 
4
4
  import { parseOptions } from './utils/parseOptions.js'
5
5
  import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
6
- import { ffmpeg } from './utils/subprocess.js'
7
6
  import { videoAttrs } from './utils/videoAttrs.js'
7
+ import { ffmpeg } from './utils/ffmpeg.js'
8
8
 
9
9
 
10
10
  const HELP = `
package/src/sqcrop.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { join } from 'node:path'
2
2
  import { rename } from 'node:fs/promises'
3
3
 
4
- import { ffmpeg } from './utils/subprocess.js'
5
4
  import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
6
5
  import { parseOptions } from './utils/parseOptions.js'
7
6
  import { videoAttrs } from './utils/videoAttrs.js'
7
+ import { ffmpeg } from './utils/ffmpeg.js'
8
8
 
9
9
 
10
10
  const HELP = `
package/src/ssim.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ffmpeg } from './utils/subprocess.js'
2
1
  import { parseOptions } from './utils/parseOptions.js'
2
+ import { ffmpeg } from './utils/ffmpeg.js'
3
3
 
4
4
 
5
5
  const HELP = `
@@ -0,0 +1,53 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { printProgress } from './printProgress.js'
3
+ import { videoAttrs } from './videoAttrs.js'
4
+ import { runSilently } from './subprocess.js'
5
+
6
+
7
+ async function assertUserHasFFmpeg() {
8
+ try {
9
+ await runSilently('ffmpeg', ['-version'])
10
+ await runSilently('ffprobe', ['-version'])
11
+ }
12
+ catch {
13
+ throw new Error('ffmpeg not found. Please install ffmpeg.')
14
+ }
15
+ }
16
+
17
+
18
+ export async function ffmpeg(args) {
19
+ await assertUserHasFFmpeg()
20
+ return runSilently('ffmpeg', args)
21
+ }
22
+
23
+
24
+ export async function ffmpegWithProgress(input, args, onProgress = printProgress) {
25
+ await assertUserHasFFmpeg()
26
+ const µsVideoDuration = 1e6 * (await videoAttrs(input)).duration
27
+
28
+ const p = spawn('ffmpeg', [
29
+ '-v', 'error',
30
+ '-nostats',
31
+ '-progress', 'pipe:1',
32
+ ...args
33
+ ], { stdio: ['inherit', 'pipe', 'pipe'] })
34
+
35
+ p.stderr.pipe(process.stderr)
36
+ p.stdout.on('data', chunk => {
37
+ const text = chunk.toString()
38
+ if (text.includes('progress=end'))
39
+ onProgress(1)
40
+ else {
41
+ const m = text.match(/out_time_us=(\d+)/)
42
+ onProgress(Number(m[1]) / µsVideoDuration)
43
+ }
44
+ })
45
+
46
+ await new Promise((resolve, reject) => {
47
+ p.on('error', reject)
48
+ p.on('close', code => {
49
+ if (code === 0) resolve()
50
+ else reject(Error(`ffmpeg failed with code ${code}`))
51
+ })
52
+ })
53
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Converts seconds to a string like "9h9m9s".
3
+ *
4
+ * Examples:
5
+ * formatSeconds(1.1) -> "1.1s"
6
+ * formatSeconds(3661, 2) -> "1h1m1s"
7
+ * formatSeconds(3661.0, 2) -> "1h1m1s"
8
+ * formatSeconds(3661.25, 2) -> "1h1m1.25s"
9
+ */
10
+ export function formatSeconds(seconds, maxDecimals = 2) {
11
+ const intSeconds = seconds | 0
12
+ const partialSeconds = seconds % 60
13
+ const minutes = (intSeconds % 3600) / 60 | 0
14
+ const hours = intSeconds / 3600 | 0
15
+
16
+ let result = ''
17
+ if (hours) result += `${hours}h`
18
+ if (minutes) result += `${minutes}m`
19
+ if (partialSeconds || !result) result += `${cleanDecimals(partialSeconds.toFixed(maxDecimals))}s`
20
+ return result
21
+ }
22
+
23
+
24
+ /**
25
+ * Removes trailing zeros and a trailing decimal point.
26
+ *
27
+ * Examples:
28
+ * cleanDecimals(3.1400) -> "3.14"
29
+ * cleanDecimals(5.0) -> "5"
30
+ */
31
+ export function cleanDecimals(number) {
32
+ return String(number).replace(/\.?0+$/, '') || '0'
33
+ }
@@ -0,0 +1,14 @@
1
+ export function printProgress(progress) {
2
+ process.stdout.write(`\r${progressBar(progress)} ${(progress * 100).toFixed(1)}%`)
3
+ if (progress === 1)
4
+ process.stdout.write('\n')
5
+ }
6
+
7
+ function progressBar(progress, width = 42) {
8
+ const nFull = (width * progress) | 0
9
+ const fPartial = (width * progress) - nFull
10
+ const nRemaining = width - nFull
11
+ const partials = ' ▏▎▍▌▋▊▉'
12
+ const partial = partials[Math.min(partials.length * fPartial | 0, partials.length - 1)]
13
+ return '█'.repeat(nFull) + partial + '⠂'.repeat(nRemaining)
14
+ }
@@ -1,21 +1,6 @@
1
1
  import { spawn } from 'node:child_process'
2
2
 
3
3
 
4
- async function assertUserHasFFmpeg() {
5
- try {
6
- await runSilently('ffmpeg', ['-version'])
7
- await runSilently('ffprobe', ['-version'])
8
- }
9
- catch {
10
- throw new Error('ffmpeg not found. Please install ffmpeg.')
11
- }
12
- }
13
-
14
- export async function ffmpeg(args) {
15
- await assertUserHasFFmpeg()
16
- return runSilently('ffmpeg', args)
17
- }
18
-
19
4
  export async function runSilently(program, args) {
20
5
  return new Promise((resolve, reject) => {
21
6
  const stdout = []
@@ -38,6 +23,7 @@ export async function runSilently(program, args) {
38
23
  })
39
24
  }
40
25
 
26
+
41
27
  export async function run(program, args) {
42
28
  return new Promise((resolve, reject) => {
43
29
  const p = spawn(program, args, { stdio: ['inherit', 'pipe', 'pipe'] })
package/src/vtrim.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolve, parse } from 'node:path'
2
2
  import { parseOptions } from './utils/parseOptions.js'
3
- import { ffmpeg } from './utils/subprocess.js'
3
+ import { ffmpeg } from './utils/ffmpeg.js'
4
4
 
5
5
 
6
6
  const HELP = `