├── .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 | [](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 |
15 |
--------------------------------------------------------------------------------
/web/static/img/noimage.svg:
--------------------------------------------------------------------------------
1 |
2 |
81 |
--------------------------------------------------------------------------------
/web/static/img/pic.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
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 |
17 |
18 | {{ $stream:= (index .streams .uuid)}}
19 | {{ $mainChannel := (index $stream.Channels "0") }}
20 |
21 |
22 |
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 |