ipycam 1.2.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.
Files changed (50) hide show
  1. ipycam/__init__.py +68 -0
  2. ipycam/__main__.py +327 -0
  3. ipycam/__version__.py +1 -0
  4. ipycam/camera.py +558 -0
  5. ipycam/config.py +161 -0
  6. ipycam/discovery.py +84 -0
  7. ipycam/http.py +632 -0
  8. ipycam/mjpeg.py +280 -0
  9. ipycam/onvif.py +416 -0
  10. ipycam/ptz.py +486 -0
  11. ipycam/rtsp.py +973 -0
  12. ipycam/static/css/style.css +572 -0
  13. ipycam/static/index.html +196 -0
  14. ipycam/static/js/app.js +808 -0
  15. ipycam/static/soap/envelope.xml +9 -0
  16. ipycam/static/soap/fault.xml +9 -0
  17. ipycam/static/soap/get_audio_decoder_configurations.xml +2 -0
  18. ipycam/static/soap/get_capabilities.xml +21 -0
  19. ipycam/static/soap/get_device_information.xml +7 -0
  20. ipycam/static/soap/get_profiles.xml +77 -0
  21. ipycam/static/soap/get_scopes.xml +5 -0
  22. ipycam/static/soap/get_services.xml +18 -0
  23. ipycam/static/soap/get_snapshot_uri.xml +8 -0
  24. ipycam/static/soap/get_stream_uri.xml +8 -0
  25. ipycam/static/soap/get_system_date_time.xml +19 -0
  26. ipycam/static/soap/get_users.xml +6 -0
  27. ipycam/static/soap/get_video_encoder_configuration.xml +16 -0
  28. ipycam/static/soap/get_video_source_configuration.xml +8 -0
  29. ipycam/static/soap/probe_match.xml +22 -0
  30. ipycam/static/soap/ptz_absolute_move.xml +1 -0
  31. ipycam/static/soap/ptz_continuous_move.xml +1 -0
  32. ipycam/static/soap/ptz_get_configurations.xml +30 -0
  33. ipycam/static/soap/ptz_get_node.xml +37 -0
  34. ipycam/static/soap/ptz_get_nodes.xml +37 -0
  35. ipycam/static/soap/ptz_get_presets.xml +3 -0
  36. ipycam/static/soap/ptz_get_service_capabilities.xml +4 -0
  37. ipycam/static/soap/ptz_get_status.xml +13 -0
  38. ipycam/static/soap/ptz_goto_home.xml +1 -0
  39. ipycam/static/soap/ptz_goto_preset.xml +1 -0
  40. ipycam/static/soap/ptz_relative_move.xml +1 -0
  41. ipycam/static/soap/ptz_set_preset.xml +3 -0
  42. ipycam/static/soap/ptz_stop.xml +1 -0
  43. ipycam/streamer.py +565 -0
  44. ipycam/webrtc.py +503 -0
  45. ipycam-1.2.0.dist-info/METADATA +305 -0
  46. ipycam-1.2.0.dist-info/RECORD +50 -0
  47. ipycam-1.2.0.dist-info/WHEEL +5 -0
  48. ipycam-1.2.0.dist-info/entry_points.txt +2 -0
  49. ipycam-1.2.0.dist-info/licenses/LICENSE +21 -0
  50. ipycam-1.2.0.dist-info/top_level.txt +1 -0
ipycam/__init__.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ IPyCam - Pure Python Virtual IP Camera
3
+
4
+ A virtual IP camera that:
5
+ - Is discoverable via ONVIF (WS-Discovery)
6
+ - Provides RTSP streams via go2rtc
7
+ - Has a web interface for configuration and live preview
8
+ - Accepts frames from any source
9
+ - Supports digital PTZ (ePTZ) via ONVIF
10
+
11
+ Usage:
12
+ from ipycam import IPCamera, CameraConfig
13
+
14
+ config = CameraConfig(name="My Camera")
15
+ camera = IPCamera(config)
16
+ camera.start()
17
+
18
+ while camera.is_running:
19
+ frame = get_frame() # Your frame source
20
+ camera.stream(frame)
21
+
22
+ camera.stop()
23
+ """
24
+
25
+ from .__version__ import __version__
26
+
27
+ from .config import CameraConfig
28
+ from .camera import IPCamera
29
+ from .streamer import VideoStreamer, StreamConfig, StreamStats, HWAccel
30
+ from .ptz import PTZController, PTZState, PTZPreset, PTZHardwareHandler, PTZVelocity
31
+ from .onvif import ONVIFService
32
+ from .discovery import WSDiscoveryServer
33
+ from .http import IPCameraHTTPHandler
34
+ from .mjpeg import MJPEGStreamer, check_go2rtc_running, check_rtsp_port_available
35
+ from .webrtc import NativeWebRTCStreamer, WebRTCStats, is_webrtc_available
36
+ from .rtsp import NativeRTSPServer, is_native_rtsp_available
37
+
38
+ __all__ = [
39
+ # Main classes
40
+ "IPCamera",
41
+ "CameraConfig",
42
+ # Streaming
43
+ "VideoStreamer",
44
+ "StreamConfig",
45
+ "StreamStats",
46
+ "HWAccel",
47
+ # MJPEG fallback
48
+ "MJPEGStreamer",
49
+ "check_go2rtc_running",
50
+ "check_rtsp_port_available",
51
+ # Native WebRTC fallback
52
+ "NativeWebRTCStreamer",
53
+ "WebRTCStats",
54
+ "is_webrtc_available",
55
+ # Native RTSP fallback
56
+ "NativeRTSPServer",
57
+ "is_native_rtsp_available",
58
+ # PTZ
59
+ "PTZController",
60
+ "PTZState",
61
+ "PTZPreset",
62
+ "PTZVelocity",
63
+ "PTZHardwareHandler",
64
+ # ONVIF
65
+ "ONVIFService",
66
+ "WSDiscoveryServer",
67
+ "IPCameraHTTPHandler",
68
+ ]
ipycam/__main__.py ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ IPyCam - Run as a module
4
+
5
+ Usage:
6
+ python -m ipycam [options]
7
+
8
+ Examples:
9
+ python -m ipycam
10
+ python -m ipycam --config camera_config.json
11
+ python -m ipycam --source 0
12
+ python -m ipycam --source rtsp://192.168.1.100/stream
13
+ python -m ipycam --no-timestamp
14
+ """
15
+
16
+ import argparse
17
+ import os
18
+ import platform
19
+ import cv2
20
+ from . import IPCamera, CameraConfig
21
+
22
+
23
+ def infer_source_type(source_arg: str) -> tuple[str, str]:
24
+ """
25
+ Infer source_type and source_info from the camera argument.
26
+
27
+ Returns:
28
+ tuple of (source_type, source_info)
29
+ """
30
+ # Special case: 'video' without a file path means video upload mode
31
+ if source_arg.lower() == 'video':
32
+ return ("video_file", "Waiting for upload...")
33
+
34
+ # Try to parse as integer (webcam index)
35
+ try:
36
+ index = int(source_arg)
37
+ return ("camera", f"Camera Index {index}")
38
+ except ValueError:
39
+ pass
40
+
41
+ # Check if it's a URL
42
+ source_lower = source_arg.lower()
43
+ if source_lower.startswith(('rtsp://', 'rtmp://', 'http://', 'https://')):
44
+ return ("rtsp", source_arg)
45
+
46
+ # Check if it's a file
47
+ if os.path.isfile(source_arg):
48
+ filename = os.path.basename(source_arg)
49
+ return ("video_file", filename)
50
+
51
+ # Could be a file path that doesn't exist yet, or other source
52
+ # Check by extension
53
+ _, ext = os.path.splitext(source_arg)
54
+ video_extensions = {'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mpeg', '.mpg', '.3gp'}
55
+ if ext.lower() in video_extensions:
56
+ filename = os.path.basename(source_arg)
57
+ return ("video_file", filename)
58
+
59
+ # Default to custom if we can't determine
60
+ return ("custom", source_arg)
61
+
62
+
63
+ def main():
64
+ parser = argparse.ArgumentParser(
65
+ prog='ipycam',
66
+ description='IPyCam - Pure Python Virtual IP Camera',
67
+ formatter_class=argparse.RawDescriptionHelpFormatter,
68
+ epilog="""
69
+ Examples:
70
+ python -m ipycam # Use webcam with default config
71
+ python -m ipycam --config custom.json # Use custom config file
72
+ python -m ipycam --source 1 # Use second webcam
73
+ python -m ipycam --source video.mp4 # Stream from video file
74
+ python -m ipycam --source rtsp://... # Stream from RTSP source
75
+ python -m ipycam --no-timestamp # Disable timestamp overlay
76
+ python -m ipycam --width 1280 --height 720 # Override resolution
77
+ python -m ipycam --fps 60 # Override FPS
78
+ """
79
+ )
80
+
81
+ parser.add_argument(
82
+ '--config', '-c',
83
+ type=str,
84
+ default='camera_config.json',
85
+ help='Path to config file (default: camera_config.json)'
86
+ )
87
+
88
+ parser.add_argument(
89
+ '--source',
90
+ type=str,
91
+ default='0',
92
+ help='Camera source: device index (0), file path, or URL (default: 0)'
93
+ )
94
+
95
+ parser.add_argument(
96
+ '--width',
97
+ type=int,
98
+ help='Override frame width'
99
+ )
100
+
101
+ parser.add_argument(
102
+ '--height',
103
+ type=int,
104
+ help='Override frame height'
105
+ )
106
+
107
+ parser.add_argument(
108
+ '--fps',
109
+ type=int,
110
+ help='Override target FPS'
111
+ )
112
+
113
+ parser.add_argument(
114
+ '--no-timestamp',
115
+ action='store_true',
116
+ help='Disable timestamp overlay'
117
+ )
118
+
119
+ parser.add_argument(
120
+ '--timestamp-position',
121
+ choices=['top-left', 'top-right', 'bottom-left', 'bottom-right'],
122
+ help='Timestamp position'
123
+ )
124
+
125
+ parser.add_argument(
126
+ '--hw',
127
+ choices=['auto', 'nvenc', 'qsv', 'cpu'],
128
+ default=None,
129
+ help='Hardware acceleration: auto, nvenc, qsv, or cpu'
130
+ )
131
+
132
+ args = parser.parse_args()
133
+
134
+ # Load config from file, or use defaults if not found
135
+ config = CameraConfig.load(args.config)
136
+
137
+ # Apply command-line overrides
138
+ if args.width:
139
+ config.main_width = args.width
140
+ if args.height:
141
+ config.main_height = args.height
142
+ if args.fps:
143
+ config.main_fps = args.fps
144
+ if args.no_timestamp:
145
+ config.show_timestamp = False
146
+ if args.timestamp_position:
147
+ config.timestamp_position = args.timestamp_position
148
+ if args.hw:
149
+ config.hw_accel = args.hw
150
+
151
+ # Infer and set source type from source argument
152
+ source_type, source_info = infer_source_type(args.source)
153
+ config.source_type = source_type
154
+ config.source_info = source_info
155
+
156
+ print(f"Loaded config: {config.name} ({config.main_width}x{config.main_height}@{config.main_fps}fps)")
157
+
158
+ camera = IPCamera(config)
159
+
160
+ if not camera.start():
161
+ print("Failed to start camera")
162
+ return 1
163
+
164
+ print("\n" + "="*50)
165
+ print("IP Camera is running!")
166
+ print("="*50)
167
+ print(f"\nOpen Web UI: http://{config.local_ip}:{config.onvif_port}/")
168
+ print(f"Snapshot URL: http://{config.local_ip}:{config.onvif_port}/{config.snapshot_url}")
169
+ print("Press Ctrl+C to stop\n")
170
+
171
+ # Check if this is video upload mode (--source video without a file)
172
+ is_video_upload_mode = args.source.lower() == 'video'
173
+
174
+ if is_video_upload_mode:
175
+ # Video upload mode - no source file, wait for upload via web UI
176
+ print("Video upload mode - no video file specified")
177
+ print("Upload a video file via the web UI to start streaming\n")
178
+ camera.set_video_upload_mode(True)
179
+
180
+ try:
181
+ # Main loop that handles video switching
182
+ while camera.is_running:
183
+ video_path = camera.get_current_video_path()
184
+
185
+ if video_path and os.path.isfile(video_path):
186
+ # We have a video file to play
187
+ print(f"Opening video source: {video_path}")
188
+ # Use auto backend for file paths (V4L2/DSHOW are for device indices only)
189
+ cap = cv2.VideoCapture(video_path)
190
+
191
+ if not cap.isOpened():
192
+ print(f"Error: Could not open video: {video_path}")
193
+ camera.notify_video_error(f"Could not open video: {os.path.basename(video_path)}")
194
+ import time
195
+ time.sleep(0.5)
196
+ continue
197
+
198
+ # Update config with video info
199
+ config.source_info = os.path.basename(video_path)
200
+ camera.notify_video_loaded(video_path)
201
+
202
+ # Stream video frames
203
+ while camera.is_running and camera.get_current_video_path() == video_path:
204
+ ret, frame = cap.read()
205
+ if not ret:
206
+ # Loop the video
207
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
208
+ continue
209
+ camera.stream(frame)
210
+
211
+ cap.release()
212
+ print(f"Video source closed: {video_path}")
213
+ else:
214
+ # No video yet, generate a placeholder frame
215
+ import numpy as np
216
+ placeholder = np.zeros((config.main_height, config.main_width, 3), dtype=np.uint8)
217
+ # Dark blue background
218
+ placeholder[:] = (30, 20, 10)
219
+ # Add text
220
+ text = "Upload a video to start"
221
+ font = cv2.FONT_HERSHEY_SIMPLEX
222
+ font_scale = 1.5
223
+ thickness = 2
224
+ (text_w, text_h), _ = cv2.getTextSize(text, font, font_scale, thickness)
225
+ x = (config.main_width - text_w) // 2
226
+ y = (config.main_height + text_h) // 2
227
+ cv2.putText(placeholder, text, (x, y), font, font_scale, (100, 100, 100), thickness)
228
+
229
+ camera.stream(placeholder)
230
+ import time
231
+ time.sleep(1.0 / config.main_fps)
232
+
233
+ except KeyboardInterrupt:
234
+ print("\nShutting down...")
235
+ finally:
236
+ camera.stop()
237
+
238
+ return 0
239
+
240
+ # Standard mode - open camera source
241
+ # Try to parse as int (device index), otherwise treat as path/URL
242
+ try:
243
+ camera_source = int(args.source)
244
+ except ValueError:
245
+ camera_source = args.source
246
+
247
+ print(f"Opening camera source: {camera_source}")
248
+ # Use auto backend for file paths; use platform backend for device indices/URLs
249
+ if isinstance(camera_source, str) and os.path.isfile(camera_source):
250
+ cap = cv2.VideoCapture(camera_source)
251
+ elif platform.system().lower() == "linux":
252
+ cap = cv2.VideoCapture(camera_source, cv2.CAP_V4L2)
253
+ else:
254
+ cap = cv2.VideoCapture(camera_source, cv2.CAP_DSHOW)
255
+
256
+ # Set resolution if using webcam (device index)
257
+ if isinstance(camera_source, int):
258
+ # Request MJPEG + low buffer for better FPS on Pi
259
+ fourcc_func = getattr(cv2, "VideoWriter_fourcc", None)
260
+ if fourcc_func is None:
261
+ fourcc_func = cv2.VideoWriter.fourcc
262
+ cap.set(cv2.CAP_PROP_FOURCC, fourcc_func(*"MJPG"))
263
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
264
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.main_width)
265
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.main_height)
266
+ cap.set(cv2.CAP_PROP_FPS, config.main_fps)
267
+
268
+ # Store initial video path if it's a video file
269
+ if source_type == "video_file" and os.path.isfile(args.source):
270
+ camera.set_video_upload_mode(True)
271
+ camera.set_current_video_path(os.path.abspath(args.source))
272
+
273
+ if not cap.isOpened():
274
+ print(f"Error: Could not open camera source: {camera_source}")
275
+ camera.stop()
276
+ return 1
277
+
278
+ try:
279
+ while camera.is_running:
280
+ # Check if video source changed (for video file mode)
281
+ if source_type == "video_file":
282
+ new_video = camera.get_current_video_path()
283
+ current_source = os.path.abspath(camera_source) if isinstance(camera_source, str) else None
284
+
285
+ if new_video and new_video != current_source:
286
+ # Video source changed - switch to new video
287
+ print(f"Switching to new video: {new_video}")
288
+ cap.release()
289
+ cap = cv2.VideoCapture(new_video)
290
+
291
+ if not cap.isOpened():
292
+ print(f"Error: Could not open new video: {new_video}")
293
+ camera.notify_video_error(f"Could not open video: {os.path.basename(new_video)}")
294
+ # Revert to previous video
295
+ if current_source and os.path.isfile(current_source):
296
+ cap = cv2.VideoCapture(current_source)
297
+ continue
298
+
299
+ camera_source = new_video
300
+ config.source_info = os.path.basename(new_video)
301
+ camera.notify_video_loaded(new_video)
302
+
303
+ ret, frame = cap.read()
304
+
305
+ if not ret:
306
+ # Loop video files, exit on camera failure
307
+ if isinstance(camera_source, str):
308
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
309
+ continue
310
+ else:
311
+ print("Error: Failed to read from camera")
312
+ break
313
+
314
+ # Stream handles PTZ, timestamp, and frame pacing automatically
315
+ camera.stream(frame)
316
+
317
+ except KeyboardInterrupt:
318
+ print("\nShutting down...")
319
+ finally:
320
+ cap.release()
321
+ camera.stop()
322
+
323
+ return 0
324
+
325
+
326
+ if __name__ == "__main__":
327
+ exit(main())
ipycam/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.2.0"