"""
极简 Flask:网页内直接嵌入 MJPEG 视频流(不需要 VNC 窗口)
- 通过 libcamera-vid 输出 MJPEG 到 stdout,再由 Flask 包成 multipart/x-mixed-replace
- 两路接口:/cam/0/mjpeg 与 /cam/1/mjpeg
- 主页内嵌 <img src=...> 连续刷新即可看到实时画面依赖:sudo apt-get updatesudo apt-get install -y python3-flask libcamera-apps运行:sudo python3 app.py --host 0.0.0.0 --port 8000浏览器打开 http://<Pi-IP>:8000/注意:
- 每个浏览器连接会各自启动一个 libcamera-vid 子进程,断开连接即结束。
- 如果提示相机被占用,请先关闭其它正在使用相机的进程(包含你开的 libcamera-hello 窗口)。
"""import os
import sys
import subprocess
from flask import Flask, Response, render_template_stringapp = Flask(__name__)INDEX_HTML = """
<!doctype html>
<html lang="zh">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Pi 双摄网页预览(MJPEG)</title><style>body{font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;margin:16px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}.card{border:1px solid #e5e7eb;border-radius:12px;padding:12px;box-shadow:0 1px 2px rgba(0,0,0,.04)}img{width:100%;height:auto;border-radius:8px;background:#000}small{color:#6b7280}</style>
</head>
<body><h1>Pi 双摄网页预览(MJPEG)</h1><div class="grid"><div class="card"><h3>Camera 0</h3><img src="/cam/0/mjpeg" alt="Camera 0"/><small>源:libcamera-vid --camera 0 --codec mjpeg -t 0 -o -</small></div><div class="card"><h3>Camera 1</h3><img src="/cam/1/mjpeg" alt="Camera 1"/><small>源:libcamera-vid --camera 1 --codec mjpeg -t 0 -o -</small></div></div>
</body>
</html>
"""def _mjpeg_stream(camera_index: int, width: int = 1280, height: int = 720):"""启动 libcamera-vid 输出 MJPEG 到 stdout,并逐帧切片为 multipart 片段。"""cmd = ["libcamera-vid","--camera", str(camera_index),"--codec", "mjpeg","--width", str(width),"--height", str(height),"-t", "0", "-o", "-", "--nopreview", ]proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)boundary = b"--FRAME"buffer = bytearray()SOI = b"\xff\xd8" EOI = b"\xff\xd9" try:while True:chunk = proc.stdout.read(4096)if not chunk:breakbuffer.extend(chunk)while True:soi = buffer.find(SOI)if soi < 0:if len(buffer) > 1024:del buffer[:-2]breakeoi = buffer.find(EOI, soi + 2)if eoi < 0:breakframe = bytes(buffer[soi:eoi + 2])del buffer[:eoi + 2]headers = (boundary + b"\r\n" +b"Content-Type: image/jpeg\r\n" +b"Content-Length: " + str(len(frame)).encode() + b"\r\n\r\n")yield headers + frame + b"\r\n"finally:try:proc.terminate()except Exception:pass@app.route("/")
def index():return render_template_string(INDEX_HTML)@app.route("/cam/<int:cam>/mjpeg")
def stream(cam: int):return Response(_mjpeg_stream(cam, width=1280, height=720),mimetype='multipart/x-mixed-replace; boundary=FRAME')if __name__ == '__main__':host = os.environ.get('HOST', '0.0.0.0')port = int(os.environ.get('PORT', '8000'))print(f"Serving on http://{host}:{port}")app.run(host=host, port=port, threaded=True, debug=False)
