1+ #!/usr/bin/env python3
2+ # -*- coding:utf-8 vi:ts=4:noexpandtab
3+
4+ from picamera2 import Picamera2
5+ from picamera2 .encoders import H264Encoder
6+ from libcamera import Transform
7+
8+ import cv2
9+ import argparse
10+ import time , signal , os , sys , shutil , traceback
11+
12+ GOT_SIGUSR1 = False
13+ GOT_SIGTERM = False
14+ VIDEO_ACTIVE = False
15+ pid = os .getpid ()
16+ print ("PID is : " , pid )
17+
18+ def handle_sigusr1 (signum , stack ):
19+ global GOT_SIGUSR1
20+ GOT_SIGUSR1 = True
21+
22+ def handle_sigterm (signum , stack ):
23+ global GOT_SIGTERM
24+ GOT_SIGTERM = True
25+
26+ # Register the signal handler functions with the actual signals
27+ signal .signal (signal .SIGTERM , handle_sigterm )
28+ signal .signal (signal .SIGUSR1 , handle_sigusr1 )
29+
30+ # Parse command line arguments
31+ parser = argparse .ArgumentParser (description = "Camera control server" )
32+ parser .add_argument ("-d" , "--destination" , dest = "mediaPath" ,
33+ help = "Save captured image to PATH. Default: /home/pi/Rpanion-server/media/" ,
34+ metavar = "PATH" ,
35+ default = "/home/pi/Rpanion-server/media/"
36+ )
37+ parser .add_argument ("-m" , "--mode" , choices = ['photo' , 'video' ],
38+ dest = "captureMode" ,
39+ help = "Capture mode options: photo [default], video" , metavar = "MODE" ,
40+ default = 'photo'
41+ )
42+ parser .add_argument ("--device" , dest = "captureDevicePath" ,
43+ help = "V4L2 capture device path. If not specified, defaults to the first available Picamera2-supported camera." ,
44+ default = None
45+ )
46+ parser .add_argument ("--width" , metavar = "W" , type = int , dest = "vidWidth" ,
47+ help = "Image width" , default = 1920
48+ )
49+ parser .add_argument ("--height" , metavar = "H" , type = int , dest = "vidHeight" ,
50+ help = "Image height" , default = 1080
51+ )
52+ parser .add_argument ("--fps" , metavar = "FPS" , type = int , dest = "vidFps" ,
53+ help = "Video framerate" , default = 30
54+ )
55+ parser .add_argument ("--format" , dest = "vidFormat" ,
56+ help = "Video format (e.g., YUV420, MJPEG, RGB888). Default: YUV420" ,
57+ default = "YUV420"
58+ )
59+ parser .add_argument ("--rotation" , metavar = "DEG" , type = int , choices = [0 , 90 , 180 , 270 ],
60+ dest = "imageRotation" , help = "Image rotation" , default = 0 )
61+ parser .add_argument ("-b" , "--bitrate" , metavar = "N" ,
62+ type = int , dest = "vidBitrate" ,
63+ help = "Video bitrate in bits per second. Default: 10000000" ,
64+ default = 10000000
65+ )
66+ parser .add_argument ("-f" , "--min-disk-space" , metavar = "N" ,
67+ type = int , dest = "minFreeSpace" ,
68+ help = "Minimum free disk space (in MB) required to save files. Default: 1000 MB" ,
69+ default = 1000
70+ )
71+ args = parser .parse_args ()
72+
73+ mediaPath = args .mediaPath
74+ captureMode = args .captureMode
75+ minFreeSpace = args .minFreeSpace * 10 ** 6
76+
77+ picam2_still = None
78+ picam2_vid = None
79+ v4l2_cam = None
80+ v4l2_writer = None
81+ encoder = None
82+ use_picamera = False
83+
84+ # Create media directory if it doesn't exist
85+ try :
86+ os .makedirs (mediaPath , exist_ok = True )
87+ print (f"Media storage directory '{ mediaPath } ' is ready." )
88+ except Exception as e :
89+ sys .exit (f"An error occurred creating directory: { e } " )
90+
91+ # Determine which backend type to use and initialize the camera
92+ if args .captureDevicePath is None :
93+ print ("No camera device specified. Defaulting to Picamera2 backend." )
94+ use_picamera = True
95+ try :
96+ if not Picamera2 .global_camera_info ():
97+ sys .exit ("Picamera2 backend selected, but no libcamera cameras found." )
98+ except Exception as e :
99+ sys .exit (f"Could not query for Picamera2 cameras: { e } " )
100+ else :
101+ print (f"Device { args .captureDevicePath } specified. Using V4L2/OpenCV backend." )
102+ use_picamera = False
103+
104+ if captureMode == "photo" :
105+ if use_picamera :
106+ picam2_still = Picamera2 ()
107+ config = picam2_still .create_still_configuration ({"size" :(args .vidWidth , args .vidHeight )}, transform = Transform (rotation = args .imageRotation ))
108+ picam2_still .configure (config )
109+ picam2_still .start ()
110+ time .sleep (2 )
111+ print (f"Camera is ready in Picamera2 photo mode. Capturing { args .vidWidth } x{ args .vidHeight } , { args .imageRotation } ° rotation" )
112+ else :
113+ v4l2_cam = cv2 .VideoCapture (args .captureDevicePath , cv2 .CAP_V4L2 )
114+ v4l2_cam .set (cv2 .CAP_PROP_FRAME_WIDTH , args .vidWidth )
115+ v4l2_cam .set (cv2 .CAP_PROP_FRAME_HEIGHT , args .vidHeight )
116+ if not v4l2_cam .isOpened ():
117+ sys .exit (f"V4L2 camera at { args .captureDevicePath } failed to open." )
118+ time .sleep (2 )
119+ print (f"Camera is ready in V4L2 photo mode. Capturing { args .vidWidth } x{ args .vidHeight } " )
120+
121+ elif captureMode == "video" :
122+ if use_picamera :
123+ picam2_vid = Picamera2 ()
124+ video_config = picam2_vid .create_video_configuration (main = {"size" : (args .vidWidth , args .vidHeight ), "format" : args .vidFormat }, controls = {"FrameRate" : args .vidFps }, transform = Transform (rotation = args .imageRotation ))
125+ picam2_vid .configure (video_config )
126+ encoder = H264Encoder (bitrate = args .vidBitrate )
127+ picam2_vid .start ()
128+ print (f"Camera is ready in Picamera2 video mode. Capturing { args .vidWidth } x{ args .vidHeight } { args .vidFormat } @ { args .vidFps } fps, { args .imageRotation } ° rotation" )
129+ else :
130+ v4l2_cam = cv2 .VideoCapture (args .captureDevicePath , cv2 .CAP_V4L2 )
131+ if not v4l2_cam .isOpened ():
132+ sys .exit (f"V4L2 camera at { args .captureDevicePath } failed to open." )
133+
134+ v4l2_cam .set (cv2 .CAP_PROP_FRAME_WIDTH , args .vidWidth )
135+ v4l2_cam .set (cv2 .CAP_PROP_FRAME_HEIGHT , args .vidHeight )
136+ v4l2_cam .set (cv2 .CAP_PROP_FPS , args .vidFps )
137+
138+ # Check if the actual width, height, and FPS were written correctly
139+ actual_v4l2_width = int (v4l2_cam .get (cv2 .CAP_PROP_FRAME_WIDTH ))
140+ actual_v4l2_height = int (v4l2_cam .get (cv2 .CAP_PROP_FRAME_HEIGHT ))
141+ actual_v4l2_fps = int (v4l2_cam .get (cv2 .CAP_PROP_FPS ))
142+
143+ print (f"Camera is ready in V4L2 video mode. Capturing { actual_v4l2_width } x{ actual_v4l2_height } { args .vidFormat } @ { actual_v4l2_fps } fps" )
144+
145+ def graceful_exit ():
146+ global VIDEO_ACTIVE
147+ print ("Gracefully exiting..." )
148+ if VIDEO_ACTIVE :
149+ startstop_video ()
150+ time .sleep (0.2 )
151+ if use_picamera :
152+ if picam2_still : picam2_still .stop ()
153+ if picam2_vid : picam2_vid .stop ()
154+ elif v4l2_cam :
155+ v4l2_cam .release ()
156+ sys .exit (0 )
157+
158+ def startstop_video ():
159+ global VIDEO_ACTIVE , v4l2_writer
160+ if use_picamera :
161+ if VIDEO_ACTIVE :
162+ picam2_vid .stop_recording ()
163+ VIDEO_ACTIVE = False
164+ print ("Picamera2 recording stopped." )
165+ else :
166+ filepath = os .path .join (mediaPath , time .strftime ("RPN%Y%m%d_%H%M%S.h264" ))
167+ picam2_vid .start_recording (encoder , filepath )
168+ VIDEO_ACTIVE = True
169+ print (f"Picamera2 recording started to { filepath } " )
170+ else : # V4L2
171+ if VIDEO_ACTIVE :
172+ VIDEO_ACTIVE = False
173+ if v4l2_writer :
174+ v4l2_writer .release ()
175+ v4l2_writer = None
176+ print ("V4L2 recording stopped." )
177+ else :
178+ for _ in range (5 ): v4l2_cam .read () # Flush buffer
179+ filepath = os .path .join (mediaPath , time .strftime ("RPN%Y%m%d_%H%M%S.avi" ))
180+ fourcc = cv2 .VideoWriter_fourcc (* 'MJPG' )
181+ v4l2_writer = cv2 .VideoWriter (filepath , fourcc , actual_v4l2_fps , (actual_v4l2_width , actual_v4l2_height ))
182+ if v4l2_writer .isOpened ():
183+ VIDEO_ACTIVE = True
184+ print (f"V4L2 recording started to { filepath } " )
185+ else :
186+ print (f"Error: Could not open VideoWriter for { filepath } " , file = sys .stderr )
187+
188+ if __name__ == '__main__' :
189+ try :
190+ while True :
191+ if GOT_SIGTERM :
192+ GOT_SIGTERM = False
193+ graceful_exit ()
194+
195+ if GOT_SIGUSR1 :
196+ GOT_SIGUSR1 = False
197+
198+ freeDiskSpace = shutil .disk_usage (mediaPath ).free
199+ if freeDiskSpace < minFreeSpace and (captureMode == 'photo' or (captureMode == 'video' and not VIDEO_ACTIVE )):
200+ print (f"Free disk space is { (int )(freeDiskSpace / 10 ** 6 )} MB, which is less than the minimum of { (int )(minFreeSpace / 10 ** 6 )} MB. Action aborted." )
201+ else :
202+ if captureMode == 'photo' :
203+ filepath = os .path .join (mediaPath , time .strftime ("RPN%Y%m%d_%H%M%S.jpg" ))
204+ print (f"Capturing photo to { filepath } " )
205+ if use_picamera :
206+ picam2_still .capture_file (filepath )
207+ print ("Photo captured." )
208+ else :
209+ for _ in range (5 ): v4l2_cam .read ()
210+ ret , frame = v4l2_cam .read ()
211+ if ret :
212+ cv2 .imwrite (filepath , frame )
213+ print ("Photo captured." )
214+ else :
215+ print ("Failed to capture frame from V4L2 camera." , file = sys .stderr )
216+ elif captureMode == 'video' :
217+ print ("Toggling video recording." )
218+ startstop_video ()
219+
220+ # Main loop with two states: paused and waiting for a signal, or actively recording V4L2 video
221+ if use_picamera or not VIDEO_ACTIVE :
222+ print ("... Paused, waiting for signal ..." , file = sys .stderr )
223+ signal .pause ()
224+ else :
225+ if v4l2_cam and v4l2_writer :
226+ ret , frame = v4l2_cam .read ()
227+ if ret :
228+ v4l2_writer .write (frame )
229+ else :
230+ print ("V4L2 frame read failed during recording. Stopping." , file = sys .stderr )
231+ startstop_video ()
232+ time .sleep (0.01 )
233+
234+ except KeyboardInterrupt :
235+ graceful_exit ()
236+ except Exception as e :
237+ print (f"!!! PYTHON ERROR: Unhandled exception: { e } " , file = sys .stderr )
238+ traceback .print_exc (file = sys .stderr )
239+ graceful_exit ()
0 commit comments