mediasnacks 0.24.0 → 0.26.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/src/moov2front.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
3
2
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
4
3
  import { parseOptions } from './utils/parseOptions.js'
@@ -20,7 +19,7 @@ NOTES
20
19
  `.trim()
21
20
 
22
21
 
23
- async function main() {
22
+ export default async function main() {
24
23
  await assertUserHasFFmpeg()
25
24
 
26
25
  const { values, files } = await parseOptions({
@@ -32,25 +31,23 @@ async function main() {
32
31
  return
33
32
  }
34
33
 
35
- if (!files.length)
36
- throw new Error(HELP)
34
+ if (!files.length) throw new Error('Missing input file(s)')
37
35
 
38
36
  console.log('Optimizing video for progressive download…')
39
37
  for (const file of files)
40
- await moov2front(file)
38
+ try {
39
+ await moov2front(file)
40
+ console.log(file)
41
+ }
42
+ catch (err) {
43
+ console.error(err?.message || err)
44
+ }
41
45
  }
42
46
 
43
- async function moov2front(file) {
44
- if (!/\.(mp4|mov)$/i.test(file)) {
45
- console.log('(skipped: not mp4/mov)', file)
46
- return
47
- }
48
- if (await moovIsBeforeMdat(file)) {
49
- console.log('(skipped: no changes needed)', file)
50
- return
51
- }
47
+ export async function moov2front(file) {
48
+ if (!/\.(mp4|mov)$/i.test(file)) throw new Error(`not mp4/mov. ${file}`)
49
+ if (await moovIsBeforeMdat(file)) throw new Error(`no changes needed. ${file}`)
52
50
 
53
- console.log(file)
54
51
  const tmp = uniqueFilenameFor(file)
55
52
  await ffmpeg([
56
53
  '-hide_banner',
@@ -71,9 +68,3 @@ async function moovIsBeforeMdat(file) {
71
68
  const firstMatchedAtom = stderr.match(/type:'(moov|mdat)'/)?.[1]
72
69
  return firstMatchedAtom === 'moov'
73
70
  }
74
-
75
-
76
- main().catch(err => {
77
- console.error(err.message)
78
- process.exit(1)
79
- })
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { join } from 'node:path'
3
2
  import { spawn } from 'node:child_process'
4
3
  import { readdirSync } from 'node:fs'
@@ -7,13 +6,13 @@ import { parseOptions } from './utils/parseOptions.js'
7
6
 
8
7
  const HELP = `
9
8
  SYNOPSIS
10
- mediasnacks random [-r | --recursive]
9
+ mediasnacks openrand [-r | --recursive]
11
10
 
12
11
  DESCRIPTION
13
12
  Opens a random file in the current working directory
14
13
  `.trim()
15
14
 
16
- async function main() {
15
+ export default async function main() {
17
16
  if (process.platform !== 'darwin')
18
17
  throw new Error('Error: This command is only supported on macOS.')
19
18
 
@@ -27,17 +26,16 @@ async function main() {
27
26
  return
28
27
  }
29
28
 
30
- spawn('open', [pickRandomFile('.', values.recursive)])
29
+ openrand('.', values.recursive)
31
30
  }
32
31
 
33
- function pickRandomFile(dir, recursive) {
32
+ export function openrand(dir = '.', recursive = false) {
33
+ spawn('open', [pickRandomFile(dir, recursive)])
34
+ }
35
+
36
+ export function pickRandomFile(dir = '.', recursive = false) {
34
37
  const files = readdirSync(dir, { withFileTypes: true, recursive })
35
38
  .filter(entry => !entry.name.startsWith('.') && entry.isFile())
36
39
  .map(entry => join(entry.parentPath, entry.name))
37
40
  return files[Math.floor(Math.random() * files.length)]
38
41
  }
39
-
40
- main().catch(err => {
41
- console.error(err.message)
42
- process.exit(1)
43
- })
package/src/play.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { spawn } from 'node:child_process'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
  import { findFiles } from './utils/fs-utils.js'
@@ -17,7 +16,7 @@ EXAMPLE
17
16
  `.trim()
18
17
 
19
18
 
20
- async function main() {
19
+ export default async function main() {
21
20
  const { values, positionals } = await parseOptions({
22
21
  recursive: { short: 'r', type: 'boolean', default: true },
23
22
  help: { short: 'h', type: 'boolean' }
@@ -41,7 +40,7 @@ async function main() {
41
40
  play(files)
42
41
  }
43
42
 
44
- function play(files) {
43
+ export function play(files) {
45
44
  const mpv = spawn('mpv', ['--playlist=-'], {
46
45
  detached: true,
47
46
  stdio: ['pipe', 'ignore', 'ignore']
@@ -57,8 +56,3 @@ function play(files) {
57
56
  process.exit(1)
58
57
  })
59
58
  }
60
-
61
- main().catch(err => {
62
- console.error(err.message)
63
- process.exit(1)
64
- })
package/src/prores.js CHANGED
@@ -1,20 +1,19 @@
1
- #!/usr/bin/env node
2
1
  import { resolve, parse, join } from 'node:path'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
  import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
5
4
 
6
5
  export const ProresProfiles = new class {
7
- // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
6
+ // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
8
7
  profiles = {
9
- // 10-bit
10
- 0: 'proxy',
11
- 1: 'lt',
12
- 2: 'standard',
13
- 3: 'hq',
8
+ // 10-bit color depth
9
+ 0: '422 Proxy',
10
+ 1: '422 LT',
11
+ 2: '422 Standard',
12
+ 3: '422 High Quality',
14
13
 
15
14
  // 12-bit
16
15
  4: '4444',
17
- 5: '4444xq'
16
+ 5: '4444XQ'
18
17
  }
19
18
  default = 3
20
19
 
@@ -28,34 +27,36 @@ SYNOPSIS
28
27
  mediasnacks prores [options] <video>
29
28
 
30
29
  DESCRIPTION
31
- Converts a video to ProRes
30
+ Converts a video to Apple ProRes
32
31
 
33
32
  OPTIONS
34
- -p, --profile <n> Default: 3 (422 HQ 10-bit)
35
- -s, --start <time> Start time (e.g. 5.0). Default: beginning.
36
- -e, --end <time> End time (e.g. 0:10.0). Default: end of video.
33
+ -p, --profile <n> Default: ${ProresProfiles.default}
34
+ -s, --start <time> In time. Unset means beginning
35
+ -e, --end <time> Out time. Unset means end
37
36
  -h, --help
38
-
37
+
39
38
  PROFILES
40
39
  ${ProresProfiles.table().map(([num, name]) =>
41
40
  ` ${num}: ${name}`).join('\n')}
42
41
 
42
+ TIME FORMAT
43
+ 5.1 -- pure seconds
44
+ 20:10.0 -- 20m 10s
45
+
43
46
  EXAMPLES
44
- mediasnacks prores video.mov
47
+ mediasnacks prores --end=60 video.mov // outputs video.prores.mov
45
48
  mediasnacks prores -p2 *.mov
46
-
47
- Both output a file named: video.prores.mov
48
49
  `.trim()
49
50
 
50
51
 
51
- async function main() {
52
+ export default async function main() {
52
53
  await assertUserHasFFmpeg()
53
54
 
54
55
  const { values, files } = await parseOptions({
55
56
  profile: { short: 'p', type: 'string', default: String(ProresProfiles.default) },
56
57
  start: { short: 's', type: 'string', default: '' },
57
58
  end: { short: 'e', type: 'string', default: '' },
58
- help: { short: 'h', type: 'boolean' },
59
+ help: { short: 'h', type: 'boolean' }
59
60
  })
60
61
 
61
62
  if (values.help) {
@@ -73,10 +74,11 @@ async function main() {
73
74
  const { name, dir } = parse(video)
74
75
  const output = join(dir, `${name}.prores.mov`)
75
76
 
76
- await prores(video, values.start, values.end, values.profile, output)
77
+ const { profile, start, end } = values
78
+ await prores({ video, profile, start, end, output })
77
79
  }
78
80
 
79
- async function prores(video, start, end, profile, output) {
81
+ export async function prores({ video, profile, start, end, output }) {
80
82
  await run('ffmpeg', [
81
83
  '-v', 'error',
82
84
  '-stats',
@@ -87,9 +89,3 @@ async function prores(video, start, end, profile, output) {
87
89
  output
88
90
  ].flat())
89
91
  }
90
-
91
- if (import.meta.main)
92
- main().catch(err => {
93
- console.error(err.message || err)
94
- process.exit(1)
95
- })
package/src/qdir.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { join } from 'node:path'
4
2
  import { spawn } from 'node:child_process'
5
3
  import { readdir, writeFile, unlink, rename } from 'node:fs/promises'
@@ -30,7 +28,7 @@ function newExt(exitCode) {
30
28
  }
31
29
 
32
30
 
33
- async function main() {
31
+ export default async function main() {
34
32
  const { values, positionals } = await parseOptions({
35
33
  help: { short: 'h', type: 'boolean' }
36
34
  })
@@ -98,10 +96,3 @@ async function runShell(scriptPath) {
98
96
  function sleep(ms) {
99
97
  return new Promise(resolve => setTimeout(resolve, ms))
100
98
  }
101
-
102
-
103
- if (import.meta.main)
104
- main().catch(err => {
105
- console.error(err.message || err)
106
- process.exit(1)
107
- })
package/src/resize.js CHANGED
@@ -1,15 +1,15 @@
1
- #!/usr/bin/env node
2
1
  import { join } from 'node:path'
3
2
  import { rename } from 'node:fs/promises'
4
3
 
5
4
  import { parseOptions } from './utils/parseOptions.js'
6
5
  import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
7
- import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
6
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
7
+ import { videoAttrs } from './utils/videoAttrs.js'
8
8
 
9
9
 
10
10
  const HELP = `
11
11
  SYNOPSIS
12
- mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--output-dir=<dir>] <files>
12
+ mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--outdir=<dir>] <files>
13
13
 
14
14
  DESCRIPTION
15
15
  Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
@@ -19,7 +19,7 @@ EXAMPLES
19
19
  mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
20
20
 
21
21
  Output directory (-o)
22
- mediasnacks resize --height 240 --output-dir /tmp/out video.mov
22
+ mediasnacks resize --height 240 --outdir /tmp/out video.mov
23
23
 
24
24
  OPTIONS
25
25
  --width and --height are -2 by default:
@@ -28,13 +28,13 @@ OPTIONS
28
28
  `.trim()
29
29
 
30
30
 
31
- async function main() {
31
+ export default async function main() {
32
32
  await assertUserHasFFmpeg()
33
33
 
34
34
  const { values, files } = await parseOptions({
35
35
  width: { type: 'string', default: '-2' },
36
36
  height: { type: 'string', default: '-2' },
37
- 'output-dir': { type: 'string', default: '' },
37
+ outdir: { type: 'string', default: '' },
38
38
  overwrite: { short: 'y', type: 'boolean' },
39
39
  help: { short: 'h', type: 'boolean' },
40
40
  })
@@ -56,31 +56,33 @@ async function main() {
56
56
 
57
57
  console.log('Resizing…')
58
58
  for (const file of files)
59
- await resize({
60
- file,
61
- outFile: join(values['output-dir'], file), // TODO basename ?
62
- overwrite: values.overwrite,
63
- width,
64
- height,
65
- })
59
+ try {
60
+ await resize({
61
+ file,
62
+ outFile: join(values.outdir, file), // TODO basename ?
63
+ overwrite: values.overwrite,
64
+ width,
65
+ height,
66
+ })
67
+ console.log(file)
68
+ }
69
+ catch (err) {
70
+ console.error(err?.message || err)
71
+ }
66
72
  }
67
73
 
68
74
 
69
- async function resize({ file, outFile, overwrite, width, height }) {
75
+ export async function resize({ file, outFile, overwrite, width, height }) {
70
76
  const v = await videoAttrs(file)
71
77
  if (width === v.width && height === v.height
72
78
  || width < 0 && height === v.height
73
- || height < 0 && width === v.width) {
74
- console.log('(skipped: no changes needed)', file)
75
- return
76
- }
79
+ || height < 0 && width === v.width
80
+ )
81
+ throw new Error(`no changes needed. ${file}`)
77
82
 
78
- if (!overwrite && isFile(outFile)) {
79
- console.log('(skipped: output file exists but --overwrite=false)', file)
80
- return
81
- }
83
+ if (!overwrite && isFile(outFile))
84
+ throw new Error(`output file exists but --overwrite=false. ${file}`)
82
85
 
83
- console.log(file)
84
86
  const tmp = uniqueFilenameFor(file)
85
87
  await ffmpeg([
86
88
  '-i', file,
@@ -90,9 +92,3 @@ async function resize({ file, outFile, overwrite, width, height }) {
90
92
  ])
91
93
  await rename(tmp, outFile)
92
94
  }
93
-
94
-
95
- main().catch(err => {
96
- console.error(err.message)
97
- process.exit(1)
98
- })
package/src/seqcheck.js CHANGED
@@ -1,7 +1,8 @@
1
- #!/usr/bin/env node
2
1
  import { parseArgs } from 'node:util'
3
2
  import { readdirSync } from 'node:fs'
4
3
 
4
+ const LEFT_DELIM = '_'
5
+ const RIGHT_DELIM = '.'
5
6
 
6
7
  const HELP = `
7
8
  SYNOPSIS
@@ -11,17 +12,17 @@ DESCRIPTION
11
12
  Find missing numbered files in a sequence.
12
13
 
13
14
  OPTIONS
14
- -ld, --left-delimiter <str> Delimiter before the number (default: "_")
15
- -rd, --right-delimiter <str> Delimiter after the number (default: ".")
15
+ -ld, --left-delimiter <str> Delimiter before the number (default: "${LEFT_DELIM}")
16
+ -rd, --right-delimiter <str> Delimiter after the number (default: "${RIGHT_DELIM}")
16
17
  -h, --help
17
18
  `.trim()
18
19
 
19
20
 
20
- function main() {
21
+ export default function main() {
21
22
  const { values, positionals } = parseArgs({
22
23
  options: {
23
- 'left-delimiter': { type: 'string', default: '_' },
24
- 'right-delimiter': { type: 'string', default: '.' },
24
+ 'left-delimiter': { type: 'string', default: LEFT_DELIM },
25
+ 'right-delimiter': { type: 'string', default: RIGHT_DELIM },
25
26
  help: { short: 'h', type: 'boolean' },
26
27
  },
27
28
  allowPositionals: true,
@@ -32,18 +33,19 @@ function main() {
32
33
  return
33
34
  }
34
35
 
35
- const seq = extractSeqNums(
36
- readdirSync(positionals[0] || process.cwd()),
37
- values['left-delimiter'],
38
- values['right-delimiter'])
39
-
40
- const missing = findMissingNumbers(seq)
36
+ const dir = positionals[0] || process.cwd()
37
+ const missing = seqcheck(dir, values['left-delimiter'], values['right-delimiter'])
41
38
  if (missing.length)
42
39
  console.log('Missing:', missing)
43
40
  }
44
41
 
45
- export function extractSeqNums(names, leftDelimiter, rightDelimiter) {
46
- const pattern = new RegExp(escapeRegex(leftDelimiter) + '(\\d+)' + escapeRegex(rightDelimiter))
42
+ export function seqcheck(dir, leftDelim = LEFT_DELIM, rightDelim = RIGHT_DELIM) {
43
+ const seq = extractSeqNums(readdirSync(dir), leftDelim, rightDelim)
44
+ return findMissingNumbers(seq)
45
+ }
46
+
47
+ export function extractSeqNums(names, leftDelim, rightDelim) {
48
+ const pattern = new RegExp(escapeRegex(leftDelim) + '(\\d+)' + escapeRegex(rightDelim))
47
49
  const seq = []
48
50
  for (const name of names) {
49
51
  const match = name.match(pattern)
@@ -66,7 +68,3 @@ export function findMissingNumbers(seq) {
66
68
  function escapeRegex(str) {
67
69
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
68
70
  }
69
-
70
-
71
- if (import.meta.main)
72
- main()
package/src/sqcrop.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { join } from 'node:path'
4
2
  import { rename } from 'node:fs/promises'
5
3
 
@@ -10,18 +8,18 @@ import { parseOptions } from './utils/parseOptions.js'
10
8
 
11
9
  const HELP = `
12
10
  SYNOPSIS
13
- mediasnacks sqcrop [-y | --overwrite] [--output-dir=<dir>] <images>
11
+ mediasnacks sqcrop [-y | --overwrite] [--outdir=<dir>] <images>
14
12
 
15
13
  DESCRIPTION
16
14
  Square crops images
17
15
  `.trim()
18
16
 
19
17
 
20
- async function main() {
18
+ export default async function main() {
21
19
  await assertUserHasFFmpeg()
22
20
 
23
21
  const { values, files } = await parseOptions({
24
- 'output-dir': { type: 'string', default: '' },
22
+ outdir: { type: 'string', default: '' },
25
23
  overwrite: { short: 'y', type: 'boolean' },
26
24
  help: { short: 'h', type: 'boolean' },
27
25
  })
@@ -36,26 +34,25 @@ async function main() {
36
34
 
37
35
  console.log('Cropping…')
38
36
  for (const file of files)
39
- await sqcrop({
40
- file,
41
- outFile: join(values['output-dir'], file),
42
- overwrite: values.overwrite
43
- })
37
+ try {
38
+ await sqcrop({
39
+ file,
40
+ outFile: join(values.outdir, file),
41
+ overwrite: values.overwrite
42
+ })
43
+ console.log(file)
44
+ }
45
+ catch (err) {
46
+ console.error(err?.message || err)
47
+ }
44
48
  }
45
49
 
46
- async function sqcrop({ file, outFile, overwrite }) {
50
+ export async function sqcrop({ file, outFile, overwrite }) {
47
51
  const stOut = lstat(outFile)
48
52
 
49
- if (!overwrite && stOut?.isFile()) {
50
- console.log('(skipped: output file exists but --overwrite=false)', file)
51
- return
52
- }
53
- if (stOut?.mtimeMs > lstat(file)?.mtimeMs) {
54
- console.log('(skipped: outputFile is newer)', file)
55
- return
56
- }
53
+ if (!overwrite && stOut?.isFile()) throw new Error(`output file exists but --overwrite=false. ${file}`)
54
+ if (stOut?.mtimeMs > lstat(file)?.mtimeMs) throw new Error(`outputFile is newer. ${file}`)
57
55
 
58
- console.log(file)
59
56
  const tmp = uniqueFilenameFor(file)
60
57
  await ffmpeg([
61
58
  '-v', 'error',
@@ -66,8 +63,3 @@ async function sqcrop({ file, outFile, overwrite }) {
66
63
  ])
67
64
  await rename(tmp, outFile)
68
65
  }
69
-
70
- main().catch(err => {
71
- console.error(err.message)
72
- process.exit(1)
73
- })
package/src/ssim.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { ffmpeg } from './utils/subprocess.js'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
 
@@ -12,7 +11,7 @@ DESCRIPTION
12
11
  `.trim()
13
12
 
14
13
 
15
- async function main() {
14
+ export default async function main() {
16
15
  const { values, positionals } = await parseOptions({
17
16
  help: { short: 'h', type: 'boolean' }
18
17
  })
@@ -41,10 +40,3 @@ export async function ssim(img1, img2) {
41
40
  throw new Error(`Could not parse SSIM output:\n${stderr}`)
42
41
  return parseFloat(match[1])
43
42
  }
44
-
45
-
46
- if (import.meta.main)
47
- main().catch(err => {
48
- console.error(err.message)
49
- process.exit(1)
50
- })
@@ -6,6 +6,7 @@ const glob = promisify(_glob)
6
6
 
7
7
  export async function parseOptions(options = {}, config = {}) {
8
8
  const { values, positionals, tokens } = parseArgs({
9
+ args: process.argv.slice(3),
9
10
  allowPositionals: true,
10
11
  options,
11
12
  ...config,
@@ -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
-