├── .github └── workflows │ ├── codeql-analysis.yml │ └── publish-docker.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── RTSPtoWeb.go ├── SECURITY.md ├── apiHTTPChannel.go ├── apiHTTPHLS.go ├── apiHTTPHLSLL.go ├── apiHTTPMSE.go ├── apiHTTPRouter.go ├── apiHTTPSaveMP4.go ├── apiHTTPServer.go ├── apiHTTPStream.go ├── apiHTTPWebRTC.go ├── config.json ├── docs ├── api.md └── examples │ ├── hls │ └── index.html │ ├── hlsll │ └── index.html │ ├── mse │ ├── index.html │ └── main.js │ └── webrtc │ ├── index.html │ └── main.js ├── go.mod ├── go.sum ├── hlsFragment.go ├── hlsMuxer.go ├── hlsSegment.go ├── loggingLog.go ├── renovate.json ├── server.crt ├── server.key ├── serverRTSP.go ├── storageClient.go ├── storageConfig.go ├── storageServer.go ├── storageStream.go ├── storageStreamChannel.go ├── storageStreamHLS.go ├── storageStruct.go ├── streamCore.go ├── streamRemoteAuthorization.go ├── supportFunc.go ├── test.bytes ├── test.curl ├── test_multi.curl └── web ├── static ├── css │ ├── adminlte.min.css │ ├── adminlte.min.css.map │ ├── fonts │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7jsDJB9cme_xc.woff2 │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7ksDJB9cme_xc.woff2 │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDJB9cme.woff2 │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7osDJB9cme_xc.woff2 │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJB9cme_xc.woff2 │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7qsDJB9cme_xc.woff2 │ │ ├── 6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7rsDJB9cme_xc.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lujVj9_mf.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lujVj9_mf.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lujVj9_mf.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lujVj9_mf.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lujVj9_mf.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lujVj9_mf.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwkxdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlBdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu3cOWxw.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmBdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmRdu3cOWxy40.woff2 │ │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmhdu3cOWxy40.woff2 │ │ └── 6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmxdu3cOWxy40.woff2 │ ├── fullmulti.css │ ├── google-fonts.css │ └── index.css ├── img │ ├── back.jpg │ ├── green.jpg │ ├── loader.svg │ ├── noimage.svg │ ├── pic.svg │ ├── red.jpg │ └── white.jpg ├── js │ ├── RtspToWeb.js │ ├── adminlte.min.js │ ├── adminlte.min.js.map │ └── index.js └── plugins │ ├── bootstrap │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── fontawesome-free │ ├── css │ │ ├── all.css │ │ ├── all.min.css │ │ ├── brands.css │ │ ├── brands.min.css │ │ ├── fontawesome.css │ │ ├── fontawesome.min.css │ │ ├── regular.css │ │ ├── regular.min.css │ │ ├── solid.css │ │ ├── solid.min.css │ │ ├── svg-with-js.css │ │ ├── svg-with-js.min.css │ │ ├── v4-shims.css │ │ └── v4-shims.min.css │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ ├── hlsjs │ ├── hls.min.js │ └── hls.min.js.map │ ├── jquery │ ├── core.js │ ├── jquery.js │ ├── jquery.min.js │ ├── jquery.min.map │ ├── jquery.slim.js │ ├── jquery.slim.min.js │ └── jquery.slim.min.map │ └── sweetalert2 │ ├── sweetalert2.all.js │ ├── sweetalert2.all.min.js │ ├── sweetalert2.css │ ├── sweetalert2.js │ ├── sweetalert2.min.css │ └── sweetalert2.min.js └── templates ├── add_stream.tmpl ├── documentation.tmpl ├── edit_stream.tmpl ├── foot.tmpl ├── fullscreenmulti.tmpl ├── head.tmpl ├── index.tmpl ├── multiview.tmpl ├── play_all.tmpl ├── play_hls.tmpl ├── play_mse.tmpl ├── play_webrtc.tmpl ├── player.tmpl └── stream_list.tmpl /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '36 4 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish Docker image 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3.6.0 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3.10.0 27 | 28 | - name: Docker Login 29 | uses: docker/login-action@v3.4.0 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Docker Metadata action 36 | id: meta 37 | uses: docker/metadata-action@v5.7.0 38 | with: 39 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 40 | 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@v5.4.0 43 | with: 44 | context: . 45 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 46 | push: true 47 | tags: ${{ steps.meta.outputs.tags }} 48 | labels: ${{ steps.meta.outputs.labels }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | RTSPtoWeb -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM --platform=${BUILDPLATFORM} golang:1.19-alpine3.15 AS builder 4 | 5 | RUN apk add git 6 | 7 | WORKDIR /go/src/app 8 | COPY . . 9 | 10 | ARG TARGETOS TARGETARCH TARGETVARIANT 11 | 12 | ENV CGO_ENABLED=0 13 | RUN go get \ 14 | && go mod download \ 15 | && GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#"v"} go build -a -o rtsp-to-web 16 | 17 | FROM alpine:3.22 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=builder /go/src/app/rtsp-to-web /app/ 22 | COPY --from=builder /go/src/app/web /app/web 23 | 24 | RUN mkdir -p /config 25 | COPY --from=builder /go/src/app/config.json /config 26 | 27 | ENV GO111MODULE="on" 28 | ENV GIN_MODE="release" 29 | 30 | CMD ["./rtsp-to-web", "--config=/config/config.json"] 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrey Semochkin 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP=RTSPtoWeb 2 | SERVER_FLAGS ?= -config config.json 3 | 4 | P="\\033[34m[+]\\033[0m" 5 | 6 | build: 7 | @echo "$(P) build" 8 | GO111MODULE=on go build *.go 9 | 10 | run: 11 | @echo "$(P) run" 12 | GO111MODULE=on go run *.go 13 | 14 | serve: 15 | @$(MAKE) server 16 | 17 | server: 18 | @echo "$(P) server $(SERVER_FLAGS)" 19 | ./${APP} $(SERVER_FLAGS) 20 | 21 | test: 22 | @echo "$(P) test" 23 | bash test.curl 24 | bash test_multi.curl 25 | 26 | lint: 27 | @echo "$(P) lint" 28 | go vet 29 | 30 | .NOTPARALLEL: 31 | 32 | .PHONY: build run server test lint 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTSPtoWeb share you ip camera to world! 2 | 3 | RTSPtoWeb converts your RTSP streams to formats consumable in a web browser 4 | like MSE (Media Source Extensions), WebRTC, or HLS. It's fully native Golang 5 | without the use of FFmpeg or GStreamer! 6 | 7 | ## Table of Contents 8 | 9 | - [Installation](#installation) 10 | - [Configuration](#configuration) 11 | - [Command-line](#command-line) 12 | - [API documentation](#api-documentation) 13 | - [Limitations](#limitations) 14 | - [Performance](#performance) 15 | - [Authors](#authors) 16 | - [License](#license) 17 | 18 | ## Installation 19 | 20 | ### Installation from source 21 | 22 | 1. Download source 23 | ```bash 24 | $ git clone https://github.com/deepch/RTSPtoWeb 25 | ``` 26 | 1. CD to Directory 27 | ```bash 28 | $ cd RTSPtoWeb/ 29 | ``` 30 | 1. Test Run 31 | ```bash 32 | $ GO111MODULE=on go run *.go 33 | ``` 34 | 1. Open Browser 35 | ```bash 36 | open web browser http://127.0.0.1:8083 work chrome, safari, firefox 37 | ``` 38 | 39 | ## Installation from docker 40 | 41 | 1. Run docker container 42 | ```bash 43 | $ docker run --name rtsp-to-web --network host ghcr.io/deepch/rtsptoweb:latest 44 | ``` 45 | 1. Open Browser 46 | ```bash 47 | open web browser http://127.0.0.1:8083 in chrome, safari, firefox 48 | ``` 49 | 50 | You may override the configuration `/PATH_TO_CONFIG/config.json` and mount as a docker volume: 51 | 52 | ```bash 53 | $ docker run --name rtsp-to-web \ 54 | -v /PATH_TO_CONFIG/config.json:/config/config.json \ 55 | --network host \ 56 | ghcr.io/deepch/rtsptoweb:latest 57 | ``` 58 | 59 | ## Configuration 60 | 61 | ### Server settings 62 | 63 | ```text 64 | debug - enable debug output 65 | log_level - log level (trace, debug, info, warning, error, fatal, or panic) 66 | 67 | http_demo - serve static files 68 | http_debug - debug http api server 69 | http_login - http auth login 70 | http_password - http auth password 71 | http_port - http server port 72 | http_dir - path to serve static files from 73 | ice_servers - array of servers to use for STUN/TURN 74 | ice_username - username to use for STUN/TURN 75 | ice_credential - credential to use for STUN/TURN 76 | webrtc_port_min - minimum WebRTC port to use (UDP) 77 | webrtc_port_max - maximum WebRTC port to use (UDP) 78 | 79 | https 80 | https_auto_tls 81 | https_auto_tls_name 82 | https_cert 83 | https_key 84 | https_port 85 | 86 | rtsp_port - rtsp server port 87 | ``` 88 | 89 | ### Stream settings 90 | 91 | ```text 92 | name - stream name 93 | ``` 94 | 95 | ### Channel settings 96 | 97 | ```text 98 | name - channel name 99 | url - channel rtsp url 100 | on_demand - stream mode static (run any time) or ondemand (run only has viewers) 101 | debug - enable debug output (RTSP client) 102 | audio - enable audio 103 | status - default stream status 104 | ``` 105 | 106 | #### Authorization play video 107 | 108 | 1 - enable config 109 | 110 | ```text 111 | "token": { 112 | "enable": true, 113 | "backend": "http://127.0.0.1/file.php" 114 | } 115 | ``` 116 | 117 | 2 - try 118 | 119 | ```text 120 | rtsp://127.0.0.1:5541/demo/0?token=you_key 121 | ``` 122 | 123 | file.php need response json 124 | 125 | ```text 126 | status: "1" or "0" 127 | ``` 128 | 129 | #### RTSP pull modes 130 | 131 | * **on demand** (on_demand=true) - only pull video from the source when there's a viewer 132 | * **static** (on_demand=false) - pull video from the source constantly 133 | 134 | ### Example config.json 135 | 136 | ```json 137 | { 138 | "server": { 139 | "debug": true, 140 | "log_level": "info", 141 | "http_demo": true, 142 | "http_debug": false, 143 | "http_login": "demo", 144 | "http_password": "demo", 145 | "http_port": ":8083", 146 | "ice_servers": ["stun:stun.l.google.com:19302"], 147 | "rtsp_port": ":5541" 148 | }, 149 | "streams": { 150 | "demo1": { 151 | "name": "test video stream 1", 152 | "channels": { 153 | "0": { 154 | "name": "ch1", 155 | "url": "rtsp://admin:admin@YOU_CAMERA_IP/uri", 156 | "on_demand": true, 157 | "debug": false, 158 | "audio": true, 159 | "status": 0 160 | }, 161 | "1": { 162 | "name": "ch2", 163 | "url": "rtsp://admin:admin@YOU_CAMERA_IP/uri", 164 | "on_demand": true, 165 | "debug": false, 166 | "audio": true, 167 | "status": 0 168 | } 169 | } 170 | }, 171 | "demo2": { 172 | "name": "test video stream 2", 173 | "channels": { 174 | "0": { 175 | "name": "ch1", 176 | "url": "rtsp://admin:admin@YOU_CAMERA_IP/uri", 177 | "on_demand": true, 178 | "debug": false, 179 | "status": 0 180 | }, 181 | "1": { 182 | "name": "ch2", 183 | "url": "rtsp://admin:admin@YOU_CAMERA_IP/uri", 184 | "on_demand": true, 185 | "debug": false, 186 | "status": 0 187 | } 188 | } 189 | } 190 | }, 191 | "channel_defaults": { 192 | "on_demand": true 193 | } 194 | } 195 | ``` 196 | 197 | ## Command-line 198 | 199 | ### Use help to show available args 200 | 201 | ```bash 202 | ./RTSPtoWeb --help 203 | ``` 204 | 205 | #### Response 206 | 207 | ```bash 208 | Usage of ./RTSPtoWeb: 209 | -config string 210 | config patch (/etc/server/config.json or config.json) (default "config.json") 211 | -debug 212 | set debug mode (default true) 213 | ``` 214 | 215 | ## API documentation 216 | 217 | See the [API docs](/docs/api.md) 218 | 219 | ## Limitations 220 | 221 | Video Codecs Supported: H264 all profiles 222 | 223 | Audio Codecs Supported: no 224 | 225 | ## Performance 226 | 227 | ```bash 228 | CPU usage ≈0.2%-1% one (thread) core cpu intel core i7 per stream 229 | ``` 230 | 231 | ## Authors 232 | 233 | * **Andrey Semochkin** - *Initial work video* - [deepch](https://github.com/deepch) 234 | * **Dmitriy Vladykin** - *Initial work web UI* - [vdalex25](https://github.com/vdalex25) 235 | 236 | See also the list of [contributors](https://github.com/deepch/RTSPtoWeb/contributors) who participated in this project. 237 | 238 | ## License 239 | 240 | This project licensed. License - see the [LICENSE.md](LICENSE.md) file for details 241 | 242 | [webrtc](https://github.com/pion/webrtc) follows license MIT [license](https://raw.githubusercontent.com/pion/webrtc/master/LICENSE). 243 | 244 | [joy4](https://github.com/nareix/joy4) follows license MIT [license](https://raw.githubusercontent.com/nareix/joy4/master/LICENSE). 245 | 246 | ## Other Example 247 | 248 | Examples of working with video on golang 249 | 250 | - [RTSPtoWeb](https://github.com/deepch/RTSPtoWeb) 251 | - [RTSPtoWebRTC](https://github.com/deepch/RTSPtoWebRTC) 252 | - [RTSPtoWSMP4f](https://github.com/deepch/RTSPtoWSMP4f) 253 | - [RTSPtoImage](https://github.com/deepch/RTSPtoImage) 254 | - [RTSPtoHLS](https://github.com/deepch/RTSPtoHLS) 255 | - [RTSPtoHLSLL](https://github.com/deepch/RTSPtoHLSLL) 256 | 257 | [![paypal.me/AndreySemochkin](https://ionicabizau.github.io/badges/paypal.svg)](https://www.paypal.me/AndreySemochkin) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea: 258 | -------------------------------------------------------------------------------- /RTSPtoWeb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func main() { 13 | log.WithFields(logrus.Fields{ 14 | "module": "main", 15 | "func": "main", 16 | }).Info("Server CORE start") 17 | go HTTPAPIServer() 18 | go RTSPServer() 19 | go Storage.StreamChannelRunAll() 20 | signalChanel := make(chan os.Signal, 1) 21 | done := make(chan bool, 1) 22 | signal.Notify(signalChanel, syscall.SIGINT, syscall.SIGTERM) 23 | go func() { 24 | sig := <-signalChanel 25 | log.WithFields(logrus.Fields{ 26 | "module": "main", 27 | "func": "main", 28 | }).Info("Server receive signal", sig) 29 | done <- true 30 | }() 31 | log.WithFields(logrus.Fields{ 32 | "module": "main", 33 | "func": "main", 34 | }).Info("Server start success a wait signals") 35 | <-done 36 | Storage.StopAll() 37 | time.Sleep(2 * time.Second) 38 | log.WithFields(logrus.Fields{ 39 | "module": "main", 40 | "func": "main", 41 | }).Info("Server stop working by signal") 42 | } 43 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /apiHTTPChannel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | //HTTPAPIServerStreamChannelCodec function return codec info struct 9 | func HTTPAPIServerStreamChannelCodec(c *gin.Context) { 10 | requestLogger := log.WithFields(logrus.Fields{ 11 | "module": "http_stream", 12 | "stream": c.Param("uuid"), 13 | "channel": c.Param("channel"), 14 | "func": "HTTPAPIServerStreamChannelCodec", 15 | }) 16 | 17 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 18 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 19 | requestLogger.WithFields(logrus.Fields{ 20 | "call": "StreamChannelExist", 21 | }).Errorln(ErrorStreamNotFound.Error()) 22 | return 23 | } 24 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 25 | if err != nil { 26 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 27 | requestLogger.WithFields(logrus.Fields{ 28 | "call": "StreamChannelCodec", 29 | }).Errorln(err.Error()) 30 | return 31 | } 32 | c.IndentedJSON(200, Message{Status: 1, Payload: codecs}) 33 | } 34 | 35 | //HTTPAPIServerStreamChannelInfo function return stream info struct 36 | func HTTPAPIServerStreamChannelInfo(c *gin.Context) { 37 | info, err := Storage.StreamChannelInfo(c.Param("uuid"), c.Param("channel")) 38 | if err != nil { 39 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 40 | log.WithFields(logrus.Fields{ 41 | "module": "http_stream", 42 | "stream": c.Param("uuid"), 43 | "channel": c.Param("channel"), 44 | "func": "HTTPAPIServerStreamChannelInfo", 45 | "call": "StreamChannelInfo", 46 | }).Errorln(err.Error()) 47 | return 48 | } 49 | c.IndentedJSON(200, Message{Status: 1, Payload: info}) 50 | } 51 | 52 | //HTTPAPIServerStreamChannelReload function reload stream 53 | func HTTPAPIServerStreamChannelReload(c *gin.Context) { 54 | err := Storage.StreamChannelReload(c.Param("uuid"), c.Param("channel")) 55 | if err != nil { 56 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 57 | log.WithFields(logrus.Fields{ 58 | "module": "http_stream", 59 | "stream": c.Param("uuid"), 60 | "channel": c.Param("channel"), 61 | "func": "HTTPAPIServerStreamChannelReload", 62 | "call": "StreamChannelReload", 63 | }).Errorln(err.Error()) 64 | return 65 | } 66 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 67 | } 68 | 69 | //HTTPAPIServerStreamChannelEdit function edit stream 70 | func HTTPAPIServerStreamChannelEdit(c *gin.Context) { 71 | requestLogger := log.WithFields(logrus.Fields{ 72 | "module": "http_stream", 73 | "stream": c.Param("uuid"), 74 | "channel": c.Param("channel"), 75 | "func": "HTTPAPIServerStreamChannelEdit", 76 | }) 77 | 78 | var payload ChannelST 79 | err := c.BindJSON(&payload) 80 | if err != nil { 81 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 82 | requestLogger.WithFields(logrus.Fields{ 83 | "call": "BindJSON", 84 | }).Errorln(err.Error()) 85 | return 86 | } 87 | err = Storage.StreamChannelEdit(c.Param("uuid"), c.Param("channel"), payload) 88 | if err != nil { 89 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 90 | requestLogger.WithFields(logrus.Fields{ 91 | "call": "StreamChannelEdit", 92 | }).Errorln(err.Error()) 93 | return 94 | } 95 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 96 | } 97 | 98 | //HTTPAPIServerStreamChannelDelete function delete stream 99 | func HTTPAPIServerStreamChannelDelete(c *gin.Context) { 100 | requestLogger := log.WithFields(logrus.Fields{ 101 | "module": "http_stream", 102 | "stream": c.Param("uuid"), 103 | "channel": c.Param("channel"), 104 | "func": "HTTPAPIServerStreamChannelDelete", 105 | }) 106 | 107 | err := Storage.StreamChannelDelete(c.Param("uuid"), c.Param("channel")) 108 | if err != nil { 109 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 110 | requestLogger.WithFields(logrus.Fields{ 111 | "call": "StreamChannelDelete", 112 | }).Errorln(err.Error()) 113 | return 114 | } 115 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 116 | } 117 | 118 | //HTTPAPIServerStreamChannelAdd function add new stream 119 | func HTTPAPIServerStreamChannelAdd(c *gin.Context) { 120 | requestLogger := log.WithFields(logrus.Fields{ 121 | "module": "http_stream", 122 | "stream": c.Param("uuid"), 123 | "channel": c.Param("channel"), 124 | "func": "HTTPAPIServerStreamChannelAdd", 125 | }) 126 | 127 | var payload ChannelST 128 | err := c.BindJSON(&payload) 129 | if err != nil { 130 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 131 | requestLogger.WithFields(logrus.Fields{ 132 | "call": "BindJSON", 133 | }).Errorln(err.Error()) 134 | return 135 | } 136 | err = Storage.StreamChannelAdd(c.Param("uuid"), c.Param("channel"), payload) 137 | if err != nil { 138 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 139 | requestLogger.WithFields(logrus.Fields{ 140 | "call": "StreamChannelAdd", 141 | }).Errorln(err.Error()) 142 | return 143 | } 144 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 145 | } 146 | -------------------------------------------------------------------------------- /apiHTTPHLS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | 7 | "github.com/deepch/vdk/format/ts" 8 | "github.com/gin-gonic/gin" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // HTTPAPIServerStreamHLSM3U8 send client m3u8 play list 13 | func HTTPAPIServerStreamHLSM3U8(c *gin.Context) { 14 | requestLogger := log.WithFields(logrus.Fields{ 15 | "module": "http_hls", 16 | "stream": c.Param("uuid"), 17 | "channel": c.Param("channel"), 18 | "func": "HTTPAPIServerStreamHLSM3U8", 19 | }) 20 | 21 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 22 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 23 | requestLogger.WithFields(logrus.Fields{ 24 | "call": "StreamChannelExist", 25 | }).Errorln(ErrorStreamNotFound.Error()) 26 | return 27 | } 28 | 29 | if !RemoteAuthorization("HLS", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) { 30 | requestLogger.WithFields(logrus.Fields{ 31 | "call": "RemoteAuthorization", 32 | }).Errorln(ErrorStreamUnauthorized.Error()) 33 | return 34 | } 35 | 36 | c.Header("Content-Type", "application/vnd.apple.mpegurl") 37 | Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel")) 38 | //If stream mode on_demand need wait ready segment's 39 | for i := 0; i < 40; i++ { 40 | index, seq, err := Storage.StreamHLSm3u8(c.Param("uuid"), c.Param("channel")) 41 | if err != nil { 42 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 43 | requestLogger.WithFields(logrus.Fields{ 44 | "call": "StreamHLSm3u8", 45 | }).Errorln(err.Error()) 46 | return 47 | } 48 | if seq >= 5 { 49 | _, err := c.Writer.Write([]byte(index)) 50 | if err != nil { 51 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 52 | requestLogger.WithFields(logrus.Fields{ 53 | "call": "Write", 54 | }).Errorln(err.Error()) 55 | return 56 | } 57 | return 58 | } 59 | time.Sleep(1 * time.Second) 60 | } 61 | } 62 | 63 | // HTTPAPIServerStreamHLSTS send client ts segment 64 | func HTTPAPIServerStreamHLSTS(c *gin.Context) { 65 | requestLogger := log.WithFields(logrus.Fields{ 66 | "module": "http_hls", 67 | "stream": c.Param("uuid"), 68 | "channel": c.Param("channel"), 69 | "func": "HTTPAPIServerStreamHLSTS", 70 | }) 71 | 72 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 73 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 74 | requestLogger.WithFields(logrus.Fields{ 75 | "call": "StreamChannelExist", 76 | }).Errorln(ErrorStreamNotFound.Error()) 77 | return 78 | } 79 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 80 | if err != nil { 81 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 82 | requestLogger.WithFields(logrus.Fields{ 83 | "call": "StreamCodecs", 84 | }).Errorln(err.Error()) 85 | return 86 | } 87 | c.Header("Content-Type", "video/MP2T") 88 | outfile := bytes.NewBuffer([]byte{}) 89 | Muxer := ts.NewMuxer(outfile) 90 | Muxer.PaddingToMakeCounterCont = true 91 | err = Muxer.WriteHeader(codecs) 92 | if err != nil { 93 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 94 | requestLogger.WithFields(logrus.Fields{ 95 | "call": "WriteHeader", 96 | }).Errorln(err.Error()) 97 | return 98 | } 99 | seqData, err := Storage.StreamHLSTS(c.Param("uuid"), c.Param("channel"), stringToInt(c.Param("seq"))) 100 | if err != nil { 101 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 102 | requestLogger.WithFields(logrus.Fields{ 103 | "call": "StreamHLSTS", 104 | }).Errorln(err.Error()) 105 | return 106 | } 107 | if len(seqData) == 0 { 108 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotHLSSegments.Error()}) 109 | requestLogger.WithFields(logrus.Fields{ 110 | "call": "seqData", 111 | }).Errorln(ErrorStreamNotHLSSegments.Error()) 112 | return 113 | } 114 | for _, v := range seqData { 115 | v.CompositionTime = 1 116 | err = Muxer.WritePacket(*v) 117 | if err != nil { 118 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 119 | requestLogger.WithFields(logrus.Fields{ 120 | "call": "WritePacket", 121 | }).Errorln(err.Error()) 122 | return 123 | } 124 | } 125 | err = Muxer.WriteTrailer() 126 | if err != nil { 127 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 128 | requestLogger.WithFields(logrus.Fields{ 129 | "call": "WriteTrailer", 130 | }).Errorln(err.Error()) 131 | return 132 | } 133 | _, err = c.Writer.Write(outfile.Bytes()) 134 | if err != nil { 135 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 136 | requestLogger.WithFields(logrus.Fields{ 137 | "call": "Write", 138 | }).Errorln(err.Error()) 139 | return 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /apiHTTPHLSLL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/deepch/vdk/format/mp4f" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | //HTTPAPIServerStreamHLSLLInit send client ts segment 11 | func HTTPAPIServerStreamHLSLLInit(c *gin.Context) { 12 | requestLogger := log.WithFields(logrus.Fields{ 13 | "module": "http_hlsll", 14 | "stream": c.Param("uuid"), 15 | "channel": c.Param("channel"), 16 | "func": "HTTPAPIServerStreamHLSLLInit", 17 | }) 18 | 19 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 20 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 21 | requestLogger.WithFields(logrus.Fields{ 22 | "call": "StreamChannelExist", 23 | }).Errorln(ErrorStreamNotFound.Error()) 24 | return 25 | } 26 | 27 | if !RemoteAuthorization("HLS", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) { 28 | requestLogger.WithFields(logrus.Fields{ 29 | "call": "RemoteAuthorization", 30 | }).Errorln(ErrorStreamUnauthorized.Error()) 31 | return 32 | } 33 | 34 | c.Header("Content-Type", "application/x-mpegURL") 35 | Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel")) 36 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 37 | if err != nil { 38 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 39 | requestLogger.WithFields(logrus.Fields{ 40 | "call": "StreamChannelCodecs", 41 | }).Errorln(err.Error()) 42 | return 43 | } 44 | Muxer := mp4f.NewMuxer(nil) 45 | err = Muxer.WriteHeader(codecs) 46 | if err != nil { 47 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 48 | requestLogger.WithFields(logrus.Fields{ 49 | "call": "WriteHeader", 50 | }).Errorln(err.Error()) 51 | return 52 | } 53 | c.Header("Content-Type", "video/mp4") 54 | _, buf := Muxer.GetInit(codecs) 55 | _, err = c.Writer.Write(buf) 56 | if err != nil { 57 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 58 | requestLogger.WithFields(logrus.Fields{ 59 | "call": "Write", 60 | }).Errorln(err.Error()) 61 | return 62 | } 63 | } 64 | 65 | //HTTPAPIServerStreamHLSLLM3U8 send client m3u8 play list 66 | func HTTPAPIServerStreamHLSLLM3U8(c *gin.Context) { 67 | requestLogger := log.WithFields(logrus.Fields{ 68 | "module": "http_hlsll", 69 | "stream": c.Param("uuid"), 70 | "channel": c.Param("channel"), 71 | "func": "HTTPAPIServerStreamHLSLLM3U8", 72 | }) 73 | 74 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 75 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 76 | requestLogger.WithFields(logrus.Fields{ 77 | "call": "StreamChannelExist", 78 | }).Errorln(ErrorStreamNotFound.Error()) 79 | return 80 | } 81 | c.Header("Content-Type", "application/x-mpegURL") 82 | Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel")) 83 | index, err := Storage.HLSMuxerM3U8(c.Param("uuid"), c.Param("channel"), stringToInt(c.DefaultQuery("_HLS_msn", "-1")), stringToInt(c.DefaultQuery("_HLS_part", "-1"))) 84 | if err != nil { 85 | requestLogger.WithFields(logrus.Fields{ 86 | "call": "HLSMuxerM3U8", 87 | }).Errorln(ErrorStreamNotFound.Error()) 88 | return 89 | } 90 | _, err = c.Writer.Write([]byte(index)) 91 | if err != nil { 92 | requestLogger.WithFields(logrus.Fields{ 93 | "call": "Write", 94 | }).Errorln(ErrorStreamNotFound.Error()) 95 | return 96 | } 97 | } 98 | 99 | //HTTPAPIServerStreamHLSLLM4Segment send client ts segment 100 | func HTTPAPIServerStreamHLSLLM4Segment(c *gin.Context) { 101 | requestLogger := log.WithFields(logrus.Fields{ 102 | "module": "http_hlsll", 103 | "stream": c.Param("uuid"), 104 | "channel": c.Param("channel"), 105 | "func": "HTTPAPIServerStreamHLSLLM4Segment", 106 | }) 107 | 108 | c.Header("Content-Type", "video/mp4") 109 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 110 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 111 | requestLogger.WithFields(logrus.Fields{ 112 | "call": "StreamChannelExist", 113 | }).Errorln(ErrorStreamNotFound.Error()) 114 | return 115 | } 116 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 117 | if err != nil { 118 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 119 | requestLogger.WithFields(logrus.Fields{ 120 | "call": "StreamChannelCodecs", 121 | }).Errorln(err.Error()) 122 | return 123 | } 124 | if codecs == nil { 125 | requestLogger.WithFields(logrus.Fields{ 126 | "call": "StreamCodecs", 127 | }).Errorln("Codec Null") 128 | return 129 | } 130 | Muxer := mp4f.NewMuxer(nil) 131 | err = Muxer.WriteHeader(codecs) 132 | if err != nil { 133 | requestLogger.WithFields(logrus.Fields{ 134 | "call": "WriteHeader", 135 | }).Errorln(err.Error()) 136 | return 137 | } 138 | seqData, err := Storage.HLSMuxerSegment(c.Param("uuid"), c.Param("channel"), stringToInt(c.Param("segment"))) 139 | if err != nil { 140 | requestLogger.WithFields(logrus.Fields{ 141 | "call": "HLSMuxerSegment", 142 | }).Errorln(err.Error()) 143 | return 144 | } 145 | for _, v := range seqData { 146 | err = Muxer.WritePacket4(*v) 147 | if err != nil { 148 | requestLogger.WithFields(logrus.Fields{ 149 | "call": "WritePacket4", 150 | }).Errorln(err.Error()) 151 | return 152 | } 153 | } 154 | buf := Muxer.Finalize() 155 | _, err = c.Writer.Write(buf) 156 | if err != nil { 157 | requestLogger.WithFields(logrus.Fields{ 158 | "call": "Write", 159 | }).Errorln(err.Error()) 160 | return 161 | } 162 | } 163 | 164 | //HTTPAPIServerStreamHLSLLM4Fragment send client ts segment 165 | func HTTPAPIServerStreamHLSLLM4Fragment(c *gin.Context) { 166 | requestLogger := log.WithFields(logrus.Fields{ 167 | "module": "http_hlsll", 168 | "stream": c.Param("uuid"), 169 | "channel": c.Param("channel"), 170 | "func": "HTTPAPIServerStreamHLSLLM4Fragment", 171 | }) 172 | 173 | c.Header("Content-Type", "video/mp4") 174 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 175 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 176 | requestLogger.WithFields(logrus.Fields{ 177 | "call": "StreamChannelExist", 178 | }).Errorln(ErrorStreamNotFound.Error()) 179 | return 180 | } 181 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 182 | if err != nil { 183 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 184 | requestLogger.WithFields(logrus.Fields{ 185 | "call": "StreamChannelCodecs", 186 | }).Errorln(err.Error()) 187 | return 188 | } 189 | if codecs == nil { 190 | requestLogger.WithFields(logrus.Fields{ 191 | "call": "StreamCodecs", 192 | }).Errorln("Codec Null") 193 | return 194 | } 195 | Muxer := mp4f.NewMuxer(nil) 196 | err = Muxer.WriteHeader(codecs) 197 | if err != nil { 198 | requestLogger.WithFields(logrus.Fields{ 199 | "call": "WriteHeader", 200 | }).Errorln(err.Error()) 201 | return 202 | } 203 | seqData, err := Storage.HLSMuxerFragment(c.Param("uuid"), c.Param("channel"), stringToInt(c.Param("segment")), stringToInt(c.Param("fragment"))) 204 | if err != nil { 205 | requestLogger.WithFields(logrus.Fields{ 206 | "call": "HLSMuxerFragment", 207 | }).Errorln(err.Error()) 208 | return 209 | } 210 | for _, v := range seqData { 211 | err = Muxer.WritePacket4(*v) 212 | if err != nil { 213 | requestLogger.WithFields(logrus.Fields{ 214 | "call": "WritePacket4", 215 | }).Errorln(err.Error()) 216 | return 217 | } 218 | } 219 | buf := Muxer.Finalize() 220 | _, err = c.Writer.Write(buf) 221 | if err != nil { 222 | requestLogger.WithFields(logrus.Fields{ 223 | "call": "Write", 224 | }).Errorln(err.Error()) 225 | return 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /apiHTTPMSE.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gobwas/ws/wsutil" 7 | 8 | "github.com/gobwas/ws" 9 | 10 | "github.com/gin-gonic/gin" 11 | 12 | "github.com/deepch/vdk/format/mp4f" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | //HTTPAPIServerStreamMSE func 17 | func HTTPAPIServerStreamMSE(c *gin.Context) { 18 | conn, _, _, err := ws.UpgradeHTTP(c.Request, c.Writer) 19 | if err != nil { 20 | return 21 | } 22 | 23 | requestLogger := log.WithFields(logrus.Fields{ 24 | "module": "http_mse", 25 | "stream": c.Param("uuid"), 26 | "channel": c.Param("channel"), 27 | "func": "HTTPAPIServerStreamMSE", 28 | }) 29 | 30 | defer func() { 31 | err = conn.Close() 32 | requestLogger.WithFields(logrus.Fields{ 33 | "call": "Close", 34 | }).Errorln(err) 35 | log.Println("Client Full Exit") 36 | }() 37 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 38 | requestLogger.WithFields(logrus.Fields{ 39 | "call": "StreamChannelExist", 40 | }).Errorln(ErrorStreamNotFound.Error()) 41 | return 42 | } 43 | 44 | if !RemoteAuthorization("WS", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) { 45 | requestLogger.WithFields(logrus.Fields{ 46 | "call": "RemoteAuthorization", 47 | }).Errorln(ErrorStreamUnauthorized.Error()) 48 | return 49 | } 50 | 51 | Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel")) 52 | err = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) 53 | if err != nil { 54 | requestLogger.WithFields(logrus.Fields{ 55 | "call": "SetWriteDeadline", 56 | }).Errorln(err.Error()) 57 | return 58 | } 59 | cid, ch, _, err := Storage.ClientAdd(c.Param("uuid"), c.Param("channel"), MSE) 60 | if err != nil { 61 | requestLogger.WithFields(logrus.Fields{ 62 | "call": "ClientAdd", 63 | }).Errorln(err.Error()) 64 | return 65 | } 66 | defer Storage.ClientDelete(c.Param("uuid"), cid, c.Param("channel")) 67 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 68 | if err != nil { 69 | requestLogger.WithFields(logrus.Fields{ 70 | "call": "StreamCodecs", 71 | }).Errorln(err.Error()) 72 | return 73 | } 74 | muxerMSE := mp4f.NewMuxer(nil) 75 | err = muxerMSE.WriteHeader(codecs) 76 | if err != nil { 77 | requestLogger.WithFields(logrus.Fields{ 78 | "call": "WriteHeader", 79 | }).Errorln(err.Error()) 80 | return 81 | } 82 | meta, init := muxerMSE.GetInit(codecs) 83 | err = wsutil.WriteServerMessage(conn, ws.OpBinary, append([]byte{9}, meta...)) 84 | if err != nil { 85 | requestLogger.WithFields(logrus.Fields{ 86 | "call": "Send", 87 | }).Errorln(err.Error()) 88 | return 89 | } 90 | err = wsutil.WriteServerMessage(conn, ws.OpBinary, init) 91 | if err != nil { 92 | requestLogger.WithFields(logrus.Fields{ 93 | "call": "Send", 94 | }).Errorln(err.Error()) 95 | return 96 | } 97 | var videoStart bool 98 | controlExit := make(chan bool, 10) 99 | noClient := time.NewTimer(10 * time.Second) 100 | go func() { 101 | defer func() { 102 | controlExit <- true 103 | }() 104 | for { 105 | header, _, err := wsutil.NextReader(conn, ws.StateServerSide) 106 | if err != nil { 107 | requestLogger.WithFields(logrus.Fields{ 108 | "call": "Receive", 109 | }).Errorln(err.Error()) 110 | return 111 | } 112 | switch header.OpCode { 113 | case ws.OpPong: 114 | noClient.Reset(10 * time.Second) 115 | case ws.OpClose: 116 | return 117 | } 118 | } 119 | }() 120 | noVideo := time.NewTimer(10 * time.Second) 121 | pingTicker := time.NewTicker(500 * time.Millisecond) 122 | defer pingTicker.Stop() 123 | defer log.Println("client exit") 124 | for { 125 | select { 126 | 127 | case <-pingTicker.C: 128 | err = conn.SetWriteDeadline(time.Now().Add(3 * time.Second)) 129 | if err != nil { 130 | return 131 | } 132 | buf, err := ws.CompileFrame(ws.NewPingFrame(nil)) 133 | if err != nil { 134 | return 135 | } 136 | _, err = conn.Write(buf) 137 | if err != nil { 138 | return 139 | } 140 | case <-controlExit: 141 | requestLogger.WithFields(logrus.Fields{ 142 | "call": "controlExit", 143 | }).Errorln("Client Reader Exit") 144 | return 145 | case <-noClient.C: 146 | requestLogger.WithFields(logrus.Fields{ 147 | "call": "ErrorClientOffline", 148 | }).Errorln("Client OffLine Exit") 149 | return 150 | case <-noVideo.C: 151 | requestLogger.WithFields(logrus.Fields{ 152 | "call": "ErrorStreamNoVideo", 153 | }).Errorln(ErrorStreamNoVideo.Error()) 154 | return 155 | case pck := <-ch: 156 | if pck.IsKeyFrame { 157 | noVideo.Reset(10 * time.Second) 158 | videoStart = true 159 | } 160 | if !videoStart { 161 | continue 162 | } 163 | ready, buf, err := muxerMSE.WritePacket(*pck, false) 164 | if err != nil { 165 | requestLogger.WithFields(logrus.Fields{ 166 | "call": "WritePacket", 167 | }).Errorln(err.Error()) 168 | return 169 | } 170 | if ready { 171 | err := conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 172 | if err != nil { 173 | requestLogger.WithFields(logrus.Fields{ 174 | "call": "SetWriteDeadline", 175 | }).Errorln(err.Error()) 176 | return 177 | } 178 | //err = websocket.Message.Send(ws, buf) 179 | err = wsutil.WriteServerMessage(conn, ws.OpBinary, buf) 180 | if err != nil { 181 | requestLogger.WithFields(logrus.Fields{ 182 | "call": "Send", 183 | }).Errorln(err.Error()) 184 | return 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /apiHTTPSaveMP4.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/deepch/vdk/format/mp4" 6 | "github.com/gin-gonic/gin" 7 | "github.com/sirupsen/logrus" 8 | "os" 9 | "time" 10 | ) 11 | 12 | // HTTPAPIServerStreamSaveToMP4 func 13 | func HTTPAPIServerStreamSaveToMP4(c *gin.Context) { 14 | var err error 15 | 16 | requestLogger := log.WithFields(logrus.Fields{ 17 | "module": "http_save_mp4", 18 | "stream": c.Param("uuid"), 19 | "channel": c.Param("channel"), 20 | "func": "HTTPAPIServerStreamSaveToMP4", 21 | }) 22 | 23 | defer func() { 24 | if err != nil { 25 | requestLogger.WithFields(logrus.Fields{ 26 | "call": "Close", 27 | }).Errorln(err) 28 | } 29 | }() 30 | 31 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 32 | requestLogger.WithFields(logrus.Fields{ 33 | "call": "StreamChannelExist", 34 | }).Errorln(ErrorStreamNotFound.Error()) 35 | return 36 | } 37 | 38 | if !RemoteAuthorization("save", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) { 39 | requestLogger.WithFields(logrus.Fields{ 40 | "call": "RemoteAuthorization", 41 | }).Errorln(ErrorStreamUnauthorized.Error()) 42 | return 43 | } 44 | c.Writer.Write([]byte("await save started")) 45 | go func() { 46 | Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel")) 47 | cid, ch, _, err := Storage.ClientAdd(c.Param("uuid"), c.Param("channel"), MSE) 48 | if err != nil { 49 | requestLogger.WithFields(logrus.Fields{ 50 | "call": "ClientAdd", 51 | }).Errorln(err.Error()) 52 | return 53 | } 54 | 55 | defer Storage.ClientDelete(c.Param("uuid"), cid, c.Param("channel")) 56 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 57 | if err != nil { 58 | requestLogger.WithFields(logrus.Fields{ 59 | "call": "StreamCodecs", 60 | }).Errorln(err.Error()) 61 | return 62 | } 63 | err = os.MkdirAll(fmt.Sprintf("save/%s/%s/", c.Param("uuid"), c.Param("channel")), 0755) 64 | if err != nil { 65 | requestLogger.WithFields(logrus.Fields{ 66 | "call": "MkdirAll", 67 | }).Errorln(err.Error()) 68 | } 69 | f, err := os.Create(fmt.Sprintf("save/%s/%s/%s.mp4", c.Param("uuid"), c.Param("channel"), time.Now().String())) 70 | if err != nil { 71 | requestLogger.WithFields(logrus.Fields{ 72 | "call": "Create", 73 | }).Errorln(err.Error()) 74 | } 75 | defer f.Close() 76 | 77 | muxer := mp4.NewMuxer(f) 78 | err = muxer.WriteHeader(codecs) 79 | if err != nil { 80 | requestLogger.WithFields(logrus.Fields{ 81 | "call": "WriteHeader", 82 | }).Errorln(err.Error()) 83 | return 84 | } 85 | defer muxer.WriteTrailer() 86 | 87 | var videoStart bool 88 | controlExit := make(chan bool, 10) 89 | dur, err := time.ParseDuration(c.Param("duration")) 90 | if err != nil { 91 | requestLogger.WithFields(logrus.Fields{ 92 | "call": "ParseDuration", 93 | }).Errorln(err.Error()) 94 | } 95 | saveLimit := time.NewTimer(dur) 96 | noVideo := time.NewTimer(10 * time.Second) 97 | defer log.Println("client exit") 98 | for { 99 | select { 100 | case <-controlExit: 101 | requestLogger.WithFields(logrus.Fields{ 102 | "call": "controlExit", 103 | }).Errorln("Client Reader Exit") 104 | return 105 | case <-saveLimit.C: 106 | requestLogger.WithFields(logrus.Fields{ 107 | "call": "saveLimit", 108 | }).Errorln("Saved Limit End") 109 | return 110 | case <-noVideo.C: 111 | requestLogger.WithFields(logrus.Fields{ 112 | "call": "ErrorStreamNoVideo", 113 | }).Errorln(ErrorStreamNoVideo.Error()) 114 | return 115 | case pck := <-ch: 116 | if pck.IsKeyFrame { 117 | noVideo.Reset(10 * time.Second) 118 | videoStart = true 119 | } 120 | if !videoStart { 121 | continue 122 | } 123 | if err = muxer.WritePacket(*pck); err != nil { 124 | return 125 | } 126 | } 127 | } 128 | }() 129 | } 130 | -------------------------------------------------------------------------------- /apiHTTPServer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //TODO add to next version 4 | -------------------------------------------------------------------------------- /apiHTTPStream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | //HTTPAPIServerStreams function return stream list 9 | func HTTPAPIServerStreams(c *gin.Context) { 10 | list, err := Storage.MarshalledStreamsList() 11 | if err != nil { 12 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 13 | return 14 | } 15 | c.IndentedJSON(200, Message{Status: 1, Payload: list}) 16 | } 17 | 18 | //HTTPAPIServerStreamsMultiControlAdd function add new stream's 19 | func HTTPAPIServerStreamsMultiControlAdd(c *gin.Context) { 20 | requestLogger := log.WithFields(logrus.Fields{ 21 | "module": "http_stream", 22 | "func": "HTTPAPIServerStreamsMultiControlAdd", 23 | }) 24 | 25 | var payload StorageST 26 | err := c.BindJSON(&payload) 27 | if err != nil { 28 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 29 | requestLogger.WithFields(logrus.Fields{ 30 | "call": "BindJSON", 31 | }).Errorln(err.Error()) 32 | return 33 | } 34 | if payload.Streams == nil || len(payload.Streams) < 1 { 35 | c.IndentedJSON(400, Message{Status: 0, Payload: ErrorStreamsLen0.Error()}) 36 | requestLogger.WithFields(logrus.Fields{ 37 | "call": "len(payload)", 38 | }).Errorln(ErrorStreamsLen0.Error()) 39 | return 40 | } 41 | var resp = make(map[string]Message) 42 | var FoundError bool 43 | for k, v := range payload.Streams { 44 | err = Storage.StreamAdd(k, v) 45 | if err != nil { 46 | requestLogger.WithFields(logrus.Fields{ 47 | "stream": k, 48 | "call": "StreamAdd", 49 | }).Errorln(err.Error()) 50 | resp[k] = Message{Status: 0, Payload: err.Error()} 51 | FoundError = true 52 | } else { 53 | resp[k] = Message{Status: 1, Payload: Success} 54 | } 55 | } 56 | if FoundError { 57 | c.IndentedJSON(200, Message{Status: 0, Payload: resp}) 58 | } else { 59 | c.IndentedJSON(200, Message{Status: 1, Payload: resp}) 60 | } 61 | } 62 | 63 | //HTTPAPIServerStreamsMultiControlDelete function delete stream's 64 | func HTTPAPIServerStreamsMultiControlDelete(c *gin.Context) { 65 | requestLogger := log.WithFields(logrus.Fields{ 66 | "module": "http_stream", 67 | "func": "HTTPAPIServerStreamsMultiControlDelete", 68 | }) 69 | 70 | var payload []string 71 | err := c.BindJSON(&payload) 72 | if err != nil { 73 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 74 | requestLogger.WithFields(logrus.Fields{ 75 | "call": "BindJSON", 76 | }).Errorln(err.Error()) 77 | return 78 | } 79 | if len(payload) < 1 { 80 | c.IndentedJSON(400, Message{Status: 0, Payload: ErrorStreamsLen0.Error()}) 81 | requestLogger.WithFields(logrus.Fields{ 82 | "call": "len(payload)", 83 | }).Errorln(ErrorStreamsLen0.Error()) 84 | return 85 | } 86 | var resp = make(map[string]Message) 87 | var FoundError bool 88 | for _, key := range payload { 89 | err := Storage.StreamDelete(key) 90 | if err != nil { 91 | requestLogger.WithFields(logrus.Fields{ 92 | "stream": key, 93 | "call": "StreamDelete", 94 | }).Errorln(err.Error()) 95 | resp[key] = Message{Status: 0, Payload: err.Error()} 96 | FoundError = true 97 | } else { 98 | resp[key] = Message{Status: 1, Payload: Success} 99 | } 100 | } 101 | if FoundError { 102 | c.IndentedJSON(200, Message{Status: 0, Payload: resp}) 103 | } else { 104 | c.IndentedJSON(200, Message{Status: 1, Payload: resp}) 105 | } 106 | } 107 | 108 | //HTTPAPIServerStreamAdd function add new stream 109 | func HTTPAPIServerStreamAdd(c *gin.Context) { 110 | var payload StreamST 111 | err := c.BindJSON(&payload) 112 | if err != nil { 113 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 114 | log.WithFields(logrus.Fields{ 115 | "module": "http_stream", 116 | "stream": c.Param("uuid"), 117 | "func": "HTTPAPIServerStreamAdd", 118 | "call": "BindJSON", 119 | }).Errorln(err.Error()) 120 | return 121 | } 122 | err = Storage.StreamAdd(c.Param("uuid"), payload) 123 | if err != nil { 124 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 125 | log.WithFields(logrus.Fields{ 126 | "module": "http_stream", 127 | "stream": c.Param("uuid"), 128 | "func": "HTTPAPIServerStreamAdd", 129 | "call": "StreamAdd", 130 | }).Errorln(err.Error()) 131 | return 132 | } 133 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 134 | } 135 | 136 | //HTTPAPIServerStreamEdit function edit stream 137 | func HTTPAPIServerStreamEdit(c *gin.Context) { 138 | var payload StreamST 139 | err := c.BindJSON(&payload) 140 | if err != nil { 141 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 142 | log.WithFields(logrus.Fields{ 143 | "module": "http_stream", 144 | "stream": c.Param("uuid"), 145 | "func": "HTTPAPIServerStreamEdit", 146 | "call": "BindJSON", 147 | }).Errorln(err.Error()) 148 | return 149 | } 150 | err = Storage.StreamEdit(c.Param("uuid"), payload) 151 | if err != nil { 152 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 153 | log.WithFields(logrus.Fields{ 154 | "module": "http_stream", 155 | "stream": c.Param("uuid"), 156 | "func": "HTTPAPIServerStreamEdit", 157 | "call": "StreamEdit", 158 | }).Errorln(err.Error()) 159 | return 160 | } 161 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 162 | } 163 | 164 | //HTTPAPIServerStreamDelete function delete stream 165 | func HTTPAPIServerStreamDelete(c *gin.Context) { 166 | err := Storage.StreamDelete(c.Param("uuid")) 167 | if err != nil { 168 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 169 | log.WithFields(logrus.Fields{ 170 | "module": "http_stream", 171 | "stream": c.Param("uuid"), 172 | "func": "HTTPAPIServerStreamDelete", 173 | "call": "StreamDelete", 174 | }).Errorln(err.Error()) 175 | return 176 | } 177 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 178 | } 179 | 180 | //HTTPAPIServerStreamDelete function reload stream 181 | func HTTPAPIServerStreamReload(c *gin.Context) { 182 | err := Storage.StreamReload(c.Param("uuid")) 183 | if err != nil { 184 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 185 | log.WithFields(logrus.Fields{ 186 | "module": "http_stream", 187 | "stream": c.Param("uuid"), 188 | "func": "HTTPAPIServerStreamReload", 189 | "call": "StreamReload", 190 | }).Errorln(err.Error()) 191 | return 192 | } 193 | c.IndentedJSON(200, Message{Status: 1, Payload: Success}) 194 | } 195 | 196 | //HTTPAPIServerStreamInfo function return stream info struct 197 | func HTTPAPIServerStreamInfo(c *gin.Context) { 198 | info, err := Storage.StreamInfo(c.Param("uuid")) 199 | if err != nil { 200 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 201 | log.WithFields(logrus.Fields{ 202 | "module": "http_stream", 203 | "stream": c.Param("uuid"), 204 | "func": "HTTPAPIServerStreamInfo", 205 | "call": "StreamInfo", 206 | }).Errorln(err.Error()) 207 | return 208 | } 209 | c.IndentedJSON(200, Message{Status: 1, Payload: info}) 210 | } 211 | -------------------------------------------------------------------------------- /apiHTTPWebRTC.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | webrtc "github.com/deepch/vdk/format/webrtcv3" 7 | "github.com/gin-gonic/gin" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | //HTTPAPIServerStreamWebRTC stream video over WebRTC 12 | func HTTPAPIServerStreamWebRTC(c *gin.Context) { 13 | requestLogger := log.WithFields(logrus.Fields{ 14 | "module": "http_webrtc", 15 | "stream": c.Param("uuid"), 16 | "channel": c.Param("channel"), 17 | "func": "HTTPAPIServerStreamWebRTC", 18 | }) 19 | 20 | if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) { 21 | c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()}) 22 | requestLogger.WithFields(logrus.Fields{ 23 | "call": "StreamChannelExist", 24 | }).Errorln(ErrorStreamNotFound.Error()) 25 | return 26 | } 27 | 28 | if !RemoteAuthorization("WebRTC", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) { 29 | requestLogger.WithFields(logrus.Fields{ 30 | "call": "RemoteAuthorization", 31 | }).Errorln(ErrorStreamUnauthorized.Error()) 32 | return 33 | } 34 | 35 | Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel")) 36 | codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel")) 37 | if err != nil { 38 | c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()}) 39 | requestLogger.WithFields(logrus.Fields{ 40 | "call": "StreamCodecs", 41 | }).Errorln(err.Error()) 42 | return 43 | } 44 | muxerWebRTC := webrtc.NewMuxer(webrtc.Options{ICEServers: Storage.ServerICEServers(), ICEUsername: Storage.ServerICEUsername(), ICECredential: Storage.ServerICECredential(), PortMin: Storage.ServerWebRTCPortMin(), PortMax: Storage.ServerWebRTCPortMax()}) 45 | answer, err := muxerWebRTC.WriteHeader(codecs, c.PostForm("data")) 46 | if err != nil { 47 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 48 | requestLogger.WithFields(logrus.Fields{ 49 | "call": "WriteHeader", 50 | }).Errorln(err.Error()) 51 | return 52 | } 53 | _, err = c.Writer.Write([]byte(answer)) 54 | if err != nil { 55 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 56 | requestLogger.WithFields(logrus.Fields{ 57 | "call": "Write", 58 | }).Errorln(err.Error()) 59 | return 60 | } 61 | go func() { 62 | cid, ch, _, err := Storage.ClientAdd(c.Param("uuid"), c.Param("channel"), WEBRTC) 63 | if err != nil { 64 | c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()}) 65 | requestLogger.WithFields(logrus.Fields{ 66 | "call": "ClientAdd", 67 | }).Errorln(err.Error()) 68 | return 69 | } 70 | defer Storage.ClientDelete(c.Param("uuid"), cid, c.Param("channel")) 71 | var videoStart bool 72 | noVideo := time.NewTimer(10 * time.Second) 73 | for { 74 | select { 75 | case <-noVideo.C: 76 | // c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNoVideo.Error()}) 77 | requestLogger.WithFields(logrus.Fields{ 78 | "call": "ErrorStreamNoVideo", 79 | }).Errorln(ErrorStreamNoVideo.Error()) 80 | return 81 | case pck := <-ch: 82 | if pck.IsKeyFrame { 83 | noVideo.Reset(10 * time.Second) 84 | videoStart = true 85 | } 86 | if !videoStart { 87 | continue 88 | } 89 | err = muxerWebRTC.WritePacket(*pck) 90 | if err != nil { 91 | requestLogger.WithFields(logrus.Fields{ 92 | "call": "WritePacket", 93 | }).Errorln(err.Error()) 94 | return 95 | } 96 | } 97 | } 98 | }() 99 | } 100 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "debug": true, 4 | "http_debug": false, 5 | "http_demo": true, 6 | "http_dir": "web", 7 | "http_login": "demo", 8 | "http_password": "demo", 9 | "http_port": ":8083", 10 | "https": false, 11 | "https_auto_tls": false, 12 | "https_auto_tls_name": "", 13 | "https_cert": "server.crt", 14 | "https_key": "server.key", 15 | "https_port": ":443", 16 | "ice_servers": ["stun:stun.l.google.com:19302"], 17 | "log_level": "debug", 18 | "rtsp_port": ":5541", 19 | "token": { 20 | "backend": "http://127.0.0.1/test.php" 21 | }, 22 | "defaults": { 23 | "audio": true 24 | } 25 | }, 26 | "streams": { 27 | "27aec28e-6181-4753-9acd-0456a75f0289": { 28 | "channels": { 29 | "0": { 30 | "url": "rtmp://171.25.232.10/12d525bc9f014e209c1280bc0d46a87e", 31 | "debug": false, 32 | "audio": true 33 | } 34 | }, 35 | "name": "111111111" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /docs/examples/hls/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RTSPtoWeb HLS example 6 | 7 | 8 |

RTSPtoWeb HLS example

9 | 10 | 12 | 13 | 15 | 16 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/examples/hlsll/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RTSPtoWeb HLS-LL example 6 | 7 | 8 |

RTSPtoWeb HLS-LL example

9 | 10 | 12 | 13 | 15 | 16 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/examples/mse/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RTSPtoWeb MSE example 6 | 7 | 8 |

RTSPtoWeb MSE example

9 | 10 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/examples/mse/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | const mseQueue = [] 3 | let mseSourceBuffer 4 | let mseStreamingStarted = false 5 | 6 | function startPlay (videoEl, url) { 7 | const mse = new MediaSource() 8 | videoEl.src = window.URL.createObjectURL(mse) 9 | mse.addEventListener('sourceopen', function () { 10 | const ws = new WebSocket(url) 11 | ws.binaryType = 'arraybuffer' 12 | ws.onopen = function (event) { 13 | console.log('Connect to ws') 14 | } 15 | ws.onmessage = function (event) { 16 | const data = new Uint8Array(event.data) 17 | if (data[0] === 9) { 18 | let mimeCodec 19 | const decodedArr = data.slice(1) 20 | if (window.TextDecoder) { 21 | mimeCodec = new TextDecoder('utf-8').decode(decodedArr) 22 | } else { 23 | mimeCodec = Utf8ArrayToStr(decodedArr) 24 | } 25 | mseSourceBuffer = mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"') 26 | mseSourceBuffer.mode = 'segments' 27 | mseSourceBuffer.addEventListener('updateend', pushPacket) 28 | } else { 29 | readPacket(event.data) 30 | } 31 | } 32 | }, false) 33 | } 34 | 35 | function pushPacket () { 36 | const videoEl = document.querySelector('#mse-video') 37 | let packet 38 | 39 | if (!mseSourceBuffer.updating) { 40 | if (mseQueue.length > 0) { 41 | packet = mseQueue.shift() 42 | mseSourceBuffer.appendBuffer(packet) 43 | } else { 44 | mseStreamingStarted = false 45 | } 46 | } 47 | if (videoEl.buffered.length > 0) { 48 | if (typeof document.hidden !== 'undefined' && document.hidden) { 49 | // no sound, browser paused video without sound in background 50 | videoEl.currentTime = videoEl.buffered.end((videoEl.buffered.length - 1)) - 0.5 51 | } 52 | } 53 | } 54 | 55 | function readPacket (packet) { 56 | if (!mseStreamingStarted) { 57 | mseSourceBuffer.appendBuffer(packet) 58 | mseStreamingStarted = true 59 | return 60 | } 61 | mseQueue.push(packet) 62 | if (!mseSourceBuffer.updating) { 63 | pushPacket() 64 | } 65 | } 66 | const videoEl = document.querySelector('#mse-video') 67 | const mseUrl = document.querySelector('#mse-url').value 68 | 69 | // fix stalled video in safari 70 | videoEl.addEventListener('pause', () => { 71 | if (videoEl.currentTime > videoEl.buffered.end(videoEl.buffered.length - 1)) { 72 | videoEl.currentTime = videoEl.buffered.end(videoEl.buffered.length - 1) - 0.1 73 | videoEl.play() 74 | } 75 | }) 76 | 77 | startPlay(videoEl, mseUrl) 78 | }) 79 | -------------------------------------------------------------------------------- /docs/examples/webrtc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RTSPtoWeb WebRTC example 6 | 7 | 8 |

RTSPtoWeb WebRTC example

9 | 10 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/examples/webrtc/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | function startPlay (videoEl, url) { 3 | const webrtc = new RTCPeerConnection({ 4 | iceServers: [{ 5 | urls: ['stun:stun.l.google.com:19302'] 6 | }], 7 | sdpSemantics: 'unified-plan' 8 | }) 9 | webrtc.ontrack = function (event) { 10 | console.log(event.streams.length + ' track is delivered') 11 | videoEl.srcObject = event.streams[0] 12 | videoEl.play() 13 | } 14 | webrtc.addTransceiver('video', { direction: 'sendrecv' }) 15 | webrtc.onnegotiationneeded = async function handleNegotiationNeeded () { 16 | const offer = await webrtc.createOffer() 17 | 18 | await webrtc.setLocalDescription(offer) 19 | 20 | fetch(url, { 21 | method: 'POST', 22 | body: new URLSearchParams({ data: btoa(webrtc.localDescription.sdp) }) 23 | }) 24 | .then(response => response.text()) 25 | .then(data => { 26 | try { 27 | webrtc.setRemoteDescription( 28 | new RTCSessionDescription({ type: 'answer', sdp: atob(data) }) 29 | ) 30 | } catch (e) { 31 | console.warn(e) 32 | } 33 | }) 34 | } 35 | 36 | const webrtcSendChannel = webrtc.createDataChannel('rtsptowebSendChannel') 37 | webrtcSendChannel.onopen = (event) => { 38 | console.log(`${webrtcSendChannel.label} has opened`) 39 | webrtcSendChannel.send('ping') 40 | } 41 | webrtcSendChannel.onclose = (_event) => { 42 | console.log(`${webrtcSendChannel.label} has closed`) 43 | startPlay(videoEl, url) 44 | } 45 | webrtcSendChannel.onmessage = event => console.log(event.data) 46 | } 47 | 48 | const videoEl = document.querySelector('#webrtc-video') 49 | const webrtcUrl = document.querySelector('#webrtc-url').value 50 | 51 | startPlay(videoEl, webrtcUrl) 52 | }) 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/deepch/RTSPtoWeb 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/deepch/vdk v0.0.27 9 | github.com/gin-gonic/autotls v1.1.3 10 | github.com/gin-gonic/gin v1.10.1 11 | github.com/gobwas/ws v1.4.0 12 | github.com/hashicorp/go-version v1.7.0 13 | github.com/imdario/mergo v0.3.16 14 | github.com/liip/sheriff v0.12.0 15 | github.com/sirupsen/logrus v1.9.3 16 | ) 17 | 18 | require ( 19 | github.com/bytedance/sonic v1.11.6 // indirect 20 | github.com/bytedance/sonic/loader v0.1.1 // indirect 21 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 22 | github.com/cloudwego/base64x v0.1.4 // indirect 23 | github.com/cloudwego/iasm v0.2.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 26 | github.com/gin-contrib/sse v0.1.0 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/go-playground/validator/v10 v10.20.0 // indirect 30 | github.com/gobwas/httphead v0.1.0 // indirect 31 | github.com/gobwas/pool v0.2.1 // indirect 32 | github.com/goccy/go-json v0.10.2 // indirect 33 | github.com/google/uuid v1.3.0 // indirect 34 | github.com/json-iterator/go v1.1.12 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 36 | github.com/leodido/go-urn v1.4.0 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 41 | github.com/pion/datachannel v1.5.5 // indirect 42 | github.com/pion/dtls/v2 v2.2.7 // indirect 43 | github.com/pion/ice/v2 v2.3.9 // indirect 44 | github.com/pion/interceptor v0.1.17 // indirect 45 | github.com/pion/logging v0.2.2 // indirect 46 | github.com/pion/mdns v0.0.7 // indirect 47 | github.com/pion/randutil v0.1.0 // indirect 48 | github.com/pion/rtcp v1.2.10 // indirect 49 | github.com/pion/rtp v1.7.13 // indirect 50 | github.com/pion/sctp v1.8.7 // indirect 51 | github.com/pion/sdp/v3 v3.0.6 // indirect 52 | github.com/pion/srtp/v2 v2.0.15 // indirect 53 | github.com/pion/stun v0.6.1 // indirect 54 | github.com/pion/transport/v2 v2.2.1 // indirect 55 | github.com/pion/turn/v2 v2.1.2 // indirect 56 | github.com/pion/webrtc/v3 v3.2.12 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/stretchr/testify v1.9.0 // indirect 59 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 60 | github.com/ugorji/go/codec v1.2.12 // indirect 61 | golang.org/x/arch v0.8.0 // indirect 62 | golang.org/x/crypto v0.38.0 // indirect 63 | golang.org/x/net v0.40.0 // indirect 64 | golang.org/x/sync v0.14.0 // indirect 65 | golang.org/x/sys v0.33.0 // indirect 66 | golang.org/x/text v0.25.0 // indirect 67 | google.golang.org/protobuf v1.34.1 // indirect 68 | gopkg.in/yaml.v3 v3.0.1 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /hlsFragment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/deepch/vdk/av" 7 | ) 8 | 9 | //Fragment struct 10 | type Fragment struct { 11 | Independent bool //Fragment have i-frame (key frame) 12 | Finish bool //Fragment Ready 13 | Duration time.Duration //Fragment Duration 14 | Packets []*av.Packet //Packet Slice 15 | } 16 | 17 | //NewFragment open new fragment 18 | func (element *Segment) NewFragment() *Fragment { 19 | res := &Fragment{} 20 | element.Fragment[element.CurrentFragmentID] = res 21 | return res 22 | } 23 | 24 | //GetDuration return fragment dur 25 | func (element *Fragment) GetDuration() time.Duration { 26 | return element.Duration 27 | } 28 | 29 | //WritePacket to fragment func 30 | func (element *Fragment) WritePacket(packet *av.Packet) { 31 | //increase fragment dur 32 | element.Duration += packet.Duration 33 | //Independent if have key 34 | if packet.IsKeyFrame { 35 | element.Independent = true 36 | } 37 | //append packet to slice of packet 38 | element.Packets = append(element.Packets, packet) 39 | } 40 | 41 | //Close fragment block func 42 | func (element *Fragment) Close() { 43 | //TODO add callback func 44 | //finalize fragment 45 | element.Finish = true 46 | } 47 | -------------------------------------------------------------------------------- /hlsSegment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/deepch/vdk/av" 7 | ) 8 | 9 | //Segment struct 10 | type Segment struct { 11 | FPS int //Current fps 12 | CurrentFragment *Fragment //CurrentFragment link 13 | CurrentFragmentID int //CurrentFragment ID 14 | Finish bool //Segment Ready 15 | Duration time.Duration //Segment Duration 16 | Time time.Time //Realtime EXT-X-PROGRAM-DATE-TIME 17 | Fragment map[int]*Fragment //Fragment map 18 | } 19 | 20 | //NewSegment func 21 | func (element *MuxerHLS) NewSegment() *Segment { 22 | res := &Segment{ 23 | Fragment: make(map[int]*Fragment), 24 | CurrentFragmentID: -1, //Default fragment -1 25 | } 26 | //Increase MSN 27 | element.MSN++ 28 | element.Segments[element.MSN] = res 29 | return res 30 | } 31 | 32 | //GetDuration func 33 | func (element *Segment) GetDuration() time.Duration { 34 | return element.Duration 35 | } 36 | 37 | //SetFPS func 38 | func (element *Segment) SetFPS(fps int) { 39 | element.FPS = fps 40 | } 41 | 42 | //WritePacket func 43 | func (element *Segment) WritePacket(packet *av.Packet) { 44 | if element.CurrentFragment == nil || element.CurrentFragment.GetDuration().Milliseconds() >= element.FragmentMS(element.FPS) { 45 | if element.CurrentFragment != nil { 46 | element.CurrentFragment.Close() 47 | } 48 | element.CurrentFragmentID++ 49 | element.CurrentFragment = element.NewFragment() 50 | } 51 | element.Duration += packet.Duration 52 | element.CurrentFragment.WritePacket(packet) 53 | } 54 | 55 | //GetFragmentID func 56 | func (element *Segment) GetFragmentID() int { 57 | return element.CurrentFragmentID 58 | } 59 | 60 | //Close segment func 61 | func (element *Segment) Close() { 62 | element.Finish = true 63 | if element.CurrentFragment != nil { 64 | element.CurrentFragment.Close() 65 | } 66 | } 67 | 68 | //FragmentMS func 69 | func (element *Segment) FragmentMS(fps int) int64 { 70 | for i := 6; i >= 1; i-- { 71 | if fps%i == 0 { 72 | return int64(float64(1000) / float64(fps) * float64(i)) 73 | } 74 | } 75 | return 100 76 | } 77 | -------------------------------------------------------------------------------- /loggingLog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | var log = logrus.New() 10 | 11 | func init() { 12 | //TODO: next add write to file 13 | if !debug { 14 | log.SetOutput(ioutil.Discard) 15 | } 16 | log.SetFormatter(&logrus.TextFormatter{ 17 | FullTimestamp: true, 18 | }) 19 | log.SetLevel(Storage.ServerLogLevel()) 20 | } 21 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "dependencyDashboard": true, 6 | "dependencyDashboardTitle": "Renovate Dashboard", 7 | "packageRules": [ 8 | { 9 | "description": "Minor updates are automatic", 10 | "automerge": true, 11 | "automergeType": "branch", 12 | "matchUpdateTypes": ["minor", "patch"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGjCCAZ8CCQCLGA2sAHSd1DAKBggqhkjOPQQDAjB2MQswCQYDVQQGEwJERTEN 3 | MAsGA1UECAwEREVNTzENMAsGA1UEBwwEREVNTzENMAsGA1UECgwEREVNTzENMAsG 4 | A1UECwwEREVNTzENMAsGA1UEAwwEREVNTzEcMBoGCSqGSIb3DQEJARYNZGVtb0Bk 5 | ZW1vLmNvbTAeFw0yMTAzMDUxMzIyNDhaFw0zMTAzMDMxMzIyNDhaMHYxCzAJBgNV 6 | BAYTAkRFMQ0wCwYDVQQIDARERU1PMQ0wCwYDVQQHDARERU1PMQ0wCwYDVQQKDARE 7 | RU1PMQ0wCwYDVQQLDARERU1PMQ0wCwYDVQQDDARERU1PMRwwGgYJKoZIhvcNAQkB 8 | Fg1kZW1vQGRlbW8uY29tMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEpQ5/6akj0JiG 9 | CE0sAnWzcecbcgXY74KgG4X4+0Z8YF7B6LwI57yIQD/9LiUvPwHl7rlT9B+s31Ui 10 | ZQ6U0Y5ChES0jeISmLhkAUkGHM8wjPUGRa23FgEgalw/I3+KPMRnMAoGCCqGSM49 11 | BAMCA2kAMGYCMQCLIugTO5xINyl32k1F3edgxun6NLhu/k+c+lvBi8EMcq8aERVC 12 | kPU1hWhF7BD0JfkCMQDS5FzusPPfK7maBF11XXuwBFJ3Zke96mSmpohuTBxT7yfW 13 | TIPP+Rk2MzxMKh/RLHw= 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIGkAgEBBDDBqBCdv9p3NihNJi3lrVfu700/pXm+tZcm22axGDZZXoWTt5c9k6W7 6 | PzK/0TgZwsmgBwYFK4EEACKhZANiAASlDn/pqSPQmIYITSwCdbNx5xtyBdjvgqAb 7 | hfj7RnxgXsHovAjnvIhAP/0uJS8/AeXuuVP0H6zfVSJlDpTRjkKERLSN4hKYuGQB 8 | SQYczzCM9QZFrbcWASBqXD8jf4o8xGc= 9 | -----END EC PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /storageClient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/deepch/vdk/av" 7 | ) 8 | 9 | //ClientAdd Add New Client to Translations 10 | func (obj *StorageST) ClientAdd(streamID string, channelID string, mode int) (string, chan *av.Packet, chan *[]byte, error) { 11 | obj.mutex.Lock() 12 | defer obj.mutex.Unlock() 13 | streamTmp, ok := obj.Streams[streamID] 14 | if !ok { 15 | return "", nil, nil, ErrorStreamNotFound 16 | } 17 | //Generate UUID client 18 | cid, err := generateUUID() 19 | if err != nil { 20 | return "", nil, nil, err 21 | } 22 | chAV := make(chan *av.Packet, 2000) 23 | chRTP := make(chan *[]byte, 2000) 24 | channelTmp, ok := streamTmp.Channels[channelID] 25 | if !ok { 26 | return "", nil, nil, ErrorStreamNotFound 27 | } 28 | 29 | channelTmp.clients[cid] = ClientST{mode: mode, outgoingAVPacket: chAV, outgoingRTPPacket: chRTP, signals: make(chan int, 100)} 30 | channelTmp.ack = time.Now() 31 | streamTmp.Channels[channelID] = channelTmp 32 | obj.Streams[streamID] = streamTmp 33 | return cid, chAV, chRTP, nil 34 | 35 | } 36 | 37 | //ClientDelete Delete Client 38 | func (obj *StorageST) ClientDelete(streamID string, cid string, channelID string) { 39 | obj.mutex.Lock() 40 | defer obj.mutex.Unlock() 41 | if _, ok := obj.Streams[streamID]; ok { 42 | delete(obj.Streams[streamID].Channels[channelID].clients, cid) 43 | } 44 | } 45 | 46 | //ClientHas check is client ext 47 | func (obj *StorageST) ClientHas(streamID string, channelID string) bool { 48 | obj.mutex.Lock() 49 | defer obj.mutex.Unlock() 50 | streamTmp, ok := obj.Streams[streamID] 51 | if !ok { 52 | return false 53 | } 54 | channelTmp, ok := streamTmp.Channels[channelID] 55 | if !ok { 56 | return false 57 | } 58 | if time.Now().Sub(channelTmp.ack).Seconds() > 30 { 59 | return false 60 | } 61 | return true 62 | } 63 | -------------------------------------------------------------------------------- /storageConfig.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "io/ioutil" 7 | "os" 8 | "time" 9 | 10 | "github.com/hashicorp/go-version" 11 | 12 | "github.com/imdario/mergo" 13 | 14 | "github.com/liip/sheriff" 15 | 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // Command line flag global variables 20 | var debug bool 21 | var configFile string 22 | 23 | //NewStreamCore do load config file 24 | func NewStreamCore() *StorageST { 25 | flag.BoolVar(&debug, "debug", true, "set debug mode") 26 | flag.StringVar(&configFile, "config", "config.json", "config patch (/etc/server/config.json or config.json)") 27 | flag.Parse() 28 | 29 | var tmp StorageST 30 | data, err := ioutil.ReadFile(configFile) 31 | if err != nil { 32 | log.WithFields(logrus.Fields{ 33 | "module": "config", 34 | "func": "NewStreamCore", 35 | "call": "ReadFile", 36 | }).Errorln(err.Error()) 37 | os.Exit(1) 38 | } 39 | err = json.Unmarshal(data, &tmp) 40 | if err != nil { 41 | log.WithFields(logrus.Fields{ 42 | "module": "config", 43 | "func": "NewStreamCore", 44 | "call": "Unmarshal", 45 | }).Errorln(err.Error()) 46 | os.Exit(1) 47 | } 48 | debug = tmp.Server.Debug 49 | for i, i2 := range tmp.Streams { 50 | for i3, i4 := range i2.Channels { 51 | channel := tmp.ChannelDefaults 52 | err = mergo.Merge(&channel, i4) 53 | if err != nil { 54 | log.WithFields(logrus.Fields{ 55 | "module": "config", 56 | "func": "NewStreamCore", 57 | "call": "Merge", 58 | }).Errorln(err.Error()) 59 | os.Exit(1) 60 | } 61 | channel.clients = make(map[string]ClientST) 62 | channel.ack = time.Now().Add(-255 * time.Hour) 63 | channel.hlsSegmentBuffer = make(map[int]SegmentOld) 64 | channel.signals = make(chan int, 100) 65 | i2.Channels[i3] = channel 66 | } 67 | tmp.Streams[i] = i2 68 | } 69 | return &tmp 70 | } 71 | 72 | //ClientDelete Delete Client 73 | func (obj *StorageST) SaveConfig() error { 74 | log.WithFields(logrus.Fields{ 75 | "module": "config", 76 | "func": "NewStreamCore", 77 | }).Debugln("Saving configuration to", configFile) 78 | v2, err := version.NewVersion("2.0.0") 79 | if err != nil { 80 | return err 81 | } 82 | data, err := sheriff.Marshal(&sheriff.Options{ 83 | Groups: []string{"config"}, 84 | ApiVersion: v2, 85 | }, obj) 86 | if err != nil { 87 | return err 88 | } 89 | res, err := json.MarshalIndent(data, "", " ") 90 | if err != nil { 91 | return err 92 | } 93 | err = ioutil.WriteFile(configFile, res, 0644) 94 | if err != nil { 95 | log.WithFields(logrus.Fields{ 96 | "module": "config", 97 | "func": "SaveConfig", 98 | "call": "WriteFile", 99 | }).Errorln(err.Error()) 100 | return err 101 | } 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /storageServer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | var ( 10 | //Default www static file dir 11 | DefaultHTTPDir = "web" 12 | ) 13 | 14 | //ServerHTTPDir 15 | func (obj *StorageST) ServerHTTPDir() string { 16 | obj.mutex.RLock() 17 | defer obj.mutex.RUnlock() 18 | if filepath.Clean(obj.Server.HTTPDir) == "." { 19 | return DefaultHTTPDir 20 | } 21 | return filepath.Clean(obj.Server.HTTPDir) 22 | } 23 | 24 | //ServerHTTPDebug read debug options 25 | func (obj *StorageST) ServerHTTPDebug() bool { 26 | obj.mutex.RLock() 27 | defer obj.mutex.RUnlock() 28 | return obj.Server.HTTPDebug 29 | } 30 | 31 | //ServerLogLevel read debug options 32 | func (obj *StorageST) ServerLogLevel() logrus.Level { 33 | obj.mutex.RLock() 34 | defer obj.mutex.RUnlock() 35 | return obj.Server.LogLevel 36 | } 37 | 38 | //ServerHTTPDemo read demo options 39 | func (obj *StorageST) ServerHTTPDemo() bool { 40 | obj.mutex.RLock() 41 | defer obj.mutex.RUnlock() 42 | return obj.Server.HTTPDemo 43 | } 44 | 45 | //ServerHTTPLogin read Login options 46 | func (obj *StorageST) ServerHTTPLogin() string { 47 | obj.mutex.RLock() 48 | defer obj.mutex.RUnlock() 49 | return obj.Server.HTTPLogin 50 | } 51 | 52 | //ServerHTTPPassword read Password options 53 | func (obj *StorageST) ServerHTTPPassword() string { 54 | obj.mutex.RLock() 55 | defer obj.mutex.RUnlock() 56 | return obj.Server.HTTPPassword 57 | } 58 | 59 | //ServerHTTPPort read HTTP Port options 60 | func (obj *StorageST) ServerHTTPPort() string { 61 | obj.mutex.RLock() 62 | defer obj.mutex.RUnlock() 63 | return obj.Server.HTTPPort 64 | } 65 | 66 | //ServerRTSPPort read HTTP Port options 67 | func (obj *StorageST) ServerRTSPPort() string { 68 | obj.mutex.RLock() 69 | defer obj.mutex.RUnlock() 70 | return obj.Server.RTSPPort 71 | } 72 | 73 | //ServerHTTPS read HTTPS Port options 74 | func (obj *StorageST) ServerHTTPS() bool { 75 | obj.mutex.RLock() 76 | defer obj.mutex.RUnlock() 77 | return obj.Server.HTTPS 78 | } 79 | 80 | //ServerHTTPSPort read HTTPS Port options 81 | func (obj *StorageST) ServerHTTPSPort() string { 82 | obj.mutex.RLock() 83 | defer obj.mutex.RUnlock() 84 | return obj.Server.HTTPSPort 85 | } 86 | 87 | //ServerHTTPSAutoTLSEnable read HTTPS Port options 88 | func (obj *StorageST) ServerHTTPSAutoTLSEnable() bool { 89 | obj.mutex.RLock() 90 | defer obj.mutex.RUnlock() 91 | return obj.Server.HTTPSAutoTLSEnable 92 | } 93 | 94 | //ServerHTTPSAutoTLSName read HTTPS Port options 95 | func (obj *StorageST) ServerHTTPSAutoTLSName() string { 96 | obj.mutex.RLock() 97 | defer obj.mutex.RUnlock() 98 | return obj.Server.HTTPSAutoTLSName 99 | } 100 | 101 | //ServerHTTPSCert read HTTPS Cert options 102 | func (obj *StorageST) ServerHTTPSCert() string { 103 | obj.mutex.RLock() 104 | defer obj.mutex.RUnlock() 105 | return obj.Server.HTTPSCert 106 | } 107 | 108 | //ServerHTTPSKey read HTTPS Key options 109 | func (obj *StorageST) ServerHTTPSKey() string { 110 | obj.mutex.RLock() 111 | defer obj.mutex.RUnlock() 112 | return obj.Server.HTTPSKey 113 | } 114 | 115 | // ServerICEServers read ICE servers 116 | func (obj *StorageST) ServerICEServers() []string { 117 | obj.mutex.Lock() 118 | defer obj.mutex.Unlock() 119 | return obj.Server.ICEServers 120 | } 121 | 122 | // ServerICEServers read ICE username 123 | func (obj *StorageST) ServerICEUsername() string { 124 | obj.mutex.Lock() 125 | defer obj.mutex.Unlock() 126 | return obj.Server.ICEUsername 127 | } 128 | 129 | // ServerICEServers read ICE credential 130 | func (obj *StorageST) ServerICECredential() string { 131 | obj.mutex.Lock() 132 | defer obj.mutex.Unlock() 133 | return obj.Server.ICECredential 134 | } 135 | 136 | //ServerTokenEnable read HTTPS Key options 137 | func (obj *StorageST) ServerTokenEnable() bool { 138 | obj.mutex.RLock() 139 | defer obj.mutex.RUnlock() 140 | return obj.Server.Token.Enable 141 | } 142 | 143 | //ServerTokenBackend read HTTPS Key options 144 | func (obj *StorageST) ServerTokenBackend() string { 145 | obj.mutex.RLock() 146 | defer obj.mutex.RUnlock() 147 | return obj.Server.Token.Backend 148 | } 149 | 150 | // ServerWebRTCPortMin read WebRTC Port Min 151 | func (obj *StorageST) ServerWebRTCPortMin() uint16 { 152 | obj.mutex.Lock() 153 | defer obj.mutex.Unlock() 154 | return obj.Server.WebRTCPortMin 155 | } 156 | 157 | // ServerWebRTCPortMax read WebRTC Port Max 158 | func (obj *StorageST) ServerWebRTCPortMax() uint16 { 159 | obj.mutex.Lock() 160 | defer obj.mutex.Unlock() 161 | return obj.Server.WebRTCPortMax 162 | } 163 | -------------------------------------------------------------------------------- /storageStream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/liip/sheriff" 4 | 5 | //MarshalledStreamsList lists all streams and includes only fields which are safe to serialize. 6 | func (obj *StorageST) MarshalledStreamsList() (interface{}, error) { 7 | obj.mutex.RLock() 8 | defer obj.mutex.RUnlock() 9 | val, err := sheriff.Marshal(&sheriff.Options{ 10 | Groups: []string{"api"}, 11 | }, obj.Streams) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return val, nil 16 | } 17 | 18 | //StreamAdd add stream 19 | func (obj *StorageST) StreamAdd(uuid string, val StreamST) error { 20 | obj.mutex.Lock() 21 | defer obj.mutex.Unlock() 22 | //TODO create empty map bug save https://github.com/liip/sheriff empty not nil map[] != {} json 23 | //data, err := sheriff.Marshal(&sheriff.Options{ 24 | // Groups: []string{"config"}, 25 | // ApiVersion: v2, 26 | // }, obj) 27 | //Not Work map[] != {} 28 | if obj.Streams == nil { 29 | obj.Streams = make(map[string]StreamST) 30 | } 31 | if _, ok := obj.Streams[uuid]; ok { 32 | return ErrorStreamAlreadyExists 33 | } 34 | for i, i2 := range val.Channels { 35 | i2 = obj.StreamChannelMake(i2) 36 | if !i2.OnDemand { 37 | i2.runLock = true 38 | val.Channels[i] = i2 39 | go StreamServerRunStreamDo(uuid, i) 40 | } else { 41 | val.Channels[i] = i2 42 | } 43 | } 44 | obj.Streams[uuid] = val 45 | err := obj.SaveConfig() 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | //StreamEdit edit stream 53 | func (obj *StorageST) StreamEdit(uuid string, val StreamST) error { 54 | obj.mutex.Lock() 55 | defer obj.mutex.Unlock() 56 | if tmp, ok := obj.Streams[uuid]; ok { 57 | for i, i2 := range tmp.Channels { 58 | if i2.runLock { 59 | tmp.Channels[i] = i2 60 | obj.Streams[uuid] = tmp 61 | i2.signals <- SignalStreamStop 62 | } 63 | } 64 | for i3, i4 := range val.Channels { 65 | i4 = obj.StreamChannelMake(i4) 66 | if !i4.OnDemand { 67 | i4.runLock = true 68 | val.Channels[i3] = i4 69 | go StreamServerRunStreamDo(uuid, i3) 70 | } else { 71 | val.Channels[i3] = i4 72 | } 73 | } 74 | obj.Streams[uuid] = val 75 | err := obj.SaveConfig() 76 | if err != nil { 77 | return err 78 | } 79 | return nil 80 | } 81 | return ErrorStreamNotFound 82 | } 83 | 84 | //StreamReload reload stream 85 | func (obj *StorageST) StopAll() { 86 | obj.mutex.RLock() 87 | defer obj.mutex.RUnlock() 88 | for _, st := range obj.Streams { 89 | for _, i2 := range st.Channels { 90 | if i2.runLock { 91 | i2.signals <- SignalStreamStop 92 | } 93 | } 94 | } 95 | } 96 | 97 | //StreamReload reload stream 98 | func (obj *StorageST) StreamReload(uuid string) error { 99 | obj.mutex.RLock() 100 | defer obj.mutex.RUnlock() 101 | if tmp, ok := obj.Streams[uuid]; ok { 102 | for _, i2 := range tmp.Channels { 103 | if i2.runLock { 104 | i2.signals <- SignalStreamRestart 105 | } 106 | } 107 | return nil 108 | } 109 | return ErrorStreamNotFound 110 | } 111 | 112 | //StreamDelete stream 113 | func (obj *StorageST) StreamDelete(uuid string) error { 114 | obj.mutex.Lock() 115 | defer obj.mutex.Unlock() 116 | if tmp, ok := obj.Streams[uuid]; ok { 117 | for _, i2 := range tmp.Channels { 118 | if i2.runLock { 119 | i2.signals <- SignalStreamStop 120 | } 121 | } 122 | delete(obj.Streams, uuid) 123 | err := obj.SaveConfig() 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | return ErrorStreamNotFound 130 | } 131 | 132 | //StreamInfo return stream info 133 | func (obj *StorageST) StreamInfo(uuid string) (*StreamST, error) { 134 | obj.mutex.RLock() 135 | defer obj.mutex.RUnlock() 136 | if tmp, ok := obj.Streams[uuid]; ok { 137 | return &tmp, nil 138 | } 139 | return nil, ErrorStreamNotFound 140 | } 141 | -------------------------------------------------------------------------------- /storageStreamHLS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/deepch/vdk/av" 9 | ) 10 | 11 | // StreamHLSAdd add hls seq to buffer 12 | func (obj *StorageST) StreamHLSAdd(uuid string, channelID string, val []*av.Packet, dur time.Duration) { 13 | obj.mutex.Lock() 14 | defer obj.mutex.Unlock() 15 | if tmp, ok := obj.Streams[uuid]; ok { 16 | if channelTmp, ok := tmp.Channels[channelID]; ok { 17 | channelTmp.hlsSegmentNumber++ 18 | channelTmp.hlsSegmentBuffer[channelTmp.hlsSegmentNumber] = SegmentOld{data: val, dur: dur} 19 | channelTmp.hlsLastDur = int(dur.Seconds()) 20 | if len(channelTmp.hlsSegmentBuffer) >= 6 { 21 | delete(channelTmp.hlsSegmentBuffer, channelTmp.hlsSegmentNumber-5) 22 | channelTmp.hlsSequence++ 23 | } 24 | tmp.Channels[channelID] = channelTmp 25 | obj.Streams[uuid] = tmp 26 | } 27 | } 28 | } 29 | 30 | // StreamHLSm3u8 get hls m3u8 list 31 | func (obj *StorageST) StreamHLSm3u8(uuid string, channelID string) (string, int, error) { 32 | obj.mutex.RLock() 33 | defer obj.mutex.RUnlock() 34 | if tmp, ok := obj.Streams[uuid]; ok { 35 | if channelTmp, ok := tmp.Channels[channelID]; ok { 36 | var out string 37 | //TODO fix it 38 | out += "#EXTM3U\r\n#EXT-X-TARGETDURATION:" + strconv.Itoa(channelTmp.hlsLastDur) + "\r\n#EXT-X-VERSION:4\r\n#EXT-X-MEDIA-SEQUENCE:" + strconv.Itoa(channelTmp.hlsSequence) + "\r\n" 39 | var keys []int 40 | for k := range channelTmp.hlsSegmentBuffer { 41 | keys = append(keys, k) 42 | } 43 | sort.Ints(keys) 44 | var count int 45 | for _, i := range keys { 46 | if i == 2 { 47 | out += "#EXT-X-DISCONTINUITY\r\n" 48 | } 49 | count++ 50 | out += "#EXTINF:" + strconv.FormatFloat(channelTmp.hlsSegmentBuffer[i].dur.Seconds(), 'f', 1, 64) + ",\r\nsegment/" + strconv.Itoa(i) + "/file.ts\r\n" 51 | } 52 | return out, count, nil 53 | } 54 | } 55 | return "", 0, ErrorStreamNotFound 56 | } 57 | 58 | // StreamHLSTS send hls segment buffer to clients 59 | func (obj *StorageST) StreamHLSTS(uuid string, channelID string, seq int) ([]*av.Packet, error) { 60 | obj.mutex.RLock() 61 | defer obj.mutex.RUnlock() 62 | if tmp, ok := obj.Streams[uuid]; ok { 63 | if channelTmp, ok := tmp.Channels[channelID]; ok { 64 | if tmp, ok := channelTmp.hlsSegmentBuffer[seq]; ok { 65 | return tmp.data, nil 66 | } 67 | } 68 | } 69 | return nil, ErrorStreamNotFound 70 | } 71 | 72 | // StreamHLSFlush delete hls cache 73 | func (obj *StorageST) StreamHLSFlush(uuid string, channelID string) { 74 | obj.mutex.Lock() 75 | defer obj.mutex.Unlock() 76 | if tmp, ok := obj.Streams[uuid]; ok { 77 | if channelTmp, ok := tmp.Channels[channelID]; ok { 78 | channelTmp.hlsSegmentBuffer = make(map[int]SegmentOld) 79 | channelTmp.hlsSegmentNumber = 0 80 | channelTmp.hlsSequence = 0 81 | tmp.Channels[channelID] = channelTmp 82 | obj.Streams[uuid] = tmp 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /storageStruct.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/deepch/vdk/av" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var Storage = NewStreamCore() 14 | 15 | // Default stream type 16 | const ( 17 | MSE = iota 18 | WEBRTC 19 | RTSP 20 | ) 21 | 22 | // Default stream status type 23 | const ( 24 | OFFLINE = iota 25 | ONLINE 26 | ) 27 | 28 | // Default stream errors 29 | var ( 30 | Success = "success" 31 | ErrorStreamNotFound = errors.New("stream not found") 32 | ErrorStreamAlreadyExists = errors.New("stream already exists") 33 | ErrorStreamChannelAlreadyExists = errors.New("stream channel already exists") 34 | ErrorStreamNotHLSSegments = errors.New("stream hls not ts seq found") 35 | ErrorStreamNoVideo = errors.New("stream no video") 36 | ErrorStreamNoClients = errors.New("stream no clients") 37 | ErrorStreamRestart = errors.New("stream restart") 38 | ErrorStreamStopCoreSignal = errors.New("stream stop core signal") 39 | ErrorStreamStopRTSPSignal = errors.New("stream stop rtsp signal") 40 | ErrorStreamChannelNotFound = errors.New("stream channel not found") 41 | ErrorStreamChannelCodecNotFound = errors.New("stream channel codec not ready, possible stream offline") 42 | ErrorStreamsLen0 = errors.New("streams len zero") 43 | ErrorStreamUnauthorized = errors.New("stream request unauthorized") 44 | ) 45 | 46 | // StorageST main storage struct 47 | type StorageST struct { 48 | mutex sync.RWMutex 49 | Server ServerST `json:"server" groups:"api,config"` 50 | Streams map[string]StreamST `json:"streams,omitempty" groups:"api,config"` 51 | ChannelDefaults ChannelST `json:"channel_defaults,omitempty" groups:"api,config"` 52 | } 53 | 54 | // ServerST server storage section 55 | type ServerST struct { 56 | Debug bool `json:"debug" groups:"api,config"` 57 | LogLevel logrus.Level `json:"log_level" groups:"api,config"` 58 | HTTPDemo bool `json:"http_demo" groups:"api,config"` 59 | HTTPDebug bool `json:"http_debug" groups:"api,config"` 60 | HTTPLogin string `json:"http_login" groups:"api,config"` 61 | HTTPPassword string `json:"http_password" groups:"api,config"` 62 | HTTPDir string `json:"http_dir" groups:"api,config"` 63 | HTTPPort string `json:"http_port" groups:"api,config"` 64 | RTSPPort string `json:"rtsp_port" groups:"api,config"` 65 | HTTPS bool `json:"https" groups:"api,config"` 66 | HTTPSPort string `json:"https_port" groups:"api,config"` 67 | HTTPSCert string `json:"https_cert" groups:"api,config"` 68 | HTTPSKey string `json:"https_key" groups:"api,config"` 69 | HTTPSAutoTLSEnable bool `json:"https_auto_tls" groups:"api,config"` 70 | HTTPSAutoTLSName string `json:"https_auto_tls_name" groups:"api,config"` 71 | ICEServers []string `json:"ice_servers" groups:"api,config"` 72 | ICEUsername string `json:"ice_username" groups:"api,config"` 73 | ICECredential string `json:"ice_credential" groups:"api,config"` 74 | Token Token `json:"token,omitempty" groups:"api,config"` 75 | WebRTCPortMin uint16 `json:"webrtc_port_min" groups:"api,config"` 76 | WebRTCPortMax uint16 `json:"webrtc_port_max" groups:"api,config"` 77 | } 78 | 79 | // Token auth 80 | type Token struct { 81 | Enable bool `json:"enable" groups:"api,config"` 82 | Backend string `json:"backend" groups:"api,config"` 83 | } 84 | 85 | // ServerST stream storage section 86 | type StreamST struct { 87 | Name string `json:"name,omitempty" groups:"api,config"` 88 | Channels map[string]ChannelST `json:"channels,omitempty" groups:"api,config"` 89 | } 90 | 91 | type ChannelST struct { 92 | Name string `json:"name,omitempty" groups:"api,config"` 93 | URL string `json:"url,omitempty" groups:"api,config"` 94 | OnDemand bool `json:"on_demand,omitempty" groups:"api,config"` 95 | Debug bool `json:"debug,omitempty" groups:"api,config"` 96 | Status int `json:"status,omitempty" groups:"api"` 97 | InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty" groups:"api,config"` 98 | Audio bool `json:"audio,omitempty" groups:"api,config"` 99 | runLock bool 100 | codecs []av.CodecData 101 | sdp []byte 102 | signals chan int 103 | hlsSegmentBuffer map[int]SegmentOld 104 | hlsSegmentNumber int 105 | hlsSequence int 106 | hlsLastDur int 107 | clients map[string]ClientST 108 | ack time.Time 109 | hlsMuxer *MuxerHLS `json:"-"` 110 | } 111 | 112 | // ClientST client storage section 113 | type ClientST struct { 114 | mode int 115 | signals chan int 116 | outgoingAVPacket chan *av.Packet 117 | outgoingRTPPacket chan *[]byte 118 | socket net.Conn 119 | } 120 | 121 | // SegmentOld HLS cache section 122 | type SegmentOld struct { 123 | dur time.Duration 124 | data []*av.Packet 125 | } 126 | -------------------------------------------------------------------------------- /streamRemoteAuthorization.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type AuthorizationReq struct { 12 | Proto string `json:"proto,omitempty"` 13 | Stream string `json:"stream,omitempty"` 14 | Channel string `json:"channel,omitempty"` 15 | Token string `json:"token,omitempty"` 16 | IP string `json:"ip,omitempty"` 17 | } 18 | 19 | type AuthorizationRes struct { 20 | Status string `json:"status,omitempty"` 21 | } 22 | 23 | func RemoteAuthorization(proto string, stream string, channel string, token string, ip string) bool { 24 | 25 | if !Storage.ServerTokenEnable() { 26 | return true 27 | } 28 | 29 | buf, err := json.Marshal(&AuthorizationReq{proto, stream, channel, token, ip}) 30 | 31 | if err != nil { 32 | return false 33 | } 34 | 35 | request, err := http.NewRequest("POST", Storage.ServerTokenBackend(), bytes.NewBuffer(buf)) 36 | 37 | if err != nil { 38 | return false 39 | } 40 | 41 | request.Header.Set("Content-Type", "application/json; charset=UTF-8") 42 | 43 | client := &http.Client{ 44 | Timeout: 1 * time.Second, 45 | } 46 | 47 | response, err := client.Do(request) 48 | 49 | if err != nil { 50 | return false 51 | } 52 | 53 | defer response.Body.Close() 54 | 55 | bodyBytes, err := io.ReadAll(response.Body) 56 | 57 | if err != nil { 58 | return false 59 | } 60 | 61 | var res AuthorizationRes 62 | 63 | err = json.Unmarshal(bodyBytes, &res) 64 | 65 | if err != nil { 66 | return false 67 | } 68 | 69 | if res.Status == "1" { 70 | return true 71 | } 72 | 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /supportFunc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | //Default streams signals 11 | const ( 12 | SignalStreamRestart = iota ///< Y Restart 13 | SignalStreamStop 14 | SignalStreamClient 15 | ) 16 | 17 | //generateUUID function make random uuid for clients and stream 18 | func generateUUID() (string, error) { 19 | b := make([]byte, 16) 20 | _, err := rand.Read(b) 21 | if err != nil { 22 | return "", err 23 | } 24 | return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil 25 | } 26 | 27 | //stringToInt convert string to int if err to zero 28 | func stringToInt(val string) int { 29 | i, err := strconv.Atoi(val) 30 | if err != nil { 31 | return 0 32 | } 33 | return i 34 | } 35 | 36 | //stringInBetween fin char to char sub string 37 | func stringInBetween(str string, start string, end string) (result string) { 38 | s := strings.Index(str, start) 39 | if s == -1 { 40 | return 41 | } 42 | str = str[s+len(start):] 43 | e := strings.Index(str, end) 44 | if e == -1 { 45 | return 46 | } 47 | str = str[:e] 48 | return str 49 | } 50 | -------------------------------------------------------------------------------- /test.bytes: -------------------------------------------------------------------------------- 1 | [0,0,0,24,102,116,121,112,105,115,111,54,0,0,0,1,105,115,111,54,100,97,115,104,0,0,2,135,109,111,111,118,0,0,0,108,109,118,104,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,232,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,219,116,114,97,107,0,0,0,92,116,107,104,100,0,0,0,7,218,62,130,252,218,62,130,252,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,1,119,109,100,105,97,0,0,0,32,109,100,104,100,0,0,0,0,218,62,130,252,218,62,130,252,0,1,95,144,0,0,0,0,85,196,0,0,0,0,0,37,104,100,108,114,0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,73,80,69,89,69,0,0,0,0,0,1,42,109,105,110,102,0,0,0,20,118,109,104,100,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,36,100,105,110,102,0,0,0,28,100,114,101,102,0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1,0,0,0,234,115,116,98,108,0,0,0,142,115,116,115,100,0,0,0,0,0,0,0,1,0,0,0,126,97,118,99,49,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,128,4,56,0,72,0,0,0,72,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,255,255,0,0,0,40,97,118,99,67,1,100,0,42,255,225,0,17,103,100,0,42,172,44,106,129,224,8,159,150,110,2,2,2,4,1,0,4,104,238,60,176,0,0,0,16,115,116,116,115,0,0,0,0,0,0,0,0,0,0,0,16,115,116,115,99,0,0,0,0,0,0,0,0,0,0,0,16,115,116,115,115,0,0,0,0,0,0,0,0,0,0,0,16,115,116,99,111,0,0,0,0,0,0,0,0,0,0,0,20,115,116,115,122,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,56,109,118,101,120,0,0,0,16,109,101,104,100,0,0,0,0,0,0,0,0,0,0,0,32,116,114,101,120,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0] 2 | -------------------------------------------------------------------------------- /test.curl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "curl http://demo:demo@127.0.0.1:8083/streams" 6 | curl http://demo:demo@127.0.0.1:8083/streams 7 | sleep 1 8 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/add" 9 | curl --header "Content-Type: application/json" \ 10 | --request POST \ 11 | --data '{ 12 | "name": "test video", 13 | "channels": { 14 | "0": { 15 | "name": "ch1", 16 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4", 17 | "on_demand": false, 18 | "debug": false, 19 | "status": 0 20 | }, 21 | "1": { 22 | "name": "ch2", 23 | "url": "rtsp://admin:admin123@10.128.18.224:999/mpeg4cif", 24 | "on_demand": true, 25 | "debug": false, 26 | "status": 0 27 | } 28 | } 29 | }' \ 30 | http://demo:demo@127.0.0.1:8083/stream/testing/add 31 | sleep 1 32 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/edit" 33 | curl --header "Content-Type: application/json" \ 34 | --request POST \ 35 | --data '{ 36 | "name": "test video", 37 | "channels": { 38 | "0": { 39 | "name": "ch1", 40 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4", 41 | "on_demand": true, 42 | "debug": false, 43 | "status": 0 44 | }, 45 | "1": { 46 | "name": "ch2", 47 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4", 48 | "on_demand": false, 49 | "debug": false, 50 | "status": 0 51 | } 52 | } 53 | }' \ 54 | http://demo:demo@127.0.0.1:8083/stream/testing/edit 55 | sleep 1 56 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/add" 57 | curl --header "Content-Type: application/json" \ 58 | --request POST \ 59 | --data '{ 60 | "name": "ch4", 61 | "url": "rtsp://admin:admin@YOU_CAMERA_IP/uri", 62 | "on_demand": false, 63 | "debug": false, 64 | "status": 0 65 | }' \ 66 | http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/add 67 | sleep 1 68 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/edit" 69 | curl --header "Content-Type: application/json" \ 70 | --request POST \ 71 | --data '{ 72 | "name": "ch4", 73 | "url": "rtsp://admin:admin@YOU_CAMERA_IP/uri", 74 | "on_demand": true, 75 | "debug": false, 76 | "status": 0 77 | }' \ 78 | http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/edit 79 | sleep 1 80 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/info" 81 | curl http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/info 82 | sleep 1 83 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/codec" 84 | curl http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/codec 85 | sleep 1 86 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/delete" 87 | curl http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/delete 88 | sleep 1 89 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/reload" 90 | curl http://demo:demo@127.0.0.1:8083/stream/testing/reload 91 | sleep 1 92 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/info" 93 | echo "/stream/testing/info" 94 | curl http://demo:demo@127.0.0.1:8083/stream/testing/info 95 | sleep 1 96 | echo "http://demo:demo@127.0.0.1:8083/stream/testing/delete" 97 | curl http://demo:demo@127.0.0.1:8083/stream/testing/delete 98 | sleep 1 99 | echo "http://demo:demo@127.0.0.1:8083/pages/multiview/full" 100 | curl --header "Content-Type: application/json" \ 101 | --request POST \ 102 | --data '{ 103 | "grid":6, 104 | "player":{ 105 | 106 | "1": { 107 | "uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211", 108 | "channel": 1, 109 | "playerType": "mse" 110 | }, 111 | "2": { 112 | "uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211", 113 | "channel": 0, 114 | "playerType": "mse" 115 | }, 116 | "3": { 117 | "uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211", 118 | "channel": 1, 119 | "playerType": "hls" 120 | }, 121 | "4": { 122 | "uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211", 123 | "channel": 0, 124 | "playerType": "mse" 125 | }, 126 | "6": { 127 | "uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211", 128 | "channel": 1, 129 | "playerType": "mse" 130 | } 131 | } 132 | }' \ 133 | http://demo:demo@127.0.0.1:8083/pages/multiview/full 134 | -------------------------------------------------------------------------------- /test_multi.curl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | curl --header "Content-Type: application/json" \ 6 | --request POST \ 7 | --data '{ 8 | "streams": { 9 | "demo1": { 10 | "channels": { 11 | "0": { 12 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 13 | }, 14 | "1": { 15 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 16 | } 17 | }, 18 | "name": "test video1" 19 | }, 20 | "demo2": { 21 | "channels": { 22 | "0": { 23 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 24 | }, 25 | "1": { 26 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 27 | } 28 | }, 29 | "name": "test video2" 30 | }, 31 | "demo3": { 32 | "channels": { 33 | "0": { 34 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 35 | }, 36 | "1": { 37 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 38 | } 39 | }, 40 | "name": "test video3" 41 | }, 42 | "demo4": { 43 | "channels": { 44 | "0": { 45 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 46 | }, 47 | "1": { 48 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 49 | } 50 | }, 51 | "name": "test video4" 52 | }, 53 | "demo5": { 54 | "channels": { 55 | "0": { 56 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 57 | }, 58 | "1": { 59 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 60 | } 61 | }, 62 | "name": "test video5" 63 | }, 64 | "demo6": { 65 | "channels": { 66 | "0": { 67 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 68 | }, 69 | "1": { 70 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 71 | } 72 | }, 73 | "name": "test video6" 74 | }, 75 | "demo7": { 76 | "channels": { 77 | "0": { 78 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 79 | }, 80 | "1": { 81 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 82 | } 83 | }, 84 | "name": "test video7" 85 | }, 86 | "demo8": { 87 | "channels": { 88 | "0": { 89 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 90 | }, 91 | "1": { 92 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 93 | } 94 | }, 95 | "name": "test video8" 96 | }, 97 | "demo9": { 98 | "channels": { 99 | "0": { 100 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 101 | }, 102 | "1": { 103 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 104 | } 105 | }, 106 | "name": "test video9" 107 | }, 108 | "demo10": { 109 | "channels": { 110 | "0": { 111 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4" 112 | }, 113 | "1": { 114 | "url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif" 115 | } 116 | }, 117 | "name": "test video10" 118 | } 119 | } 120 | }' \ 121 | http://demo:demo@127.0.0.1:8083/streams/multi/control/add 122 | sleep 1 123 | echo "curl http://demo:demo@127.0.0.1:8083/streams" 124 | curl http://demo:demo@127.0.0.1:8083/streams 125 | sleep 1 126 | curl --header "Content-Type: application/json" \ 127 | --request POST \ 128 | --data '["demo1", "demo2", "demo3", "demo4", "demo5", "demo6", "demo7", "demo8", "demo9", "demo10"]' \ 129 | http://demo:demo@127.0.0.1:8083/streams/multi/control/delete 130 | sleep 1 131 | echo "curl http://demo:demo@127.0.0.1:8083/streams" 132 | curl http://demo:demo@127.0.0.1:8083/streams 133 | -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7jsDJB9cme_xc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7jsDJB9cme_xc.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7ksDJB9cme_xc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7ksDJB9cme_xc.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDJB9cme.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDJB9cme.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7osDJB9cme_xc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7osDJB9cme_xc.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJB9cme_xc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJB9cme_xc.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7qsDJB9cme_xc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7qsDJB9cme_xc.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7rsDJB9cme_xc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7rsDJB9cme_xc.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lujVj9_mf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lujVj9_mf.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lujVj9_mf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lujVj9_mf.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lujVj9_mf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lujVj9_mf.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lujVj9_mf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lujVj9_mf.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lujVj9_mf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lujVj9_mf.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lujVj9_mf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lujVj9_mf.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwkxdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwkxdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlBdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlBdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu3cOWxw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu3cOWxw.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmBdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmBdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmRdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmRdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmhdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmhdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmxdu3cOWxy40.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/css/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmxdu3cOWxy40.woff2 -------------------------------------------------------------------------------- /web/static/css/fullmulti.css: -------------------------------------------------------------------------------- 1 | .grid-wrapper{ 2 | height: calc(100vh - 30px); 3 | margin: 0px; 4 | background: transparent; 5 | display: flex; 6 | flex-wrap: wrap; 7 | } 8 | .layout-top-nav .wrapper .main-header { 9 | margin-left: 0; 10 | height: 30px; 11 | } 12 | video.background{ 13 | pointer-events: none; 14 | position: absolute; 15 | top:0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | object-fit: cover; 20 | } 21 | .control-sidebar.custom{ 22 | background: black; 23 | height: 100%; 24 | margin-top: -57px; 25 | padding: 30px 8px 8px; 26 | background: linear-gradient(45deg, black, transparent); 27 | overflow: hidden; 28 | } 29 | .control-sidebar.custom .row{ 30 | height:100%; 31 | overflow: auto; 32 | } 33 | .control-sidebar.custom>h5{ 34 | color: white; 35 | text-align: center; 36 | } 37 | 38 | .grid-wrapper .player video.video-class { 39 | background: #000; 40 | object-fit: fill; 41 | } 42 | .grid-wrapper .player video.video-class.empty { 43 | background: transparent; 44 | } 45 | 46 | .img-background{ 47 | /* background: url(/../static/img/back.jpg); */ 48 | background-color: #343a40; 49 | background-position: center; 50 | background-repeat: no-repeat; 51 | background-size: cover; 52 | backdrop-filter: sepia(0.5) grayscale(0.4); 53 | } 54 | .img-background .content-wrapper{ 55 | background: transparent; 56 | } 57 | .img-background .main-header{ 58 | background: 59 | } 60 | .grid-wrapper .player { 61 | 62 | padding: 0px; 63 | border: 2px solid #000; 64 | } 65 | 66 | .grid-wrapper .player .remove-btn{ 67 | color:white; 68 | display: none; 69 | position: absolute; 70 | right: 0px; 71 | top: 0px; 72 | cursor: pointer; 73 | padding: 5px 10px; 74 | border-radius: 50px; 75 | filter: drop-shadow(1px 1px 1px black); 76 | } 77 | .grid-wrapper .player.empty .remove-btn{ 78 | display: none; 79 | } 80 | .grid-wrapper .player:not(.empty):hover .remove-btn{ 81 | display: block; 82 | } 83 | 84 | .grid-wrapper .player .add-stream-btn{ 85 | position: absolute; 86 | left: 0; 87 | top: calc(50% - 2vw); 88 | text-align: center; 89 | width: 100%; 90 | font-variant-caps: all-petite-caps; 91 | font-size: 1.5vw; 92 | line-height: 1; 93 | color: white; 94 | cursor: pointer; 95 | display: none; 96 | filter: drop-shadow(2px 4px 6px black); 97 | } 98 | .grid-wrapper .player.empty .add-stream-btn{ 99 | display: block; 100 | 101 | } 102 | 103 | .carousel-caption { 104 | pointer-events: none; 105 | filter: drop-shadow(1px 1px 1px black); 106 | } 107 | .stream-img{ 108 | height: 150px; 109 | } 110 | .player .loader, .main-player .loader{ 111 | position: absolute; 112 | width: 100px; 113 | height: 70px; 114 | top: calc(50% - 35px); 115 | left: calc(50% - 50px); 116 | } 117 | .control-sidebar-close-btn{ 118 | position: absolute; 119 | top: 37px; 120 | right: 12px; 121 | color: #fff; 122 | } 123 | .control-sidebar-close-btn:hover{ 124 | color: #999; 125 | } 126 | .dropdown-menu.custom-dropdown.show{ 127 | color: #ffF; 128 | border: 1px solid #343a40; 129 | background: linear-gradient(45deg, #000000d1 30%,#343a40); 130 | padding: 0; 131 | text-align: center; 132 | } 133 | 134 | .dropdown-menu.custom-dropdown .custom-dropdown-item{ 135 | float: left; 136 | width: 33%; 137 | padding: 10px; 138 | font-size: 12px; 139 | cursor: pointer; 140 | } 141 | .dropdown-menu.custom-dropdown .custom-dropdown-item.active,.dropdown-menu.custom-dropdown .custom-dropdown-item:hover{ 142 | border-radius: 5px; 143 | box-shadow: inset 0px 0px 15px -5px; 144 | } 145 | .dropdown-menu.custom-dropdown .dropdown-item{ 146 | color: #fff; 147 | 148 | } 149 | .dropdown-menu.custom-dropdown .dropdown-item:hover{ 150 | box-shadow: inset 0px 0px 15px -5px; 151 | background: transparent; 152 | } 153 | 154 | .custom-dropdown-item.with-img{ 155 | width: 50%!important; 156 | height: auto; 157 | padding: 2px!important; 158 | 159 | } 160 | .custom-dropdown-item.with-img img{ 161 | width: 100%; 162 | height: auto; 163 | } 164 | -------------------------------------------------------------------------------- /web/static/css/index.css: -------------------------------------------------------------------------------- 1 | #videoPlayer{ 2 | width: 100%; 3 | background: black; 4 | position: -webkit-sticky; /* Safari */ 5 | position: sticky; 6 | top: 15px; 7 | margin-bottom: -6px; 8 | 9 | } 10 | .videoPlayer{ 11 | width: 100%; 12 | background: black; 13 | position: -webkit-sticky; /* Safari */ 14 | position: sticky; 15 | top: 15px; 16 | margin-bottom: -6px; 17 | 18 | } 19 | .btn-group.stream{ 20 | display: flex; 21 | justify-content: space-between; 22 | flex-wrap: wrap; 23 | } 24 | .btn-group.stream .btn{ 25 | flex: 1 0 30%; 26 | } 27 | .btn-group.stream .input-group-prepend{ 28 | flex: 1 0 30%; 29 | } 30 | .img-wrapper{ 31 | width: 100%; 32 | height: 180px; 33 | background: black; 34 | cursor: pointer; 35 | } 36 | .img-wrapper img{ 37 | width: 100%; 38 | height: 100%; 39 | } 40 | .text-wrapper{ 41 | padding: 10px; 42 | } 43 | 44 | @media (min-width: 576px){ 45 | .card-columns { 46 | -webkit-column-count: 4; 47 | -moz-column-count: 4; 48 | column-count: 4; 49 | -webkit-column-gap: 1.25rem; 50 | -moz-column-gap: 1.25rem; 51 | column-gap: 1.25rem; 52 | orphans: 1; 53 | widows: 1; 54 | } 55 | } 56 | @media (min-width: 576px){ 57 | .card-column { 58 | column-count: 2; 59 | } 60 | } 61 | @media (min-width: 768px){ 62 | .card-column { 63 | column-count: 4; 64 | } 65 | } 66 | @media (min-width: 1920px){ 67 | .card-column { 68 | column-count: 6; 69 | } 70 | } 71 | 72 | 73 | .grid-wrapper{ 74 | height: 80vh; 75 | margin: -1px; 76 | background: #000; 77 | display: flex; 78 | flex-wrap: wrap; 79 | } 80 | :-webkit-full-screen .grid-wrapper{/*WebKit, Opera 15+*/ 81 | height: 100vh; 82 | width: 100vw; 83 | } 84 | :-moz-full-screen .grid-wrapper{/*FireFox*/ 85 | height: 100vh; 86 | width: 100vw; 87 | } 88 | :full-screen .grid-wrapper{/*Opera 12.15-, Blink, w3c standard*/ 89 | height: 100vh; 90 | width: 100vw; 91 | } 92 | .grid-wrapper .player{ 93 | height: 50%; 94 | width: 50%; 95 | position: relative; 96 | padding: 2px; 97 | } 98 | .grid-wrapper .player.grid-1{ 99 | width: 100%; 100 | height: 100%; 101 | } 102 | .grid-wrapper .player.grid-6{ 103 | width: 33.33%; 104 | } 105 | .grid-wrapper .player.grid-9{ 106 | width: 33.33%; 107 | height: 33.33%; 108 | } 109 | .grid-wrapper .player.grid-12{ 110 | width: 25%; 111 | height: 33.33%; 112 | } 113 | .grid-wrapper .player.grid-16{ 114 | width: 25%; 115 | height: 25%; 116 | } 117 | .grid-wrapper .player.grid-25{ 118 | width: 20%; 119 | height: 20%; 120 | } 121 | .grid-wrapper .player.grid-36{ 122 | width: 16.66%; 123 | height: 16.66%; 124 | } 125 | .grid-wrapper .player.grid-49{ 126 | width: 14.285%; 127 | height: 14.29%; 128 | } 129 | .grid-wrapper .player video{ 130 | height: 100%; 131 | width: 100%; 132 | background: #343a40; 133 | margin-bottom: -6px; 134 | } 135 | .grid-wrapper .player .play-info{ 136 | position: absolute; 137 | padding: 10px; 138 | text-align: center; 139 | color: #fff; 140 | top:0; 141 | left: 0; 142 | width: 100%; 143 | } 144 | .grid-wrapper .player .control{ 145 | width: 100%; 146 | position: absolute; 147 | bottom: 0; 148 | padding: 10px; 149 | text-align: center; 150 | } 151 | .grid-wrapper .player .control .btn{ 152 | margin: 0 0 10px 10px; 153 | } 154 | 155 | .card .stream-name{ 156 | position: absolute; 157 | width: 100%; 158 | height: 100%; 159 | display: flex; 160 | align-items: center; 161 | } 162 | .card .stream-name .card-title{ 163 | color: #FFF; 164 | width: 100%; 165 | filter: drop-shadow(1px 0px 1px black); 166 | font-size: 30px; 167 | } 168 | .one-line-header{ 169 | white-space: nowrap; 170 | overflow: hidden; 171 | text-overflow: ellipsis; 172 | max-width: 95%; 173 | } 174 | .main-player-wrapper{ 175 | position: fixed; 176 | top: 0; 177 | left: 0; 178 | width: 100vw; 179 | height: 100vh; 180 | background: rgb(0 0 0 / 0.8); 181 | z-index: 1100; 182 | padding: 15px; 183 | } 184 | .main-player-wrapper a{ 185 | color: #fff; 186 | position: absolute; 187 | right: 5px; 188 | top: 5px; 189 | font-size: 30px; 190 | padding: 0 12px; 191 | border-radius: 50px; 192 | box-shadow: 0px 0px 10px -1px #fff; 193 | cursor: pointer; 194 | } 195 | .main-player-wrapper a:hover{ 196 | color: #dc3545; 197 | box-shadow: 0px 0px 10px -1px #dc3545; 198 | } 199 | .main-player-wrapper .main-player{ 200 | width: 100%; 201 | height: 100%; 202 | position: relative; 203 | } 204 | .main-player-wrapper .main-player video{ 205 | width: 100%; 206 | height: 100%; 207 | background: #000; 208 | } 209 | .main-player-wrapper .main-player .play-info{ 210 | position: absolute; 211 | padding: 10px; 212 | text-align: center; 213 | color: #fff; 214 | top: 0; 215 | width: 100%; 216 | } 217 | .carousel-caption{ 218 | pointer-events: none; 219 | } 220 | .fix-height{ 221 | object-fit: fill; 222 | aspect-ratio: 16/9; 223 | } 224 | -------------------------------------------------------------------------------- /web/static/img/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/img/back.jpg -------------------------------------------------------------------------------- /web/static/img/green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/img/green.jpg -------------------------------------------------------------------------------- /web/static/img/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web/static/img/noimage.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 49 | 57 | 61 | 64 | 68 | 69 | 72 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /web/static/img/pic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 56 | 58 | 65 | 69 | 73 | 74 | 75 | 82 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /web/static/img/red.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/img/red.jpg -------------------------------------------------------------------------------- /web/static/img/white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/img/white.jpg -------------------------------------------------------------------------------- /web/static/js/RtspToWeb.js: -------------------------------------------------------------------------------- 1 | var rtspPlayer={ 2 | active:false, 3 | type:'live', 4 | hls:null, 5 | ws:null, 6 | mseSourceBuffer:null, 7 | mse:null, 8 | mseQueue:[], 9 | mseStreamingStarted:false, 10 | webrtc:null, 11 | webrtcSendChannel:null, 12 | webrtcSendChannelInterval:null, 13 | uuid:null, 14 | 15 | clearPlayer:function(){ 16 | if(this.active){ 17 | 18 | if(this.hls!=null){ 19 | this.hls.destroy(); 20 | this.hls=null; 21 | } 22 | if(this.ws!=null){ 23 | //close WebSocket connection if opened 24 | this.ws.close(1000); 25 | this.ws=null; 26 | } 27 | if(this.webrtc!=null){ 28 | clearInterval(this.webrtcSendChannelInterval); 29 | 30 | this.webrtc=null; 31 | } 32 | $('#videoPlayer')[0].src = ''; 33 | $('#videoPlayer')[0].load(); 34 | 35 | 36 | this.active=false; 37 | } 38 | }, 39 | livePlayer:function(type,uuid){ 40 | this.clearPlayer(); 41 | this.uuid=uuid; 42 | this.active=true; 43 | 44 | $('.streams-vs-player').addClass('active-player'); 45 | if(type==0){ 46 | type=$('input[name=defaultPlayer]:checked').val() 47 | } 48 | switch (type) { 49 | case 'hls': 50 | this.playHls(); 51 | break; 52 | case 'mse': 53 | this.playMse(); 54 | break; 55 | case 'webrtc': 56 | this.playWebrtc(); 57 | break; 58 | default: 59 | Swal.fire( 60 | 'Sorry', 61 | 'This option is still under development', 62 | 'question' 63 | ) 64 | return; 65 | } 66 | 67 | }, 68 | playHls:function(){ 69 | if(this.hls==null && Hls.isSupported()){ 70 | this.hls = new Hls(); 71 | } 72 | if ($("#videoPlayer")[0].canPlayType('application/vnd.apple.mpegurl')) { 73 | $("#videoPlayer")[0].src = this.streamPlayUrl('hls'); 74 | $("#videoPlayer")[0].load(); 75 | } else { 76 | if (this.hls != null) { 77 | this.hls.loadSource(this.streamPlayUrl('hls')); 78 | this.hls.attachMedia($("#videoPlayer")[0]); 79 | } else { 80 | Swal.fire({ 81 | icon: 'error', 82 | title: 'Oops...', 83 | text: 'Your browser don`t support hls ' 84 | }) 85 | } 86 | } 87 | }, 88 | playWebrtc:function(){ 89 | var _this=this; 90 | this.webrtc=new RTCPeerConnection({ 91 | iceServers: [{ 92 | urls: ["stun:stun.l.google.com:19302"] 93 | }] 94 | }); 95 | this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded; 96 | this.webrtc.ontrack = function(event) { 97 | console.log(event.streams.length + ' track is delivered'); 98 | $("#videoPlayer")[0].srcObject = event.streams[0]; 99 | $("#videoPlayer")[0].play(); 100 | } 101 | this.webrtc.addTransceiver('video', { 102 | 'direction': 'sendrecv' 103 | }); 104 | this.webrtcSendChannel = this.webrtc.createDataChannel('foo'); 105 | this.webrtcSendChannel.onclose = () => console.log('sendChannel has closed'); 106 | this.webrtcSendChannel.onopen = () => { 107 | console.log('sendChannel has opened'); 108 | this.webrtcSendChannel.send('ping'); 109 | this.webrtcSendChannelInterval = setInterval(() => { 110 | this.webrtcSendChannel.send('ping'); 111 | }, 1000) 112 | } 113 | 114 | this.webrtcSendChannel.onmessage = e => console.log(e.data); 115 | }, 116 | handleNegotiationNeeded: async function(){ 117 | var _this=rtspPlayer; 118 | 119 | offer = await _this.webrtc.createOffer(); 120 | await _this.webrtc.setLocalDescription(offer); 121 | $.post(_this.streamPlayUrl('webrtc'), { 122 | data: btoa(_this.webrtc.localDescription.sdp) 123 | }, function(data) { 124 | //console.log(data) 125 | try { 126 | 127 | _this.webrtc.setRemoteDescription(new RTCSessionDescription({ 128 | type: 'answer', 129 | sdp: atob(data) 130 | })) 131 | 132 | 133 | 134 | } catch (e) { 135 | console.warn(e); 136 | } 137 | 138 | }); 139 | }, 140 | playMse:function(){ 141 | //console.log(this.streamPlayUrl('mse')); 142 | var _this=this; 143 | this.mse = new MediaSource(); 144 | $("#videoPlayer")[0].src=window.URL.createObjectURL(this.mse); 145 | this.mse.addEventListener('sourceopen', function(){ 146 | _this.ws=new WebSocket(_this.streamPlayUrl('mse')); 147 | _this.ws.binaryType = "arraybuffer"; 148 | _this.ws.onopen = function(event) { 149 | console.log('Connect to ws'); 150 | } 151 | 152 | _this.ws.onmessage = function(event) { 153 | var data = new Uint8Array(event.data); 154 | if (data[0] == 9) { 155 | decoded_arr=data.slice(1); 156 | if (window.TextDecoder) { 157 | mimeCodec = new TextDecoder("utf-8").decode(decoded_arr); 158 | } else { 159 | mimeCodec = Utf8ArrayToStr(decoded_arr); 160 | } 161 | console.log(mimeCodec); 162 | _this.mseSourceBuffer = _this.mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"'); 163 | _this.mseSourceBuffer.mode = "segments" 164 | _this.mseSourceBuffer.addEventListener("updateend", _this.pushPacket); 165 | 166 | } else { 167 | _this.readPacket(event.data); 168 | } 169 | }; 170 | }, false); 171 | 172 | }, 173 | readPacket:function(packet){ 174 | if (!this.mseStreamingStarted) { 175 | this.mseSourceBuffer.appendBuffer(packet); 176 | this.mseStreamingStarted = true; 177 | return; 178 | } 179 | this.mseQueue.push(packet); 180 | 181 | if (!this.mseSourceBuffer.updating) { 182 | this.pushPacket(); 183 | } 184 | }, 185 | pushPacket:function(){ 186 | var _this=rtspPlayer; 187 | if (!_this.mseSourceBuffer.updating) { 188 | if (_this.mseQueue.length > 0) { 189 | packet = _this.mseQueue.shift(); 190 | var view = new Uint8Array(packet); 191 | _this.mseSourceBuffer.appendBuffer(packet); 192 | } else { 193 | _this.mseStreamingStarted = false; 194 | } 195 | } 196 | }, 197 | streamPlayUrl:function(type){ 198 | switch (type) { 199 | case 'hls': 200 | return '/stream/' + this.uuid + '/hls/live/index.m3u8'; 201 | break; 202 | case 'mse': 203 | var potocol = 'ws'; 204 | if (location.protocol == 'https:') { 205 | potocol = 'wss'; 206 | } 207 | return potocol+'://'+location.host+'/stream/' + this.uuid +'/mse?uuid='+this.uuid; 208 | //return 'ws://sr4.ipeye.ru/ws/mp4/live?name=d4ee855e40874ef7b7149357a42f18f0'; 209 | break; 210 | case 'webrtc': 211 | return "/stream/"+this.uuid+"/webrtc?uuid=" + this.uuid; 212 | break; 213 | default: 214 | return ''; 215 | } 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /web/static/plugins/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([class]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([class]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | } 190 | 191 | label { 192 | display: inline-block; 193 | margin-bottom: 0.5rem; 194 | } 195 | 196 | button { 197 | border-radius: 0; 198 | } 199 | 200 | button:focus { 201 | outline: 1px dotted; 202 | outline: 5px auto -webkit-focus-ring-color; 203 | } 204 | 205 | input, 206 | button, 207 | select, 208 | optgroup, 209 | textarea { 210 | margin: 0; 211 | font-family: inherit; 212 | font-size: inherit; 213 | line-height: inherit; 214 | } 215 | 216 | button, 217 | input { 218 | overflow: visible; 219 | } 220 | 221 | button, 222 | select { 223 | text-transform: none; 224 | } 225 | 226 | [role="button"] { 227 | cursor: pointer; 228 | } 229 | 230 | select { 231 | word-wrap: normal; 232 | } 233 | 234 | button, 235 | [type="button"], 236 | [type="reset"], 237 | [type="submit"] { 238 | -webkit-appearance: button; 239 | } 240 | 241 | button:not(:disabled), 242 | [type="button"]:not(:disabled), 243 | [type="reset"]:not(:disabled), 244 | [type="submit"]:not(:disabled) { 245 | cursor: pointer; 246 | } 247 | 248 | button::-moz-focus-inner, 249 | [type="button"]::-moz-focus-inner, 250 | [type="reset"]::-moz-focus-inner, 251 | [type="submit"]::-moz-focus-inner { 252 | padding: 0; 253 | border-style: none; 254 | } 255 | 256 | input[type="radio"], 257 | input[type="checkbox"] { 258 | box-sizing: border-box; 259 | padding: 0; 260 | } 261 | 262 | textarea { 263 | overflow: auto; 264 | resize: vertical; 265 | } 266 | 267 | fieldset { 268 | min-width: 0; 269 | padding: 0; 270 | margin: 0; 271 | border: 0; 272 | } 273 | 274 | legend { 275 | display: block; 276 | width: 100%; 277 | max-width: 100%; 278 | padding: 0; 279 | margin-bottom: .5rem; 280 | font-size: 1.5rem; 281 | line-height: inherit; 282 | color: inherit; 283 | white-space: normal; 284 | } 285 | 286 | progress { 287 | vertical-align: baseline; 288 | } 289 | 290 | [type="number"]::-webkit-inner-spin-button, 291 | [type="number"]::-webkit-outer-spin-button { 292 | height: auto; 293 | } 294 | 295 | [type="search"] { 296 | outline-offset: -2px; 297 | -webkit-appearance: none; 298 | } 299 | 300 | [type="search"]::-webkit-search-decoration { 301 | -webkit-appearance: none; 302 | } 303 | 304 | ::-webkit-file-upload-button { 305 | font: inherit; 306 | -webkit-appearance: button; 307 | } 308 | 309 | output { 310 | display: inline-block; 311 | } 312 | 313 | summary { 314 | display: list-item; 315 | cursor: pointer; 316 | } 317 | 318 | template { 319 | display: none; 320 | } 321 | 322 | [hidden] { 323 | display: none !important; 324 | } 325 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /web/static/plugins/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.5.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2020 The Bootstrap Authors 4 | * Copyright 2011-2020 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: block; 10 | src: url("../webfonts/fa-brands-400.eot"); 11 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 12 | 13 | .fab { 14 | font-family: 'Font Awesome 5 Brands'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400} -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: block; 10 | src: url("../webfonts/fa-regular-400.eot"); 11 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 12 | 13 | .far { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400} -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | font-display: block; 10 | src: url("../webfonts/fa-solid-900.eot"); 11 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 12 | 13 | .fa, 14 | .fas { 15 | font-family: 'Font Awesome 5 Free'; 16 | font-weight: 900; } 17 | -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900} -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/css/svg-with-js.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top left;transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor)}.svg-inline--fa .fa-secondary,.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:.4;opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.fad.fa-inverse{color:#fff} -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /web/static/plugins/fontawesome-free/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepch/RTSPtoWeb/7ace5ed26faaadf3735c1efeec7ebdf20b635dca/web/static/plugins/fontawesome-free/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /web/templates/add_stream.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Add stream

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |

Main channel parameters

29 |
30 |
31 |
32 |
33 | 34 | 35 | You can choose any name for the stream, for example "My room" or "Happy sausage" 36 |
37 |
38 | 39 | 40 | Enter rtsp address as instructed by your camera. Look like rtsp://<ip>:<port>/path 41 |
42 |
43 | 44 | 49 | On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button 50 |
51 |
52 |
53 | 54 | 55 |
56 | Select this options if you want get more data about the stream 57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | {{template "foot.tmpl" .}} 74 | 75 | 100 | -------------------------------------------------------------------------------- /web/templates/documentation.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Documentation

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |

23 | API documentation is available in the GitHub repository. 24 |

25 |

26 | See the project README for installation and configuration instructions. 27 |

28 |
29 |
30 |
31 |
32 | {{template "foot.tmpl" .}} 33 | -------------------------------------------------------------------------------- /web/templates/edit_stream.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Edit stream

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 | 18 | {{ $stream:= (index .streams .uuid)}} 19 | {{ $mainChannel := (index $stream.Channels "0") }} 20 |
21 |
22 |
23 |
24 |
25 |
26 |

Parameters main

27 |
28 |
29 |
30 |
31 | 32 | 33 | You can choose any name for the stream, for example "My room" or "Happy sausage" 34 |
35 |
36 | 37 | 38 | Enter rtsp address as instructed by your camera. Look like rtsp://<ip>:<port>/path 39 |
40 |
41 | 42 | 52 | On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button 53 |
54 |
55 |
56 | 59 | 60 |
61 | Select this options if you want get more data about the stream 62 |
63 |
64 |
65 |
66 |
67 | {{ if gt (len $stream.Channels) 1}} 68 | {{ range $key, $value := $stream.Channels }} 69 | {{ if ne $key "0"}} 70 |
71 |
72 |
73 |

Sub channel parameters

74 |
75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 | 83 | Enter rtsp address as instructed by your camera. Look like rtsp://<ip>:<port>/path 84 |
85 |
86 | 87 | 88 | 99 | On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button 100 |
101 |
102 |
103 | 106 | 107 |
108 | Select this options if you want get more data about the stream 109 |
110 |
111 |
112 |
113 |
114 | {{ end }} 115 | {{ end }} 116 | {{ end }} 117 |
118 |
119 |
120 | 121 | 122 |
123 |
124 | 125 |
126 |
127 | 128 | {{template "foot.tmpl" .}} 129 | 130 | 152 | -------------------------------------------------------------------------------- /web/templates/foot.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 30 | 31 | 32 | 33 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /web/templates/head.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | RTSPtoWEB 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 37 | 38 | 39 | 40 | 103 | 104 | 105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /web/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Dashboard

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |
Streams ({{ len .streams}})
23 |
24 |
25 | 26 |
27 | {{ range $key, $value := .streams }} 28 |
29 | 30 |
31 |
32 |

{{.Name}}

33 |
34 | {{len .Channels }} 35 |
36 |
37 |
38 | 39 | 64 | 65 |
66 |
67 |
68 | {{ if gt (len .Channels) 1}} 69 |
70 | MSE 71 | 76 |
77 |
78 | HLS 79 | 84 |
85 |
86 | WebRTC 87 | 92 |
93 |
94 | ALL 95 | 100 |
101 | {{else}} 102 | MSE 103 | HLS 104 | WebRTC 105 | ALL 106 | {{end}} 107 | 108 | 109 | 110 | Edit 111 | Delete 112 |
113 |
114 |
115 | 116 | 117 |
118 | 119 |
120 |
121 | {{ end }} 122 |
123 |
124 | 125 |
126 | 127 | 128 | 129 | {{template "foot.tmpl" .}} 130 | -------------------------------------------------------------------------------- /web/templates/play_hls.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Play hls video

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 |
18 | {{template "player.tmpl" .}} 19 | 20 | {{template "foot.tmpl" .}} 21 |
22 | 23 | 79 | -------------------------------------------------------------------------------- /web/templates/play_mse.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Play mse video

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 |
18 | {{template "player.tmpl" .}} 19 | 20 | {{template "foot.tmpl" .}} 21 |
22 | 115 | -------------------------------------------------------------------------------- /web/templates/play_webrtc.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Play webrtc video

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 |
18 | {{template "player.tmpl" .}} 19 | 20 | {{template "foot.tmpl" .}} 21 |
22 | 129 | -------------------------------------------------------------------------------- /web/templates/player.tmpl: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 | 9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | {{ range $key, $value := .streams }} 18 |
19 | 20 |
21 |
22 |

{{.Name}}

23 |
24 | {{len .Channels }} 25 |
26 |
27 |
28 | 29 | 54 | 55 |
56 |
57 |
58 | {{ if gt (len .Channels) 1}} 59 |
60 | MSE 61 | 66 |
67 |
68 | HLS 69 | 74 |
75 |
76 | WebRTC 77 | 82 |
83 |
84 | ALL 85 | 90 |
91 | {{else}} 92 | MSE 93 | HLS 94 | WebRTC 95 | ALL 96 | {{end}} 97 | 98 | 99 | 100 | Edit 101 | Delete 102 |
103 |
104 |
105 | 106 | 107 |
108 | 109 |
110 |
111 | {{ end }} 112 |
113 |
114 |
115 |
116 |
117 | -------------------------------------------------------------------------------- /web/templates/stream_list.tmpl: -------------------------------------------------------------------------------- 1 | {{template "head.tmpl" .}} 2 |
3 |
4 |
5 |
6 |

Streams list

7 |
8 |
9 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{ range $key, $value := .streams }} 21 |
22 | 23 |
24 |
25 |

{{.Name}}

26 |
27 | {{len .Channels }} 28 |
29 |
30 |
31 | 32 | 57 | 58 |
59 |
60 |
61 | {{ if gt (len .Channels) 1}} 62 |
63 | MSE 64 | 69 |
70 |
71 | HLS 72 | 77 |
78 |
79 | WebRTC 80 | 85 |
86 |
87 | ALL 88 | 93 |
94 | {{else}} 95 | MSE 96 | HLS 97 | WebRTC 98 | ALL 99 | {{end}} 100 | 101 | 102 | 103 | Edit 104 | Delete 105 |
106 |
107 |
108 | 109 | 110 |
111 | 112 |
113 |
114 | {{ end }} 115 |
116 | 117 |
118 |
119 | {{template "foot.tmpl" .}} 120 | --------------------------------------------------------------------------------