mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-24 06:33:48 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Regular → Executable
+45
-81
@@ -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__':
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
/data/openpilot/selfdrive/assets/img_spinner_comma.png
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../selfdrive/assets/img_spinner_comma.png
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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">        Dashcam Routes</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/screenrecords" class="nav-link">        Screen Recordings</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/about" class="nav-link">        About</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container-fluid p-7 text-center"> {% block main %}{% endblock %} </main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user