Dashcam Viewer support (#155)

* add webserver + dashcam reviewer

* add offroad flag to webserver process

* fix process and rename tag in webpage

* add read me

* formatting & shadow names

* small cleanup

* +

* Force offroad only

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>

* Update function name

change from "hello_world" to "index"

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>

* Change webserver process name

now dashcam_viewer

* change process name to dashcam(_)viewer

* sort

* Update README.md

* Credit

* fix

* shadow

* name

* name

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
This commit is contained in:
Curtis Jenkins
2023-05-29 19:32:12 -04:00
committed by GitHub
parent 4fc2e41c47
commit 13616ab771
6 changed files with 294 additions and 0 deletions
+1
View File
@@ -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),
+5
View File
@@ -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.
+131
View File
@@ -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/<cameratype>/<route>")
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/<cameratype>/<segment>")
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/<route>")
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 += "<a href='"+route+"?"+segment.split("--")[2]+","+query_type+"'>"+segment+"</a><br>"
segments += "'"+segment+"',"
return """<html>
<head>
<meta name="viewport" content="initial-scale=1, width=device-width"/>
<link href="/static/favicon.ico" rel="icon">
<title>Dashcam Footage</title>
</head>
<body>
<center>
<video id="video" width="320" height="240" controls autoplay="autoplay" style="background:black">
</video>
<br><br>
current segment: <span id="currentsegment"></span>
<br>
current view: <span id="currentview"></span>
<br>
<a download=\""""+route+"-"+ query_type + ".mp4" + """\" href=\"/footage/full/"""+query_type+"""/"""+route+"""\">download full route """ + query_type + """</a>
<br><br>
<a href="/footage">back to routes</a>
<br><br>
<a href=\""""+route+"""?0,qcamera\">qcamera</a> -
<a href=\""""+route+"""?0,fcamera\">fcamera</a> -
<a href=\""""+route+"""?0,dcamera\">dcamera</a> -
<a href=\""""+route+"""?0,ecamera\">ecamera</a>
<br><br>
"""+links+"""
</center>
</body>
<script>
var video = document.getElementById('video');
var tracks = {
list: ["""+segments+"""],
index: """+query_segment+""",
next: function() {
if (this.index == this.list.length - 1) this.index = 0;
else {
this.index += 1;
}
},
play: function() {
return ( \""""+query_type+"""/" + this.list[this.index] );
}
}
video.addEventListener('ended', function(e) {
tracks.next();
video.src = tracks.play();
document.getElementById("currentsegment").textContent=video.src.split("/")[5];
document.getElementById("currentview").textContent=video.src.split("/")[4];
video.load();
video.play();
});
video.src = tracks.play();
document.getElementById("currentsegment").textContent=video.src.split("/")[5];
document.getElementById("currentview").textContent=video.src.split("/")[4];
</script>
</html>
"""
@app.route("/footage")
def index():
result = """
<html>
<head>
<meta name="viewport" content="initial-scale=1, width=device-width"/>
<link href="/static/favicon.ico" rel="icon">
<title>Dashcam Footage</title>
</head>
<body><center><br><a href='\\'>Back to landing page</a><br>"""
for route in all_routes():
result += "<a href='footage/"+route+"'>"+route+"</a><br>"
result += """</center></body></html>"""
return result
def main():
app.run(host="0.0.0.0")
if __name__ == '__main__':
main()
+91
View File
@@ -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
)
+1
View File
@@ -0,0 +1 @@
/data/openpilot/selfdrive/assets/img_spinner_comma.png
+65
View File
@@ -0,0 +1,65 @@
<html>
<head>
<meta name="viewport" content="initial-scale=1, width=device-width"/>
<link href="/static/favicon.ico" rel="icon">
<title>remote door locker</title>
<style>
.button {
border: none;
color: white;
padding: 16px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
transition-duration: 0.4s;
cursor: pointer;
}
.button1 {
background-color: white;
color: black;
border: 2px solid #4CAF50;
}
.button1:hover {
background-color: #4CAF50;
color: white;
}
.button2 {
background-color: white;
color: black;
border: 2px solid #008CBA;
}
.button2:hover {
background-color: #008CBA;
color: white;
}
.container {
height: 200px;
position: relative;
border: 3px solid green;
}
.center {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<center>
<br><a href='footage'>Viem DashCam Footage</a><br>
</center>
</body>
</html>