mediasnacks 0.23.0 → 0.25.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
@@ -10,12 +10,6 @@ Utilities video and images.
10
10
  npm install -g mediasnacks
11
11
  ```
12
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.js
17
- ```
18
-
19
13
 
20
14
 
21
15
  ## Overview
@@ -32,6 +26,8 @@ mediasnacks <command> <args>
32
26
 
33
27
  - `resize` Resizes videos or images
34
28
  - `edgespic` Extracts first and last frames
29
+ - `frameseq` Converts video to sequence of PNGs
30
+ - `countframes` Counts frames in a video
35
31
  - `ssim` Computes similarity of two images
36
32
  - `gif`: Video to GIF
37
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
@@ -9,7 +9,8 @@
9
9
  "mediasnacks": "src/cli.js"
10
10
  },
11
11
  "scripts": {
12
- "test": "docker run --rm $(docker build -q .)",
12
+ "test": "FORCE_COLOR=1 node --test",
13
+ "test-docker": "docker run --rm $(docker build -q .)",
13
14
  "postinstall": "node install-zsh-completions.js",
14
15
  "dev-install": "npm i -g . --ignore-scripts=false"
15
16
  },
package/src/cli.js CHANGED
@@ -13,6 +13,8 @@ const COMMANDS = {
13
13
 
14
14
  resize: ['resize.js', 'Resizes videos or images'],
15
15
  edgespic: ['edgespic.js', 'Extracts first and last frames'],
16
+ frameseq: ['frameseq.js', 'Converts video to sequence of PNGs'],
17
+ countframes: ['countframes.js', 'Counts frames in a video'],
16
18
  ssim: ['ssim.js', 'Computes SSIM between two images'],
17
19
  gif: ['gif.sh', 'Video to GIF\n'],
18
20
 
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseOptions } from './utils/parseOptions.js'
4
+ import { assertUserHasFFmpeg } from './utils/subprocess.js'
5
+ import { videoAttrs } from './utils/videoAttrs.js'
6
+ import { parseTimecode } from './utils/parseTimecode.js'
7
+
8
+
9
+ const HELP = `
10
+ SYNOPSIS
11
+ mediasnacks countframes [options] <file>
12
+
13
+ DESCRIPTION
14
+ Counts the number of frames in a video between optional start and end bounds.
15
+
16
+ OPTIONS
17
+ -f, --fps <num> Frames per second (default: same as video)
18
+ -s, --start <timecode> Start time in seconds (default: 0)
19
+ -e, --end <timecode> End time in seconds (default: end of video)
20
+
21
+ EXAMPLES
22
+ mediasnacks countframes --start=1:30.16 --end=60 video.mov
23
+ mediasnacks countframes --fps=12 video.mov
24
+ `.trim()
25
+
26
+
27
+ async function main() {
28
+ await assertUserHasFFmpeg()
29
+
30
+ const { values, files } = await parseOptions({
31
+ fps: { type: 'string', default: '' },
32
+ start: { short: 's', type: 'string', default: '' },
33
+ end: { short: 'e', type: 'string', default: '' },
34
+ help: { short: 'h', type: 'boolean' }
35
+ })
36
+
37
+ if (values.help) {
38
+ console.log(HELP)
39
+ return
40
+ }
41
+
42
+ const { fps, start, end } = values
43
+ const file = files[0]
44
+ if (!file) throw new Error('No video file specified')
45
+
46
+ const n = await countframes(file, fps, start, end)
47
+ console.log(String(n))
48
+ }
49
+
50
+
51
+ export async function countframes(file, fps, start, end) {
52
+ const v = await videoAttrs(file)
53
+ const videoDuration = parseFloat(v.duration || 0)
54
+ const startSecs = start ? parseTimecode(start) : 0
55
+ const endSecs = end ? parseTimecode(end) : videoDuration
56
+ const durationLimit = Math.max(0, endSecs - startSecs)
57
+ const actualFps = fps ? Number(fps) : eval(v.r_frame_rate)
58
+ return Math.ceil(durationLimit * actualFps)
59
+ }
60
+
61
+
62
+ if (import.meta.main)
63
+ main().catch(err => {
64
+ console.error(err.message)
65
+ process.exit(1)
66
+ })
package/src/detectdups.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
- import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/subprocess.js'
4
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
5
+ import { videoAttrs } from './utils/videoAttrs.js'
5
6
 
6
7
  const STDEV_THRESHOLD = 0.2
7
8
 
package/src/dropdups.js CHANGED
@@ -4,10 +4,10 @@ import { resolve, parse, format } from 'node:path'
4
4
 
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
6
  import { ffmpeg, assertUserHasFFmpeg, run } from './utils/subprocess.js'
7
- import { PRORES_PROFILES } from './prores.js'
7
+ import { ProresProfiles } from './prores.js'
8
8
 
9
9
 
10
- const PROFILE = PRORES_PROFILES.hq
10
+ const PROFILE = ProresProfiles.default
11
11
 
12
12
  const HELP = `
13
13
  SYNOPSIS
@@ -65,9 +65,7 @@ async function dropdups(video, dupFrameNum) {
65
65
  ? `decimate=cycle=${dupFrameNum}`
66
66
  : 'mpdecimate,setpts=N/FRAME_RATE/TB',
67
67
  '-fps_mode', 'cfr',
68
- '-c:v', 'prores_ks',
69
- '-profile:v', PROFILE,
70
- '-pix_fmt', 'yuv422p10le',
68
+ '-c:v', 'prores_ks', '-profile:v', PROFILE,
71
69
  makeOutputPath(video)
72
70
  ])
73
71
  }
package/src/edgespic.js CHANGED
@@ -4,7 +4,8 @@ import { basename, extname, join, parse } from 'node:path'
4
4
 
5
5
  import { mkDir } from './utils/fs-utils.js'
6
6
  import { parseOptions } from './utils/parseOptions.js'
7
- import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
7
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
8
+ import { videoAttrs } from './utils/videoAttrs.js'
8
9
 
9
10
 
10
11
  const HELP = `
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { basename, extname, join, parse } from 'node:path'
4
+
5
+ import { mkDir } from './utils/fs-utils.js'
6
+ import { parseOptions } from './utils/parseOptions.js'
7
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
8
+ import { countframes } from './countframes.js'
9
+
10
+
11
+ const HELP = `
12
+ SYNOPSIS
13
+ mediasnacks frameseq [options] <files>
14
+
15
+ DESCRIPTION
16
+ Converts a video into a sequence of PNGs.
17
+
18
+ OPTIONS
19
+ -f, --fps <num> Frames per second (default: same as video)
20
+ -s, --start <timecode> Start time in seconds (default: 0)
21
+ -e, --end <timecode> End time in seconds (default: end of video)
22
+
23
+ EXAMPLES
24
+ Start and End
25
+ mediasnacks frameseq --start=1:30.16 --end=60 video.mov
26
+
27
+ Custom framerate, all video duration
28
+ mediasnacks frameseq --fps=12 video.mov
29
+ `.trim()
30
+
31
+
32
+ async function main() {
33
+ await assertUserHasFFmpeg()
34
+
35
+ const { values, files } = await parseOptions({
36
+ fps: { type: 'string', default: '' },
37
+ start: { short: 's', type: 'string', default: '' },
38
+ end: { short: 'e', type: 'string', default: '' },
39
+ help: { short: 'h', type: 'boolean' }
40
+ })
41
+
42
+ if (values.help) {
43
+ console.log(HELP)
44
+ return
45
+ }
46
+
47
+ const { fps, start, end } = values
48
+ const file = files[0]
49
+ if (!file) throw new Error('No video files specified')
50
+ if (fps && isNaN(parseFloat(fps))) throw new Error('Invalid --fps')
51
+ if (start && isNaN(parseFloat(start))) throw new Error('Invalid --start')
52
+ if (end && isNaN(parseFloat(end))) throw new Error('Invalid --end')
53
+
54
+ const nFrames = await countframes(file, fps, start, end)
55
+ const pad = String(nFrames).length
56
+ await frameseq(file, fps, start, end, pad)
57
+ }
58
+
59
+ async function frameseq(video, fps, start, end, pad) {
60
+ const name = basename(video, extname(video))
61
+ const outDir = join(parse(video).dir, name)
62
+ await mkDir(outDir)
63
+ await ffmpeg([
64
+ start ? ['-ss', start] : [],
65
+ end ? ['-to', end] : [],
66
+ '-i', video,
67
+ fps ? ['-vf', `fps=${fps}`] : [],
68
+ join(outDir, `${name}_%0${pad}d.png`)
69
+ ].flat())
70
+ }
71
+
72
+ main().catch(err => {
73
+ console.error(err.message)
74
+ process.exit(1)
75
+ })
package/src/hev1tohvc1.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
4
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
- import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
5
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
6
+ import { videoAttrs } from './utils/videoAttrs.js'
6
7
 
7
8
 
8
9
  const HELP = `
@@ -31,7 +32,6 @@ async function main() {
31
32
  if (!files.length)
32
33
  throw new Error(HELP)
33
34
 
34
- console.log('HEV1 to HVC1…')
35
35
  for (const file of files)
36
36
  await hev1tohvc1(file)
37
37
  }
package/src/prores.js CHANGED
@@ -3,13 +3,24 @@ import { resolve, parse, join } from 'node:path'
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
4
  import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
5
5
 
6
- export const PRORES_PROFILES = {
7
- 'proxy': 0,
8
- 'lt': 1,
9
- 'standard': 2,
10
- 'hq': 3,
11
- '4444': 4,
12
- '4444xq': 5,
6
+ export const ProresProfiles = new class {
7
+ // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
8
+ profiles = {
9
+ // 10-bit color depth
10
+ 0: '422 Proxy',
11
+ 1: '422 LT',
12
+ 2: '422 Standard',
13
+ 3: '422 High Quality',
14
+
15
+ // 12-bit
16
+ 4: '4444',
17
+ 5: '4444XQ'
18
+ }
19
+ default = 3
20
+
21
+ list = () => Object.keys(this.profiles)
22
+ isValid = n => Object.hasOwn(this.profiles, n)
23
+ table = () => Object.entries(this.profiles)
13
24
  }
14
25
 
15
26
  const HELP = `
@@ -17,17 +28,25 @@ SYNOPSIS
17
28
  mediasnacks prores [options] <video>
18
29
 
19
30
  DESCRIPTION
20
- Converts a video to ProRes format.
31
+ Converts a video to Apple ProRes
21
32
 
22
33
  OPTIONS
23
- -p, --profile <n> ProRes profile (default: 3 (422 HQ))
34
+ -p, --profile <n> Default: ${ProresProfiles.default}
35
+ -s, --start <time> In time. Unset means beginning
36
+ -e, --end <time> Out time. Unset means end
24
37
  -h, --help
38
+
39
+ PROFILES
40
+ ${ProresProfiles.table().map(([num, name]) =>
41
+ ` ${num}: ${name}`).join('\n')}
25
42
 
26
- EXAMPLES
27
- mediasnacks prores video.mov
28
- mediasnacks prores --profile 2 video.mov
43
+ TIME FORMAT
44
+ 5.1 -- pure seconds
45
+ 20:10.0 -- 20m 10s
29
46
 
30
- Both output a file named: video.prores.mov
47
+ EXAMPLES
48
+ mediasnacks prores --end=60 video.mov // outputs video.prores.mov
49
+ mediasnacks prores -p2 *.mov
31
50
  `.trim()
32
51
 
33
52
 
@@ -35,8 +54,10 @@ async function main() {
35
54
  await assertUserHasFFmpeg()
36
55
 
37
56
  const { values, files } = await parseOptions({
38
- profile: { short: 'p', type: 'string', default: String(PRORES_PROFILES.hq) },
39
- help: { short: 'h', type: 'boolean' },
57
+ profile: { short: 'p', type: 'string', default: String(ProresProfiles.default) },
58
+ start: { short: 's', type: 'string', default: '' },
59
+ end: { short: 'e', type: 'string', default: '' },
60
+ help: { short: 'h', type: 'boolean' }
40
61
  })
41
62
 
42
63
  if (values.help) {
@@ -44,6 +65,9 @@ async function main() {
44
65
  return
45
66
  }
46
67
 
68
+ if (!ProresProfiles.isValid(Number(values.profile)))
69
+ throw new Error('Invalid profile. Must be one of: ' + ProresProfiles.list().join(','))
70
+
47
71
  if (files.length !== 1)
48
72
  throw new Error('Expected 1 argument: video file. See mediasnacks prores --help')
49
73
 
@@ -51,20 +75,19 @@ async function main() {
51
75
  const { name, dir } = parse(video)
52
76
  const output = join(dir, `${name}.prores.mov`)
53
77
 
54
- console.log(`Converting to ProRes…`)
55
- await prores(video, values.profile, output)
78
+ await prores(video, values.start, values.end, values.profile, output)
56
79
  }
57
80
 
58
- async function prores(video, profile, output) {
81
+ async function prores(video, start, end, profile, output) {
59
82
  await run('ffmpeg', [
60
83
  '-v', 'error',
61
84
  '-stats',
85
+ start ? ['-ss', start] : [],
86
+ end ? ['-to', end] : [],
62
87
  '-i', video,
63
- '-c:v', 'prores_ks',
64
- '-profile:v', profile,
65
- '-pix_fmt', 'yuv422p10le',
88
+ '-c:v', 'prores_ks', '-profile:v', profile,
66
89
  output
67
- ])
90
+ ].flat())
68
91
  }
69
92
 
70
93
  if (import.meta.main)
package/src/resize.js CHANGED
@@ -4,7 +4,8 @@ import { rename } from 'node:fs/promises'
4
4
 
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
6
  import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
7
- import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
7
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
8
+ import { videoAttrs } from './utils/videoAttrs.js'
8
9
 
9
10
 
10
11
  const HELP = `
@@ -0,0 +1,16 @@
1
+ export function parseTimecode(time) {
2
+ if (Number.isFinite(time))
3
+ return time
4
+
5
+ const parts = time.split(':').map(Number)
6
+ if (parts.some(isNaN) || parts.length > 3)
7
+ throw new Error(`Invalid time: ${time}`)
8
+
9
+ // HH:MM:SS or HH:MM:SS.mmm
10
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]
11
+
12
+ // MM:SS or MM:SS.mmm
13
+ if (parts.length === 2) return parts[0] * 60 + parts[1]
14
+
15
+ return parts[0]
16
+ }
@@ -15,7 +15,7 @@ export async function ffmpeg(args) {
15
15
  return runSilently('ffmpeg', args)
16
16
  }
17
17
 
18
- async function runSilently(program, args) {
18
+ export async function runSilently(program, args) {
19
19
  return new Promise((resolve, reject) => {
20
20
  const stdout = []
21
21
  const stderr = []
@@ -52,95 +52,3 @@ export async function run(program, args) {
52
52
  })
53
53
  }
54
54
 
55
-
56
- /**
57
- * Describes disposition flags that define how the video stream should be treated
58
- * by players or downstream consumers.
59
- * @typedef {Object} VideoStreamDisposition
60
- * @prop {number} default Is whether this stream is the default selection.
61
- * @prop {number} dub is dubbed?
62
- * @prop {number} original is original?
63
- * @prop {number} comment contains commentary?
64
- * @prop {number} lyrics has lyrics?
65
- * @prop {number} karaoke is karaoke?
66
- * @prop {number} forced must always be rendered?
67
- * @prop {number} hearing_impaired targets hearing-impaired audiences?
68
- * @prop {number} visual_impaired targets visually-impaired audiences?
69
- * @prop {number} clean_effects removes certain effects or noise?
70
- * @prop {number} attached_pic represents embedded artwork?
71
- * @prop {number} timed_thumbnails timed thumbnail data?
72
- * @prop {number} non_diegetic non-diegetic content?
73
- * @prop {number} captions has captions?
74
- * @prop {number} descriptions has audio descriptions?
75
- * @prop {number} metadata has supplemental metadata?
76
- * @prop {number} dependent depends on another stream?
77
- * @prop {number} still_image still-image video content?
78
- * @prop {number} multilayer multilayer stream content?
79
- */
80
-
81
- /**
82
- * Describes metadata tags associated with a video stream.
83
- * @typedef {Object} VideoStreamTags
84
- * @prop {string} language stream language.
85
- * @prop {string} handler_name handler or track label.
86
- * @prop {string} vendor_id vendor for the encoder or container.
87
- */
88
-
89
- /**
90
- * Full set of attributes returned by ffprobe for a single video stream.
91
- * @typedef {Object} VideoStream
92
- * @prop {number} index Numerical index of the stream within the container.
93
- * @prop {string} codec_name Short codec identifier used by FFmpeg.
94
- * @prop {string} codec_long_name Descriptive codec name.
95
- * @prop {string} profile Codec profile used during encoding.
96
- * @prop {string} codec_type The media type, typically "video".
97
- * @prop {string} codec_tag_string Codec tag string declared in the container.
98
- * @prop {string} codec_tag Numeric codec tag in hexadecimal form.
99
- * @prop {number} width Video width in pixels.
100
- * @prop {number} height Video height in pixels.
101
- * @prop {number} coded_width Internal coded width, which may differ from output width.
102
- * @prop {number} coded_height Internal coded height.
103
- * @prop {number} has_b_frames Number of B-frames used by the encoder.
104
- * @prop {string} sample_aspect_ratio Pixel aspect ratio declared in the stream.
105
- * @prop {string} display_aspect_ratio Display aspect ratio after scaling.
106
- * @prop {string} pix_fmt Pixel format used by the video stream.
107
- * @prop {number} level Codec level used during encoding.
108
- * @prop {string} chroma_location The chroma sample position pattern.
109
- * @prop {string} field_order Field order (progressive, top-field-first, etc.).
110
- * @prop {number} refs Number of reference frames used by the encoder.
111
- * @prop {string} is_avc Indicates whether the stream uses AVC-style NAL units.
112
- * @prop {string} nal_length_size Length of NAL unit size prefixes.
113
- * @prop {string} id Stream identifier within the container.
114
- * @prop {string} r_frame_rate Raw frame rate reported by the demuxer.
115
- * @prop {string} avg_frame_rate Average frame rate.
116
- * @prop {string} time_base The fundamental time base of the stream.
117
- * @prop {number} start_pts Presentation timestamp where the stream begins.
118
- * @prop {string} start_time Wall-clock start time in seconds.
119
- * @prop {number} duration_ts Duration expressed in time-base units.
120
- * @prop {string} duration Stream duration in seconds.
121
- * @prop {string} bit_rate Declared bit rate of the video stream.
122
- * @prop {string} bits_per_raw_sample Bit depth of the raw samples.
123
- * @prop {string} nb_frames Number of frames according to the container.
124
- * @prop {number} extradata_size Size of the extra codec data.
125
- * @prop {VideoStreamDisposition} disposition Disposition flags describing playback intent.
126
- * @prop {VideoStreamTags} tags Metadata tags for the stream.
127
- */
128
-
129
- /**
130
- * Extracts full metadata for the primary video stream (v:0) using ffprobe.
131
- * @param {string} video Path to the video file.
132
- * @returns {Promise<VideoStream>} All video stream attributes.
133
- */
134
- export async function videoAttrs(v) {
135
- const { stdout } = await runSilently('ffprobe', [
136
- '-v', 'error',
137
- '-select_streams', 'v:0',
138
- '-show_entries', 'stream',
139
- '-of', 'json',
140
- v
141
- ])
142
- return JSON.parse(stdout).streams?.[0] || {}
143
- }
144
-
145
-
146
-
@@ -0,0 +1,91 @@
1
+ import { runSilently } from './subprocess.js'
2
+
3
+ /**
4
+ * Describes disposition flags that define how the video stream should be treated
5
+ * by players or downstream consumers.
6
+ * @typedef {Object} VideoStreamDisposition
7
+ * @prop {number} default Is whether this stream is the default selection.
8
+ * @prop {number} dub is dubbed?
9
+ * @prop {number} original is original?
10
+ * @prop {number} comment contains commentary?
11
+ * @prop {number} lyrics has lyrics?
12
+ * @prop {number} karaoke is karaoke?
13
+ * @prop {number} forced must always be rendered?
14
+ * @prop {number} hearing_impaired targets hearing-impaired audiences?
15
+ * @prop {number} visual_impaired targets visually-impaired audiences?
16
+ * @prop {number} clean_effects removes certain effects or noise?
17
+ * @prop {number} attached_pic represents embedded artwork?
18
+ * @prop {number} timed_thumbnails timed thumbnail data?
19
+ * @prop {number} non_diegetic non-diegetic content?
20
+ * @prop {number} captions has captions?
21
+ * @prop {number} descriptions has audio descriptions?
22
+ * @prop {number} metadata has supplemental metadata?
23
+ * @prop {number} dependent depends on another stream?
24
+ * @prop {number} still_image still-image video content?
25
+ * @prop {number} multilayer multilayer stream content?
26
+ */
27
+
28
+ /**
29
+ * Describes metadata tags associated with a video stream.
30
+ * @typedef {Object} VideoStreamTags
31
+ * @prop {string} language stream language.
32
+ * @prop {string} handler_name handler or track label.
33
+ * @prop {string} vendor_id vendor for the encoder or container.
34
+ */
35
+
36
+ /**
37
+ * Full set of attributes returned by ffprobe for a single video stream.
38
+ * @typedef {Object} VideoStream
39
+ * @prop {number} index Numerical index of the stream within the container.
40
+ * @prop {string} codec_name Short codec identifier used by FFmpeg.
41
+ * @prop {string} codec_long_name Descriptive codec name.
42
+ * @prop {string} profile Codec profile used during encoding.
43
+ * @prop {string} codec_type The media type, typically "video".
44
+ * @prop {string} codec_tag_string Codec tag string declared in the container.
45
+ * @prop {string} codec_tag Numeric codec tag in hexadecimal form.
46
+ * @prop {number} width Video width in pixels.
47
+ * @prop {number} height Video height in pixels.
48
+ * @prop {number} coded_width Internal coded width, which may differ from output width.
49
+ * @prop {number} coded_height Internal coded height.
50
+ * @prop {number} has_b_frames Number of B-frames used by the encoder.
51
+ * @prop {string} sample_aspect_ratio Pixel aspect ratio declared in the stream.
52
+ * @prop {string} display_aspect_ratio Display aspect ratio after scaling.
53
+ * @prop {string} pix_fmt Pixel format used by the video stream.
54
+ * @prop {number} level Codec level used during encoding.
55
+ * @prop {string} chroma_location The chroma sample position pattern.
56
+ * @prop {string} field_order Field order (progressive, top-field-first, etc.).
57
+ * @prop {number} refs Number of reference frames used by the encoder.
58
+ * @prop {string} is_avc Indicates whether the stream uses AVC-style NAL units.
59
+ * @prop {string} nal_length_size Length of NAL unit size prefixes.
60
+ * @prop {string} id Stream identifier within the container.
61
+ * @prop {string} r_frame_rate Raw frame rate reported by the demuxer.
62
+ * @prop {string} avg_frame_rate Average frame rate.
63
+ * @prop {string} time_base The fundamental time base of the stream.
64
+ * @prop {number} start_pts Presentation timestamp where the stream begins.
65
+ * @prop {string} start_time Wall-clock start time in seconds.
66
+ * @prop {number} duration_ts Duration expressed in time-base units.
67
+ * @prop {string} duration Stream duration in seconds.
68
+ * @prop {string} bit_rate Declared bit rate of the video stream.
69
+ * @prop {string} bits_per_raw_sample Bit depth of the raw samples.
70
+ * @prop {string} nb_frames Number of frames according to the container.
71
+ * @prop {number} extradata_size Size of the extra codec data.
72
+ * @prop {VideoStreamDisposition} disposition Disposition flags describing playback intent.
73
+ * @prop {VideoStreamTags} tags Metadata tags for the stream.
74
+ */
75
+
76
+
77
+ /**
78
+ * Extracts full metadata for the primary video stream (v:0) using ffprobe.
79
+ * @param {string} video Path to the video file.
80
+ * @returns {Promise<VideoStream>} All video stream attributes.
81
+ */
82
+ export async function videoAttrs(v) {
83
+ const { stdout } = await runSilently('ffprobe', [
84
+ '-v', 'error',
85
+ '-select_streams', 'v:0',
86
+ '-show_entries', 'stream',
87
+ '-of', 'json',
88
+ v
89
+ ])
90
+ return JSON.parse(stdout).streams?.[0] || {}
91
+ }