mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-30 14:53:53 +08:00
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:
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
/data/openpilot/selfdrive/assets/img_spinner_comma.png
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user