mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
126 lines
3.8 KiB
Bash
126 lines
3.8 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Configurable via environment
|
|
export DEVICE="${DEVICE:-/dev/video0}"
|
|
export RESOLUTION="${RESOLUTION:-640x480}"
|
|
export QUALITY="${QUALITY:-10}" # ffmpeg MJPEG quality (lower is better)
|
|
export PORT="${PORT:-8088}"
|
|
export WORKDIR="${WORKDIR:-/run/roomcam}"
|
|
# Optional: set INPUT_FORMAT=bayer_grbg8 to transcode raw Bayer cams (e.g., OV534) to JPEG.
|
|
export INPUT_FORMAT="${INPUT_FORMAT:-mjpeg}"
|
|
export MJPEG_FPS="${MJPEG_FPS:-15}"
|
|
export MJPEG_QUALITY="${MJPEG_QUALITY:-8}"
|
|
|
|
mkdir -p "${WORKDIR}"
|
|
SNAPSHOT_PATH="${WORKDIR}/snapshot.jpg"
|
|
rm -f "${SNAPSHOT_PATH}"
|
|
# push
|
|
cleanup() {
|
|
[[ -n "${FFMPEG_PID:-}" ]] && kill "${FFMPEG_PID}" 2>/dev/null || true
|
|
[[ -n "${HTTP_PID:-}" ]] && kill "${HTTP_PID}" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 0' SIGTERM INT
|
|
|
|
FFMPEG_INPUT_ARGS=(-f v4l2 -input_format "${INPUT_FORMAT}" -video_size "${RESOLUTION}" -i "${DEVICE}")
|
|
FFMPEG_FILTERS=()
|
|
if [[ "${INPUT_FORMAT}" == bayer_* ]]; then
|
|
# Convert raw Bayer to a JPEG-friendly pixel format.
|
|
FFMPEG_FILTERS=(-pix_fmt yuv420p)
|
|
fi
|
|
|
|
/usr/bin/ffmpeg -y \
|
|
-loglevel warning -nostats \
|
|
"${FFMPEG_INPUT_ARGS[@]}" \
|
|
"${FFMPEG_FILTERS[@]}" \
|
|
-q:v "${QUALITY}" \
|
|
-f image2 -update 1 "${SNAPSHOT_PATH}" &
|
|
FFMPEG_PID=$!
|
|
|
|
cat > "${WORKDIR}/mjpeg_server.py" <<'PY'
|
|
import os
|
|
import subprocess
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
DEVICE = os.environ.get("DEVICE", "/dev/video0")
|
|
RESOLUTION = os.environ.get("RESOLUTION", "640x480")
|
|
INPUT_FORMAT = os.environ.get("INPUT_FORMAT", "mjpeg")
|
|
MJPEG_FPS = os.environ.get("MJPEG_FPS", "15")
|
|
MJPEG_QUALITY = os.environ.get("MJPEG_QUALITY", "8")
|
|
WORKDIR = os.environ.get("WORKDIR", "/run/roomcam")
|
|
SNAPSHOT_PATH = os.path.join(WORKDIR, "snapshot.jpg")
|
|
|
|
FFMPEG_INPUT_ARGS = [
|
|
"-f", "v4l2",
|
|
"-input_format", INPUT_FORMAT,
|
|
"-video_size", RESOLUTION,
|
|
"-i", DEVICE,
|
|
]
|
|
FFMPEG_FILTERS = []
|
|
if INPUT_FORMAT.startswith("bayer_"):
|
|
FFMPEG_FILTERS = ["-pix_fmt", "yuv420p"]
|
|
|
|
def spawn_mjpeg():
|
|
cmd = [
|
|
"/usr/bin/ffmpeg",
|
|
"-loglevel", "warning", "-nostats",
|
|
*FFMPEG_INPUT_ARGS,
|
|
*FFMPEG_FILTERS,
|
|
"-r", str(MJPEG_FPS),
|
|
"-q:v", str(MJPEG_QUALITY),
|
|
"-f", "mpjpeg",
|
|
"-",
|
|
]
|
|
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == "/" or self.path == "/snapshot.jpg":
|
|
try:
|
|
with open(SNAPSHOT_PATH, "rb") as fh:
|
|
data = fh.read()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "image/jpeg")
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
except FileNotFoundError:
|
|
self.send_error(404, "snapshot missing")
|
|
return
|
|
|
|
if self.path == "/stream.mjpg":
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=ffmpeg")
|
|
self.end_headers()
|
|
proc = spawn_mjpeg()
|
|
try:
|
|
while True:
|
|
chunk = proc.stdout.read(8192)
|
|
if not chunk:
|
|
break
|
|
self.wfile.write(chunk)
|
|
except BrokenPipeError:
|
|
pass
|
|
finally:
|
|
proc.kill()
|
|
return
|
|
|
|
self.send_error(404, "not found")
|
|
|
|
def log_message(self, format, *args):
|
|
return
|
|
|
|
def main():
|
|
addr = ("0.0.0.0", int(os.environ.get("PORT", "8088")))
|
|
ThreadingHTTPServer(addr, Handler).serve_forever()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
PY
|
|
|
|
/usr/bin/python3 -u "${WORKDIR}/mjpeg_server.py" &
|
|
HTTP_PID=$!
|
|
|
|
wait -n "${FFMPEG_PID}" "${HTTP_PID}"
|