kinemotion 0.64.0__py3-none-any.whl → 0.66.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,6 @@
1
1
  """Shared debug overlay utilities for video rendering."""
2
2
 
3
+ # pyright: reportCallIssue=false
3
4
  import os
4
5
  import shutil
5
6
  import subprocess
@@ -12,6 +13,31 @@ from typing_extensions import Self
12
13
 
13
14
  from .timing import NULL_TIMER, Timer
14
15
 
16
+ # Setup logging with structlog support for backend, fallback to standard logging for CLI
17
+ try:
18
+ import structlog
19
+
20
+ logger = structlog.get_logger(__name__)
21
+ _using_structlog = True
22
+ except ImportError:
23
+ import logging
24
+
25
+ logger = logging.getLogger(__name__)
26
+ _using_structlog = False
27
+
28
+
29
+ def _log(level: str, message: str, **kwargs: object) -> None:
30
+ """Log message with kwargs support for both structlog and standard logging."""
31
+ if _using_structlog:
32
+ getattr(logger, level)(message, **kwargs)
33
+ else:
34
+ # For standard logging, format kwargs as part of the message
35
+ if kwargs:
36
+ kwargs_str = " ".join(f"{k}={v}" for k, v in kwargs.items())
37
+ getattr(logger, level)(f"{message} {kwargs_str}")
38
+ else:
39
+ getattr(logger, level)(message)
40
+
15
41
 
16
42
  def create_video_writer(
17
43
  output_path: str,
@@ -24,6 +50,10 @@ def create_video_writer(
24
50
  """
25
51
  Create a video writer with fallback codec support.
26
52
 
53
+ ⚠️ CRITICAL: DO NOT add "vp09" (VP9) to the codec list!
54
+ VP9 is not supported on iOS browsers (iPhone/iPad) and causes playback failures.
55
+ Regression test: tests/core/test_debug_overlay_utils.py::test_vp09_codec_never_in_codec_list
56
+
27
57
  Args:
28
58
  output_path: Path for output video
29
59
  width: Encoded frame width (from source video)
@@ -38,13 +68,15 @@ def create_video_writer(
38
68
  needs_resize = (display_width != width) or (display_height != height)
39
69
 
40
70
  # Try browser-compatible codecs first
41
- # avc1/h264: H.264 (Most compatible)
42
- # vp09: VP9 (Good compatibility)
43
- # mp4v: MPEG-4 (Poor browser support, last resort)
44
- codecs_to_try = ["avc1", "h264", "vp09", "mp4v"]
71
+ # avc1/h264: H.264 (Most compatible, including iOS)
72
+ # mp4v: MPEG-4 (Poor browser support, will trigger ffmpeg re-encoding for H.264)
73
+ # ⚠️ CRITICAL: VP9 (vp09) is EXCLUDED - not supported on iOS/iPhone/iPad browsers!
74
+ # Adding VP9 will break debug video playback on all iOS devices.
75
+ codecs_to_try = ["avc1", "h264", "mp4v"]
45
76
 
46
77
  writer = None
47
78
  used_codec = "mp4v" # Default fallback
79
+ codec_attempt_log = []
48
80
 
49
81
  for codec in codecs_to_try:
50
82
  try:
@@ -52,13 +84,36 @@ def create_video_writer(
52
84
  writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
53
85
  if writer.isOpened():
54
86
  used_codec = codec
87
+ codec_attempt_log.append({"codec": codec, "status": "success"})
88
+ _log(
89
+ "info",
90
+ "debug_video_codec_selected",
91
+ codec=codec,
92
+ width=display_width,
93
+ height=display_height,
94
+ fps=fps,
95
+ )
55
96
  if codec == "mp4v":
56
- print(f"Warning: Fallback to {codec} codec. Video may not play in browsers.")
97
+ msg = (
98
+ "Using fallback MPEG-4 codec; will re-encode with ffmpeg for "
99
+ "browser compatibility"
100
+ )
101
+ _log("warning", "debug_video_fallback_codec", codec="mp4v", message=msg)
57
102
  break
58
- except Exception:
103
+ except Exception as e:
104
+ codec_attempt_log.append({"codec": codec, "status": "failed", "error": str(e)})
105
+ _log("info", "debug_video_codec_attempt_failed", codec=codec, error=str(e))
59
106
  continue
60
107
 
61
108
  if writer is None or not writer.isOpened():
109
+ _log(
110
+ "error",
111
+ "debug_video_writer_creation_failed",
112
+ output_path=output_path,
113
+ dimensions=f"{display_width}x{display_height}",
114
+ fps=fps,
115
+ codec_attempts=codec_attempt_log,
116
+ )
62
117
  raise ValueError(
63
118
  f"Failed to create video writer for {output_path} with dimensions "
64
119
  f"{display_width}x{display_height}"
@@ -130,9 +185,35 @@ class BaseDebugOverlayRenderer:
130
185
  # Ensure dimensions are even for codec compatibility
131
186
  self.display_width = int(display_width * scale) // 2 * 2
132
187
  self.display_height = int(display_height * scale) // 2 * 2
188
+ _log(
189
+ "info",
190
+ "debug_video_resolution_optimized",
191
+ original_width=display_width,
192
+ original_height=display_height,
193
+ optimized_width=self.display_width,
194
+ optimized_height=self.display_height,
195
+ scale_factor=round(scale, 2),
196
+ )
133
197
  else:
134
198
  self.display_width = display_width
135
199
  self.display_height = display_height
200
+ _log(
201
+ "info",
202
+ "debug_video_resolution_native",
203
+ width=self.display_width,
204
+ height=self.display_height,
205
+ )
206
+
207
+ _log(
208
+ "info",
209
+ "debug_overlay_renderer_initialized",
210
+ output_path=output_path,
211
+ source_width=width,
212
+ source_height=height,
213
+ output_width=self.display_width,
214
+ output_height=self.display_height,
215
+ fps=fps,
216
+ )
136
217
 
137
218
  # Duration of ffmpeg re-encoding (0.0 if not needed)
138
219
  self.reencode_duration_s = 0.0
@@ -174,6 +255,12 @@ class BaseDebugOverlayRenderer:
174
255
  def close(self) -> None:
175
256
  """Release video writer and re-encode if possible."""
176
257
  self.writer.release()
258
+ _log(
259
+ "info",
260
+ "debug_video_writer_released",
261
+ output_path=self.output_path,
262
+ codec=self.used_codec,
263
+ )
177
264
 
178
265
  # Post-process with ffmpeg ONLY if we fell back to the incompatible mp4v codec
179
266
  if self.used_codec == "mp4v" and shutil.which("ffmpeg"):
@@ -187,7 +274,7 @@ class BaseDebugOverlayRenderer:
187
274
  # -y: Overwrite output file
188
275
  # -vcodec libx264: Use H.264 codec
189
276
  # -pix_fmt yuv420p: Required for wide browser support (Chrome,
190
- # Safari, Firefox)
277
+ # Safari, Firefox, iOS)
191
278
  # -preset fast: Reasonable speed/compression tradeoff
192
279
  # -crf 23: Standard quality
193
280
  # -an: Remove audio (debug video has no audio)
@@ -208,6 +295,16 @@ class BaseDebugOverlayRenderer:
208
295
  temp_path,
209
296
  ]
210
297
 
298
+ _log(
299
+ "info",
300
+ "debug_video_ffmpeg_reencoding_start",
301
+ input_file=self.output_path,
302
+ output_file=temp_path,
303
+ output_codec="libx264",
304
+ pixel_format="yuv420p",
305
+ reason="iOS_compatibility",
306
+ )
307
+
211
308
  # Suppress output unless error
212
309
  reencode_start = time.time()
213
310
  subprocess.run(
@@ -217,19 +314,54 @@ class BaseDebugOverlayRenderer:
217
314
  stderr=subprocess.PIPE,
218
315
  )
219
316
  self.reencode_duration_s = time.time() - reencode_start
220
- print(f"Debug video re-encoded in {self.reencode_duration_s:.2f}s")
317
+
318
+ _log(
319
+ "info",
320
+ "debug_video_ffmpeg_reencoding_complete",
321
+ duration_ms=round(self.reencode_duration_s * 1000, 1),
322
+ )
221
323
 
222
324
  # Overwrite original file
223
325
  os.replace(temp_path, self.output_path)
326
+ _log(
327
+ "info",
328
+ "debug_video_reencoded_file_replaced",
329
+ output_path=self.output_path,
330
+ final_codec="libx264",
331
+ pixel_format="yuv420p",
332
+ )
224
333
 
225
334
  except subprocess.CalledProcessError as e:
226
- print(f"Warning: Failed to re-encode debug video with ffmpeg: {e}")
335
+ stderr_msg = e.stderr.decode("utf-8", errors="ignore") if e.stderr else "N/A"
336
+ _log(
337
+ "warning",
338
+ "debug_video_ffmpeg_reencoding_failed",
339
+ error=str(e),
340
+ stderr=stderr_msg,
341
+ )
227
342
  if temp_path and os.path.exists(temp_path):
228
343
  os.remove(temp_path)
344
+ _log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
229
345
  except Exception as e:
230
- print(f"Warning: Error during video post-processing: {e}")
346
+ _log("warning", "debug_video_post_processing_error", error=str(e))
231
347
  if temp_path and os.path.exists(temp_path):
232
348
  os.remove(temp_path)
349
+ _log("info", "debug_video_temp_file_cleaned_up", temp_file=temp_path)
350
+ elif self.used_codec == "mp4v" and not shutil.which("ffmpeg"):
351
+ _log(
352
+ "warning",
353
+ "debug_video_ffmpeg_not_available",
354
+ codec=self.used_codec,
355
+ output_path=self.output_path,
356
+ warning="Video may not play in all browsers",
357
+ )
358
+ else:
359
+ _log(
360
+ "info",
361
+ "debug_video_ready_for_playback",
362
+ codec=self.used_codec,
363
+ path=self.output_path,
364
+ )
233
365
 
234
366
  def __enter__(self) -> Self:
235
367
  return self
@@ -203,6 +203,17 @@ class VideoProcessor:
203
203
  """Release video capture."""
204
204
  self.cap.release()
205
205
 
206
+ def __iter__(self) -> "VideoProcessor":
207
+ """Make the processor iterable."""
208
+ return self
209
+
210
+ def __next__(self) -> np.ndarray:
211
+ """Get the next frame during iteration."""
212
+ frame = self.read_frame()
213
+ if frame is None:
214
+ raise StopIteration
215
+ return frame
216
+
206
217
  def __enter__(self) -> "VideoProcessor":
207
218
  return self
208
219
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.64.0
3
+ Version: 0.66.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
@@ -13,7 +13,7 @@ kinemotion/cmj/validation_bounds.py,sha256=Ry915JdInPXbqjaVGNY_urnDO1PAkCSJqHwNK
13
13
  kinemotion/core/__init__.py,sha256=U2fnLUGXQ0jbwpXhdksYKDXbeQndEHjn9gwTAEJ9Av0,1451
14
14
  kinemotion/core/auto_tuning.py,sha256=lhAqPc-eLjMYx9BCvKdECE7TD2Dweb9KcifV6JHaXOE,11278
15
15
  kinemotion/core/cli_utils.py,sha256=sQPbT6XWWau-sm9yuN5c3eS5xNzoQGGXwSz6hQXtRvM,1859
16
- kinemotion/core/debug_overlay_utils.py,sha256=Ne8GqEWUySwwtkmAzbBs527mh2QsBW2hbgV9CbdJjxE,8441
16
+ kinemotion/core/debug_overlay_utils.py,sha256=Rbmuslc-imondbljiMrQ08JRxaw9TfyFi75nA17sZ9w,13390
17
17
  kinemotion/core/determinism.py,sha256=Frw-KAOvAxTL_XtxoWpXCjMbQPUKEAusK6JctlkeuRo,2509
18
18
  kinemotion/core/experimental.py,sha256=IK05AF4aZS15ke85hF3TWCqRIXU1AlD_XKzFz735Ua8,3640
19
19
  kinemotion/core/filtering.py,sha256=Oc__pV6iHEGyyovbqa5SUi-6v8QyvaRVwA0LRayM884,11355
@@ -26,7 +26,7 @@ kinemotion/core/smoothing.py,sha256=ELMHL7pzSqYffjnLDBUMBJIgt1AwOssDInE8IiXBbig,
26
26
  kinemotion/core/timing.py,sha256=d1rjZc07Nbi5Jrio9AC-zeS0dNAlbPyNIydLz7X75Pk,7804
27
27
  kinemotion/core/types.py,sha256=A_HclzKpf3By5DiJ0wY9B-dQJrIVAAhUfGab7qTSIL8,1279
28
28
  kinemotion/core/validation.py,sha256=0xVv-ftWveV60fJ97kmZMuy2Qqqb5aZLR50dDIrjnhg,6773
29
- kinemotion/core/video_io.py,sha256=vCwpWnlW2y29l48dFXokdehQn42w_IQvayxbVTjpXqQ,7863
29
+ kinemotion/core/video_io.py,sha256=ufbwfYkxuerat4ab6Hn1tkI5T9TAOvHwREIazJ0Q2tE,8174
30
30
  kinemotion/dropjump/__init__.py,sha256=tC3H3BrCg8Oj-db-Vrtx4PH_llR1Ppkd5jwaOjhQcLg,862
31
31
  kinemotion/dropjump/analysis.py,sha256=YomuoJF_peyrBSpeT89Q5_sBgY0kEDyq7TFrtEnRLjs,28049
32
32
  kinemotion/dropjump/api.py,sha256=uidio49CXisyWKd287CnCrM51GusG9DWAIUKGH85fpM,20584
@@ -36,8 +36,8 @@ kinemotion/dropjump/kinematics.py,sha256=dx4PuXKfKMKcsc_HX6sXj8rHXf9ksiZIOAIkJ4v
36
36
  kinemotion/dropjump/metrics_validator.py,sha256=lSfo4Lm5FHccl8ijUP6SA-kcSh50LS9hF8UIyWxcnW8,9243
37
37
  kinemotion/dropjump/validation_bounds.py,sha256=x4yjcFxyvdMp5e7MkcoUosGLeGsxBh1Lft6h__AQ2G8,5124
38
38
  kinemotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- kinemotion-0.64.0.dist-info/METADATA,sha256=yyIEMoc-ce13yauhkU6v4lYI303kW4iACckeBDNhHvo,26061
40
- kinemotion-0.64.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
- kinemotion-0.64.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
42
- kinemotion-0.64.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
43
- kinemotion-0.64.0.dist-info/RECORD,,
39
+ kinemotion-0.66.0.dist-info/METADATA,sha256=-fRqQu9C9z3U5xlHHgDeTwIeURdrbv9Kyt1e5hoMrUA,26061
40
+ kinemotion-0.66.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
41
+ kinemotion-0.66.0.dist-info/entry_points.txt,sha256=zaqnAnjLvcdrk1Qvj5nvXZCZ2gp0prS7it1zTJygcIY,50
42
+ kinemotion-0.66.0.dist-info/licenses/LICENSE,sha256=KZajvqsHw0NoOHOi2q0FZ4NBe9HdV6oey-IPYAtHXfg,1088
43
+ kinemotion-0.66.0.dist-info/RECORD,,