Add v16.7.2

This commit is contained in:
Jan Stabenow
2022-05-13 19:26:45 +02:00
parent 8e70517dff
commit 9c0b535199
2368 changed files with 687657 additions and 1 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
.env
/data/**
/test/**
Dockerfile*
*.md
.github/*
.github_build/*

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
# For more information about the properties used in
# this file, please see the EditorConfig documentation:
# http://editorconfig.org/
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = outside
[*.md]
trim_trailing_whitespace = false
indent_style = space
[*.patch]
indent_style = space

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
.DS_Store
.env
/core*
/import*
/data/**
/test/**
.vscode
*.ts
*.ts.tmp
*.m3u8
*.mp4
*.avi
*.flv
.VSCodeCounter

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
ARG GOLANG_IMAGE=golang:1.17.6-alpine3.15
ARG BUILD_IMAGE=alpine:3.15
FROM $GOLANG_IMAGE as builder
COPY . /dist/core
RUN apk add \
git \
make && \
cd /dist/core && \
go version && \
make release && \
make import
FROM $BUILD_IMAGE
COPY --from=builder /dist/core/core /core/bin/core
COPY --from=builder /dist/core/import /core/bin/import
COPY --from=builder /dist/core/mime.types /core/mime.types
COPY --from=builder /dist/core/run.sh /core/bin/run.sh
RUN mkdir /core/config /core/data
ENV CORE_CONFIGFILE=/core/config/config.json
ENV CORE_STORAGE_DISK_DIR=/core/data
ENV CORE_DB_DIR=/core/config
VOLUME ["/core/data", "/core/config"]
ENTRYPOINT ["/core/bin/run.sh"]
WORKDIR /core

33
Dockerfile.bundle Normal file
View File

@@ -0,0 +1,33 @@
ARG GOLANG_IMAGE=golang:1.18.2-alpine3.15
ARG FFMPEG_IMAGE=datarhei/base:alpine-ffmpeg-3.15-4.4.2
FROM $GOLANG_IMAGE as builder
COPY . /dist/core
RUN apk add \
git \
make && \
cd /dist/core && \
go version && \
make release && \
make import
FROM $FFMPEG_IMAGE
COPY --from=builder /dist/core/core /core/bin/core
COPY --from=builder /dist/core/import /core/bin/import
COPY --from=builder /dist/core/mime.types /core/mime.types
COPY --from=builder /dist/core/run.sh /core/bin/run.sh
RUN ffmpeg -buildconf && \
mkdir /core/config /core/data
ENV CORE_CONFIGFILE=/core/config/config.json
ENV CORE_STORAGE_DISK_DIR=/core/data
ENV CORE_DB_DIR=/core/config
VOLUME ["/core/data", "/core/config"]
ENTRYPOINT ["/core/bin/run.sh"]
WORKDIR /core

18
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,18 @@
pipeline {
agent any
triggers {
cron('H 0 * * 6')
}
options {
buildDiscarder(logRotator(numToKeepStr: '5', artifactNumToKeepStr: '5'))
}
stages {
stage('Build AMD64') {
steps {
sh 'docker build -f Dockerfile -t docker-registry.marathon.l4lb.thisdcos.directory:5000/core:amd64 .'
sh 'docker push docker-registry.marathon.l4lb.thisdcos.directory:5000/core:amd64'
sh 'docker rmi docker-registry.marathon.l4lb.thisdcos.directory:5000/core:amd64'
}
}
}
}

83
Makefile Normal file
View File

@@ -0,0 +1,83 @@
COMMIT := $(shell if [ -d .git ]; then git rev-parse HEAD; else echo "unknown"; fi)
SHORTCOMMIT := $(shell echo $(COMMIT) | head -c 7)
BRANCH := $(shell if [ -d .git ]; then git rev-parse --abbrev-ref HEAD; else echo "master"; fi)
BUILD := $(shell date -u "+%Y-%m-%dT%H:%M:%SZ")
OSARCH := $(shell if [ "${GOOS}" -a "${GOARCH}" ]; then echo "-${GOOS}-${GOARCH}"; else echo ""; fi)
all: build
## build: Build core (default)
build:
go build -o core$(OSARCH)
## swagger: Update swagger API documentation (requires github.com/swaggo/swag)
swagger:
swag init -g http/server.go
## gqlgen: Regenerate GraphQL server from schema
gqlgen:
go run github.com/99designs/gqlgen generate --config http/graph/gqlgen.yml
## test: Run all tests
test:
go test -coverprofile=/dev/null ./...
## vet: Analyze code for potential errors
vet:
go vet ./...
## fmt: Format code
fmt:
go fmt ./...
## update: Update dependencies
update:
go get -u
@-$(MAKE) tidy
## tidy: Tidy up go.mod
tidy:
go mod tidy
## vendor: Update vendored packages
vendor:
go mod vendor
## run: Build and run core
run: build
./core
## lint: Static analysis with staticcheck
lint:
staticcheck ./...
## import: Build import binary
import:
cd app/import && go build -o ../../import -ldflags="-s -w"
## coverage: Generate code coverage analysis
coverage:
go test -coverprofile test/cover.out ./...
go tool cover -html=test/cover.out -o test/cover.html
## commit: Prepare code for commit (vet, fmt, test)
commit: vet fmt lint test build
@echo "No errors found. Ready for a commit."
## release: Build a release binary of core
release:
go build -o core -ldflags="-s -w -X github.com/datarhei/core/app.Commit=$(COMMIT) -X github.com/datarhei/core/app.Branch=$(BRANCH) -X github.com/datarhei/core/app.Build=$(BUILD)"
## docker: Build standard Docker image
docker:
docker build -t core:$(SHORTCOMMIT) .
.PHONY: help build swagger test vet fmt vendor commit coverage lint release import
## help: Show all commands
help: Makefile
@echo
@echo " Choose a command:"
@echo
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
@echo

980
README.md
View File

@@ -1 +1,979 @@
# core
# Core
The cloud-native a/v media processor.
datarhei Core is management for FFmpeg processes without development effort. It is a central interface for mapping AV processes, is responsible for design and management, and provides all necessary interfaces to access the video content. The included control for FFmpeg can keep all used functions reliable and executable without the need for software developers to take care of it. In addition, process and resource limitation for all FFmpeg processes protects the host system from application overload. The overall system gives access to current process values (CPU, RAM) and complete control of system resources and loads with statistical access to process data and current and historical logs.
## Features
- Unrestricted FFmpeg process management
- Optimized for long-running tasks
- In-Memory- and Disk-Filesystem for media assets
- HTTP and RTMP services
- Let's Encrypt for HTTPS and RTMPS
- HLS/DASH Session tracking with bandwidth and current viewer limiters
- Multiple resource limiters and monitoring
- FFmpeg progress data
- Metrics incl. Prometheus support
- Logging and debugging for FFmpeg processes with history
- Multiple auth. by JWT and Auth0
- 100% JSON REST API (Swagger documented)
- GraphQL for metrics, process, and progress data
## Quick start
1. Run the Docker image
```sh
docker run --name core -d
-e CORE_API_AUTH_USERNAME=admin \
-e CORE_API_AUTH_PASSWORD=secret \
-p 8080:8080 \
-v ${HOME}/core/config:/core/config \
-v ${HOME}/core/data:/core/data \
datarhei/core:latest
```
2. Open Swagger
http://host-ip:8080/api/swagger/index.html
3. Log in with Swagger
Authorize > Basic authorization > Username: admin, Password: secret
## Docker images
Native (linux/amd64,linux/arm64,linux/arm/v7)
- datarhei/base:alpine-core-latest
- datarhei/base:ubuntu-core-latest
Bundle with FFmpeg (linux/amd64,linux/arm64,linux/arm/v7)
- datarhei/core:latest
Bundle with FFmpeg for Raspberry Pi (linux/arm/v7)
- datarhei/core:rpi-latest
Bundle with FFmpeg for Nvidia Cuda (linux/amd64)
- datarhei/core:cuda-latest
Bundle with FFmpeg for Intel VAAPI (linux/amd64)
- datarhei/core:vaapi-latest
## Documentation
## Environment variables
The environment variables can be set in the file `.env`, e.g.
```
CORE_API_AUTH_USERNAME=admin
CORE_API_AUTH_PASSWORD=datarhei
...
```
You can also provide them on the command line, whatever you prefer. If the same environment variable is set
in the `.env` file and on the command line, the one set on the command line will overrule the one from the `.env` file.
The currently known environment variables (but not all will be respected) are:
| Name | Default | Description |
| ----------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| CORE_CONFIGFILE | (not set) | Path to a config file. The following environment variables will override the respective values in the config file. |
| CORE_ADDRESS | `:8080` | HTTP listening address. |
| CORE_LOG_LEVEL | `info` | silent, error, warn, info, debug. |
| CORE_LOG_TOPICS | (not set) | List of topics to log (comma separated) |
| CORE_LOG_MAXLINES | `1000` | Number of latest log lines to keep in memory. |
| CORE_DB_DIR | `.` | Directory for holding the operational data. This directory must exist. |
| CORE_HOST_NAME | (not set) | Set to the domain name of the host this instance is running on. |
| CORE_HOST_AUTO | `true` | Enable detection of public IP addresses. |
| CORE_API_READ_ONLY | `false` | Allow only ready only access to the API |
| CORE_API_ACCESS_HTTP_ALLOW | (not set) | Comma separated list of IP ranges in CIDR notation (HTTP traffic), e.g. `127.0.0.1/32,::1/128`. |
| CORE_API_ACCESS_HTTP_BLOCK | (not set) | Comma separated list of IP ranges in CIDR notation (HTTP traffic), e.g. `127.0.0.1/32,::1/128`. |
| CORE_API_ACCESS_HTTPS_ALLOW | (not set) | Comma separated list of IP ranges in CIDR notation (HTTPS traffic), e.g. `127.0.0.1/32,::1/128`. |
| CORE_API_ACCESS_HTTPS_BLOCK | (not set) | Comma separated list of IP ranges in CIDR notation (HTTPS traffic), e.g. `127.0.0.1/32,::1/128`. |
| CORE_API_AUTH_ENABLE | `true` | Set to `false` to disable auth for all clients. |
| CORE_API_AUTH_DISABLE_LOCALHOST | `false` | Set to `true` to disable auth for clients from localhost. |
| CORE_API_AUTH_USERNAME | (required) | Username for auth. |
| CORE_API_AUTH_PASSWORD | (required) | Password for auth. |
| CORE_API_AUTH_JWT_SECRET | (not set) | A secret for en- and decrypting the JWT. If not set, a secret will be generated. |
| CORE_API_AUTH_AUTH0_ENABLE | `false` | Enable Auth0. |
| CORE_API_AUTH_AUTH0_TENANTS | (not set) | List of base64 encoded Auth0 tenant JSON objects (comma-separated). The tenant JSON object is defined as `{"domain":string,"audience":string,"users":array of strings}` |
| CORE_TLS_ADDRESS | `:8181` | Port to listen on for HTTPS requests. |
| CORE_TLS_ENABLE | `false` | Set to `true` to enable TLS support. |
| CORE_TLS_AUTO | `false` | Set to `true` to enable automatic retrieval of a Let's Encrypt certificate. Requires `CORE_TLS_ENABLE` to be `true` and `CORE_HOST_NAME` to be set with `CORE_HOST_AUTO` to `false`. |
| CORE_TLS_CERTFILE | (not set) | TLS certificate file in PEM format. |
| CORE_TLS_KEYFILE | (not set) | TLS key file in PEM format. |
| CORE_STORAGE_DISK_DIR | `.` | A directory that will be exposed by HTTP on /. This directory must exist. |
| CORE_STORAGE_DISK_MAXSIZEMBYTES | `0` | Max. allowed megabytes for `CORE_STORAGE_DISK_DIR`. |
| CORE_STORAGE_DISK_CACHE_ENABLE | `true` | Enable cache for files from `CORE_STORAGE_DISK_DIR`. |
| CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES | `0` | Max. allowed cache size, 0 for unlimited. |
| CORE_STORAGE_DISK_CACHE_TTLSECONDS | `300` | Seconds to keep files in cache. |
| CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES | `1` | Max. file size to put in cache. |
| CORE_STORAGE_DISK_CACHE_TYPES | (not set) | List of file extensions to cache (space-separated, e.g. ".html .js"), empty for all. |
| CORE_STORAGE_MEMORY_AUTH_ENABLE | `true` | Enable basic auth for PUT,POST, and DELETE on /memfs. |
| CORE_STORAGE_MEMORY_AUTH_USERNAME | (not set) | Username for Basic-Auth of `/memfs`. Required if auth is enabled. |
| CORE_STORAGE_MEMORY_AUTH_PASSWORD | (not set) | Password for Basic-Auth of `/memfs`. Required if auth is enabled. |
| CORE_STORAGE_MEMORY_MAXSIZEMBYTES | `0` | Max. allowed megabytes for `/memfs`. Any value <= 0 means unlimited. |
| CORE_STORAGE_MEMORY_PURGE | `false` | Set to `true` to remove the oldest entries if the `/memfs` is full. |
| CORE_STORAGE_COCORE_ORIGINS | `*` | List of allowed CORS origins (comma separated). Will be used for `/` and `/memfs`. |
| CORE_STORAGE_MIMETYPES_FILE | `mime.types` | Path to file with MIME type definitions. |
| CORE_RTMP_ENABLE | `false` | Enable RTMP server. |
| CORE_RTMP_ENABLE_TLS | `false` | Enable RTMP over TLS (RTMPS). Requires `CORE_TLS_ENABLE` to be `true`. |
| CORE_RTMP_ADDRESS | `:1935` | RTMP server listen address. |
| CORE_RTMP_APP | `/` | RTMP app for publishing. |
| CORE_RTMP_TOKEN | (not set) | RTMP token for publishing and playing. The token is the value of the URL query parameter `token`. |
| CORE_FFMPEG_BINARY | `ffmpeg` | Path to FFmpeg binary. |
| CORE_FFMPEG_MAXPROCESSES | `0` | Max. allowed simultaneously running FFmpeg instances. Any value <= 0 means unlimited. |
| CORE_FFMPEG_ACCESS_INPUT_ALLOW | (not set) | List of pattern for allowed input URI (space-separated), leave emtpy to allow any. |
| CORE_FFMPEG_ACCESS_INPUT_BLOCK | (not set) | List of pattern for blocked input URI (space-separated), leave emtpy to block none. |
| CORE_FFMPEG_ACCESS_OUTPUT_ALLOW | (not set) | List of pattern for allowed output URI (space-separated), leave emtpy to allow any. |
| CORE_FFMPEG_ACCESS_OUTPUT_BLOCK | (not set) | List of pattern for blocked output URI (space-separated), leave emtpy to block none. |
| CORE_FFMPEG_LOG_MAXLINES | `50` | Number of latest log lines to keep for each process. |
| CORE_FFMPEG_LOG_MAXHISTORY | `3` | Number of latest logs to keep for each process. |
| CORE_PLAYOUT_ENABLE | `false` | Enable playout API where available |
| CORE_PLAYOUT_MINPORT | `0` | Min. port a playout server per input can run on. |
| CORE_PLAYOUT_MAXPORT | `0` | Max. port a playout server per input can run on. |
| CORE_DEBUG_PROFILING | `false` | Set to `true` to enable profiling endpoint on `/profiling`. |
| CORE_DEBUG_FORCEGC | `0` | Number of seconds between forcing GC to return memory to the OS. Use in conjuction with `GODEBUG=madvdontneed=1`. Any value <= 0 means not to force GC. |
| CORE_METRICS_ENABLE | `false` | Enable collecting historic metrics data. |
| CORE_METRICS_ENABLE_PROMETHEUS | `false` | Enable prometheus endpoint /metrics. |
| CORE_METRICS_RANGE_SECONDS | `300` | Seconds to keep history metric data. |
| CORE_METRICS_INTERVAL_SECONDS | `2` | Interval for collecting metrics. |
| CORE_SESSIONS_ENABLE | `false` | Enable HLS statistics for `/memfs`. |
| CORE_SESSIONS_IP_IGNORELIST | (not set) | Comma separated list of IP ranges in CIDR notation, e.g. `127.0.0.1/32,::1/128`. |
| CORE_SESSIONS_SESSION_TIMEOUT_SEC | `30` | Timeout of a session in seconds. |
| CORE_SESSIONS_PERSIST | `false` | Whether to persist the session history. Will be stored in `CORE_DB_DIR`. |
| CORE_SESSIONS_MAXBITRATE_MBIT | `0` | Max. allowed outgoing bitrate in mbit/s, 0 for unlimited. |
| CORE_SESSIONS_MAXSESSIONS | `0` | Max. allowed number of simultaneous sessions, 0 for unlimited. |
| CORE_ROUTER_BLOCKED_PREFIXES | `/api` | List of path prefixes that can't be routed. |
| CORE_ROUTER_ROUTES | (not set) | List of route mappings of the form [from]:[to], e.g. `/foo:/bar`. Leave empty for no routings. |
| CORE_ROUTER_UI_PATH | (not set) | Path to directory with files for a UI. It will be mounted to `/ui` and uses `index.html` as default index page. |
## Config
The minimum config file has to look like this:
```
{
"version": 1
}
```
All other values will be filled with default values and persisted on disk. The entire default config file:
```
{
"version": 1,
"id": "[will be generated if not given]",
"name": "[will be generated if not given]",
"address": ":8080",
"log": {
"level": "info",
"topics": [],
"max_lines": 1000
},
"db": {
"dir": "./config"
},
"host": {
"name": [],
"auto": true
},
"api": {
"read_only": false,
"access": {
"http": {
"allow": [],
"block": []
},
"https": {
"allow": [],
"block": []
}
},
"auth": {
"enable": true,
"disable_localhost": false,
"username": "",
"password": "",
"jwt": {
"secret": ""
},
"auth0": {
"enable": false,
"tenants": []
}
}
},
"tls": {
"address": ":8181",
"enable": false,
"auto": false,
"cert_file": "",
"key_file": ""
},
"storage": {
"disk": {
"dir": "./data",
"max_size_mbytes": 0,
"cache": {
"enable": true,
"max_size_mbytes": 0,
"ttl_seconds": 300,
"max_file_size_mbytes": 1,
"types": []
}
},
"memory": {
"auth": {
"enable": true,
"username": "admin",
"password": "vxbx0ViqfA75P1KCyw"
},
"max_size_mbytes": 0,
"purge": false
},
"cors": {
"origins": [
"*"
]
},
"mimetypes_file": "mime.types"
},
"rtmp": {
"enable": false,
"enable_tls": false,
"address": ":1935",
"app": "/",
"token": ""
},
"ffmpeg": {
"binary": "ffmpeg",
"max_processes": 0,
"access": {
"input": {
"allow": [],
"block": []
},
"output": {
"allow": [],
"block": []
}
},
"log": {
"max_lines": 50,
"max_history": 3
}
},
"playout": {
"enable": false,
"min_port": 0,
"max_port": 0
},
"debug": {
"profiling": false,
"force_gc": 0
},
"stats": {
"enable": true,
"ip_ignorelist": [
"127.0.0.1/32",
"::1/128"
],
"session_timeout_sec": 30,
"persist": false,
"persist_interval_sec": 300,
"max_bitrate_mbit": 0,
"max_sessions": 0
},
"service": {
"enable": false,
"token": "",
"url": "https://service.datarhei.com"
},
"router": {
"blocked_prefixes": [
"/api"
],
"routes": {}
}
}
```
If you don't provide a path to a config file, the default config will be used, and nothing will be persisted to the disk. Default values can be overruled by environment variables.
## TLS / HTTPS
Enable TLS / HTTPS support by setting `CORE_TLS_ENABLE=true` and provide the certificate file and key file in PEM format by setting the environment variables `CORE_TLS_CERTFILE` and `CORE_TLS_KEYFILE` accordingly. If a certificate authority signs the certificate, the certificate file should be the concatenation of the server's certificate, any intermediates, and the CA's certificate.
If TLS with given certificates is enabled, an HTTP server listening on `CORE_ADDRESS` (address) will be additionally started. This server provides access to the same memory filesystem as the HTTPS server (including limits and authorization), but its access is restricted to localhost only.
### Let's Encrypt
If you want to use automatic certificates from Let's Encrypt, set the environment variable `CORE_TLS_AUTO` to `true.` To work, the
environment variables `CORE_TLS_ENABLE` have to be `true,` and `CORE_HOST_NAME` has to be set to the host this host will be reachable. Otherwise, the ACME challenge will not work. The environment variables `CORE_TLS_CERTFILE` and `CORE_TLS_KEYFILE` will be ignored.
If automatic TLS is enabled, the HTTP server (CORE_ADDRESS, resp. address) must listen on port 80. It is required to automatically acquire the certificate (serving the `HTTP-01` challenge). As a further requirement, `CORE_HOST_NAME` (host.name) must be set because it is used a the canonical name for the certificate.
The obtained certificates will be stored in `CORE_DB_DIR/cert` to be available after a restart.
The obtained certificates will be stored in `CORE_DB_DIR/cert` to be available after a restart.
### Self-Signed certificates
To create a self-signed certificate and key file pair, run this command and provide a reasonable value for the Common Name (CN). The CN is the fully qualified name of the host the instance is running on (e.g., `localhost`). You can also use an IP address or a wildcard name, e.g., `*.example.com`.
RSA SSL certificate
```sh
openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem -subj '/CN=localhost'
```
ECDSA SSL certificate
```sh
openssl ecparam -name secp521r1 -genkey -out key.pem
openssl req -new -x509 -key key.pem -out cert.pem -days 365 -subj '/CN=localhost'
```
Call `openssl ecparam -list_curves` to see all available supported curves listed.
## Access Control
To control who has access to the API, a list of allowed IPs can be defined. This list is provided at startup with the environment variables `CORE_API_ACCESS_HTTP_BLOCK` and `CORE_API_ACCESS_HTTP_ALLOW.` This is a comma-separated list of IPs in CIDR notation,
e.g. `127.0.0.1/32,::1/128`. If the list is empty, then all IPs are allowed. If the list contains any invalid IP range, the server
will refuse to start. This can be separately defined for the HTTP and HTTPS server if you have TLS enabled with the environment variables `CORE_API_ACCESS_HTTPS_BLOCK` and `CORE_API_ACCESS_HTTPS_ALLOW.`
## Input/Output Control
To control where FFmpeg can read and where FFmpeg can write, you can define a pattern that matches the
input addresses or the output addresses. These patterns are regular expressions that can be provided at startup with the
environment variables `CORE_FFMPEG_ACCESS_INPUT` and `CORE_FFMPEG_ACCESS_OUTPUT.` The expressions need to be space-separated, e.g.
`HTTPS?:// RTSP:// RTMP://`. If one of the lists is empty, then no restriction on input, resp. The output will be applied.
Independently of the value of `CORE_FFMPEG_ACCESS_OUTPUT` there's a check that verifies that output can only be written to the specified `CORE_STORAGE_DISK_DIR` and works as follows: If the address has a protocol specifier other than `file:,` then no further checks will be applied. If the protocol is `file:` or no protocol specifier is given, the address is assumed to be a path that is checked against the path shown in `CORE_STORAGE_DISK_DIR.`
It will be rejected if the address is outside the `CORE_STORAGE_DISK_DIR` directory. Otherwise, the protocol `file:` will be prepended. If you give some expressions for `CORE_FFMPEG_ACCESS_OUTPUT,` you should also allow `file:.`
Special cases are the output addresses `-` (which will be rewritten to `pipe:`), and `/dev/null` (which will be allowed even though it's outside of `CORE_STORAGE_DISK_DIR`).
If you set a value for `CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES`, which is larger than `0`, it will be interpreted as max—allowed megabytes for the `CORE_STORAGE_DISK_DIR.` As soon as the limit is reached, all processes that have outputs writing to `CORE_STORAGE_DISK_DIR` will be stopped. You are responsible for cleaning up the directory and restarting these processes.
## RTMP
The datarhei Core includes a simple RTMP server for publishing and playing streams. Set the environment variable `CORE_RTMP_ENABLE` to `true` to enable the RTMP server. It is listening on `CORE_RTMP_ADDRESS.` Use `CORE_RTMP_APP` to limit the app a stream can be published on, e.g. `/live` to require URLs to start with `/live`. To prevent anybody can publish streams, set `CORE_RTMP_TOKEN` to a secret only known to the publishers. The token has to be put in the query of the stream URL, e.g. `/live/stream?token=...`.
| Method | Path | Description |
| ------ | ------------ | ------------------------------------- |
| GET | /api/v3/rtmp | List all currently published streams. |
## Playout
FFmpeg processes with a `avstream:` (or `playout:`) input stream can expose an HTTP API to control the playout of that stream. With
`CORE_PLAYOUT_ENABLE` you enable exposing this API. The API is only exposed to `localhost` and is transparently connected to the datarhei Core API. You have to provide a port range (`CORE_PLAYOUT_MINPORT` and `CORE_PLAYOUT_MAXPORT`) where datarhei/core can use ports to assign it to the playout API.
| Method | Path | Description |
| -------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET | /api/v3/process/:id/playout/:inputid/status | Retrieve the current status as JSON. |
| GET | /api/v3/process/:id/playout/:inputid/keyframe/\*name | Retrieve the last deliverd key frame from the input stream as JPEG (if `name` has the ending `.jpg`) or PNG (if `name` has the ending `.png`). |
| GET | /api/v3/process/:id/playout/:inputid/errorframe/encode | Immediately encode the error frame to a GOP. Will only have an effect if the last key frame is currently in a loop. |
| PUT/POST | /api/v3/process/:id/playout/:inputid/errorframe/\*name | Upload any image or video media that can be decoded and will be used to replace the key frame loop. If the key frame is currently in a loop, it will be repaced immediately. Otherwise, it will be used the next time the key frame is in a loop. The body of the request is the media file. |
| PUT | /api/v3/process/:id/playout/:inputid/stream | Replace the current stream. The body of the request is the URL of the new stream. |
## MIME Types
The file with the MIME types has one MIME type per line followed by a list of file extensions (including the ".").
```
text/plain .txt
text/html .htm .html
...
```
## Memory Filesystem
AA very simple in-memory filesystem is available. The uploaded data is stored in a map, where the path used to upload the file
is used as the key. Use the `POST` or `PUT` method with the proper direction for uploading a file. The body of the request contains the contents of the file. No particular encoding or `Content-Type` is required. The file can then be downloaded from the same path.
| Method | Path | Description |
| ------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| POST | /memfs/\*path | Upload a file to the memory filesystem. The filename is `path` which can contain slashes. If there's already a file with the same `path`, it will be overwritten. |
| PUT | /memfs/\*path | Same as POST. |
| GET | /memfs/\*path | Download the file stored under `path`. The MIME types are applied based on the extension in the `path`. |
| DELETE | /memfs/\*path | Delete the file stored under `path`. |
| POST | /api/v3/memfs/\*path | Upload a file to the memory filesystem. |
| PUT | /api/v3/memfs/\*path | Same as POST. |
| GET | /api/v3/memfs/\*path | Download the file stored under `path`. |
| PATCH | /api/v3/memfs/\*path | Create a link to a file. The body contains the path to that file. |
| DELETE | /api/v3/memfs/\*path | Delete the file stored under `path`. |
| GET | /api/v3/memfs | List all files that are currently stored in the in-memory filesystem. |
Use these endpoints to, e.g., store HLS chunks and .m3u8 files (in contrast to an actual disk or a ramdisk):
```
ffmpeg -f lavfi -re -i testsrc2=size=640x480:rate=25 -c:v libx264 -preset:v ultrafast -r 25 -g 50 -f hls -start_number 0 -hls_time 2 -hls_list_size 6 -hls_flags delete_segments+temp_file+append_list -method PUT -hls_segment_filename http://localhost:8080/memfs/foobar_%04d.ts -y http://localhost:8080/memfs/foobar.m3u8
```
Then you can play it generally with, e.g., `ffplay http://localhost:3000/memfs/foobar.m3u8`.
Use the environment variables `CORE_STORAGE_MEMORY_AUTH_USERNAME` and `CORE_STORAGE_MEMORY_AUTH_PASSWORD` to protect the `/memfs` with Basic-Auth. Basic-Auth will only be enabled
if both environment variables are set to non-empty values. The `GET /memfs/:path` will not be protected with Basic-Auth.
Use the environment variable `CORE_STORAGE_MEMORY_MAXSIZEMBYTES` to limit the amount of data that is allowed to be stored. The value is interpreted as megabytes. Use a value equal to or smaller than `0` not to impose any limits. A `507 Insufficient Storage` will be returned if you hit the limit.
Listing all currently stored files is done by calling `/v3/memfs` with the credentials set by the environment variables `CORE_API_AUTH_USERNAME` and `CORE_API_AUTH_PASSWORD`.
It also accepts the query parameter `sort` (`name,` `size,` or `lastmod`) and `order` (`asc` or `desc`). If a valid value for `sort` is given, the results are sorted in ascending order.
## Routes
All contents in `CORE_STORAGE_DISK_DIR` are served from `/.` If you want to redirect some paths to an existing file, you can add static routes in `router.routes` by providing a direct mapping, e.g.
```
router: {
routes: {
"/foo.txt": "/bar.txt",
}
}
```
The paths have to start with a `/.` Alternatively, you can serve whole directories from another root than `CORE_STORAGE_DISK_DIR.` Use a `/*` at the end of a path as key and a path on the filesystem as the target, e.g.
```
router: {
routes: {
"/ui/*": "/path/to/ui",
}
}
```
If you use a relative path as target, then it will be added to the current working directory.
## API
Check the detailed API description on `/api/swagger/index.html`.
### Login / Auth
With auth enabled, you have to retrieve a JWT/OAuth token before you can access the `/v3/` API calls.
| Method | Path | Description |
| ------ | --------------------- | ---------------------------------------------- |
| POST | /api/login | Retrieve a token to access the API. |
| GET | /api/v3/refresh_token | Retrieve a fresh token with a new expiry date. |
For the login you have to send
```
{
"username": "...",
"password": "..."
}
```
The `username` and the `password` are set by the environment variables `CORE_API_AUTH_USERNAME` and `CORE_API_AUTH_PASSWORD`.
On successful login, the response looks like this:
```
{
"expire": "2019-01-18T19:55:55+01:00",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDc4Mzc3NTUsImlkIjpudWxsLCJvcmlnX2lhdCI6MTU0NzgzNDE1NX0.ZcrpD4oRBqG3wUrfnh1DOVpXdUT7dvUnvetKFEVRKKc"
}
```
Use the `token` in all subsequent calls to the `/api/v3/` endpoints, e.g.
```
http http://localhost:8080/api/v3/process "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDc4Mzc3NTUsImlkIjpudWxsLCJvcmlnX2lhdCI6MTU0NzgzNDE1NX0.ZcrpD4oRBqG3wUrfnh1DOVpXdUT7dvUnvetKFEVRKKc"
```
### Config
| Method | Path | Description |
| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------- |
| GET | /api/v3/config | Retrieve the current config without the override values from the environment variables. |
| GET | /api/v3/config/active | Retrieve the current config with the override values from the environment variables are taken into account. |
| PUT | /api/v3/config | Store a new config. Only some values are respected, and the new config will only be used after a restart. |
| GET | /api/v3/config/reload | Reload the config. The config will be re-read and validated from the store. It will cause a restart of all services. |
When retrieving the config via the API, critical values (such as passwords) will be disguised if not required otherwise.
### Process
With the process API call, you can manage different FFmpeg processes. A process is defined as:
```
{
"id": "SomeId",
"reference": "SomeId",
"type": "ffmpeg",
"input": [
{
"id": "inputid",
"address": "rtsp://1.2.3.4/stream.sdp",
"options": [
... list of input options ...
]
},
... list of inputs ...
],
"output": [
{
"id": "outputid",
"address": "rtmp://rtmp.youtube.com/live2/...",
"options": [
... list of output options ...
],
"cleanup": [{
"pattern": "(memfs|diskfs):...",
"max_files: "number,
"max_file_age_seconds": "number"
}]
},
... list of outputs ...
],
"options": [
... list of global options ...
],
"reconnect": (true|false),
"reconnect_delay_seconds": 10,
"autostart": (true|false),
"stale_timeout_seconds": 30
}
```
The input, output, and global options are interpreted as command-line options for FFmpeg.
#### Process Cleanup
With the optional array of cleanup rules for each output, it is possible to define rules for removing files from the
memory filesystem or disk. Each rule consists of a glob pattern and a max. allowed number of files matching that pattern or
permitted maximum age for the files matching that pattern. The pattern starts with either `memfs:` or `diskfs:` depending on
which filesystem this rule is designated to. Then a [glob pattern](https://pkg.go.dev/path/filepath#Match) follows to
identify the files. If `max_files` is set to a number > 0, then the oldest files from the matching files will be deleted if
the list of matching files is longer than that number. If `max_file_age_seconds` is set to a number > 0, then all files
that are older than this number of seconds from the matching files will be deleted.
The API calls are
| Method | Path | Description |
| ------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| POST | /api/v3/process | Adds a process. Overwriting an existing ID will result in an error. |
| GET | /api/v3/process | Retrieve a list of all known processes. Use the query parameter `ids` to list (comma separated) the IDs of the process you want to be part of the response. If the list is empty, all processes will be listed. Use the query parameter `filter` to list (comma separated) the wanted details per process (`config`, `state`, `log`). If the list is empty, all details will be included. |
| GET | /api/v3/process/:id | Retreive the details of a process including the config, state, and logs. Use the query parameter `filter` to list (comma separated) the wanted details per process (`config`, `state`, `log`). If the list is empty, all details will be included. |
| PUT | /api/v3/process/:id | Replaces the process with a new config. |
| GET | /api/v3/process/:id/config | Retrieve the config of a process as it was provided. |
| GET | /api/v3/process/:id/state | Retrieve the current state of a process. This includes the progress data if the process is running. |
| GET | /api/v3/process/:id/report | Retrieve the report and logs of a process. |
| GET | /api/v3/process/:id/debug | Retrieve an anonymized version of the details of a process. |
| DELETE | /api/v3/process/:id | Remove a specific process. Only possible if the process is not running. |
| PUT | /api/v3/process/:id/command | Send a command to a process. |
| GET | /api/v3/process/:id/data | Get all arbitrary JSON data that is stored with this process. |
| GET | /api/v3/process/:id/data/:key | Get arbitrary JSON data that is stored under the key `key.` |
| PUT | /api/v3/process/:id/data/:key | Store aribtrary JSON data under the key `key.` If the data is `null,` the key will be removed. |
### Commands
A command is defined as:
```
{
"command": ("start"|"stop"|"restart"|"reload")
}
```
| Command | Description |
| --------- | ---------------------------------------------------------------------------------------------- |
| `start` | Start the process. If the process is already started, this won't have any effect. |
| `stop` | Stop the process. If the process is already stopped, this won't have any effect. |
| `restart` | Restart the process. If the process is not running, this won't have any effect. |
| `reload` | Reload the process. If the process was running, the reloaded process will start automatically. |
### Placeholder
Currently supported placeholders are:
| Placeholder | Description | Location |
| ------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `{diskfs}` | Will be replaced by the provided `CORE_STORAGE_DISK_DIR`. | `options`, `input.address`, `input.options`, `output.address`, `output.options` |
| `{memfs}` | Will be replace by the base URL of the MemFS. | `input.address`, `input.options`, `output.address`, `output.options` |
| `{processid}` | Will be replaced by the ID of the process. | `input.id`, `input.address`, `input.options`, `output.id`, `output.address`, `output.options`, `output.cleanup.pattern` |
| `{reference}` | Will be replaced by the reference of the process | `input.id`, `input.address`, `input.options`, `output.id`, `output.address`, `output.options`, `output.cleanup.pattern` |
| `{inputid}` | Will be replaced by the ID of the input. | `input.address`, `input.options` |
| `{outputid}` | Will be replaced by the ID of the output. | `output.address`, `output.options`, `output.cleanup.pattern` |
Before replacing the placeholder in the process, all references will be resolved, i.e., you can put the placeholder also in the params for an
option.
### References
The input address of a process may contain a reference to the output of another process. It has the form `#[processid]:output=[id]`.
A reference starts with a `#` followed by the process ID it refers to, followed by a `:.` Then comes `output,` followed by a `=.`
and the ID of the output.
## FFmpeg
### Statistics
This repository contains a patch for the FFmpeg program to provide detailed progress information. With this patch, FFmpeg will output
the progress information in a JSON string that contains the data for each input and output stream individually. The JSON output is enabled
by default. It can be enabled with the global `-jsonstats` switch on the command line. Use the `-stats` switch
on the command line for the standard progress output.
The Docker image that you can build with the provided Dockerfile includes the patched version of FFmpeg for your convenience.
Example output with `-stats`:
```
frame= 143 fps= 25 q=-1.0 Lsize= 941kB time=00:00:05.68 bitrate=1357.0kbits/s speed=0.995x
```
Example output with `-jsonstats`:
```
JSONProgress:{"inputs":[{"id":0, "stream":0, "type":"video", "codec":"rawvideo", "coder":"rawvideo", "pix_fmt":"rgb24", "frame":188, "fps":24.95, "width":1280, "height":720, "size_kb":507600, "bitrate_kbps":552960.0},{"id":1, "stream":0, "type":"audio", "codec":"pcm_u8", "coder":"pcm_u8", "frame":314, "sampling_hz":44100, "layout":"stereo", "size_kb":628, "bitrate_kbps":705.6}], "outputs":[{"id":0, "stream":0, "type":"video", "codec":"h264", "coder":"libx264", "pix_fmt":"yuv420p", "frame":188, "fps":24.95, "q":-1.0, "width":1280, "height":720, "size_kb":1247, "bitrate_kbps":1365.6},{"id":0, "stream":1, "type":"audio", "codec":"aac", "coder":"aac", "frame":315, "sampling_hz":44100, "layout":"stereo", "size_kb":2, "bitrate_kbps":2.1}], "frame":188, "fps":24.95, "q":-1.0, "size_kb":1249, "bitrate_kbps":1367.7, "time":"0h0m7.48s", "speed":0.993, "dup":0, "drop":0}
```
The same output but nicely formatted:
```json
{
"bitrate_kbps": 1367.7,
"drop": 0,
"dup": 0,
"fps": 24.95,
"frame": 188,
"inputs": [
{
"bitrate_kbps": 552960.0,
"codec": "rawvideo",
"coder": "rawvideo",
"fps": 24.95,
"frame": 188,
"height": 720,
"id": 0,
"pix_fmt": "rgb24",
"size_kb": 507600,
"stream": 0,
"type": "video",
"width": 1280
},
{
"bitrate_kbps": 705.6,
"codec": "pcm_u8",
"coder": "pcm_u8",
"frame": 314,
"id": 1,
"layout": "stereo",
"sampling_hz": 44100,
"size_kb": 628,
"stream": 0,
"type": "audio"
}
],
"outputs": [
{
"bitrate_kbps": 1365.6,
"codec": "h264",
"coder": "libx264",
"fps": 24.95,
"frame": 188,
"height": 720,
"id": 0,
"pix_fmt": "yuv420p",
"q": -1.0,
"size_kb": 1247,
"stream": 0,
"type": "video",
"width": 1280
},
{
"bitrate_kbps": 2.1,
"codec": "aac",
"coder": "aac",
"frame": 315,
"id": 0,
"layout": "stereo",
"sampling_hz": 44100,
"size_kb": 2,
"stream": 1,
"type": "audio"
}
],
"q": -1.0,
"size_kb": 1249,
"speed": 0.993,
"time": "0h0m7.48s"
}
```
### Resilient Streaming
Prepend the input source with `avstream:`, e.g. `... -i avstream:rtsp://1.2.3.4/stream.sdp ...`. It will reconnect to the stream if it breaks and repeats the last known intraframe until new data from the input stream is available.
## Example
Start `core` with the proper environment variables. Create a `.env` file or provide them on the command line. For this example, please use the following command line:
```
env CORE_API_AUTH_USERNAME=admin CORE_API_AUTH_PASSWORD=datarhei CORE_LOGLEVEL=debug CORE_STORAGE_DISK_DIR=./data ./core
```
Also, make sure that the directory `./data` exists. Otherwise, the state will not be stored and will be lost after a restart of
datarhei/core and the FFmpeg process will not be able to write the files.
In this example, we will add a fake video and audio source. The video will be encoded with H264, and the audio will be encoded with AAC. The output will be an m3u8 stream.
To talk to the API, we use the program [httpie](https://httpie.org/).
First, we create a JSON file with the process definition (e.g. `testsrc.json`):
```json
{
"id": "testsrc",
"type": "ffmpeg",
"options": ["-loglevel", "info", "-err_detect", "ignore_err"],
"input": [
{
"address": "testsrc=size=1280x720:rate=25",
"id": "video",
"options": ["-f", "lavfi", "-re"]
},
{
"address": "anullsrc=r=44100:cl=stereo",
"id": "audio",
"options": ["-f", "lavfi"]
}
],
"output": [
{
"address": "http://127.0.0.1:8080/memfs/{processid}_{outputid}.m3u8",
"id": "hls",
"options": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-b:v",
"1024k",
"-codec:a",
"aac",
"-b:a",
"64k",
"-hls_time",
"2",
"-hls_list_size",
"10",
"-hls_flags",
"delete_segments+temp_file+append_list",
"-hls_segment_filename",
"http://127.0.0.1:8080/memfs/{processid}_{outputid}_%04d.ts"
]
}
],
"reconnect": true,
"reconnect_delay_seconds": 10,
"stale_timeout_seconds": 10
}
```
and POST it to the API:
```
http POST http://localhost:8080/v3/process < testsrc.json
```
Then check if it is there (as provided)
```
http http://localhost:8080/v3/process/testsrc
```
For the advanced, create another JSON file (e.g. `dump.json`):
```json
{
"id": "dump",
"type": "ffmpeg",
"options": ["-loglevel", "info", "-err_detect", "ignore_err"],
"input": [
{
"address": "#testsrc:output=hls",
"id": "video",
"options": []
}
],
"output": [
{
"address": "{diskfs}/{processid}.mp4",
"id": "hls",
"options": ["-codec", "copy", "-y"]
}
],
"reconnect": true,
"reconnect_delay_seconds": 10,
"stale_timeout_seconds": 10
}
```
and POST it to the API:
```
http POST http://localhost:8080/v3/process < dump.json
```
Then check if it is there (as provided)
```
http http://localhost:8080/v3/process/dump
```
Let's start the `testsrc` process
```
http PUT http://localhost:8080/v3/process/testsrc/command command=start
```
Now we can observe the progress of the process
```
http http://localhost:8080/v3/process/testsrc
```
or the log of the process
```
http http://localhost:8080/v3/process/testsrc/log
```
If you want to change the video bitrate, edit the `testsrc.json` file accordingly and replace the process:
```
http PUT http://localhost:8080/v3/process/testsrc < testsrc.json
```
It will stop the process, replace the config, and restart it.
Now open, e.g., VLC, and load the stream `http://localhost:8080/memfs/testsrc_hls.m3u8`.
This is enough; let's stop it
```
http PUT http://localhost:8080/v3/process/testsrc/command command=stop
```
and check its progress again
```
HTTP http://localhost:8080/v3/process/testsrc
```
Delete the process
```
HTTP DELETE http://localhost:8080/v3/process/testsrc
```
## Metrics
Metrics for the processes and other aspects are provided for a Prometheus scraper on `/metrics.`
Currently, available metrics are:
| Metric | Type | Dimensions | Description |
| --------------------- | ------- | --------------------------------------------------------- | ----------------------------------------------- |
| ffmpeg_process | gauge | `core`, `process`, `name` | General stats per process. |
| ffmpeg_process_io | gauge | `core`, `process`, `type`, `id`, `index`, `media`, `name` | Stats per input and output of a process. |
| mem_limit_bytes | gauge | `core` | Total available memory in bytes. |
| mem_free_bytes | gauge | `core` | Free memory in bytes. |
| net_rx_bytes | gauge | `core`, `interface` | Number of received bytes by interface. |
| net_tx_bytes | gauge | `core`, `interface` | Number of sent bytes by interface. |
| cpus_system_time_secs | gauge | `core`, `cpu` | System time per CPU in seconds. |
| cpus_user_time_secs | gauge | `core`, `cpu` | User time per CPU in seconds. |
| cpus_idle_time_secs | gauge | `core`, `cpu` | Idle time per CPU in seconds. |
| session_total | counter | `core`, `collector` | Total number of sessions by collector. |
| session_active | gauge | `core`, `collector` | Current number of active sessions by collector. |
| session_rx_bytes | counter | `core`, `collector` | Total received bytes by collector. |
| session_tx_bytes | counter | `core`, `collector` | Total sent bytes by collector. |
## Profiling
Profiling information is available under `/profiling.` Set the environment variable `CORE_DEBUG_PROFILING=true` to make this endpoint
available. If authentication is enabled, you have to provide the token in the header.
## Development
### Requirement
- Go v1.16+ ([Download here](https://golang.org/dl/))
### Build
Clone the repository and build the binary
```
git clone git@github.com:datarhei/core.git
cd core
make
```
After the build process, the binary is available as `core`
For more build options, run `make help.`
### Cross Compile
If you want to run the binary on a different operating system and/or architecture, you create the appropriate binary by simply setting some
environment variables, e.g.
```
env GOOS=linux GOARCH=arm go build -o core-linux-arm
env GOOS=linux GOARCH=arm64 go build -o core-linux-arm64
env GOOS=freebsd GOARCH=amd64 go build -o core-freebsd-amd64
env GOOS=windows GOARCH=amd64 go build -o core-windows-amd64
env GOOS=macos GOARCH=amd64 go build -o core-macos-amd64
...
```
### Docker
Build the Docker image and run it to try out the API
```
docker build -t core .
docker run -it --rm -v ${PWD}/data:/core/data -p 8080:8080 core
```
### API Documentation
The documentation of the API is available on `/api/swagger/index.html.`
To generate the API documentation from the code, use [swag](https://github.com/swaggo/swag).
```
go install github.com/swaggo/swag (unless already installed)
make swagger
```
### Code style
The source code is formatted with `go fmt`, or simply run `make fmt`. Static analysis of the source code is done with `staticcheck`
(see [staticcheck](https://staticcheck.io/docs/)), or simply run `make lint`.
Before committing changes, you should run `make commit` to ensure that the source code is in shape.
## License
See the [LICENSE](./LICENSE) file for licensing information.

1105
app/api/api.go Normal file

File diff suppressed because it is too large Load Diff

1
app/import/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
import

28
app/import/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Restreamer v1 database import
This programe reads a Restreamer v1 database (v1.json, created with 0.6.7+) and writes a new database including the metadata for the UI.
Also static routes will be added to the configuration file in order to main the paths to the player, snapshot, and m3u8.
This will make the migration from the old to the new Restreamer seamless.
If the v1 database is from a Restreamer before 0.6.7, then some assumptions are made that the input stream has a H264 video track.
All the environment variables for defining the USB device or Rapsicam are respected and are used to define the inputs.
## Processing
The importer understands and respects the same environment variables as the Restreamer. It uses `RS_CONFIGFILE` (and possibly
`RS_DB_DIR`) to find out where to look for the databases. If the v1 database is not found in this directory, there's nothing
to import and the importer aborts. If there's a v1 database, the importer checks if there's also a non-empty new database. If
there is an existing non-empty new database then the v1 database will not be imported. The existing non-empty new database is
interpreted that this is not a fresh Restreamer installation and should not be altered.
In case the new database is considered empty (i.e. no process and no metadata entries), then the v1 database will be read and
transformed to the corresponding new database format. In order keep compatibility with the v1 Restreamer, the importer also
alters the configuration file by adding static redirect routes to the new locations of the player, snapshot, and m3u8.
## Return Values
The importer exits with `0` if the import was successful or if the import did not happen (either because there wasn't a v1 database,
or there was already a non-empty new database). If there were any error during the importing process, the importer exits with `1`.
During the runtime of the importer, log messages are written to `stderr`.

View File

@@ -0,0 +1,59 @@
{
"addresses": {
"srcAddress": "",
"optionalOutputAddress": ""
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "4096",
"fps": "25",
"profile": "auto",
"tune": "none"
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "44100"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "http://127.0.0.1:8080/memfs/live.stream.m3u8",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 2,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,61 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": ""
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "4096",
"fps": "25",
"profile": "auto",
"tune": "none",
"id": 0
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "44100",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "4096",
"fps": "25",
"profile": "auto",
"tune": "none",
"id": 0
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "44100",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "4096",
"fps": "25",
"profile": "auto",
"tune": "none",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "4096",
"fps": "25",
"profile": "auto",
"tune": "none",
"id": 0
},
"audio": {
"codec": "mp3",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "4096",
"fps": "25",
"profile": "auto",
"tune": "none",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "44100",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "http://admin:foobar@127.0.0.1:8080/memfs/stream.m3u8",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 2
},
"audio": {
"codec": "copy",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "hls",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "copy",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "rtmp://127.0.0.1:1936/live/stream",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 2
},
"audio": {
"codec": "copy",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "encode",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "encode",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "copy",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "encode",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "mp3",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "copy",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtsp://127.0.0.1:8554/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "mp3",
"layout": "stereo",
"channels": 2,
"sampling": "44100"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "mp3",
"preset": "encode",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": 1
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "silence",
"bitrate": "128",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,72 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": null,
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "slow",
"bitrate": "4096",
"fps": "29.95",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "11000"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "11000"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "copy",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "11000"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "encode",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "11000"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "aac",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "11000"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "copy",
"preset": "ultrafast",
"bitrate": "2048",
"fps": "25",
"profile": "auto",
"tune": "zerolatency",
"id": 0
},
"audio": {
"codec": "none",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,79 @@
{
"addresses": {
"srcAddress": "rtmp://127.0.0.1:1936/live/stream",
"optionalOutputAddress": "",
"srcStreams": {
"audio": {
"index": 1,
"type": "audio",
"codec": "aac",
"layout": "stereo",
"channels": 2,
"sampling": "11000"
},
"video": {
"index": 0,
"type": "video",
"codec": "h264",
"width": 1280,
"height": 720,
"format": "yuv420p"
}
}
},
"options": {
"rtspTcp": true,
"video": {
"codec": "h264",
"preset": "slow",
"bitrate": "4096",
"fps": "30",
"profile": "high",
"tune": "grain",
"id": 0
},
"audio": {
"codec": "auto",
"preset": "silence",
"bitrate": "64",
"channels": "mono",
"sampling": "22050",
"id": "a"
},
"player": {
"autoplay": false,
"mute": false,
"statistics": false,
"color": "#3daa48",
"logo": {
"image": "",
"position": "bottom-right",
"link": ""
}
},
"output": {
"type": "rtmp",
"rtmp": {},
"hls": {
"method": "POST",
"time": "2",
"listSize": "10",
"timeout": "10"
}
}
},
"userActions": {
"repeatToLocalNginx": "stop",
"repeatToOptionalOutput": "stop"
},
"states": {
"repeatToLocalNginx": {
"type": "disconnected",
"message": ""
},
"repeatToOptionalOutput": {
"type": "disconnected",
"message": ""
}
}
}

View File

@@ -0,0 +1,8 @@
{
"version": 4,
"process": {},
"metadata": {
"system": {},
"process": {}
}
}

View File

@@ -0,0 +1,269 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "http://127.0.0.1:8080/memfs/live.stream.m3u8",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-analyzeduration",
"20000000",
"-re"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:2",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"codec": "",
"coder": "default",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"codec": "",
"coder": "default",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 2
}
}
],
"sources": [
{
"inputs": [
{
"address": "http://127.0.0.1:8080/memfs/live.stream.m3u8",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-analyzeduration",
"20000000",
"-re"
]
}
],
"settings": {
"address": "http://127.0.0.1:8080/memfs/live.stream.m3u8",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 2,
"type": "video",
"url": "http://127.0.0.1:8080/memfs/live.stream.m3u8",
"width": 1280
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,342 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo"
],
"settings": {
"bitrate": "32",
"channels": "2",
"layout": "stereo",
"sampling": "44100"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
}
],
"type": "network"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,267 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtmp://127.0.0.1:1936/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-analyzeduration",
"3000000"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtmp://127.0.0.1:1936/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-analyzeduration",
"3000000"
]
}
],
"settings": {
"address": "rtmp://127.0.0.1:1936/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtmp://127.0.0.1:1936/live/stream",
"width": 1280
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,342 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo"
],
"settings": {
"bitrate": "32",
"channels": "2",
"layout": "stereo",
"sampling": "44100"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
}
],
"type": "network"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,342 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
}
],
"type": "network"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,342 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"1:0",
"-codec:a",
"libmp3lame",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "mp3",
"coder": "libmp3lame",
"mapping": [
"-codec:a",
"libmp3lame",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
}
],
"type": "network"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,271 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,305 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "2048",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,437 @@
{
"version": 4,
"process": {
"restreamer-ui:egress:hls:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:egress:hls:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:egress:hls:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": [
"-re"
]
}
],
"output": [
{
"id": "output_0",
"address": "http://admin:foobar@127.0.0.1:8080/memfs/stream.m3u8",
"options": [
"-codec",
"copy",
"-f",
"hls",
"-hls_init_time",
"0",
"-hls_time",
"2",
"-hls_list_size",
"10",
"-hls_delete_threshold",
"1",
"-hls_start_number_source",
"generic",
"-start_number",
"0",
"-hls_allow_cache",
"0",
"-hls_enc",
"0",
"-hls_segment_type",
"mpegts",
"-hls_flags",
"delete_segments,append_list",
"-method",
"POST",
"-http_persistent",
"0",
"-timeout",
"10",
"-ignore_io_errors",
"0"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 30,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"0:1",
"-codec:a",
"copy",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:egress:hls:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"process": {
"autostart": true,
"delay": 30,
"reconnect": true,
"staleTimeout": 30
}
},
"name": "HLS",
"output": {
"address": "http://admin:foobar@127.0.0.1:8080/memfs/stream.m3u8",
"options": [
"-codec",
"copy",
"-f",
"hls",
"-hls_init_time",
"0",
"-hls_time",
"2",
"-hls_list_size",
"10",
"-hls_delete_threshold",
"1",
"-hls_start_number_source",
"generic",
"-start_number",
"0",
"-hls_allow_cache",
"0",
"-hls_enc",
"0",
"-hls_segment_type",
"mpegts",
"-hls_flags",
"delete_segments,append_list",
"-method",
"POST",
"-http_persistent",
"0",
"-timeout",
"10",
"-ignore_io_errors",
"0"
]
},
"settings": {
"address": "admin:foobar@127.0.0.1:8080/memfs/stream.m3u8",
"options": {
"hls_delete_threshold": "1",
"hls_flags": [
"delete_segments",
"append_list"
],
"hls_init_time": "0",
"hls_list_size": "10",
"hls_segment_type": "mpegts",
"hls_start_number_source": "generic",
"hls_time": "2",
"method": "POST",
"start_number": "0",
"timeout": "10",
"ignore_io_errors": "0",
"hls_enc": "0",
"hls_allow_cache": "0",
"http_persistent": "0"
},
"protocol": "http://"
},
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "copy",
"coder": "copy",
"mapping": [
"-codec:a",
"copy"
],
"settings": {}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,296 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"0:1",
"-codec:a",
"copy",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "copy",
"coder": "copy",
"mapping": [
"-codec:a",
"copy"
],
"settings": {}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,366 @@
{
"version": 4,
"process": {
"restreamer-ui:egress:rtmp:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:egress:rtmp:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:egress:rtmp:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": [
"-re"
]
}
],
"output": [
{
"id": "output_0",
"address": "rtmp://127.0.0.1:1936/live/stream",
"options": [
"-codec",
"copy",
"-f",
"flv",
"-rtmp_flashver",
"FMLE/3.0"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 30,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"0:1",
"-codec:a",
"copy",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:egress:rtmp:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"process": {
"autostart": true,
"delay": 30,
"reconnect": true,
"staleTimeout": 30
}
},
"name": "RTMP",
"output": {
"address": "rtmp://127.0.0.1:1936/live/stream",
"options": [
"-codec",
"copy",
"-f",
"flv",
"-rtmp_flashver",
"FMLE/3.0"
]
},
"settings": {
"address": "127.0.0.1:1936/live/stream",
"protocol": "rtmp://"
},
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "copy",
"coder": "copy",
"mapping": [
"-codec:a",
"copy"
],
"settings": {}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,315 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"0:1",
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-bsf:a",
"aac_adtstoasc",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-bsf:a",
"aac_adtstoasc"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,361 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,290 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,330 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"0:1",
"-codec:a",
"copy",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "copy",
"coder": "copy",
"mapping": [
"-codec:a",
"copy"
],
"settings": {}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "2048",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,349 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"0:1",
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-bsf:a",
"aac_adtstoasc",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-bsf:a",
"aac_adtstoasc"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "2048",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,395 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "2048",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,324 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"2048k",
"-maxrate:v",
"2048k",
"-bufsize:v",
"2048k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "2048",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "aac",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,296 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"0:1",
"-codec:a",
"copy",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "copy",
"coder": "copy",
"mapping": [
"-codec:a",
"copy"
],
"settings": {}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "mp3",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,311 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero",
"-map",
"0:1",
"-codec:a",
"libmp3lame",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "mp3",
"coder": "libmp3lame",
"mapping": [
"-codec:a",
"libmp3lame",
"-b:a",
"64k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono"
],
"settings": {
"bitrate": "64",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 0,
"stream": 1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "copy",
"mapping": [
"-codec:v",
"copy",
"-vsync",
"0",
"-copyts",
"-start_at_zero"
],
"settings": {}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "rtsp://127.0.0.1:8554/live/stream",
"options": [
"-fflags",
"+genpts",
"-thread_queue_size",
"512",
"-stimeout",
"5000000",
"-rtsp_transport",
"tcp"
]
}
],
"settings": {
"address": "rtsp://127.0.0.1:8554/live/stream",
"general": {
"fflags": [
"genpts"
],
"thread_queue_size": 512
},
"http": {
"forceFramerate": false,
"framerate": 25,
"readNative": true
},
"mode": "pull",
"rtsp": {
"stimeout": 5000000,
"udp": false
}
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "h264",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "yuv420p",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 1280
},
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "mp3",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 1,
"type": "audio",
"url": "rtsp://127.0.0.1:8554/live/stream",
"width": 0
}
],
"type": "network"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,367 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo"
],
"settings": {
"bitrate": "32",
"channels": "2",
"layout": "stereo",
"sampling": "44100"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,367 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"128k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"128k",
"-shortest",
"-af",
"aresample=osr=22050:ocl=mono"
],
"settings": {
"bitrate": "128",
"channels": "1",
"layout": "mono",
"sampling": "22050"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,296 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,367 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"slow",
"-b:v",
"4096k",
"-maxrate:v",
"4096k",
"-bufsize:v",
"4096k",
"-r",
"29.95",
"-g",
"60",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"32k",
"-shortest",
"-af",
"aresample=osr=44100:ocl=stereo"
],
"settings": {
"bitrate": "32",
"channels": "2",
"layout": "stereo",
"sampling": "44100"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"slow",
"-b:v",
"4096k",
"-maxrate:v",
"4096k",
"-bufsize:v",
"4096k",
"-r",
"29.95",
"-g",
"60",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "4096",
"fps": "29.95",
"preset": "slow",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "anullsrc=r=44100:cl=stereo",
"options": [
"-f",
"lavfi"
]
}
],
"settings": {
"amplitude": 1,
"beepfactor": 4,
"color": "white",
"frequency": 440,
"layout": "stereo",
"sampling": 44100,
"source": "silence"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "lavfi",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 44100,
"stream": 0,
"type": "audio",
"url": "anullsrc=r=44100:cl=stereo",
"width": 0
}
],
"type": "virtualaudio"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,377 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo"
],
"settings": {
"bitrate": "28",
"channels": "2",
"layout": "stereo",
"sampling": "11000"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"settings": {
"address": "hw:1,0",
"device": "1,0",
"channels": 2,
"sampling": 11000,
"delay": 0
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "alsa",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 11000,
"stream": 0,
"type": "audio",
"url": "hw:1,0",
"width": 0
}
],
"type": "alsa"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,377 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo"
],
"settings": {
"bitrate": "28",
"channels": "2",
"layout": "stereo",
"sampling": "11000"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"settings": {
"address": "hw:1,0",
"device": "1,0",
"channels": 2,
"sampling": 11000,
"delay": 0
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "alsa",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 11000,
"stream": 0,
"type": "audio",
"url": "hw:1,0",
"width": 0
}
],
"type": "alsa"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,377 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo"
],
"settings": {
"bitrate": "28",
"channels": "2",
"layout": "stereo",
"sampling": "11000"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"settings": {
"address": "hw:1,0",
"device": "1,0",
"channels": 2,
"sampling": 11000,
"delay": 0
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "alsa",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 11000,
"stream": 0,
"type": "audio",
"url": "hw:1,0",
"width": 0
}
],
"type": "alsa"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,377 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo"
],
"settings": {
"bitrate": "28",
"channels": "2",
"layout": "stereo",
"sampling": "11000"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"settings": {
"address": "hw:1,0",
"device": "1,0",
"channels": 2,
"sampling": 11000,
"delay": 0
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "alsa",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 11000,
"stream": 0,
"type": "audio",
"url": "hw:1,0",
"width": 0
}
],
"type": "alsa"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,296 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency",
"-an",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "none",
"coder": "none",
"mapping": [],
"settings": {}
},
"source": -1,
"stream": -1
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"ultrafast",
"-b:v",
"5000k",
"-maxrate:v",
"5000k",
"-bufsize:v",
"5000k",
"-r",
"25",
"-g",
"50",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-tune:v",
"zerolatency"
],
"settings": {
"bitrate": "5000",
"fps": "25",
"preset": "ultrafast",
"profile": "auto",
"tune": "zerolatency"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

View File

@@ -0,0 +1,381 @@
{
"version": 4,
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
},
{
"id": "input_1",
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.m3u8",
"options": [
"-dn",
"-sn",
"-map",
"0:0",
"-codec:v",
"libx264",
"-preset:v",
"slow",
"-b:v",
"4096k",
"-maxrate:v",
"4096k",
"-bufsize:v",
"4096k",
"-r",
"30",
"-g",
"60",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-profile:v",
"high",
"-tune:v",
"grain",
"-map",
"1:0",
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo",
"-f",
"hls",
"-start_number",
"0",
"-hls_time",
"2",
"-hls_list_size",
"6",
"-hls_flags",
"append_list+delete_segments",
"-hls_segment_filename",
"{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f_%04d.ts",
"-y",
"-method",
"PUT"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 15,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"config": {
"id": "restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot",
"reference": "4186b095-7f0a-4e94-8c3d-f17459ab252f",
"input": [
{
"id": "input_0",
"address": "#restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f:output=output_0",
"options": []
}
],
"output": [
{
"id": "output_0",
"address": "{memfs}/4186b095-7f0a-4e94-8c3d-f17459ab252f.jpg",
"options": [
"-vframes",
"1",
"-f",
"image2",
"-update",
"1"
]
}
],
"options": [
"-err_detect",
"ignore_err"
],
"reconnect": true,
"reconnect_delay_seconds": 60,
"autostart": true,
"stale_timeout_seconds": 30
},
"created_at": 0,
"order": "stop"
}
},
"metadata": {
"system": {},
"process": {
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f": {
"restreamer-ui": {
"control": {
"hls": {
"lhls": false,
"listSize": 6,
"segmentDuration": 2
},
"process": {
"autostart": true,
"delay": 15,
"reconnect": true,
"staleTimeout": 30
},
"snapshot": {
"enable": true,
"interval": 60
}
},
"imported": true,
"license": "CC BY 4.0",
"meta": {
"author": {
"description": "",
"name": ""
},
"description": "Live from earth. Powered by datarhei Restreamer.",
"name": "Livestream"
},
"player": {
"autoplay": false,
"color": {
"buttons": "#3daa48",
"seekbar": "#3daa48"
},
"ga": {
"account": "",
"name": ""
},
"logo": {
"image": "",
"link": "",
"position": "bottom-right"
},
"mute": false,
"statistics": false
},
"profiles": [
{
"audio": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "aac",
"coder": "aac",
"mapping": [
"-codec:a",
"aac",
"-b:a",
"28k",
"-shortest",
"-af",
"aresample=osr=11000:ocl=stereo"
],
"settings": {
"bitrate": "28",
"channels": "2",
"layout": "stereo",
"sampling": "11000"
}
},
"source": 1,
"stream": 0
},
"video": {
"decoder": {
"coder": "default",
"codec": "",
"mapping": [],
"settings": {}
},
"encoder": {
"codec": "h264",
"coder": "libx264",
"mapping": [
"-codec:v",
"libx264",
"-preset:v",
"slow",
"-b:v",
"4096k",
"-maxrate:v",
"4096k",
"-bufsize:v",
"4096k",
"-r",
"30",
"-g",
"60",
"-pix_fmt",
"yuv420p",
"-vsync",
"1",
"-profile:v",
"high",
"-tune:v",
"grain"
],
"settings": {
"bitrate": "4096",
"fps": "30",
"preset": "slow",
"profile": "high",
"tune": "grain"
}
},
"source": 0,
"stream": 0
}
}
],
"sources": [
{
"inputs": [
{
"address": "/dev/video",
"options": [
"-thread_queue_size",
"512",
"-f",
"v4l2",
"-framerate",
"25",
"-video_size",
"1280x720",
"-input_format",
"nv12"
]
}
],
"settings": {
"device": "/dev/video",
"format": "nv12",
"framerate": 25,
"size": "1280x720"
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 0,
"codec": "rawvideo",
"coder": "",
"duration_sec": 0,
"format": "",
"fps": 0,
"height": 720,
"index": 0,
"language": "",
"layout": "",
"pix_fmt": "nv12",
"sampling_hz": 0,
"stream": 0,
"type": "video",
"url": "/dev/video",
"width": 1280
}
],
"type": "video4linux2"
},
{
"inputs": [
{
"address": "hw:1,0",
"options": [
"-thread_queue_size",
"512",
"-f",
"alsa",
"-ac",
"2",
"-ar",
"11000"
]
}
],
"settings": {
"address": "hw:1,0",
"device": "1,0",
"channels": 2,
"sampling": 11000,
"delay": 0
},
"streams": [
{
"bitrate_kbps": 0,
"channels": 2,
"codec": "pcm_u8",
"coder": "",
"duration_sec": 0,
"format": "alsa",
"fps": 0,
"height": 0,
"index": 0,
"language": "",
"layout": "stereo",
"pix_fmt": "",
"sampling_hz": 11000,
"stream": 0,
"type": "audio",
"url": "hw:1,0",
"width": 0
}
],
"type": "alsa"
}
],
"version": 1
}
},
"restreamer-ui:ingest:4186b095-7f0a-4e94-8c3d-f17459ab252f_snapshot": null
}
}
}

1434
app/import/import.go Normal file

File diff suppressed because it is too large Load Diff

315
app/import/import_test.go Normal file
View File

@@ -0,0 +1,315 @@
package main
import (
gojson "encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/datarhei/core/encoding/json"
"github.com/datarhei/core/restream/store"
"github.com/stretchr/testify/require"
)
// Scenarios:
// - Empty v1 file - v1/v4 empty
// - Input: RTSP (h264/), copy video, no audio, Output: none - v1/v4 rtsp_h264,-_copyvideo_noaudio_output_none
// - Input: HLS (h264/), copy video, no audio, Output: none - v1/v4 hls_h264,-_copyvideo_noaudio_output_none
// - Input: RTMP (h264/), copy video, no audio, Output: none - v1/v4 rtmp_h264,-_copyvideo_noaudio_output_none
// - Input: RTSP (h264/), copy video, auto audio, Output: none - v1/v4 rtsp_h264,-_copyvideo_autoaudio_output_none
// - Input: RTSP (h264/aac), copy video, copy audio, Output: none - v1/v4 rtsp_h264,aac_copyvideo_copyaudio_output_none
// - Input: RTSP (h264/mp3), copy video, copy audio, Output: none - v1/v4 rtsp_h264,mp3_copyvideo_copyaudio_output_none
// - Input: RTSP (h264/aac), copy video, encode audio aac, Output: none - v1/v4 rtsp_h264,aac_copyvideo_encodeaudioaac_output_none
// - Input: RTSP (h264/mp3), copy video, encode audio mp3, Output: none - v1/v4 rtsp_h264,mp3_copyvideo_encodeaudiomp3_output_none
// - Input: RTSP (h264/aac), copy video, no audio, Output: none - v1/v4 rtsp_h264,aac_copyvideo_noaudio_output_none
// - Input: RTSP (h264/aac), copy video, encode silence aac, Output: none - v1/v4 rtsp_h264,aac_copyvideo_encodesilenceaac_output_none
// - Input: RTSP (h264/), copy video, encode silence aac, Output: none - v1/v4 rtsp_h264,-_copyvideo_encodesilenceaac_output_none
// - Input: RTSP (h264/), copy video, encode silence mp3, Output: none - v1/v4 rtsp_h264_copyvideo_encodesilencemp3_output_none
// - Input: RTSP (h264/), encode video, no audio, Output: none - v1v4/ rtsp_h264,-_encodevideo_noaudio_output_none
// - Input: RTSP (h264/aac), encode video, copy audio, Output: none - v1/v4 rtsp_h264,aac_encodevideo_copyaudio_output_none
// - Input: RTSP (h264/aac), encode video, encode audio aac, Output: none - v1/v4 rtsp_h264,aac_encodevideo_encodeaudioaac_output_none
// - Input: RTSP (h264/aac), encode video, encode silence aac, Output: none - v1/v4 rtsp_h264,aac_encodevideo_encodesilenceaac_output_none
// - Input: RTSP (h264/aac), encode video, no audio, Output: none - v1/v4 rtsp_h264,aac_encodevideo_noaudio_output_none
// - Input: RTSP (h264/aac), copy video, copy audio, Output: RTMP - v1/v4 rtsp_h264,aac_copyvideo_copyaudio_output_rtmp
// - Input: RTSP (h264/aac), copy video, copy audio, Output: HLS - v1/v4 rtsp_h264,aac_copyvideo_copyaudio_output_hls
var id string = "4186b095-7f0a-4e94-8c3d-f17459ab252f"
func testV1Import(t *testing.T, v1Fixture, v4Fixture string, config importConfig) {
// Import v1 database
v4, err := importV1(v1Fixture, config)
require.Equal(t, nil, err)
// Reset variants
for n := range v4.Process {
v4.Process[n].CreatedAt = 0
}
// Convert to JSON
datav4, err := gojson.MarshalIndent(&v4, "", " ")
require.Equal(t, nil, err)
// Read the wanted result
wantdatav4, err := ioutil.ReadFile(v4Fixture)
require.Equal(t, nil, err)
var wantv4 store.StoreData
err = gojson.Unmarshal(wantdatav4, &wantv4)
require.Equal(t, nil, err, json.FormatError(wantdatav4, err))
// Convert to JSON
wantdatav4, err = gojson.MarshalIndent(&wantv4, "", " ")
require.Equal(t, nil, err)
// Re-convert both to golang type
gojson.Unmarshal(wantdatav4, &wantv4)
gojson.Unmarshal(datav4, &v4)
require.Equal(t, wantv4, v4)
}
func TestV1Import(t *testing.T) {
tests := []string{
"empty",
"hls_h264,-_copyvideo_noaudio_output_none",
"rtmp_h264,-_copyvideo_noaudio_output_none",
"rtsp_h264,-_copyvideo_autoaudio_output_none",
"rtsp_h264,-_copyvideo_encodesilenceaac_output_none",
"rtsp_h264,-_copyvideo_encodesilencemp3_output_none",
"rtsp_h264,-_copyvideo_noaudio_output_none",
"rtsp_h264,-_encodevideo_noaudio_output_none",
"rtsp_h264,aac_copyvideo_copyaudio_output_hls",
"rtsp_h264,aac_copyvideo_copyaudio_output_none",
"rtsp_h264,aac_copyvideo_copyaudio_output_rtmp",
"rtsp_h264,aac_copyvideo_encodeaudioaac_output_none",
"rtsp_h264,aac_copyvideo_encodesilenceaac_output_none",
"rtsp_h264,aac_copyvideo_noaudio_output_none",
"rtsp_h264,aac_encodevideo_copyaudio_output_none",
"rtsp_h264,aac_encodevideo_encodeaudioaac_output_none",
"rtsp_h264,aac_encodevideo_encodesilenceaac_output_none",
"rtsp_h264,aac_encodevideo_noaudio_output_none",
"rtsp_h264,mp3_copyvideo_copyaudio_output_none",
"rtsp_h264,mp3_copyvideo_encodeaudiomp3_output_none",
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
testV1Import(t, "./fixtures/v1_"+test+".json", "./fixtures/v4_"+test+".json", importConfig{
id: id,
snapshotInterval: 60,
})
})
}
}
func TestImportSnapshotInterval(t *testing.T) {
type testdata struct {
value string
expected int
}
tests := []testdata{
{"0", 0},
{"0ms", 0},
{"0s", 0},
{"0m", 0},
{"60000", 60},
{"60000ms", 60},
{"60s", 60},
{"1m", 60},
}
for _, test := range tests {
t.Run(test.value, func(t *testing.T) {
actual := importSnapshotInterval(test.value, -1)
require.Equal(t, test.expected, actual)
})
}
}
// Scenarios:
// Input: V4L (h264), copy video, auto audio, Output: none - v1/v4 v4l_h264,-_copyvideo_autoaudio_output_none
// Input: V4L (h264), copy video, no audio, Output: none - v1/v4 v4l_h264,-_copyvideo_noaudio_output_none
// Input: V4L (h264), copy video, encode silence aac, Output: none - v1/v4 v4l_h264,-_copyvideo_encodesilenceaac_output_none
// Input: V4L (h264), encode video, auto audio, Output: none - v1/v4 v4l_h264,-_encodevideo_autoaudio_output_none
func TestImportUSBCamWithoutAudio(t *testing.T) {
tests := []string{
"v4l_h264,-_copyvideo_autoaudio_output_none",
"v4l_h264,-_copyvideo_noaudio_output_none",
"v4l_h264,-_copyvideo_encodesilenceaac_output_none",
"v4l_h264,-_encodevideo_autoaudio_output_none",
}
config := importConfig{
id: id,
snapshotInterval: 60,
usbcam: importConfigUSBCam{
enable: true,
device: "/dev/video",
fps: "25",
gop: "50",
bitrate: "5000000",
preset: "ultrafast",
profile: "auto",
width: "1280",
height: "720",
},
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
testV1Import(t, "./fixtures/v1_"+test+".json", "./fixtures/v4_"+test+".json", config)
})
}
}
// Scenarios:
// Input: V4L/ALSA (h264/aac), copy video, auto audio, Output: none - v1/v4 v4lalsa_h264,aac_copyvideo_autoaudio_output_none
// Input: V4L/ALSA (h264/aac), copy video, copy audio, Output: none - v1/v4 v4lalsa_h264,aac_copyvideo_copyaudio_output_none
// Input: V4L/ALSA (h264/aac), copy video, no audio, Output: none - v1/v4 v4lalsa_h264,aac_copyvideo_noaudio_output_none
// Input: V4L/ALSA (h264/aac), copy video, encode silence aac, Output: none - v1/v4 v4lalsa_h264,aac_copyvideo_encodesilenceaac_output_none
// Input: V4L/ALSA (h264/aac), copy video, encode audio aac, Output: none - v1/v4 v4lalsa_h264,aac_copyvideo_encodeaudioaac_output_none
// Input: V4L/ALSA (h264/aac), encode video, auto audio, Output: none - v1/v4 v4lalsa_h264,aac_encodevideo_autoaudio_output_none
func TestImportUSBCamWithAudio(t *testing.T) {
tests := []string{
"v4lalsa_h264,aac_copyvideo_autoaudio_output_none",
"v4lalsa_h264,aac_copyvideo_copyaudio_output_none",
"v4lalsa_h264,aac_copyvideo_noaudio_output_none",
"v4lalsa_h264,aac_copyvideo_encodesilenceaac_output_none",
"v4lalsa_h264,aac_copyvideo_encodeaudioaac_output_none",
"v4lalsa_h264,aac_encodevideo_autoaudio_output_none",
}
config := importConfig{
id: id,
snapshotInterval: 60,
usbcam: importConfigUSBCam{
enable: true,
device: "/dev/video",
fps: "25",
gop: "50",
bitrate: "5000000",
preset: "ultrafast",
profile: "auto",
width: "1280",
height: "720",
},
audio: importConfigAudio{
enable: true,
device: "1,0",
bitrate: "28000",
channels: "2",
layout: "stereo",
sampling: "11000",
},
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
testV1Import(t, "./fixtures/v1_"+test+".json", "./fixtures/v4_"+test+".json", config)
})
}
}
func TestV1EnvironmentDefaults(t *testing.T) {
for key := range v1Environment {
os.Unsetenv(key)
}
initV1Environment()
for _, val := range v1Environment {
require.Equal(t, val.defval, val.value)
}
config := importConfigFromEnvironment()
require.Equal(t, false, config.usbcam.enable)
require.Equal(t, false, config.raspicam.enable)
require.Equal(t, false, config.audio.enable)
}
func TestV1EnvironmentUSBCam(t *testing.T) {
for key := range v1Environment {
os.Unsetenv(key)
}
os.Setenv("RS_MODE", "USBCAM")
config := importConfigFromEnvironment()
require.Equal(t, true, config.usbcam.enable)
require.Equal(t, "/dev/video", config.usbcam.device)
require.Equal(t, "25", config.usbcam.fps)
require.Equal(t, "50", config.usbcam.gop)
require.Equal(t, "5000000", config.usbcam.bitrate)
require.Equal(t, "ultrafast", config.usbcam.preset)
require.Equal(t, "baseline", config.usbcam.profile)
require.Equal(t, "1280", config.usbcam.width)
require.Equal(t, "720", config.usbcam.height)
os.Setenv("RS_USBCAM_AUDIO", "true")
config = importConfigFromEnvironment()
require.Equal(t, true, config.audio.enable)
require.Equal(t, "0", config.audio.device)
require.Equal(t, "64000", config.audio.bitrate)
require.Equal(t, "1", config.audio.channels)
require.Equal(t, "mono", config.audio.layout)
require.Equal(t, "44100", config.audio.sampling)
}
func TestV1EnvironmentRASPICam(t *testing.T) {
for key := range v1Environment {
os.Unsetenv(key)
}
os.Setenv("RS_MODE", "RASPICAM")
config := importConfigFromEnvironment()
require.Equal(t, true, config.raspicam.enable)
require.Equal(t, "25", config.raspicam.fps)
require.Equal(t, "1920", config.raspicam.width)
require.Equal(t, "1080", config.raspicam.height)
os.Setenv("RS_RASPICAM_AUDIO", "true")
config = importConfigFromEnvironment()
require.Equal(t, true, config.audio.enable)
require.Equal(t, "0", config.audio.device)
require.Equal(t, "64000", config.audio.bitrate)
require.Equal(t, "1", config.audio.channels)
require.Equal(t, "mono", config.audio.layout)
require.Equal(t, "44100", config.audio.sampling)
}
func TestV1EnvironmentInputstream(t *testing.T) {
for key := range v1Environment {
os.Unsetenv(key)
}
os.Setenv("RS_INPUTSTREAM", "")
}
func TestV1Pre067(t *testing.T) {
tests := []string{
"pre-0.6.7",
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
testV1Import(t, "./fixtures/v1_"+test+".json", "./fixtures/v4_"+test+".json", importConfig{
id: id,
snapshotInterval: 60,
binary: "ffmpeg",
})
})
}
}

126
app/import/main.go Normal file
View File

@@ -0,0 +1,126 @@
package main
import (
"os"
"github.com/datarhei/core/config"
"github.com/datarhei/core/log"
"github.com/datarhei/core/restream/store"
_ "github.com/joho/godotenv/autoload"
)
func main() {
if ok := doImport(); !ok {
os.Exit(1)
}
}
func doImport() bool {
logger := log.New("Import").WithOutput(log.NewConsoleWriter(os.Stderr, log.Linfo, true)).WithField("version", "v1")
logger.Info().Log("Database import")
configstore, err := config.NewJSONStore(os.Getenv("CORE_CONFIGFILE"), nil)
if err != nil {
logger.Error().WithError(err).Log("Loading configuration failed")
return false
}
cfg := configstore.Get()
// Merging the persisted config with the environment variables
cfg.Merge()
cfg.Validate(false)
if cfg.HasErrors() {
logger.Error().Log("The configuration contains errors")
cfg.Messages(func(level string, v config.Variable, message string) {
if level == "error" {
logger.Error().WithFields(log.Fields{
"variable": v.Name,
"value": v.Value,
"env": v.EnvName,
"description": v.Description,
}).Log(message)
}
})
return false
}
logger.Info().Log("Checking for database ...")
// Check if there's a v1.json from the old Restreamer
v1filename := cfg.DB.Dir + "/v1.json"
logger = logger.WithField("database", v1filename)
if _, err := os.Stat(v1filename); err != nil {
if os.IsNotExist(err) {
logger.Info().Log("Database doesn't exist and nothing will be imported")
return true
}
logger.Error().WithError(err).Log("Checking for v1 database")
return false
}
logger.Info().Log("Found database")
// Load an existing DB
datastore := store.NewJSONStore(store.JSONConfig{
Dir: cfg.DB.Dir,
})
data, err := datastore.Load()
if err != nil {
logger.Error().WithError(err).Log("Loading new database failed")
return false
}
// Check if the existing DB has already some data in it.
// If it's not empty, we will not import any v1 DB.
if !data.IsEmpty() {
logger.Info().Log("There's already information stored in the new database and the v1 database will not be imported")
return true
}
logger.Info().Log("Importing database ...")
// Read the Restreamer config from the environment variables
importConfig := importConfigFromEnvironment()
importConfig.binary = cfg.FFmpeg.Binary
// Rewrite the old database to the new database
r, err := importV1(v1filename, importConfig)
if err != nil {
logger.Error().WithError(err).Log("Importing database failed")
return false
}
// Persist the imported DB
if err := datastore.Store(r); err != nil {
logger.Error().WithError(err).Log("Storing imported data to new database failed")
return false
}
// Get the unmerged config for persisting
cfg = configstore.Get()
// Add static routes to mimic the old URLs
cfg.Router.Routes["/hls/live.stream.m3u8"] = "/memfs/" + importConfig.id + ".m3u8"
cfg.Router.Routes["/images/live.jpg"] = "/memfs/" + importConfig.id + ".jpg"
cfg.Router.Routes["/player.html"] = "/" + importConfig.id + ".html"
// Persist the modified config
if err := configstore.Set(cfg); err != nil {
logger.Error().WithError(err).Log("Storing adjusted config failed")
return false
}
logger.Info().Log("Successfully imported data")
return true
}

49
app/version.go Normal file
View File

@@ -0,0 +1,49 @@
package app
import (
"fmt"
"runtime"
)
// Name of the app
const Name = "datarhei-core"
type versionInfo struct {
Major int
Minor int
Patch int
}
func (v versionInfo) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}
func (v versionInfo) MajorString() string {
return fmt.Sprintf("%d.0.0", v.Major)
}
func (v versionInfo) MinorString() string {
return fmt.Sprintf("%d.%d.0", v.Major, v.Minor)
}
// Version of the app
var Version = versionInfo{
Major: 16,
Minor: 7,
Patch: 2,
}
// Commit is the git commit the app is build from. It should be filled in during compilation
var Commit = ""
// Branch is the git branch the app is build from. It should be filled in during compilation
var Branch = ""
// Build is the timestamp of when the app has been build. It should be filled in during compilation
var Build = ""
// Arch is the OS and CPU architecture this app is build for.
var Arch = runtime.GOOS + "/" + runtime.GOARCH
// Compiler is the golang version this app has been build with.
var Compiler = runtime.Version()

3
build.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env /bin/sh
make release

679
config/config.go Normal file
View File

@@ -0,0 +1,679 @@
// Package config implements types for handling the configuation for the app.
package config
import (
"context"
"fmt"
"net"
"os"
"time"
"github.com/datarhei/core/math/rand"
haikunator "github.com/atrox/haikunatorgo/v2"
"github.com/google/uuid"
)
type variable struct {
value value // The actual value
defVal string // The default value in string representation
name string // A name for this value
envName string // The environment variable that corresponds to this value
envAltNames []string // Alternative environment variable names
description string // A desriptions for this value
required bool // Whether a non-empty value is required
disguise bool // Whether the value should be disguised if printed
merged bool // Whether this value has been replaced by its corresponding environment variable
}
type Variable struct {
Value string
Name string
EnvName string
Description string
Merged bool
}
type message struct {
message string // The log message
variable Variable // The config field this message refers to
level string // The loglevel for this message
}
type Auth0Tenant struct {
Domain string `json:"domain"`
Audience string `json:"audience"`
ClientID string `json:"clientid"`
Users []string `json:"users"`
}
// Data is the actual configuration data for the app
type Data struct {
CreatedAt time.Time `json:"created_at"`
LoadedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
Version int64 `json:"version" jsonschema:"minimum=1,maximum=1"`
ID string `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
CheckForUpdates bool `json:"update_check"`
Log struct {
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
Topics []string `json:"topics"`
MaxLines int `json:"max_lines"`
} `json:"log"`
DB struct {
Dir string `json:"dir"`
} `json:"db"`
Host struct {
Name []string `json:"name"`
Auto bool `json:"auto"`
} `json:"host"`
API struct {
ReadOnly bool `json:"read_only"`
Access struct {
HTTP struct {
Allow []string `json:"allow"`
Block []string `json:"block"`
} `json:"http"`
HTTPS struct {
Allow []string `json:"allow"`
Block []string `json:"block"`
} `json:"https"`
} `json:"access"`
Auth struct {
Enable bool `json:"enable"`
DisableLocalhost bool `json:"disable_localhost"`
Username string `json:"username"`
Password string `json:"password"`
JWT struct {
Secret string `json:"secret"`
} `json:"jwt"`
Auth0 struct {
Enable bool `json:"enable"`
Tenants []Auth0Tenant `json:"tenants"`
} `json:"auth0"`
} `json:"auth"`
} `json:"api"`
TLS struct {
Address string `json:"address"`
Enable bool `json:"enable"`
Auto bool `json:"auto"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
} `json:"tls"`
Storage struct {
Disk struct {
Dir string `json:"dir"`
Size int64 `json:"max_size_mbytes"`
Cache struct {
Enable bool `json:"enable"`
Size uint64 `json:"max_size_mbytes"`
TTL int64 `json:"ttl_seconds"`
FileSize uint64 `json:"max_file_size_mbytes"`
Types []string `json:"types"`
} `json:"cache"`
} `json:"disk"`
Memory struct {
Auth struct {
Enable bool `json:"enable"`
Username string `json:"username"`
Password string `json:"password"`
} `json:"auth"`
Size int64 `json:"max_size_mbytes"`
Purge bool `json:"purge"`
} `json:"memory"`
CORS struct {
Origins []string `json:"origins"`
} `json:"cors"`
MimeTypes string `json:"mimetypes_file"`
} `json:"storage"`
RTMP struct {
Enable bool `json:"enable"`
EnableTLS bool `json:"enable_tls"`
Address string `json:"address"`
App string `json:"app"`
Token string `json:"token"`
} `json:"rtmp"`
FFmpeg struct {
Binary string `json:"binary"`
MaxProcesses int64 `json:"max_processes"`
Access struct {
Input struct {
Allow []string `json:"allow"`
Block []string `json:"block"`
} `json:"input"`
Output struct {
Allow []string `json:"allow"`
Block []string `json:"block"`
} `json:"output"`
} `json:"access"`
Log struct {
MaxLines int `json:"max_lines"`
MaxHistory int `json:"max_history"`
} `json:"log"`
} `json:"ffmpeg"`
Playout struct {
Enable bool `json:"enable"`
MinPort int `json:"min_port"`
MaxPort int `json:"max_port"`
} `json:"playout"`
Debug struct {
Profiling bool `json:"profiling"`
ForceGC int `json:"force_gc"`
} `json:"debug"`
Metrics struct {
Enable bool `json:"enable"`
EnablePrometheus bool `json:"enable_prometheus"`
Range int64 `json:"range_sec"` // seconds
Interval int64 `json:"interval_sec"` // seconds
} `json:"metrics"`
Sessions struct {
Enable bool `json:"enable"`
IPIgnoreList []string `json:"ip_ignorelist"`
SessionTimeout int `json:"session_timeout_sec"`
Persist bool `json:"persist"`
PersistInterval int `json:"persist_interval_sec"`
MaxBitrate uint64 `json:"max_bitrate_mbit"`
MaxSessions uint64 `json:"max_sessions"`
} `json:"sessions"`
Service struct {
Enable bool `json:"enable"`
Token string `json:"token"`
URL string `json:"url"`
} `json:"service"`
Router struct {
BlockedPrefixes []string `json:"blocked_prefixes"`
Routes map[string]string `json:"routes"`
UIPath string `json:"ui_path"`
} `json:"router"`
}
// Config is a wrapper for Data
type Config struct {
vars []*variable
logs []message
Data
}
// New returns a Config which is initialized with its default values
func New() *Config {
data := &Config{}
data.init()
return data
}
// NewConfigFrom returns a clone of a Config
func NewConfigFrom(d *Config) *Config {
data := New()
data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt
data.UpdatedAt = d.UpdatedAt
data.Version = d.Version
data.ID = d.ID
data.Name = d.Name
data.Address = d.Address
data.CheckForUpdates = d.CheckForUpdates
data.Log = d.Log
data.DB = d.DB
data.Host = d.Host
data.API = d.API
data.TLS = d.TLS
data.Storage = d.Storage
data.RTMP = d.RTMP
data.FFmpeg = d.FFmpeg
data.Playout = d.Playout
data.Debug = d.Debug
data.Metrics = d.Metrics
data.Sessions = d.Sessions
data.Service = d.Service
data.Router = d.Router
data.Log.Topics = copyStringSlice(d.Log.Topics)
data.Host.Name = copyStringSlice(d.Host.Name)
data.API.Access.HTTP.Allow = copyStringSlice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copyStringSlice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copyStringSlice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copyStringSlice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copyTenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copyStringSlice(d.Storage.CORS.Origins)
data.FFmpeg.Access.Input.Allow = copyStringSlice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copyStringSlice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copyStringSlice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copyStringSlice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copyStringSlice(d.Sessions.IPIgnoreList)
data.Router.BlockedPrefixes = copyStringSlice(d.Router.BlockedPrefixes)
data.Router.Routes = copyStringMap(d.Router.Routes)
for i, v := range d.vars {
data.vars[i].merged = v.merged
}
return data
}
func (d *Config) init() {
d.val(newInt64Value(&d.Version, 1), "version", "", nil, "Configuration file layout version", true, false)
d.val(newTimeValue(&d.CreatedAt, time.Now()), "created_at", "", nil, "Configuration file creation time", false, false)
d.val(newStringValue(&d.ID, uuid.New().String()), "id", "CORE_ID", nil, "ID for this instance", true, false)
d.val(newStringValue(&d.Name, haikunator.New().Haikunate()), "name", "CORE_NAME", nil, "A human readable name for this instance", false, false)
d.val(newAddressValue(&d.Address, ":8080"), "address", "CORE_ADDRESS", nil, "HTTP listening address", false, false)
d.val(newBoolValue(&d.CheckForUpdates, true), "update_check", "CORE_UPDATE_CHECK", nil, "Check for updates and send anonymized data", false, false)
// Log
d.val(newStringValue(&d.Log.Level, "info"), "log.level", "CORE_LOG_LEVEL", nil, "Loglevel: silent, error, warn, info, debug", false, false)
d.val(newStringListValue(&d.Log.Topics, []string{}, ","), "log.topics", "CORE_LOG_TOPICS", nil, "Show only selected log topics", false, false)
d.val(newIntValue(&d.Log.MaxLines, 1000), "log.max_lines", "CORE_LOG_MAXLINES", nil, "Number of latest log lines to keep in memory", false, false)
// DB
d.val(newMustDirValue(&d.DB.Dir, "./config"), "db.dir", "CORE_DB_DIR", nil, "Directory for holding the operational data", false, false)
// Host
d.val(newStringListValue(&d.Host.Name, []string{}, ","), "host.name", "CORE_HOST_NAME", nil, "Comma separated list of public host/domain names or IPs", false, false)
d.val(newBoolValue(&d.Host.Auto, true), "host.auto", "CORE_HOST_AUTO", nil, "Enable detection of public IP addresses", false, false)
// API
d.val(newBoolValue(&d.API.ReadOnly, false), "api.read_only", "CORE_API_READ_ONLY", nil, "Allow only ready only access to the API", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTP.Allow, []string{}, ","), "api.access.http.allow", "CORE_API_ACCESS_HTTP_ALLOW", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTP.Block, []string{}, ","), "api.access.http.block", "CORE_API_ACCESS_HTTP_BLOCK", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTPS.Allow, []string{}, ","), "api.access.https.allow", "CORE_API_ACCESS_HTTPS_ALLOW", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTPS.Block, []string{}, ","), "api.access.https.block", "CORE_API_ACCESS_HTTPS_BLOCK", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.val(newBoolValue(&d.API.Auth.Enable, false), "api.auth.enable", "CORE_API_AUTH_ENABLE", nil, "Enable authentication for all clients", false, false)
d.val(newBoolValue(&d.API.Auth.DisableLocalhost, false), "api.auth.disable_localhost", "CORE_API_AUTH_DISABLE_LOCALHOST", nil, "Disable authentication for clients from localhost", false, false)
d.val(newStringValue(&d.API.Auth.Username, ""), "api.auth.username", "CORE_API_AUTH_USERNAME", []string{"RS_USERNAME"}, "Username", false, false)
d.val(newStringValue(&d.API.Auth.Password, ""), "api.auth.password", "CORE_API_AUTH_PASSWORD", []string{"RS_PASSWORD"}, "Password", false, true)
// Auth JWT
d.val(newStringValue(&d.API.Auth.JWT.Secret, rand.String(32)), "api.auth.jwt.secret", "CORE_API_AUTH_JWT_SECRET", nil, "JWT secret, leave empty for generating a random value", false, true)
// Auth Auth0
d.val(newBoolValue(&d.API.Auth.Auth0.Enable, false), "api.auth.auth0.enable", "CORE_API_AUTH_AUTH0_ENABLE", nil, "Enable Auth0", false, false)
d.val(newTenantListValue(&d.API.Auth.Auth0.Tenants, []Auth0Tenant{}, ","), "api.auth.auth0.tenants", "CORE_API_AUTH_AUTH0_TENANTS", nil, "List of Auth0 tenants", false, false)
// TLS
d.val(newAddressValue(&d.TLS.Address, ":8181"), "tls.address", "CORE_TLS_ADDRESS", nil, "HTTPS listening address", false, false)
d.val(newBoolValue(&d.TLS.Enable, false), "tls.enable", "CORE_TLS_ENABLE", nil, "Enable HTTPS", false, false)
d.val(newBoolValue(&d.TLS.Auto, false), "tls.auto", "CORE_TLS_AUTO", nil, "Enable Let's Encrypt certificate", false, false)
d.val(newFileValue(&d.TLS.CertFile, ""), "tls.cert_file", "CORE_TLS_CERTFILE", nil, "Path to certificate file in PEM format", false, false)
d.val(newFileValue(&d.TLS.KeyFile, ""), "tls.key_file", "CORE_TLS_KEYFILE", nil, "Path to key file in PEM format", false, false)
// Storage
d.val(newFileValue(&d.Storage.MimeTypes, "./mime.types"), "storage.mimetypes_file", "CORE_STORAGE_MIMETYPES_FILE", []string{"CORE_MIMETYPES_FILE"}, "Path to file with mime-types", false, false)
// Storage (Disk)
d.val(newMustDirValue(&d.Storage.Disk.Dir, "./data"), "storage.disk.dir", "CORE_STORAGE_DISK_DIR", nil, "Directory on disk, exposed on /", false, false)
d.val(newInt64Value(&d.Storage.Disk.Size, 0), "storage.disk.max_size_mbytes", "CORE_STORAGE_DISK_MAXSIZEMBYTES", nil, "Max. allowed megabytes for storage.disk.dir, 0 for unlimited", false, false)
d.val(newBoolValue(&d.Storage.Disk.Cache.Enable, true), "storage.disk.cache.enable", "CORE_STORAGE_DISK_CACHE_ENABLE", nil, "Enable cache for /", false, false)
d.val(newUint64Value(&d.Storage.Disk.Cache.Size, 0), "storage.disk.cache.max_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES", nil, "Max. allowed cache size, 0 for unlimited", false, false)
d.val(newInt64Value(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTLSECONDS", nil, "Seconds to keep files in cache", false, false)
d.val(newUint64Value(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES", nil, "Max. file size to put in cache", false, false)
d.val(newStringListValue(&d.Storage.Disk.Cache.Types, []string{}, " "), "storage.disk.cache.types", "CORE_STORAGE_DISK_CACHE_TYPES", nil, "File extensions to cache, empty for all", false, false)
// Storage (Memory)
d.val(newBoolValue(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false)
d.val(newStringValue(&d.Storage.Memory.Auth.Username, "admin"), "storage.memory.auth.username", "CORE_STORAGE_MEMORY_AUTH_USERNAME", nil, "Username for Basic-Auth of /memfs", false, false)
d.val(newStringValue(&d.Storage.Memory.Auth.Password, rand.StringAlphanumeric(18)), "storage.memory.auth.password", "CORE_STORAGE_MEMORY_AUTH_PASSWORD", nil, "Password for Basic-Auth of /memfs", false, true)
d.val(newInt64Value(&d.Storage.Memory.Size, 0), "storage.memory.max_size_mbytes", "CORE_STORAGE_MEMORY_MAXSIZEMBYTES", nil, "Max. allowed megabytes for /memfs, 0 for unlimited", false, false)
d.val(newBoolValue(&d.Storage.Memory.Purge, false), "storage.memory.purge", "CORE_STORAGE_MEMORY_PURGE", nil, "Automatically remove the oldest files if /memfs is full", false, false)
// Storage (CORS)
d.val(newCORSOriginsValue(&d.Storage.CORS.Origins, []string{"*"}, ","), "storage.cors.origins", "CORE_STORAGE_CORS_ORIGINS", nil, "Allowed CORS origins for /memfs and /data", false, false)
// RTMP
d.val(newBoolValue(&d.RTMP.Enable, false), "rtmp.enable", "CORE_RTMP_ENABLE", nil, "Enable RTMP server", false, false)
d.val(newBoolValue(&d.RTMP.EnableTLS, false), "rtmp.enable_tls", "CORE_RTMP_ENABLE_TLS", nil, "Enable RTMPS server instead of RTMP", false, false)
d.val(newAddressValue(&d.RTMP.Address, ":1935"), "rtmp.address", "CORE_RTMP_ADDRESS", nil, "RTMP server listen address", false, false)
d.val(newStringValue(&d.RTMP.App, "/"), "rtmp.app", "CORE_RTMP_APP", nil, "RTMP app for publishing", false, false)
d.val(newStringValue(&d.RTMP.Token, ""), "rtmp.token", "CORE_RTMP_TOKEN", nil, "RTMP token for publishing and playing", false, true)
// FFmpeg
d.val(newExecValue(&d.FFmpeg.Binary, "ffmpeg"), "ffmpeg.binary", "CORE_FFMPEG_BINARY", nil, "Path to ffmpeg binary", true, false)
d.val(newInt64Value(&d.FFmpeg.MaxProcesses, 0), "ffmpeg.max_processes", "CORE_FFMPEG_MAXPROCESSES", nil, "Max. allowed simultaneously running ffmpeg instances, 0 for unlimited", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Input.Allow, []string{}, " "), "ffmpeg.access.input.allow", "CORE_FFMPEG_ACCESS_INPUT_ALLOW", nil, "List of allowed expression to match against the input addresses", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Input.Block, []string{}, " "), "ffmpeg.access.input.block", "CORE_FFMPEG_ACCESS_INPUT_BLOCK", nil, "List of blocked expression to match against the input addresses", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Output.Allow, []string{}, " "), "ffmpeg.access.output.allow", "CORE_FFMPEG_ACCESS_OUTPUT_ALLOW", nil, "List of allowed expression to match against the output addresses", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Output.Block, []string{}, " "), "ffmpeg.access.output.block", "CORE_FFMPEG_ACCESS_OUTPUT_BLOCK", nil, "List of blocked expression to match against the output addresses", false, false)
d.val(newIntValue(&d.FFmpeg.Log.MaxLines, 50), "ffmpeg.log.max_lines", "CORE_FFMPEG_LOG_MAXLINES", nil, "Number of latest log lines to keep for each process", false, false)
d.val(newIntValue(&d.FFmpeg.Log.MaxHistory, 3), "ffmpeg.log.max_history", "CORE_FFMPEG_LOG_MAXHISTORY", nil, "Number of latest logs to keep for each process", false, false)
// Playout
d.val(newBoolValue(&d.Playout.Enable, false), "playout.enable", "CORE_PLAYOUT_ENABLE", nil, "Enable playout proxy where available", false, false)
d.val(newPortValue(&d.Playout.MinPort, 0), "playout.min_port", "CORE_PLAYOUT_MINPORT", nil, "Min. playout server port", false, false)
d.val(newPortValue(&d.Playout.MaxPort, 0), "playout.max_port", "CORE_PLAYOUT_MAXPORT", nil, "Max. playout server port", false, false)
// Debug
d.val(newBoolValue(&d.Debug.Profiling, false), "debug.profiling", "CORE_DEBUG_PROFILING", nil, "Enable profiling endpoint on /profiling", false, false)
d.val(newIntValue(&d.Debug.ForceGC, 0), "debug.force_gc", "CORE_DEBUG_FORCEGC", nil, "Number of seconds between forcing GC to return memory to the OS", false, false)
// Metrics
d.val(newBoolValue(&d.Metrics.Enable, false), "metrics.enable", "CORE_METRICS_ENABLE", nil, "Enable collecting historic metrics data", false, false)
d.val(newBoolValue(&d.Metrics.EnablePrometheus, false), "metrics.enable_prometheus", "CORE_METRICS_ENABLE_PROMETHEUS", nil, "Enable prometheus endpoint /metrics", false, false)
d.val(newInt64Value(&d.Metrics.Range, 300), "metrics.range_seconds", "CORE_METRICS_RANGE_SECONDS", nil, "Seconds to keep history data", false, false)
d.val(newInt64Value(&d.Metrics.Interval, 2), "metrics.interval_seconds", "CORE_METRICS_INTERVAL_SECONDS", nil, "Interval for collecting metrics", false, false)
// Sessions
d.val(newBoolValue(&d.Sessions.Enable, true), "sessions.enable", "CORE_SESSIONS_ENABLE", nil, "Enable collecting HLS session stats for /memfs", false, false)
d.val(newCIDRListValue(&d.Sessions.IPIgnoreList, []string{"127.0.0.1/32", "::1/128"}, ","), "sessions.ip_ignorelist", "CORE_SESSIONS_IP_IGNORELIST", nil, "List of IP ranges in CIDR notation to ignore", false, false)
d.val(newIntValue(&d.Sessions.SessionTimeout, 30), "sessions.session_timeout_sec", "CORE_SESSIONS_SESSION_TIMEOUT_SEC", nil, "Timeout for an idle session", false, false)
d.val(newBoolValue(&d.Sessions.Persist, false), "sessions.persist", "CORE_SESSIONS_PERSIST", nil, "Whether to persist session history. Will be stored as sessions.json in db.dir", false, false)
d.val(newIntValue(&d.Sessions.PersistInterval, 300), "sessions.persist_interval_sec", "CORE_SESSIONS_PERSIST_INTERVAL_SEC", nil, "Interval in seconds in which to persist the current session history", false, false)
d.val(newUint64Value(&d.Sessions.MaxBitrate, 0), "sessions.max_bitrate_mbit", "CORE_SESSIONS_MAXBITRATE_MBIT", nil, "Max. allowed outgoing bitrate in mbit/s, 0 for unlimited", false, false)
d.val(newUint64Value(&d.Sessions.MaxSessions, 0), "sessions.max_sessions", "CORE_SESSIONS_MAXSESSIONS", nil, "Max. allowed number of simultaneous sessions, 0 for unlimited", false, false)
// Service
d.val(newBoolValue(&d.Service.Enable, false), "service.enable", "CORE_SERVICE_ENABLE", nil, "Enable connecting to the Restreamer Service", false, false)
d.val(newStringValue(&d.Service.Token, ""), "service.token", "CORE_SERVICE_TOKEN", nil, "Restreamer Service account token", false, true)
d.val(newURLValue(&d.Service.URL, "https://service.datarhei.com"), "service.url", "CORE_SERVICE_URL", nil, "URL of the Restreamer Service", false, false)
// Router
d.val(newStringListValue(&d.Router.BlockedPrefixes, []string{"/api"}, ","), "router.blocked_prefixes", "CORE_ROUTER_BLOCKED_PREFIXES", nil, "List of path prefixes that can't be routed", false, false)
d.val(newStringMapStringValue(&d.Router.Routes, nil), "router.routes", "CORE_ROUTER_ROUTES", nil, "List of route mappings", false, false)
d.val(newDirValue(&d.Router.UIPath, ""), "router.ui_path", "CORE_ROUTER_UI_PATH", nil, "Path to a directory holding UI files mounted as /ui", false, false)
}
func (d *Config) val(val value, name, envName string, envAltNames []string, description string, required, disguise bool) {
d.vars = append(d.vars, &variable{
value: val,
defVal: val.String(),
name: name,
envName: envName,
envAltNames: envAltNames,
description: description,
required: required,
disguise: disguise,
})
}
func (d *Config) log(level string, v *variable, format string, args ...interface{}) {
variable := Variable{
Value: v.value.String(),
Name: v.name,
EnvName: v.envName,
Description: v.description,
Merged: v.merged,
}
if v.disguise {
variable.Value = "***"
}
l := message{
message: fmt.Sprintf(format, args...),
variable: variable,
level: level,
}
d.logs = append(d.logs, l)
}
// Merge merges the values of the known environment variables into the configuration
func (d *Config) Merge() {
for _, v := range d.vars {
if len(v.envName) == 0 {
continue
}
var envval string
var ok bool
envval, ok = os.LookupEnv(v.envName)
if !ok {
foundAltName := false
for _, envName := range v.envAltNames {
envval, ok = os.LookupEnv(envName)
if ok {
foundAltName = true
d.log("warn", v, "deprecated name, please use %s", v.envName)
break
}
}
if !foundAltName {
continue
}
}
err := v.value.Set(envval)
if err != nil {
d.log("error", v, "%s", err.Error())
}
v.merged = true
}
}
// Validate validates the current state of the Config for completeness and sanity. Errors are
// written to the log. Use resetLogs to indicate to reset the logs prior validation.
func (d *Config) Validate(resetLogs bool) {
if resetLogs {
d.logs = nil
}
if d.Version != 1 {
d.log("error", d.findVariable("version"), "unknown configuration layout version")
return
}
for _, v := range d.vars {
d.log("info", v, "%s", "")
err := v.value.Validate()
if err != nil {
d.log("error", v, "%s", err.Error())
}
if v.required && v.value.IsEmpty() {
d.log("error", v, "a value is required")
}
}
// Individual sanity checks
// If HTTP Auth is enabled, check that the username and password are set
if d.API.Auth.Enable {
if len(d.API.Auth.Username) == 0 || len(d.API.Auth.Password) == 0 {
d.log("error", d.findVariable("api.auth.enable"), "api.auth.username and api.auth.password must be set")
}
}
// If Auth0 is enabled, check that domain, audience, and clientid are set
if d.API.Auth.Auth0.Enable {
if len(d.API.Auth.Auth0.Tenants) == 0 {
d.log("error", d.findVariable("api.auth.auth0.enable"), "at least one tenants must be set")
}
for i, t := range d.API.Auth.Auth0.Tenants {
if len(t.Domain) == 0 || len(t.Audience) == 0 || len(t.ClientID) == 0 {
d.log("error", d.findVariable("api.auth.auth0.tenants"), "domain, audience, and clientid must be set (tenant %d)", i)
}
}
}
// If TLS is enabled and Let's Encrypt is disabled, require certfile and keyfile
if d.TLS.Enable && !d.TLS.Auto {
if len(d.TLS.CertFile) == 0 || len(d.TLS.KeyFile) == 0 {
d.log("error", d.findVariable("tls.enable"), "tls.certfile and tls.keyfile must be set")
}
}
// If TLS and Let's Encrypt certificate is enabled, we require a public hostname
if d.TLS.Enable && d.TLS.Auto {
if len(d.Host.Name) == 0 {
d.log("error", d.findVariable("host.name"), "a hostname must be set in order to get an automatic TLS certificate")
} else {
r := &net.Resolver{
PreferGo: true,
StrictErrors: true,
}
for _, host := range d.Host.Name {
// Don't lookup IP addresses
if ip := net.ParseIP(host); ip != nil {
d.log("error", d.findVariable("host.name"), "only host names are allowed if automatic TLS is enabled, but found IP address: %s", host)
}
// Lookup host name with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
_, err := r.LookupHost(ctx, host)
if err != nil {
d.log("error", d.findVariable("host.name"), "the host '%s' can't be resolved and will not work with automatic TLS", host)
}
cancel()
}
}
}
// If TLS for RTMP is enabled, TLS must be enabled
if d.RTMP.EnableTLS {
if !d.TLS.Enable {
d.log("error", d.findVariable("rtmp.enable_tls"), "RTMPS server can only be enabled if TLS is enabled")
}
}
// If CORE_MEMFS_USERNAME and CORE_MEMFS_PASSWORD are set, automatically active/deactivate Basic-Auth for memfs
if d.findVariable("storage.memory.auth.username").merged && d.findVariable("storage.memory.auth.password").merged {
d.Storage.Memory.Auth.Enable = true
if len(d.Storage.Memory.Auth.Username) == 0 && len(d.Storage.Memory.Auth.Password) == 0 {
d.Storage.Memory.Auth.Enable = false
}
}
// If Basic-Auth for memfs is enable, check that the username and password are set
if d.Storage.Memory.Auth.Enable {
if len(d.Storage.Memory.Auth.Username) == 0 || len(d.Storage.Memory.Auth.Password) == 0 {
d.log("error", d.findVariable("storage.memory.auth.enable"), "storage.memory.auth.username and storage.memory.auth.password must be set")
}
}
// If playout is enabled, check that the port range is sane
if d.Playout.Enable {
if d.Playout.MinPort >= d.Playout.MaxPort {
d.log("error", d.findVariable("playout.min_port"), "must be bigger than playout.max_port")
}
}
// If cache is enabled, a valid TTL has to be set to a useful value
if d.Storage.Disk.Cache.Enable && d.Storage.Disk.Cache.TTL < 0 {
d.log("error", d.findVariable("storage.disk.cache.ttl_seconds"), "must be equal or greater than 0")
}
// If the stats are enabled, the session timeout has to be set to a useful value
if d.Sessions.Enable && d.Sessions.SessionTimeout < 1 {
d.log("error", d.findVariable("stats.session_timeout_sec"), "must be equal or greater than 1")
}
// If the stats and their persistence are enabled, the persist interval has to be set to a useful value
if d.Sessions.Enable && d.Sessions.PersistInterval < 0 {
d.log("error", d.findVariable("stats.persist_interval_sec"), "must be at equal or greater than 0")
}
// If the service is enabled, the token and enpoint have to be defined
if d.Service.Enable {
if len(d.Service.Token) == 0 {
d.log("error", d.findVariable("service.token"), "must be non-empty")
}
if len(d.Service.URL) == 0 {
d.log("error", d.findVariable("service.url"), "must be non-empty")
}
}
// If historic metrics are enabled, the timerange and interval have to be valid
if d.Metrics.Enable {
if d.Metrics.Range <= 0 {
d.log("error", d.findVariable("metrics.range"), "must be greater 0")
}
if d.Metrics.Interval <= 0 {
d.log("error", d.findVariable("metrics.interval"), "must be greater 0")
}
if d.Metrics.Interval > d.Metrics.Range {
d.log("error", d.findVariable("metrics.interval"), "must be smaller than the range")
}
}
}
func (d *Config) findVariable(name string) *variable {
for _, v := range d.vars {
if v.name == name {
return v
}
}
return nil
}
// Messages calls for each log entry the provided callback. The level has the values 'error', 'warn', or 'info'.
// The name is the name of the configuration value, e.g. 'api.auth.enable'. The message is the log message.
func (d *Config) Messages(logger func(level string, v Variable, message string)) {
for _, l := range d.logs {
logger(l.level, l.variable, l.message)
}
}
// HasErrors returns whether there are some error messages in the log.
func (d *Config) HasErrors() bool {
for _, l := range d.logs {
if l.level == "error" {
return true
}
}
return false
}
// Overrides returns a list of configuration value names that have been overriden by an environment variable.
func (d *Config) Overrides() []string {
overrides := []string{}
for _, v := range d.vars {
if v.merged {
overrides = append(overrides, v.name)
}
}
return overrides
}
func copyStringSlice(src []string) []string {
dst := make([]string, len(src))
copy(dst, src)
return dst
}
func copyStringMap(src map[string]string) map[string]string {
dst := make(map[string]string)
for k, v := range src {
dst[k] = v
}
return dst
}
func copyTenantSlice(src []Auth0Tenant) []Auth0Tenant {
dst := make([]Auth0Tenant, len(src))
copy(dst, src)
return dst
}

55
config/config_test.go Normal file
View File

@@ -0,0 +1,55 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfigCopy(t *testing.T) {
config1 := New()
config1.Version = 42
config1.DB.Dir = "foo"
val1 := config1.findVariable("version")
val2 := config1.findVariable("db.dir")
val3 := config1.findVariable("host.name")
assert.Equal(t, "42", val1.value.String())
assert.Equal(t, nil, val1.value.Validate())
assert.Equal(t, false, val1.value.IsEmpty())
assert.Equal(t, "foo", val2.value.String())
assert.Equal(t, "(empty)", val3.value.String())
val3.value.Set("foo.com")
assert.Equal(t, "foo.com", val3.value.String())
config2 := NewConfigFrom(config1)
assert.Equal(t, int64(42), config2.Version)
assert.Equal(t, "foo", config2.DB.Dir)
assert.Equal(t, []string{"foo.com"}, config2.Host.Name)
val1.value.Set("77")
assert.Equal(t, int64(77), config1.Version)
assert.Equal(t, int64(42), config2.Version)
val2.value.Set("bar")
assert.Equal(t, "bar", config1.DB.Dir)
assert.Equal(t, "foo", config2.DB.Dir)
config2.DB.Dir = "baz"
assert.Equal(t, "bar", config1.DB.Dir)
assert.Equal(t, "baz", config2.DB.Dir)
config1.Host.Name[0] = "bar.com"
assert.Equal(t, []string{"bar.com"}, config1.Host.Name)
assert.Equal(t, []string{"foo.com"}, config2.Host.Name)
}

52
config/dummy.go Normal file
View File

@@ -0,0 +1,52 @@
package config
import "fmt"
type dummyStore struct{}
// NewDummyStore returns a store that returns the default config
func NewDummyStore() Store {
return &dummyStore{}
}
func (c *dummyStore) Get() *Config {
cfg := New()
cfg.DB.Dir = "."
cfg.Storage.Disk.Dir = "."
return cfg
}
func (c *dummyStore) Set(d *Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
return nil
}
func (c *dummyStore) GetActive() *Config {
cfg := New()
cfg.DB.Dir = "."
cfg.Storage.Disk.Dir = "."
return cfg
}
func (c *dummyStore) SetActive(d *Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
return nil
}
func (c *dummyStore) Reload() error {
return nil
}

71
config/ip.go Normal file
View File

@@ -0,0 +1,71 @@
package config
import (
"io/ioutil"
"net/http"
"sync"
"time"
)
// SetPublicIPs will try to figure out the public IPs (v4 and v6)
// we're running on. There's a timeout of max. 5 seconds to do it.
// If it fails, the IPs will simply not be set.
func (d *Config) SetPublicIPs() {
var wg sync.WaitGroup
ipv4 := ""
ipv6 := ""
wg.Add(2)
go func() {
defer wg.Done()
ipv4 = doRequest("https://api.ipify.org")
}()
go func() {
defer wg.Done()
ipv6 = doRequest("https://api6.ipify.org")
}()
wg.Wait()
if len(ipv4) != 0 {
d.Host.Name = append(d.Host.Name, ipv4)
}
if len(ipv6) != 0 && ipv4 != ipv6 {
d.Host.Name = append(d.Host.Name, ipv6)
}
}
func doRequest(url string) string {
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ""
}
if resp.StatusCode != 200 {
return ""
}
return string(body)
}

163
config/json.go Normal file
View File

@@ -0,0 +1,163 @@
package config
import (
gojson "encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/datarhei/core/encoding/json"
"github.com/datarhei/core/io/file"
)
type jsonStore struct {
path string
data map[string]*Config
reloadFn func()
}
// NewJSONStore will read a JSON config file from the given path. After successfully reading it in, it will be written
// back to the path. The returned error will be nil if everything went fine.
// If the path doesn't exist, a default JSON config file will be written to that path.
// The returned ConfigStore can be used to retrieve or write the config.
func NewJSONStore(path string, reloadFn func()) (Store, error) {
c := &jsonStore{
path: path,
data: make(map[string]*Config),
reloadFn: reloadFn,
}
c.data["base"] = New()
if err := c.load(c.data["base"]); err != nil {
return nil, fmt.Errorf("failed to read JSON from '%s': %w", path, err)
}
if err := c.store(c.data["base"]); err != nil {
return nil, fmt.Errorf("failed to write JSON to '%s': %w", path, err)
}
return c, nil
}
func (c *jsonStore) Get() *Config {
return NewConfigFrom(c.data["base"])
}
func (c *jsonStore) Set(d *Config) error {
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
data := NewConfigFrom(d)
data.CreatedAt = time.Now()
if err := c.store(data); err != nil {
return fmt.Errorf("failed to write JSON to '%s': %w", c.path, err)
}
data.UpdatedAt = time.Now()
c.data["base"] = data
return nil
}
func (c *jsonStore) GetActive() *Config {
if x, ok := c.data["merged"]; ok {
return NewConfigFrom(x)
}
if x, ok := c.data["base"]; ok {
return NewConfigFrom(x)
}
return nil
}
func (c *jsonStore) SetActive(d *Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
c.data["merged"] = NewConfigFrom(d)
return nil
}
func (c *jsonStore) Reload() error {
if c.reloadFn == nil {
return nil
}
c.reloadFn()
return nil
}
func (c *jsonStore) load(data *Config) error {
if len(c.path) == 0 {
return nil
}
if _, err := os.Stat(c.path); os.IsNotExist(err) {
return nil
}
jsondata, err := ioutil.ReadFile(c.path)
if err != nil {
return err
}
if err = gojson.Unmarshal(jsondata, data); err != nil {
return json.FormatError(jsondata, err)
}
data.LoadedAt = time.Now()
data.UpdatedAt = data.LoadedAt
return nil
}
func (c *jsonStore) store(data *Config) error {
data.CreatedAt = time.Now()
if len(c.path) == 0 {
return nil
}
jsondata, err := gojson.MarshalIndent(data, "", " ")
if err != nil {
return err
}
dir, filename := filepath.Split(c.path)
tmpfile, err := ioutil.TempFile(dir, filename)
if err != nil {
return err
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(jsondata); err != nil {
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
if err := file.Rename(tmpfile.Name(), c.path); err != nil {
return err
}
return nil
}

23
config/store.go Normal file
View File

@@ -0,0 +1,23 @@
package config
// Store is a store for the configuration data.
type Store interface {
// Get the current configuration.
Get() *Config
// Set a new configuration for persistence.
Set(data *Config) error
// GetActive returns the configuration that has been set as
// active before, otherwise it return nil.
GetActive() *Config
// SetActive will keep the given configuration
// as active in memory. It can be retrieved later with GetActive()
SetActive(data *Config) error
// Reload will reload the stored configuration. It has to make sure
// that all affected components will receiver their potentially
// changed configuration.
Reload() error
}

774
config/types.go Normal file
View File

@@ -0,0 +1,774 @@
package config
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/datarhei/core/http/cors"
)
type value interface {
// String returns a string representation of the value.
String() string
// Set a new value for the value. Returns an
// error if the given string representation can't
// be transformed to the value. Returns nil
// if the new value has been set.
Set(string) error
// Validate the value. The returned error will
// indicate what is wrong with the current value.
// Returns nil if the value is OK.
Validate() error
// IsEmpty returns whether the value represents an empty
// representation for that value.
IsEmpty() bool
}
// string
type stringValue string
func newStringValue(p *string, val string) *stringValue {
*p = val
return (*stringValue)(p)
}
func (s *stringValue) Set(val string) error {
*s = stringValue(val)
return nil
}
func (s *stringValue) String() string {
return string(*s)
}
func (s *stringValue) Validate() error {
return nil
}
func (s *stringValue) IsEmpty() bool {
return len(string(*s)) == 0
}
// address (host?:port)
type addressValue string
func newAddressValue(p *string, val string) *addressValue {
*p = val
return (*addressValue)(p)
}
func (s *addressValue) Set(val string) error {
// Check if the new value is only a port number
re := regexp.MustCompile("^[0-9]+$")
if re.MatchString(val) {
val = ":" + val
}
*s = addressValue(val)
return nil
}
func (s *addressValue) String() string {
return string(*s)
}
func (s *addressValue) Validate() error {
_, port, err := net.SplitHostPort(string(*s))
if err != nil {
return err
}
re := regexp.MustCompile("^[0-9]+$")
if !re.MatchString(port) {
return fmt.Errorf("the port must be numerical")
}
return nil
}
func (s *addressValue) IsEmpty() bool {
return s.Validate() != nil
}
// array of strings
type stringListValue struct {
p *[]string
separator string
}
func newStringListValue(p *[]string, val []string, separator string) *stringListValue {
v := &stringListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *stringListValue) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *stringListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *stringListValue) Validate() error {
return nil
}
func (s *stringListValue) IsEmpty() bool {
return len(*s.p) == 0
}
// array of auth0 tenants
type tenantListValue struct {
p *[]Auth0Tenant
separator string
}
func newTenantListValue(p *[]Auth0Tenant, val []Auth0Tenant, separator string) *tenantListValue {
v := &tenantListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *tenantListValue) Set(val string) error {
list := []Auth0Tenant{}
for i, elm := range strings.Split(val, s.separator) {
data, err := base64.StdEncoding.DecodeString(elm)
if err != nil {
return fmt.Errorf("invalid base64 encoding of tenant %d: %w", i, err)
}
t := Auth0Tenant{}
if err := json.Unmarshal(data, &t); err != nil {
return fmt.Errorf("invalid JSON in tenant %d: %w", i, err)
}
list = append(list, t)
}
*s.p = list
return nil
}
func (s *tenantListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
list := []string{}
for _, t := range *s.p {
list = append(list, fmt.Sprintf("%s (%d users)", t.Domain, len(t.Users)))
}
return strings.Join(list, ",")
}
func (s *tenantListValue) Validate() error {
for i, t := range *s.p {
if len(t.Domain) == 0 {
return fmt.Errorf("the domain for tenant %d is missing", i)
}
if len(t.Audience) == 0 {
return fmt.Errorf("the audience for tenant %d is missing", i)
}
}
return nil
}
func (s *tenantListValue) IsEmpty() bool {
return len(*s.p) == 0
}
// map of strings to strings
type stringMapStringValue struct {
p *map[string]string
}
func newStringMapStringValue(p *map[string]string, val map[string]string) *stringMapStringValue {
v := &stringMapStringValue{
p: p,
}
if *p == nil {
*p = make(map[string]string)
}
if val != nil {
*p = val
}
return v
}
func (s *stringMapStringValue) Set(val string) error {
mappings := make(map[string]string)
for _, elm := range strings.Split(val, " ") {
elm = strings.TrimSpace(elm)
if len(elm) == 0 {
continue
}
mapping := strings.SplitN(elm, ":", 2)
mappings[mapping[0]] = mapping[1]
}
*s.p = mappings
return nil
}
func (s *stringMapStringValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
mappings := make([]string, len(*s.p))
i := 0
for k, v := range *s.p {
mappings[i] = k + ":" + v
i++
}
return strings.Join(mappings, " ")
}
func (s *stringMapStringValue) Validate() error {
return nil
}
func (s *stringMapStringValue) IsEmpty() bool {
return len(*s.p) == 0
}
// array of CIDR notation IP adresses
type cidrListValue struct {
p *[]string
separator string
}
func newCIDRListValue(p *[]string, val []string, separator string) *cidrListValue {
v := &cidrListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *cidrListValue) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *cidrListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *cidrListValue) Validate() error {
for _, cidr := range *s.p {
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
}
return nil
}
func (s *cidrListValue) IsEmpty() bool {
return len(*s.p) == 0
}
// array of origins for CORS
type corsOriginsValue struct {
p *[]string
separator string
}
func newCORSOriginsValue(p *[]string, val []string, separator string) *corsOriginsValue {
v := &corsOriginsValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *corsOriginsValue) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *corsOriginsValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *corsOriginsValue) Validate() error {
return cors.Validate(*s.p)
}
func (s *corsOriginsValue) IsEmpty() bool {
return len(*s.p) == 0
}
// boolean
type boolValue bool
func newBoolValue(p *bool, val bool) *boolValue {
*p = val
return (*boolValue)(p)
}
func (b *boolValue) Set(val string) error {
v, err := strconv.ParseBool(val)
if err != nil {
return err
}
*b = boolValue(v)
return nil
}
func (b *boolValue) String() string {
return strconv.FormatBool(bool(*b))
}
func (b *boolValue) Validate() error {
return nil
}
func (b *boolValue) IsEmpty() bool {
return !bool(*b)
}
// int
type intValue int
func newIntValue(p *int, val int) *intValue {
*p = val
return (*intValue)(p)
}
func (i *intValue) Set(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return err
}
*i = intValue(v)
return nil
}
func (i *intValue) String() string {
return strconv.Itoa(int(*i))
}
func (i *intValue) Validate() error {
return nil
}
func (i *intValue) IsEmpty() bool {
return int(*i) == 0
}
// int64
type int64Value int64
func newInt64Value(p *int64, val int64) *int64Value {
*p = val
return (*int64Value)(p)
}
func (u *int64Value) Set(val string) error {
v, err := strconv.ParseInt(val, 0, 64)
if err != nil {
return err
}
*u = int64Value(v)
return nil
}
func (u *int64Value) String() string {
return strconv.FormatInt(int64(*u), 10)
}
func (u *int64Value) Validate() error {
return nil
}
func (u *int64Value) IsEmpty() bool {
return int64(*u) == 0
}
// uint64
type uint64Value uint64
func newUint64Value(p *uint64, val uint64) *uint64Value {
*p = val
return (*uint64Value)(p)
}
func (u *uint64Value) Set(val string) error {
v, err := strconv.ParseUint(val, 0, 64)
if err != nil {
return err
}
*u = uint64Value(v)
return nil
}
func (u *uint64Value) String() string {
return strconv.FormatUint(uint64(*u), 10)
}
func (u *uint64Value) Validate() error {
return nil
}
func (u *uint64Value) IsEmpty() bool {
return uint64(*u) == 0
}
// network port
type portValue int
func newPortValue(p *int, val int) *portValue {
*p = val
return (*portValue)(p)
}
func (i *portValue) Set(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return err
}
*i = portValue(v)
return nil
}
func (i *portValue) String() string {
return strconv.Itoa(int(*i))
}
func (i *portValue) Validate() error {
val := int(*i)
if val < 0 || val >= (1<<16) {
return fmt.Errorf("%d is not in the range of [0, %d]", val, 1<<16-1)
}
return nil
}
func (i *portValue) IsEmpty() bool {
return int(*i) == 0
}
// must directory
type mustDirValue string
func newMustDirValue(p *string, val string) *mustDirValue {
*p = val
return (*mustDirValue)(p)
}
func (u *mustDirValue) Set(val string) error {
*u = mustDirValue(val)
return nil
}
func (u *mustDirValue) String() string {
return string(*u)
}
func (u *mustDirValue) Validate() error {
val := string(*u)
if len(strings.TrimSpace(val)) == 0 {
return fmt.Errorf("path name must not be empty")
}
finfo, err := os.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.IsDir() {
return fmt.Errorf("%s is not a directory", val)
}
return nil
}
func (u *mustDirValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// directory
type dirValue string
func newDirValue(p *string, val string) *dirValue {
*p = val
return (*dirValue)(p)
}
func (u *dirValue) Set(val string) error {
*u = dirValue(val)
return nil
}
func (u *dirValue) String() string {
return string(*u)
}
func (u *dirValue) Validate() error {
val := string(*u)
if len(strings.TrimSpace(val)) == 0 {
return nil
}
finfo, err := os.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.IsDir() {
return fmt.Errorf("%s is not a directory", val)
}
return nil
}
func (u *dirValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// executable
type execValue string
func newExecValue(p *string, val string) *execValue {
*p = val
return (*execValue)(p)
}
func (u *execValue) Set(val string) error {
*u = execValue(val)
return nil
}
func (u *execValue) String() string {
return string(*u)
}
func (u *execValue) Validate() error {
val := string(*u)
_, err := exec.LookPath(val)
if err != nil {
return fmt.Errorf("%s not found or is not executable", val)
}
return nil
}
func (u *execValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// regular file
type fileValue string
func newFileValue(p *string, val string) *fileValue {
*p = val
return (*fileValue)(p)
}
func (u *fileValue) Set(val string) error {
*u = fileValue(val)
return nil
}
func (u *fileValue) String() string {
return string(*u)
}
func (u *fileValue) Validate() error {
val := string(*u)
if len(val) == 0 {
return nil
}
finfo, err := os.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", val)
}
return nil
}
func (u *fileValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// time
type timeValue time.Time
func newTimeValue(p *time.Time, val time.Time) *timeValue {
*p = val
return (*timeValue)(p)
}
func (u *timeValue) Set(val string) error {
v, err := time.Parse(time.RFC3339, val)
if err != nil {
return err
}
*u = timeValue(v)
return nil
}
func (u *timeValue) String() string {
v := time.Time(*u)
return v.Format(time.RFC3339)
}
func (u *timeValue) Validate() error {
return nil
}
func (u *timeValue) IsEmpty() bool {
v := time.Time(*u)
return v.IsZero()
}
// url
type urlValue string
func newURLValue(p *string, val string) *urlValue {
*p = val
return (*urlValue)(p)
}
func (u *urlValue) Set(val string) error {
*u = urlValue(val)
return nil
}
func (u *urlValue) String() string {
return string(*u)
}
func (u *urlValue) Validate() error {
val := string(*u)
if len(val) == 0 {
return nil
}
URL, err := url.Parse(val)
if err != nil {
return fmt.Errorf("%s is not a valid URL", val)
}
if len(URL.Scheme) == 0 || len(URL.Host) == 0 {
return fmt.Errorf("%s is not a valid URL", val)
}
return nil
}
func (u *urlValue) IsEmpty() bool {
return len(string(*u)) == 0
}

58
config/types_test.go Normal file
View File

@@ -0,0 +1,58 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIntValue(t *testing.T) {
var i int
ivar := newIntValue(&i, 11)
assert.Equal(t, "11", ivar.String())
assert.Equal(t, nil, ivar.Validate())
assert.Equal(t, false, ivar.IsEmpty())
i = 42
assert.Equal(t, "42", ivar.String())
assert.Equal(t, nil, ivar.Validate())
assert.Equal(t, false, ivar.IsEmpty())
ivar.Set("77")
assert.Equal(t, int(77), i)
}
type testdata struct {
value1 int
value2 int
}
func TestCopyStruct(t *testing.T) {
data1 := testdata{}
newIntValue(&data1.value1, 1)
newIntValue(&data1.value2, 2)
assert.Equal(t, int(1), data1.value1)
assert.Equal(t, int(2), data1.value2)
data2 := testdata{}
val21 := newIntValue(&data2.value1, 3)
val22 := newIntValue(&data2.value2, 4)
assert.Equal(t, int(3), data2.value1)
assert.Equal(t, int(4), data2.value2)
data2 = data1
assert.Equal(t, int(1), data2.value1)
assert.Equal(t, int(2), data2.value2)
assert.Equal(t, "1", val21.String())
assert.Equal(t, "2", val22.String())
}

3967
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

3903
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2568
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

60
encoding/json/json.go Normal file
View File

@@ -0,0 +1,60 @@
// Package json implements
package json
import (
"encoding/json"
"fmt"
)
// Unmarshal is a wrapper for json.Unmarshal
func Unmarshal(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
// FormatError takes the marshalled data and the error from Unmarshal and returns a detailed
// error message where the error was and what the error is.
func FormatError(input []byte, err error) error {
if jsonError, ok := err.(*json.SyntaxError); ok {
line, character, offsetError := lineAndCharacter(input, int(jsonError.Offset))
if offsetError != nil {
return err
}
return fmt.Errorf("syntax error at line %d, character %d: %w", line, character, err)
}
if jsonError, ok := err.(*json.UnmarshalTypeError); ok {
line, character, offsetError := lineAndCharacter(input, int(jsonError.Offset))
if offsetError != nil {
return err
}
return fmt.Errorf("expect type '%s' for '%s' at line %d, character %d: %w", jsonError.Type.String(), jsonError.Field, line, character, err)
}
return err
}
func lineAndCharacter(input []byte, offset int) (line int, character int, err error) {
lf := byte(0x0A)
if offset > len(input) || offset < 0 {
return 0, 0, fmt.Errorf("couldn't find offset %d within the input", offset)
}
// Humans tend to count from 1.
line = 1
for i, b := range input {
if b == lf {
line++
character = 0
}
character++
if i == offset {
break
}
}
return line, character, nil
}

70
ffmpeg/collector.go Normal file
View File

@@ -0,0 +1,70 @@
package ffmpeg
import (
"github.com/datarhei/core/session"
)
type wrappedCollector struct {
session.Collector
prefix string
reference string
}
func NewWrappedCollector(prefix, reference string, collector session.Collector) session.Collector {
w := &wrappedCollector{
prefix: prefix,
reference: reference,
Collector: collector,
}
return w
}
func (w *wrappedCollector) Register(id, reference, location, peer string) {
w.Collector.Register(w.prefix+id, w.reference, location, peer)
}
func (w *wrappedCollector) Activate(id string) bool {
return w.Collector.Activate(w.prefix + id)
}
func (w *wrappedCollector) RegisterAndActivate(id, reference, location, peer string) {
w.Collector.RegisterAndActivate(w.prefix+id, w.reference, location, peer)
}
func (w *wrappedCollector) Extra(id, extra string) {
w.Collector.Extra(w.prefix+id, extra)
}
func (w *wrappedCollector) Unregister(id string) {
w.Collector.Unregister(w.prefix + id)
}
func (w *wrappedCollector) Ingress(id string, size int64) {
w.Collector.Ingress(w.prefix+id, size)
}
func (w *wrappedCollector) Egress(id string, size int64) {
w.Collector.Egress(w.prefix+id, size)
}
func (w *wrappedCollector) IsKnownSession(id string) bool {
return w.Collector.IsKnownSession(w.prefix + id)
}
func (w *wrappedCollector) SessionTopIngressBitrate(id string) float64 {
return w.Collector.SessionTopIngressBitrate(w.prefix + id)
}
func (w *wrappedCollector) SessionTopEgressBitrate(id string) float64 {
return w.Collector.SessionTopEgressBitrate(w.prefix + id)
}
func (w *wrappedCollector) SessionSetTopIngressBitrate(id string, bitrate float64) {
w.Collector.SessionSetTopIngressBitrate(w.prefix+id, bitrate)
}
func (w *wrappedCollector) SessionSetTopEgressBitrate(id string, bitrate float64) {
w.Collector.SessionSetTopEgressBitrate(w.prefix+id, bitrate)
}

200
ffmpeg/ffmpeg.go Normal file
View File

@@ -0,0 +1,200 @@
package ffmpeg
import (
"fmt"
"os/exec"
"time"
"github.com/datarhei/core/ffmpeg/parse"
"github.com/datarhei/core/ffmpeg/probe"
"github.com/datarhei/core/ffmpeg/skills"
"github.com/datarhei/core/log"
"github.com/datarhei/core/net"
"github.com/datarhei/core/process"
"github.com/datarhei/core/session"
)
type FFmpeg interface {
New(config ProcessConfig) (process.Process, error)
NewProcessParser(logger log.Logger, id, reference string) parse.Parser
NewProbeParser(logger log.Logger) probe.Parser
ValidateInputAddress(address string) bool
ValidateOutputAddress(address string) bool
Skills() skills.Skills
ReloadSkills() error
GetPort() (int, error)
PutPort(port int)
States() process.States
}
type ProcessConfig struct {
Reconnect bool
ReconnectDelay time.Duration
StaleTimeout time.Duration
Command []string
Parser process.Parser
Logger log.Logger
OnExit func()
OnStart func()
OnStateChange func(from, to string)
}
// Config is the configuration for ffmpeg that is part of the configuration
// for the restreamer instance.
type Config struct {
Binary string
MaxProc int64
MaxLogLines int
LogHistoryLength int
ValidatorInput Validator
ValidatorOutput Validator
Portrange net.Portranger
Collector session.Collector
}
type ffmpeg struct {
binary string
validatorIn Validator
validatorOut Validator
portrange net.Portranger
skills skills.Skills
logLines int
historyLength int
collector session.Collector
states process.States
}
func New(config Config) (FFmpeg, error) {
f := &ffmpeg{}
binary, err := exec.LookPath(config.Binary)
if err != nil {
return nil, fmt.Errorf("invalid ffmpeg binary given: %w", err)
}
f.binary = binary
f.historyLength = config.LogHistoryLength
f.logLines = config.MaxLogLines
f.portrange = config.Portrange
if f.portrange == nil {
f.portrange = net.NewDummyPortrange()
}
f.validatorIn = config.ValidatorInput
if f.validatorIn == nil {
f.validatorIn, _ = NewValidator(nil, nil)
}
f.validatorOut = config.ValidatorOutput
if f.validatorOut == nil {
f.validatorOut, _ = NewValidator(nil, nil)
}
f.collector = config.Collector
if f.collector == nil {
f.collector = session.NewNullCollector()
}
s, err := skills.New(f.binary)
if err != nil {
return nil, fmt.Errorf("invalid ffmpeg binary given: %w", err)
}
f.skills = s
return f, nil
}
func (f *ffmpeg) New(config ProcessConfig) (process.Process, error) {
ffmpeg, err := process.New(process.Config{
Binary: f.binary,
Args: config.Command,
Reconnect: config.Reconnect,
ReconnectDelay: config.ReconnectDelay,
StaleTimeout: config.StaleTimeout,
Parser: config.Parser,
Logger: config.Logger,
OnStart: config.OnStart,
OnExit: config.OnExit,
OnStateChange: func(from, to string) {
switch to {
case "finished":
f.states.Finished++
case "starting":
f.states.Starting++
case "running":
f.states.Running++
case "finishing":
f.states.Finishing++
case "failed":
f.states.Failed++
case "killed":
f.states.Killed++
default:
}
if config.OnStateChange != nil {
config.OnStateChange(from, to)
}
},
})
return ffmpeg, err
}
func (f *ffmpeg) NewProcessParser(logger log.Logger, id, reference string) parse.Parser {
p := parse.New(parse.Config{
LogHistory: f.historyLength,
LogLines: f.logLines,
Logger: logger,
Collector: NewWrappedCollector(id, reference, f.collector),
})
return p
}
func (f *ffmpeg) NewProbeParser(logger log.Logger) probe.Parser {
p := probe.New(probe.Config{
Logger: logger,
})
return p
}
func (f *ffmpeg) ValidateInputAddress(address string) bool {
return f.validatorIn.IsValid(address)
}
func (f *ffmpeg) ValidateOutputAddress(address string) bool {
return f.validatorOut.IsValid(address)
}
func (f *ffmpeg) Skills() skills.Skills {
return f.skills
}
func (f *ffmpeg) ReloadSkills() error {
s, err := skills.New(f.binary)
if err != nil {
return fmt.Errorf("invalid ffmpeg binary given: %w", err)
}
f.skills = s
return nil
}
func (f *ffmpeg) GetPort() (int, error) {
return f.portrange.Get()
}
func (f *ffmpeg) PutPort(port int) {
f.portrange.Put(port)
}
func (f *ffmpeg) States() process.States {
return f.states
}

25
ffmpeg/parse/average.go Normal file
View File

@@ -0,0 +1,25 @@
package parse
import (
"time"
"github.com/prep/average"
)
type averager struct {
fps *average.SlidingWindow
pps *average.SlidingWindow
bitrate *average.SlidingWindow
}
func (a *averager) init(window, granularity time.Duration) {
a.fps, _ = average.New(window, granularity)
a.pps, _ = average.New(window, granularity)
a.bitrate, _ = average.New(window, granularity)
}
func (a *averager) stop() {
a.fps.Stop()
a.pps.Stop()
a.bitrate.Stop()
}

800
ffmpeg/parse/parser.go Normal file
View File

@@ -0,0 +1,800 @@
package parse
import (
"container/ring"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/datarhei/core/log"
"github.com/datarhei/core/net/url"
"github.com/datarhei/core/process"
"github.com/datarhei/core/restream/app"
"github.com/datarhei/core/session"
)
// Parser is an extension to the process.Parser interface
type Parser interface {
process.Parser
// Progress returns the current progress information of the process
Progress() app.Progress
// Prelude returns an array of the lines before the progress information started
Prelude() []string
// Report returns the current logs
Report() Report
// ReportHistory returns an array of previews logs
ReportHistory() []Report
}
// Config is the config for the Parser implementation
type Config struct {
LogHistory int
LogLines int
PreludeHeadLines int
PreludeTailLines int
Logger log.Logger
Collector session.Collector
}
type parser struct {
re struct {
frame *regexp.Regexp
quantizer *regexp.Regexp
size *regexp.Regexp
time *regexp.Regexp
speed *regexp.Regexp
drop *regexp.Regexp
dup *regexp.Regexp
}
prelude struct {
headLines int
tailLines int
truncatedLines uint64
data []string
tail *ring.Ring
done bool
}
log *ring.Ring
logLines int
logStart time.Time
logHistory *ring.Ring
logHistoryLength int
progress struct {
ffmpeg ffmpegProgress
avstream map[string]ffmpegAVstream
}
process ffmpegProcess
stats struct {
initialized bool
main stats
input []stats
output []stats
}
averager struct {
initialized bool
window time.Duration
granularity time.Duration
main averager
input []averager
output []averager
}
collector session.Collector
logger log.Logger
lock struct {
progress sync.RWMutex
prelude sync.RWMutex
log sync.RWMutex
}
}
// New returns a Parser that satisfies the Parser interface
func New(config Config) Parser {
p := &parser{
logHistoryLength: config.LogHistory,
logLines: config.LogLines,
logger: config.Logger,
collector: config.Collector,
}
if p.logger == nil {
p.logger = log.New("Parser")
}
if p.logLines <= 0 {
p.logLines = 1
}
p.averager.window = 30 * time.Second
p.averager.granularity = time.Second
p.re.frame = regexp.MustCompile(`frame=\s*([0-9]+)`)
p.re.quantizer = regexp.MustCompile(`q=\s*([0-9\.]+)`)
p.re.size = regexp.MustCompile(`size=\s*([0-9]+)kB`)
p.re.time = regexp.MustCompile(`time=\s*([0-9]+):([0-9]{2}):([0-9]{2}).([0-9]{2})`)
p.re.speed = regexp.MustCompile(`speed=\s*([0-9\.]+)x`)
p.re.drop = regexp.MustCompile(`drop=\s*([0-9]+)`)
p.re.dup = regexp.MustCompile(`dup=\s*([0-9]+)`)
p.prelude.headLines = config.PreludeHeadLines
if p.prelude.headLines <= 0 {
p.prelude.headLines = 100
}
p.prelude.tailLines = config.PreludeTailLines
if p.prelude.tailLines <= 0 {
p.prelude.tailLines = 50
}
p.prelude.tail = ring.New(p.prelude.tailLines)
p.log = ring.New(config.LogLines)
if p.logHistoryLength > 0 {
p.logHistory = ring.New(p.logHistoryLength)
}
if p.collector == nil {
p.collector = session.NewNullCollector()
}
p.logStart = time.Now()
p.ResetStats()
return p
}
func (p *parser) Parse(line string) uint64 {
isDefaultProgress := strings.HasPrefix(line, "frame=")
isFFmpegInputs := strings.HasPrefix(line, "ffmpeg.inputs:")
isFFmpegOutputs := strings.HasPrefix(line, "ffmpeg.outputs:")
isFFmpegProgress := strings.HasPrefix(line, "ffmpeg.progress:")
isAVstreamProgress := strings.HasPrefix(line, "avstream.progress:")
if p.logStart.IsZero() {
p.logStart = time.Now()
}
if !p.prelude.done {
if isAVstreamProgress {
return 0
}
if isFFmpegProgress {
return 0
}
if isFFmpegInputs {
if err := p.parseIO("input", strings.TrimPrefix(line, "ffmpeg.inputs:")); err != nil {
p.logger.WithFields(log.Fields{
"line": line,
"error": err,
}).Error().Log("Failed parsing inputs")
}
return 0
}
if isFFmpegOutputs {
if err := p.parseIO("output", strings.TrimPrefix(line, "ffmpeg.outputs:")); err != nil {
p.logger.WithFields(log.Fields{
"line": line,
"error": err,
}).Error().Log("Failed parsing outputs")
} else {
p.logger.WithField("prelude", p.Prelude()).Debug().Log("")
p.prelude.done = true
}
return 0
}
if isDefaultProgress {
if !p.parsePrelude() {
return 0
}
p.logger.WithField("prelude", p.Prelude()).Debug().Log("")
p.prelude.done = true
}
}
if !isDefaultProgress && !isFFmpegProgress && !isAVstreamProgress {
// Write the current non-progress line to the log
p.addLog(line)
if !p.prelude.done {
if len(p.prelude.data) < p.prelude.headLines {
p.prelude.data = append(p.prelude.data, line)
} else {
p.lock.prelude.Lock()
p.prelude.tail.Value = line
p.prelude.tail = p.prelude.tail.Next()
p.lock.prelude.Unlock()
p.prelude.truncatedLines++
}
}
return 0
}
if !p.prelude.done {
return 0
}
p.lock.progress.Lock()
defer p.lock.progress.Unlock()
// Initialize the averagers
if !p.averager.initialized {
p.averager.main.init(p.averager.window, p.averager.granularity)
p.averager.input = make([]averager, len(p.process.input))
for i := range p.averager.input {
p.averager.input[i].init(p.averager.window, p.averager.granularity)
}
p.averager.output = make([]averager, len(p.process.output))
for i := range p.averager.output {
p.averager.output[i].init(p.averager.window, p.averager.granularity)
}
p.averager.initialized = true
}
// Initialize the stats
if !p.stats.initialized {
p.stats.input = make([]stats, len(p.process.input))
p.stats.output = make([]stats, len(p.process.output))
p.collector.Register("", "", "", "")
p.stats.initialized = true
}
// Update the progress
if isAVstreamProgress {
if err := p.parseAVstreamProgress(strings.TrimPrefix(line, "avstream.progress:")); err != nil {
p.logger.WithFields(log.Fields{
"line": line,
"error": err,
}).Error().Log("Failed parsing AVStream progress")
}
return 0
}
if isDefaultProgress {
if err := p.parseDefaultProgress(line); err != nil {
p.logger.WithFields(log.Fields{
"line": line,
"error": err,
}).Error().Log("Failed parsing default progress")
return 0
}
} else if isFFmpegProgress {
if err := p.parseFFmpegProgress(strings.TrimPrefix(line, "ffmpeg.progress:")); err != nil {
p.logger.WithFields(log.Fields{
"line": line,
"error": err,
}).Error().Log("Failed parsing progress")
return 0
}
} else {
return 0
}
// Update the averages
p.stats.main.updateFromProgress(&p.progress.ffmpeg)
if len(p.stats.input) != 0 && len(p.stats.input) == len(p.progress.ffmpeg.Input) {
for i := range p.progress.ffmpeg.Input {
p.stats.input[i].updateFromProgressIO(&p.progress.ffmpeg.Input[i])
}
}
if len(p.stats.output) != 0 && len(p.stats.output) == len(p.progress.ffmpeg.Output) {
for i := range p.progress.ffmpeg.Output {
p.stats.output[i].updateFromProgressIO(&p.progress.ffmpeg.Output[i])
}
}
p.averager.main.fps.Add(int64(p.stats.main.diff.frame))
p.averager.main.pps.Add(int64(p.stats.main.diff.packet))
p.averager.main.bitrate.Add(int64(p.stats.main.diff.size) * 8)
p.progress.ffmpeg.FPS = p.averager.main.fps.Average(p.averager.window)
p.progress.ffmpeg.PPS = p.averager.main.pps.Average(p.averager.window)
p.progress.ffmpeg.Bitrate = p.averager.main.bitrate.Average(p.averager.window)
if len(p.averager.input) != 0 && len(p.averager.input) == len(p.progress.ffmpeg.Input) {
for i := range p.progress.ffmpeg.Input {
p.averager.input[i].fps.Add(int64(p.stats.input[i].diff.frame))
p.averager.input[i].pps.Add(int64(p.stats.input[i].diff.packet))
p.averager.input[i].bitrate.Add(int64(p.stats.input[i].diff.size) * 8)
p.progress.ffmpeg.Input[i].FPS = p.averager.input[i].fps.Average(p.averager.window)
p.progress.ffmpeg.Input[i].PPS = p.averager.input[i].pps.Average(p.averager.window)
p.progress.ffmpeg.Input[i].Bitrate = p.averager.input[i].bitrate.Average(p.averager.window)
if p.collector.IsCollectableIP(p.process.input[i].IP) {
p.collector.Activate("")
p.collector.Ingress("", int64(p.stats.input[i].diff.size)*1024)
}
}
}
if len(p.averager.output) != 0 && len(p.averager.output) == len(p.progress.ffmpeg.Output) {
for i := range p.progress.ffmpeg.Output {
p.averager.output[i].fps.Add(int64(p.stats.output[i].diff.frame))
p.averager.output[i].pps.Add(int64(p.stats.output[i].diff.packet))
p.averager.output[i].bitrate.Add(int64(p.stats.output[i].diff.size) * 8)
p.progress.ffmpeg.Output[i].FPS = p.averager.output[i].fps.Average(p.averager.window)
p.progress.ffmpeg.Output[i].PPS = p.averager.output[i].pps.Average(p.averager.window)
p.progress.ffmpeg.Output[i].Bitrate = p.averager.output[i].bitrate.Average(p.averager.window)
if p.collector.IsCollectableIP(p.process.output[i].IP) {
p.collector.Activate("")
p.collector.Egress("", int64(p.stats.output[i].diff.size)*1024)
}
}
}
// Calculate if any of the processed frames staled.
// If one number of frames in an output is the same as
// before, then pFrames becomes 0.
var pFrames uint64 = 0
pFrames = p.stats.main.diff.frame
if isFFmpegProgress {
for i := range p.stats.output {
pFrames *= p.stats.output[i].diff.frame
}
}
return pFrames
}
func (p *parser) parseDefaultProgress(line string) error {
var matches []string
if matches = p.re.frame.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
p.progress.ffmpeg.Frame = x
}
}
if matches = p.re.quantizer.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
p.progress.ffmpeg.Quantizer = x
}
}
if matches = p.re.size.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
p.progress.ffmpeg.Size = x
}
}
if matches = p.re.time.FindStringSubmatch(line); matches != nil {
s := fmt.Sprintf("%sh%sm%ss%s0ms", matches[1], matches[2], matches[3], matches[4])
if x, err := time.ParseDuration(s); err == nil {
p.progress.ffmpeg.Time.Duration = x
}
}
if matches = p.re.speed.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
p.progress.ffmpeg.Speed = x
}
}
if matches = p.re.drop.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
p.progress.ffmpeg.Drop = x
}
}
if matches = p.re.dup.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
p.progress.ffmpeg.Dup = x
}
}
return nil
}
func (p *parser) parseIO(kind, line string) error {
processIO := []ffmpegProcessIO{}
err := json.Unmarshal([]byte(line), &processIO)
if err != nil {
return err
}
if len(processIO) == 0 {
return fmt.Errorf("the %s length must not be 0", kind)
}
for i := range processIO {
if ip, _ := url.Lookup(processIO[i].Address); len(ip) != 0 {
processIO[i].IP = ip
}
}
if kind == "input" {
p.process.input = processIO
} else if kind == "output" {
p.process.output = processIO
}
return nil
}
func (p *parser) parseFFmpegProgress(line string) error {
progress := ffmpegProgress{}
err := json.Unmarshal([]byte(line), &progress)
if err != nil {
return err
}
if len(progress.Input) != len(p.process.input) {
return fmt.Errorf("input length mismatch (have: %d, want: %d)", len(progress.Input), len(p.process.input))
}
if len(progress.Output) != len(p.process.output) {
return fmt.Errorf("output length mismatch (have: %d, want: %d)", len(progress.Output), len(p.process.output))
}
p.progress.ffmpeg = progress
return nil
}
func (p *parser) parseAVstreamProgress(line string) error {
progress := ffmpegAVstream{}
err := json.Unmarshal([]byte(line), &progress)
if err != nil {
return err
}
p.progress.avstream[progress.Address] = progress
return nil
}
func (p *parser) Progress() app.Progress {
p.lock.progress.RLock()
defer p.lock.progress.RUnlock()
progress := p.process.export()
p.progress.ffmpeg.exportTo(&progress)
for i, io := range progress.Input {
av, ok := p.progress.avstream[io.Address]
if !ok {
continue
}
progress.Input[i].AVstream = av.export()
}
return progress
}
func (p *parser) Prelude() []string {
if p.prelude.data == nil {
return []string{}
}
prelude := make([]string, len(p.prelude.data))
copy(prelude, p.prelude.data)
tail := []string{}
p.lock.prelude.RLock()
p.prelude.tail.Do(func(l interface{}) {
if l == nil {
return
}
tail = append(tail, l.(string))
})
p.lock.prelude.RUnlock()
if len(tail) != 0 {
if p.prelude.truncatedLines > uint64(p.prelude.tailLines) {
prelude = append(prelude, fmt.Sprintf("... truncated %d lines ...", p.prelude.truncatedLines-uint64(p.prelude.tailLines)))
}
prelude = append(prelude, tail...)
}
return prelude
}
func (p *parser) parsePrelude() bool {
process := ffmpegProcess{}
p.lock.progress.Lock()
defer p.lock.progress.Unlock()
// Input #0, lavfi, from 'testsrc=size=1280x720:rate=25':
// Input #1, lavfi, from 'anullsrc=r=44100:cl=stereo':
// Output #0, hls, to './data/testsrc.m3u8':
reFormat := regexp.MustCompile(`^(Input|Output) #([0-9]+), (.*?), (from|to) '([^']+)`)
// Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 1280x720 [SAR 1:1 DAR 16:9], 25 tbr, 25 tbn, 25 tbc
// Stream #1:0: Audio: pcm_u8, 44100 Hz, stereo, u8, 705 kb/s
// Stream #0:0: Video: h264 (libx264), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 90k tbn, 25 tbc
// Stream #0:1(eng): Audio: aac (LC), 44100 Hz, stereo, fltp, 64 kb/s
reStream := regexp.MustCompile(`Stream #([0-9]+):([0-9]+)(?:\(([a-z]+)\))?: (Video|Audio|Subtitle): (.*)`)
reStreamCodec := regexp.MustCompile(`^([^\s,]+)`)
reStreamVideoSize := regexp.MustCompile(`, ([0-9]+)x([0-9]+)`)
//reStreamVideoFPS := regexp.MustCompile(`, ([0-9]+) fps`)
reStreamAudio := regexp.MustCompile(`, ([0-9]+) Hz, ([^,]+)`)
//reStreamBitrate := regexp.MustCompile(`, ([0-9]+) kb/s`)
reStreamMapping := regexp.MustCompile(`^Stream mapping:`)
reStreamMap := regexp.MustCompile(`^[\s]+Stream #[0-9]+:[0-9]+`)
//format := InputOutput{}
formatType := ""
formatURL := ""
var noutputs int
streamMapping := false
data := p.Prelude()
for _, line := range data {
if reStreamMapping.MatchString(line) {
streamMapping = true
continue
}
if streamMapping {
if reStreamMap.MatchString(line) {
noutputs++
} else {
streamMapping = false
}
continue
}
if matches := reFormat.FindStringSubmatch(line); matches != nil {
formatType = strings.ToLower(matches[1])
formatURL = matches[5]
continue
}
if matches := reStream.FindStringSubmatch(line); matches != nil {
format := ffmpegProcessIO{}
format.Address = formatURL
if ip, _ := url.Lookup(format.Address); len(ip) != 0 {
format.IP = ip
}
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
format.Index = x
}
if x, err := strconv.ParseUint(matches[2], 10, 64); err == nil {
format.Stream = x
}
format.Type = strings.ToLower(matches[4])
streamDetail := matches[5]
if matches = reStreamCodec.FindStringSubmatch(streamDetail); matches != nil {
format.Codec = matches[1]
}
/*
if matches = reStreamBitrate.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
format.Bitrate = x
}
}
*/
if format.Type == "video" {
if matches = reStreamVideoSize.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
format.Width = x
}
if x, err := strconv.ParseUint(matches[2], 10, 64); err == nil {
format.Height = x
}
}
/*
if matches = reStreamVideoFPS.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
format.FPS = x
}
}
*/
} else if format.Type == "audio" {
if matches = reStreamAudio.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
format.Sampling = x
}
format.Layout = matches[2]
}
}
if formatType == "input" {
process.input = append(process.input, format)
} else {
process.output = append(process.output, format)
}
}
}
if len(process.output) != noutputs {
return false
}
p.process.input = process.input
p.process.output = process.output
return true
}
func (p *parser) addLog(line string) {
p.lock.log.Lock()
defer p.lock.log.Unlock()
p.log.Value = process.Line{
Timestamp: time.Now(),
Data: line,
}
p.log = p.log.Next()
}
func (p *parser) Log() []process.Line {
var log = []process.Line{}
p.lock.log.RLock()
defer p.lock.log.RUnlock()
p.log.Do(func(l interface{}) {
if l == nil {
return
}
log = append(log, l.(process.Line))
})
return log
}
func (p *parser) ResetStats() {
p.lock.progress.Lock()
defer p.lock.progress.Unlock()
if p.averager.initialized {
p.averager.main.stop()
p.averager.main = averager{}
for i := range p.averager.input {
p.averager.input[i].stop()
}
p.averager.input = []averager{}
for i := range p.averager.output {
p.averager.output[i].stop()
}
p.averager.output = []averager{}
p.averager.initialized = false
}
if p.stats.initialized {
p.stats.main = stats{}
p.stats.input = []stats{}
p.stats.output = []stats{}
p.stats.initialized = false
}
p.process = ffmpegProcess{}
p.progress.ffmpeg = ffmpegProgress{}
p.progress.avstream = make(map[string]ffmpegAVstream)
p.prelude.done = false
}
func (p *parser) ResetLog() {
p.storeLogHistory()
p.prelude.data = []string{}
p.lock.prelude.Lock()
p.prelude.tail = ring.New(p.prelude.tailLines)
p.lock.prelude.Unlock()
p.prelude.truncatedLines = 0
p.prelude.done = false
p.lock.log.Lock()
p.log = ring.New(p.logLines)
p.lock.log.Unlock()
p.logStart = time.Now()
}
// Report represents a log report, including the prelude and the last log lines
// of the process.
type Report struct {
CreatedAt time.Time
Prelude []string
Log []process.Line
}
func (p *parser) storeLogHistory() {
if p.logHistory == nil {
return
}
h := p.Report()
if len(h.Prelude) != 0 {
p.logHistory.Value = h
p.logHistory = p.logHistory.Next()
}
}
func (p *parser) Report() Report {
h := Report{
CreatedAt: p.logStart,
Prelude: p.Prelude(),
Log: p.Log(),
}
return h
}
func (p *parser) ReportHistory() []Report {
var history = []Report{}
p.logHistory.Do(func(l interface{}) {
if l == nil {
return
}
history = append(history, l.(Report))
})
return history
}

523
ffmpeg/parse/parser_test.go Normal file
View File

@@ -0,0 +1,523 @@
package parse
import (
"fmt"
"strings"
"testing"
"time"
"github.com/datarhei/core/restream/app"
"github.com/stretchr/testify/require"
)
func TestParserProgress(t *testing.T) {
parser := New(Config{
LogLines: 20,
}).(*parser)
parser.prelude.done = true
parser.Parse("frame= 5968 fps= 25 q=19.4 size=443kB time=00:03:58.44 bitrate=5632kbits/s speed=0.999x skip=9733 drop=3522 dup=87463")
d, _ := time.ParseDuration("3m58s440ms")
wantP := app.Progress{
Frame: 5968,
FPS: 25,
Quantizer: 19.4,
Size: 453632,
Time: d.Seconds(),
Bitrate: 5632,
Speed: 0.999,
Drop: 3522,
Dup: 87463,
}
p := parser.Progress()
require.Equal(t, wantP.Frame, p.Frame)
require.Equal(t, wantP.Quantizer, p.Quantizer)
require.Equal(t, wantP.Size, p.Size)
require.Equal(t, wantP.Time, p.Time)
require.Equal(t, wantP.Speed, p.Speed)
require.Equal(t, wantP.Drop, p.Drop)
require.Equal(t, wantP.Dup, p.Dup)
}
func TestParserPrelude(t *testing.T) {
parser := New(Config{
LogLines: 20,
PreludeHeadLines: 100,
PreludeTailLines: 50,
})
log := parser.Prelude()
require.Equal(t, 0, len(log))
parser.Parse("prelude")
log = parser.Prelude()
require.Equal(t, 1, len(log))
}
func TestParserLongPrelude(t *testing.T) {
parser := New(Config{
LogLines: 20,
PreludeHeadLines: 100,
PreludeTailLines: 50,
})
log := parser.Prelude()
require.Equal(t, 0, len(log))
for i := 0; i < 150; i++ {
parser.Parse(fmt.Sprintf("prelude %3d", i))
}
log = parser.Prelude()
require.Equal(t, 150, len(log))
}
func TestParserVeryLongPrelude(t *testing.T) {
parser := New(Config{
LogLines: 20,
PreludeHeadLines: 100,
PreludeTailLines: 50,
})
log := parser.Prelude()
require.Equal(t, 0, len(log))
for i := 0; i < 300; i++ {
parser.Parse(fmt.Sprintf("prelude %3d", i))
}
log = parser.Prelude()
require.Equal(t, 151, len(log))
}
func TestParserLog(t *testing.T) {
parser := New(Config{
LogLines: 20,
})
log := parser.Log()
require.Equal(t, 0, len(log))
parser.Parse("bla")
log = parser.Log()
require.Equal(t, 1, len(log))
}
func TestParserReset(t *testing.T) {
parser := New(Config{
LogLines: 20,
PreludeHeadLines: 100,
PreludeTailLines: 50,
})
log := parser.Log()
prelude := parser.Prelude()
require.Equal(t, 0, len(log))
require.Equal(t, 0, len(prelude))
parser.Parse("prelude")
log = parser.Log()
prelude = parser.Prelude()
require.Equal(t, 1, len(log))
require.Equal(t, 1, len(prelude))
parser.ResetStats()
log = parser.Log()
prelude = parser.Prelude()
require.NotEqual(t, 0, len(log))
require.NotEqual(t, 0, len(prelude))
parser.ResetLog()
log = parser.Log()
prelude = parser.Prelude()
require.Equal(t, 0, len(log))
require.Equal(t, 0, len(prelude))
}
func TestParserDefault(t *testing.T) {
parser := New(Config{
LogLines: 20,
}).(*parser)
rawdata := `ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
built with Apple LLVM version 9.1.0 (clang-902.0.39.2)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.0.2 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-gpl --enable-libmp3lame --enable-libx264 --enable-libx265 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma
libavutil 56. 14.100 / 56. 14.100
libavcodec 58. 18.100 / 58. 18.100
libavformat 58. 12.100 / 58. 12.100
libavdevice 58. 3.100 / 58. 3.100
libavfilter 7. 16.100 / 7. 16.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
libpostproc 55. 1.100 / 55. 1.100
Input #0, lavfi, from 'testsrc=size=1280x720:rate=25':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 1280x720 [SAR 1:1 DAR 16:9], 25 tbr, 25 tbn, 25 tbc
Input #1, lavfi, from 'anullsrc=r=44100:cl=stereo':
Duration: N/A, start: 0.000000, bitrate: 705 kb/s
Stream #1:0: Audio: pcm_u8, 44100 Hz, stereo, u8, 705 kb/s
Stream #1:1(eng): Audio: aac (LC), 48000 Hz, stereo, fltp (default)
Stream mapping:
Stream #0:0 -> #0:0 (rawvideo (native) -> h264 (libx264))
Stream #1:0 -> #0:1 (pcm_u8 (native) -> aac (native))
Press [q] to stop, [?] for help
[libx264 @ 0x7fa96a800600] using SAR=1/1
[libx264 @ 0x7fa96a800600] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x7fa96a800600] profile Constrained Baseline, level 3.1
[libx264 @ 0x7fa96a800600] 264 - core 152 r2854 e9a5903 - H.264/MPEG-4 AVC codec - Copyleft 2003-2017 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=50 keyint_min=5 scenecut=0 intra_refresh=0 rc=crf mbtree=0 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
[hls @ 0x7fa969803a00] Opening './data/testsrc5375.ts.tmp' for writing
Output #0, hls, to './data/testsrc.m3u8':
Metadata:
encoder : Lavf58.12.100
Stream #0:0: Video: h264 (libx264), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 90k tbn, 25 tbc
Metadata:
encoder : Lavc58.18.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: -1
Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 64 kb/s
Metadata:
encoder : Lavc58.18.100 aac
[hls @ 0x7fa969803a00] Opening './data/testsrc5376.ts.tmp' for writing=0.872x
[hls @ 0x7fa969803a00] Opening './data/testsrc.m3u8.tmp' for writing
[hls @ 0x7fa969803a00] Opening './data/testsrc.m3u8.tmp' for writing
frame= 58 fps= 25 q=-1.0 Lsize=N/A time=00:00:02.32 bitrate=N/A speed=0.999x`
data := strings.Split(rawdata, "\n")
for _, d := range data {
parser.Parse(d)
}
require.Equal(t, 3, len(parser.process.input), "expected 3 inputs")
require.Equal(t, 2, len(parser.process.output), "expected 2 outputs")
}
func TestParserDefaultDelayed(t *testing.T) {
parser := New(Config{
LogLines: 20,
}).(*parser)
rawdata := `ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
built with Apple LLVM version 9.1.0 (clang-902.0.39.2)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.0.2 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-gpl --enable-libmp3lame --enable-libx264 --enable-libx265 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma
libavutil 56. 14.100 / 56. 14.100
libavcodec 58. 18.100 / 58. 18.100
libavformat 58. 12.100 / 58. 12.100
libavdevice 58. 3.100 / 58. 3.100
libavfilter 7. 16.100 / 7. 16.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
libpostproc 55. 1.100 / 55. 1.100
Input #0, lavfi, from 'testsrc=size=1280x720:rate=25':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 1280x720 [SAR 1:1 DAR 16:9], 25 tbr, 25 tbn, 25 tbc
Input #1, lavfi, from 'anullsrc=r=44100:cl=stereo':
Duration: N/A, start: 0.000000, bitrate: 705 kb/s
Stream #1:0: Audio: pcm_u8, 44100 Hz, stereo, u8, 705 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (rawvideo (native) -> h264 (libx264))
Stream #1:0 -> #0:1 (pcm_u8 (native) -> aac (native))
Press [q] to stop, [?] for help
frame= 29 fps= 25 q=-1.0 Lsize=N/A time=00:00:01.32 bitrate=N/A speed=0.999x
[libx264 @ 0x7fa96a800600] using SAR=1/1
[libx264 @ 0x7fa96a800600] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x7fa96a800600] profile Constrained Baseline, level 3.1
[libx264 @ 0x7fa96a800600] 264 - core 152 r2854 e9a5903 - H.264/MPEG-4 AVC codec - Copyleft 2003-2017 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=50 keyint_min=5 scenecut=0 intra_refresh=0 rc=crf mbtree=0 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
[hls @ 0x7fa969803a00] Opening './data/testsrc5375.ts.tmp' for writing
Output #0, hls, to './data/testsrc.m3u8':
Metadata:
encoder : Lavf58.12.100
Stream #0:0: Video: h264 (libx264), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 90k tbn, 25 tbc
Metadata:
encoder : Lavc58.18.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: -1
Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 64 kb/s
Metadata:
encoder : Lavc58.18.100 aac
[hls @ 0x7fa969803a00] Opening './data/testsrc5376.ts.tmp' for writing=0.872x
[hls @ 0x7fa969803a00] Opening './data/testsrc.m3u8.tmp' for writing
[hls @ 0x7fa969803a00] Opening './data/testsrc.m3u8.tmp' for writing
frame= 58 fps= 25 q=-1.0 Lsize=N/A time=00:00:02.32 bitrate=N/A speed=0.999x`
data := strings.Split(rawdata, "\n")
for _, d := range data {
parser.Parse(d)
}
require.Equal(t, 2, len(parser.process.input), "expected 2 inputs")
require.Equal(t, 2, len(parser.process.output), "expected 2 outputs")
}
func TestParserJSON(t *testing.T) {
parser := New(Config{
LogLines: 20,
}).(*parser)
rawdata := `ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
built with gcc 9.3.0 (Alpine 9.3.0)
configuration: --prefix=/usr/local --enable-nonfree --enable-gpl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-openssl --enable-shared --enable-libfreetype --disable-ffplay --disable-debug --disable-doc
libavutil 56. 51.100 / 56. 51.100
libavcodec 58. 91.100 / 58. 91.100
libavformat 58. 45.100 / 58. 45.100
libavdevice 58. 10.100 / 58. 10.100
libavfilter 7. 85.100 / 7. 85.100
libswscale 5. 7.100 / 5. 7.100
libswresample 3. 7.100 / 3. 7.100
libpostproc 55. 7.100 / 55. 7.100
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-SESSION-DATA:DATA-ID="net.z60wzayk.metadata",URI="https://vpu.livespotting.com/e9slfpe3/z60wzayk_metadata.json"')
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100788.ts' for reading
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100789.ts' for reading
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100790.ts' for reading
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100791.ts' for reading
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100792.ts' for reading
Input #0, playout, from 'https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuvj420p(pc, bt709), 1280x720 [SAR 1:1 DAR 16:9], 20.67 fps, 25 tbr, 1000k tbn, 2000k tbc
Metadata:
variant_bitrate : 1024000
ffmpeg.inputs:[{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8","format":"playout","index":0,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":20.666666,"pix_fmt":"yuvj420p","width":1280,"height":720}]
Output #1, mp4, to '/dev/null':
Metadata:
encoder : Lavf58.45.100
Stream #1:0: Video: h264 (Baseline) (avc1 / 0x31637661), yuvj420p(pc, bt709), 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 20.67 fps, 25 tbr, 1000k tbn, 1000k tbc
Metadata:
variant_bitrate : 1024000
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
Stream #0:0 -> #1:0 (copy)
Press [q] to stop, [?] for help
[libx264 @ 0x557c8443e200] using SAR=1/1
[libx264 @ 0x557c8443e200] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x557c8443e200] profile Constrained Baseline, level 3.1, 4:2:0, 8-bit
[libx264 @ 0x557c8443e200] 264 - core 161 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=crf mbtree=0 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
Output #0, flv, to '/dev/null':
Metadata:
encoder : Lavf58.45.100
Stream #0:0: Video: h264 (libx264) ([7][0][0][0] / 0x0007), yuvj420p(pc), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 1k tbn, 25 tbc
Metadata:
variant_bitrate : 1024000
encoder : Lavc58.91.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
ffmpeg.outputs:[{"url":"/dev/null","format":"flv","index":0,"stream":0,"type":"video","codec":"h264","coder":"libx264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"/dev/null","format":"mp4","index":1,"stream":0,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":20.666666,"pix_fmt":"yuvj420p","width":1280,"height":720}]
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":2,"packet":6,"size_kb":222}],"outputs":[{"index":0,"stream":0,"frame":2,"packet":0,"q":0.0,"size_kb":0},{"index":1,"stream":0,"frame":6,"packet":6,"q":-1.0,"size_kb":222}],"frame":2,"packet":0,"q":0.0,"size_kb":222,"time":"0h0m0.20s","speed":0.281,"dup":0,"drop":0}
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100793.ts' for reading
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":7,"packet":11,"size_kb":226}],"outputs":[{"index":0,"stream":0,"frame":7,"packet":0,"q":0.0,"size_kb":0},{"index":1,"stream":0,"frame":11,"packet":11,"q":-1.0,"size_kb":226}],"frame":7,"packet":0,"q":0.0,"size_kb":226,"time":"0h0m0.56s","speed":0.4,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":10,"packet":14,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":10,"packet":3,"q":16.0,"size_kb":281},{"index":1,"stream":0,"frame":14,"packet":14,"q":-1.0,"size_kb":227}],"frame":10,"packet":3,"q":16.0,"size_kb":508,"time":"0h0m0.68s","speed":0.358,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":13,"packet":17,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":13,"packet":6,"q":14.0,"size_kb":350},{"index":1,"stream":0,"frame":17,"packet":17,"q":-1.0,"size_kb":227}],"frame":13,"packet":6,"q":14.0,"size_kb":577,"time":"0h0m0.80s","speed":0.333,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":16,"packet":20,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":16,"packet":9,"q":13.0,"size_kb":489},{"index":1,"stream":0,"frame":20,"packet":20,"q":-1.0,"size_kb":227}],"frame":16,"packet":9,"q":13.0,"size_kb":716,"time":"0h0m0.92s","speed":0.306,"dup":0,"drop":0}
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100794.ts' for reading
[https @ 0x557c840f3180] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100795.ts' for reading
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":21,"packet":24,"size_kb":228}],"outputs":[{"index":0,"stream":0,"frame":20,"packet":20,"q":-1.0,"size_kb":562},{"index":1,"stream":0,"frame":24,"packet":24,"q":-1.0,"size_kb":228}],"frame":20,"packet":20,"q":-1.0,"size_kb":789,"time":"0h0m1.8s","speed":0.245,"dup":0,"drop":0}`
data := strings.Split(rawdata, "\n")
for _, d := range data {
parser.Parse(d)
}
require.Equal(t, 1, len(parser.process.input), "expected 1 input")
require.Equal(t, 2, len(parser.process.output), "expected 2 outputs")
}
func TestParserJSONDelayed(t *testing.T) {
parser := New(Config{
LogLines: 20,
}).(*parser)
rawdata := `ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
built with gcc 9.3.0 (Alpine 9.3.0)
configuration: --prefix=/usr/local --enable-nonfree --enable-gpl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-openssl --enable-shared --enable-libfreetype --disable-ffplay --disable-debug --disable-doc
libavutil 56. 51.100 / 56. 51.100
libavcodec 58. 91.100 / 58. 91.100
libavformat 58. 45.100 / 58. 45.100
libavdevice 58. 10.100 / 58. 10.100
libavfilter 7. 85.100 / 7. 85.100
libswscale 5. 7.100 / 5. 7.100
libswresample 3. 7.100 / 3. 7.100
libpostproc 55. 7.100 / 55. 7.100
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-SESSION-DATA:DATA-ID="net.z60wzayk.metadata",URI="https://vpu.livespotting.com/e9slfpe3/z60wzayk_metadata.json"')
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100788.ts' for reading
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100789.ts' for reading
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100790.ts' for reading
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100791.ts' for reading
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100792.ts' for reading
Input #0, playout, from 'https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuvj420p(pc, bt709), 1280x720 [SAR 1:1 DAR 16:9], 20.67 fps, 25 tbr, 1000k tbn, 2000k tbc
Metadata:
variant_bitrate : 1024000
ffmpeg.inputs:[{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8","format":"playout","index":0,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":20.666666,"pix_fmt":"yuvj420p","width":1280,"height":720}]
Output #1, mp4, to '/dev/null':
Metadata:
encoder : Lavf58.45.100
Stream #1:0: Video: h264 (Baseline) (avc1 / 0x31637661), yuvj420p(pc, bt709), 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 20.67 fps, 25 tbr, 1000k tbn, 1000k tbc
Metadata:
variant_bitrate : 1024000
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
Stream #0:0 -> #1:0 (copy)
Press [q] to stop, [?] for help
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":2,"packet":6,"size_kb":222}],"outputs":[{"index":0,"stream":0,"frame":2,"packet":0,"q":0.0,"size_kb":0},{"index":1,"stream":0,"frame":6,"packet":6,"q":-1.0,"size_kb":222}],"frame":2,"packet":0,"q":0.0,"size_kb":222,"time":"0h0m0.20s","speed":0.281,"dup":0,"drop":0}
[libx264 @ 0x557c8443e200] using SAR=1/1
[libx264 @ 0x557c8443e200] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x557c8443e200] profile Constrained Baseline, level 3.1, 4:2:0, 8-bit
[libx264 @ 0x557c8443e200] 264 - core 161 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=crf mbtree=0 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
Output #0, flv, to '/dev/null':
Metadata:
encoder : Lavf58.45.100
Stream #0:0: Video: h264 (libx264) ([7][0][0][0] / 0x0007), yuvj420p(pc), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 1k tbn, 25 tbc
Metadata:
variant_bitrate : 1024000
encoder : Lavc58.91.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
ffmpeg.outputs:[{"url":"/dev/null","format":"flv","index":0,"stream":0,"type":"video","codec":"h264","coder":"libx264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"/dev/null","format":"mp4","index":1,"stream":0,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":20.666666,"pix_fmt":"yuvj420p","width":1280,"height":720}]
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100793.ts' for reading
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":7,"packet":11,"size_kb":226}],"outputs":[{"index":0,"stream":0,"frame":7,"packet":0,"q":0.0,"size_kb":0},{"index":1,"stream":0,"frame":11,"packet":11,"q":-1.0,"size_kb":226}],"frame":7,"packet":0,"q":0.0,"size_kb":226,"time":"0h0m0.56s","speed":0.4,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":10,"packet":14,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":10,"packet":3,"q":16.0,"size_kb":281},{"index":1,"stream":0,"frame":14,"packet":14,"q":-1.0,"size_kb":227}],"frame":10,"packet":3,"q":16.0,"size_kb":508,"time":"0h0m0.68s","speed":0.358,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":13,"packet":17,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":13,"packet":6,"q":14.0,"size_kb":350},{"index":1,"stream":0,"frame":17,"packet":17,"q":-1.0,"size_kb":227}],"frame":13,"packet":6,"q":14.0,"size_kb":577,"time":"0h0m0.80s","speed":0.333,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":16,"packet":20,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":16,"packet":9,"q":13.0,"size_kb":489},{"index":1,"stream":0,"frame":20,"packet":20,"q":-1.0,"size_kb":227}],"frame":16,"packet":9,"q":13.0,"size_kb":716,"time":"0h0m0.92s","speed":0.306,"dup":0,"drop":0}
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100794.ts' for reading
[https @ 0x557c840f3180] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100795.ts' for reading
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":21,"packet":24,"size_kb":228}],"outputs":[{"index":0,"stream":0,"frame":20,"packet":20,"q":-1.0,"size_kb":562},{"index":1,"stream":0,"frame":24,"packet":24,"q":-1.0,"size_kb":228}],"frame":20,"packet":20,"q":-1.0,"size_kb":789,"time":"0h0m1.8s","speed":0.245,"dup":0,"drop":0}`
data := strings.Split(rawdata, "\n")
for _, d := range data {
parser.Parse(d)
}
require.Equal(t, 1, len(parser.process.input), "expected 1 input")
require.Equal(t, 2, len(parser.process.output), "expected 2 outputs")
}
func TestParserJSONDelayedPlayout(t *testing.T) {
parser := New(Config{
LogLines: 20,
}).(*parser)
rawdata := `ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
built with gcc 9.3.0 (Alpine 9.3.0)
configuration: --prefix=/usr/local --enable-nonfree --enable-gpl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-openssl --enable-shared --enable-libfreetype --disable-ffplay --disable-debug --disable-doc
libavutil 56. 51.100 / 56. 51.100
libavcodec 58. 91.100 / 58. 91.100
libavformat 58. 45.100 / 58. 45.100
libavdevice 58. 10.100 / 58. 10.100
libavfilter 7. 85.100 / 7. 85.100
libswscale 5. 7.100 / 5. 7.100
libswresample 3. 7.100 / 3. 7.100
libpostproc 55. 7.100 / 55. 7.100
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-SESSION-DATA:DATA-ID="net.z60wzayk.metadata",URI="https://vpu.livespotting.com/e9slfpe3/z60wzayk_metadata.json"')
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100788.ts' for reading
[hls @ 0x7f92a96e8100] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100789.ts' for reading
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100790.ts' for reading
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100791.ts' for reading
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100792.ts' for reading
Input #0, playout, from 'https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuvj420p(pc, bt709), 1280x720 [SAR 1:1 DAR 16:9], 20.67 fps, 25 tbr, 1000k tbn, 2000k tbc
Metadata:
variant_bitrate : 1024000
ffmpeg.inputs:[{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8","format":"playout","index":0,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":20.666666,"pix_fmt":"yuvj420p","width":1280,"height":720}]
Output #1, mp4, to '/dev/null':
Metadata:
encoder : Lavf58.45.100
Stream #1:0: Video: h264 (Baseline) (avc1 / 0x31637661), yuvj420p(pc, bt709), 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 20.67 fps, 25 tbr, 1000k tbn, 1000k tbc
Metadata:
variant_bitrate : 1024000
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
Stream #0:0 -> #1:0 (copy)
Press [q] to stop, [?] for help
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":2,"packet":6,"size_kb":222}],"outputs":[{"index":0,"stream":0,"frame":2,"packet":0,"q":0.0,"size_kb":0},{"index":1,"stream":0,"frame":6,"packet":6,"q":-1.0,"size_kb":222}],"frame":2,"packet":0,"q":0.0,"size_kb":222,"time":"0h0m0.20s","speed":0.281,"dup":0,"drop":0}
[libx264 @ 0x557c8443e200] using SAR=1/1
[libx264 @ 0x557c8443e200] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x557c8443e200] profile Constrained Baseline, level 3.1, 4:2:0, 8-bit
[libx264 @ 0x557c8443e200] 264 - core 161 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=crf mbtree=0 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
Output #0, flv, to '/dev/null':
Metadata:
encoder : Lavf58.45.100
Stream #0:0: Video: h264 (libx264) ([7][0][0][0] / 0x0007), yuvj420p(pc), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 1k tbn, 25 tbc
Metadata:
variant_bitrate : 1024000
encoder : Lavc58.91.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
ffmpeg.outputs:[{"url":"/dev/null","format":"flv","index":0,"stream":0,"type":"video","codec":"h264","coder":"libx264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"/dev/null","format":"mp4","index":1,"stream":0,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":20.666666,"pix_fmt":"yuvj420p","width":1280,"height":720}]
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
avstream.progress:{"id":"playout:https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8","url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk.m3u8","stream":0,"queue":140,"aqueue":0,"dup":0,"drop":0,"enc":0,"looping":false,"duplicating":false,"gop":"none","input":{"state":"running","packet":148,"size_kb":1529,"time":5},"output":{"state":"running","packet":8,"size_kb":128,"time":1},"swap":{"url":"","status":"waiting","lasturl":"","lasterror":""}}
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100793.ts' for reading
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":7,"packet":11,"size_kb":226}],"outputs":[{"index":0,"stream":0,"frame":7,"packet":0,"q":0.0,"size_kb":0},{"index":1,"stream":0,"frame":11,"packet":11,"q":-1.0,"size_kb":226}],"frame":7,"packet":0,"q":0.0,"size_kb":226,"time":"0h0m0.56s","speed":0.4,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":10,"packet":14,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":10,"packet":3,"q":16.0,"size_kb":281},{"index":1,"stream":0,"frame":14,"packet":14,"q":-1.0,"size_kb":227}],"frame":10,"packet":3,"q":16.0,"size_kb":508,"time":"0h0m0.68s","speed":0.358,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":13,"packet":17,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":13,"packet":6,"q":14.0,"size_kb":350},{"index":1,"stream":0,"frame":17,"packet":17,"q":-1.0,"size_kb":227}],"frame":13,"packet":6,"q":14.0,"size_kb":577,"time":"0h0m0.80s","speed":0.333,"dup":0,"drop":0}
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":16,"packet":20,"size_kb":227}],"outputs":[{"index":0,"stream":0,"frame":16,"packet":9,"q":13.0,"size_kb":489},{"index":1,"stream":0,"frame":20,"packet":20,"q":-1.0,"size_kb":227}],"frame":16,"packet":9,"q":13.0,"size_kb":716,"time":"0h0m0.92s","speed":0.306,"dup":0,"drop":0}
[https @ 0x557c840bf480] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8' for reading
[hls @ 0x7f92a96e8100] Skip ('#EXT-X-VERSION:3')
[https @ 0x557c840d1080] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100794.ts' for reading
[https @ 0x557c840f3180] Opening 'https://ch-fra-n16.livespotting.com/vpu/e9slfpe3/z60wzayk_720_100795.ts' for reading
ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"frame":21,"packet":24,"size_kb":228}],"outputs":[{"index":0,"stream":0,"frame":20,"packet":20,"q":-1.0,"size_kb":562},{"index":1,"stream":0,"frame":24,"packet":24,"q":-1.0,"size_kb":228}],"frame":20,"packet":20,"q":-1.0,"size_kb":789,"time":"0h0m1.8s","speed":0.245,"dup":0,"drop":0}`
data := strings.Split(rawdata, "\n")
for _, d := range data {
parser.Parse(d)
}
require.Equal(t, 1, len(parser.process.input), "expected 1 input")
require.Equal(t, 2, len(parser.process.output), "expected 2 outputs")
}

38
ffmpeg/parse/stats.go Normal file
View File

@@ -0,0 +1,38 @@
package parse
type statsData struct {
frame uint64
packet uint64
size uint64 // kbytes
dup uint64
drop uint64
}
type stats struct {
last statsData
diff statsData
}
func (s *stats) updateFromProgress(p *ffmpegProgress) {
s.diff.frame = p.Frame - s.last.frame
s.diff.packet = p.Packet - s.last.packet
s.diff.size = p.Size - s.last.size
s.diff.drop = p.Drop - s.last.drop
s.diff.dup = p.Dup - s.last.dup
s.last.frame = p.Frame
s.last.packet = p.Packet
s.last.size = p.Size
s.last.dup = p.Dup
s.last.drop = p.Drop
}
func (s *stats) updateFromProgressIO(p *ffmpegProgressIO) {
s.diff.frame = p.Frame - s.last.frame
s.diff.packet = p.Packet - s.last.packet
s.diff.size = p.Size - s.last.size
s.last.frame = p.Frame
s.last.packet = p.Packet
s.last.size = p.Size
}

226
ffmpeg/parse/types.go Normal file
View File

@@ -0,0 +1,226 @@
package parse
import (
"encoding/json"
"errors"
"time"
"github.com/datarhei/core/restream/app"
)
// Duration represents a time.Duration
type Duration struct {
time.Duration
}
// MarshalJSON marshals a time.Duration to JSON
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Seconds())
}
// UnmarshalJSON unmarshals a JSON value to time.Duration. The JSON value
// can be either a float which is interpreted as seconds or a string
// that is interpreted as a formatted duration.
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
d.Duration = time.Duration(value)
return nil
case string:
var err error
d.Duration, err = time.ParseDuration(value)
if err != nil {
return err
}
return nil
default:
return errors.New("invalid duration")
}
}
type ffmpegAVstreamIO struct {
State string `json:"state"`
Packet uint64 `json:"packet"`
Time uint64 `json:"time"`
Size uint64 `json:"size_kb"`
}
func (avio *ffmpegAVstreamIO) export() app.AVstreamIO {
return app.AVstreamIO{
State: avio.State,
Packet: avio.Packet,
Time: avio.Time,
Size: avio.Size,
}
}
type ffmpegAVstream struct {
Input ffmpegAVstreamIO `json:"input"`
Output ffmpegAVstreamIO `json:"output"`
Address string `json:"id"`
URL string `json:"url"`
Stream uint64 `json:"stream"`
Aqueue uint64 `json:"aqueue"`
Queue uint64 `json:"queue"`
Dup uint64 `json:"dup"`
Drop uint64 `json:"drop"`
Enc uint64 `json:"enc"`
Looping bool `json:"looping"`
Duplicating bool `json:"duplicating"`
GOP string `json:"gop"`
}
func (av *ffmpegAVstream) export() *app.AVstream {
return &app.AVstream{
Aqueue: av.Aqueue,
Queue: av.Queue,
Drop: av.Drop,
Dup: av.Dup,
Enc: av.Enc,
Looping: av.Looping,
Duplicating: av.Duplicating,
GOP: av.GOP,
Input: av.Input.export(),
Output: av.Output.export(),
}
}
type ffmpegProgressIO struct {
// common
Index uint64 `json:"index"`
Stream uint64 `json:"stream"`
Size uint64 `json:"size_kb"` // kbytes
Bitrate float64 `json:"-"` // kbit/s
Frame uint64 `json:"frame"`
Packet uint64 `json:"packet"`
FPS float64 `json:"-"`
PPS float64 `json:"-"`
// video
Quantizer float64 `json:"q"`
}
func (io *ffmpegProgressIO) exportTo(progress *app.ProgressIO) {
progress.Index = io.Index
progress.Stream = io.Stream
progress.Frame = io.Frame
progress.Packet = io.Packet
progress.FPS = io.FPS
progress.PPS = io.PPS
progress.Quantizer = io.Quantizer
progress.Size = io.Size * 1024
progress.Bitrate = io.Bitrate * 1024
}
type ffmpegProgress struct {
Input []ffmpegProgressIO `json:"inputs"`
Output []ffmpegProgressIO `json:"outputs"`
Frame uint64 `json:"frame"`
Packet uint64 `json:"packet"`
FPS float64 `json:"-"`
PPS float64 `json:"-"`
Quantizer float64 `json:"q"`
Size uint64 `json:"size_kb"` // kbytes
Bitrate float64 `json:"-"` // kbit/s
Time Duration `json:"time"`
Speed float64 `json:"speed"`
Drop uint64 `json:"drop"`
Dup uint64 `json:"dup"`
}
func (p *ffmpegProgress) exportTo(progress *app.Progress) {
progress.Frame = p.Frame
progress.Packet = p.Packet
progress.FPS = p.FPS
progress.PPS = p.PPS
progress.Quantizer = p.Quantizer
progress.Size = p.Size * 1024
progress.Time = p.Time.Seconds()
progress.Bitrate = p.Bitrate * 1024
progress.Speed = p.Speed
progress.Drop = p.Drop
progress.Dup = p.Dup
for i := range p.Input {
if len(progress.Input) <= i {
break
}
p.Input[i].exportTo(&progress.Input[i])
}
for i := range p.Output {
if len(progress.Output) <= i {
break
}
p.Output[i].exportTo(&progress.Output[i])
}
}
type ffmpegProcessIO struct {
// common
Address string `json:"url"`
IP string `json:"-"`
Format string `json:"format"`
Index uint64 `json:"index"`
Stream uint64 `json:"stream"`
Type string `json:"type"`
Codec string `json:"codec"`
Coder string `json:"coder"`
// video
Pixfmt string `json:"pix_fmt"`
Width uint64 `json:"width"`
Height uint64 `json:"height"`
// audio
Sampling uint64 `json:"sampling_hz"`
Layout string `json:"layout"`
Channels uint64 `json:"channels"`
}
func (io *ffmpegProcessIO) export() app.ProgressIO {
return app.ProgressIO{
Address: io.Address,
Format: io.Format,
Index: io.Index,
Stream: io.Stream,
Type: io.Type,
Codec: io.Codec,
Coder: io.Coder,
Pixfmt: io.Pixfmt,
Width: io.Width,
Height: io.Height,
Sampling: io.Sampling,
Layout: io.Layout,
Channels: io.Channels,
}
}
type ffmpegProcess struct {
input []ffmpegProcessIO
output []ffmpegProcessIO
}
func (p *ffmpegProcess) export() app.Progress {
progress := app.Progress{}
for _, io := range p.input {
aio := io.export()
progress.Input = append(progress.Input, aio)
}
for _, io := range p.output {
aio := io.export()
progress.Output = append(progress.Output, aio)
}
return progress
}

239
ffmpeg/probe/prober.go Normal file
View File

@@ -0,0 +1,239 @@
package probe
import (
"encoding/json"
"regexp"
"strconv"
"strings"
"time"
"github.com/datarhei/core/log"
"github.com/datarhei/core/process"
"github.com/datarhei/core/restream/app"
)
type Parser interface {
process.Parser
Probe() app.Probe
}
type Config struct {
Logger log.Logger
}
type prober struct {
data []process.Line
inputs []probeIO
logger log.Logger
}
func New(config Config) Parser {
p := &prober{
logger: config.Logger,
}
if p.logger == nil {
p.logger = log.New("Parser")
}
return p
}
func (p *prober) Probe() app.Probe {
probe := app.Probe{}
for _, io := range p.inputs {
probe.Streams = append(probe.Streams, io.export())
}
probe.Log = make([]string, len(p.data))
for i, line := range p.data {
probe.Log[i] = line.Data
}
return probe
}
func (p *prober) Parse(line string) uint64 {
if strings.HasPrefix(line, "avstream.progress:") {
return 0
}
p.data = append(p.data, process.Line{
Timestamp: time.Now(),
Data: line,
})
return 0
}
func (p *prober) parseJSON(line string) {
inputs := []probeIO{}
err := json.Unmarshal([]byte(line), &inputs)
if err != nil {
p.logger.WithFields(log.Fields{
"line": line,
"error": err,
}).Error().Log("Failed parsing inputs")
}
p.inputs = inputs
}
func (p *prober) parseDefault() {
// Input #0, lavfi, from 'testsrc=size=1280x720:rate=25':
// Input #1, lavfi, from 'anullsrc=r=44100:cl=stereo':
// Output #0, hls, to './data/testsrc.m3u8':
reFormat := regexp.MustCompile(`^Input #([0-9]+), (.*?), (from|to) '([^']+)`)
// Duration: 00:01:02.28, start: 0.000000, bitrate: 5895 kb/s
// Duration: N/A, start: 0.000000, bitrate: 5895 kb/s
reDuration := regexp.MustCompile(`Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)`)
// Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 1280x720 [SAR 1:1 DAR 16:9], 25 tbr, 25 tbn, 25 tbc
// Stream #1:0: Audio: pcm_u8, 44100 Hz, stereo, u8, 705 kb/s
// Stream #0:0: Video: h264 (libx264), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], q=-1--1, 25 fps, 90k tbn, 25 tbc
// Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 64 kb/s
reStream := regexp.MustCompile(`Stream #([0-9]+):([0-9]+)(?:\(([a-z]+)\))?: (Video|Audio|Subtitle): (.*)`)
reStreamCodec := regexp.MustCompile(`^([^\s,]+)`)
reStreamVideoPixfmtSize := regexp.MustCompile(`, ([0-9A-Za-z]+)(\([^\)]+\))?, ([0-9]+)x([0-9]+)`)
reStreamVideoFPS := regexp.MustCompile(`, ([0-9]+(\.[0-9]+)?) fps`)
reStreamAudio := regexp.MustCompile(`, ([0-9]+) Hz, ([^,]+)`)
reStreamBitrate := regexp.MustCompile(`, ([0-9]+) kb/s`)
format := ""
address := ""
var duration float64 = 0.0
for _, line := range p.data {
if matches := reFormat.FindStringSubmatch(line.Data); matches != nil {
format = matches[2]
address = matches[4]
continue
}
if matches := reDuration.FindStringSubmatch(line.Data); matches != nil {
duration = 0.0
// hours
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
duration += x * 60 * 60
}
// minutes
if x, err := strconv.ParseFloat(matches[2], 64); err == nil {
duration += x * 60
}
// seconds
if x, err := strconv.ParseFloat(matches[3], 64); err == nil {
duration += x
}
// fractions
if x, err := strconv.ParseFloat(matches[4], 64); err == nil {
duration += x / 100
}
continue
}
if matches := reStream.FindStringSubmatch(line.Data); matches != nil {
io := probeIO{}
io.Address = address
io.Format = format
io.Duration = duration
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
io.Index = x
}
if x, err := strconv.ParseUint(matches[2], 10, 64); err == nil {
io.Stream = x
}
io.Language = "und"
if len(matches[3]) == 3 {
io.Language = matches[3]
}
io.Type = strings.ToLower(matches[4])
streamDetail := matches[5]
if matches = reStreamCodec.FindStringSubmatch(streamDetail); matches != nil {
io.Codec = matches[1]
}
if matches = reStreamBitrate.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
io.Bitrate = x
}
}
if io.Type == "video" {
if matches = reStreamVideoPixfmtSize.FindStringSubmatch(streamDetail); matches != nil {
io.Pixfmt = matches[1]
if x, err := strconv.ParseUint(matches[3], 10, 64); err == nil {
io.Width = x
}
if x, err := strconv.ParseUint(matches[4], 10, 64); err == nil {
io.Height = x
}
}
if matches = reStreamVideoFPS.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseFloat(matches[1], 64); err == nil {
io.FPS = x
}
}
} else if io.Type == "audio" {
if matches = reStreamAudio.FindStringSubmatch(streamDetail); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
io.Sampling = x
}
io.Layout = matches[2]
}
}
p.inputs = append(p.inputs, io)
}
}
}
func (p *prober) Log() []process.Line {
return p.data
}
func (p *prober) ResetStats() {
hasJSON := false
prefix := "ffmpeg.inputs:"
for _, line := range p.data {
isFFmpegInputs := strings.HasPrefix(line.Data, prefix)
if isFFmpegInputs {
p.parseJSON(strings.TrimPrefix(line.Data, prefix))
hasJSON = true
break
}
}
if !hasJSON {
p.parseDefault()
}
}
func (p *prober) ResetLog() {
p.data = []process.Line{}
p.inputs = []probeIO{}
}

280
ffmpeg/probe/prober_test.go Normal file
View File

@@ -0,0 +1,280 @@
package probe
import (
"strings"
"testing"
)
func TestProber(t *testing.T) {
prober := New(Config{}).(*prober)
rawdata := `ffmpeg version 4.0.2 Copyright (c) 2000-2018 the FFmpeg developers
built with Apple LLVM version 9.1.0 (clang-902.0.39.2)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.0.2 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-gpl --enable-libmp3lame --enable-libx264 --enable-libx265 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma
libavutil 56. 14.100 / 56. 14.100
libavcodec 58. 18.100 / 58. 18.100
libavformat 58. 12.100 / 58. 12.100
libavdevice 58. 3.100 / 58. 3.100
libavfilter 7. 16.100 / 7. 16.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 1.100 / 5. 1.100
libswresample 3. 1.100 / 3. 1.100
libpostproc 55. 1.100 / 55. 1.100
Input #0, lavfi, from 'testsrc=size=1280x720:rate=25':
Duration: N/A, start: 0.000000, bitrate: N/A
Stream #0:0: Video: rawvideo (RGB[24] / 0x18424752), rgb24, 1280x720 [SAR 1:1 DAR 16:9], 25 tbr, 25 tbn, 25 tbc
Input #1, lavfi, from 'anullsrc=r=44100:cl=stereo':
Duration: N/A, start: 0.000000, bitrate: 705 kb/s
Stream #1:0: Audio: pcm_u8, 44100 Hz, stereo, u8, 705 kb/s
Input #2, playout, from 'playout:rtmp://l5gn74l5-vpu.livespotting.com/live/0chl6hu7_360?token=m5ZuiCQYRlIon8':
Duration: N/A, start: 0.000000, bitrate: 265 kb/s
Stream #2:0: Video: h264 (Constrained Baseline), yuvj420p(pc, progressive), 640x360 [SAR 1:1 DAR 16:9], 265 kb/s, 10 fps, 10 tbr, 1000k tbn, 20 tbc
Input #3, mov,mp4,m4a,3gp,3g2,mj2, from 'movie.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf58.20.100
Duration: 00:01:02.28, start: 0.000000, bitrate: 5895 kb/s
Stream #3:0(eng): Video: h264 (Main) (avc1 / 0x31637661), yuvj420p(pc, bt709), 2560x1440 [SAR 1:1 DAR 16:9], 5894 kb/s, 23.98 fps, 25 tbr, 90k tbn, 50 tbc (default)
Stream #3:1(por): Subtitle: subrip
Stream mapping:
Stream #0:0 -> #0:0 (rawvideo (native) -> h264 (libx264))
Stream #1:0 -> #0:1 (pcm_u8 (native) -> aac (native))
Press [q] to stop, [?] for help`
data := strings.Split(rawdata, "\n")
for _, d := range data {
prober.Parse(d)
}
prober.ResetStats()
if len(prober.inputs) != 5 {
t.Errorf("#inputs: want=5, have=%d\n", len(prober.inputs))
return
}
i := prober.inputs[0]
if i.Address != "testsrc=size=1280x720:rate=25" {
t.Errorf("#input0.address: want=testsrc=size=1280x720:rate=25, have=%s\n", i.Address)
}
if i.Format != "lavfi" {
t.Errorf("#input0.format: want=lavfi, have=%s\n", i.Format)
}
if i.Index != 0 {
t.Errorf("#input0.index: want=0, have=%d\n", i.Index)
}
if i.Stream != 0 {
t.Errorf("#input0.stream: want=0, have=%d\n", i.Stream)
}
if i.Language != "und" {
t.Errorf("#input0.language: want=und, have=%s\n", i.Language)
}
if i.Type != "video" {
t.Errorf("#input0.type: want=video, have=%s\n", i.Type)
}
if i.Codec != "rawvideo" {
t.Errorf("#input0.codec: want=rawvideo, have=%s\n", i.Codec)
}
if i.Bitrate != 0 {
t.Errorf("#input0.bitrate: want=0, have=%f\n", i.Bitrate)
}
if i.Duration != 0 {
t.Errorf("#input0.duration: want=0, have=%f\n", i.Duration)
}
if i.FPS != 0 {
t.Errorf("#input0.fps: want=0, have=%f\n", i.FPS)
}
if i.Pixfmt != "rgb24" {
t.Errorf("#input0.pixfmt: want=rgb24, have=%s\n", i.Pixfmt)
}
if i.Width != 1280 {
t.Errorf("#input0.width: want=1280, have=%d\n", i.Width)
}
if i.Height != 720 {
t.Errorf("#input0.height: want=720, have=%d\n", i.Height)
}
i = prober.inputs[1]
if i.Address != "anullsrc=r=44100:cl=stereo" {
t.Errorf("#input1.address: want=anullsrc=r=44100:cl=stereo, have=%s\n", i.Address)
}
if i.Format != "lavfi" {
t.Errorf("#input1.format: want=lavfi, have=%s\n", i.Format)
}
if i.Index != 1 {
t.Errorf("#input1.index: want=1, have=%d\n", i.Index)
}
if i.Stream != 0 {
t.Errorf("#input1.stream: want=0, have=%d\n", i.Stream)
}
if i.Language != "und" {
t.Errorf("#input1.language: want=und, have=%s\n", i.Language)
}
if i.Type != "audio" {
t.Errorf("#input1.type: want=audio, have=%s\n", i.Type)
}
if i.Codec != "pcm_u8" {
t.Errorf("#input1.codec: want=pcm_u8, have=%s\n", i.Codec)
}
if i.Bitrate != 705 {
t.Errorf("#input1.bitrate: want=705, have=%f\n", i.Bitrate)
}
if i.Duration != 0 {
t.Errorf("#input1.duration: want=0, have=%f\n", i.Duration)
}
if i.Sampling != 44100 {
t.Errorf("#input1.sampling: want=44100, have=%d\n", i.Sampling)
}
if i.Layout != "stereo" {
t.Errorf("#input1.layout: want=stereo, have=%s\n", i.Layout)
}
i = prober.inputs[2]
if i.Address != "playout:rtmp://l5gn74l5-vpu.livespotting.com/live/0chl6hu7_360?token=m5ZuiCQYRlIon8" {
t.Errorf("#input2.address: want=playout:rtmp://l5gn74l5-vpu.livespotting.com/live/0chl6hu7_360?token=m5ZuiCQYRlIon8, have=%s\n", i.Address)
}
if i.Format != "playout" {
t.Errorf("#input2.format: want=playout, have=%s\n", i.Format)
}
if i.Index != 2 {
t.Errorf("#input2.index: want=2, have=%d\n", i.Index)
}
if i.Stream != 0 {
t.Errorf("#input2.stream: want=0, have=%d\n", i.Stream)
}
if i.Language != "und" {
t.Errorf("#input2.language: want=und, have=%s\n", i.Language)
}
if i.Type != "video" {
t.Errorf("#input2.type: want=video, have=%s\n", i.Type)
}
if i.Codec != "h264" {
t.Errorf("#input2.codec: want=h264, have=%s\n", i.Codec)
}
if i.Bitrate != 265 {
t.Errorf("#input2.bitrate: want=265, have=%f\n", i.Bitrate)
}
if i.Duration != 0 {
t.Errorf("#input2.duration: want=0, have=%f\n", i.Duration)
}
if i.FPS != 10 {
t.Errorf("#input2.fps: want=10, have=%f\n", i.FPS)
}
if i.Pixfmt != "yuvj420p" {
t.Errorf("#input2.pixfmt: want=yuvj420p, have=%s\n", i.Pixfmt)
}
if i.Width != 640 {
t.Errorf("#input2.width: want=640, have=%d\n", i.Width)
}
if i.Height != 360 {
t.Errorf("#input2.height: want=360, have=%d\n", i.Height)
}
i = prober.inputs[3]
if i.Address != "movie.mp4" {
t.Errorf("#input3.address: want=movie.mp4, have=%s\n", i.Address)
}
if i.Format != "mov,mp4,m4a,3gp,3g2,mj2" {
t.Errorf("#input3.format: want=mov,mp4,m4a,3gp,3g2,mj2, have=%s\n", i.Format)
}
if i.Index != 3 {
t.Errorf("#input3.index: want=3, have=%d\n", i.Index)
}
if i.Stream != 0 {
t.Errorf("#input3.stream: want=0, have=%d\n", i.Stream)
}
if i.Language != "eng" {
t.Errorf("#input3.language: want=eng, have=%s\n", i.Language)
}
if i.Type != "video" {
t.Errorf("#input3.type: want=video, have=%s\n", i.Type)
}
if i.Codec != "h264" {
t.Errorf("#input3.codec: want=h264, have=%s\n", i.Codec)
}
if i.Bitrate != 5894 {
t.Errorf("#input3.bitrate: want=5894, have=%f\n", i.Bitrate)
}
if i.Duration != 62.28 {
t.Errorf("#input3.duration: want=62.82, have=%f\n", i.Duration)
}
if i.FPS != 23.98 {
t.Errorf("#input3.fps: want=23.98, have=%f\n", i.FPS)
}
if i.Pixfmt != "yuvj420p" {
t.Errorf("#input3.pixfmt: want=yuvj420p, have=%s\n", i.Pixfmt)
}
if i.Width != 2560 {
t.Errorf("#input3.width: want=2560, have=%d\n", i.Width)
}
if i.Height != 1440 {
t.Errorf("#input3.height: want=1440, have=%d\n", i.Height)
}
i = prober.inputs[4]
if i.Language != "por" {
t.Errorf("#input4.language: want=por, have=%s\n", i.Language)
}
if i.Type != "subtitle" {
t.Errorf("#input4.type: want=subtitle, have=%s\n", i.Type)
}
if i.Codec != "subrip" {
t.Errorf("#input4.codec: want=subtip, have=%s\n", i.Codec)
}
}

52
ffmpeg/probe/types.go Normal file
View File

@@ -0,0 +1,52 @@
package probe
import (
"github.com/datarhei/core/restream/app"
)
type probeIO struct {
// common
Address string `json:"url"`
Format string `json:"format"`
Index uint64 `json:"index"`
Stream uint64 `json:"stream"`
Language string `json:"language"`
Type string `json:"type"`
Codec string `json:"codec"`
Coder string `json:"coder"`
Bitrate float64 `json:"bitrate_kbps"`
Duration float64 `json:"duration_sec"`
// video
FPS float64 `json:"fps"`
Pixfmt string `json:"pix_fmt"`
Width uint64 `json:"width"`
Height uint64 `json:"height"`
// audio
Sampling uint64 `json:"sampling_hz"`
Layout string `json:"layout"`
Channels uint64 `json:"channels"`
}
func (io *probeIO) export() app.ProbeIO {
return app.ProbeIO{
Address: io.Address,
Format: io.Format,
Index: io.Index,
Stream: io.Stream,
Language: io.Language,
Type: io.Type,
Codec: io.Codec,
Coder: io.Coder,
Bitrate: io.Bitrate,
Duration: io.Duration,
FPS: io.FPS,
Pixfmt: io.Pixfmt,
Width: io.Width,
Height: io.Height,
Sampling: io.Sampling,
Layout: io.Layout,
Channels: io.Channels,
}
}

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