Initial commit

main ziyun
karlkyo 11 months ago
commit fe342c3ed2

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '36 4 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# 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
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

@ -0,0 +1,48 @@
name: Create and publish Docker image
on:
release:
types: [created]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Docker Login
uses: docker/login-action@v2.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Metadata action
id: meta
uses: docker/metadata-action@v4.3.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v3.3.1
with:
context: .
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

2
.gitignore vendored

@ -0,0 +1,2 @@
.idea/
RTSPtoWeb

@ -0,0 +1,30 @@
# syntax=docker/dockerfile:1
FROM --platform=${BUILDPLATFORM} golang:1.19-alpine3.15 AS builder
RUN apk add git
WORKDIR /go/src/app
COPY . .
ARG TARGETOS TARGETARCH TARGETVARIANT
ENV CGO_ENABLED=0
RUN go get \
&& go mod download \
&& GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#"v"} go build -a -o rtsp-to-web
FROM alpine:3.17
WORKDIR /app
COPY --from=builder /go/src/app/rtsp-to-web /app/
COPY --from=builder /go/src/app/web /app/web
RUN mkdir -p /config
COPY --from=builder /go/src/app/config.json /config
ENV GO111MODULE="on"
ENV GIN_MODE="release"
CMD ["./rtsp-to-web", "--config=/config/config.json"]

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Andrey Semochkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,32 @@
APP=RTSPtoWeb
SERVER_FLAGS ?= -config config.json
P="\\033[34m[+]\\033[0m"
build:
@echo "$(P) build"
GO111MODULE=on go build *.go
run:
@echo "$(P) run"
GO111MODULE=on go run *.go
serve:
@$(MAKE) server
server:
@echo "$(P) server $(SERVER_FLAGS)"
./${APP} $(SERVER_FLAGS)
test:
@echo "$(P) test"
bash test.curl
bash test_multi.curl
lint:
@echo "$(P) lint"
go vet
.NOTPARALLEL:
.PHONY: build run server test lint

@ -0,0 +1,257 @@
# RTSPtoWeb share you ip camera to world!
RTSPtoWeb converts your RTSP streams to formats consumable in a web browser
like MSE (Media Source Extensions), WebRTC, or HLS. It's fully native Golang
without the use of FFmpeg or GStreamer!
## Table of Contents
- [Installation](#installation)
- [Configuration](#configuration)
- [Command-line](#command-line)
- [API documentation](#api-documentation)
- [Limitations](#Limitations)
- [Performance](#Performance)
- [Authors](#Authors)
- [License](#license)
## Installation
### Installation from source
1. Download source
```bash
$ git clone https://github.com/deepch/RTSPtoWeb
```
1. CD to Directory
```bash
$ cd RTSPtoWeb/
```
1. Test Run
```bash
$ GO111MODULE=on go run *.go
```
1. Open Browser
```bash
open web browser http://127.0.0.1:8083 work chrome, safari, firefox
```
## Installation from docker
1. Run docker container
```bash
$ docker run --name rtsp-to-web --network host ghcr.io/deepch/rtsptoweb:latest
```
1. Open Browser
```bash
open web browser http://127.0.0.1:8083 in chrome, safari, firefox
```
You may override the <a href="#example-configjson">configuration</a> `/PATH_TO_CONFIG/config.json` and mount as a docker volume:
```bash
$ docker run --name rtsp-to-web \
-v /PATH_TO_CONFIG/config.json:/config/config.json \
--network host \
ghcr.io/deepch/rtsptoweb:latest
```
## Configuration
### Server settings
```text
debug - enable debug output
log_level - log level (trace, debug, info, warning, error, fatal, or panic)
http_demo - serve static files
http_debug - debug http api server
http_login - http auth login
http_password - http auth password
http_port - http server port
http_dir - path to serve static files from
ice_servers - array of servers to use for STUN/TURN
ice_username - username to use for STUN/TURN
ice_credential - credential to use for STUN/TURN
webrtc_port_min - minimum WebRTC port to use (UDP)
webrtc_port_max - maximum WebRTC port to use (UDP)
https
https_auto_tls
https_auto_tls_name
https_cert
https_key
https_port
rtsp_port - rtsp server port
```
### Stream settings
```text
name - stream name
```
### Channel settings
```text
name - channel name
url - channel rtsp url
on_demand - stream mode static (run any time) or ondemand (run only has viewers)
debug - enable debug output (RTSP client)
audio - enable audio
status - default stream status
```
#### Authorization play video
1 - enable config
```text
"token": {
"enable": true,
"backend": "http://127.0.0.1/file.php"
}
```
2 - try
```text
rtsp://127.0.0.1:5541/demo/0?token=you_key
```
file.php need response json
```text
status: "1" or "0"
```
#### RTSP pull modes
* **on demand** (on_demand=true) - only pull video from the source when there's a viewer
* **static** (on_demand=false) - pull video from the source constantly
### Example config.json
```json
{
"server": {
"debug": true,
"log_level": "info",
"http_demo": true,
"http_debug": false,
"http_login": "demo",
"http_password": "demo",
"http_port": ":8083",
"ice_servers": ["stun:stun.l.google.com:19302"],
"rtsp_port": ":5541"
},
"streams": {
"demo1": {
"name": "test video stream 1",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@YOU_CAMERA_IP/uri",
"on_demand": true,
"debug": false,
"audio": true,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@YOU_CAMERA_IP/uri",
"on_demand": true,
"debug": false,
"audio": true,
"status": 0
}
}
},
"demo2": {
"name": "test video stream 2",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@YOU_CAMERA_IP/uri",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@YOU_CAMERA_IP/uri",
"on_demand": true,
"debug": false,
"status": 0
}
}
}
},
"channel_defaults": {
"on_demand": true
}
}
```
## Command-line
### Use help to show available args
```bash
./RTSPtoWeb --help
```
#### Response
```bash
Usage of ./RTSPtoWeb:
-config string
config patch (/etc/server/config.json or config.json) (default "config.json")
-debug
set debug mode (default true)
```
## API documentation
See the [API docs](/docs/api.md)
## Limitations
Video Codecs Supported: H264 all profiles
Audio Codecs Supported: no
## Performance
```bash
CPU usage ≈0.2%-1% one (thread) core cpu intel core i7 per stream
```
## Authors
* **Andrey Semochkin** - *Initial work video* - [deepch](https://github.com/deepch)
* **Dmitriy Vladykin** - *Initial work web UI* - [vdalex25](https://github.com/vdalex25)
See also the list of [contributors](https://github.com/deepch/RTSPtoWeb/contributors) who participated in this project.
## License
This project licensed. License - see the [LICENSE.md](LICENSE.md) file for details
[webrtc](https://github.com/pion/webrtc) follows license MIT [license](https://raw.githubusercontent.com/pion/webrtc/master/LICENSE).
[joy4](https://github.com/nareix/joy4) follows license MIT [license](https://raw.githubusercontent.com/nareix/joy4/master/LICENSE).
## Other Example
Examples of working with video on golang
- [RTSPtoWeb](https://github.com/deepch/RTSPtoWeb)
- [RTSPtoWebRTC](https://github.com/deepch/RTSPtoWebRTC)
- [RTSPtoWSMP4f](https://github.com/deepch/RTSPtoWSMP4f)
- [RTSPtoImage](https://github.com/deepch/RTSPtoImage)
- [RTSPtoHLS](https://github.com/deepch/RTSPtoHLS)
- [RTSPtoHLSLL](https://github.com/deepch/RTSPtoHLSLL)
[![paypal.me/AndreySemochkin](https://ionicabizau.github.io/badges/paypal.svg)](https://www.paypal.me/AndreySemochkin) - You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea:

@ -0,0 +1,42 @@
package main
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
func main() {
log.WithFields(logrus.Fields{
"module": "main",
"func": "main",
}).Info("Server CORE start")
go HTTPAPIServer()
go RTSPServer()
go Storage.StreamChannelRunAll()
signalChanel := make(chan os.Signal, 1)
done := make(chan bool, 1)
signal.Notify(signalChanel, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalChanel
log.WithFields(logrus.Fields{
"module": "main",
"func": "main",
}).Info("Server receive signal", sig)
done <- true
}()
log.WithFields(logrus.Fields{
"module": "main",
"func": "main",
}).Info("Server start success a wait signals")
<-done
Storage.StopAll()
time.Sleep(2 * time.Second)
log.WithFields(logrus.Fields{
"module": "main",
"func": "main",
}).Info("Server stop working by signal")
}

@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

@ -0,0 +1,145 @@
package main
import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
//HTTPAPIServerStreamChannelCodec function return codec info struct
func HTTPAPIServerStreamChannelCodec(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamChannelCodec",
})
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelCodec",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: codecs})
}
//HTTPAPIServerStreamChannelInfo function return stream info struct
func HTTPAPIServerStreamChannelInfo(c *gin.Context) {
info, err := Storage.StreamChannelInfo(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamChannelInfo",
"call": "StreamChannelInfo",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: info})
}
//HTTPAPIServerStreamChannelReload function reload stream
func HTTPAPIServerStreamChannelReload(c *gin.Context) {
err := Storage.StreamChannelReload(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamChannelReload",
"call": "StreamChannelReload",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamChannelEdit function edit stream
func HTTPAPIServerStreamChannelEdit(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamChannelEdit",
})
var payload ChannelST
err := c.BindJSON(&payload)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "BindJSON",
}).Errorln(err.Error())
return
}
err = Storage.StreamChannelEdit(c.Param("uuid"), c.Param("channel"), payload)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelEdit",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamChannelDelete function delete stream
func HTTPAPIServerStreamChannelDelete(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamChannelDelete",
})
err := Storage.StreamChannelDelete(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelDelete",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamChannelAdd function add new stream
func HTTPAPIServerStreamChannelAdd(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamChannelAdd",
})
var payload ChannelST
err := c.BindJSON(&payload)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "BindJSON",
}).Errorln(err.Error())
return
}
err = Storage.StreamChannelAdd(c.Param("uuid"), c.Param("channel"), payload)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelAdd",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}

@ -0,0 +1,141 @@
package main
import (
"bytes"
"time"
"github.com/deepch/vdk/format/ts"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
//HTTPAPIServerStreamHLSM3U8 send client m3u8 play list
func HTTPAPIServerStreamHLSM3U8(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_hls",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamHLSM3U8",
})
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
if !RemoteAuthorization("HLS", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) {
requestLogger.WithFields(logrus.Fields{
"call": "RemoteAuthorization",
}).Errorln(ErrorStreamUnauthorized.Error())
return
}
c.Header("Content-Type", "application/x-mpegURL")
Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel"))
//If stream mode on_demand need wait ready segment's
for i := 0; i < 40; i++ {
index, seq, err := Storage.StreamHLSm3u8(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamHLSm3u8",
}).Errorln(err.Error())
return
}
if seq >= 6 {
_, err := c.Writer.Write([]byte(index))
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(err.Error())
return
}
return
}
time.Sleep(1 * time.Second)
}
}
//HTTPAPIServerStreamHLSTS send client ts segment
func HTTPAPIServerStreamHLSTS(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_hls",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamHLSTS",
})
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamCodecs",
}).Errorln(err.Error())
return
}
outfile := bytes.NewBuffer([]byte{})
Muxer := ts.NewMuxer(outfile)
Muxer.PaddingToMakeCounterCont = true
err = Muxer.WriteHeader(codecs)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
seqData, err := Storage.StreamHLSTS(c.Param("uuid"), c.Param("channel"), stringToInt(c.Param("seq")))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamHLSTS",
}).Errorln(err.Error())
return
}
if len(seqData) == 0 {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotHLSSegments.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "seqData",
}).Errorln(ErrorStreamNotHLSSegments.Error())
return
}
for _, v := range seqData {
v.CompositionTime = 1
err = Muxer.WritePacket(*v)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "WritePacket",
}).Errorln(err.Error())
return
}
}
err = Muxer.WriteTrailer()
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "WriteTrailer",
}).Errorln(err.Error())
return
}
_, err = c.Writer.Write(outfile.Bytes())
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(err.Error())
return
}
}

@ -0,0 +1,227 @@
package main
import (
"github.com/deepch/vdk/format/mp4f"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
//HTTPAPIServerStreamHLSLLInit send client ts segment
func HTTPAPIServerStreamHLSLLInit(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_hlsll",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamHLSLLInit",
})
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
if !RemoteAuthorization("HLS", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) {
requestLogger.WithFields(logrus.Fields{
"call": "RemoteAuthorization",
}).Errorln(ErrorStreamUnauthorized.Error())
return
}
c.Header("Content-Type", "application/x-mpegURL")
Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel"))
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelCodecs",
}).Errorln(err.Error())
return
}
Muxer := mp4f.NewMuxer(nil)
err = Muxer.WriteHeader(codecs)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
c.Header("Content-Type", "video/mp4")
_, buf := Muxer.GetInit(codecs)
_, err = c.Writer.Write(buf)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(err.Error())
return
}
}
//HTTPAPIServerStreamHLSLLM3U8 send client m3u8 play list
func HTTPAPIServerStreamHLSLLM3U8(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_hlsll",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamHLSLLM3U8",
})
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
c.Header("Content-Type", "application/x-mpegURL")
Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel"))
index, err := Storage.HLSMuxerM3U8(c.Param("uuid"), c.Param("channel"), stringToInt(c.DefaultQuery("_HLS_msn", "-1")), stringToInt(c.DefaultQuery("_HLS_part", "-1")))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "HLSMuxerM3U8",
}).Errorln(ErrorStreamNotFound.Error())
return
}
_, err = c.Writer.Write([]byte(index))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(ErrorStreamNotFound.Error())
return
}
}
//HTTPAPIServerStreamHLSLLM4Segment send client ts segment
func HTTPAPIServerStreamHLSLLM4Segment(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_hlsll",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamHLSLLM4Segment",
})
c.Header("Content-Type", "video/mp4")
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelCodecs",
}).Errorln(err.Error())
return
}
if codecs == nil {
requestLogger.WithFields(logrus.Fields{
"call": "StreamCodecs",
}).Errorln("Codec Null")
return
}
Muxer := mp4f.NewMuxer(nil)
err = Muxer.WriteHeader(codecs)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
seqData, err := Storage.HLSMuxerSegment(c.Param("uuid"), c.Param("channel"), stringToInt(c.Param("segment")))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "HLSMuxerSegment",
}).Errorln(err.Error())
return
}
for _, v := range seqData {
err = Muxer.WritePacket4(*v)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WritePacket4",
}).Errorln(err.Error())
return
}
}
buf := Muxer.Finalize()
_, err = c.Writer.Write(buf)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(err.Error())
return
}
}
//HTTPAPIServerStreamHLSLLM4Fragment send client ts segment
func HTTPAPIServerStreamHLSLLM4Fragment(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_hlsll",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamHLSLLM4Fragment",
})
c.Header("Content-Type", "video/mp4")
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelCodecs",
}).Errorln(err.Error())
return
}
if codecs == nil {
requestLogger.WithFields(logrus.Fields{
"call": "StreamCodecs",
}).Errorln("Codec Null")
return
}
Muxer := mp4f.NewMuxer(nil)
err = Muxer.WriteHeader(codecs)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
seqData, err := Storage.HLSMuxerFragment(c.Param("uuid"), c.Param("channel"), stringToInt(c.Param("segment")), stringToInt(c.Param("fragment")))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "HLSMuxerFragment",
}).Errorln(err.Error())
return
}
for _, v := range seqData {
err = Muxer.WritePacket4(*v)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WritePacket4",
}).Errorln(err.Error())
return
}
}
buf := Muxer.Finalize()
_, err = c.Writer.Write(buf)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(err.Error())
return
}
}

@ -0,0 +1,189 @@
package main
import (
"time"
"github.com/gobwas/ws/wsutil"
"github.com/gobwas/ws"
"github.com/gin-gonic/gin"
"github.com/deepch/vdk/format/mp4f"
"github.com/sirupsen/logrus"
)
//HTTPAPIServerStreamMSE func
func HTTPAPIServerStreamMSE(c *gin.Context) {
conn, _, _, err := ws.UpgradeHTTP(c.Request, c.Writer)
if err != nil {
return
}
requestLogger := log.WithFields(logrus.Fields{
"module": "http_mse",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamMSE",
})
defer func() {
err = conn.Close()
requestLogger.WithFields(logrus.Fields{
"call": "Close",
}).Errorln(err)
log.Println("Client Full Exit")
}()
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
if !RemoteAuthorization("WS", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) {
requestLogger.WithFields(logrus.Fields{
"call": "RemoteAuthorization",
}).Errorln(ErrorStreamUnauthorized.Error())
return
}
Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel"))
err = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "SetWriteDeadline",
}).Errorln(err.Error())
return
}
cid, ch, _, err := Storage.ClientAdd(c.Param("uuid"), c.Param("channel"), MSE)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "ClientAdd",
}).Errorln(err.Error())
return
}
defer Storage.ClientDelete(c.Param("uuid"), cid, c.Param("channel"))
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "StreamCodecs",
}).Errorln(err.Error())
return
}
muxerMSE := mp4f.NewMuxer(nil)
err = muxerMSE.WriteHeader(codecs)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
meta, init := muxerMSE.GetInit(codecs)
err = wsutil.WriteServerMessage(conn, ws.OpBinary, append([]byte{9}, meta...))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Send",
}).Errorln(err.Error())
return
}
err = wsutil.WriteServerMessage(conn, ws.OpBinary, init)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Send",
}).Errorln(err.Error())
return
}
var videoStart bool
controlExit := make(chan bool, 10)
noClient := time.NewTimer(10 * time.Second)
go func() {
defer func() {
controlExit <- true
}()
for {
header, _, err := wsutil.NextReader(conn, ws.StateServerSide)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Receive",
}).Errorln(err.Error())
return
}
switch header.OpCode {
case ws.OpPong:
noClient.Reset(10 * time.Second)
case ws.OpClose:
return
}
}
}()
noVideo := time.NewTimer(10 * time.Second)
pingTicker := time.NewTicker(500 * time.Millisecond)
defer pingTicker.Stop()
defer log.Println("client exit")
for {
select {
case <-pingTicker.C:
err = conn.SetWriteDeadline(time.Now().Add(3 * time.Second))
if err != nil {
return
}
buf, err := ws.CompileFrame(ws.NewPingFrame(nil))
if err != nil {
return
}
_, err = conn.Write(buf)
if err != nil {
return
}
case <-controlExit:
requestLogger.WithFields(logrus.Fields{
"call": "controlExit",
}).Errorln("Client Reader Exit")
return
case <-noClient.C:
requestLogger.WithFields(logrus.Fields{
"call": "ErrorClientOffline",
}).Errorln("Client OffLine Exit")
return
case <-noVideo.C:
requestLogger.WithFields(logrus.Fields{
"call": "ErrorStreamNoVideo",
}).Errorln(ErrorStreamNoVideo.Error())
return
case pck := <-ch:
if pck.IsKeyFrame {
noVideo.Reset(10 * time.Second)
videoStart = true
}
if !videoStart {
continue
}
ready, buf, err := muxerMSE.WritePacket(*pck, false)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WritePacket",
}).Errorln(err.Error())
return
}
if ready {
err := conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "SetWriteDeadline",
}).Errorln(err.Error())
return
}
//err = websocket.Message.Send(ws, buf)
err = wsutil.WriteServerMessage(conn, ws.OpBinary, buf)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Send",
}).Errorln(err.Error())
return
}
}
}
}
}

@ -0,0 +1,302 @@
package main
import (
"net/http"
"os"
"time"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// Message resp struct
type Message struct {
Status int `json:"status"`
Payload interface{} `json:"payload"`
}
// HTTPAPIServer start http server routes
func HTTPAPIServer() {
//Set HTTP API mode
log.WithFields(logrus.Fields{
"module": "http_server",
"func": "RTSPServer",
"call": "Start",
}).Infoln("Server HTTP start")
var public *gin.Engine
if !Storage.ServerHTTPDebug() {
gin.SetMode(gin.ReleaseMode)
public = gin.New()
} else {
gin.SetMode(gin.DebugMode)
public = gin.Default()
}
public.Use(CrossOrigin())
//Add private login password protect methods
privat := public.Group("/")
if Storage.ServerHTTPLogin() != "" && Storage.ServerHTTPPassword() != "" {
privat.Use(gin.BasicAuth(gin.Accounts{Storage.ServerHTTPLogin(): Storage.ServerHTTPPassword()}))
}
/*
Static HTML Files Demo Mode
*/
if Storage.ServerHTTPDemo() {
public.LoadHTMLGlob(Storage.ServerHTTPDir() + "/templates/*")
public.GET("/", HTTPAPIServerIndex)
public.GET("/pages/stream/list", HTTPAPIStreamList)
public.GET("/pages/stream/add", HTTPAPIAddStream)
public.GET("/pages/stream/edit/:uuid", HTTPAPIEditStream)
public.GET("/pages/player/hls/:uuid/:channel", HTTPAPIPlayHls)
public.GET("/pages/player/mse/:uuid/:channel", HTTPAPIPlayMse)
public.GET("/pages/player/webrtc/:uuid/:channel", HTTPAPIPlayWebrtc)
public.GET("/pages/multiview", HTTPAPIMultiview)
public.Any("/pages/multiview/full", HTTPAPIFullScreenMultiView)
public.GET("/pages/documentation", HTTPAPIServerDocumentation)
public.GET("/pages/player/all/:uuid/:channel", HTTPAPIPlayAll)
public.StaticFS("/static", http.Dir(Storage.ServerHTTPDir()+"/static"))
}
/*
Stream Control elements
*/
privat.GET("/streams", HTTPAPIServerStreams)
privat.POST("/stream/:uuid/add", HTTPAPIServerStreamAdd)
privat.POST("/stream/:uuid/edit", HTTPAPIServerStreamEdit)
privat.GET("/stream/:uuid/delete", HTTPAPIServerStreamDelete)
privat.GET("/stream/:uuid/reload", HTTPAPIServerStreamReload)
privat.GET("/stream/:uuid/info", HTTPAPIServerStreamInfo)
/*
Streams Multi Control elements
*/
privat.POST("/streams/multi/control/add", HTTPAPIServerStreamsMultiControlAdd)
privat.POST("/streams/multi/control/delete", HTTPAPIServerStreamsMultiControlDelete)
/*
Stream Channel elements
*/
privat.POST("/stream/:uuid/channel/:channel/add", HTTPAPIServerStreamChannelAdd)
privat.POST("/stream/:uuid/channel/:channel/edit", HTTPAPIServerStreamChannelEdit)
privat.GET("/stream/:uuid/channel/:channel/delete", HTTPAPIServerStreamChannelDelete)
privat.GET("/stream/:uuid/channel/:channel/codec", HTTPAPIServerStreamChannelCodec)
privat.GET("/stream/:uuid/channel/:channel/reload", HTTPAPIServerStreamChannelReload)
privat.GET("/stream/:uuid/channel/:channel/info", HTTPAPIServerStreamChannelInfo)
/*
Stream video elements
*/
//HLS
public.GET("/stream/:uuid/channel/:channel/hls/live/index.m3u8", HTTPAPIServerStreamHLSM3U8)
public.GET("/stream/:uuid/channel/:channel/hls/live/segment/:seq/file.ts", HTTPAPIServerStreamHLSTS)
//HLS remote record
//public.GET("/stream/:uuid/channel/:channel/hls/rr/:s/:e/index.m3u8", HTTPAPIServerStreamRRM3U8)
//public.GET("/stream/:uuid/channel/:channel/hls/rr/:s/:e/:seq/file.ts", HTTPAPIServerStreamRRTS)
//HLS LL
public.GET("/stream/:uuid/channel/:channel/hlsll/live/index.m3u8", HTTPAPIServerStreamHLSLLM3U8)
public.GET("/stream/:uuid/channel/:channel/hlsll/live/init.mp4", HTTPAPIServerStreamHLSLLInit)
public.GET("/stream/:uuid/channel/:channel/hlsll/live/segment/:segment/:any", HTTPAPIServerStreamHLSLLM4Segment)
public.GET("/stream/:uuid/channel/:channel/hlsll/live/fragment/:segment/:fragment/:any", HTTPAPIServerStreamHLSLLM4Fragment)
//MSE
public.GET("/stream/:uuid/channel/:channel/mse", HTTPAPIServerStreamMSE)
public.POST("/stream/:uuid/channel/:channel/webrtc", HTTPAPIServerStreamWebRTC)
//Save fragment to mp4
public.GET("/stream/:uuid/channel/:channel/save/mp4/fragment/:duration", HTTPAPIServerStreamSaveToMP4)
/*
HTTPS Mode Cert
# Key considerations for algorithm "RSA" 2048-bit
openssl genrsa -out server.key 2048
# Key considerations for algorithm "ECDSA" secp384r1
# List ECDSA the supported curves (openssl ecparam -list_curves)
#openssl ecparam -genkey -name secp384r1 -out server.key
#Generation of self-signed(x509) public key (PEM-encodings .pem|.crt) based on the private (.key)
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
*/
if Storage.ServerHTTPS() {
if Storage.ServerHTTPSAutoTLSEnable() {
go func() {
err := autotls.Run(public, Storage.ServerHTTPSAutoTLSName()+Storage.ServerHTTPSPort())
if err != nil {
log.Println("Start HTTPS Server Error", err)
}
}()
} else {
go func() {
err := public.RunTLS(Storage.ServerHTTPSPort(), Storage.ServerHTTPSCert(), Storage.ServerHTTPSKey())
if err != nil {
log.WithFields(logrus.Fields{
"module": "http_router",
"func": "HTTPSAPIServer",
"call": "ServerHTTPSPort",
}).Fatalln(err.Error())
os.Exit(1)
}
}()
}
}
err := public.Run(Storage.ServerHTTPPort())
if err != nil {
log.WithFields(logrus.Fields{
"module": "http_router",
"func": "HTTPAPIServer",
"call": "ServerHTTPPort",
}).Fatalln(err.Error())
os.Exit(1)
}
}
// HTTPAPIServerIndex index file
func HTTPAPIServerIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "index",
})
}
func HTTPAPIServerDocumentation(c *gin.Context) {
c.HTML(http.StatusOK, "documentation.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "documentation",
})
}
func HTTPAPIStreamList(c *gin.Context) {
c.HTML(http.StatusOK, "stream_list.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "stream_list",
})
}
func HTTPAPIPlayHls(c *gin.Context) {
c.HTML(http.StatusOK, "play_hls.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_hls",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
func HTTPAPIPlayMse(c *gin.Context) {
c.HTML(http.StatusOK, "play_mse.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_mse",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
func HTTPAPIPlayWebrtc(c *gin.Context) {
c.HTML(http.StatusOK, "play_webrtc.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_webrtc",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
func HTTPAPIAddStream(c *gin.Context) {
c.HTML(http.StatusOK, "add_stream.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "add_stream",
})
}
func HTTPAPIEditStream(c *gin.Context) {
c.HTML(http.StatusOK, "edit_stream.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "edit_stream",
"uuid": c.Param("uuid"),
})
}
func HTTPAPIMultiview(c *gin.Context) {
c.HTML(http.StatusOK, "multiview.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "multiview",
})
}
func HTTPAPIPlayAll(c *gin.Context) {
c.HTML(http.StatusOK, "play_all.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_all",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
type MultiViewOptions struct {
Grid int `json:"grid"`
Player map[string]MultiViewOptionsGrid `json:"player"`
}
type MultiViewOptionsGrid struct {
UUID string `json:"uuid"`
Channel int `json:"channel"`
PlayerType string `json:"playerType"`
}
func HTTPAPIFullScreenMultiView(c *gin.Context) {
var createParams MultiViewOptions
err := c.ShouldBindJSON(&createParams)
if err != nil {
log.WithFields(logrus.Fields{
"module": "http_page",
"func": "HTTPAPIFullScreenMultiView",
"call": "BindJSON",
}).Errorln(err.Error())
}
log.WithFields(logrus.Fields{
"module": "http_page",
"func": "HTTPAPIFullScreenMultiView",
"call": "Options",
}).Debugln(createParams)
c.HTML(http.StatusOK, "fullscreenmulti.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"options": createParams,
"page": "fullscreenmulti",
"query": c.Request.URL.Query(),
})
}
// CrossOrigin Access-Control-Allow-Origin any methods
func CrossOrigin() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

@ -0,0 +1,129 @@
package main
import (
"fmt"
"github.com/deepch/vdk/format/mp4"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"os"
"time"
)
// HTTPAPIServerStreamSaveToMP4 func
func HTTPAPIServerStreamSaveToMP4(c *gin.Context) {
var err error
requestLogger := log.WithFields(logrus.Fields{
"module": "http_save_mp4",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamSaveToMP4",
})
defer func() {
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Close",
}).Errorln(err)
}
}()
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
if !RemoteAuthorization("save", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) {
requestLogger.WithFields(logrus.Fields{
"call": "RemoteAuthorization",
}).Errorln(ErrorStreamUnauthorized.Error())
return
}
c.Writer.Write([]byte("await save started"))
go func() {
Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel"))
cid, ch, _, err := Storage.ClientAdd(c.Param("uuid"), c.Param("channel"), MSE)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "ClientAdd",
}).Errorln(err.Error())
return
}
defer Storage.ClientDelete(c.Param("uuid"), cid, c.Param("channel"))
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "StreamCodecs",
}).Errorln(err.Error())
return
}
err = os.MkdirAll(fmt.Sprintf("save/%s/%s/", c.Param("uuid"), c.Param("channel")), 0755)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "MkdirAll",
}).Errorln(err.Error())
}
f, err := os.Create(fmt.Sprintf("save/%s/%s/%s.mp4", c.Param("uuid"), c.Param("channel"), time.Now().String()))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "Create",
}).Errorln(err.Error())
}
defer f.Close()
muxer := mp4.NewMuxer(f)
err = muxer.WriteHeader(codecs)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
defer muxer.WriteTrailer()
var videoStart bool
controlExit := make(chan bool, 10)
dur, err := time.ParseDuration(c.Param("duration"))
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "ParseDuration",
}).Errorln(err.Error())
}
saveLimit := time.NewTimer(dur)
noVideo := time.NewTimer(10 * time.Second)
defer log.Println("client exit")
for {
select {
case <-controlExit:
requestLogger.WithFields(logrus.Fields{
"call": "controlExit",
}).Errorln("Client Reader Exit")
return
case <-saveLimit.C:
requestLogger.WithFields(logrus.Fields{
"call": "saveLimit",
}).Errorln("Saved Limit End")
return
case <-noVideo.C:
requestLogger.WithFields(logrus.Fields{
"call": "ErrorStreamNoVideo",
}).Errorln(ErrorStreamNoVideo.Error())
return
case pck := <-ch:
if pck.IsKeyFrame {
noVideo.Reset(10 * time.Second)
videoStart = true
}
if !videoStart {
continue
}
if err = muxer.WritePacket(*pck); err != nil {
return
}
}
}
}()
}

@ -0,0 +1,3 @@
package main
//TODO add to next version

@ -0,0 +1,210 @@
package main
import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
//HTTPAPIServerStreams function return stream list
func HTTPAPIServerStreams(c *gin.Context) {
list, err := Storage.MarshalledStreamsList()
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: list})
}
//HTTPAPIServerStreamsMultiControlAdd function add new stream's
func HTTPAPIServerStreamsMultiControlAdd(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_stream",
"func": "HTTPAPIServerStreamsMultiControlAdd",
})
var payload StorageST
err := c.BindJSON(&payload)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "BindJSON",
}).Errorln(err.Error())
return
}
if payload.Streams == nil || len(payload.Streams) < 1 {
c.IndentedJSON(400, Message{Status: 0, Payload: ErrorStreamsLen0.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "len(payload)",
}).Errorln(ErrorStreamsLen0.Error())
return
}
var resp = make(map[string]Message)
var FoundError bool
for k, v := range payload.Streams {
err = Storage.StreamAdd(k, v)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"stream": k,
"call": "StreamAdd",
}).Errorln(err.Error())
resp[k] = Message{Status: 0, Payload: err.Error()}
FoundError = true
} else {
resp[k] = Message{Status: 1, Payload: Success}
}
}
if FoundError {
c.IndentedJSON(200, Message{Status: 0, Payload: resp})
} else {
c.IndentedJSON(200, Message{Status: 1, Payload: resp})
}
}
//HTTPAPIServerStreamsMultiControlDelete function delete stream's
func HTTPAPIServerStreamsMultiControlDelete(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_stream",
"func": "HTTPAPIServerStreamsMultiControlDelete",
})
var payload []string
err := c.BindJSON(&payload)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "BindJSON",
}).Errorln(err.Error())
return
}
if len(payload) < 1 {
c.IndentedJSON(400, Message{Status: 0, Payload: ErrorStreamsLen0.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "len(payload)",
}).Errorln(ErrorStreamsLen0.Error())
return
}
var resp = make(map[string]Message)
var FoundError bool
for _, key := range payload {
err := Storage.StreamDelete(key)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"stream": key,
"call": "StreamDelete",
}).Errorln(err.Error())
resp[key] = Message{Status: 0, Payload: err.Error()}
FoundError = true
} else {
resp[key] = Message{Status: 1, Payload: Success}
}
}
if FoundError {
c.IndentedJSON(200, Message{Status: 0, Payload: resp})
} else {
c.IndentedJSON(200, Message{Status: 1, Payload: resp})
}
}
//HTTPAPIServerStreamAdd function add new stream
func HTTPAPIServerStreamAdd(c *gin.Context) {
var payload StreamST
err := c.BindJSON(&payload)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamAdd",
"call": "BindJSON",
}).Errorln(err.Error())
return
}
err = Storage.StreamAdd(c.Param("uuid"), payload)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamAdd",
"call": "StreamAdd",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamEdit function edit stream
func HTTPAPIServerStreamEdit(c *gin.Context) {
var payload StreamST
err := c.BindJSON(&payload)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamEdit",
"call": "BindJSON",
}).Errorln(err.Error())
return
}
err = Storage.StreamEdit(c.Param("uuid"), payload)
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamEdit",
"call": "StreamEdit",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamDelete function delete stream
func HTTPAPIServerStreamDelete(c *gin.Context) {
err := Storage.StreamDelete(c.Param("uuid"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamDelete",
"call": "StreamDelete",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamDelete function reload stream
func HTTPAPIServerStreamReload(c *gin.Context) {
err := Storage.StreamReload(c.Param("uuid"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamReload",
"call": "StreamReload",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: Success})
}
//HTTPAPIServerStreamInfo function return stream info struct
func HTTPAPIServerStreamInfo(c *gin.Context) {
info, err := Storage.StreamInfo(c.Param("uuid"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
log.WithFields(logrus.Fields{
"module": "http_stream",
"stream": c.Param("uuid"),
"func": "HTTPAPIServerStreamInfo",
"call": "StreamInfo",
}).Errorln(err.Error())
return
}
c.IndentedJSON(200, Message{Status: 1, Payload: info})
}

@ -0,0 +1,99 @@
package main
import (
"time"
webrtc "github.com/deepch/vdk/format/webrtcv3"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
//HTTPAPIServerStreamWebRTC stream video over WebRTC
func HTTPAPIServerStreamWebRTC(c *gin.Context) {
requestLogger := log.WithFields(logrus.Fields{
"module": "http_webrtc",
"stream": c.Param("uuid"),
"channel": c.Param("channel"),
"func": "HTTPAPIServerStreamWebRTC",
})
if !Storage.StreamChannelExist(c.Param("uuid"), c.Param("channel")) {
c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNotFound.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
return
}
if !RemoteAuthorization("WebRTC", c.Param("uuid"), c.Param("channel"), c.Query("token"), c.ClientIP()) {
requestLogger.WithFields(logrus.Fields{
"call": "RemoteAuthorization",
}).Errorln(ErrorStreamUnauthorized.Error())
return
}
Storage.StreamChannelRun(c.Param("uuid"), c.Param("channel"))
codecs, err := Storage.StreamChannelCodecs(c.Param("uuid"), c.Param("channel"))
if err != nil {
c.IndentedJSON(500, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "StreamCodecs",
}).Errorln(err.Error())
return
}
muxerWebRTC := webrtc.NewMuxer(webrtc.Options{ICEServers: Storage.ServerICEServers(), ICEUsername: Storage.ServerICEUsername(), ICECredential: Storage.ServerICECredential(), PortMin: Storage.ServerWebRTCPortMin(), PortMax: Storage.ServerWebRTCPortMax()})
answer, err := muxerWebRTC.WriteHeader(codecs, c.PostForm("data"))
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "WriteHeader",
}).Errorln(err.Error())
return
}
_, err = c.Writer.Write([]byte(answer))
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "Write",
}).Errorln(err.Error())
return
}
go func() {
cid, ch, _, err := Storage.ClientAdd(c.Param("uuid"), c.Param("channel"), WEBRTC)
if err != nil {
c.IndentedJSON(400, Message{Status: 0, Payload: err.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "ClientAdd",
}).Errorln(err.Error())
return
}
defer Storage.ClientDelete(c.Param("uuid"), cid, c.Param("channel"))
var videoStart bool
noVideo := time.NewTimer(10 * time.Second)
for {
select {
case <-noVideo.C:
// c.IndentedJSON(500, Message{Status: 0, Payload: ErrorStreamNoVideo.Error()})
requestLogger.WithFields(logrus.Fields{
"call": "ErrorStreamNoVideo",
}).Errorln(ErrorStreamNoVideo.Error())
return
case pck := <-ch:
if pck.IsKeyFrame {
noVideo.Reset(10 * time.Second)
videoStart = true
}
if !videoStart {
continue
}
err = muxerWebRTC.WritePacket(*pck)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"call": "WritePacket",
}).Errorln(err.Error())
return
}
}
}
}()
}

@ -0,0 +1,38 @@
{
"server": {
"debug": true,
"http_debug": false,
"http_demo": true,
"http_dir": "web",
"http_login": "demo",
"http_password": "demo",
"http_port": ":8083",
"https": false,
"https_auto_tls": false,
"https_auto_tls_name": "",
"https_cert": "server.crt",
"https_key": "server.key",
"https_port": ":443",
"ice_servers": ["stun:stun.l.google.com:19302"],
"log_level": "debug",
"rtsp_port": ":5541",
"token": {
"backend": "http://127.0.0.1/test.php"
},
"defaults": {
"audio": true
}
},
"streams": {
"27aec28e-6181-4753-9acd-0456a75f0289": {
"channels": {
"0": {
"url": "rtmp://171.25.232.10/12d525bc9f014e209c1280bc0d46a87e",
"debug": false,
"audio": true
}
},
"name": "111111111"
}
}
}

@ -0,0 +1,470 @@
# RTSPtoWeb API
* [Streams](#streams)
* [List streams](#list-streams)
* [Add a stream](#add-a-stream)
* [Update a stream](#update-a-stream)
* [Reload a stream](#reload-a-stream)
* [Get stream info](#get-stream-info)
* [Delete a stream](#delete-a-stream)
* [Channels](#channels)
* [Add a channel to a stream](#add-a-channel-to-a-stream)
* [Update a stream channel](#update-a-stream-channel)
* [Reload a stream channel](#reload-a-stream-channel)
* [Get stream channel info](#get-stream-channel-info)
* [Get stream channel codec](#get-stream-channel-codec)
* [Delete a stream channel](#delete-a-stream-channel)
* [Video endpoints](#video-endpoints)
* [HLS](#hls)
* [HLS-LL](#hls-ll)
* [MSE](#mse)
* [WebRTC](#webrtc)
* [RTSP](#rtsp)
## Streams
### List streams
#### Request
`GET /streams`
```bash
curl http://demo:demo@127.0.0.1:8083/streams
```
#### Response
```json
{
"status": 1,
"payload": {
"demo1": {
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
}
}
},
"demo2": {
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
}
}
}
}
}
```
### Add a stream
#### Request
`POST /stream/{STREAM_ID}/add`
```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
}
}
}' \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/add
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
### Update a stream
#### Request
`POST /stream/{STREAM_ID}/edit`
```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
}
}
}' \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/edit
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
### Reload a stream
#### Request
`GET /stream/{STREAM_ID}/reload`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/reload
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
### Get stream info
#### Request
`GET /stream/{STREAM_ID}/info`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/info
```
#### Response
```json
{
"status": 1,
"payload": {
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
}
}
}
}
```
### Delete a stream
#### Request
`GET /stream/{STREAM_ID}/delete`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/delete
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
## Channels
### Add a channel to a stream
#### Request
`POST /stream/{STREAM_ID}/channel/{CHANNEL_ID}/add`
```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{
"name": "ch4",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": false,
"debug": false,
"status": 0
}' \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/add
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
### Update a stream channel
#### Request
`POST /stream/{STREAM_ID}/channel/{CHANNEL_ID}/edit`
```bash
curl \
--header "Content-Type: application/json" \
--request POST \
--data '{
"name": "ch4",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": true,
"debug": false,
"status": 0
}' \
http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/edit
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
### Reload a stream channel
#### Request
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/reload`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/reload
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
### Get stream channel info
#### Request
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/info`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/info
```
#### Response
```json
{
"status": 1,
"payload": {
"name": "ch4",
"url": "rtsp://admin:admin@{YOUR_CAMERA_IP}/uri",
"on_demand": false,
"debug": false,
"status": 1
}
}
```
### Get stream channel codec
#### Request
`GET /stream/{STREAM_ID}/{CHANNEL_ID}/codec`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/{CHANNEL_ID}/codec
```
#### Response
```json
{
"status": 1,
"payload": [
{
"Record": "AU0AFP/hABRnTQAUlahQfoQAAAMABAAAAwCiEAEABGjuPIA=",
"RecordInfo": {
"AVCProfileIndication": 77,
"ProfileCompatibility": 0,
"AVCLevelIndication": 20,
"LengthSizeMinusOne": 3,
"SPS": [
"Z00AFJWoUH6EAAADAAQAAAMAohA="
],
"PPS": [
"aO48gA=="
]
},
"SPSInfo": {
"ProfileIdc": 77,
"LevelIdc": 20,
"MbWidth": 20,
"MbHeight": 15,
"CropLeft": 0,
"CropRight": 0,
"CropTop": 0,
"CropBottom": 0,
"Width": 320,
"Height": 240
}
}
]
}
```
### Delete a stream channel
#### Request
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/delete`
```bash
curl http://demo:demo@127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/delete
```
#### Response
```json
{
"status": 1,
"payload": "success"
}
```
## Video endpoints
### HLS
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/hls/live/index.m3u8`
```bash
curl http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/hls/live/index.m3u8
```
```bash
ffplay http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/hls/live/index.m3u8
```
### HLS-LL
`GET /stream/{STREAM_ID}/channel/{CHANNEL_ID}/hlsll/live/index.m3u8`
```bash
curl http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/hlsll/live/index.m3u8
```
```bash
ffplay http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/hlsll/live/index.m3u8
```
### MSE
`/stream/{STREAM_ID}/channel/{CHANNEL_ID}/mse?uuid={STREAM_ID}&channel={CHANNEL_ID}`
```
ws://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/mse?uuid={STREAM_ID}&channel={CHANNEL_ID}
```
NOTE: Use `wss` for a secure connection.
### WebRTC
`/stream/{STREAM_ID}/channel/{CHANNEL_ID}/webrtc`
```
http://127.0.0.1:8083/stream/{STREAM_ID}/channel/{CHANNEL_ID}/webrtc
```
#### Request
The request is an HTTP `POST` with a FormData parameter `data` that is a base64 encoded SDP offer (e.g. `v=0...`) from a WebRTC client.
#### Response
The response is a base64 encoded SDP Answer.
### RTSP
`/{STREAM_ID}/{CHANNEL_ID}`
```
rtsp://127.0.0.1:{RTSP_PORT}/{STREAM_ID}/{CHANNEL_ID}
```
```bash
ffplay -rtsp_transport tcp rtsp://127.0.0.1/{STREAM_ID}/{CHANNEL_ID}
```

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RTSPtoWeb HLS example</title>
</head>
<body>
<h1>RTSPtoWeb HLS example</h1>
<input type="hidden" name="hls-url" id="hls-url"
value="http://localhost:8083/stream/demo/channel/0/hls/live/index.m3u8">
<video id="hls-video" autoplay muted playsinline controls
style="max-width: 100%; max-height: 100%;"></video>
<script>
document.addEventListener('DOMContentLoaded', function () {
const videoEl = document.querySelector('#hls-video')
const hlsUrl = document.querySelector('#hls-url').value
if (Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(hlsUrl)
hls.attachMedia(videoEl)
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
videoEl.src = hlsUrl
}
})
</script>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</body>
</html>

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RTSPtoWeb HLS-LL example</title>
</head>
<body>
<h1>RTSPtoWeb HLS-LL example</h1>
<input type="hidden" name="hlsll-url" id="hlsll-url"
value="http://localhost:8083/stream/demo/channel/0/hlsll/live/index.m3u8">
<video id="hlsll-video" autoplay muted playsinline controls
style="max-width: 100%; max-height: 100%;"></video>
<script>
document.addEventListener('DOMContentLoaded', function () {
const videoEl = document.querySelector('#hlsll-video')
const hlsllUrl = document.querySelector('#hlsll-url').value
if (Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(hlsllUrl)
hls.attachMedia(videoEl)
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
videoEl.src = hlsllUrl
}
})
</script>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</body>
</html>

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RTSPtoWeb MSE example</title>
</head>
<body>
<h1>RTSPtoWeb MSE example</h1>
<input type="hidden" name="mse-url" id="mse-url"
value="ws://localhost:8083/stream/demo/channel/0/mse?uuid=demo&channel=0">
<video id="mse-video" autoplay muted playsinline controls
style="max-width: 100%; max-height: 100%;"></video>
<script src="main.js"></script>
</body>
</html>

@ -0,0 +1,78 @@
document.addEventListener('DOMContentLoaded', function () {
const mseQueue = []
let mseSourceBuffer
let mseStreamingStarted = false
function startPlay (videoEl, url) {
const mse = new MediaSource()
videoEl.src = window.URL.createObjectURL(mse)
mse.addEventListener('sourceopen', function () {
const ws = new WebSocket(url)
ws.binaryType = 'arraybuffer'
ws.onopen = function (event) {
console.log('Connect to ws')
}
ws.onmessage = function (event) {
const data = new Uint8Array(event.data)
if (data[0] === 9) {
let mimeCodec
const decodedArr = data.slice(1)
if (window.TextDecoder) {
mimeCodec = new TextDecoder('utf-8').decode(decodedArr)
} else {
mimeCodec = Utf8ArrayToStr(decodedArr)
}
mseSourceBuffer = mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"')
mseSourceBuffer.mode = 'segments'
mseSourceBuffer.addEventListener('updateend', pushPacket)
} else {
readPacket(event.data)
}
}
}, false)
}
function pushPacket () {
const videoEl = document.querySelector('#mse-video')
let packet
if (!mseSourceBuffer.updating) {
if (mseQueue.length > 0) {
packet = mseQueue.shift()
mseSourceBuffer.appendBuffer(packet)
} else {
mseStreamingStarted = false
}
}
if (videoEl.buffered.length > 0) {
if (typeof document.hidden !== 'undefined' && document.hidden) {
// no sound, browser paused video without sound in background
videoEl.currentTime = videoEl.buffered.end((videoEl.buffered.length - 1)) - 0.5
}
}
}
function readPacket (packet) {
if (!mseStreamingStarted) {
mseSourceBuffer.appendBuffer(packet)
mseStreamingStarted = true
return
}
mseQueue.push(packet)
if (!mseSourceBuffer.updating) {
pushPacket()
}
}
const videoEl = document.querySelector('#mse-video')
const mseUrl = document.querySelector('#mse-url').value
// fix stalled video in safari
videoEl.addEventListener('pause', () => {
if (videoEl.currentTime > videoEl.buffered.end(videoEl.buffered.length - 1)) {
videoEl.currentTime = videoEl.buffered.end(videoEl.buffered.length - 1) - 0.1
videoEl.play()
}
})
startPlay(videoEl, mseUrl)
})

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RTSPtoWeb WebRTC example</title>
</head>
<body>
<h1>RTSPtoWeb WebRTC example</h1>
<input type="hidden" name="webrtc-url" id="webrtc-url"
value="http://localhost:8083/stream/demo/channel/0/webrtc">
<video id="webrtc-video" autoplay muted playsinline controls
style="max-width: 100%; max-height: 100%;"></video>
<script src="main.js"></script>
</body>
</html>

@ -0,0 +1,52 @@
document.addEventListener('DOMContentLoaded', function () {
function startPlay (videoEl, url) {
const webrtc = new RTCPeerConnection({
iceServers: [{
urls: ['stun:stun.l.google.com:19302']
}],
sdpSemantics: 'unified-plan'
})
webrtc.ontrack = function (event) {
console.log(event.streams.length + ' track is delivered')
videoEl.srcObject = event.streams[0]
videoEl.play()
}
webrtc.addTransceiver('video', { direction: 'sendrecv' })
webrtc.onnegotiationneeded = async function handleNegotiationNeeded () {
const offer = await webrtc.createOffer()
await webrtc.setLocalDescription(offer)
fetch(url, {
method: 'POST',
body: new URLSearchParams({ data: btoa(webrtc.localDescription.sdp) })
})
.then(response => response.text())
.then(data => {
try {
webrtc.setRemoteDescription(
new RTCSessionDescription({ type: 'answer', sdp: atob(data) })
)
} catch (e) {
console.warn(e)
}
})
}
const webrtcSendChannel = webrtc.createDataChannel('rtsptowebSendChannel')
webrtcSendChannel.onopen = (event) => {
console.log(`${webrtcSendChannel.label} has opened`)
webrtcSendChannel.send('ping')
}
webrtcSendChannel.onclose = (_event) => {
console.log(`${webrtcSendChannel.label} has closed`)
startPlay(videoEl, url)
}
webrtcSendChannel.onmessage = event => console.log(event.data)
}
const videoEl = document.querySelector('#webrtc-video')
const webrtcUrl = document.querySelector('#webrtc-url').value
startPlay(videoEl, webrtcUrl)
})

@ -0,0 +1,61 @@
module github.com/deepch/RTSPtoWeb
go 1.19
require (
github.com/deepch/vdk v0.0.20
github.com/gin-gonic/autotls v0.0.5
github.com/gin-gonic/gin v1.9.0
github.com/gobwas/ws v1.1.0
github.com/hashicorp/go-version v1.6.0
github.com/imdario/mergo v0.3.15
github.com/liip/sheriff v0.11.1
github.com/sirupsen/logrus v1.9.0
)
require (
github.com/bytedance/sonic v1.8.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/ice/v2 v2.3.1 // indirect
github.com/pion/interceptor v0.1.12 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.10 // indirect
github.com/pion/rtp v1.7.13 // indirect
github.com/pion/sctp v1.8.6 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect
github.com/pion/srtp/v2 v2.0.12 // indirect
github.com/pion/stun v0.4.0 // indirect
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/turn/v2 v2.1.0 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pion/webrtc/v3 v3.1.58 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/protobuf v1.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

261
go.sum

@ -0,0 +1,261 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.3 h1:pf6fGl5eqWYKkx1RcD4qpuX+BIUaduv/wTm5ekWJ80M=
github.com/bytedance/sonic v1.8.3/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepch/vdk v0.0.20 h1:GNQjfgapEEzaownzfRWYdQw+SqchW2yC1ha+/d0Bl24=
github.com/deepch/vdk v0.0.20/go.mod h1:774MjElr4PMgmXuTi9HXrK0nM0phMXE80W/g+vyTpAc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/autotls v0.0.5 h1:SXQWwWGDJHujDlthIij1+jxn3m5IPV8I9Za9bcPzMdo=
github.com/gin-gonic/autotls v0.0.5/go.mod h1:RK6LjOz47xARPGuceCOz3pQcYruxM0bVB7jb4AsDYeI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v0.0.0-20161031182605-e96d38404026/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/liip/sheriff v0.11.1 h1:52YGzskXFPSEnwfEtXnbPiMKKXJGm5IP45s8Ogw0Wyk=
github.com/liip/sheriff v0.11.1/go.mod h1:nVTQYHxfdIfOHnk5FREt4j6cnaSlJPUfXFVORfgGmTo=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

@ -0,0 +1,46 @@
package main
import (
"time"
"github.com/deepch/vdk/av"
)
//Fragment struct
type Fragment struct {
Independent bool //Fragment have i-frame (key frame)
Finish bool //Fragment Ready
Duration time.Duration //Fragment Duration
Packets []*av.Packet //Packet Slice
}
//NewFragment open new fragment
func (element *Segment) NewFragment() *Fragment {
res := &Fragment{}
element.Fragment[element.CurrentFragmentID] = res
return res
}
//GetDuration return fragment dur
func (element *Fragment) GetDuration() time.Duration {
return element.Duration
}
//WritePacket to fragment func
func (element *Fragment) WritePacket(packet *av.Packet) {
//increase fragment dur
element.Duration += packet.Duration
//Independent if have key
if packet.IsKeyFrame {
element.Independent = true
}
//append packet to slice of packet
element.Packets = append(element.Packets, packet)
}
//Close fragment block func
func (element *Fragment) Close() {
//TODO add callback func
//finalize fragment
element.Finish = true
}

@ -0,0 +1,236 @@
package main
import (
"context"
"math"
"sort"
"strconv"
"sync"
"time"
"github.com/deepch/vdk/av"
)
//MuxerHLS struct
type MuxerHLS struct {
mutex sync.RWMutex
UUID string //Current UUID
MSN int //Current MSN
FPS int //Current FPS
MediaSequence int //Current MediaSequence
CurrentFragmentID int //Current fragment id
CacheM3U8 string //Current index cache
CurrentSegment *Segment //Current segment link
Segments map[int]*Segment //Current segments group
FragmentCtx context.Context //chan 1-N
FragmentCancel context.CancelFunc //chan 1-N
}
//NewHLSMuxer Segments
func NewHLSMuxer(uuid string) *MuxerHLS {
ctx, cancel := context.WithCancel(context.Background())
return &MuxerHLS{
UUID: uuid,
MSN: -1,
Segments: make(map[int]*Segment),
FragmentCtx: ctx,
FragmentCancel: cancel,
}
}
//SetFPS func
func (element *MuxerHLS) SetFPS(fps int) {
element.FPS = fps
}
//WritePacket func
func (element *MuxerHLS) WritePacket(packet *av.Packet) {
element.mutex.Lock()
defer element.mutex.Unlock()
//TODO delete packet.IsKeyFrame if need no EXT-X-INDEPENDENT-SEGMENTS
if !packet.IsKeyFrame && element.CurrentSegment == nil {
// Wait for the first keyframe before initializing
return
}
if packet.IsKeyFrame && (element.CurrentSegment == nil || element.CurrentSegment.GetDuration().Seconds() >= 4) {
if element.CurrentSegment != nil {
element.CurrentSegment.Close()
if len(element.Segments) > 6 {
delete(element.Segments, element.MSN-6)
element.MediaSequence++
}
}
element.CurrentSegment = element.NewSegment()
element.CurrentSegment.SetFPS(element.FPS)
}
element.CurrentSegment.WritePacket(packet)
CurrentFragmentID := element.CurrentSegment.GetFragmentID()
if CurrentFragmentID != element.CurrentFragmentID {
element.UpdateIndexM3u8()
}
element.CurrentFragmentID = CurrentFragmentID
}
//UpdateIndexM3u8 func
func (element *MuxerHLS) UpdateIndexM3u8() {
var header string
var body string
var partTarget time.Duration
var segmentTarget time.Duration
segmentTarget = time.Second * 2
for _, segmentKey := range element.SortSegments(element.Segments) {
for _, fragmentKey := range element.SortFragment(element.Segments[segmentKey].Fragment) {
if element.Segments[segmentKey].Fragment[fragmentKey].Finish {
var independent string
if element.Segments[segmentKey].Fragment[fragmentKey].Independent {
independent = ",INDEPENDENT=YES"
}
body += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(element.Segments[segmentKey].Fragment[fragmentKey].GetDuration().Seconds(), 'f', 5, 64) + "" + independent + ",URI=\"fragment/" + strconv.Itoa(segmentKey) + "/" + strconv.Itoa(fragmentKey) + "/0qrm9ru6." + strconv.Itoa(fragmentKey) + ".m4s\"\n"
partTarget = element.Segments[segmentKey].Fragment[fragmentKey].Duration
} else {
body += "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fragment/" + strconv.Itoa(segmentKey) + "/" + strconv.Itoa(fragmentKey) + "/0qrm9ru6." + strconv.Itoa(fragmentKey) + ".m4s\"\n"
}
}
if element.Segments[segmentKey].Finish {
segmentTarget = element.Segments[segmentKey].Duration
body += "#EXT-X-PROGRAM-DATE-TIME:" + element.Segments[segmentKey].Time.Format("2006-01-02T15:04:05.000000Z") + "\n#EXTINF:" + strconv.FormatFloat(element.Segments[segmentKey].Duration.Seconds(), 'f', 5, 64) + ",\n"
body += "segment/" + strconv.Itoa(segmentKey) + "/" + element.UUID + "." + strconv.Itoa(segmentKey) + ".m4s\n"
}
}
header += "#EXTM3U\n"
header += "#EXT-X-TARGETDURATION:" + strconv.Itoa(int(math.Round(segmentTarget.Seconds()))) + "\n"
header += "#EXT-X-VERSION:7\n"
header += "#EXT-X-INDEPENDENT-SEGMENTS\n"
header += "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=" + strconv.FormatFloat(partTarget.Seconds()*4, 'f', 5, 64) + ",HOLD-BACK=" + strconv.FormatFloat(segmentTarget.Seconds()*4, 'f', 5, 64) + "\n"
header += "#EXT-X-MAP:URI=\"init.mp4\"\n"
header += "#EXT-X-PART-INF:PART-TARGET=" + strconv.FormatFloat(partTarget.Seconds(), 'f', 5, 64) + "\n"
header += "#EXT-X-MEDIA-SEQUENCE:" + strconv.Itoa(element.MediaSequence) + "\n"
header += body
element.CacheM3U8 = header
element.PlaylistUpdate()
}
//PlaylistUpdate func
func (element *MuxerHLS) PlaylistUpdate() {
element.FragmentCancel()
element.FragmentCtx, element.FragmentCancel = context.WithCancel(context.Background())
}
//GetSegment func
func (element *MuxerHLS) GetSegment(segment int) ([]*av.Packet, error) {
element.mutex.Lock()
defer element.mutex.Unlock()
if segmentTmp, ok := element.Segments[segment]; ok && len(segmentTmp.Fragment) > 0 {
var res []*av.Packet
for _, v := range element.SortFragment(segmentTmp.Fragment) {
res = append(res, segmentTmp.Fragment[v].Packets...)
}
return res, nil
}
return nil, ErrorStreamNotFound
}
//GetFragment func
func (element *MuxerHLS) GetFragment(segment int, fragment int) ([]*av.Packet, error) {
element.mutex.Lock()
if segmentTmp, segmentTmpOK := element.Segments[segment]; segmentTmpOK {
if fragmentTmp, fragmentTmpOK := segmentTmp.Fragment[fragment]; fragmentTmpOK {
if fragmentTmp.Finish {
element.mutex.Unlock()
return fragmentTmp.Packets, nil
} else {
element.mutex.Unlock()
pck, err := element.WaitFragment(time.Second*1, segment, fragment)
if err != nil {
return nil, err
}
return pck, err
}
}
}
element.mutex.Unlock()
return nil, ErrorStreamNotFound
}
//GetIndexM3u8 func
func (element *MuxerHLS) GetIndexM3u8(needMSN int, needPart int) (string, error) {
element.mutex.Lock()
if len(element.CacheM3U8) != 0 && ((needMSN == -1 || needPart == -1) || (needMSN-element.MSN > 1) || (needMSN == element.MSN && needPart < element.CurrentFragmentID)) {
element.mutex.Unlock()
return element.CacheM3U8, nil
} else {
element.mutex.Unlock()
index, err := element.WaitIndex(time.Second*3, needMSN, needPart)
if err != nil {
return "", err
}
return index, err
}
}
//WaitFragment func
func (element *MuxerHLS) WaitFragment(timeOut time.Duration, segment, fragment int) ([]*av.Packet, error) {
select {
case <-time.After(timeOut):
return nil, ErrorStreamNotFound
case <-element.FragmentCtx.Done():
element.mutex.Lock()
defer element.mutex.Unlock()
if segmentTmp, segmentTmpOK := element.Segments[segment]; segmentTmpOK {
if fragmentTmp, fragmentTmpOK := segmentTmp.Fragment[fragment]; fragmentTmpOK {
if fragmentTmp.Finish {
return fragmentTmp.Packets, nil
}
}
}
return nil, ErrorStreamNotFound
}
}
//WaitIndex func
func (element *MuxerHLS) WaitIndex(timeOut time.Duration, segment, fragment int) (string, error) {
for {
select {
case <-time.After(timeOut):
return "", ErrorStreamNotFound
case <-element.FragmentCtx.Done():
element.mutex.Lock()
if element.MSN < segment || (element.MSN == segment && element.CurrentFragmentID < fragment) {
log.Println("wait req", element.MSN, element.CurrentFragmentID, segment, fragment)
element.mutex.Unlock()
continue
}
element.mutex.Unlock()
return element.CacheM3U8, nil
}
}
}
//SortFragment func
func (element *MuxerHLS) SortFragment(val map[int]*Fragment) []int {
keys := make([]int, len(val))
i := 0
for k := range val {
keys[i] = k
i++
}
sort.Ints(keys)
return keys
}
//SortSegments fuc
func (element *MuxerHLS) SortSegments(val map[int]*Segment) []int {
keys := make([]int, len(val))
i := 0
for k := range val {
keys[i] = k
i++
}
sort.Ints(keys)
return keys
}
func (element *MuxerHLS) Close() {
}

@ -0,0 +1,76 @@
package main
import (
"time"
"github.com/deepch/vdk/av"
)
//Segment struct
type Segment struct {
FPS int //Current fps
CurrentFragment *Fragment //CurrentFragment link
CurrentFragmentID int //CurrentFragment ID
Finish bool //Segment Ready
Duration time.Duration //Segment Duration
Time time.Time //Realtime EXT-X-PROGRAM-DATE-TIME
Fragment map[int]*Fragment //Fragment map
}
//NewSegment func
func (element *MuxerHLS) NewSegment() *Segment {
res := &Segment{
Fragment: make(map[int]*Fragment),
CurrentFragmentID: -1, //Default fragment -1
}
//Increase MSN
element.MSN++
element.Segments[element.MSN] = res
return res
}
//GetDuration func
func (element *Segment) GetDuration() time.Duration {
return element.Duration
}
//SetFPS func
func (element *Segment) SetFPS(fps int) {
element.FPS = fps
}
//WritePacket func
func (element *Segment) WritePacket(packet *av.Packet) {
if element.CurrentFragment == nil || element.CurrentFragment.GetDuration().Milliseconds() >= element.FragmentMS(element.FPS) {
if element.CurrentFragment != nil {
element.CurrentFragment.Close()
}
element.CurrentFragmentID++
element.CurrentFragment = element.NewFragment()
}
element.Duration += packet.Duration
element.CurrentFragment.WritePacket(packet)
}
//GetFragmentID func
func (element *Segment) GetFragmentID() int {
return element.CurrentFragmentID
}
//Close segment func
func (element *Segment) Close() {
element.Finish = true
if element.CurrentFragment != nil {
element.CurrentFragment.Close()
}
}
//FragmentMS func
func (element *Segment) FragmentMS(fps int) int64 {
for i := 6; i >= 1; i-- {
if fps%i == 0 {
return int64(float64(1000) / float64(fps) * float64(i))
}
}
return 100
}

@ -0,0 +1,20 @@
package main
import (
"io/ioutil"
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func init() {
//TODO: next add write to file
if !debug {
log.SetOutput(ioutil.Discard)
}
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
log.SetLevel(Storage.ServerLogLevel())
}

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"dependencyDashboard": true,
"dependencyDashboardTitle": "Renovate Dashboard",
"packageRules": [
{
"description": "Minor updates are automatic",
"automerge": true,
"automergeType": "branch",
"matchUpdateTypes": ["minor", "patch"]
}
]
}

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICGjCCAZ8CCQCLGA2sAHSd1DAKBggqhkjOPQQDAjB2MQswCQYDVQQGEwJERTEN
MAsGA1UECAwEREVNTzENMAsGA1UEBwwEREVNTzENMAsGA1UECgwEREVNTzENMAsG
A1UECwwEREVNTzENMAsGA1UEAwwEREVNTzEcMBoGCSqGSIb3DQEJARYNZGVtb0Bk
ZW1vLmNvbTAeFw0yMTAzMDUxMzIyNDhaFw0zMTAzMDMxMzIyNDhaMHYxCzAJBgNV
BAYTAkRFMQ0wCwYDVQQIDARERU1PMQ0wCwYDVQQHDARERU1PMQ0wCwYDVQQKDARE
RU1PMQ0wCwYDVQQLDARERU1PMQ0wCwYDVQQDDARERU1PMRwwGgYJKoZIhvcNAQkB
Fg1kZW1vQGRlbW8uY29tMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEpQ5/6akj0JiG
CE0sAnWzcecbcgXY74KgG4X4+0Z8YF7B6LwI57yIQD/9LiUvPwHl7rlT9B+s31Ui
ZQ6U0Y5ChES0jeISmLhkAUkGHM8wjPUGRa23FgEgalw/I3+KPMRnMAoGCCqGSM49
BAMCA2kAMGYCMQCLIugTO5xINyl32k1F3edgxun6NLhu/k+c+lvBi8EMcq8aERVC
kPU1hWhF7BD0JfkCMQDS5FzusPPfK7maBF11XXuwBFJ3Zke96mSmpohuTBxT7yfW
TIPP+Rk2MzxMKh/RLHw=
-----END CERTIFICATE-----

@ -0,0 +1,9 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDDBqBCdv9p3NihNJi3lrVfu700/pXm+tZcm22axGDZZXoWTt5c9k6W7
PzK/0TgZwsmgBwYFK4EEACKhZANiAASlDn/pqSPQmIYITSwCdbNx5xtyBdjvgqAb
hfj7RnxgXsHovAjnvIhAP/0uJS8/AeXuuVP0H6zfVSJlDpTRjkKERLSN4hKYuGQB
SQYczzCM9QZFrbcWASBqXD8jf4o8xGc=
-----END EC PRIVATE KEY-----

@ -0,0 +1,473 @@
package main
import (
"bytes"
"errors"
"fmt"
"net"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
)
var (
Version = "RTSP/1.0"
UserAgent = "Lavf58.29.100"
Session = "000a959d6816"
)
var (
OPTIONS = "OPTIONS"
DESCRIBE = "DESCRIBE"
SETUP = "SETUP"
PLAY = "PLAY"
TEARDOWN = "TEARDOWN"
)
// RTSP response status codes
const (
StatusContinue = 100
StatusOK = 200
StatusCreated = 201
StatusLowOnStorageSpace = 250
StatusMultipleChoices = 300
StatusMovedPermanently = 301
StatusMovedTemporarily = 302
StatusSeeOther = 303
StatusNotModified = 304
StatusUseProxy = 305
StatusBadRequest = 400
StatusUnauthorized = 401
StatusPaymentRequired = 402
StatusForbidden = 403
StatusNotFound = 404
StatusMethodNotAllowed = 405
StatusNotAcceptable = 406
StatusProxyAuthenticationRequired = 407
StatusRequestTimeout = 408
StatusGone = 410
StatusLengthRequired = 411
StatusPreconditionFailed = 412
StatusRequestEntityTooLarge = 413
StatusRequestURITooLong = 414
StatusUnsupportedMediaType = 415
StatusInvalidparameter = 451
StatusIllegalConferenceIdentifier = 452
StatusNotEnoughBandwidth = 453
StatusSessionNotFound = 454
StatusMethodNotValidInThisState = 455
StatusHeaderFieldNotValid = 456
StatusInvalidRange = 457
StatusParameterIsReadOnly = 458
StatusAggregateOperationNotAllowed = 459
StatusOnlyAggregateOperationAllowed = 460
StatusUnsupportedTransport = 461
StatusDestinationUnreachable = 462
StatusInternalServerError = 500
StatusNotImplemented = 501
StatusBadGateway = 502
StatusServiceUnavailable = 503
StatusGatewayTimeout = 504
StatusRTSPVersionNotSupported = 505
StatusOptionNotsupport = 551
)
func StatusText(code int) string {
return statusText[code]
}
var statusText = map[int]string{
StatusContinue: "Continue",
StatusOK: "OK",
StatusCreated: "Created",
StatusLowOnStorageSpace: "Low on Storage Space",
StatusMultipleChoices: "Multiple Choices",
StatusMovedPermanently: "Moved Permanently",
StatusMovedTemporarily: "Moved Temporarily",
StatusSeeOther: "See Other",
StatusNotModified: "Not Modified",
StatusUseProxy: "Use Proxy",
StatusBadRequest: "Bad Request",
StatusUnauthorized: "Unauthorized",
StatusPaymentRequired: "Payment Required",
StatusForbidden: "Forbidden",
StatusNotFound: "Not Found",
StatusMethodNotAllowed: "Method Not Allowed",
StatusNotAcceptable: "Not Acceptable",
StatusProxyAuthenticationRequired: "Proxy Authentication Required",
StatusRequestTimeout: "Request Time-out",
StatusGone: "Gone",
StatusLengthRequired: "Length Required",
StatusPreconditionFailed: "Precondition Failed",
StatusRequestEntityTooLarge: "Request Entity Too Large",
StatusRequestURITooLong: "Request-URI Too Large",
StatusUnsupportedMediaType: "Unsupported Media Type",
StatusInvalidparameter: "Parameter Not Understood",
StatusIllegalConferenceIdentifier: "Conference Not Found",
StatusNotEnoughBandwidth: "Not Enough Bandwidth",
StatusSessionNotFound: "Session Not Found",
StatusMethodNotValidInThisState: "Method Not Valid in This State",
StatusHeaderFieldNotValid: "Header Field Not Valid for Resource",
StatusInvalidRange: "Invalid Range",
StatusParameterIsReadOnly: "Parameter Is Read-Only",
StatusAggregateOperationNotAllowed: "Aggregate operation not allowed",
StatusOnlyAggregateOperationAllowed: "Only aggregate operation allowed",
StatusUnsupportedTransport: "Unsupported transport",
StatusDestinationUnreachable: "Destination unreachable",
StatusInternalServerError: "Internal Server Error",
StatusNotImplemented: "Not Implemented",
StatusBadGateway: "Bad Gateway",
StatusServiceUnavailable: "Service Unavailable",
StatusGatewayTimeout: "Gateway Time-out",
StatusRTSPVersionNotSupported: "RTSP Version not supported",
StatusOptionNotsupport: "Option not supported",
}
//RTSPServer func
func RTSPServer() {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"func": "RTSPServer",
"call": "Start",
}).Infoln("Server RTSP start")
l, err := net.Listen("tcp", Storage.ServerRTSPPort())
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"func": "RTSPServer",
"call": "Listen",
}).Errorln(err)
os.Exit(1)
}
defer func() {
err := l.Close()
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"func": "RTSPServer",
"call": "Close",
}).Errorln(err)
}
}()
for {
conn, err := l.Accept()
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"func": "RTSPServer",
"call": "Accept",
}).Errorln(err)
os.Exit(1)
}
go RTSPServerClientHandle(conn)
}
}
//RTSPServerClientHandle func
func RTSPServerClientHandle(conn net.Conn) {
buf := make([]byte, 4096)
token, uuid, channel, in, cSEQ := "", "", "0", 0, 0
var playStarted bool
defer func() {
err := conn.Close()
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "Close",
}).Errorln(err.Error())
}
}()
err := conn.SetDeadline(time.Now().Add(10 * time.Second))
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "SetDeadline",
}).Errorln(err.Error())
return
}
for {
n, err := conn.Read(buf)
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "Read",
}).Errorln(err.Error())
return
}
cSEQ = parsecSEQ(buf[:n])
stage, err := parseStage(buf[:n])
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "parseStage",
}).Errorln(err.Error())
}
err = conn.SetDeadline(time.Now().Add(60 * time.Second))
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "Request",
}).Debugln(string(buf[:n]))
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "SetDeadline",
}).Errorln(err.Error())
return
}
switch stage {
case OPTIONS:
if playStarted {
err = RTSPServerClientResponse(uuid, channel, conn, 200, map[string]string{"CSeq": strconv.Itoa(cSEQ), "Public": "DESCRIBE, SETUP, TEARDOWN, PLAY"})
if err != nil {
return
}
continue
}
uuid, channel, token, err = parseStreamChannel(buf[:n])
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "parseStreamChannel",
}).Errorln(err.Error())
return
}
if !Storage.StreamChannelExist(uuid, channel) {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "StreamChannelExist",
}).Errorln(ErrorStreamNotFound.Error())
err = RTSPServerClientResponse(uuid, channel, conn, 404, map[string]string{"CSeq": strconv.Itoa(cSEQ)})
if err != nil {
return
}
return
}
if !RemoteAuthorization("RTSP", uuid, channel, token, conn.RemoteAddr().String()) {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "StreamChannelExist",
}).Errorln(ErrorStreamUnauthorized.Error())
err = RTSPServerClientResponse(uuid, channel, conn, 401, map[string]string{"CSeq": strconv.Itoa(cSEQ)})
if err != nil {
return
}
return
}
Storage.StreamChannelRun(uuid, channel)
err = RTSPServerClientResponse(uuid, channel, conn, 200, map[string]string{"CSeq": strconv.Itoa(cSEQ), "Public": "DESCRIBE, SETUP, TEARDOWN, PLAY"})
if err != nil {
return
}
case SETUP:
if !strings.Contains(string(buf[:n]), "interleaved") {
err = RTSPServerClientResponse(uuid, channel, conn, 461, map[string]string{"CSeq": strconv.Itoa(cSEQ)})
if err != nil {
return
}
continue
}
err = RTSPServerClientResponse(uuid, channel, conn, 200, map[string]string{"CSeq": strconv.Itoa(cSEQ), "User-Agent:": UserAgent, "Session": Session, "Transport": "RTP/AVP/TCP;unicast;interleaved=" + strconv.Itoa(in) + "-" + strconv.Itoa(in+1)})
if err != nil {
return
}
in = in + 2
case DESCRIBE:
sdp, err := Storage.StreamChannelSDP(uuid, channel)
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "StreamSDP",
}).Errorln(err.Error())
return
}
err = RTSPServerClientResponse(uuid, channel, conn, 200, map[string]string{"CSeq": strconv.Itoa(cSEQ), "User-Agent:": UserAgent, "Session": Session, "Content-Type": "application/sdp\r\nContent-Length: " + strconv.Itoa(len(sdp)), "sdp": string(sdp)})
if err != nil {
return
}
case PLAY:
err = RTSPServerClientResponse(uuid, channel, conn, 200, map[string]string{"CSeq": strconv.Itoa(cSEQ), "User-Agent:": UserAgent, "Session": Session})
if err != nil {
return
}
playStarted = true
go RTSPServerClientPlay(uuid, channel, conn)
case TEARDOWN:
err = RTSPServerClientResponse(uuid, channel, conn, 200, map[string]string{"CSeq": strconv.Itoa(cSEQ), "User-Agent:": UserAgent, "Session": Session})
if err != nil {
return
}
return
default:
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "Stage",
}).Debugln("stage bad", stage)
}
}
}
//handleRTSPServerPlay func
func RTSPServerClientPlay(uuid string, channel string, conn net.Conn) {
cid, _, ch, err := Storage.ClientAdd(uuid, channel, RTSP)
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "ClientAdd",
}).Errorln(err.Error())
return
}
defer func() {
Storage.ClientDelete(uuid, cid, channel)
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "ClientDelete",
}).Infoln("Client offline")
err := conn.Close()
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "Close",
}).Errorln(err.Error())
}
}()
noVideo := time.NewTimer(10 * time.Second)
for {
select {
case <-noVideo.C:
return
case pck := <-ch:
noVideo.Reset(10 * time.Second)
_, err := conn.Write(*pck)
if err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "handleRTSPServerRequest",
"call": "Write",
}).Errorln(err.Error())
return
}
}
}
}
//handleRTSPServerPlay func
func RTSPServerClientResponse(uuid string, channel string, conn net.Conn, status int, headers map[string]string) error {
var sdp string
builder := bytes.Buffer{}
builder.WriteString(fmt.Sprintf(Version+" %d %s\r\n", status, StatusText(status)))
for k, v := range headers {
if k == "sdp" {
sdp = v
continue
}
builder.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
builder.WriteString(fmt.Sprintf("\r\n"))
builder.WriteString(sdp)
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "RTSPServerClientResponse",
"call": "Response",
}).Debugln(builder.String())
if _, err := conn.Write(builder.Bytes()); err != nil {
log.WithFields(logrus.Fields{
"module": "rtsp_server",
"stream": uuid,
"channel": channel,
"func": "RTSPServerClientResponse",
"call": "Write",
}).Errorln(err.Error())
return err
}
return nil
}
//parsecSEQ func
func parsecSEQ(buf []byte) int {
return stringToInt(stringInBetween(string(buf), "CSeq: ", "\r\n"))
}
//parseStage func
func parseStage(buf []byte) (string, error) {
st := strings.Split(string(buf), " ")
if len(st) > 0 {
return st[0], nil
}
return "", errors.New("parse stage error " + string(buf))
}
//parseStreamChannel func
func parseStreamChannel(buf []byte) (string, string, string, error) {
var token string
uri := stringInBetween(string(buf), " ", " ")
u, err := url.Parse(uri)
if err == nil {
token = u.Query().Get("token")
uri = u.Path
}
st := strings.Split(uri, "/")
if len(st) >= 3 {
return st[1], st[2], token, nil
}
return "", "0", token, errors.New("parse stream error " + string(buf))
}

@ -0,0 +1,62 @@
package main
import (
"time"
"github.com/deepch/vdk/av"
)
//ClientAdd Add New Client to Translations
func (obj *StorageST) ClientAdd(streamID string, channelID string, mode int) (string, chan *av.Packet, chan *[]byte, error) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
streamTmp, ok := obj.Streams[streamID]
if !ok {
return "", nil, nil, ErrorStreamNotFound
}
//Generate UUID client
cid, err := generateUUID()
if err != nil {
return "", nil, nil, err
}
chAV := make(chan *av.Packet, 2000)
chRTP := make(chan *[]byte, 2000)
channelTmp, ok := streamTmp.Channels[channelID]
if !ok {
return "", nil, nil, ErrorStreamNotFound
}
channelTmp.clients[cid] = ClientST{mode: mode, outgoingAVPacket: chAV, outgoingRTPPacket: chRTP, signals: make(chan int, 100)}
channelTmp.ack = time.Now()
streamTmp.Channels[channelID] = channelTmp
obj.Streams[streamID] = streamTmp
return cid, chAV, chRTP, nil
}
//ClientDelete Delete Client
func (obj *StorageST) ClientDelete(streamID string, cid string, channelID string) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if _, ok := obj.Streams[streamID]; ok {
delete(obj.Streams[streamID].Channels[channelID].clients, cid)
}
}
//ClientHas check is client ext
func (obj *StorageST) ClientHas(streamID string, channelID string) bool {
obj.mutex.Lock()
defer obj.mutex.Unlock()
streamTmp, ok := obj.Streams[streamID]
if !ok {
return false
}
channelTmp, ok := streamTmp.Channels[channelID]
if !ok {
return false
}
if time.Now().Sub(channelTmp.ack).Seconds() > 30 {
return false
}
return true
}

@ -0,0 +1,103 @@
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"os"
"time"
"github.com/hashicorp/go-version"
"github.com/imdario/mergo"
"github.com/liip/sheriff"
"github.com/sirupsen/logrus"
)
// Command line flag global variables
var debug bool
var configFile string
//NewStreamCore do load config file
func NewStreamCore() *StorageST {
flag.BoolVar(&debug, "debug", true, "set debug mode")
flag.StringVar(&configFile, "config", "config.json", "config patch (/etc/server/config.json or config.json)")
flag.Parse()
var tmp StorageST
data, err := ioutil.ReadFile(configFile)
if err != nil {
log.WithFields(logrus.Fields{
"module": "config",
"func": "NewStreamCore",
"call": "ReadFile",
}).Errorln(err.Error())
os.Exit(1)
}
err = json.Unmarshal(data, &tmp)
if err != nil {
log.WithFields(logrus.Fields{
"module": "config",
"func": "NewStreamCore",
"call": "Unmarshal",
}).Errorln(err.Error())
os.Exit(1)
}
debug = tmp.Server.Debug
for i, i2 := range tmp.Streams {
for i3, i4 := range i2.Channels {
channel := tmp.ChannelDefaults
err = mergo.Merge(&channel, i4)
if err != nil {
log.WithFields(logrus.Fields{
"module": "config",
"func": "NewStreamCore",
"call": "Merge",
}).Errorln(err.Error())
os.Exit(1)
}
channel.clients = make(map[string]ClientST)
channel.ack = time.Now().Add(-255 * time.Hour)
channel.hlsSegmentBuffer = make(map[int]SegmentOld)
channel.signals = make(chan int, 100)
i2.Channels[i3] = channel
}
tmp.Streams[i] = i2
}
return &tmp
}
//ClientDelete Delete Client
func (obj *StorageST) SaveConfig() error {
log.WithFields(logrus.Fields{
"module": "config",
"func": "NewStreamCore",
}).Debugln("Saving configuration to", configFile)
v2, err := version.NewVersion("2.0.0")
if err != nil {
return err
}
data, err := sheriff.Marshal(&sheriff.Options{
Groups: []string{"config"},
ApiVersion: v2,
}, obj)
if err != nil {
return err
}
res, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(configFile, res, 0644)
if err != nil {
log.WithFields(logrus.Fields{
"module": "config",
"func": "SaveConfig",
"call": "WriteFile",
}).Errorln(err.Error())
return err
}
return nil
}

@ -0,0 +1,162 @@
package main
import (
"path/filepath"
"github.com/sirupsen/logrus"
)
var (
//Default www static file dir
DefaultHTTPDir = "web"
)
//ServerHTTPDir
func (obj *StorageST) ServerHTTPDir() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if filepath.Clean(obj.Server.HTTPDir) == "." {
return DefaultHTTPDir
}
return filepath.Clean(obj.Server.HTTPDir)
}
//ServerHTTPDebug read debug options
func (obj *StorageST) ServerHTTPDebug() bool {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPDebug
}
//ServerLogLevel read debug options
func (obj *StorageST) ServerLogLevel() logrus.Level {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.LogLevel
}
//ServerHTTPDemo read demo options
func (obj *StorageST) ServerHTTPDemo() bool {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPDemo
}
//ServerHTTPLogin read Login options
func (obj *StorageST) ServerHTTPLogin() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPLogin
}
//ServerHTTPPassword read Password options
func (obj *StorageST) ServerHTTPPassword() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPPassword
}
//ServerHTTPPort read HTTP Port options
func (obj *StorageST) ServerHTTPPort() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPPort
}
//ServerRTSPPort read HTTP Port options
func (obj *StorageST) ServerRTSPPort() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.RTSPPort
}
//ServerHTTPS read HTTPS Port options
func (obj *StorageST) ServerHTTPS() bool {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPS
}
//ServerHTTPSPort read HTTPS Port options
func (obj *StorageST) ServerHTTPSPort() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPSPort
}
//ServerHTTPSAutoTLSEnable read HTTPS Port options
func (obj *StorageST) ServerHTTPSAutoTLSEnable() bool {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPSAutoTLSEnable
}
//ServerHTTPSAutoTLSName read HTTPS Port options
func (obj *StorageST) ServerHTTPSAutoTLSName() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPSAutoTLSName
}
//ServerHTTPSCert read HTTPS Cert options
func (obj *StorageST) ServerHTTPSCert() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPSCert
}
//ServerHTTPSKey read HTTPS Key options
func (obj *StorageST) ServerHTTPSKey() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.HTTPSKey
}
// ServerICEServers read ICE servers
func (obj *StorageST) ServerICEServers() []string {
obj.mutex.Lock()
defer obj.mutex.Unlock()
return obj.Server.ICEServers
}
// ServerICEServers read ICE username
func (obj *StorageST) ServerICEUsername() string {
obj.mutex.Lock()
defer obj.mutex.Unlock()
return obj.Server.ICEUsername
}
// ServerICEServers read ICE credential
func (obj *StorageST) ServerICECredential() string {
obj.mutex.Lock()
defer obj.mutex.Unlock()
return obj.Server.ICECredential
}
//ServerTokenEnable read HTTPS Key options
func (obj *StorageST) ServerTokenEnable() bool {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.Token.Enable
}
//ServerTokenBackend read HTTPS Key options
func (obj *StorageST) ServerTokenBackend() string {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
return obj.Server.Token.Backend
}
// ServerWebRTCPortMin read WebRTC Port Min
func (obj *StorageST) ServerWebRTCPortMin() uint16 {
obj.mutex.Lock()
defer obj.mutex.Unlock()
return obj.Server.WebRTCPortMin
}
// ServerWebRTCPortMax read WebRTC Port Max
func (obj *StorageST) ServerWebRTCPortMax() uint16 {
obj.mutex.Lock()
defer obj.mutex.Unlock()
return obj.Server.WebRTCPortMax
}

@ -0,0 +1,140 @@
package main
import "github.com/liip/sheriff"
//MarshalledStreamsList lists all streams and includes only fields which are safe to serialize.
func (obj *StorageST) MarshalledStreamsList() (interface{}, error) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
val, err := sheriff.Marshal(&sheriff.Options{
Groups: []string{"api"},
}, obj.Streams)
if err != nil {
return nil, err
}
return val, nil
}
//StreamAdd add stream
func (obj *StorageST) StreamAdd(uuid string, val StreamST) error {
obj.mutex.Lock()
defer obj.mutex.Unlock()
//TODO create empty map bug save https://github.com/liip/sheriff empty not nil map[] != {} json
//data, err := sheriff.Marshal(&sheriff.Options{
// Groups: []string{"config"},
// ApiVersion: v2,
// }, obj)
//Not Work map[] != {}
if obj.Streams == nil {
obj.Streams = make(map[string]StreamST)
}
if _, ok := obj.Streams[uuid]; ok {
return ErrorStreamAlreadyExists
}
for i, i2 := range val.Channels {
i2 = obj.StreamChannelMake(i2)
if !i2.OnDemand {
i2.runLock = true
val.Channels[i] = i2
go StreamServerRunStreamDo(uuid, i)
} else {
val.Channels[i] = i2
}
}
obj.Streams[uuid] = val
err := obj.SaveConfig()
if err != nil {
return err
}
return nil
}
//StreamEdit edit stream
func (obj *StorageST) StreamEdit(uuid string, val StreamST) error {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
for i, i2 := range tmp.Channels {
if i2.runLock {
tmp.Channels[i] = i2
obj.Streams[uuid] = tmp
i2.signals <- SignalStreamStop
}
}
for i3, i4 := range val.Channels {
i4 = obj.StreamChannelMake(i4)
if !i4.OnDemand {
i4.runLock = true
val.Channels[i3] = i4
go StreamServerRunStreamDo(uuid, i3)
} else {
val.Channels[i3] = i4
}
}
obj.Streams[uuid] = val
err := obj.SaveConfig()
if err != nil {
return err
}
return nil
}
return ErrorStreamNotFound
}
//StreamReload reload stream
func (obj *StorageST) StopAll() {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
for _, st := range obj.Streams {
for _, i2 := range st.Channels {
if i2.runLock {
i2.signals <- SignalStreamStop
}
}
}
}
//StreamReload reload stream
func (obj *StorageST) StreamReload(uuid string) error {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
for _, i2 := range tmp.Channels {
if i2.runLock {
i2.signals <- SignalStreamRestart
}
}
return nil
}
return ErrorStreamNotFound
}
//StreamDelete stream
func (obj *StorageST) StreamDelete(uuid string) error {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
for _, i2 := range tmp.Channels {
if i2.runLock {
i2.signals <- SignalStreamStop
}
}
delete(obj.Streams, uuid)
err := obj.SaveConfig()
if err != nil {
return err
}
return nil
}
return ErrorStreamNotFound
}
//StreamInfo return stream info
func (obj *StorageST) StreamInfo(uuid string) (*StreamST, error) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
return &tmp, nil
}
return nil, ErrorStreamNotFound
}

@ -0,0 +1,410 @@
package main
import (
"time"
"github.com/deepch/vdk/av"
"github.com/imdario/mergo"
"github.com/sirupsen/logrus"
)
// StreamChannelMake check stream exist
func (obj *StorageST) StreamChannelMake(val ChannelST) ChannelST {
channel := obj.ChannelDefaults
if err := mergo.Merge(&channel, val); err != nil {
// Just ignore the default values and continue
channel = val
log.WithFields(logrus.Fields{
"module": "storage",
"func": "StreamChannelMake",
"call": "mergo.Merge",
}).Errorln(err.Error())
}
//make client's
channel.clients = make(map[string]ClientST)
//make last ack
channel.ack = time.Now().Add(-255 * time.Hour)
//make hls buffer
channel.hlsSegmentBuffer = make(map[int]SegmentOld)
//make signals buffer chain
channel.signals = make(chan int, 100)
return channel
}
// StreamChannelRunAll run all stream go
func (obj *StorageST) StreamChannelRunAll() {
obj.mutex.Lock()
defer obj.mutex.Unlock()
for k, v := range obj.Streams {
for ks, vs := range v.Channels {
if !vs.OnDemand {
vs.runLock = true
go StreamServerRunStreamDo(k, ks)
v.Channels[ks] = vs
obj.Streams[k] = v
}
}
}
}
// StreamChannelRun one stream and lock
func (obj *StorageST) StreamChannelRun(streamID string, channelID string) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if streamTmp, ok := obj.Streams[streamID]; ok {
if channelTmp, ok := streamTmp.Channels[channelID]; ok {
if !channelTmp.runLock {
channelTmp.runLock = true
streamTmp.Channels[channelID] = channelTmp
obj.Streams[streamID] = streamTmp
go StreamServerRunStreamDo(streamID, channelID)
}
}
}
}
// StreamChannelUnlock unlock status to no lock
func (obj *StorageST) StreamChannelUnlock(streamID string, channelID string) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if streamTmp, ok := obj.Streams[streamID]; ok {
if channelTmp, ok := streamTmp.Channels[channelID]; ok {
channelTmp.runLock = false
streamTmp.Channels[channelID] = channelTmp
obj.Streams[streamID] = streamTmp
}
}
}
// StreamChannelControl get stream
func (obj *StorageST) StreamChannelControl(key string, channelID string) (*ChannelST, error) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if streamTmp, ok := obj.Streams[key]; ok {
if channelTmp, ok := streamTmp.Channels[channelID]; ok {
return &channelTmp, nil
}
}
return nil, ErrorStreamNotFound
}
// StreamChannelExist check stream exist
func (obj *StorageST) StreamChannelExist(streamID string, channelID string) bool {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if streamTmp, ok := obj.Streams[streamID]; ok {
if channelTmp, ok := streamTmp.Channels[channelID]; ok {
channelTmp.ack = time.Now()
streamTmp.Channels[channelID] = channelTmp
obj.Streams[streamID] = streamTmp
return ok
}
}
return false
}
// StreamChannelReload reload stream
func (obj *StorageST) StreamChannelReload(uuid string, channelID string) error {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.signals <- SignalStreamRestart
return nil
}
}
return ErrorStreamNotFound
}
// StreamInfo return stream info
func (obj *StorageST) StreamChannelInfo(uuid string, channelID string) (*ChannelST, error) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
return &channelTmp, nil
}
}
return nil, ErrorStreamNotFound
}
// StreamChannelCodecs get stream codec storage or wait
func (obj *StorageST) StreamChannelCodecs(streamID string, channelID string) ([]av.CodecData, error) {
for i := 0; i < 100; i++ {
ret, err := (func() ([]av.CodecData, error) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
tmp, ok := obj.Streams[streamID]
if !ok {
return nil, ErrorStreamNotFound
}
channelTmp, ok := tmp.Channels[channelID]
if !ok {
return nil, ErrorStreamChannelNotFound
}
return channelTmp.codecs, nil
})()
if ret != nil || err != nil {
return ret, err
}
time.Sleep(50 * time.Millisecond)
}
return nil, ErrorStreamChannelCodecNotFound
}
// StreamChannelStatus change stream status
func (obj *StorageST) StreamChannelStatus(key string, channelID string, val int) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[key]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.Status = val
tmp.Channels[channelID] = channelTmp
obj.Streams[key] = tmp
}
}
}
// StreamChannelCast broadcast stream
func (obj *StorageST) StreamChannelCast(key string, channelID string, val *av.Packet) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[key]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
if len(channelTmp.clients) > 0 {
for _, i2 := range channelTmp.clients {
if i2.mode == RTSP {
continue
}
if len(i2.outgoingAVPacket) < 1000 {
i2.outgoingAVPacket <- val
} else if len(i2.signals) < 10 {
i2.signals <- SignalStreamStop
}
}
channelTmp.ack = time.Now()
tmp.Channels[channelID] = channelTmp
obj.Streams[key] = tmp
}
}
}
}
// StreamChannelCastProxy broadcast stream
func (obj *StorageST) StreamChannelCastProxy(key string, channelID string, val *[]byte) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[key]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
if len(channelTmp.clients) > 0 {
for _, i2 := range channelTmp.clients {
if i2.mode != RTSP {
continue
}
if len(i2.outgoingRTPPacket) < 1000 {
i2.outgoingRTPPacket <- val
} else if len(i2.signals) < 10 {
i2.signals <- SignalStreamStop
}
}
channelTmp.ack = time.Now()
tmp.Channels[channelID] = channelTmp
obj.Streams[key] = tmp
}
}
}
}
// StreamChannelCodecsUpdate update stream codec storage
func (obj *StorageST) StreamChannelCodecsUpdate(streamID string, channelID string, val []av.CodecData, sdp []byte) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[streamID]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.codecs = val
channelTmp.sdp = sdp
tmp.Channels[channelID] = channelTmp
obj.Streams[streamID] = tmp
}
}
}
// StreamChannelSDP codec storage or wait
func (obj *StorageST) StreamChannelSDP(streamID string, channelID string) ([]byte, error) {
for i := 0; i < 100; i++ {
obj.mutex.RLock()
tmp, ok := obj.Streams[streamID]
obj.mutex.RUnlock()
if !ok {
return nil, ErrorStreamNotFound
}
channelTmp, ok := tmp.Channels[channelID]
if !ok {
return nil, ErrorStreamChannelNotFound
}
if len(channelTmp.sdp) > 0 {
return channelTmp.sdp, nil
}
time.Sleep(50 * time.Millisecond)
}
return nil, ErrorStreamNotFound
}
// StreamChannelAdd add stream
func (obj *StorageST) StreamChannelAdd(uuid string, channelID string, val ChannelST) error {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if _, ok := obj.Streams[uuid]; !ok {
return ErrorStreamNotFound
}
if _, ok := obj.Streams[uuid].Channels[channelID]; ok {
return ErrorStreamChannelAlreadyExists
}
val = obj.StreamChannelMake(val)
obj.Streams[uuid].Channels[channelID] = val
if !val.OnDemand {
val.runLock = true
go StreamServerRunStreamDo(uuid, channelID)
}
err := obj.SaveConfig()
if err != nil {
return err
}
return nil
}
// StreamEdit edit stream
func (obj *StorageST) StreamChannelEdit(uuid string, channelID string, val ChannelST) error {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if currentChannel, ok := tmp.Channels[channelID]; ok {
if currentChannel.runLock {
currentChannel.signals <- SignalStreamStop
}
val = obj.StreamChannelMake(val)
obj.Streams[uuid].Channels[channelID] = val
if !val.OnDemand {
val.runLock = true
go StreamServerRunStreamDo(uuid, channelID)
}
err := obj.SaveConfig()
if err != nil {
return err
}
return nil
}
}
return ErrorStreamNotFound
}
// StreamChannelDelete stream
func (obj *StorageST) StreamChannelDelete(uuid string, channelID string) error {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
if channelTmp.runLock {
channelTmp.signals <- SignalStreamStop
}
delete(obj.Streams[uuid].Channels, channelID)
err := obj.SaveConfig()
if err != nil {
return err
}
return nil
}
}
return ErrorStreamNotFound
}
// NewHLSMuxer new muxer init
func (obj *StorageST) NewHLSMuxer(uuid string, channelID string) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.hlsMuxer = NewHLSMuxer(uuid)
tmp.Channels[channelID] = channelTmp
obj.Streams[uuid] = tmp
}
}
}
// HlsMuxerSetFPS write packet
func (obj *StorageST) HlsMuxerSetFPS(uuid string, channelID string, fps int) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok && channelTmp.hlsMuxer != nil {
channelTmp.hlsMuxer.SetFPS(fps)
tmp.Channels[channelID] = channelTmp
obj.Streams[uuid] = tmp
}
}
}
// HlsMuxerWritePacket write packet
func (obj *StorageST) HlsMuxerWritePacket(uuid string, channelID string, packet *av.Packet) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok && channelTmp.hlsMuxer != nil {
channelTmp.hlsMuxer.WritePacket(packet)
}
}
}
// HLSMuxerClose close muxer
func (obj *StorageST) HLSMuxerClose(uuid string, channelID string) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.hlsMuxer.Close()
}
}
}
// HLSMuxerM3U8 get m3u8 list
func (obj *StorageST) HLSMuxerM3U8(uuid string, channelID string, msn, part int) (string, error) {
obj.mutex.Lock()
tmp, ok := obj.Streams[uuid]
obj.mutex.Unlock()
if ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
index, err := channelTmp.hlsMuxer.GetIndexM3u8(msn, part)
return index, err
}
}
return "", ErrorStreamNotFound
}
// HLSMuxerSegment get segment
func (obj *StorageST) HLSMuxerSegment(uuid string, channelID string, segment int) ([]*av.Packet, error) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
return channelTmp.hlsMuxer.GetSegment(segment)
}
}
return nil, ErrorStreamChannelNotFound
}
// HLSMuxerFragment get fragment
func (obj *StorageST) HLSMuxerFragment(uuid string, channelID string, segment, fragment int) ([]*av.Packet, error) {
obj.mutex.Lock()
tmp, ok := obj.Streams[uuid]
obj.mutex.Unlock()
if ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
packet, err := channelTmp.hlsMuxer.GetFragment(segment, fragment)
return packet, err
}
}
return nil, ErrorStreamChannelNotFound
}

@ -0,0 +1,80 @@
package main
import (
"sort"
"strconv"
"time"
"github.com/deepch/vdk/av"
)
//StreamHLSAdd add hls seq to buffer
func (obj *StorageST) StreamHLSAdd(uuid string, channelID string, val []*av.Packet, dur time.Duration) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.hlsSegmentNumber++
channelTmp.hlsSegmentBuffer[channelTmp.hlsSegmentNumber] = SegmentOld{data: val, dur: dur}
if len(channelTmp.hlsSegmentBuffer) >= 6 {
delete(channelTmp.hlsSegmentBuffer, channelTmp.hlsSegmentNumber-6-1)
}
tmp.Channels[channelID] = channelTmp
obj.Streams[uuid] = tmp
}
}
}
//StreamHLSm3u8 get hls m3u8 list
func (obj *StorageST) StreamHLSm3u8(uuid string, channelID string) (string, int, error) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
var out string
//TODO fix it
out += "#EXTM3U\r\n#EXT-X-TARGETDURATION:4\r\n#EXT-X-VERSION:4\r\n#EXT-X-MEDIA-SEQUENCE:" + strconv.Itoa(channelTmp.hlsSegmentNumber) + "\r\n"
var keys []int
for k := range channelTmp.hlsSegmentBuffer {
keys = append(keys, k)
}
sort.Ints(keys)
var count int
for _, i := range keys {
count++
out += "#EXTINF:" + strconv.FormatFloat(channelTmp.hlsSegmentBuffer[i].dur.Seconds(), 'f', 1, 64) + ",\r\nsegment/" + strconv.Itoa(i) + "/file.ts\r\n"
}
return out, count, nil
}
}
return "", 0, ErrorStreamNotFound
}
//StreamHLSTS send hls segment buffer to clients
func (obj *StorageST) StreamHLSTS(uuid string, channelID string, seq int) ([]*av.Packet, error) {
obj.mutex.RLock()
defer obj.mutex.RUnlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
if tmp, ok := channelTmp.hlsSegmentBuffer[seq]; ok {
return tmp.data, nil
}
}
}
return nil, ErrorStreamNotFound
}
//StreamHLSFlush delete hls cache
func (obj *StorageST) StreamHLSFlush(uuid string, channelID string) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if tmp, ok := obj.Streams[uuid]; ok {
if channelTmp, ok := tmp.Channels[channelID]; ok {
channelTmp.hlsSegmentBuffer = make(map[int]SegmentOld)
channelTmp.hlsSegmentNumber = 0
tmp.Channels[channelID] = channelTmp
obj.Streams[uuid] = tmp
}
}
}

@ -0,0 +1,123 @@
package main
import (
"errors"
"net"
"sync"
"time"
"github.com/deepch/vdk/av"
"github.com/sirupsen/logrus"
)
var Storage = NewStreamCore()
//Default stream type
const (
MSE = iota
WEBRTC
RTSP
)
//Default stream status type
const (
OFFLINE = iota
ONLINE
)
//Default stream errors
var (
Success = "success"
ErrorStreamNotFound = errors.New("stream not found")
ErrorStreamAlreadyExists = errors.New("stream already exists")
ErrorStreamChannelAlreadyExists = errors.New("stream channel already exists")
ErrorStreamNotHLSSegments = errors.New("stream hls not ts seq found")
ErrorStreamNoVideo = errors.New("stream no video")
ErrorStreamNoClients = errors.New("stream no clients")
ErrorStreamRestart = errors.New("stream restart")
ErrorStreamStopCoreSignal = errors.New("stream stop core signal")
ErrorStreamStopRTSPSignal = errors.New("stream stop rtsp signal")
ErrorStreamChannelNotFound = errors.New("stream channel not found")
ErrorStreamChannelCodecNotFound = errors.New("stream channel codec not ready, possible stream offline")
ErrorStreamsLen0 = errors.New("streams len zero")
ErrorStreamUnauthorized = errors.New("stream request unauthorized")
)
//StorageST main storage struct
type StorageST struct {
mutex sync.RWMutex
Server ServerST `json:"server" groups:"api,config"`
Streams map[string]StreamST `json:"streams,omitempty" groups:"api,config"`
ChannelDefaults ChannelST `json:"channel_defaults,omitempty" groups:"api,config"`
}
//ServerST server storage section
type ServerST struct {
Debug bool `json:"debug" groups:"api,config"`
LogLevel logrus.Level `json:"log_level" groups:"api,config"`
HTTPDemo bool `json:"http_demo" groups:"api,config"`
HTTPDebug bool `json:"http_debug" groups:"api,config"`
HTTPLogin string `json:"http_login" groups:"api,config"`
HTTPPassword string `json:"http_password" groups:"api,config"`
HTTPDir string `json:"http_dir" groups:"api,config"`
HTTPPort string `json:"http_port" groups:"api,config"`
RTSPPort string `json:"rtsp_port" groups:"api,config"`
HTTPS bool `json:"https" groups:"api,config"`
HTTPSPort string `json:"https_port" groups:"api,config"`
HTTPSCert string `json:"https_cert" groups:"api,config"`
HTTPSKey string `json:"https_key" groups:"api,config"`
HTTPSAutoTLSEnable bool `json:"https_auto_tls" groups:"api,config"`
HTTPSAutoTLSName string `json:"https_auto_tls_name" groups:"api,config"`
ICEServers []string `json:"ice_servers" groups:"api,config"`
ICEUsername string `json:"ice_username" groups:"api,config"`
ICECredential string `json:"ice_credential" groups:"api,config"`
Token Token `json:"token,omitempty" groups:"api,config"`
WebRTCPortMin uint16 `json:"webrtc_port_min" groups:"api,config"`
WebRTCPortMax uint16 `json:"webrtc_port_max" groups:"api,config"`
}
//Token auth
type Token struct {
Enable bool `json:"enable" groups:"api,config"`
Backend string `json:"backend" groups:"api,config"`
}
//ServerST stream storage section
type StreamST struct {
Name string `json:"name,omitempty" groups:"api,config"`
Channels map[string]ChannelST `json:"channels,omitempty" groups:"api,config"`
}
type ChannelST struct {
Name string `json:"name,omitempty" groups:"api,config"`
URL string `json:"url,omitempty" groups:"api,config"`
OnDemand bool `json:"on_demand,omitempty" groups:"api,config"`
Debug bool `json:"debug,omitempty" groups:"api,config"`
Status int `json:"status,omitempty" groups:"api"`
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty" groups:"api,config"`
Audio bool `json:"audio,omitempty" groups:"api,config"`
runLock bool
codecs []av.CodecData
sdp []byte
signals chan int
hlsSegmentBuffer map[int]SegmentOld
hlsSegmentNumber int
clients map[string]ClientST
ack time.Time
hlsMuxer *MuxerHLS `json:"-"`
}
//ClientST client storage section
type ClientST struct {
mode int
signals chan int
outgoingAVPacket chan *av.Packet
outgoingRTPPacket chan *[]byte
socket net.Conn
}
//SegmentOld HLS cache section
type SegmentOld struct {
dur time.Duration
data []*av.Packet
}

@ -0,0 +1,313 @@
package main
import (
"math"
"net/url"
"strings"
"time"
"github.com/deepch/vdk/format/rtmp"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/format/rtspv2"
"github.com/sirupsen/logrus"
)
//StreamServerRunStreamDo stream run do mux
func StreamServerRunStreamDo(streamID string, channelID string) {
var status int
defer func() {
//TODO fix it no need unlock run if delete stream
if status != 2 {
Storage.StreamChannelUnlock(streamID, channelID)
}
}()
for {
baseLogger := log.WithFields(logrus.Fields{
"module": "core",
"stream": streamID,
"channel": channelID,
"func": "StreamServerRunStreamDo",
})
baseLogger.WithFields(logrus.Fields{"call": "Run"}).Infoln("Run stream")
opt, err := Storage.StreamChannelControl(streamID, channelID)
if err != nil {
baseLogger.WithFields(logrus.Fields{
"call": "StreamChannelControl",
}).Infoln("Exit", err)
return
}
if opt.OnDemand && !Storage.ClientHas(streamID, channelID) {
baseLogger.WithFields(logrus.Fields{
"call": "ClientHas",
}).Infoln("Stop stream no client")
return
}
status, err = StreamServerRunStream(streamID, channelID, opt)
if status > 0 {
baseLogger.WithFields(logrus.Fields{
"call": "StreamServerRunStream",
}).Infoln("Stream exit by signal or not client")
return
}
if err != nil {
log.WithFields(logrus.Fields{
"call": "Restart",
}).Errorln("Stream error restart stream", err)
}
time.Sleep(2 * time.Second)
}
}
//StreamServerRunStream core stream
func StreamServerRunStream(streamID string, channelID string, opt *ChannelST) (int, error) {
if url, err := url.Parse(opt.URL); err == nil && strings.ToLower(url.Scheme) == "rtmp" {
return StreamServerRunStreamRTMP(streamID, channelID, opt)
}
keyTest := time.NewTimer(20 * time.Second)
checkClients := time.NewTimer(20 * time.Second)
var start bool
var fps int
var preKeyTS = time.Duration(0)
var Seq []*av.Packet
RTSPClient, err := rtspv2.Dial(rtspv2.RTSPClientOptions{URL: opt.URL, InsecureSkipVerify: opt.InsecureSkipVerify, DisableAudio: !opt.Audio, DialTimeout: 3 * time.Second, ReadWriteTimeout: 5 * time.Second, Debug: opt.Debug, OutgoingProxy: true})
if err != nil {
return 0, err
}
Storage.StreamChannelStatus(streamID, channelID, ONLINE)
defer func() {
RTSPClient.Close()
Storage.StreamChannelStatus(streamID, channelID, OFFLINE)
Storage.StreamHLSFlush(streamID, channelID)
}()
var WaitCodec bool
/*
Example wait codec
*/
if RTSPClient.WaitCodec {
WaitCodec = true
} else {
if len(RTSPClient.CodecData) > 0 {
Storage.StreamChannelCodecsUpdate(streamID, channelID, RTSPClient.CodecData, RTSPClient.SDPRaw)
}
}
log.WithFields(logrus.Fields{
"module": "core",
"stream": streamID,
"channel": channelID,
"func": "StreamServerRunStream",
"call": "Start",
}).Infoln("Success connection RTSP")
var ProbeCount int
var ProbeFrame int
var ProbePTS time.Duration
Storage.NewHLSMuxer(streamID, channelID)
defer Storage.HLSMuxerClose(streamID, channelID)
for {
select {
//Check stream have clients
case <-checkClients.C:
if opt.OnDemand && !Storage.ClientHas(streamID, channelID) {
return 1, ErrorStreamNoClients
}
checkClients.Reset(20 * time.Second)
//Check stream send key
case <-keyTest.C:
return 0, ErrorStreamNoVideo
//Read core signals
case signals := <-opt.signals:
switch signals {
case SignalStreamStop:
return 2, ErrorStreamStopCoreSignal
case SignalStreamRestart:
return 0, ErrorStreamRestart
case SignalStreamClient:
return 1, ErrorStreamNoClients
}
//Read rtsp signals
case signals := <-RTSPClient.Signals:
switch signals {
case rtspv2.SignalCodecUpdate:
Storage.StreamChannelCodecsUpdate(streamID, channelID, RTSPClient.CodecData, RTSPClient.SDPRaw)
WaitCodec = false
case rtspv2.SignalStreamRTPStop:
return 0, ErrorStreamStopRTSPSignal
}
case packetRTP := <-RTSPClient.OutgoingProxyQueue:
Storage.StreamChannelCastProxy(streamID, channelID, packetRTP)
case packetAV := <-RTSPClient.OutgoingPacketQueue:
if WaitCodec {
continue
}
if packetAV.IsKeyFrame {
keyTest.Reset(20 * time.Second)
if preKeyTS > 0 {
Storage.StreamHLSAdd(streamID, channelID, Seq, packetAV.Time-preKeyTS)
Seq = []*av.Packet{}
}
preKeyTS = packetAV.Time
}
Seq = append(Seq, packetAV)
Storage.StreamChannelCast(streamID, channelID, packetAV)
/*
HLS LL Test
*/
if packetAV.IsKeyFrame && !start {
start = true
}
/*
FPS mode probe
*/
if start {
ProbePTS += packetAV.Duration
ProbeFrame++
if packetAV.IsKeyFrame && ProbePTS.Seconds() >= 1 {
ProbeCount++
if ProbeCount == 2 {
fps = int(math.Round(float64(ProbeFrame) / ProbePTS.Seconds()))
}
ProbeFrame = 0
ProbePTS = 0
}
}
if start && fps != 0 {
//TODO fix it
packetAV.Duration = time.Duration((float32(1000)/float32(fps))*1000*1000) * time.Nanosecond
Storage.HlsMuxerSetFPS(streamID, channelID, fps)
Storage.HlsMuxerWritePacket(streamID, channelID, packetAV)
}
}
}
}
func StreamServerRunStreamRTMP(streamID string, channelID string, opt *ChannelST) (int, error) {
keyTest := time.NewTimer(20 * time.Second)
checkClients := time.NewTimer(20 * time.Second)
OutgoingPacketQueue := make(chan *av.Packet, 1000)
Signals := make(chan int, 100)
var start bool
var fps int
var preKeyTS = time.Duration(0)
var Seq []*av.Packet
conn, err := rtmp.DialTimeout(opt.URL, 3*time.Second)
if err != nil {
return 0, err
}
Storage.StreamChannelStatus(streamID, channelID, ONLINE)
defer func() {
conn.Close()
Storage.StreamChannelStatus(streamID, channelID, OFFLINE)
Storage.StreamHLSFlush(streamID, channelID)
}()
var WaitCodec bool
codecs, err := conn.Streams()
if err != nil {
return 0, err
}
preDur := make([]time.Duration, len(codecs))
Storage.StreamChannelCodecsUpdate(streamID, channelID, codecs, []byte{})
log.WithFields(logrus.Fields{
"module": "core",
"stream": streamID,
"channel": channelID,
"func": "StreamServerRunStream",
"call": "Start",
}).Infoln("Success connection RTSP")
var ProbeCount int
var ProbeFrame int
var ProbePTS time.Duration
Storage.NewHLSMuxer(streamID, channelID)
defer Storage.HLSMuxerClose(streamID, channelID)
go func() {
for {
ptk, err := conn.ReadPacket()
if err != nil {
break
}
OutgoingPacketQueue <- &ptk
}
Signals <- 1
}()
for {
select {
//Check stream have clients
case <-checkClients.C:
if opt.OnDemand && !Storage.ClientHas(streamID, channelID) {
return 1, ErrorStreamNoClients
}
checkClients.Reset(20 * time.Second)
//Check stream send key
case <-keyTest.C:
return 0, ErrorStreamNoVideo
//Read core signals
case signals := <-opt.signals:
switch signals {
case SignalStreamStop:
return 2, ErrorStreamStopCoreSignal
case SignalStreamRestart:
return 0, ErrorStreamRestart
case SignalStreamClient:
return 1, ErrorStreamNoClients
}
//Read rtsp signals
case <-Signals:
return 0, ErrorStreamStopRTSPSignal
case packetAV := <-OutgoingPacketQueue:
if preDur[packetAV.Idx] != 0 {
packetAV.Duration = packetAV.Time - preDur[packetAV.Idx]
}
preDur[packetAV.Idx] = packetAV.Time
if WaitCodec {
continue
}
if packetAV.IsKeyFrame {
keyTest.Reset(20 * time.Second)
if preKeyTS > 0 {
Storage.StreamHLSAdd(streamID, channelID, Seq, packetAV.Time-preKeyTS)
Seq = []*av.Packet{}
}
preKeyTS = packetAV.Time
}
Seq = append(Seq, packetAV)
Storage.StreamChannelCast(streamID, channelID, packetAV)
/*
HLS LL Test
*/
if packetAV.IsKeyFrame && !start {
start = true
}
/*
FPS mode probe
*/
if start {
ProbePTS += packetAV.Duration
ProbeFrame++
if packetAV.IsKeyFrame && ProbePTS.Seconds() >= 1 {
ProbeCount++
if ProbeCount == 2 {
fps = int(math.Round(float64(ProbeFrame) / ProbePTS.Seconds()))
}
ProbeFrame = 0
ProbePTS = 0
}
}
if start && fps != 0 {
//TODO fix it
packetAV.Duration = time.Duration((float32(1000)/float32(fps))*1000*1000) * time.Nanosecond
Storage.HlsMuxerSetFPS(streamID, channelID, fps)
Storage.HlsMuxerWritePacket(streamID, channelID, packetAV)
}
}
}
}

@ -0,0 +1,74 @@
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"time"
)
type AuthorizationReq struct {
Proto string `json:"proto,omitempty"`
Stream string `json:"stream,omitempty"`
Channel string `json:"channel,omitempty"`
Token string `json:"token,omitempty"`
IP string `json:"ip,omitempty"`
}
type AuthorizationRes struct {
Status string `json:"status,omitempty"`
}
func RemoteAuthorization(proto string, stream string, channel string, token string, ip string) bool {
if !Storage.ServerTokenEnable() {
return true
}
buf, err := json.Marshal(&AuthorizationReq{proto, stream, channel, token, ip})
if err != nil {
return false
}
request, err := http.NewRequest("POST", Storage.ServerTokenBackend(), bytes.NewBuffer(buf))
if err != nil {
return false
}
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
client := &http.Client{
Timeout: 1 * time.Second,
}
response, err := client.Do(request)
if err != nil {
return false
}
defer response.Body.Close()
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return false
}
var res AuthorizationRes
err = json.Unmarshal(bodyBytes, &res)
if err != nil {
return false
}
if res.Status == "1" {
return true
}
return false
}

@ -0,0 +1,49 @@
package main
import (
"crypto/rand"
"fmt"
"strconv"
"strings"
)
//Default streams signals
const (
SignalStreamRestart = iota ///< Y Restart
SignalStreamStop
SignalStreamClient
)
//generateUUID function make random uuid for clients and stream
func generateUUID() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
}
//stringToInt convert string to int if err to zero
func stringToInt(val string) int {
i, err := strconv.Atoi(val)
if err != nil {
return 0
}
return i
}
//stringInBetween fin char to char sub string
func stringInBetween(str string, start string, end string) (result string) {
s := strings.Index(str, start)
if s == -1 {
return
}
str = str[s+len(start):]
e := strings.Index(str, end)
if e == -1 {
return
}
str = str[:e]
return str
}

@ -0,0 +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]

@ -0,0 +1,133 @@
#!/bin/bash
set -euo pipefail
echo "curl http://demo:demo@127.0.0.1:8083/streams"
curl http://demo:demo@127.0.0.1:8083/streams
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/add"
curl --header "Content-Type: application/json" \
--request POST \
--data '{
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4",
"on_demand": false,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin123@10.128.18.224:999/mpeg4cif",
"on_demand": true,
"debug": false,
"status": 0
}
}
}' \
http://demo:demo@127.0.0.1:8083/stream/testing/add
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/edit"
curl --header "Content-Type: application/json" \
--request POST \
--data '{
"name": "test video",
"channels": {
"0": {
"name": "ch1",
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4",
"on_demand": true,
"debug": false,
"status": 0
},
"1": {
"name": "ch2",
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4",
"on_demand": false,
"debug": false,
"status": 0
}
}
}' \
http://demo:demo@127.0.0.1:8083/stream/testing/edit
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/add"
curl --header "Content-Type: application/json" \
--request POST \
--data '{
"name": "ch4",
"url": "rtsp://admin:admin@YOU_CAMERA_IP/uri",
"on_demand": false,
"debug": false,
"status": 0
}' \
http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/add
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/edit"
curl --header "Content-Type: application/json" \
--request POST \
--data '{
"name": "ch4",
"url": "rtsp://admin:admin@YOU_CAMERA_IP/uri",
"on_demand": true,
"debug": false,
"status": 0
}' \
http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/edit
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/info"
curl http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/info
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/codec"
curl http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/codec
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/delete"
curl http://demo:demo@127.0.0.1:8083/stream/testing/channel/4/delete
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/reload"
curl http://demo:demo@127.0.0.1:8083/stream/testing/reload
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/info"
echo "/stream/testing/info"
curl http://demo:demo@127.0.0.1:8083/stream/testing/info
sleep 1
echo "http://demo:demo@127.0.0.1:8083/stream/testing/delete"
curl http://demo:demo@127.0.0.1:8083/stream/testing/delete
sleep 1
echo "http://demo:demo@127.0.0.1:8083/pages/multiview/full"
curl --header "Content-Type: application/json" \
--request POST \
--data '{
"grid":6,
"player":{
"1": {
"uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211",
"channel": 1,
"playerType": "mse"
},
"2": {
"uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211",
"channel": 0,
"playerType": "mse"
},
"3": {
"uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211",
"channel": 1,
"playerType": "hls"
},
"4": {
"uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211",
"channel": 0,
"playerType": "mse"
},
"6": {
"uuid": "d43e9364-e2e3-4b41-9f78-b90de1991211",
"channel": 1,
"playerType": "mse"
}
}
}' \
http://demo:demo@127.0.0.1:8083/pages/multiview/full

@ -0,0 +1,132 @@
#!/bin/bash
set -euo pipefail
curl --header "Content-Type: application/json" \
--request POST \
--data '{
"streams": {
"demo1": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video1"
},
"demo2": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video2"
},
"demo3": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video3"
},
"demo4": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video4"
},
"demo5": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video5"
},
"demo6": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video6"
},
"demo7": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video7"
},
"demo8": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video8"
},
"demo9": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video9"
},
"demo10": {
"channels": {
"0": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4"
},
"1": {
"url": "rtsp://admin:admin123@10.128.18.224/mpeg4cif"
}
},
"name": "test video10"
}
}
}' \
http://demo:demo@127.0.0.1:8083/streams/multi/control/add
sleep 1
echo "curl http://demo:demo@127.0.0.1:8083/streams"
curl http://demo:demo@127.0.0.1:8083/streams
sleep 1
curl --header "Content-Type: application/json" \
--request POST \
--data '["demo1", "demo2", "demo3", "demo4", "demo5", "demo6", "demo7", "demo8", "demo9", "demo10"]' \
http://demo:demo@127.0.0.1:8083/streams/multi/control/delete
sleep 1
echo "curl http://demo:demo@127.0.0.1:8083/streams"
curl http://demo:demo@127.0.0.1:8083/streams

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,163 @@
.grid-wrapper{
height: calc(100vh - 30px);
margin: 0px;
background: transparent;
display: flex;
flex-wrap: wrap;
}
.layout-top-nav .wrapper .main-header {
margin-left: 0;
height: 30px;
}
video.background{
pointer-events: none;
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.control-sidebar.custom{
background: black;
height: 100%;
margin-top: -57px;
padding: 30px 8px 8px;
background: linear-gradient(45deg, black, transparent);
overflow: hidden;
}
.control-sidebar.custom .row{
height:100%;
overflow: auto;
}
.control-sidebar.custom>h5{
color: white;
text-align: center;
}
.grid-wrapper .player video.video-class {
background: #000;
object-fit: fill;
}
.grid-wrapper .player video.video-class.empty {
background: transparent;
}
.img-background{
/* background: url(/../static/img/back.jpg); */
background-color: #343a40;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
backdrop-filter: sepia(0.5) grayscale(0.4);
}
.img-background .content-wrapper{
background: transparent;
}
.img-background .main-header{
background:
}
.grid-wrapper .player {
padding: 0px;
border: 2px solid #000;
}
.grid-wrapper .player .remove-btn{
color:white;
display: none;
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
padding: 5px 10px;
border-radius: 50px;
filter: drop-shadow(1px 1px 1px black);
}
.grid-wrapper .player.empty .remove-btn{
display: none;
}
.grid-wrapper .player:not(.empty):hover .remove-btn{
display: block;
}
.grid-wrapper .player .add-stream-btn{
position: absolute;
left: 0;
top: calc(50% - 2vw);
text-align: center;
width: 100%;
font-variant-caps: all-petite-caps;
font-size: 1.5vw;
line-height: 1;
color: white;
cursor: pointer;
display: none;
filter: drop-shadow(2px 4px 6px black);
}
.grid-wrapper .player.empty .add-stream-btn{
display: block;
}
.carousel-caption {
pointer-events: none;
filter: drop-shadow(1px 1px 1px black);
}
.stream-img{
height: 150px;
}
.player .loader, .main-player .loader{
position: absolute;
width: 100px;
height: 70px;
top: calc(50% - 35px);
left: calc(50% - 50px);
}
.control-sidebar-close-btn{
position: absolute;
top: 37px;
right: 12px;
color: #fff;
}
.control-sidebar-close-btn:hover{
color: #999;
}
.dropdown-menu.custom-dropdown.show{
color: #ffF;
border: 1px solid #343a40;
background: linear-gradient(45deg, #000000d1 30%,#343a40);
padding: 0;
text-align: center;
}
.dropdown-menu.custom-dropdown .custom-dropdown-item{
float: left;
width: 33%;
padding: 10px;
font-size: 12px;
cursor: pointer;
}
.dropdown-menu.custom-dropdown .custom-dropdown-item.active,.dropdown-menu.custom-dropdown .custom-dropdown-item:hover{
border-radius: 5px;
box-shadow: inset 0px 0px 15px -5px;
}
.dropdown-menu.custom-dropdown .dropdown-item{
color: #fff;
}
.dropdown-menu.custom-dropdown .dropdown-item:hover{
box-shadow: inset 0px 0px 15px -5px;
background: transparent;
}
.custom-dropdown-item.with-img{
width: 50%!important;
height: auto;
padding: 2px!important;
}
.custom-dropdown-item.with-img img{
width: 100%;
height: auto;
}

@ -0,0 +1,224 @@
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7qsDJB9cme_xc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7jsDJB9cme_xc.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7rsDJB9cme_xc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7ksDJB9cme_xc.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7osDJB9cme_xc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJB9cme_xc.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url(fonts/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDJB9cme.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmhdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwkxdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmxdu3cOWxy40.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlBdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmBdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmRdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu3cOWxw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lujVj9_mf.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lujVj9_mf.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lujVj9_mf.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lujVj9_mf.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lujVj9_mf.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lujVj9_mf.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url(fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxdu3cOWxy40.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRdu3cOWxy40.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url(fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@ -0,0 +1,223 @@
#videoPlayer{
width: 100%;
background: black;
position: -webkit-sticky; /* Safari */
position: sticky;
top: 15px;
margin-bottom: -6px;
}
.videoPlayer{
width: 100%;
background: black;
position: -webkit-sticky; /* Safari */
position: sticky;
top: 15px;
margin-bottom: -6px;
}
.btn-group.stream{
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.btn-group.stream .btn{
flex: 1 0 30%;
}
.btn-group.stream .input-group-prepend{
flex: 1 0 30%;
}
.img-wrapper{
width: 100%;
height: 180px;
background: black;
cursor: pointer;
}
.img-wrapper img{
width: 100%;
height: 100%;
}
.text-wrapper{
padding: 10px;
}
@media (min-width: 576px){
.card-columns {
-webkit-column-count: 4;
-moz-column-count: 4;
column-count: 4;
-webkit-column-gap: 1.25rem;
-moz-column-gap: 1.25rem;
column-gap: 1.25rem;
orphans: 1;
widows: 1;
}
}
@media (min-width: 576px){
.card-column {
column-count: 2;
}
}
@media (min-width: 768px){
.card-column {
column-count: 4;
}
}
@media (min-width: 1920px){
.card-column {
column-count: 6;
}
}
.grid-wrapper{
height: 80vh;
margin: -1px;
background: #000;
display: flex;
flex-wrap: wrap;
}
:-webkit-full-screen .grid-wrapper{/*WebKit, Opera 15+*/
height: 100vh;
width: 100vw;
}
:-moz-full-screen .grid-wrapper{/*FireFox*/
height: 100vh;
width: 100vw;
}
:full-screen .grid-wrapper{/*Opera 12.15-, Blink, w3c standard*/
height: 100vh;
width: 100vw;
}
.grid-wrapper .player{
height: 50%;
width: 50%;
position: relative;
padding: 2px;
}
.grid-wrapper .player.grid-1{
width: 100%;
height: 100%;
}
.grid-wrapper .player.grid-6{
width: 33.33%;
}
.grid-wrapper .player.grid-9{
width: 33.33%;
height: 33.33%;
}
.grid-wrapper .player.grid-12{
width: 25%;
height: 33.33%;
}
.grid-wrapper .player.grid-16{
width: 25%;
height: 25%;
}
.grid-wrapper .player.grid-25{
width: 20%;
height: 20%;
}
.grid-wrapper .player.grid-36{
width: 16.66%;
height: 16.66%;
}
.grid-wrapper .player.grid-49{
width: 14.285%;
height: 14.29%;
}
.grid-wrapper .player video{
height: 100%;
width: 100%;
background: #343a40;
margin-bottom: -6px;
}
.grid-wrapper .player .play-info{
position: absolute;
padding: 10px;
text-align: center;
color: #fff;
top:0;
left: 0;
width: 100%;
}
.grid-wrapper .player .control{
width: 100%;
position: absolute;
bottom: 0;
padding: 10px;
text-align: center;
}
.grid-wrapper .player .control .btn{
margin: 0 0 10px 10px;
}
.card .stream-name{
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.card .stream-name .card-title{
color: #FFF;
width: 100%;
filter: drop-shadow(1px 0px 1px black);
font-size: 30px;
}
.one-line-header{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 95%;
}
.main-player-wrapper{
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgb(0 0 0 / 0.8);
z-index: 1100;
padding: 15px;
}
.main-player-wrapper a{
color: #fff;
position: absolute;
right: 5px;
top: 5px;
font-size: 30px;
padding: 0 12px;
border-radius: 50px;
box-shadow: 0px 0px 10px -1px #fff;
cursor: pointer;
}
.main-player-wrapper a:hover{
color: #dc3545;
box-shadow: 0px 0px 10px -1px #dc3545;
}
.main-player-wrapper .main-player{
width: 100%;
height: 100%;
position: relative;
}
.main-player-wrapper .main-player video{
width: 100%;
height: 100%;
background: #000;
}
.main-player-wrapper .main-player .play-info{
position: absolute;
padding: 10px;
text-align: center;
color: #fff;
top: 0;
width: 100%;
}
.carousel-caption{
pointer-events: none;
}
.fix-height{
object-fit: fill;
aspect-ratio: 16/9;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

@ -0,0 +1,14 @@
<svg version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<path fill="#044669" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
<animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="2s" from="0 50 50" to="360 50 50" repeatCount="indefinite"></animateTransform>
</path>
<path fill="#eeb492" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
<animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="1s" from="0 50 50" to="-360 50 50" repeatCount="indefinite"></animateTransform>
</path>
<path fill="#0e71bb" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
L82,35.7z">
<animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="2s" from="0 50 50" to="360 50 50" repeatCount="indefinite"></animateTransform>
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Warstwa_1"
x="0px"
y="0px"
viewBox="0 0 160 90"
xml:space="preserve"
sodipodi:docname="noimage.svg"
width="160"
height="90"
inkscape:version="1.0 (4035a4f, 2020-05-01)"><metadata
id="metadata35"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs33" /><sodipodi:namedview
inkscape:document-rotation="0"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1430"
inkscape:window-height="646"
id="namedview31"
showgrid="false"
fit-margin-top="3"
fit-margin-left="18"
fit-margin-right="18"
fit-margin-bottom="3"
lock-margins="false"
inkscape:zoom="2.3805928"
inkscape:cx="5.4286223"
inkscape:cy="31.462046"
inkscape:window-x="0"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="Warstwa_1" />
<style
type="text/css"
id="style10">
.st0{fill:none;stroke:#B3B3B3;stroke-width:4;stroke-miterlimit:10;}
.st1{fill:#B3B3B3;}
.st2{fill:#B3B3B3;stroke:#FFFFFF;stroke-width:4;stroke-miterlimit:10;}
.st3{fill:none;stroke:#FFFFFF;stroke-width:4;stroke-linecap:round;stroke-miterlimit:10;}
</style>
<path
class="st0"
d="M 134,85 H 26 c -3.3,0 -6,-2.7 -6,-6 V 11 c 0,-3.3 2.7,-6 6,-6 h 108 c 3.3,0 6,2.7 6,6 v 68 c 0,3.3 -2.7,6 -6,6 z"
id="path12" />
<g
id="g16"
transform="translate(0,-15)">
<path
class="st1"
d="M 140.9,95.7 V 82.3 c 0,-0.8 -0.3,-1.5 -0.8,-2.1 l -34.5,-42 c -1.4,-1.6 -3.9,-1.6 -5.2,0 L 70.1,76.5 c -1.3,1.7 -3.9,1.7 -5.2,0 L 51.5,60.2 c -1.4,-1.7 -3.9,-1.6 -5.2,0.1 l -26,33.3 c -1.7,2.2 -0.1,5.4 2.6,5.4 h 114.7 c 1.8,0 3.3,-1.5 3.3,-3.3 z"
id="path14" />
</g>
<g
id="g20"
transform="translate(0,-15)">
<circle
class="st1"
cx="47.5"
cy="42.5"
r="7.5"
id="circle18" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4f, 2020-05-01)"
sodipodi:docname="pic.svg"
id="svg88"
version="1.1"
overflow="visible"
viewBox="0 0 54.699371 54.699371"
preserveAspectRatio="xMidYMid meet"
height="54.699371"
width="54.699371">
<metadata
id="metadata92">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:current-layer="svg88"
inkscape:window-maximized="0"
inkscape:window-y="23"
inkscape:window-x="0"
inkscape:cy="40.691348"
inkscape:cx="153.5309"
inkscape:zoom="2.1716669"
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
inkscape:pagecheckerboard="true"
showgrid="false"
id="namedview90"
inkscape:window-height="690"
inkscape:window-width="1468"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<defs
id="SvgjsDefs1115">
<linearGradient
y2="5"
x2="95"
y1="95"
x1="5"
gradientUnits="userSpaceOnUse"
id="SvgjsLinearGradientdK8lI-Ssh">
<stop
offset="0.05"
stop-color="#ddabb2"
id="SvgjsStop1118" />
<stop
offset="0.95"
stop-color="#b9505e"
id="SvgjsStop1119" />
</linearGradient>
</defs>
<g
id="SvgjsG1120"
class="WnM5in50c"
transform="matrix(0.60777079,0,0,0.60777079,-3.038854,-3.038854)"
light-content="false"
non-strokable="false"
fill="#ffffff">
<path
d="M 50,35 C 42.185,35 35.755,41.009 35.068,48.648 A 0.991,0.991 0 0 0 35.04,49.206 C 35.026,49.471 35,49.732 35,50 c 0,8.271 6.729,15 15,15 8.271,0 15,-6.729 15,-15 0,-8.271 -6.729,-15 -15,-15 z m 4.079,27.335 2.628,-2.628 a 0.99984899,0.99984899 0 1 0 -1.414,-1.414 l -4,4 A 0.988,0.988 0 0 0 51.01,62.949 C 50.676,62.975 50.341,63 50,63 c -0.817,0 -1.613,-0.085 -2.388,-0.229 0.03,-0.024 0.067,-0.035 0.095,-0.063 l 4,-4 a 0.99984899,0.99984899 0 1 0 -1.414,-1.414 l -4,4 a 0.996,0.996 0 0 0 -0.207,1.103 12.97,12.97 0 0 1 -2.79,-1.278 l 3.411,-3.41 a 0.99984899,0.99984899 0 1 0 -1.414,-1.414 l -3.65,3.65 a 13.055,13.055 0 0 1 -1.586,-1.586 l 3.65,-3.65 a 0.99984899,0.99984899 0 1 0 -1.414,-1.414 l -3.411,3.41 a 12.952,12.952 0 0 1 -1.038,-2.134 l 3.862,-3.862 a 0.99984899,0.99984899 0 1 0 -1.414,-1.414 l -3.067,3.067 A 13.052,13.052 0 0 1 37,50 c 0,-0.208 0.021,-0.411 0.031,-0.617 l 3.676,-3.676 a 0.99984899,0.99984899 0 1 0 -1.414,-1.414 l -1.628,1.628 C 39.381,40.747 44.257,37 50,37 c 7.168,0 13,5.832 13,13 0,5.742 -3.746,10.619 -8.921,12.335 z"
id="path79" />
<path
d="m 86.063,35.063 c 1.02,-6.916 -0.286,-12.925 -4.242,-16.881 a 0.99984899,0.99984899 0 1 0 -1.414,1.414 c 3.355,3.355 4.565,8.386 3.891,14.246 C 80.805,31.557 76.59,29.625 71.847,28.155 67.57,14.357 59.383,5 50,5 44.422,5 39.267,8.307 35.103,13.885 a 29.54,29.54 0 0 0 -4.321,-0.332 c -5.217,0 -9.575,1.6 -12.602,4.626 -3.896,3.895 -5.364,9.794 -4.298,16.925 C 8.306,39.269 5,44.423 5,50 c 0,5.596 3.327,10.768 8.938,14.938 -1.019,6.918 0.286,12.927 4.243,16.883 3.103,3.104 7.465,4.575 12.54,4.575 5.771,-0.001 12.46,-1.922 19.255,-5.498 4.348,2.308 8.689,3.963 12.805,4.832 C 59.123,90.318 54.727,93 50,93 a 1,1 0 1 0 0,2 c 5.578,0 10.733,-3.307 14.896,-8.886 1.48,0.22 2.924,0.332 4.322,0.332 5.217,0 9.574,-1.6 12.602,-4.626 4.804,-4.804 5.918,-12.653 3.139,-22.103 -0.945,-3.218 -2.316,-6.495 -4.044,-9.755 2.304,-4.38 3.911,-8.713 4.763,-12.787 C 90.299,40.842 93,45.255 93,50 a 1,1 0 1 0 2,0 C 95,44.405 91.674,39.233 86.063,35.063 Z M 50,7 c 8.271,0 15.535,8.209 19.589,20.497 A 71.163,71.163 0 0 0 60.083,25.641 C 52.564,19.82 44.538,15.816 37.219,14.271 40.877,9.682 45.273,7 50,7 Z m 23,43 c 0,3.194 -0.193,6.305 -0.549,9.302 a 78.93,78.93 0 0 1 -6.188,6.962 C 61.088,71.44 55.516,75.618 50.021,78.663 a 66.94,66.94 0 0 1 -6.21,-3.905 C 45.836,74.913 47.899,75 50,75 a 1,1 0 1 0 0,-2 78.89,78.89 0 0 1 -9.286,-0.547 78.472,78.472 0 0 1 -6.977,-6.189 C 28.652,61.178 24.429,55.618 21.328,49.996 A 67.12,67.12 0 0 1 25.244,43.781 80.775,80.775 0 0 0 25,50 a 1,1 0 1 0 2,0 c 0,-3.193 0.193,-6.302 0.548,-9.298 a 78.982,78.982 0 0 1 6.189,-6.964 0.99984899,0.99984899 0 1 0 -1.414,-1.414 80.518,80.518 0 0 0 -4.227,4.573 67.22,67.22 0 0 1 1.628,-7.172 C 35.769,27.988 42.672,27 50,27 c 3.189,0 6.294,0.192 9.287,0.546 A 78.786,78.786 0 0 1 72.452,40.697 C 72.807,43.695 73,46.806 73,50 Z m 1.756,-6.222 a 66.014,66.014 0 0 1 3.916,6.226 67.17,67.17 0 0 1 -3.916,6.219 C 74.912,54.188 75,52.112 75,50 75,47.888 74.912,45.813 74.756,43.778 Z M 36.888,71.902 c -8.354,-1.434 -15.597,-4.2 -20.863,-7.829 0.695,-3.778 2.112,-7.839 4.197,-11.981 3.124,5.399 7.226,10.711 12.101,15.586 a 81.469,81.469 0 0 0 4.565,4.224 z M 20.243,47.929 C 18.864,45.178 17.757,42.425 16.96,39.718 a 37.788,37.788 0 0 1 -0.909,-3.809 c 3.166,-2.177 7.04,-4.044 11.445,-5.498 a 71.247,71.247 0 0 0 -1.855,9.501 71.338,71.338 0 0 0 -5.398,8.017 z M 30.411,27.497 c 1.452,-4.402 3.318,-8.274 5.493,-11.438 6.414,1.166 13.481,4.384 20.285,9.184 A 80.5,80.5 0 0 0 50,25 c -7.018,0 -13.664,0.898 -19.589,2.497 z m 37.267,4.825 a 80.78,80.78 0 0 0 -4.564,-4.225 c 2.494,0.428 4.889,0.974 7.162,1.626 a 67.256,67.256 0 0 1 1.626,7.158 83.241,83.241 0 0 0 -4.224,-4.559 z M 19.595,19.594 c 2.644,-2.644 6.512,-4.041 11.188,-4.041 0.984,0 2,0.079 3.029,0.195 -2.271,3.484 -4.194,7.683 -5.657,12.405 -4.729,1.465 -8.933,3.391 -12.42,5.667 -0.703,-6.031 0.605,-10.971 3.86,-14.226 z M 7,50 c 0,-4.727 2.683,-9.124 7.272,-12.782 a 40.51,40.51 0 0 0 0.77,3.064 c 0.946,3.218 2.316,6.495 4.044,9.757 -2.304,4.379 -3.911,8.713 -4.763,12.786 C 9.701,59.158 7,54.745 7,50 Z M 19.595,80.406 C 16.24,77.051 15.029,72.02 15.704,66.16 c 6.175,4.038 14.612,6.973 24.214,8.199 a 71.814,71.814 0 0 0 8.015,5.408 c -11.552,5.821 -22.49,6.489 -28.338,0.639 z m 32.471,-0.659 c 5.318,-3.06 10.651,-7.108 15.611,-12.069 a 80.82,80.82 0 0 0 4.226,-4.57 c -1.431,8.338 -4.189,15.57 -7.808,20.834 -3.84,-0.698 -7.913,-2.125 -12.029,-4.195 z M 83.041,60.282 c 2.563,8.715 1.627,15.861 -2.635,20.124 -2.644,2.643 -6.512,4.04 -11.188,4.04 -0.984,0 -2,-0.079 -3.03,-0.195 4.022,-6.169 6.946,-14.584 8.17,-24.159 a 71.39,71.39 0 0 0 5.4,-8.02 c 1.379,2.75 2.486,5.504 3.283,8.21 z M 79.779,47.909 a 71.36,71.36 0 0 0 -5.424,-8.021 71.235,71.235 0 0 0 -1.852,-9.477 c 4.417,1.457 8.301,3.331 11.472,5.516 -0.695,3.778 -2.112,7.839 -4.196,11.982 z"
id="path81" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

@ -0,0 +1,218 @@
var rtspPlayer={
active:false,
type:'live',
hls:null,
ws:null,
mseSourceBuffer:null,
mse:null,
mseQueue:[],
mseStreamingStarted:false,
webrtc:null,
webrtcSendChannel:null,
webrtcSendChannelInterval:null,
uuid:null,
clearPlayer:function(){
if(this.active){
if(this.hls!=null){
this.hls.destroy();
this.hls=null;
}
if(this.ws!=null){
//close WebSocket connection if opened
this.ws.close(1000);
this.ws=null;
}
if(this.webrtc!=null){
clearInterval(this.webrtcSendChannelInterval);
this.webrtc=null;
}
$('#videoPlayer')[0].src = '';
$('#videoPlayer')[0].load();
this.active=false;
}
},
livePlayer:function(type,uuid){
this.clearPlayer();
this.uuid=uuid;
this.active=true;
$('.streams-vs-player').addClass('active-player');
if(type==0){
type=$('input[name=defaultPlayer]:checked').val()
}
switch (type) {
case 'hls':
this.playHls();
break;
case 'mse':
this.playMse();
break;
case 'webrtc':
this.playWebrtc();
break;
default:
Swal.fire(
'Sorry',
'This option is still under development',
'question'
)
return;
}
},
playHls:function(){
if(this.hls==null && Hls.isSupported()){
this.hls = new Hls();
}
if ($("#videoPlayer")[0].canPlayType('application/vnd.apple.mpegurl')) {
$("#videoPlayer")[0].src = this.streamPlayUrl('hls');
$("#videoPlayer")[0].load();
} else {
if (this.hls != null) {
this.hls.loadSource(this.streamPlayUrl('hls'));
this.hls.attachMedia($("#videoPlayer")[0]);
} else {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Your browser don`t support hls '
})
}
}
},
playWebrtc:function(){
var _this=this;
this.webrtc=new RTCPeerConnection({
iceServers: [{
urls: ["stun:stun.l.google.com:19302"]
}]
});
this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded;
this.webrtc.ontrack = function(event) {
console.log(event.streams.length + ' track is delivered');
$("#videoPlayer")[0].srcObject = event.streams[0];
$("#videoPlayer")[0].play();
}
this.webrtc.addTransceiver('video', {
'direction': 'sendrecv'
});
this.webrtcSendChannel = this.webrtc.createDataChannel('foo');
this.webrtcSendChannel.onclose = () => console.log('sendChannel has closed');
this.webrtcSendChannel.onopen = () => {
console.log('sendChannel has opened');
this.webrtcSendChannel.send('ping');
this.webrtcSendChannelInterval = setInterval(() => {
this.webrtcSendChannel.send('ping');
}, 1000)
}
this.webrtcSendChannel.onmessage = e => console.log(e.data);
},
handleNegotiationNeeded: async function(){
var _this=rtspPlayer;
offer = await _this.webrtc.createOffer();
await _this.webrtc.setLocalDescription(offer);
$.post(_this.streamPlayUrl('webrtc'), {
data: btoa(_this.webrtc.localDescription.sdp)
}, function(data) {
//console.log(data)
try {
_this.webrtc.setRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp: atob(data)
}))
} catch (e) {
console.warn(e);
}
});
},
playMse:function(){
//console.log(this.streamPlayUrl('mse'));
var _this=this;
this.mse = new MediaSource();
$("#videoPlayer")[0].src=window.URL.createObjectURL(this.mse);
this.mse.addEventListener('sourceopen', function(){
_this.ws=new WebSocket(_this.streamPlayUrl('mse'));
_this.ws.binaryType = "arraybuffer";
_this.ws.onopen = function(event) {
console.log('Connect to ws');
}
_this.ws.onmessage = function(event) {
var data = new Uint8Array(event.data);
if (data[0] == 9) {
decoded_arr=data.slice(1);
if (window.TextDecoder) {
mimeCodec = new TextDecoder("utf-8").decode(decoded_arr);
} else {
mimeCodec = Utf8ArrayToStr(decoded_arr);
}
console.log(mimeCodec);
_this.mseSourceBuffer = _this.mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"');
_this.mseSourceBuffer.mode = "segments"
_this.mseSourceBuffer.addEventListener("updateend", _this.pushPacket);
} else {
_this.readPacket(event.data);
}
};
}, false);
},
readPacket:function(packet){
if (!this.mseStreamingStarted) {
this.mseSourceBuffer.appendBuffer(packet);
this.mseStreamingStarted = true;
return;
}
this.mseQueue.push(packet);
if (!this.mseSourceBuffer.updating) {
this.pushPacket();
}
},
pushPacket:function(){
var _this=rtspPlayer;
if (!_this.mseSourceBuffer.updating) {
if (_this.mseQueue.length > 0) {
packet = _this.mseQueue.shift();
var view = new Uint8Array(packet);
_this.mseSourceBuffer.appendBuffer(packet);
} else {
_this.mseStreamingStarted = false;
}
}
},
streamPlayUrl:function(type){
switch (type) {
case 'hls':
return '/stream/' + this.uuid + '/hls/live/index.m3u8';
break;
case 'mse':
var potocol = 'ws';
if (location.protocol == 'https:') {
potocol = 'wss';
}
return potocol+'://'+location.host+'/stream/' + this.uuid +'/mse?uuid='+this.uuid;
//return 'ws://sr4.ipeye.ru/ws/mp4/live?name=d4ee855e40874ef7b7149357a42f18f0';
break;
case 'webrtc':
return "/stream/"+this.uuid+"/webrtc?uuid=" + this.uuid;
break;
default:
return '';
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,531 @@
$(document).ready(() => {
localImages();
if (localStorage.getItem('defaultPlayer') != null) {
$('input[name=defaultPlayer]').val([localStorage.getItem('defaultPlayer')]);
}
})
$('input[name=defaultPlayer]').on('change', function() {
localStorage.setItem('defaultPlayer', $(this).val());
})
var activeStream = null;
function showAddStream(streamName, streamUrl) {
streamName = streamName || '';
streamUrl = streamUrl || '';
Swal.fire({
title: 'Add stream',
html: '<form class="text-left"> ' +
'<div class="form-group">' +
'<label>Name</label>' +
'<input type="text" class="form-control" id="stream-name">' +
'<small class="form-text text-muted"></small>' +
'</div>' +
'<div class="form-group">' +
' <label>URL</label>' +
' <input type="text" class="form-control" id="stream-url">' +
' </div>' +
'<div class="form-group form-check">' +
'<input type="checkbox" class="form-check-input" id="stream-ondemand">' +
'<label class="form-check-label">ondemand</label>' +
'</div>' +
'</form>',
focusConfirm: true,
showCancelButton: true,
preConfirm: () => {
var uuid = randomUuid(),
name = $('#stream-name').val(),
url = $('#stream-url').val(),
ondemand = $('#stream-ondemand').val();
if (!validURL(url)) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'wrong url',
confirmButtonText: 'return back',
preConfirm: () => {
showAddStream(name, url)
}
})
} else {
goRequest('add', uuid, {
name: name,
url: url,
ondemand: ondemand
});
}
}
})
}
function showEditStream(uuid) {
console.log(streams[uuid]);
}
function deleteStream(uuid) {
activeStream = uuid;
Swal.fire({
title: 'Are you sure?',
text: "Do you want delete stream " + streams[uuid].name + " ?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, delete it!'
}).then((result) => {
if (result.value) {
goRequest('delete', uuid)
}
})
}
function renewStreamlist() {
goRequest('streams');
}
function goRequest(method, uuid, data) {
data = data || null;
uuid = uuid || null;
var path = '';
var type = 'GET';
switch (method) {
case 'add':
path = '/stream/' + uuid + '/add';
type = 'POST';
break;
case 'edit':
path = '/stream/' + uuid + '/edit';
type = 'POST';
break;
case 'delete':
path = '/stream/' + uuid + '/delete';
break;
case 'reload':
path = '/stream/' + uuid + '/reload';
break;
case 'info':
path = '/stream/' + uuid + '/info';
break;
case 'streams':
path = '/streams';
break;
default:
path = '';
type = 'GET';
}
if (path == '') {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'It`s goRequest function mistake',
confirmButtonText: 'Close',
})
return;
}
var ajaxParam = {
url: path,
type: type,
dataType: 'json',
beforeSend: function(xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa("demo:demo"));
},
success: function(response) {
goRequestHandle(method, response, uuid);
},
error: function(e) {
console.log(e);
}
};
if (data != null) {
ajaxParam.data = JSON.stringify(data);
}
$.ajax(ajaxParam);
}
function goRequestHandle(method, response, uuid) {
switch (method) {
case 'add':
if (response.status == 1) {
renewStreamlist();
Swal.fire(
'Added!',
'Your stream has been Added.',
'success'
);
} else {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Same mistake issset',
})
}
break;
case 'edit':
if (response.status == 1) {
renewStreamlist();
Swal.fire(
'Success!',
'Your stream has been modified.',
'success'
);
} else {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Same mistake issset',
})
}
break;
case 'delete':
if (response.status == 1) {
$('#' + uuid).remove();
delete(streams[uuid]);
$('#stream-counter').html(Object.keys(streams).length);
Swal.fire(
'Deleted!',
'Your stream has been deleted.',
'success'
)
}
break;
case 'reload':
break;
case 'info':
break;
case 'streams':
if (response.status == 1) {
streams = response.payload;
$('#stream-counter').html(Object.keys(streams).length);
if (Object.keys(streams).length > 0) {
$.each(streams, function(uuid, param) {
if ($('#' + uuid).length == 0) {
$('.streams').append(streamHtmlTemplate(uuid, param.name));
}
})
}
}
break;
default:
}
}
function getImageBase64(videoEl){
const canvas = document.createElement("canvas");
canvas.width = videoEl.videoWidth;
canvas.height = videoEl.videoHeight;
canvas.getContext('2d').drawImage(videoEl, 0, 0, canvas.width, canvas.height);
const dataURL = canvas.toDataURL();
canvas.remove();
return dataURL;
}
function downloadBase64Image(base64Data){
const a = document.createElement("a");
a.href = base64Data;
a.download = "screenshot.png";
a.click();
a.remove();
}
function makePic(video_element, uuid, chan) {
if (typeof(video_element) === "undefined") {
video_element = $("#videoPlayer")[0];
}
ratio = video_element.videoWidth / video_element.videoHeight;
w = 400;
h = parseInt(w / ratio, 10);
$('#canvas')[0].width = w;
$('#canvas')[0].height = h;
$('#canvas')[0].getContext('2d').fillRect(0, 0, w, h);
$('#canvas')[0].getContext('2d').drawImage(video_element, 0, 0, w, h);
var imageData = $('#canvas')[0].toDataURL();
var images = localStorage.getItem('imagesNew');
if (images != null) {
images = JSON.parse(images);
} else {
images = {};
}
var uid = $('#uuid').val();
if (!!uuid) {
uid = uuid;
}
var channel = $('#channel').val() || chan || 0;
if (typeof(images[uid]) === "undefined") {
images[uid] = {};
}
images[uid][channel] = imageData;
localStorage.setItem('imagesNew', JSON.stringify(images));
$('#' + uid).find('.stream-img[channel="' + channel + '"]').attr('src', imageData);
}
function localImages() {
var images = localStorage.getItem('imagesNew');
if (images != null) {
images = JSON.parse(images);
$.each(images, function(k, v) {
$.each(v, function(channel, img) {
$('#' + k).find('.stream-img[channel="' + channel + '"]').attr('src', img);
})
});
}
}
function clearLocalImg() {
localStorage.setItem('imagesNew', '{}');
}
function streamHtmlTemplate(uuid, name) {
return '<div class="item" id="' + uuid + '">' +
'<div class="stream">' +
'<div class="thumbs" onclick="rtspPlayer.livePlayer(0, \'' + uuid + '\')">' +
'<img src="../static/img/noimage.svg" alt="" class="stream-img">' +
'</div>' +
'<div class="text">' +
'<h5>' + name + '</h5>' +
'<p>property</p>' +
'<div class="input-group-prepend dropleft text-muted">' +
'<a class="btn" data-toggle="dropdown" >' +
'<i class="fas fa-ellipsis-v"></i>' +
'</a>' +
'<div class="dropdown-menu">' +
'<a class="dropdown-item" onclick="rtspPlayer.livePlayer(\'hls\', \'' + uuid + '\')" href="#">Play HLS</a>' +
'<a class="dropdown-item" onclick="rtspPlayer.livePlayer(\'mse\', \'' + uuid + '\')" href="#">Play MSE</a>' +
'<a class="dropdown-item" onclick="rtspPlayer.livePlayer(\'webrtc\', \'' + uuid + '\')" href="#">Play WebRTC</a>' +
'<div class="dropdown-divider"></div>' +
'<a class="dropdown-item" onclick="showEditStream(\'' + uuid + '\')" href="#">Edit</a>' +
'<a class="dropdown-item" onclick="deleteStream(\'' + uuid + '\')" href="#">Delete</a>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
}
function randomUuid() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
function validURL(url) {
//TODO: fix it
return true;
}
function Utf8ArrayToStr(array) {
var out, i, len, c;
var char2, char3;
out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 7:
out += String.fromCharCode(c);
break;
case 13:
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
}
function browserDetector() {
var Browser;
var ua = self.navigator.userAgent.toLowerCase();
var match =
/(edge)\/([\w.]+)/.exec(ua) ||
/(opr)[\/]([\w.]+)/.exec(ua) ||
/(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(iemobile)[\/]([\w.]+)/.exec(ua) ||
/(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(
ua
) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
(ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua)) ||
(ua.indexOf("compatible") < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua)) || [];
var platform_match =
/(ipad)/.exec(ua) ||
/(ipod)/.exec(ua) ||
/(windows phone)/.exec(ua) ||
/(iphone)/.exec(ua) ||
/(kindle)/.exec(ua) ||
/(android)/.exec(ua) ||
/(windows)/.exec(ua) ||
/(mac)/.exec(ua) ||
/(linux)/.exec(ua) ||
/(cros)/.exec(ua) || [];
var matched = {
browser: match[5] || match[3] || match[1] || "",
version: match[2] || match[4] || "0",
majorVersion: match[4] || match[2] || "0",
platform: platform_match[0] || ""
};
var browser = {};
if (matched.browser) {
browser[matched.browser] = true;
var versionArray = matched.majorVersion.split(".");
browser.version = {
major: parseInt(matched.majorVersion, 10),
string: matched.version
};
if (versionArray.length > 1) {
browser.version.minor = parseInt(versionArray[1], 10);
}
if (versionArray.length > 2) {
browser.version.build = parseInt(versionArray[2], 10);
}
}
if (matched.platform) {
browser[matched.platform] = true;
}
if (browser.chrome || browser.opr || browser.safari) {
browser.webkit = true;
} // MSIE. IE11 has 'rv' identifer
if (browser.rv || browser.iemobile) {
if (browser.rv) {
delete browser.rv;
}
var msie = "msie";
matched.browser = msie;
browser[msie] = true;
} // Microsoft Edge
if (browser.edge) {
delete browser.edge;
var msedge = "msedge";
matched.browser = msedge;
browser[msedge] = true;
} // Opera 15+
if (browser.opr) {
var opera = "opera";
matched.browser = opera;
browser[opera] = true;
} // Stock android browsers are marked as Safari
if (browser.safari && browser.android) {
var android = "android";
matched.browser = android;
browser[android] = true;
}
browser.name = matched.browser;
browser.platform = matched.platform;
return browser;
}
function addChannel() {
$('#streams-form-wrapper').append(chanellTemplate());
}
function chanellTemplate() {
let random = Math.ceil(Math.random() * 1000);
let html = `
<div class="col-12">
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Sub channel<small> parameters</small></h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" onclick="removeChannelDiv(this)"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="card-body">
<form class="stream-form">
<div class="form-group">
<label for="exampleInputPassword1">Substream url</label>
<input type="text" name="stream-url" class="form-control" placeholder="Enter stream url" >
<small class="form-text text-muted">Enter rtsp address as instructed by your camera. Look like <code>rtsp://&lt;ip&gt;:&lt;port&gt;/path </code> </small>
</div>
<div class="form-group">
<label for="inputStatus">Substream type</label>
<select class="form-control custom-select" name="stream-ondemand" >
<option selected disabled><small>Select One</small></option>
<option value="1">On demand only</option>
<option value="0">Persistent connection</option>
</select>
<small class="form-text text-muted">On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button </small>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" name="debug" id="substream-debug-switch-` + random + `" >
<label class="custom-control-label" for="substream-debug-switch-` + random + `">Enable debug</label>
</div>
<small class="form-text text-muted">Select this options if you want get more data about the stream </small>
</div>
</form>
</div>
</div>
</div>`;
return html;
}
function removeChannelDiv(element) {
$(element).closest('.col-12').remove();
}
function logger() {
if (!colordebug) {
return;
}
let colors = {
"0": "color:green",
"1": "color:#66CDAA",
"2": "color:blue",
"3": "color:#FF1493",
"4": "color:#40E0D0",
"5": "color:red",
"6": "color:red",
"7": "color:red",
"8": "color:red",
"9": "color:red",
"10": "color:red",
"11": "color:red",
"12": "color:red",
"13": "color:red",
"14": "color:red",
"15": "color:red",
}
console.log('%c%s', colors[arguments[0]], new Date().toLocaleString() + " " + [].slice.call(arguments).join('|'))
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,325 @@
/*!
* Bootstrap Reboot v4.5.1 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
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: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-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;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
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;
}
pre,
code,
kbd,
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: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
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;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
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;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.5.1 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::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}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save