├── .travis.yml ├── Dockerfile ├── build.sh ├── run.sh └── web_streaming.py /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | env: 5 | - DOCKER_IMAGE=pschmitt/picamera 6 | script: 7 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" 8 | - docker run --rm --privileged multiarch/qemu-user-static:register --reset 9 | - docker build -t "$DOCKER_IMAGE" . 10 | - docker push "$DOCKER_IMAGE" 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM resin/raspberry-pi-python:3 2 | 3 | LABEL maintainer "Philipp Schmitt " 4 | 5 | RUN READTHEDOCS=True pip install picamera 6 | 7 | COPY web_streaming.py /web_streaming.py 8 | 9 | ENV AUTH_USERNAME=pi AUTH_PASSWORD=picamera RESOLUTION=800x600 FRAMERATE=24 10 | 11 | ENTRYPOINT ["/usr/local/bin/python", "/web_streaming.py"] 12 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(readlink -f "$(dirname "$0")")" || exit 9 4 | 5 | docker build -t pschmitt/picamera . 6 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker run -d --restart=always \ 4 | --device /dev/vchiq \ 5 | -p 8000:8000 \ 6 | -e AUTH_USERNAME=pi \ 7 | -e AUTH_PASSWORD=picamera \ 8 | pschmitt/picamera 9 | -------------------------------------------------------------------------------- /web_streaming.py: -------------------------------------------------------------------------------- 1 | from http import server 2 | from threading import Condition 3 | import base64 4 | import io 5 | import logging 6 | import os 7 | import picamera 8 | import socketserver 9 | 10 | 11 | # Parameters 12 | AUTH_USERNAME = os.environ.get('AUTH_USERNAME', 'pi') 13 | AUTH_PASSWORD = os.environ.get('AUTH_PASSWORD', 'picamera') 14 | AUTH_BASE64 = base64.b64encode('{}:{}'.format( 15 | AUTH_USERNAME, AUTH_PASSWORD).encode('utf-8')) 16 | BASIC_AUTH = 'Basic {}'.format(AUTH_BASE64.decode('utf-8')) 17 | RESOLUTION = os.environ.get('RESOLUTION', '800x600').split('x') 18 | RESOLUTION_X = int(RESOLUTION[0]) 19 | RESOLUTION_Y = int(RESOLUTION[1]) 20 | FRAMERATE = int(os.environ.get('FRAMERATE', '24')) 21 | ROTATION = int(os.environ.get('ROTATE', 0)) 22 | HFLIP = os.environ.get('HFLIP', 'false').lower() == 'true' 23 | VFLIP = os.environ.get('VFLIP', 'false').lower() == 'true' 24 | 25 | PAGE = """\ 26 | 27 | 28 | picamera MJPEG streaming demo 29 | 30 | 31 |

PiCamera MJPEG Streaming Demo

32 | 33 | 34 | 35 | """.format(RESOLUTION_X, RESOLUTION_Y) 36 | 37 | 38 | class StreamingOutput(object): 39 | def __init__(self): 40 | self.frame = None 41 | self.buffer = io.BytesIO() 42 | self.condition = Condition() 43 | 44 | def write(self, buf): 45 | if buf.startswith(b'\xff\xd8'): 46 | # New frame, copy the existing buffer's content and notify all 47 | # clients it's available 48 | self.buffer.truncate() 49 | with self.condition: 50 | self.frame = self.buffer.getvalue() 51 | self.condition.notify_all() 52 | self.buffer.seek(0) 53 | return self.buffer.write(buf) 54 | 55 | 56 | class StreamingHandler(server.BaseHTTPRequestHandler): 57 | def do_GET(self): 58 | if self.headers.get('Authorization') is None: 59 | self.do_AUTHHEAD() 60 | self.wfile.write(b'no auth header received') 61 | elif self.headers.get('Authorization') == BASIC_AUTH: 62 | self.authorized_get() 63 | else: 64 | self.do_AUTHHEAD() 65 | self.wfile.write(b'not authenticated') 66 | 67 | def do_AUTHHEAD(self): 68 | self.send_response(401) 69 | self.send_header('WWW-Authenticate', 'Basic realm=\"picamera\"') 70 | self.send_header('Content-type', 'text/html') 71 | self.end_headers() 72 | 73 | def authorized_get(self): 74 | if self.path == '/': 75 | self.send_response(301) 76 | self.send_header('Location', '/index.html') 77 | self.end_headers() 78 | elif self.path == '/index.html': 79 | content = PAGE.encode('utf-8') 80 | self.send_response(200) 81 | self.send_header('Content-Type', 'text/html') 82 | self.send_header('Content-Length', len(content)) 83 | self.end_headers() 84 | self.wfile.write(content) 85 | elif self.path == '/stream.mjpg': 86 | self.send_response(200) 87 | self.send_header('Age', 0) 88 | self.send_header('Cache-Control', 'no-cache, private') 89 | self.send_header('Pragma', 'no-cache') 90 | self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME') 91 | self.end_headers() 92 | try: 93 | while True: 94 | with output.condition: 95 | output.condition.wait() 96 | frame = output.frame 97 | self.wfile.write(b'--FRAME\r\n') 98 | self.send_header('Content-Type', 'image/jpeg') 99 | self.send_header('Content-Length', len(frame)) 100 | self.end_headers() 101 | self.wfile.write(frame) 102 | self.wfile.write(b'\r\n') 103 | except Exception as e: 104 | logging.warning( 105 | 'Removed streaming client %s: %s', 106 | self.client_address, str(e)) 107 | else: 108 | self.send_error(404) 109 | self.end_headers() 110 | 111 | 112 | class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer): 113 | allow_reuse_address = True 114 | daemon_threads = True 115 | 116 | 117 | if __name__ == '__main__': 118 | res = '{}x{}'.format(RESOLUTION_X, RESOLUTION_Y) 119 | with picamera.PiCamera(resolution=res, framerate=FRAMERATE) as camera: 120 | output = StreamingOutput() 121 | camera.hflip = HFLIP 122 | camera.vflip = VFLIP 123 | camera.rotation = ROTATION 124 | camera.start_recording(output, format='mjpeg') 125 | try: 126 | address = ('', 8000) 127 | server = StreamingServer(address, StreamingHandler) 128 | server.serve_forever() 129 | finally: 130 | camera.stop_recording() 131 | --------------------------------------------------------------------------------