@ -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 }}
|
||||
@ -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)
|
||||
|
||||
[](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
|
||||
)
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 298 KiB |
|
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 |
|
After Width: | Height: | Size: 411 KiB |
|
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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://<ip>:<port>/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('|'))
|
||||
}
|
||||
@ -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 */
|
||||
@ -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 */
|
||||