49 parser = argparse.ArgumentParser(description=
"Capture video from a file or webcam and encode it with MXWrite using timestamps.")
51 source = parser.add_mutually_exclusive_group(required=
True)
52 source.add_argument(
"--input", type=Path, help=
"Input video file.")
53 source.add_argument(
"--camera", type=int, help=
"Webcam device index, for example --camera 0.")
55 parser.add_argument(
"--output", type=Path, default=Path(
"mxwrite-output.mp4"))
56 parser.add_argument(
"--width", type=int, default=1280)
57 parser.add_argument(
"--height", type=int, default=720)
58 parser.add_argument(
"--fps", type=float, default=30.0)
59 parser.add_argument(
"--frames", type=int, default=0, help=
"Maximum number of frames. 0 means unlimited.")
60 parser.add_argument(
"--codec", default=
"libx264")
61 parser.add_argument(
"--preset", default=
"veryfast")
62 parser.add_argument(
"--crf", type=int, default=20)
63 parser.add_argument(
"--normalize-pts", action=
"store_true")
65 return parser.parse_args()
104 if sys.platform.startswith(
"linux"):
105 print(
"Opening camera with Linux V4L2 backend.")
106 capture = mxvk.Capture()
108 if not capture.open_camera(index, cv2.CAP_V4L2):
109 raise SystemExit(f
"Could not open camera {index} using V4L2")
111 fourcc = cv2.VideoWriter_fourcc(
"M",
"J",
"P",
"G")
113 capture.set(cv2.CAP_PROP_FOURCC, fourcc)
114 capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
115 capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
116 capture.set(cv2.CAP_PROP_FPS, fps)
118 print(
"Opening camera with the default OpenCV backend.")
119 capture = mxvk.Capture()
121 if not capture.open_camera(index):
122 raise SystemExit(f
"Could not open camera {index}")
124 capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
125 capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
126 capture.set(cv2.CAP_PROP_FPS, fps)
128 actual_width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
129 actual_height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
130 actual_fps = float(capture.get(cv2.CAP_PROP_FPS))
131 actual_fourcc = capture.get(cv2.CAP_PROP_FOURCC)
133 print(
"Camera backend: MXVK OpenCV capture")
134 print(f
"Camera format: {fourcc_to_string(actual_fourcc)}")
135 print(f
"Camera resolution: {actual_width}x{actual_height}")
136 print(f
"Camera FPS: {actual_fps:.3f}")
138 if actual_width != width
or actual_height != height:
139 print(f
"Warning: requested {width}x{height}, but camera negotiated {actual_width}x{actual_height}.")
141 if actual_fps > 0.0
and not math.isclose(actual_fps, fps, rel_tol=0.01, abs_tol=0.1):
142 print(f
"Warning: requested {fps:.3f} FPS, but camera negotiated {actual_fps:.3f} FPS.")
148 if hasattr(cv2,
"CAP_PROP_PTS"):
149 pts = capture.get(cv2.CAP_PROP_PTS)
151 if math.isfinite(pts)
and pts >= 0.0:
152 return pts / source_fps,
"PTS"
154 position_ms = capture.get(cv2.CAP_PROP_POS_MSEC)
156 if math.isfinite(position_ms)
and position_ms >= 0.0:
157 return position_ms / 1000.0,
"POS_MSEC"
159 return frame_index / source_fps,
"FRAME_INDEX"
163 encoders = mxwrite_ext.available_video_encoders()
164 matching_encoders = [encoder
for encoder
in encoders
if encoder.name == args.codec]
166 print(f
"MXWrite reported {len(encoders)} video encoder(s).")
168 if matching_encoders:
169 encoder = matching_encoders[0]
170 mode =
"hardware" if encoder.hardware
else "software"
172 print(f
"Using {encoder.name} ({encoder.long_name}, {mode}).")
174 options = mxwrite_ext.video_encoder_options(encoder.name)
176 print(f
"The encoder exposes {len(options)} configurable option(s).")
178 elif args.codec
not in {
"auto",
"software",
"cpu",
"x264",
"h264",
"hevc",
"h265",
"nvenc"}:
179 names =
", ".join(encoder.name
for encoder
in encoders[:12])
180 raise SystemExit(f
"Requested encoder '{args.codec}' is unavailable. Available encoders include: {names}")
182 encode_options = mxwrite_ext.EncodeOptions()
183 encode_options.codec = args.codec
184 encode_options.preset = args.preset
185 encode_options.crf = args.crf
186 encode_options.block_when_full =
True
188 return encode_options
285 args.output.parent.mkdir(parents=
True, exist_ok=
True)
286 is_camera = args.camera
is not None
292 source_width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
293 source_height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
294 source_fps = float(capture.get(cv2.CAP_PROP_FPS))
296 if source_width <= 0
or source_height <= 0:
297 raise SystemExit(f
"Invalid capture resolution: {source_width}x{source_height}")
299 if not math.isfinite(source_fps)
or source_fps <= 0.0:
300 source_fps = args.fps
302 output_fps = args.fps
if is_camera
else source_fps
305 print(
"Backend: MXVK OpenCV capture")
306 print(f
"Resolution: {source_width}x{source_height}")
307 print(f
"Capture FPS: {source_fps:.6f}")
308 print(f
"Output time base: 1/{output_fps:.6f}")
309 print(f
"{'Camera: ' + str(args.camera) if is_camera else 'Input: ' + str(args.input)}")
310 print(
"Press Escape to stop capture.")
313 writer = mxwrite_ext.Writer()
315 if not writer.open_ts(str(args.output), source_width, source_height, float(output_fps), encode_options):
316 raise SystemExit(f
"MXWrite could not open {args.output}")
318 window =
CaptureWindow(args, capture, is_camera, source_width, source_height, source_fps, output_fps, writer)
322 except KeyboardInterrupt:
323 print(
"\nCapture stopped.")
326 frame_count = writer.get_frame_count()
327 duration = writer.get_duration()
328 byte_count = writer.get_bytes_written()
329 file_size = args.output.stat().st_size
if args.output.is_file()
else 0
332 raise SystemExit(f
"MXWrite did not create a non-empty file at {args.output}.")
335 print(f
"PASS: wrote {frame_count} frames, {duration:.3f}s, {file_size} filesystem bytes to {args.output}")
336 print(f
"MXWrite's muxer byte counter reported {byte_count} bytes.")
338 if window.last_pts
is not None:
339 print(f
"Last video PTS: {window.last_pts} ({window.last_pts / output_fps:.6f}s)")
341 if window
is not None:
344 if writer
is not None:
__init__(self, args, capture, is_camera, source_width, source_height, source_fps, output_fps, writer)