{"id":15690537,"url":"https://github.com/pinto0309/rtspserver-ffmpeg","last_synced_at":"2025-09-13T08:15:08.956Z","repository":{"id":180044167,"uuid":"620756250","full_name":"PINTO0309/rtspserver-ffmpeg","owner":"PINTO0309","description":"Build an ffmpeg RTSP distribution server using an old alpine:3.8 Docker Image.","archived":false,"fork":false,"pushed_at":"2023-07-10T05:40:40.000Z","size":35,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-07T23:43:44.892Z","etag":null,"topics":["docker","docker-compose","ffmpeg","mp4","rtsp","rtsp-server"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PINTO0309.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-29T10:05:16.000Z","updated_at":"2023-11-08T07:19:10.000Z","dependencies_parsed_at":null,"dependency_job_id":"ca879345-bec3-4525-862d-533adf297154","html_url":"https://github.com/PINTO0309/rtspserver-ffmpeg","commit_stats":{"total_commits":9,"total_committers":1,"mean_commits":9.0,"dds":0.0,"last_synced_commit":"0f661462757de974156fec750d6650022d4c9298"},"previous_names":["pinto0309/rtspserver-ffmpeg"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PINTO0309%2Frtspserver-ffmpeg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PINTO0309%2Frtspserver-ffmpeg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PINTO0309%2Frtspserver-ffmpeg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PINTO0309%2Frtspserver-ffmpeg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PINTO0309","download_url":"https://codeload.github.com/PINTO0309/rtspserver-ffmpeg/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252973617,"owners_count":21834105,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","docker-compose","ffmpeg","mp4","rtsp","rtsp-server"],"created_at":"2024-10-03T18:11:22.860Z","updated_at":"2025-05-07T23:43:51.128Z","avatar_url":"https://github.com/PINTO0309.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rtspserver-ffmpeg\nBuild an ffmpeg RTSP distribution server using an old alpine:3.8 Docker Image.\n\nv4l2 version: https://github.com/PINTO0309/rtspserver-v4l2\n\n## 0. 前準備\n- `git clone`\n    ```bash\n    git clone https://github.com/PINTO0309/rtspserver-ffmpeg.git \u0026\u0026 cd rtspserver-ffmpeg\n    ```\n- `xxxx.mp4` ファイルを `rtspserver-ffmpeg` フォルダの直下にコピーしておく\n\n## 1. `docker compose` パターン\n- `docker compose` 経由でコンテナを起動\n    ```bash\n    docker compose up -d\n    ```\n- `docker compose` 経由でコンテナを起動したあとに `docker compose exec` コマンドを使用して `ffmpeg` によるRTSP配信開始指示、`-stream_loop -1` は無限ループ再生オプション\n    ```bash\n    docker compose exec rtspserver-ffmpeg \\\n    ffmpeg -re -stream_loop -1 -i xxxx.mp4 http://localhost:8090/feed.ffm\n    ```\n- `docker compose` 経由でコンテナを起動したあとに `docker compose exec` コマンドを使用して `ffmpeg` によるRTSP配信開始指示、`-stream_loop -1` は無限ループしないバージョン\n    ```bash\n    docker compose exec rtspserver-ffmpeg \\\n    ffmpeg -re -i xxxx.mp4 -b:v 1.5M http://localhost:8090/feed.ffm\n    ```\n- `docker compose` 経由でコンテナを起動したあとに配信用コンテナを終了\n    ```bash\n    docker compose down\n    ```\n\n## 2. `docker run` パターン\n- `docker build` で Docker Image をローカルに生成 (Docker HubからPullするだけで問題ない場合は実施不要)\n    ```bash\n    docker build -t pinto0309/rtspserver-ffmpeg:latest -f Dockerfile.ffmpegrtsp .\n    docker push pinto0309/rtspserver-ffmpeg:latest\n    ```\n- `docker run` 経由でデーモンとしてコンテナをバックエンド起動\n    ```bash\n    docker run --rm -d \\\n    -p 8554:8554 \\\n    -p 8090:8090 \\\n    -v ${PWD}/ffserver.conf:/etc/ffserver.conf \\\n    -v ${PWD}:/home/user/ \\\n    --net=host \\\n    --name rtspserver-ffmpeg \\\n    pinto0309/rtspserver-ffmpeg:latest\n    ```\n- `docker run` 経由でコンテナを起動したあとに `docker exec` コマンドを使用して `ffmpeg` によるRTSP配信開始指示、`-stream_loop -1` は無限ループ再生オプション\n    ```bash\n    docker exec rtspserver-ffmpeg \\\n    ffmpeg -re -stream_loop -1 -i xxxx.mp4 http://localhost:8090/feed.ffm\n    ```\n- `docker run` 経由でコンテナを起動したあとに `docker exec` コマンドを使用して `ffmpeg` によるRTSP配信開始指示、無限ループ再生しないバージョン\n    ```bash\n    docker exec rtspserver-ffmpeg \\\n    ffmpeg -re -i xxxx.mp4 -b:v 1.5M http://localhost:8090/feed.ffm\n    ```\n- `docker run` 経由でコンテナを起動したあとに配信用コンテナを終了\n    ```bash\n    docker stop rtspserver-ffmpeg\n    ```\n\n## 3. 配信映像の受信テスト\n- `vlc` を使用したRTSP配信内容の確認\n    ```bash\n    vlc rtsp://0.0.0.0:8554/unicast\n    ```\n- `opencv` を使用したRTSP配信内容の確認\n\n    \u003cdetails\u003e\u003csummary\u003evideo.py\u003c/summary\u003e\n\n    ```python:video.py\n    from pathlib import Path\n    from enum import Enum\n    from collections import deque\n    from urllib.parse import urlparse\n    import subprocess\n    import threading\n    import logging\n    import cv2\n    from typing import Tuple\n\n\n    LOGGER = logging.getLogger(__name__)\n    WITH_GSTREAMER = False # True\n\n\n    class Protocol(Enum):\n        IMAGE = 0\n        VIDEO = 1\n        CSI   = 2\n        V4L2  = 3\n        RTSP  = 4\n        HTTP  = 5\n\n\n    class VideoIO:\n        def __init__(\n            self,\n            output_size: Tuple,\n            input_uri: str,\n            output_uri: str=None,\n            output_fps: int=30,\n            input_resolution: Tuple=(640, 480),\n            frame_rate: int=30,\n            buffer_size: int=10,\n            proc_fps: int=30\n        ):\n            \"\"\"Class for video capturing and output saving.\n            Encoding, decoding, and scaling can be accelerated using the GStreamer backend.\n\n            Parameters\n            ----------\n            output_size : tuple\n                Width and height of each frame to output.\n            input_uri : str\n                URI to input stream. It could be image sequence (e.g. '%06d.jpg'), video file (e.g. 'file.mp4'),\n                MIPI CSI camera (e.g. 'csi://0'), USB/V4L2 camera (e.g. '/dev/video0'),\n                RTSP stream (e.g. 'rtsp://\u003cuser\u003e:\u003cpassword\u003e@\u003cip\u003e:\u003cport\u003e/\u003cpath\u003e'),\n                or HTTP live stream (e.g. 'http://\u003cuser\u003e:\u003cpassword\u003e@\u003cip\u003e:\u003cport\u003e/\u003cpath\u003e')\n            output_uri : str, optionals\n                URI to an output video file.\n            output_fps : int, optionals\n                Output video recording frame rate. Specify a value less than 30.\n            input_resolution : tuple, optional\n                Original resolution of the input source.\n                Useful to set a certain capture mode of a USB/CSI camera.\n            frame_rate : int, optional\n                Frame rate of the input source.\n                Required if frame rate cannot be deduced, e.g. image sequence and/or RTSP.\n                Useful to set a certain capture mode of a USB/CSI camera.\n            buffer_size : int, optional\n                Number of frames to buffer.\n                For live sources, a larger buffer drops less frames but increases latency.\n            proc_fps : int, optional\n                Estimated processing speed that may limit the capture interval `cap_dt`.\n                This depends on hardware and processing complexity.\n            \"\"\"\n            self.size = output_size\n            self.input_uri = input_uri\n            self.output_uri = output_uri\n            self.output_fps = output_fps\n            self.resolution = input_resolution\n            assert frame_rate \u003e 0\n            self.frame_rate = frame_rate\n            assert buffer_size \u003e= 1\n            self.buffer_size = buffer_size\n            assert proc_fps \u003e 0\n            self.proc_fps = proc_fps\n\n            self.protocol = self._parse_uri(self.input_uri)\n            if self.protocol == Protocol.V4L2:\n                result = subprocess.check_output(\n                    [\n                        'sudo', 'chmod', '777', fr'{self.input_uri[:-1]}0'\n                    ],\n                    stderr=subprocess.PIPE\n                ).decode('utf-8')\n                result = subprocess.check_output(\n                    [\n                        'sudo', 'chmod', '777', fr'{self.input_uri[:-1]}1'\n                    ],\n                    stderr=subprocess.PIPE\n                ).decode('utf-8')\n            self.is_live = self.protocol != Protocol.IMAGE and self.protocol != Protocol.VIDEO\n            if WITH_GSTREAMER:\n                self.source = cv2.VideoCapture(self._gst_cap_pipeline(), cv2.CAP_GSTREAMER)\n            else:\n                self.source = cv2.VideoCapture(self.input_uri)\n\n            self.frame_queue: deque = deque([], maxlen=self.buffer_size)\n            self.cond = threading.Condition()\n            self.exit_event = threading.Event()\n            self.cap_thread = threading.Thread(target=self._capture_frames)\n\n            ret, frame = self.source.read()\n            if not ret:\n                raise RuntimeError(f'Unable to read video stream: {self.input_uri}')\n            self.frame_queue.append(frame)\n\n            width = self.source.get(cv2.CAP_PROP_FRAME_WIDTH)\n            height = self.source.get(cv2.CAP_PROP_FRAME_HEIGHT)\n            self.cap_fps = self.source.get(cv2.CAP_PROP_FPS)\n            self.do_resize = (width, height) != self.size\n            if self.cap_fps == 0:\n                self.cap_fps = self.frame_rate # fallback to config if unknown\n            LOGGER.info('%dx%d stream @ %d FPS', width, height, self.cap_fps)\n\n            if self.output_uri is not None:\n                Path(self.output_uri).parent.mkdir(parents=True, exist_ok=True)\n                output_fps = 1 / self.cap_dt\n                if WITH_GSTREAMER:\n                    self.writer = cv2.VideoWriter(\n                        self._gst_write_pipeline(),\n                        cv2.CAP_GSTREAMER,\n                        0,\n                        fps=output_fps,\n                        frameSize=self.size,\n                        isColor=True\n                    )\n                else:\n                    fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n                    self.writer = cv2.VideoWriter(\n                        filename=self.output_uri,\n                        fourcc=fourcc,\n                        fps=output_fps if self.output_fps \u003e= output_fps else self.output_fps,\n                        frameSize=self.size,\n                        isColor=True,\n                    )\n\n        @property\n        def cap_dt(self):\n            # limit capture interval at processing latency for live sources\n            return 1 / min(self.cap_fps, self.proc_fps) if self.is_live else 1 / self.cap_fps\n\n        def start_capture(self):\n            \"\"\"Start capturing from file or device.\"\"\"\n            if not self.source.isOpened():\n                self.source.open(self._gst_cap_pipeline(), cv2.CAP_GSTREAMER)\n            if not self.cap_thread.is_alive():\n                self.cap_thread.start()\n\n        def stop_capture(self):\n            \"\"\"Stop capturing from file or device.\"\"\"\n            with self.cond:\n                self.exit_event.set()\n                self.cond.notify()\n            self.frame_queue.clear()\n            self.cap_thread.join()\n\n        def read(self):\n            \"\"\"Reads the next video frame.\n\n            Returns\n            -------\n            ndarray\n                Returns None if there are no more frames.\n            \"\"\"\n            with self.cond:\n                while len(self.frame_queue) == 0 and not self.exit_event.is_set():\n                    self.cond.wait()\n                if len(self.frame_queue) == 0 and self.exit_event.is_set():\n                    return None\n                frame = self.frame_queue.popleft()\n                self.cond.notify()\n            if self.do_resize:\n                frame = cv2.resize(frame, self.size)\n            return frame\n\n        def write(self, frame):\n            \"\"\"Writes the next video frame.\"\"\"\n            assert hasattr(self, 'writer')\n            self.writer.write(frame)\n\n        def release(self):\n            \"\"\"Cleans up input and output sources.\"\"\"\n            self.stop_capture()\n            if hasattr(self, 'writer'):\n                self.writer.release()\n            self.source.release()\n\n        def _gst_cap_pipeline(self):\n            gst_elements = str(subprocess.check_output('gst-inspect-1.0'))\n            if 'nvvidconv' in gst_elements and self.protocol != Protocol.V4L2:\n                # format conversion for hardware decoder\n                cvt_pipeline = (\n                    'nvvidconv interpolation-method=5 ! '\n                    'video/x-raw, width=%d, height=%d, format=BGRx !'\n                    'videoconvert ! appsink sync=false'\n                    % self.size\n                )\n            else:\n                cvt_pipeline = (\n                    'videoscale ! '\n                    'video/x-raw, width=%d, height=%d !'\n                    'videoconvert ! appsink sync=false'\n                    % self.size\n                )\n\n            if self.protocol == Protocol.IMAGE:\n                pipeline = (\n                    'multifilesrc location=%s index=1 caps=\"image/%s,framerate=%d/1\" ! decodebin ! '\n                    % (\n                        self.input_uri,\n                        self._img_format(self.input_uri),\n                        self.frame_rate\n                    )\n                )\n            elif self.protocol == Protocol.VIDEO:\n                pipeline = 'filesrc location=%s ! decodebin ! ' % self.input_uri\n            elif self.protocol == Protocol.CSI:\n                if 'nvarguscamerasrc' in gst_elements:\n                    pipeline = (\n                        'nvarguscamerasrc sensor_id=%s ! '\n                        'video/x-raw(memory:NVMM), width=%d, height=%d, '\n                        'format=NV12, framerate=%d/1 ! '\n                        % (\n                            self.input_uri[6:],\n                            *self.resolution,\n                            self.frame_rate\n                        )\n                    )\n                else:\n                    raise RuntimeError('GStreamer CSI plugin not found')\n            elif self.protocol == Protocol.V4L2:\n                if 'v4l2src' in gst_elements:\n                    pipeline = (\n                        'v4l2src device=%s ! '\n                        'video/x-raw, width=%d, height=%d, '\n                        'format=YUY2, framerate=%d/1 ! '\n                        % (\n                            self.input_uri,\n                            *self.resolution,\n                            self.frame_rate\n                        )\n                    )\n                else:\n                    raise RuntimeError('GStreamer V4L2 plugin not found')\n            elif self.protocol == Protocol.RTSP:\n                pipeline = (\n                    'rtspsrc location=%s latency=0 ! '\n                    'capsfilter caps=application/x-rtp,media=video ! decodebin ! ' % self.input_uri\n                )\n            elif self.protocol == Protocol.HTTP:\n                pipeline = 'souphttpsrc location=%s is-live=true ! decodebin ! ' % self.input_uri\n\n            \"\"\"\n            'v4l2src device=/dev/video0 ! video/x-raw, width=640, height=480, format=YUY2, framerate=30/1 ! videoscale ! video/x-raw, width=640, height=480 !videoconvert ! appsink sync=false'\n            \"\"\"\n            return pipeline + cvt_pipeline\n\n        def _gst_write_pipeline(self):\n            gst_elements = str(subprocess.check_output('gst-inspect-1.0'))\n            # use hardware encoder if found\n            if 'omxh264enc' in gst_elements:\n                h264_encoder = 'omxh264enc preset-level=2'\n            elif 'x264enc' in gst_elements:\n                h264_encoder = 'x264enc pass=4'\n            else:\n                raise RuntimeError('GStreamer H.264 encoder not found')\n            pipeline = (\n                'appsrc ! autovideoconvert ! %s ! qtmux ! filesink location=%s '\n                % (\n                    h264_encoder,\n                    self.output_uri\n                )\n            )\n            return pipeline\n\n        def _capture_frames(self):\n            while not self.exit_event.is_set():\n                ret, frame = self.source.read()\n                with self.cond:\n                    if not ret:\n                        self.exit_event.set()\n                        self.cond.notify()\n                        break\n                    # keep unprocessed frames in the buffer for file\n                    if not self.is_live:\n                        while (len(self.frame_queue) == self.buffer_size and\n                               not self.exit_event.is_set()):\n                            self.cond.wait()\n                    self.frame_queue.append(frame)\n                    self.cond.notify()\n\n        @staticmethod\n        def _parse_uri(uri):\n            result = urlparse(uri)\n            if result.scheme == 'csi':\n                protocol = Protocol.CSI\n            elif result.scheme == 'rtsp':\n                protocol = Protocol.RTSP\n            elif result.scheme == 'http':\n                protocol = Protocol.HTTP\n            else:\n                if '/dev/video' in result.path:\n                    protocol = Protocol.V4L2\n                elif '%' in result.path:\n                    protocol = Protocol.IMAGE\n                else:\n                    protocol = Protocol.VIDEO\n            return protocol\n\n        @staticmethod\n        def _img_format(uri):\n            img_format = Path(uri).suffix[1:]\n            return 'jpeg' if img_format == 'jpg' else img_format\n    ```\n\n    \u003c/details\u003e\n\n    \u003cdetails\u003e\u003csummary\u003etest_rtsp_recv.py\u003c/summary\u003e\n\n    ```python:test_rtsp_recv.py\n    import cv2\n    from videoio import VideoIO\n\n    stream = VideoIO(\n        output_size=(\n            640,\n            480\n        ),\n        input_uri=f'rtsp://0.0.0.0:8554/unicast',\n        output_uri=None,\n        output_fps=30,\n        input_resolution=(\n            640,\n            480\n        ),\n        frame_rate=30,\n        buffer_size=10,\n    )\n    stream.start_capture()\n\n    try:\n        while True:\n            frame = stream.read()\n            key = cv2.waitKey(1)\n            if key == 27:  # ESC\n                break\n            if frame is None:\n                continue\n            cv2.imshow('test', frame)\n    except Exception as ex:\n        pass\n    finally:\n        if stream:\n            stream.release()\n    ```\n\n    \u003c/details\u003e\n\n    ```bash\n    python test_rtsp_recv.py\n    ```\n    ![image](https://user-images.githubusercontent.com/33194443/228771434-5dd72a81-91b5-487c-a853-b8a8ad0e0f67.png)\n\n## 4. 謝辞\n1. https://geek.tacoskingdom.com/blog/48\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpinto0309%2Frtspserver-ffmpeg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpinto0309%2Frtspserver-ffmpeg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpinto0309%2Frtspserver-ffmpeg/lists"}