kinemotion 0.39.0__py3-none-any.whl → 0.40.0__py3-none-any.whl

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.

Potentially problematic release.


This version of kinemotion might be problematic. Click here for more details.

@@ -1,5 +1,11 @@
1
1
  """Shared debug overlay utilities for video rendering."""
2
2
 
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+
3
9
  import cv2
4
10
  import numpy as np
5
11
 
@@ -11,7 +17,7 @@ def create_video_writer(
11
17
  display_width: int,
12
18
  display_height: int,
13
19
  fps: float,
14
- ) -> tuple[cv2.VideoWriter, bool]:
20
+ ) -> tuple[cv2.VideoWriter, bool, str]:
15
21
  """
16
22
  Create a video writer with fallback codec support.
17
23
 
@@ -24,28 +30,43 @@ def create_video_writer(
24
30
  fps: Frames per second
25
31
 
26
32
  Returns:
27
- Tuple of (video_writer, needs_resize)
33
+ Tuple of (video_writer, needs_resize, used_codec)
28
34
  """
29
35
  needs_resize = (display_width != width) or (display_height != height)
30
36
 
31
- # Try H.264 codec first (better quality/compatibility), fallback to mp4v
32
- fourcc = cv2.VideoWriter_fourcc(*"avc1")
33
- writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
37
+ # Try browser-compatible codecs first
38
+ # avc1/h264: H.264 (Most compatible)
39
+ # vp09: VP9 (Good compatibility)
40
+ # mp4v: MPEG-4 (Poor browser support, last resort)
41
+ codecs_to_try = ["avc1", "h264", "vp09", "mp4v"]
34
42
 
35
- # Check if writer opened successfully, fallback to mp4v if not
36
- if not writer.isOpened():
37
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
38
- writer = cv2.VideoWriter(
39
- output_path, fourcc, fps, (display_width, display_height)
40
- )
43
+ writer = None
44
+ used_codec = "mp4v" # Default fallback
41
45
 
42
- if not writer.isOpened():
46
+ for codec in codecs_to_try:
47
+ try:
48
+ fourcc = cv2.VideoWriter_fourcc(*codec)
49
+ writer = cv2.VideoWriter(
50
+ output_path, fourcc, fps, (display_width, display_height)
51
+ )
52
+ if writer.isOpened():
53
+ used_codec = codec
54
+ if codec == "mp4v":
55
+ print(
56
+ f"Warning: Fallback to {codec} codec. "
57
+ "Video may not play in browsers."
58
+ )
59
+ break
60
+ except Exception:
61
+ continue
62
+
63
+ if writer is None or not writer.isOpened():
43
64
  raise ValueError(
44
65
  f"Failed to create video writer for {output_path} with dimensions "
45
66
  f"{display_width}x{display_height}"
46
67
  )
47
68
 
48
- return writer, needs_resize
69
+ return writer, needs_resize, used_codec
49
70
 
50
71
 
51
72
  def write_overlay_frame(
@@ -95,11 +116,12 @@ class BaseDebugOverlayRenderer:
95
116
  display_height: Display height (considering SAR)
96
117
  fps: Frames per second
97
118
  """
119
+ self.output_path = output_path
98
120
  self.width = width
99
121
  self.height = height
100
122
  self.display_width = display_width
101
123
  self.display_height = display_height
102
- self.writer, self.needs_resize = create_video_writer(
124
+ self.writer, self.needs_resize, self.used_codec = create_video_writer(
103
125
  output_path, width, height, display_width, display_height, fps
104
126
  )
105
127
 
@@ -133,9 +155,67 @@ class BaseDebugOverlayRenderer:
133
155
  write_overlay_frame(self.writer, frame, self.display_width, self.display_height)
134
156
 
135
157
  def close(self) -> None:
136
- """Release video writer."""
158
+ """Release video writer and re-encode if possible."""
137
159
  self.writer.release()
138
160
 
161
+ # Post-process with ffmpeg ONLY if we fell back to the incompatible mp4v codec
162
+ if self.used_codec == "mp4v" and shutil.which("ffmpeg"):
163
+ temp_path = None
164
+ try:
165
+ temp_path = str(
166
+ Path(self.output_path).with_suffix(
167
+ ".temp" + Path(self.output_path).suffix
168
+ )
169
+ )
170
+
171
+ # Convert to H.264 with yuv420p pixel format for browser compatibility
172
+ # -y: Overwrite output file
173
+ # -vcodec libx264: Use H.264 codec
174
+ # -pix_fmt yuv420p: Required for wide browser support (Chrome,
175
+ # Safari, Firefox)
176
+ # -preset fast: Reasonable speed/compression tradeoff
177
+ # -crf 23: Standard quality
178
+ # -an: Remove audio (debug video has no audio)
179
+ cmd = [
180
+ "ffmpeg",
181
+ "-y",
182
+ "-i",
183
+ self.output_path,
184
+ "-vcodec",
185
+ "libx264",
186
+ "-pix_fmt",
187
+ "yuv420p",
188
+ "-preset",
189
+ "fast",
190
+ "-crf",
191
+ "23",
192
+ "-an",
193
+ temp_path,
194
+ ]
195
+
196
+ # Suppress output unless error
197
+ reencode_start = time.time()
198
+ subprocess.run(
199
+ cmd,
200
+ check=True,
201
+ stdout=subprocess.DEVNULL,
202
+ stderr=subprocess.PIPE,
203
+ )
204
+ reencode_duration = time.time() - reencode_start
205
+ print(f"Debug video re-encoded in {reencode_duration:.2f}s")
206
+
207
+ # Overwrite original file
208
+ os.replace(temp_path, self.output_path)
209
+
210
+ except subprocess.CalledProcessError as e:
211
+ print(f"Warning: Failed to re-encode debug video with ffmpeg: {e}")
212
+ if temp_path and os.path.exists(temp_path):
213
+ os.remove(temp_path)
214
+ except Exception as e:
215
+ print(f"Warning: Error during video post-processing: {e}")
216
+ if temp_path and os.path.exists(temp_path):
217
+ os.remove(temp_path)
218
+
139
219
  def __enter__(self) -> "BaseDebugOverlayRenderer":
140
220
  return self
141
221
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.39.0
3
+ Version: 0.40.0
4
4
  Summary: Video-based kinematic analysis for athletic performance
5
5
  Project-URL: Homepage, https://github.com/feniix/kinemotion
6
6
  Project-URL: Repository, https://github.com/feniix/kinemotion
@@ -12,7 +12,7 @@ kinemotion/cmj/validation_bounds.py,sha256=9ZTo68fl3ooyWjXXyTMRLpK9tFANa_rQf3oHh
12
12
  kinemotion/core/__init__.py,sha256=HsqolRa60cW3vrG8F9Lvr9WvWcs5hCmsTzSgo7imi-4,1278
13
13
  kinemotion/core/auto_tuning.py,sha256=wtCUMOhBChVJNXfEeku3GCMW4qED6MF-O_mv2sPTiVQ,11324
14
14
  kinemotion/core/cli_utils.py,sha256=zbnifPhD-OYofJioeYfJtshuWcl8OAEWtqCGVF4ctAI,7966
15
- kinemotion/core/debug_overlay_utils.py,sha256=TyUb5okv5qw8oeaX3jsUO_kpwf1NnaHEAOTm-8LwTno,4587
15
+ kinemotion/core/debug_overlay_utils.py,sha256=-HRHw5Ew7hVH0MWwdutZV1JwPqQwzQ6Jex7xHWc3q88,7511
16
16
  kinemotion/core/determinism.py,sha256=NwVrHqJiVxxFHTBPVy8aDBJH2SLIcYIpdGFp7glblB8,2515
17
17
  kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
18
18
  kinemotion/core/filtering.py,sha256=GsC9BB71V07LJJHgS2lsaxUAtJsupcUiwtZFDgODh8c,11417
@@ -31,8 +31,8 @@ kinemotion/dropjump/kinematics.py,sha256=kH-XM66wlOCYMpjvyb6_Qh5ZebyOfFZ47rmhgE1
31
31
  kinemotion/dropjump/metrics_validator.py,sha256=CrTlGup8q2kyPXtA6HNwm7_yq0AsBaDllG7RVZdXmYA,9342
32
32
  kinemotion/dropjump/validation_bounds.py,sha256=5b4I3CKPybuvrbn-nP5yCcGF_sH4Vtyw3a5AWWvWnBk,4645
33
33
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- kinemotion-0.39.0.dist-info/METADATA,sha256=ytvO76fvfntV_Vpjkc-4BezahvOO91a2I6X3PUM0eAk,26020
35
- kinemotion-0.39.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
- kinemotion-0.39.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
37
- kinemotion-0.39.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
38
- kinemotion-0.39.0.dist-info/RECORD,,
34
+ kinemotion-0.40.0.dist-info/METADATA,sha256=0X_uEEzEi-DH9d4HwfseQVEzlDmbJRw96Dn1o-m5DbI,26020
35
+ kinemotion-0.40.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ kinemotion-0.40.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
37
+ kinemotion-0.40.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
38
+ kinemotion-0.40.0.dist-info/RECORD,,