├── src └── streamusb │ ├── __init__.py │ ├── __main__.py │ └── core.py ├── Makefile ├── requirements.txt ├── setup.py ├── services.yml ├── docker-build.sh ├── docker-build-multi.sh ├── Dockerfile ├── LICENSE └── README.md /src/streamusb/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .core import main 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-build: 2 | bash docker-build.sh 3 | 4 | docker-push: 5 | bash docker-build.sh push 6 | 7 | docker-build-multi: 8 | bash docker-build-multi.sh 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyGObject==3.42.0 2 | git+https://github.com/latonaio/rabbitmq-python-client.git@main#egg=rabbitmq_client 3 | git+https://github.com/latonaio/python-logging-library.git@main#egg=custom_logger 4 | -------------------------------------------------------------------------------- /src/streamusb/__main__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Copyright (c) 2019-2020 Latona. All rights reserved. 4 | 5 | import asyncio 6 | 7 | from . import main 8 | 9 | if __name__ == "__main__": 10 | asyncio.run(main()) 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Copyright (c) 2019-2020 Latona. All rights reserved. 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name="stream-usb-video-by-rtsp-multiple-camera", 9 | version="0.0.1", 10 | author="Latona", 11 | packages=find_packages("./src"), 12 | package_dir={"": "src"}, 13 | install_requires=[], 14 | tests_require=[] 15 | ) 16 | -------------------------------------------------------------------------------- /services.yml: -------------------------------------------------------------------------------- 1 | stream-usb-video-by-rtsp-multiple-camera: 2 | scale: 1 3 | startup: yes 4 | always: yes 5 | network: NodePort 6 | privileged: yes 7 | ports: 8 | - name: usb 9 | protocol: TCP 10 | port: 8555 11 | nodePort: 30055 12 | env: 13 | SUFFIX: 1 14 | RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/famanager 15 | QUEUE_ORIGIN: stream-usb-video-by-rtsp-multiple-camera-queue 16 | QUEUE_TO: template-matching-by-opencv-for-rtsp-queue 17 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PUSH=$1 4 | DATE="$(date "+%Y%m%d%H%M")" 5 | REPOSITORY_PREFIX="latonaio" 6 | SERVICE_NAME="stream-usb-video-by-rtsp-multiple-camera" 7 | 8 | DOCKER_BUILDKIT=1 docker build --progress=plain -t ${SERVICE_NAME}:"${DATE}" . 9 | # tagging 10 | docker tag ${SERVICE_NAME}:"${DATE}" ${SERVICE_NAME}:latest 11 | docker tag ${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" 12 | docker tag ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 13 | 14 | if [[ $PUSH == "push" ]]; then 15 | docker push ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" 16 | docker push ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 17 | fi 18 | -------------------------------------------------------------------------------- /docker-build-multi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PUSH=$1 4 | DATE="$(date "+%Y%m%d%H%M")" 5 | REPOSITORY_PREFIX="latonaio" 6 | SERVICE_NAME="stream-usb-video-by-rtsp-multiple-camera" 7 | 8 | DOCKER_BUILDKIT=1 docker build --progress=plain -t ${SERVICE_NAME}:"${DATE}" . 9 | # tagging 10 | docker tag ${SERVICE_NAME}:"${DATE}" ${SERVICE_NAME}:latest 11 | docker tag ${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" 12 | docker tag ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 13 | 14 | docker tag ${SERVICE_NAME}:"${DATE}" ${SERVICE_NAME}:latest 15 | docker tag ${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}-2:"${DATE}" 16 | docker tag ${REPOSITORY_PREFIX}/${SERVICE_NAME}-2:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}-2:latest 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM l4t-ds-opencv-7.2:latest 2 | # FROM latonaio/l4t-ds-opencv-7.2-jetpack-4.4:latest 3 | FROM python:3.9.9-bullseye 4 | 5 | # Definition of a Device & Service 6 | ENV POSITION=Runtime \ 7 | SERVICE=stream-usb-video-by-rtsp-multiple-camera \ 8 | AION_HOME=/var/lib/aion 9 | 10 | # Install dependencies 11 | RUN apt-get update && apt-get install -y \ 12 | pkg-config \ 13 | libcairo2-dev \ 14 | build-essential \ 15 | libgirepository1.0-dev \ 16 | libmariadb-dev \ 17 | libgstrtspserver-1.0-dev \ 18 | gstreamer1.0-rtsp \ 19 | && apt-get clean \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | RUN mkdir -p ${AION_HONE}/$POSITION/$SERVICE 23 | WORKDIR ${AION_HOME}/$POSITION/$SERVICE/ 24 | 25 | ADD . . 26 | 27 | RUN pip3 install -r requirements.txt 28 | 29 | RUN python3 setup.py install 30 | 31 | CMD ["python3","-m", "streamusb"] 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Latona, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stream-usb-video-by-rtsp-multiple-camera 2 | stream-usb-video-by-rtsp-multiple-cameraは、主にエッジコンピューティング環境において、接続された複数のUSBカメラから画像データを取得し、RTSP方式で他マイクロサービスに画像データを配信するマイクロサービスです。 3 | 配信は、RabbitMQにより行われます。 4 | 5 | # 動作環境 6 | stream-usb-video-by-rtsp-multiple-cameraは、aion-coreのプラットフォーム上での動作を前提としています。 7 | 使用する際は、事前に下記の通りAIONの動作環境を用意してください。 8 | 9 | * Linux OS 10 | * ARM/AMD/Intel 11 | * Kubernetes 12 | * AIONのリソース 13 | 14 | # カメラ設定 15 | 下記のpathに配置してある設定ファイルを読み込んで、auto_focusなどの設定を行います。 16 | 17 | `/var/lib/aion/Data/stream-usb-video-by-rtsp_{MS_NUMBER}/config.json` 18 | 19 | 現状では各設定はデフォルトとして以下の状態に設定されています。(変更不可) 20 | ``` 21 | auto_focus:True 22 | focus_absolute: 50 23 | ``` 24 | 25 | # I/O 26 | RabbitMQにより、下記のデータを入出力します。 27 | ### input 28 | 29 | * device_list: [check-multiple-camera-multiple-device-connection-kube](https://github.com/latonaio/check-multiple-camera-multiple-device-connection-kube)で生成された、接続されたデバイスのリスト 30 | * auto_focus: オートフォーカスの設定 31 | 32 | ### output 33 | - width: 解像度横幅 34 | - height: 解像度縦幅 35 | - framerate: 秒間フレームレート 36 | - addr:rtspで通信するためのuri。`rtsp://{SERVICE_NAME}-{MS_NUMBER}-srv:{ポート番号}`の形式になります。 37 | 38 | ※ ポート番号は、環境変数で指定したデフォルトの番号に、全登録デバイスの中のそのデバイスの登録順を加えたものが割り振られます。 39 | 40 | ※ docker環境では一律`localhost:{port}`に指定されます。 41 | 42 | 43 | ## 環境変数 44 | - WIDTH(解像度横幅,default: 864) 45 | - HEIGHT(解像度縦幅,default: 480) 46 | - FPSI(フレームレート,default: 10) 47 | - PORT(TSP通信ポート,default: 8554) 48 | - URI(RTSP通信アドレス,default:"/usb") 49 | - MS_NUMBER(aion-core全体の変数. 値はproject.yamlを参照してください) 50 | 51 | ## デプロイ on AION 52 | AION上でデプロイする場合、services.yamlに次の設定を追加してください。 53 | ``` 54 | stream-usb-video-by-rtsp-multiple-camera: 55 | scale: 1 56 | startup: yes 57 | always: yes 58 | network: NodePort 59 | privileged: yes 60 | ports: 61 | - name: usb 62 | protocol: TCP 63 | port: 8555 64 | nodePort: 30055 65 | env: 66 | SUFFIX: 1 67 | RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/famanager 68 | QUEUE_ORIGIN: stream-usb-video-by-rtsp-multiple-camera-queue 69 | QUEUE_TO: template-matching-by-opencv-for-rtsp-queue 70 | ``` -------------------------------------------------------------------------------- /src/streamusb/core.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Copyright (c) 2019-2020 Latona. All rights reserved. 4 | 5 | from asyncio import sleep 6 | import logging 7 | from multiprocessing import Process 8 | 9 | from custom_logger import init_logger 10 | from rabbitmq_client import RabbitmqClient 11 | 12 | import gi 13 | 14 | gi.require_version('Gst', '1.0') # noqa 15 | gi.require_version('GstRtspServer', '1.0') # noqa 16 | from gi.repository import GLib, Gst, GstRtspServer # isort:skip 17 | import os 18 | 19 | Gst.init(None) 20 | # Gst.debug_set_active(True) 21 | # Gst.debug_set_default_threshold(4) 22 | 23 | 24 | DEFAULT_WIDTH = 864 25 | DEFAULT_HEIGHT = 480 26 | DEFAULT_FPS = 10 27 | DEFAULT_PORT = 8554 28 | DEFAULT_URI = "/usb" 29 | 30 | SERVICE_NAME = "stream-usb-video-by-rtsp-multiple-camera" 31 | SUFFIX = int(os.environ.get('SUFFIX', 0)) 32 | SERVICE_NAME = SERVICE_NAME + '-' + str(SUFFIX) if SUFFIX > 1 else SERVICE_NAME 33 | 34 | RABBITMQ_URL = os.environ.get("RABBITMQ_URL") 35 | QUEUE_ORIGIN = os.environ.get("QUEUE_ORIGIN") 36 | QUEUE_TO = os.environ.get("QUEUE_TO") 37 | 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | 42 | def get_pipeline(width, height, fps): 43 | return f""" 44 | ( v4l2src io-mode=2 name=source ! 45 | image/jpeg, width={width}, height={height}, framerate={fps}/1 ! 46 | queue ! rtpjpegpay name=pay0 pt=96 ) 47 | """ 48 | 49 | 50 | class GstServer: 51 | def __init__(self, port, width, height, fps, device_path): 52 | port_str = str(port) 53 | pipe = get_pipeline(width, height, fps) 54 | self.pipe = None 55 | 56 | self.server = GstRtspServer.RTSPServer().new() 57 | self.server.set_service(port_str) 58 | self.server.connect("client-connected", self.client_connected) 59 | 60 | self.f = GstRtspServer.RTSPMediaFactory().new() 61 | self.f.set_eos_shutdown(True) 62 | self.f.set_launch(pipe) 63 | self.f.set_shared(True) 64 | self.f.connect("media-constructed", self.on_media_constructed) 65 | self.device_path = device_path 66 | 67 | m = self.server.get_mount_points() 68 | m.add_factory(DEFAULT_URI, self.f) 69 | self.server.attach(None) 70 | 71 | def start(self): 72 | loop = GLib.MainLoop() 73 | loop.run() 74 | self.stop() 75 | 76 | def client_connected(self, server, client): 77 | logger.info(f'[RTSP] next service is connected') 78 | 79 | def on_media_constructed(self, factory, media): 80 | # get camera path 81 | if self.device_path is None: 82 | logger.info("[RTSP] device is not connected") 83 | self.stop() 84 | return 85 | # get element state and check state 86 | self.pipe = media.get_element() 87 | appsrc = self.pipe.get_by_name('source') 88 | appsrc.set_property('device', self.device_path) 89 | 90 | self.pipe.set_state(Gst.State.PLAYING) 91 | ret, _, _ = self.pipe.get_state(Gst.CLOCK_TIME_NONE) 92 | if ret == Gst.StateChangeReturn.FAILURE: 93 | logger.info("[RTSP] cant connect to device: " + self.device_path) 94 | self.stop() 95 | else: 96 | logger.info(f"[RTSP] connect to device ({self.device_path})") 97 | 98 | def set_device_path(self, device_path): 99 | self.device_path = device_path 100 | 101 | def stop(self): 102 | if self.pipe is not None: 103 | self.pipe.send_event(Gst.Event.new_eos()) 104 | 105 | 106 | class DeviceData: 107 | process = None 108 | 109 | def __init__(self, serial, device_path, number, width, height, fps, is_docker, num): 110 | self.serial = serial 111 | port = DEFAULT_PORT + number 112 | self.addr = SERVICE_NAME + "-" + str(num).zfill(3) + "-srv:" + str(port) \ 113 | if is_docker else "localhost:" + str(port) 114 | self.addr += DEFAULT_URI 115 | self.server = GstServer(port, width, height, fps, device_path) 116 | 117 | self.process = Process(target=self.server.start) 118 | self.process.start() 119 | logger.info(f"[RTSP] ready at rtsp://{self.addr}") 120 | 121 | def get_serial(self): 122 | return self.serial 123 | 124 | def get_addr(self): 125 | return self.addr 126 | 127 | def set_device_path(self, device_path): 128 | self.server.set_device_path(device_path) 129 | 130 | def stop(self): 131 | self.server.stop() 132 | if self.process is not None: 133 | self.process.terminate() 134 | self.process.join() 135 | 136 | 137 | class DeviceDataList: 138 | device_data_list = {} 139 | previous_device_list = [] 140 | 141 | def start_rtsp_server(self, device_list: dict, scale: int, is_docker: bool, num: int): 142 | metadata_list = [] 143 | # start device list 144 | for serial, path in device_list.items(): 145 | logger.info(f"Get device data (serial: {serial}, path: {path})") 146 | # set device path 147 | if self.device_data_list.get(serial): 148 | self.device_data_list[serial].set_device_path(path) 149 | 150 | # check over scale or already set in device list 151 | if len(self.previous_device_list) >= scale or serial in self.previous_device_list: 152 | continue 153 | 154 | # add new camera connection 155 | width = DEFAULT_WIDTH 156 | height = DEFAULT_HEIGHT 157 | fps = DEFAULT_FPS 158 | 159 | self.previous_device_list.append(serial) 160 | 161 | output_num = len(self.previous_device_list) 162 | device_data = DeviceData(serial, path, output_num, width, height, fps, is_docker, num) 163 | 164 | logger.info("%s %s", self.previous_device_list, output_num) 165 | 166 | metadata = { 167 | "width": width, 168 | "height": height, 169 | "framerate": fps, 170 | "addr": device_data.get_addr(), 171 | } 172 | metadata_list.append((metadata, output_num)) 173 | return metadata_list 174 | 175 | def stop_all_device(self): 176 | for data in self.device_data_list: 177 | data.stop() 178 | 179 | 180 | async def main(): 181 | init_logger() 182 | 183 | client = await RabbitmqClient.create( 184 | RABBITMQ_URL, 185 | [QUEUE_ORIGIN], 186 | [QUEUE_TO] 187 | ) 188 | 189 | scale = os.environ.get("SCALE") 190 | scale = 2 if not isinstance(scale, int) or scale <= 0 else scale 191 | debug = os.environ.get("DEBUG") 192 | device = DeviceDataList() 193 | 194 | is_docker = True 195 | 196 | # for debug 197 | if debug: 198 | device.start_rtsp_server({"test": "/dev/video0"}, scale, is_docker, 1) 199 | while True: 200 | sleep(5) 201 | 202 | try: 203 | async for message in client.iterator(): 204 | try: 205 | async with message.process(): 206 | device_list = message.data.get("device_list") 207 | if not device_list: 208 | continue 209 | metadata_list = device.start_rtsp_server(device_list, scale, is_docker, 1) 210 | for metadata, num in metadata_list: 211 | logger.info(metadata, num) 212 | await client.send(QUEUE_TO, { 213 | "type": "start", 214 | "rtsp": metadata, 215 | }) 216 | # conn.output_kanban( 217 | # metadata={ 218 | # "type": "start", 219 | # "rtsp": metadata, 220 | # }, 221 | # process_number=num, 222 | # ) 223 | 224 | except Exception as e: 225 | logger.error(e) 226 | 227 | finally: 228 | device.stop_all_device() 229 | --------------------------------------------------------------------------------