Coverage for pyguymer3/media/ffprobe.py: 68%
19 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def ffprobe(
5 fname,
6 /,
7 *,
8 cwd = None,
9 ensureNFC = True,
10 ffprobePath = None,
11 playlist = -1,
12 timeout = 60.0,
13):
14 """
15 Run "ffprobe" on a file and return the format and stream information.
17 Parameters
18 ----------
19 fname : str
20 the file to be surveyed
21 cwd : str, optional
22 the child working directory
23 ensureNFC : bool, optional
24 ensure that the Unicode encoding is NFC
25 ffprobePath : str, optional
26 the path to the "ffprobe" binary (if not provided then Python will
27 attempt to find the binary itself)
28 playlist : int, optional
29 the playlist within the Blu-ray folder structure to be surveyed
30 timeout : float, optional
31 the timeout for any requests/subprocess calls
33 Returns
34 -------
35 ans : dict
36 the format and stream information
38 Notes
39 -----
40 Copyright 2017 Thomas Guymer [1]_
42 References
43 ----------
44 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
45 """
47 # Import standard modules ...
48 import json
49 import shutil
50 import subprocess
51 import unicodedata
53 # **************************************************************************
55 # Try to find the paths if the user did not provide them ...
56 if ffprobePath is None:
57 ffprobePath = shutil.which("ffprobe")
58 assert ffprobePath is not None, "\"ffprobe\" is not installed"
60 # Check input ...
61 if fname.startswith("bluray:") and playlist < 0:
62 raise Exception("a Blu-ray was specified but no playlist was supplied") from None
64 # Check if it is a Blu-ray ...
65 if fname.startswith("bluray:"):
66 # Find stream info ...
67 # NOTE: Sometimes "ffprobe" appears to work fine but even with
68 # "-loglevel quiet" it sometimes outputs things like:
69 # disc.c:424: error opening file CERTIFICATE/id.bdmv
70 # disc.c:424: error opening file CERTIFICATE/BACKUP/id.bdmv
71 # bluray.c:255: 00008.m2ts: no timestamp for SPN 0 (got 0). clip 90000-7467995.
72 # ... to standard error, hence I have to only attempt to parse
73 # standard out as JSON rather than both standard error and
74 # standard out together.
75 resp = subprocess.run(
76 [
77 ffprobePath,
78 "-hide_banner",
79 "-loglevel", "quiet",
80 "-probesize", "1G",
81 "-analyzeduration", "1800M",
82 "-print_format", "json",
83 "-show_format",
84 "-show_streams",
85 "-playlist", f"{playlist:d}",
86 fname,
87 ],
88 check = True,
89 cwd = cwd,
90 encoding = "utf-8",
91 stderr = subprocess.DEVNULL,
92 stdout = subprocess.PIPE,
93 timeout = timeout,
94 )
95 else:
96 # Attempt to survey the file ...
97 try:
98 # Find stream info ...
99 # NOTE: Sometimes "ffprobe" appears to work fine but even with
100 # "-loglevel quiet" it sometimes outputs things like:
101 # disc.c:424: error opening file CERTIFICATE/id.bdmv
102 # disc.c:424: error opening file CERTIFICATE/BACKUP/id.bdmv
103 # bluray.c:255: 00008.m2ts: no timestamp for SPN 0 (got 0). clip 90000-7467995.
104 # ... to standard error, hence I have to only attempt to parse
105 # standard out as JSON rather than both standard error and
106 # standard out together.
107 resp = subprocess.run(
108 [
109 ffprobePath,
110 "-hide_banner",
111 "-loglevel", "quiet",
112 "-probesize", "1G",
113 "-analyzeduration", "1800M",
114 "-print_format", "json",
115 "-show_format",
116 "-show_streams",
117 fname,
118 ],
119 check = True,
120 cwd = cwd,
121 encoding = "utf-8",
122 stderr = subprocess.DEVNULL,
123 stdout = subprocess.PIPE,
124 timeout = timeout,
125 )
126 except subprocess.CalledProcessError:
127 # Fallback and attempt to find stream info as a raw M-JPEG stream ...
128 # NOTE: Sometimes "ffprobe" appears to work fine but even with
129 # "-loglevel quiet" it sometimes outputs things like:
130 # disc.c:424: error opening file CERTIFICATE/id.bdmv
131 # disc.c:424: error opening file CERTIFICATE/BACKUP/id.bdmv
132 # bluray.c:255: 00008.m2ts: no timestamp for SPN 0 (got 0). clip 90000-7467995.
133 # ... to standard error, hence I have to only attempt to parse
134 # standard out as JSON rather than both standard error and
135 # standard out together.
136 resp = subprocess.run(
137 [
138 ffprobePath,
139 "-hide_banner",
140 "-loglevel", "quiet",
141 "-probesize", "1G",
142 "-analyzeduration", "1800M",
143 "-print_format", "json",
144 "-show_format",
145 "-show_streams",
146 "-f", "mjpeg",
147 fname,
148 ],
149 check = True,
150 cwd = cwd,
151 encoding = "utf-8",
152 stderr = subprocess.DEVNULL,
153 stdout = subprocess.PIPE,
154 timeout = timeout,
155 )
157 # Return ffprobe output as dictionary ...
158 if ensureNFC and not unicodedata.is_normalized("NFC", resp.stdout):
159 return json.loads(unicodedata.normalize("NFC", resp.stdout))
160 return json.loads(resp.stdout)