I think netthinktank wrote this originally and I made some small modifications to it.
It worked enough for me to prove that I couldn't get my pair of Logitech BRIO webcams arranged in a way to make real VR recording happen.
| import cv2 | |
| import time | |
| import logging | |
| logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s") | |
| logger = logging.getLogger(__name__) | |
| def main(): | |
| logger.info("Initialize cameras with higher priority for VR") | |
| cap_left = cv2.VideoCapture(0, cv2.CAP_ANY) | |
| cap_right = cv2.VideoCapture(1, cv2.CAP_ANY) | |
| logger.info("Set desired frame rate and resolution") | |
| fps = 30 # Higher FPS gives smoother VR experience | |
| frame_width = 3840 // 2 # Split 4K into 1920 per eye | |
| frame_height = 2160 | |
| logger.debug(f"{frame_width=}x{frame_height=} @ {fps=}") | |
| logger.info("Configure cameras...") | |
| cap_left.set(cv2.CAP_PROP_FRAME_WIDTH, 3840) | |
| cap_left.set(cv2.CAP_PROP_FRAME_HEIGHT, 2160) | |
| cap_left.set(cv2.CAP_PROP_FPS, fps) | |
| cap_right.set(cv2.CAP_PROP_FRAME_WIDTH, 3840) | |
| cap_right.set(cv2.CAP_PROP_FRAME_HEIGHT, 2160) | |
| cap_right.set(cv2.CAP_PROP_FPS, fps) | |
| logger.info("Verify settings...") | |
| if not cap_left.isOpened() or not cap_right.isOpened(): | |
| raise RuntimeError("Could not open video cameras") | |
| # Get actual resolutions (might differ from requested) | |
| width_left = int(cap_left.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| height_left = int(cap_left.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| width_right = int(cap_right.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| height_right = int(cap_right.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| logger.info(f"Using left camera: {width_left}x{height_left} at {fps} FPS") | |
| logger.info(f"Using right camera: {width_right}x{height_right} at {fps} FPS") | |
| logger.info("Set up video writer with H264 codec for better compression") | |
| output_filename = "vr_stream.mp4" | |
| logger.info("Writing to %s", output_filename) | |
| fourcc = cv2.VideoWriter_fourcc(*'H264') # Better compression than MJPG | |
| out = cv2.VideoWriter(output_filename, fourcc, fps, (width_left + width_right, height_left)) | |
| logger.info("Warmup - let cameras stabilize") | |
| time.sleep(2) | |
| logger.info("VR streaming in progress. Press 'q' to quit...") | |
| # Add frame counter for performance tracking | |
| frame_count = 0 | |
| start_time = time.time() | |
| while True: | |
| # Read frames (non-blocking) | |
| cap_left.grab() | |
| cap_right.grab() | |
| # Retrieve frames | |
| ret_left, frame_left = cap_left.retrieve() | |
| ret_right, frame_right = cap_right.retrieve() | |
| if not ret_left or not ret_right: | |
| logger.info("Frame capture failed. Attempting to reconnect...") | |
| cap_left.release() | |
| cap_right.release() | |
| time.sleep(1) | |
| cap_left = cv2.VideoCapture(0, cv2.CAP_V4L2) | |
| cap_right = cv2.VideoCapture(1, cv2.CAP_V4L2) | |
| continue | |
| # Convert to RGB for proper color space (OpenCV uses BGR by default) | |
| frame_left = cv2.cvtColor(frame_left, cv2.COLOR_BGR2RGB) | |
| frame_right = cv2.cvtColor(frame_right, cv2.COLOR_BGR2RGB) | |
| logger.debug("Stitching frame {frame_count+1}") | |
| vr_frame = cv2.hconcat([frame_left, frame_right]) | |
| # Convert back to BGR for OpenCV operations | |
| vr_frame = cv2.cvtColor(vr_frame, cv2.COLOR_RGB2BGR) | |
| # Write to file | |
| out.write(vr_frame) | |
| # Display preview | |
| cv2.imshow("VR Stream (Press q to quit)", vr_frame) | |
| # Update frame counter | |
| frame_count += 1 | |
| # Calculate and show FPS | |
| if frame_count % 10 == 0: | |
| elapsed = time.time() - start_time | |
| current_fps = frame_count / elapsed | |
| logger.info(f"Current FPS: {current_fps:.2f}") | |
| # Check for exit | |
| if cv2.waitKey(1) & 0xFF == ord('q'): | |
| break | |
| # Cleanup | |
| cap_left.release() | |
| cap_right.release() | |
| out.release() | |
| cv2.destroyAllWindows() | |
| # Calculate final performance metrics | |
| total_time = time.time() - start_time | |
| avg_fps = frame_count / total_time if frame_count > 0 else 0 | |
| logger.info(f"Session complete. Average FPS: {avg_fps:.2f}") | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except Exception as e: | |
| logger.error(f"Error: {str(e)}") | |
| logger.error("Press any key to exit...") | |
| cv2.waitKey(0) |