-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
53ccc90
commit 7383db6
Showing
6 changed files
with
223 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import os | ||
import time | ||
import psutil | ||
import threading | ||
from collections import defaultdict | ||
import json | ||
import datetime | ||
import subprocess as sp | ||
import queue | ||
|
||
class EventProcessor(threading.Thread): | ||
def __init__(self, config, camera_processes, cache_dir, clip_dir, event_queue): | ||
threading.Thread.__init__(self) | ||
self.config = config | ||
self.camera_processes = camera_processes | ||
self.cache_dir = cache_dir | ||
self.clip_dir = clip_dir | ||
self.cached_clips = {} | ||
self.event_queue = event_queue | ||
self.events_in_process = {} | ||
|
||
def refresh_cache(self): | ||
cached_files = os.listdir(self.cache_dir) | ||
|
||
files_in_use = [] | ||
for process_data in self.camera_processes.values(): | ||
try: | ||
ffmpeg_process = psutil.Process(pid=process_data['ffmpeg_process'].pid) | ||
flist = ffmpeg_process.open_files() | ||
if flist: | ||
for nt in flist: | ||
if nt.path.startswith(self.cache_dir): | ||
files_in_use.append(nt.path.split('/')[-1]) | ||
except: | ||
continue | ||
|
||
for f in cached_files: | ||
if f in files_in_use or f in self.cached_clips: | ||
continue | ||
|
||
camera = f.split('-')[0] | ||
start_time = datetime.datetime.strptime(f.split('-')[1].split('.')[0], '%Y%m%d%H%M%S') | ||
|
||
ffprobe_cmd = " ".join([ | ||
'ffprobe', | ||
'-v', | ||
'error', | ||
'-show_entries', | ||
'format=duration', | ||
'-of', | ||
'default=noprint_wrappers=1:nokey=1', | ||
f"{os.path.join(self.cache_dir,f)}" | ||
]) | ||
p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True) | ||
(output, err) = p.communicate() | ||
p_status = p.wait() | ||
if p_status == 0: | ||
duration = float(output.decode('utf-8').strip()) | ||
else: | ||
print(f"bad file: {f}") | ||
os.remove(os.path.join(self.cache_dir,f)) | ||
continue | ||
|
||
self.cached_clips[f] = { | ||
'path': f, | ||
'camera': camera, | ||
'start_time': start_time.timestamp(), | ||
'duration': duration | ||
} | ||
|
||
if len(self.events_in_process) > 0: | ||
earliest_event = min(self.events_in_process.values(), key=lambda x:x['start_time'])['start_time'] | ||
else: | ||
earliest_event = datetime.datetime.now().timestamp() | ||
|
||
for f, data in list(self.cached_clips.items()): | ||
if earliest_event-90 > data['start_time']+data['duration']: | ||
del self.cached_clips[f] | ||
os.remove(os.path.join(self.cache_dir,f)) | ||
|
||
def create_clip(self, camera, event_data): | ||
# get all clips from the camera with the event sorted | ||
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time']) | ||
|
||
while sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']: | ||
time.sleep(5) | ||
self.refresh_cache() | ||
# get all clips from the camera with the event sorted | ||
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time']) | ||
|
||
playlist_start = event_data['start_time']-30 | ||
playlist_end = event_data['end_time']+5 | ||
playlist_lines = [] | ||
for clip in sorted_clips: | ||
# clip ends before playlist start time, skip | ||
if clip['start_time']+clip['duration'] < playlist_start: | ||
continue | ||
# clip starts after playlist ends, finish | ||
if clip['start_time'] > playlist_end: | ||
break | ||
playlist_lines.append(f"file '{os.path.join(self.cache_dir,clip['path'])}'") | ||
# if this is the starting clip, add an inpoint | ||
if clip['start_time'] < playlist_start: | ||
playlist_lines.append(f"inpoint {int(playlist_start-clip['start_time'])}") | ||
# if this is the ending clip, add an outpoint | ||
if clip['start_time']+clip['duration'] > playlist_end: | ||
playlist_lines.append(f"outpoint {int(playlist_end-clip['start_time'])}") | ||
|
||
clip_name = f"{camera}-{event_data['id']}" | ||
ffmpeg_cmd = [ | ||
'ffmpeg', | ||
'-y', | ||
'-protocol_whitelist', | ||
'pipe,file', | ||
'-f', | ||
'concat', | ||
'-safe', | ||
'0', | ||
'-i', | ||
'-', | ||
'-c', | ||
'copy', | ||
f"{os.path.join(self.clip_dir, clip_name)}.mp4" | ||
] | ||
|
||
p = sp.run(ffmpeg_cmd, input="\n".join(playlist_lines), encoding='ascii', capture_output=True) | ||
if p.returncode != 0: | ||
print(p.stderr) | ||
return | ||
|
||
with open(f"{os.path.join(self.clip_dir, clip_name)}.json", 'w') as outfile: | ||
json.dump(event_data, outfile) | ||
|
||
def run(self): | ||
while True: | ||
try: | ||
event_type, camera, event_data = self.event_queue.get(timeout=10) | ||
except queue.Empty: | ||
self.refresh_cache() | ||
continue | ||
|
||
self.refresh_cache() | ||
|
||
if event_type == 'start': | ||
self.events_in_process[event_data['id']] = event_data | ||
|
||
if event_type == 'end': | ||
if self.config[camera].get('save_clips', False) and len(self.cached_clips) > 0: | ||
self.create_clip(camera, event_data) | ||
del self.events_in_process[event_data['id']] | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters