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

1#!/usr/bin/env python3 

2 

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 

20 

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 

25 

26 # ************************************************************************** 

27 

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" 

35 

36 # ************************************************************************** 

37 

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 

41 

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] 

72 

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] 

77 

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 ) 

159 

160 # Loop over lines ... 

161 for line in resp.stdout.splitlines(): 

162 # Skip irrelevant lines ... 

163 if not line.startswith("[Parsed_cropdetect"): 

164 continue 

165 

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 

178 

179 # Update variables ... 

180 outX = max(outX, int(db["x"])) # [px] 

181 outY = max(outY, int(db["y"])) # [px] 

182 

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}" 

187 

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 

193 

194 # Return top-left corner, width, height and FFMPEG crop parameter string ... 

195 return outX, outY, outW, outH, cropParams