Skip to content

Commit

Permalink
Another MJPEG server example, this time with 90/270 degree rotation
Browse files Browse the repository at this point in the history
We perform the rotation by munging the exif header orientation.
Browsers I've tried seem to support this!

Signed-off-by: David Plowman <[email protected]>
  • Loading branch information
davidplowman committed Apr 18, 2024
1 parent 301eec8 commit adcefaa
Showing 1 changed file with 108 additions and 0 deletions.
108 changes: 108 additions & 0 deletions examples/mjpeg_server_with_rotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/python3

# This is the same as mjpeg_server_2.py, but allows 90 or 270 degree rotations.

import io
import logging
import socketserver
from http import server
from threading import Condition

import piexif

from picamera2 import Picamera2
from picamera2.encoders import MJPEGEncoder
from picamera2.outputs import FileOutput

ROTATION = 270 # Use 0, 90 or 270
WIDTH = 640
HEIGHT = 480

rotation_header = bytes()
if ROTATION:
WIDTH, HEIGHT = HEIGHT, WIDTH
code = 6 if ROTATION == 90 else 8
exif_bytes = piexif.dump({'0th': {piexif.ImageIFD.Orientation: code}})
exif_len = len(exif_bytes) + 2
rotation_header = bytes.fromhex('ffe1') + exif_len.to_bytes(2, 'big') + exif_bytes

PAGE = f"""\
<html>
<head>
<title>picamera2 MJPEG streaming demo</title>
</head>
<body>
<h1>Picamera2 MJPEG Streaming Demo</h1>
<img src="stream.mjpg" width="{WIDTH}" height="{HEIGHT}" />
</body>
</html>
"""


class StreamingOutput(io.BufferedIOBase):
def __init__(self):
self.frame = None
self.condition = Condition()

def write(self, buf):
with self.condition:
self.frame = buf[:2] + rotation_header + buf[2:]
self.condition.notify_all()


class StreamingHandler(server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.send_response(301)
self.send_header('Location', '/index.html')
self.end_headers()
elif self.path == '/index.html':
content = PAGE.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/stream.mjpg':
self.send_response(200)
self.send_header('Age', 0)
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
self.end_headers()
try:
while True:
with output.condition:
output.condition.wait()
frame = output.frame
self.wfile.write(b'--FRAME\r\n')
self.send_header('Content-Type', 'image/jpeg')
self.send_header('Content-Length', len(frame))
self.end_headers()
self.wfile.write(frame)
self.wfile.write(b'\r\n')
except Exception as e:
logging.warning(
'Removed streaming client %s: %s',
self.client_address, str(e))
else:
self.send_error(404)
self.end_headers()


class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
allow_reuse_address = True
daemon_threads = True


picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (640, 480)}))
output = StreamingOutput()
picam2.start_recording(MJPEGEncoder(), FileOutput(output))

try:
address = ('', 8000)
server = StreamingServer(address, StreamingHandler)
server.serve_forever()
finally:
picam2.stop_recording()

0 comments on commit adcefaa

Please sign in to comment.