Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4202afe30 | ||
|
|
ae208bd39b | ||
|
|
a38964a1b4 |
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
# Root TeslaCam directory containing RecentClips/SavedClips/SentryClips
|
||||||
|
TESLACAM_DIRECTORY=/mnt/TeslaCam
|
||||||
|
|
||||||
|
# Local dev server only (ignored when run under a WSGI server like gunicorn)
|
||||||
|
FLASK_RUN_HOST=0.0.0.0
|
||||||
|
FLASK_RUN_PORT=5000
|
||||||
|
FLASK_DEBUG=0
|
||||||
@@ -8,3 +8,4 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.github/AGENTS.md
|
.github/AGENTS.md
|
||||||
.github/instructions/
|
.github/instructions/
|
||||||
|
.env
|
||||||
@@ -1 +1,125 @@
|
|||||||
# TeslaCam View
|
# TeslaCam View
|
||||||
|
|
||||||
|
A Flask web app for browsing and playing TeslaCam (Tesla Dashcam/Sentry) recordings from a local drive. It scans a TeslaCam folder for video files, categorizes them by camera view, and presents them in a dark, Tesla-styled web interface with synchronized multi-angle playback.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Recursive scan** of the TeslaCam directory tree (`RecentClips`, `SavedClips`, `SentryClips`, etc.) for `.mp4` files matching the TeslaCam naming convention `YYYY-MM-DD_HH-MM-SS-${camera_view}.mp4`.
|
||||||
|
- **Categorization by camera view** — front, left repeater, right repeater, rear, left pillar, right pillar — with friendly display names.
|
||||||
|
- **Multi-angle synchronized playback** — clips sharing the same timestamp are grouped so you can switch between camera angles for the same moment.
|
||||||
|
- **Search & sort** — filter clips by filename, sort oldest-first or A–Z.
|
||||||
|
- **Safe streaming** — videos are served via Flask with path resolution relative to the configured TeslaCam root.
|
||||||
|
- **Dark Tesla-themed UI** built with responsive HTML/CSS (no build step).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```txt
|
||||||
|
tesla-cam-view-1/
|
||||||
|
├── videoplayer.py # Flask app: scanning, routing, video streaming
|
||||||
|
├── requirements.txt # Python dependencies (Flask)
|
||||||
|
├── templates/
|
||||||
|
│ ├── index.html # Main page: camera tabs, player, clip list
|
||||||
|
│ └── camera_view.html # Single-camera view page
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- Flask, python-dotenv, gunicorn (see `requirements.txt`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Create and activate a virtual environment (optional but recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
# Windows
|
||||||
|
venv\Scripts\activate
|
||||||
|
# macOS/Linux
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure the TeslaCam directory via environment variables. Copy the example
|
||||||
|
file and edit it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
TESLACAM_DIRECTORY=/mnt/TeslaCam
|
||||||
|
FLASK_RUN_HOST=0.0.0.0
|
||||||
|
FLASK_RUN_PORT=5000
|
||||||
|
FLASK_DEBUG=0
|
||||||
|
```
|
||||||
|
|
||||||
|
`TESLACAM_DIRECTORY` should point to the root folder containing your
|
||||||
|
`RecentClips`, `SavedClips`, and `SentryClips` subfolders. Variables set in
|
||||||
|
the real environment take precedence over `.env`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Local development server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python videoplayer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open [http://localhost:5000](http://localhost:5000) in your browser.
|
||||||
|
|
||||||
|
### Production (WSGI server on Linux)
|
||||||
|
|
||||||
|
For production, run the app behind a WSGI server such as **gunicorn**. The
|
||||||
|
`app` object in `videoplayer.py` is the WSGI entry point:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gunicorn -w 4 -b 0.0.0.0:5000 'videoplayer:app'
|
||||||
|
```
|
||||||
|
|
||||||
|
`TESLACAM_DIRECTORY` is read from the environment (or `.env`), so make sure it
|
||||||
|
is set for the gunicorn process, e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TESLACAM_DIRECTORY=/mnt/TeslaCam
|
||||||
|
gunicorn -w 4 -b 0.0.0.0:5000 'videoplayer:app'
|
||||||
|
```
|
||||||
|
|
||||||
|
For a systemd service or other process manager, set the variable in the unit
|
||||||
|
file / environment rather than relying on `.env`.
|
||||||
|
|
||||||
|
- The **main page** lists all clips grouped by camera view with a player and searchable clip list.
|
||||||
|
- Click a clip to play it; use the camera switch buttons to jump between angles for the same timestamp.
|
||||||
|
- The `/camera/<view>` route shows all clips for a single camera.
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
| Route | Description |
|
||||||
|
| ------------------------ | ---------------------------------------------------- |
|
||||||
|
| `/` | Main page with all videos categorized by camera view |
|
||||||
|
| `/camera/<camera_view>` | All videos for a single camera view |
|
||||||
|
| `/video/<path:filepath>` | Streams a video file from the TeslaCam directory |
|
||||||
|
|
||||||
|
## Camera Views
|
||||||
|
|
||||||
|
| Key | Display Name |
|
||||||
|
| ---------------- | -------------- |
|
||||||
|
| `front` | Front |
|
||||||
|
| `left_repeater` | Left Repeater |
|
||||||
|
| `right_repeater` | Right Repeater |
|
||||||
|
| `rear` | Rear |
|
||||||
|
| `left_pillar` | Left Pillar |
|
||||||
|
| `right_pillar` | Right Pillar |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Debug mode is off by default (`FLASK_DEBUG=0`) and is only used by the local
|
||||||
|
dev server. It has no effect when running under a WSGI server like gunicorn.
|
||||||
|
- The TeslaCam directory must be accessible from the machine running the app (e.g., a mounted USB drive or network share).
|
||||||
|
- Filename parsing assumes the standard TeslaCam naming format; files not matching the regex are ignored.
|
||||||
@@ -1 +1,3 @@
|
|||||||
flask
|
flask
|
||||||
|
python-dotenv
|
||||||
|
gunicorn
|
||||||
@@ -505,9 +505,9 @@
|
|||||||
>Right Repeater</a
|
>Right Repeater</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/camera/rear"
|
href="/camera/back"
|
||||||
class="{% if camera_view_key == 'rear' %}active{% endif %}"
|
class="{% if camera_view_key == 'back' %}active{% endif %}"
|
||||||
>Rear</a
|
>Back</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/camera/left_pillar"
|
href="/camera/left_pillar"
|
||||||
@@ -555,7 +555,6 @@
|
|||||||
<div class="list-toolbar">
|
<div class="list-toolbar">
|
||||||
<span id="resultCount">0 clips</span>
|
<span id="resultCount">0 clips</span>
|
||||||
<select id="sortSelect" aria-label="Sort options">
|
<select id="sortSelect" aria-label="Sort options">
|
||||||
<option value="newest">Newest first</option>
|
|
||||||
<option value="oldest">Oldest first</option>
|
<option value="oldest">Oldest first</option>
|
||||||
<option value="name">Filename A–Z</option>
|
<option value="name">Filename A–Z</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -601,7 +600,7 @@
|
|||||||
const resultCount = document.getElementById("resultCount");
|
const resultCount = document.getElementById("resultCount");
|
||||||
|
|
||||||
let query = "";
|
let query = "";
|
||||||
let sortMode = "newest";
|
let sortMode = "oldest";
|
||||||
let currentVideo = null;
|
let currentVideo = null;
|
||||||
|
|
||||||
function parseTimestamp(ts) {
|
function parseTimestamp(ts) {
|
||||||
|
|||||||
+15
-3
@@ -11,12 +11,18 @@ play videos from different camera views.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
from flask import Flask, render_template, send_from_directory, abort
|
from flask import Flask, render_template, send_from_directory, abort
|
||||||
|
|
||||||
|
# Load variables from a .env file if present (does not override real env vars)
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Root TeslaCam directory containing RecentClips, SavedClips, SentryClips, etc.
|
# Root TeslaCam directory containing RecentClips, SavedClips, SentryClips, etc.
|
||||||
TESLACAM_DIRECTORY = '/mnt/TeslaCam'
|
# Override via the TESLACAM_DIRECTORY environment variable (see .env.example).
|
||||||
|
TESLACAM_DIRECTORY = os.environ.get('TESLACAM_DIRECTORY', '/mnt/TeslaCam')
|
||||||
|
|
||||||
# Regular expression matching TeslaCam filenames: YYYY-MM-DD_HH-MM-SS-${camera_view}.mp4
|
# Regular expression matching TeslaCam filenames: YYYY-MM-DD_HH-MM-SS-${camera_view}.mp4
|
||||||
VIDEO_FILENAME_REGEX = re.compile(
|
VIDEO_FILENAME_REGEX = re.compile(
|
||||||
@@ -132,5 +138,11 @@ def serve_video(filepath):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
host = '0.0.0.0'
|
# Local dev server. For production, run behind a WSGI server instead, e.g.:
|
||||||
app.run(debug=True, host=host, port=5000)
|
# gunicorn -w 4 -b 0.0.0.0:5000 'videoplayer:app'
|
||||||
|
# or
|
||||||
|
# waitress-serve --host=0.0.0.0 --port=5000 videoplayer:app
|
||||||
|
host = os.environ.get('FLASK_RUN_HOST', '0.0.0.0')
|
||||||
|
port = int(os.environ.get('FLASK_RUN_PORT', '5000'))
|
||||||
|
debug = os.environ.get('FLASK_DEBUG', '0').lower() in ('1', 'true', 'yes')
|
||||||
|
app.run(debug=debug, host=host, port=port)
|
||||||
Reference in new issue
Block a user