first commit
This commit is contained in:
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Serveur Flask - Stream vidéo Raspberry Pi HQ Camera M12
|
||||||
|
Compatible Raspberry Pi 4 et 5 avec Picamera2
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from flask import Flask, Response, render_template_string
|
||||||
|
|
||||||
|
from picamera2 import Picamera2
|
||||||
|
from picamera2.encoders import JpegEncoder
|
||||||
|
from picamera2.outputs import FileOutput
|
||||||
|
|
||||||
|
# ─── Configuration ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
RESOLUTION = (1280, 720) # Résolution du stream (largeur, hauteur)
|
||||||
|
FRAMERATE = 30 # Images par seconde
|
||||||
|
QUALITY = 85 # Qualité JPEG (1-100)
|
||||||
|
HOST = "0.0.0.0" # Ecoute sur toutes les interfaces réseau
|
||||||
|
PORT = 5000
|
||||||
|
|
||||||
|
# ─── Buffer de frames thread-safe ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class StreamOutput(io.BufferedIOBase):
|
||||||
|
def __init__(self):
|
||||||
|
self.frame = None
|
||||||
|
self.condition = threading.Condition()
|
||||||
|
|
||||||
|
def write(self, buf):
|
||||||
|
with self.condition:
|
||||||
|
self.frame = buf
|
||||||
|
self.condition.notify_all()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Initialisation caméra ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
picam2 = Picamera2()
|
||||||
|
|
||||||
|
video_config = picam2.create_video_configuration(
|
||||||
|
main={"size": RESOLUTION, "format": "RGB888"},
|
||||||
|
controls={"FrameRate": FRAMERATE}
|
||||||
|
)
|
||||||
|
picam2.configure(video_config)
|
||||||
|
|
||||||
|
output = StreamOutput()
|
||||||
|
encoder = JpegEncoder(q=QUALITY)
|
||||||
|
|
||||||
|
picam2.start_recording(encoder, FileOutput(output))
|
||||||
|
print(f"[✓] Caméra démarrée ({RESOLUTION[0]}x{RESOLUTION[1]} @ {FRAMERATE}fps)")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Générateur MJPEG ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def generate_frames():
|
||||||
|
"""Génère un flux MJPEG frame par frame."""
|
||||||
|
while True:
|
||||||
|
with output.condition:
|
||||||
|
output.condition.wait()
|
||||||
|
frame = output.frame
|
||||||
|
yield (
|
||||||
|
b"--frame\r\n"
|
||||||
|
b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 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 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template_string(
|
||||||
|
HTML_PAGE,
|
||||||
|
width=RESOLUTION[0],
|
||||||
|
height=RESOLUTION[1],
|
||||||
|
quality=QUALITY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/video_feed")
|
||||||
|
def video_feed():
|
||||||
|
"""Endpoint du flux MJPEG."""
|
||||||
|
return Response(
|
||||||
|
generate_frames(),
|
||||||
|
mimetype="multipart/x-mixed-replace; boundary=frame"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Lancement ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
print(f"[✓] Serveur démarré → http://{HOST}:{PORT}")
|
||||||
|
print(f" Accès local : http://localhost:{PORT}")
|
||||||
|
print(f" Accès réseau : http://<IP_DU_PI>:{PORT}")
|
||||||
|
app.run(host=HOST, port=PORT, threaded=True)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n[!] Arrêt du serveur...")
|
||||||
|
finally:
|
||||||
|
picam2.stop_recording()
|
||||||
|
picam2.close()
|
||||||
|
print("[✓] Caméra arrêtée proprement.")
|
||||||
Reference in New Issue
Block a user