WIP
This commit is contained in:
+17
-103
@@ -9,7 +9,7 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Flask, Response, jsonify, render_template_string
|
from flask import Flask, Response, jsonify, render_template, send_from_directory
|
||||||
|
|
||||||
from picamera2 import Picamera2
|
from picamera2 import Picamera2
|
||||||
from picamera2.encoders import JpegEncoder
|
from picamera2.encoders import JpegEncoder
|
||||||
@@ -70,106 +70,6 @@ def generate_frames():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ─── Interface HTML ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
HTML_PAGE = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Pi Camera Stream</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #0a0a0a;
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
letter-spacing: 0.3em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #00ff88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #555;
|
|
||||||
letter-spacing: 0.15em;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-container {
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid #1a1a1a;
|
|
||||||
box-shadow: 0 0 40px rgba(0, 255, 136, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-container::before {
|
|
||||||
content: '● REC';
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: #ff4444;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
z-index: 10;
|
|
||||||
animation: blink 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.2; }
|
|
||||||
}
|
|
||||||
|
|
||||||
img#stream {
|
|
||||||
display: block;
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 75vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #333;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer span {
|
|
||||||
color: #00ff88;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>Raspberry Pi Camera</h1>
|
|
||||||
<p class="subtitle">HQ Camera M12 — Live Stream</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="stream-container">
|
|
||||||
<img id="stream" src="/video_feed" alt="Camera stream">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
Résolution <span>{{ width }}×{{ height }}</span> —
|
|
||||||
Qualité <span>{{ quality }}%</span>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ─── Application Flask ────────────────────────────────────────────────────────
|
# ─── Application Flask ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -177,8 +77,8 @@ app = Flask(__name__)
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template_string(
|
return render_template(
|
||||||
HTML_PAGE,
|
"index.html",
|
||||||
width=RESOLUTION[0],
|
width=RESOLUTION[0],
|
||||||
height=RESOLUTION[1],
|
height=RESOLUTION[1],
|
||||||
quality=QUALITY
|
quality=QUALITY
|
||||||
@@ -200,6 +100,20 @@ def capture():
|
|||||||
return jsonify({"status": "ok", "file": filename})
|
return jsonify({"status": "ok", "file": filename})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/photos")
|
||||||
|
def photos():
|
||||||
|
files = sorted(
|
||||||
|
[f for f in os.listdir(PHOTOS_DIR) if f.endswith(".jpg")],
|
||||||
|
reverse=True
|
||||||
|
)[:5]
|
||||||
|
return jsonify(files)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/photo/<filename>")
|
||||||
|
def photo(filename):
|
||||||
|
return send_from_directory(PHOTOS_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/video_feed")
|
@app.route("/video_feed")
|
||||||
def video_feed():
|
def video_feed():
|
||||||
"""Endpoint du flux MJPEG."""
|
"""Endpoint du flux MJPEG."""
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
async function refreshGallery() {
|
||||||
|
const res = await fetch("/photos");
|
||||||
|
const files = await res.json();
|
||||||
|
const gallery = document.getElementById("gallery");
|
||||||
|
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
gallery.innerHTML = files
|
||||||
|
.map((f) => `<img src="/photo/${f}" alt="${f}" title="${f}">`)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", async (e) => {
|
||||||
|
if (e.key !== "c" && e.key !== "C") return;
|
||||||
|
await fetch("/capture");
|
||||||
|
refreshGallery();
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshGallery();
|
||||||
|
setInterval(refreshGallery, 10000);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #555;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-container {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #1a1a1a;
|
||||||
|
box-shadow: 0 0 40px rgba(0, 255, 136, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-container::before {
|
||||||
|
content: "● REC";
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #ff4444;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
z-index: 10;
|
||||||
|
animation: blink 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img#stream {
|
||||||
|
display: block;
|
||||||
|
max-width: 75vh;
|
||||||
|
max-height: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-empty {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #333;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery img {
|
||||||
|
width: 120px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #1a1a1a;
|
||||||
|
opacity: 0.75;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery img:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #333;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer span {
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Pi Camera Stream</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Raspberry Pi Camera</h1>
|
||||||
|
<p class="subtitle">HQ Camera M12 — Live Stream</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stream-container">
|
||||||
|
<img id="stream" src="/video_feed" alt="Camera stream">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="gallery" id="gallery">
|
||||||
|
<p class="gallery-empty">Aucune photo pour l'instant</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Résolution <span>{{ width }}×{{ height }}</span> —
|
||||||
|
Qualité <span>{{ quality }}%</span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user