diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index fc86e94e4d..9957a9a055 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -73,6 +73,7 @@ procs = [ PythonProcess("gpxd", "selfdrive.gpxd.gpxd"), PythonProcess("gpxd_uploader", "selfdrive.gpxd.gpx_uploader", offroad=True), PythonProcess("mapd", "selfdrive.mapd.mapd"), + PythonProcess("dashcam_viewer", "system.dashcamviewer.dashcam_viewer", onroad=False, offroad=True), # debug procs NativeProcess("bridge", "cereal/messaging", ["./bridge"], onroad=False, callback=notcar), diff --git a/system/dashcamviewer/README.md b/system/dashcamviewer/README.md new file mode 100644 index 0000000000..4b2574ac9b --- /dev/null +++ b/system/dashcamviewer/README.md @@ -0,0 +1,5 @@ +# Dashcam Viewer + +View dashcam footage by connecting to the comma device via the same network, with your mobile device or PC. Big thanks to [actuallylemoncurd](https://github.com/actuallylemoncurd), [AlexandreSato](https://github.com/alexandreSato), [ntegan1](https://github.com/ntegan1), and [royjr](https://github.com/royjr). + +The network can be set up by Wi-Fi, mobile hotspot, or tethering on the comma device. Navigate to http://tici:5000/ OR ipAddress:5000 to access the dashcam footage. diff --git a/system/dashcamviewer/dashcam_viewer.py b/system/dashcamviewer/dashcam_viewer.py new file mode 100644 index 0000000000..9686bfe1e8 --- /dev/null +++ b/system/dashcamviewer/dashcam_viewer.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +from flask import Flask, render_template, Response, request +from system.dashcamviewer.helpers import * + +app = Flask(__name__) + + +@app.route("/") +def index_page(): + return render_template("index.html") + + +@app.route("/footage/full//") +def full(cameratype, route): + chunk_size = 1024 * 512 # 5KiB + file_name = cameratype + (".ts" if cameratype == "qcamera" else ".hevc") + vidlist = "|".join(ROOT + "/" + segment + "/" + file_name for segment in segments_in_route(route)) + + def generate_buffered_stream(): + with ffmpeg_mp4_concat_wrap_process_builder(vidlist, cameratype, chunk_size) as process: + for chunk in iter(lambda: process.stdout.read(chunk_size), b""): + yield bytes(chunk) + return Response(generate_buffered_stream(), status=200, mimetype='video/mp4') + + +@app.route("/footage//") +def fcamera(cameratype, segment): + if not is_valid_segment(segment): + return "invalid segment" + file_name = ROOT + "/" + segment + "/" + cameratype + (".ts" if cameratype == "qcamera" else ".hevc") + + return Response(ffmpeg_mp4_wrap_process_builder(file_name).stdout.read(), status=200, mimetype='video/mp4') + + +@app.route("/footage/") +def route(route): + if len(route) != 20: + return "route not found" + + if str(request.query_string) == "b''": + query_segment = str("0") + query_type = "qcamera" + else: + query_segment = (str(request.query_string).split(","))[0][2:] + query_type = (str(request.query_string).split(","))[1][:-1] + + links = "" + segments = "" + for segment in segments_in_route(route): + links += ""+segment+"
" + segments += "'"+segment+"'," + return """ + + + + Dashcam Footage + + +
+ +

+ current segment: +
+ current view: +
+ download full route """ + query_type + """ +

+ back to routes +

+ qcamera - + fcamera - + dcamera - + ecamera +

+ """+links+""" +
+ + + +""" + + +@app.route("/footage") +def index(): + result = """ + + + + + Dashcam Footage + +

Back to landing page
""" + for route in all_routes(): + result += ""+route+"
" + result += """
""" + return result + + +def main(): + app.run(host="0.0.0.0") + + +if __name__ == '__main__': + main() diff --git a/system/dashcamviewer/helpers.py b/system/dashcamviewer/helpers.py new file mode 100644 index 0000000000..7e0ce2ce9f --- /dev/null +++ b/system/dashcamviewer/helpers.py @@ -0,0 +1,91 @@ +import os +import subprocess +from system.loggerd.config import ROOT +from system.loggerd.uploader import listdir_by_creation +from tools.lib.route import SegmentName + + +def is_valid_segment(segment): + try: + segment_to_segment_name(ROOT, segment) + return True + except AssertionError: + return False + + +def segment_to_segment_name(data_dir, segment): + fake_dongle = "ffffffffffffffff" + return SegmentName(str(os.path.join(data_dir, fake_dongle + "|" + segment))) + + +def all_segment_names(): + segments = [] + for segment in listdir_by_creation(ROOT): + try: + segments.append(segment_to_segment_name(ROOT, segment)) + except AssertionError: + pass + return segments + + +def all_routes(): + segment_names = all_segment_names() + route_names = [segment_name.route_name for segment_name in segment_names] + route_times = [route_name.time_str for route_name in route_names] + unique_routes = list(dict.fromkeys(route_times)) + return sorted(unique_routes, reverse=True) + + +def segments_in_route(route): + segment_names = [segment_name for segment_name in all_segment_names() if segment_name.time_str == route] + segments = [segment_name.time_str + "--" + str(segment_name.segment_num) for segment_name in segment_names] + return segments + + +def ffmpeg_mp4_concat_wrap_process_builder(file_list, cameratype, chunk_size=1024*512): + command_line = ["ffmpeg"] + if not cameratype == "qcamera": + command_line += ["-f", "hevc"] + command_line += ["-r", "20"] + command_line += ["-i", "concat:" + file_list] + command_line += ["-c", "copy"] + command_line += ["-map", "0"] + if not cameratype == "qcamera": + command_line += ["-vtag", "hvc1"] + command_line += ["-f", "mp4"] + command_line += ["-movflags", "empty_moov"] + command_line += ["-"] + return subprocess.Popen( + command_line, stdout=subprocess.PIPE, + bufsize=chunk_size + ) + + +def ffmpeg_mp4_wrap_process_builder(filename): + """Returns a process that will wrap the given filename + inside a mp4 container, for easier playback by browsers + and other devices. Primary use case is streaming segment videos + to the vidserver tool. + filename is expected to be a pathname to one of the following + /path/to/a/qcamera.ts + /path/to/a/dcamera.hevc + /path/to/a/ecamera.hevc + /path/to/a/fcamera.hevc + """ + basename = filename.rsplit("/")[-1] + extension = basename.rsplit(".")[-1] + command_line = ["ffmpeg"] + if extension == "hevc": + command_line += ["-f", "hevc"] + command_line += ["-r", "20"] + command_line += ["-i", filename] + command_line += ["-c", "copy"] + command_line += ["-map", "0"] + if extension == "hevc": + command_line += ["-vtag", "hvc1"] + command_line += ["-f", "mp4"] + command_line += ["-movflags", "empty_moov"] + command_line += ["-"] + return subprocess.Popen( + command_line, stdout=subprocess.PIPE + ) diff --git a/system/dashcamviewer/static/favicon.ico b/system/dashcamviewer/static/favicon.ico new file mode 100644 index 0000000000..d6b869f84c --- /dev/null +++ b/system/dashcamviewer/static/favicon.ico @@ -0,0 +1 @@ +/data/openpilot/selfdrive/assets/img_spinner_comma.png \ No newline at end of file diff --git a/system/dashcamviewer/templates/index.html b/system/dashcamviewer/templates/index.html new file mode 100644 index 0000000000..4704dbb244 --- /dev/null +++ b/system/dashcamviewer/templates/index.html @@ -0,0 +1,65 @@ + + + + + remote door locker + + + + +
+
Viem DashCam Footage
+
+ +