Dashcam Viewer: Screen Recordings support and interface refresh (#160)

* init bootstrap 5.3

* bootstrap done

* watch screen records

* formatting

* syntax

* import

* specific path

* pathing

* text

* not needed

* sunnypilot home

* better name

* Revert "syntax"

This reverts commit 45c54a54ef.

* path

* fix miss "/" at end of path and show current screenrecord video

* Created the About page

* Display errors in a more communicative way

* enable <br> at error messages

* display title on top of the page

* link to download screenrecord

* Update README.md

* Update styles

* Navbar on top, don't block content

* Don't center and format

* Update CHANGELOGS.md

* Update About page

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
This commit is contained in:
AlexandreSato
2023-06-02 19:16:02 -03:00
committed by GitHub
parent a004024ef4
commit 1846814ac5
12 changed files with 256 additions and 151 deletions
+1
View File
@@ -1,5 +1,6 @@
sunnypilot - 0.9.2.x (2023-02-22)
========================
* Dashcam Viewer (native & screen recordings) via Browser support thanks to actuallylemoncurd, AlexandreSato, ntegan1, and royjr!
* Honda Clarity 2018-22 support thanks to mcallbosco, vanillagorillaa and wirelessnet2!
sunnypilot - Version Latest (2023-02-22)
+2 -2
View File
@@ -1,5 +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).
View dashcam footage and screen recordings 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.
The network can be set up by Wi-Fi, mobile hotspot, or tethering on the comma device. Navigate to http://tici:5050/ OR http://ipAddress:5050 to access the dashcam footage.
+45 -81
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
from flask import Flask, render_template, Response, request
from system.dashcamviewer.helpers import *
import os
import system.dashcamviewer.helpers as dashcam
from flask import Flask, render_template, Response, request, send_from_directory
from system.loggerd.config import ROOT as REALDATA
app = Flask(__name__)
@@ -14,10 +16,10 @@ def index_page():
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))
vidlist = "|".join(REALDATA + "/" + segment + "/" + file_name for segment in dashcam.segments_in_route(route))
def generate_buffered_stream():
with ffmpeg_mp4_concat_wrap_process_builder(vidlist, cameratype, chunk_size) as process:
with dashcam.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')
@@ -25,17 +27,16 @@ def full(cameratype, route):
@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')
if not dashcam.is_valid_segment(segment):
return render_template("error.html", error="invalid segment")
file_name = REALDATA + "/" + segment + "/" + cameratype + (".ts" if cameratype == "qcamera" else ".hevc")
return Response(dashcam.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"
return render_template("error.html", error="route not found")
if str(request.query_string) == "b''":
query_segment = str("0")
@@ -46,85 +47,48 @@ def route(route):
links = ""
segments = ""
for segment in segments_in_route(route):
for segment in dashcam.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>
"""
return render_template("route.html", route=route, query_type=query_type, links=links, segments=segments, query_segment=query_segment)
@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 footage():
return render_template("footage.html", rows=dashcam.all_routes())
@app.route("/screenrecords")
def screenrecords():
rows = dashcam.all_screenrecords()
if not rows:
return render_template("error.html", error="no screenrecords found at:<br><br>" + dashcam.SCREENRECORD_PATH)
return render_template("screenrecords.html", rows=rows, clip=rows[0])
@app.route("/screenrecords/<clip>")
def screenrecord(clip):
return render_template("screenrecords.html", rows=dashcam.all_screenrecords(), clip=clip)
@app.route("/screenrecords/play/pipe/<file>")
def videoscreenrecord(file):
file_name = dashcam.SCREENRECORD_PATH + file
return Response(dashcam.ffplay_mp4_wrap_process_builder(file_name).stdout.read(), status=200, mimetype='video/mp4')
@app.route("/screenrecords/download/<clip>")
def download_file(clip):
return send_from_directory(dashcam.SCREENRECORD_PATH, clip, as_attachment=True)
@app.route("/about")
def about():
return render_template("about.html")
def main():
app.run(host="0.0.0.0")
app.run(host="0.0.0.0", port=5050)
if __name__ == '__main__':
+30 -4
View File
@@ -1,13 +1,26 @@
import os
import subprocess
from system.loggerd.config import ROOT
from pathlib import Path
from system.hardware import PC
from system.loggerd.config import ROOT as REALDATA
from system.loggerd.uploader import listdir_by_creation
from tools.lib.route import SegmentName
# path to sunnypilot screen recordings
if PC:
SCREENRECORD_PATH = os.path.join(str(Path.home()), ".comma", "media", "0", "videos", "")
else:
SCREENRECORD_PATH = "/data/media/0/videos/"
def all_screenrecords():
return sorted(listdir_by_creation(SCREENRECORD_PATH), reverse=True)
def is_valid_segment(segment):
try:
segment_to_segment_name(ROOT, segment)
segment_to_segment_name(REALDATA, segment)
return True
except AssertionError:
return False
@@ -20,9 +33,9 @@ def segment_to_segment_name(data_dir, segment):
def all_segment_names():
segments = []
for segment in listdir_by_creation(ROOT):
for segment in listdir_by_creation(REALDATA):
try:
segments.append(segment_to_segment_name(ROOT, segment))
segments.append(segment_to_segment_name(REALDATA, segment))
except AssertionError:
pass
return segments
@@ -89,3 +102,16 @@ def ffmpeg_mp4_wrap_process_builder(filename):
return subprocess.Popen(
command_line, stdout=subprocess.PIPE
)
def ffplay_mp4_wrap_process_builder(file_name):
command_line = ["ffmpeg"]
command_line += ["-i", file_name]
command_line += ["-c", "copy"]
command_line += ["-map", "0"]
command_line += ["-f", "mp4"]
command_line += ["-movflags", "empty_moov"]
command_line += ["-"]
return subprocess.Popen(
command_line, stdout=subprocess.PIPE
)
-1
View File
@@ -1 +0,0 @@
/data/openpilot/selfdrive/assets/img_spinner_comma.png
+1
View File
@@ -0,0 +1 @@
../../../selfdrive/assets/img_spinner_comma.png
+18
View File
@@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block title %}
About
{% endblock %}
{% block main %}
<br>
<b>About</b>
<br><br>
<footer class="small text-center text-muted" style="word-wrap: break-word;">
Special thanks to:<br><br>
ntegan1<br>
royjr<br>
AlexandreSato<br>
actuallylemoncurd
</footer>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block title %}
About
{% endblock %}
{% block main %}
<br> Oopss
<br><br><br><br>
{{ error | safe }}
{% endblock %}
@@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block title %}
Dashcam Routes
{% endblock %}
{% block main %}
<br>
<b>Dashcam Routes</b>
<br><br>
{% for row in rows %}
<a href="footage/{{ row }}">{{ row }}</a><br>
{% endfor %}
{% endblock %}
+11 -63
View File
@@ -1,65 +1,13 @@
<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;
}
{% extends "layout.html" %}
.button1 {
background-color: white;
color: black;
border: 2px solid #4CAF50;
}
{% block title %}
Home
{% endblock %}
.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>
{% block main %}
<br>
<b>Dashcam Viewer</b>
<br><br>
<a href='/footage'>View Dashcam Footage</a><br>
<br><a href='/screenrecords'>View Screen Recordings</a><br>
{% endblock %}
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, width=device-width">
<link href="/static/favicon.ico" rel="icon">
<!-- http://getbootstrap.com/docs/5.3/ -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<title>sunnypilot: {% block title %}{% endblock %}</title>
</head>
<body>
<nav class="navbar navbar-fixed-top navbar-expand-sm navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand mb-0 h1">
<img class="d-inline-block align-top mr-2" src="/static/favicon.ico" width="30" height="30" /> sunnypilot </a>
<button type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" class="navbar-toggler" aria-controls="navbarNav" aria-expanded="false" arial-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse ml-auto" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active">
<a href="/footage" class="nav-link">&nbsp &nbsp &nbsp &nbsp Dashcam Routes</a>
</li>
<li class="nav-item active">
<a href="/screenrecords" class="nav-link">&nbsp &nbsp &nbsp &nbsp Screen Recordings</a>
</li>
<li class="nav-item active">
<a href="/about" class="nav-link">&nbsp &nbsp &nbsp &nbsp About</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid p-7 text-center"> {% block main %}{% endblock %} </main>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
{% extends "layout.html" %}
{% block title %}
Dashcam Segments
{% endblock %}
{% block main %}
{% autoescape false %}
<br>
<b>Dashcam Segments (one per minute)</b>
<br><br>
<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="{{ 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 }}
<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>
{% endautoescape %}
{% endblock %}
@@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block title %}
Screen Recordings
{% endblock %}
{% block main %}
<br>
<b>Screen Recordings</b>
<br><br>
<video id="video" width="320" height="240" controls autoplay="autoplay" style="background:black">
</video>
<br><br>
current view: <span id="mycurrentview"></span>
<br>
<a href="/screenrecords/download/{{ clip }}">download: {{ clip }}</a><br><br>
<script>
var video = document.getElementById("video");
var track = {
play: function() {
return ( "/screenrecords/play/pipe/" + '{{ clip }}' );
}
}
video.src = track.play();
document.getElementById("mycurrentview").textContent=video.src.split("/")[6];
</script>
{% for row in rows %}
<a href="/screenrecords/{{ row }}">{{ row }}</a><br>
{% endfor %}
{% endblock %}