Skip to content

Commit

Permalink
Fixed buffered recording
Browse files Browse the repository at this point in the history
  • Loading branch information
connervieira committed Apr 28, 2024
1 parent a9ab409 commit f6d8e36
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 33 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,11 @@ March 6th, 2024
- Instead of creating each segment between the original time and new time, Predator skips to the next segment.
- Predator will now attempt to resume recording if the video capture drops on a particular device.
- A failure on a single capture device will no longer kill recording on other capture devices.
- Predator now shows a warning instead of an error when the merged audio/video file is missing at the end of a saved segment.
- This occurs when the audio recording process fails, and doesn't necessarily mean that the video capture has encountered a fatal problem.
- Added diagnostic stamp, which is capable of displaying various pieces of technical information.
- Added a frame-rate stamp, which shows the instantaneous frame-rate.
- Added a state stamp, which shows Predator's current operating mode.
- Added configuration option to use a different audio recording device.
- Added per-device resolution configuration.
- Added the ability to disable capture devices without removing them from the configuration entirely.
Expand Down
18 changes: 9 additions & 9 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ These are the features actively planned for Predator and are likely to be added
- [X] Add the ability to disable dashcam capture devices from the configuration.
- [X] Fix dashcam saving when the first segment is saved.
- [X] Move status lighting configuration to general section.
- [X] Add individual resolution configuration for dashcam capture devices.
- [X] Add status light interfacing to dashcam mode.
- [X] Test dashcam saving with audio recording.
- [X] Test when merging is enabled.
- [ ] Test when merging is disabled.
- [ ] Test different output saving intervals.
- [ ] Test background dash-cam recording.
- [ ] Test dashcam saving with audio recording.
- [ ] Test when merging is enabled.
- [ ] Test when merging is disabled.
- [ ] Add more stamp options to dash-cam mode.
- [ ] Add custom relay status stamps through GPIO.
- [X] Add dash-cam operation mode stamp.
- [ ] Add remote motion detection alerts for dashcam mode via Reticulum.


## Hypothetical

Features in this section may be added in the future, but are not actively planned.

- [ ] Add more stamp options to dash-cam mode.
- [ ] Add custom relay status stamps through GPIO.
- [ ] Add dash-cam operation mode stamp.
- [ ] Add individual resolution configuration for dashcam capture devices.
- [ ] Add status light interfacing to dashcam mode.
- [ ] Add remote motion detection alerts for dashcam mode via Reticulum.
4 changes: 4 additions & 0 deletions assets/support/configoutline.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,11 @@
"color": "list",
"framerate": {
"enabled": "bool",
"mode": ["instant", "average", "hybrid"],
"precision": "+int"
},
"state": {
"enabled": "bool"
}
},
"gps": {
Expand Down
5 changes: 4 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
},
"sensitivity": 0.2,
"timeout": 5,
"buffer": 60
"buffer": 30
}
},
"notifications": {
Expand Down Expand Up @@ -253,6 +253,9 @@
"enabled": true,
"mode": "hybrid",
"precision": 1
},
"state": {
"enabled": true
}
},
"gps": {
Expand Down
49 changes: 35 additions & 14 deletions dashcam.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,12 @@ def save_dashcam_segments(file1, file2=""):




def apply_dashcam_stamps(frame, device=""):
global instant_framerate
global calculated_framerate
global shortterm_framerate
global saving_active
global parked

process_timing("start", "Dashcam/Apply Stamps")
try:
Expand Down Expand Up @@ -278,6 +279,18 @@ def apply_dashcam_stamps(frame, device=""):
diagnostic_stamp = diagnostic_stamp + (str("%." + str(config["dashcam"]["stamps"]["diagnostic"]["framerate"]["precision"]) + "f") % calculated_framerate[device]) + "FPS " # Add the current frame-rate to the main stamp.
elif (config["dashcam"]["stamps"]["diagnostic"]["framerate"]["mode"] == "hybrid" and device in shortterm_framerate): # Only add the frame-rate stamp if there is frame-rate information for this device.
diagnostic_stamp = diagnostic_stamp + (str("%." + str(config["dashcam"]["stamps"]["diagnostic"]["framerate"]["precision"]) + "f") % shortterm_framerate[device]["framerate"]) + "FPS " # Add the current frame-rate to the main stamp.
if (config["dashcam"]["stamps"]["diagnostic"]["state"]["enabled"] == True): # Check to see if the state overlay stamp is enabled.
current_state = utils.get_current_state()
if (parked == False):
if (saving_active == False):
diagnostic_stamp = diagnostic_stamp + "NN"
elif (saving_active == True):
diagnostic_stamp = diagnostic_stamp + "NS"
elif (parked == True):
if (current_state["mode"] == "dashcam/parked_dormant"):
diagnostic_stamp = diagnostic_stamp + "PD"
elif (current_state["mode"] == "dashcam/parked_active"):
diagnostic_stamp = diagnostic_stamp + "PA"


gps_stamp_position = [10, 30] # Determine where the GPS overlay stamp should be positioned in the video stream.
Expand Down Expand Up @@ -362,11 +375,6 @@ def record_parked_motion(capture, framerate, width, height, device, directory, f
background_subtractor = cv2.createBackgroundSubtractorMOG2() # Initialize the background subtractor for motion detection.
process_timing("end", "Dashcam/Detection Motion")

process_timing("start", "Dashcam/Writing")
for frame in frame_history: # Iterate through each frame in the frame history.
write_frame(frame, device)
process_timing("end", "Dashcam/Writing")


current_segment_name[device] = directory + "/predator_dashcam_" + str(round(utils.get_time())) + "_" + str(device) + "_0_P"

Expand All @@ -376,6 +384,12 @@ def record_parked_motion(capture, framerate, width, height, device, directory, f
audio_recorders[device] = subprocess.Popen((audio_record_command + " " + audio_filepath).split(" "), stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) # Start the next segment's audio recorder.
process_timing("end", "Dashcam/Audio Processing")


process_timing("start", "Dashcam/Writing")
for frame in frame_history: # Iterate through each frame in the frame history.
write_frame(frame, device)
process_timing("end", "Dashcam/Writing")

frames_captured = 0 # This is a placeholder that will keep track of how many frames are captured in this parked recording.
capture_start_time = utils.get_time() # This stores the time that this parked recording started.
last_frame_captured = time.time() # This will hold the exact time that the last frame was captured. Here, the value is initialized to the current time before any frames have been captured.
Expand Down Expand Up @@ -776,12 +790,14 @@ def start_dashcam_recording(dashcam_devices, directory, background=False): # Thi



saving_active = False # This is a flag that will be changed to true when a dashcam segment save is triggered, and will be returned to false when the segment to save finishes. This is only used to update the corresponding overlay stamp.
def dashcam_output_handler(directory, device, width, height, framerate):
global calculated_framerate
global frames_since_last_segment
global frames_to_write
global audio_recorders
global recording_active
global saving_active

if (os.path.isdir(config["general"]["working_directory"] + "/" + config["dashcam"]["saving"]["directory"]) == False): # Check to see if the saved dashcam video folder needs to be created.
os.system("mkdir -p '" + config["general"]["working_directory"] + "/" + config["dashcam"]["saving"]["directory"] + "'") # Create the saved dashcam video directory.
Expand All @@ -807,7 +823,7 @@ def dashcam_output_handler(directory, device, width, height, framerate):
output = cv2.VideoWriter(current_segment_name[device] + ".avi", cv2.VideoWriter_fourcc(*'XVID'), float(framerate), (width, height)) # Initialize the first video output.

while True:
time.sleep(0.001)
time.sleep(0.01)

video_filepath = current_segment_name[device] + ".avi"
audio_filepath = current_segment_name[device] + "." + config["dashcam"]["capture"]["audio"]["extension"]
Expand Down Expand Up @@ -838,17 +854,13 @@ def dashcam_output_handler(directory, device, width, height, framerate):
dashcam_segment_saving = threading.Thread(target=save_dashcam_segments, args=[audio_filepath, last_audio_file], name="DashcamSegmentSave") # Create the thread to save the current and last audio segments.
dashcam_segment_saving.start() # Start the dashcam segment saving thread.
save_this_segment = True # This flag causes Predator to save this entire segment again when the next segment is started.
saving_active = True
update_status_lighting("dashcam_save") # Run the function to update the status lighting.

process_timing("end", "Dashcam/Interface Interactions")



for frame in frames_to_write[device]: # Iterate through each frame that needs to be written.
output.write(frame)
frames_to_write[device] = [] # Clear the frame buffer.


# ===== Handle the updating of the output file =====
if (recording_active == True): # Check to see if recording is active before updating the output.
if (previous_loop_segment_name != current_segment_name[device]): # Check to see if the current segment name has changed since the last loop (meaning a new segment has started).
last_segment_name = previous_loop_segment_name
Expand Down Expand Up @@ -897,7 +909,7 @@ def dashcam_output_handler(directory, device, width, height, framerate):
os.system("rm '" + base_file + ".avi'")
os.system("rm '" + base_file + "." + str(config["dashcam"]["capture"]["audio"]["extension"]) + "'")
else: # If the merged video file doesn't exist, it is likely something went wrong with the merging process.
display_message("The merged video/audio file did exist when Predator tried to save it. It is likely the merge process has failed unexpectedly. The separate files are being saved as a fallback.", 3)
display_message("The merged video/audio file did exist when Predator tried to save it. It is likely the merge process has failed unexpectedly. The separate files are being saved as a fallback.", 2)
dashcam_segment_saving = threading.Thread(target=save_dashcam_segments, args=[last_video_file, last_audio_path], name="DashcamSegmentSave") # Create the thread to save the dashcam segment. At this point, "last_video_file" is actually the completed previous video segment, since we just started a new segment.
dashcam_segment_saving.start() # Start the dashcam segment saving thread.
else: # If audio/video merging is disabled, then save the separate video and audio files.
Expand All @@ -907,13 +919,22 @@ def dashcam_output_handler(directory, device, width, height, framerate):
dashcam_segment_saving = threading.Thread(target=save_dashcam_segments, args=[last_audio_path], name="DashcamSegmentSave") # Create the thread to save the dashcam segment. At this point, "last_audio_path" is actually the completed previous video segment, since we just started a new segment.
dashcam_segment_saving.start() # Start the dashcam segment saving thread.
save_this_segment = False # Reset the segment saving flag.
saving_active = False
update_status_lighting("normal") # Return status lighting to normal.


delete_old_segments() # Handle the erasing of any old dash-cam segments that need to be deleted.

previous_loop_segment_name = current_segment_name[device] # Updated the previous segment name to be the current name, since we are about to restart the loop.


# ===== Check to see if any frames need to be written =====
for frame in frames_to_write[device]: # Iterate through each frame that needs to be written.
output.write(frame)
frames_to_write[device] = [] # Clear the frame buffer.






Expand Down
7 changes: 7 additions & 0 deletions docs/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ This document describes the configuration values found `config.json`.
- "average" is the average frame-rate, calculated based on the number of frames captured in the previous segment.
- "hybrid" uses the short-term frame-rate, calculated by average frame-rate over a short period of time.
- `precision` is an integer number that determines how many decimal places the frame-rate will be displayed to.
- `state` contains settings for configuring if/how Predator shows the current operating mode as an overlay stamp.
- `enabled` is a boolean that determines if this overlay stamp is enabled.
- When enabled, this overlay stamp adds one of the following values to the overlay stamp:
- "NN" for normal recording.
- "NS" for normal recording when the current segment is being saved to the event folder.
- "PD" for parked recording while the system is dormant, and waiting for motion to trigger recording.
- "PA" for parked recording while the system is actively recording an event.
- `gps` contains configuration values for the stamp shown at the top of the frame, containing location information.
- `color` is a list of three values between 0 and 255 that determines the font cover of the overlay stamp.
- The first value represents red, the second value represents green, and the third value represents blue.
Expand Down
18 changes: 9 additions & 9 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,19 @@ def issue_heartbeat(): # This is the function that actually issues a heartbeat.
state_file_location = config["general"]["interface_directory"] + "/state.json"
save_to_file(state_file_location, "{}") # Save a blank placeholder dictionary to the state log file.

gps_state = 0
current_state = {}
def update_state(mode, performance={}): # This is the function that is called to issue a state update.
global current_state
current_state["mode"] = mode
current_state["performance"] = performance
if (config["general"]["interface_directory"] != ""): # Check to see if the interface directory is enabled.
global gps_state
current_state = {}
current_state["mode"] = mode
current_state["gps"] = gps_state
current_state["performance"] = performance
state_update_thread = threading.Thread(target=update_state_file, args=[current_state], name="InterfaceStateUpdate")
state_update_thread.start()

def update_state_file(current_state): # This is the function that actually issues a status update to disk.
save_to_file(state_file_location, json.dumps(current_state)) # Save the modified state to the disk as JSON data.
def get_current_state():
global current_state
return current_state



Expand Down Expand Up @@ -627,7 +627,7 @@ def display_shape(shape):
gpsd.connect() # Connect to the GPS daemon.
def get_gps_location():
global gps_demo_gpx_data
global gps_state
global current_state
global global_time_offset
if (config["general"]["gps"]["enabled"] == True): # Check to see if GPS is enabled.
if (len(config["general"]["gps"]["demo_file"]) > 0): # Check to see if there is a demo file set.
Expand All @@ -638,7 +638,7 @@ def get_gps_location():
try:
gps_data_packet = gpsd.get_current() # Query the GPS for the most recent information.

gps_state = gps_data_packet.mode
current_state["gps"] = gps_data_packet.mode
if (gps_data_packet.mode >= 2): # Check to see if the GPS has a 2D fix yet.
try:
gps_time = datetime.datetime.strptime(str(gps_data_packet.time)[:-1]+"000" , '%Y-%m-%dT%H:%M:%S.%f').astimezone().timestamp() + timezone_offset # Determine the local Unix timestamp from the GPS timestamp.
Expand Down

0 comments on commit f6d8e36

Please sign in to comment.