Coverage for pyguymer3/media/return_video_crop_parameters.py: 86%
49 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 return_video_crop_parameters(
5 fname,
6 /,
7 *,
8 cwd = None,
9 debug = __debug__,
10 dt = 2.0,
11 ensureNFC = True,
12 ffmpegPath = None,
13 ffprobePath = None,
14 playlist = -1,
15 timeout = 60.0,
16):
17 # Import standard modules ...
18 import shutil
19 import subprocess
21 # Import sub-functions ...
22 from .return_media_duration import return_media_duration
23 from .return_video_height import return_video_height
24 from .return_video_width import return_video_width
26 # **************************************************************************
28 # Try to find the paths if the user did not provide them ...
29 if ffmpegPath is None:
30 ffmpegPath = shutil.which("ffmpeg")
31 if ffprobePath is None:
32 ffprobePath = shutil.which("ffprobe")
33 assert ffmpegPath is not None, "\"ffmpeg\" is not installed"
34 assert ffprobePath is not None, "\"ffprobe\" is not installed"
36 # **************************************************************************
38 # Check input ...
39 if fname.startswith("bluray:") and playlist < 0:
40 raise Exception("a Blu-ray was specified but no playlist was supplied") from None
42 # Initialize variables ...
43 dur = return_media_duration(
44 fname,
45 cwd = cwd,
46 debug = debug,
47 ensureNFC = ensureNFC,
48 ffprobePath = ffprobePath,
49 playlist = playlist,
50 timeout = timeout,
51 ) # [s]
52 inW = return_video_width(
53 fname,
54 cwd = cwd,
55 debug = debug,
56 ensureNFC = ensureNFC,
57 ffprobePath = ffprobePath,
58 playlist = playlist,
59 timeout = timeout,
60 ) # [px]
61 inH = return_video_height(
62 fname,
63 cwd = cwd,
64 debug = debug,
65 ensureNFC = ensureNFC,
66 ffprobePath = ffprobePath,
67 playlist = playlist,
68 timeout = timeout,
69 ) # [px]
70 outX = 0 # [px]
71 outY = 0 # [px]
73 # Loop over fractions ...
74 for frac in [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]:
75 # Deduce start time ...
76 t = frac * dur - dt / 2.0 # [s]
78 # Check if it is a Blu-ray ...
79 if fname.startswith("bluray:"):
80 # Find crop parameters ...
81 resp = subprocess.run(
82 [
83 ffmpegPath,
84 "-hide_banner",
85 "-probesize", "1G",
86 "-analyzeduration", "1800M",
87 "-playlist", f"{playlist:d}",
88 "-ss", f"{t:.3f}",
89 "-i", fname,
90 "-an",
91 "-sn",
92 "-t", f"{dt:f}",
93 "-vf", "cropdetect",
94 "-y",
95 "-f", "null",
96 "/dev/null",
97 ],
98 check = True,
99 cwd = cwd,
100 encoding = "utf-8",
101 stderr = subprocess.STDOUT,
102 stdout = subprocess.PIPE,
103 timeout = None,
104 )
105 else:
106 # Attempt to survey the file ...
107 try:
108 # Find crop parameters ...
109 resp = subprocess.run(
110 [
111 ffmpegPath,
112 "-hide_banner",
113 "-probesize", "1G",
114 "-analyzeduration", "1800M",
115 "-ss", f"{t:.3f}",
116 "-i", fname,
117 "-an",
118 "-sn",
119 "-t", f"{dt:f}",
120 "-vf", "cropdetect",
121 "-y",
122 "-f", "null",
123 "/dev/null",
124 ],
125 check = True,
126 cwd = cwd,
127 encoding = "utf-8",
128 stderr = subprocess.STDOUT,
129 stdout = subprocess.PIPE,
130 timeout = None,
131 )
132 except subprocess.CalledProcessError:
133 # Fallback and attempt to find crop parameters as a raw M-JPEG
134 # stream ...
135 resp = subprocess.run(
136 [
137 ffmpegPath,
138 "-hide_banner",
139 "-probesize", "1G",
140 "-analyzeduration", "1800M",
141 "-ss", f"{t:.3f}",
142 "-f", "mjpeg",
143 "-i", fname,
144 "-an",
145 "-sn",
146 "-t", f"{dt:f}",
147 "-vf", "cropdetect",
148 "-y",
149 "-f", "null",
150 "/dev/null",
151 ],
152 check = True,
153 cwd = cwd,
154 encoding = "utf-8",
155 stderr = subprocess.STDOUT,
156 stdout = subprocess.PIPE,
157 timeout = None,
158 )
160 # Loop over lines ...
161 for line in resp.stdout.splitlines():
162 # Skip irrelevant lines ...
163 if not line.startswith("[Parsed_cropdetect"):
164 continue
166 # Extract the information part of the line and make a dictionary of
167 # all of the key+value pairs ...
168 db = {}
169 info = line.strip().split("]")[-1]
170 for keyvalue in info.strip().split():
171 if keyvalue.count(":") == 1:
172 key, value = keyvalue.split(":")
173 elif keyvalue.count("=") == 1:
174 key, value = keyvalue.split("=")
175 else:
176 raise Exception(f"an unexpected string format was encountered (\"{keyvalue}\")") from None
177 db[key] = value
179 # Update variables ...
180 outX = max(outX, int(db["x"])) # [px]
181 outY = max(outY, int(db["y"])) # [px]
183 # Update variables ...
184 outW = inW - 2 * outX # [px]
185 outH = inH - 2 * outY # [px]
186 cropParams = f"{outW:d}:{outH:d}:{outX:d}:{outY:d}"
188 # Check results ...
189 if outW > inW or outW <= 0:
190 raise Exception(f"failed to find cropped width (inW = {inW:d}, inH = {inH:d}, outX = {outX:d}, outY = {outY:d}, outW = {outW:d}, outH = {outH:d})") from None
191 if outH > inH or outH <= 0:
192 raise Exception(f"failed to find cropped height (inW = {inW:d}, inH = {inH:d}, outX = {outX:d}, outY = {outY:d}, outW = {outW:d}, outH = {outH:d})") from None
194 # Return top-left corner, width, height and FFMPEG crop parameter string ...
195 return outX, outY, outW, outH, cropParams