diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bed3c1b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/Library/Frameworks/Python.framework/Versions/3.7/bin/python3" +} \ No newline at end of file diff --git a/core/README.md b/core/README.md index aeda159..4e5274f 100644 --- a/core/README.md +++ b/core/README.md @@ -1,3 +1,17 @@ # Camlio Core Core module responsible for processing webcam stream + +## Start engine + +```bash +python engine.py --port 8081 --cert-file secrets/server.crt --key-file secrets/server.key --video_device_index=4 +``` + +### FAQs + +1. How to find the webcam index? + +```bash +ffmpeg -f avfoundation -list_devices true -i "" +``` diff --git a/core/engine.py b/core/engine.py new file mode 100644 index 0000000..6bdbfee --- /dev/null +++ b/core/engine.py @@ -0,0 +1,101 @@ +import argparse +import asyncio +import json +import logging +import os +import platform +import ssl + +import aiohttp_cors +from aiohttp import web +from aiortc import RTCPeerConnection, RTCSessionDescription +from aiortc.contrib.media import MediaPlayer + +from video_stream import CamlioVideoStreamTrack + +ROOT = os.path.dirname(__file__) + + +async def offer(request): + params = await request.json() + offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) + + pc = RTCPeerConnection() + pcs.add(pc) + + @pc.on("iceconnectionstatechange") + async def on_iceconnectionstatechange(): + print("ICE connection state is %s" % pc.iceConnectionState) + if pc.iceConnectionState == "failed": + await pc.close() + pcs.discard(pc) + + # open media source + if args.play_from: + player = MediaPlayer(args.play_from) + else: + player = None + + await pc.setRemoteDescription(offer) + for t in pc.getTransceivers(): + if t.kind == "audio" and player and player.audio: + pc.addTrack(player.audio) + elif t.kind == "video": + pc.addTrack(CamlioVideoStreamTrack(args.video_device_index)) + + answer = await pc.createAnswer() + await pc.setLocalDescription(answer) + + return web.Response( + content_type="application/json", + text=json.dumps( + {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type} + ), + ) + + +pcs = set() + + +async def on_shutdown(app): + # close peer connections + coros = [pc.close() for pc in pcs] + await asyncio.gather(*coros) + pcs.clear() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Camlio engine") + parser.add_argument("--video-device-index", + help="Index of video camera", type=int, default=0) + parser.add_argument("--cert-file", help="SSL certificate file (for HTTPS)") + parser.add_argument("--key-file", help="SSL key file (for HTTPS)") + parser.add_argument( + "--play-from", help="Read the media from a file and stream it."), + parser.add_argument( + "--port", type=int, default=8080, help="Port for HTTP server (default: 8080)" + ) + parser.add_argument("--verbose", "-v", action="count") + args = parser.parse_args() + + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + + if args.cert_file: + ssl_context = ssl.SSLContext() + ssl_context.load_cert_chain(args.cert_file, args.key_file) + else: + ssl_context = None + + app = web.Application() + cors = aiohttp_cors.setup(app, defaults={ + # Allow all to read all CORS-enabled resources from + # *. + "*": aiohttp_cors.ResourceOptions(expose_headers="*", + allow_headers="*"), + }) + app.on_shutdown.append(on_shutdown) + cors.add( + app.router.add_route("POST", "/offer", offer) + ) + web.run_app(app, port=args.port, ssl_context=ssl_context) diff --git a/core/requirements.txt b/core/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/core/video_stream.py b/core/video_stream.py new file mode 100644 index 0000000..bec72ec --- /dev/null +++ b/core/video_stream.py @@ -0,0 +1,22 @@ +import cv2 +import numpy +from aiortc import VideoStreamTrack +from av import VideoFrame + + +class CamlioVideoStreamTrack(VideoStreamTrack): + def __init__(self, video_device_index): + super().__init__() + self.video_capture = cv2.VideoCapture(video_device_index) + + async def recv(self): + try: + pts, time_base = await self.next_timestamp() + ret, raw = self.video_capture.read() + + frame = VideoFrame.from_ndarray(raw, format="bgr24") + frame.pts = pts + frame.time_base = time_base + return frame + except Exception as e: + print(e)