mirror of
https://github.com/datarhei/core.git
synced 2025-09-26 20:11:29 +08:00
1434 lines
38 KiB
Go
1434 lines
38 KiB
Go
package main
|
|
|
|
// TODO: Sonderzeichen im userpass von importierten URLs
|
|
// TODO: import von internal RTMP (external.stream), wenn jemand z.B. von OBS reinschiesst
|
|
|
|
import (
|
|
gojson "encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/datarhei/core/v16/encoding/json"
|
|
"github.com/datarhei/core/v16/ffmpeg"
|
|
"github.com/datarhei/core/v16/ffmpeg/skills"
|
|
"github.com/datarhei/core/v16/restream"
|
|
"github.com/datarhei/core/v16/restream/app"
|
|
"github.com/datarhei/core/v16/restream/store"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type storeDataV1 struct {
|
|
Addresses struct {
|
|
Output string `json:"optionalOutputAddress"`
|
|
Input string `json:"srcAddress"`
|
|
Streams struct {
|
|
Audio *storeDataV1Stream `json:"audio"`
|
|
Video *storeDataV1Stream `json:"video"`
|
|
} `json:"srcStreams"`
|
|
} `json:"addresses"`
|
|
|
|
Options struct {
|
|
Audio struct {
|
|
Bitrate string `json:"bitrate"`
|
|
Channels string `json:"channels"`
|
|
Codec string `json:"codec"`
|
|
Preset string `json:"preset"`
|
|
Sampling string `json:"sampling"`
|
|
} `json:"audio"`
|
|
|
|
Output struct {
|
|
HLS struct {
|
|
ListSize string `json:"listSize"`
|
|
Method string `json:"method"`
|
|
Time string `json:"time"`
|
|
Timeout string `json:"timeout"`
|
|
} `json:"hls"`
|
|
Type string `json:"type"`
|
|
} `json:"output"`
|
|
|
|
Player struct {
|
|
Autoplay bool `json:"autoplay"`
|
|
Color string `json:"color"`
|
|
Mute bool `json:"mute"`
|
|
Statistics bool `json:"statistics"`
|
|
Logo struct {
|
|
Image string `json:"image"`
|
|
Link string `json:"link"`
|
|
Position string `json:"position"`
|
|
} `json:"logo"`
|
|
} `json:"player"`
|
|
|
|
RTSPTCP bool `json:"rtspTcp"`
|
|
|
|
Video struct {
|
|
Bitrate string `json:"bitrate"`
|
|
Codec string `json:"codec"`
|
|
FPS string `json:"fps"`
|
|
GOP string `json:"gop"`
|
|
Preset string `json:"preset"`
|
|
Profile string `json:"profile"`
|
|
Tune string `json:"tune"`
|
|
}
|
|
} `json:"options"`
|
|
|
|
States struct {
|
|
Ingest struct {
|
|
Type string `json:"type"`
|
|
} `json:"repeatToLocalNginx"`
|
|
Egress struct {
|
|
Type string `json:"type"`
|
|
} `json:"repeatToOptionalOutput"`
|
|
} `json:"states"`
|
|
|
|
Actions struct {
|
|
Ingest string `json:"repeatToLocalNginx"`
|
|
Egress string `json:"repeatToOptionalOutput"`
|
|
} `json:"userActions"`
|
|
}
|
|
|
|
type storeDataV1Stream struct {
|
|
Index int `json:"index"`
|
|
Type string `json:"type"`
|
|
Codec string `json:"codec"`
|
|
|
|
// Video
|
|
Width int `json:"width,omitempty"`
|
|
Height int `json:"height,omitempty"`
|
|
Format string `json:"format,omitempty"`
|
|
|
|
// Audio
|
|
Layout string `json:"layout,omitempty"`
|
|
Channels int `json:"channels,omitempty"`
|
|
Sampling string `json:"sampling,omitempty"`
|
|
}
|
|
|
|
type restreamerUISourceSettingsNetwork struct {
|
|
Address string `json:"address"`
|
|
General struct {
|
|
Flags []string `json:"fflags"`
|
|
ThreadQueueSize int `json:"thread_queue_size"`
|
|
} `json:"general"`
|
|
HTTP struct {
|
|
ForceFramerate bool `json:"forceFramerate"`
|
|
Framerate int `json:"framerate"`
|
|
ReadNative bool `json:"readNative"`
|
|
} `json:"http"`
|
|
Mode string `json:"mode"`
|
|
RTSP struct {
|
|
STimeout int `json:"stimeout"`
|
|
UDP bool `json:"udp"`
|
|
} `json:"rtsp"`
|
|
}
|
|
|
|
type restreamerUISourceSettingsV4L struct {
|
|
Device string `json:"device"`
|
|
Format string `json:"format"`
|
|
Framerate int `json:"framerate"`
|
|
Size string `json:"size"`
|
|
}
|
|
|
|
type restreamerUISourceSettingsVirtualAudio struct {
|
|
Amplitude int `json:"amplitude"`
|
|
BeepFactor int `json:"beepfactor"`
|
|
Color string `json:"color"`
|
|
Frequency int `json:"frequency"`
|
|
Layout string `json:"layout"`
|
|
Sampling int `json:"sampling"`
|
|
Source string `json:"source"`
|
|
}
|
|
|
|
type restreamerUISourceSettingsALSA struct {
|
|
Address string `json:"address"`
|
|
Device string `json:"device"`
|
|
Sampling int `json:"sampling"`
|
|
Channels int `json:"channels"`
|
|
Delay int `json:"delay"`
|
|
}
|
|
|
|
type restreamerUISourceStream struct {
|
|
Bitrate int `json:"bitrate_kbps"`
|
|
Channels int `json:"channels"`
|
|
Codec string `json:"codec"`
|
|
Coder string `json:"coder"`
|
|
Duration int `json:"duration_sec"`
|
|
Format string `json:"format"`
|
|
FPS int `json:"fps"`
|
|
Height int `json:"height"`
|
|
Index int `json:"index"`
|
|
Language string `json:"language"`
|
|
Layout string `json:"layout"`
|
|
PixelFormat string `json:"pix_fmt"`
|
|
Sampling int `json:"sampling_hz"`
|
|
Stream int `json:"stream"`
|
|
Type string `json:"type"`
|
|
URL string `json:"url"`
|
|
Width int `json:"width"`
|
|
}
|
|
|
|
type restreamerUISourceInput struct {
|
|
Address string `json:"address"`
|
|
Options []string `json:"options"`
|
|
}
|
|
|
|
type restreamerUISource struct {
|
|
Inputs []restreamerUISourceInput `json:"inputs"`
|
|
Settings interface{} `json:"settings"`
|
|
Streams []restreamerUISourceStream `json:"streams"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type restreamerUIPlayer struct {
|
|
Autoplay bool `json:"autoplay"`
|
|
Color struct {
|
|
Buttons string `json:"buttons"`
|
|
Seekbar string `json:"seekbar"`
|
|
} `json:"color"`
|
|
GA struct {
|
|
Account string `json:"account"`
|
|
Name string `json:"name"`
|
|
} `json:"ga"`
|
|
Logo struct {
|
|
Image string `json:"image"`
|
|
Link string `json:"link"`
|
|
Position string `json:"position"`
|
|
} `json:"logo"`
|
|
Mute bool `json:"mute"`
|
|
Statistics bool `json:"statistics"`
|
|
}
|
|
|
|
type restreamerUIProfileCoderSettingsAAC struct {
|
|
Bitrate string `json:"bitrate"`
|
|
Channels string `json:"channels"`
|
|
Layout string `json:"layout"`
|
|
Sampling string `json:"sampling"`
|
|
}
|
|
|
|
type restreamerUIProfileCoderSettingsX264 struct {
|
|
Bitrate string `json:"bitrate"`
|
|
FPS string `json:"fps"`
|
|
Preset string `json:"preset"`
|
|
Profile string `json:"profile"`
|
|
Tune string `json:"tune"`
|
|
}
|
|
|
|
type restreamerUIProfileCoderSettingsCopy struct{}
|
|
|
|
type restreamerUIProfileAV struct {
|
|
Coder string `json:"coder"`
|
|
Codec string `json:"codec"`
|
|
Mapping []string `json:"mapping"`
|
|
Settings interface{} `json:"settings"`
|
|
}
|
|
|
|
type restreamerUIProfile struct {
|
|
Audio struct {
|
|
Encoder restreamerUIProfileAV `json:"encoder"`
|
|
Decoder restreamerUIProfileAV `json:"decoder"`
|
|
Source int `json:"source"`
|
|
Stream int `json:"stream"`
|
|
} `json:"audio"`
|
|
Video struct {
|
|
Encoder restreamerUIProfileAV `json:"encoder"`
|
|
Decoder restreamerUIProfileAV `json:"decoder"`
|
|
Source int `json:"source"`
|
|
Stream int `json:"stream"`
|
|
} `json:"video"`
|
|
}
|
|
|
|
type restreamerUIIngest struct {
|
|
Control struct {
|
|
HLS struct {
|
|
LHLS bool `json:"lhls"`
|
|
SegmentDuration int `json:"segmentDuration"`
|
|
ListSize int `json:"listSize"`
|
|
} `json:"hls"`
|
|
Process struct {
|
|
Autostart bool `json:"autostart"`
|
|
Reconnect bool `json:"reconnect"`
|
|
Delay int `json:"delay"`
|
|
StaleTimeout int `json:"staleTimeout"`
|
|
} `json:"process"`
|
|
Snapshot struct {
|
|
Enable bool `json:"enable"`
|
|
Interval int `json:"interval"`
|
|
} `json:"snapshot"`
|
|
} `json:"control"`
|
|
License string `json:"license"`
|
|
Meta struct {
|
|
Author struct {
|
|
Description string `json:"description"`
|
|
Name string `json:"name"`
|
|
} `json:"author"`
|
|
Description string `json:"description"`
|
|
Name string `json:"name"`
|
|
} `json:"meta"`
|
|
Player restreamerUIPlayer `json:"player"`
|
|
Profiles []restreamerUIProfile `json:"profiles"`
|
|
Sources []restreamerUISource `json:"sources"`
|
|
Version int `json:"version"`
|
|
Imported bool `json:"imported"`
|
|
}
|
|
|
|
type restreamerUIEgressSettingsRTMP struct {
|
|
Address string `json:"address"`
|
|
Protocol string `json:"protocol"`
|
|
}
|
|
|
|
type restreamerUIEgressSettingsHLS struct {
|
|
Address string `json:"address"`
|
|
Protocol string `json:"protocol"`
|
|
Options struct {
|
|
DeleteThreshold string `json:"hls_delete_threshold"`
|
|
Flags []string `json:"hls_flags"`
|
|
InitTime string `json:"hls_init_time"`
|
|
ListSize string `json:"hls_list_size"`
|
|
SegmentType string `json:"hls_segment_type"`
|
|
Time string `json:"hls_time"`
|
|
Method string `json:"method"`
|
|
StartNumber string `json:"start_number"`
|
|
Timeout string `json:"timeout"`
|
|
StartNumberSource string `json:"hls_start_number_source"`
|
|
AllowCache string `json:"hls_allow_cache"`
|
|
Enc string `json:"hls_enc"`
|
|
IgnoreIOErrors string `json:"ignore_io_errors"`
|
|
HTTPersistent string `json:"http_persistent"`
|
|
} `json:"options"`
|
|
}
|
|
|
|
type restreamerUIEgress struct {
|
|
Control struct {
|
|
Process struct {
|
|
Autostart bool `json:"autostart"`
|
|
Reconnect bool `json:"reconnect"`
|
|
Delay int `json:"delay"`
|
|
StaleTimeout int `json:"staleTimeout"`
|
|
} `json:"process"`
|
|
} `json:"control"`
|
|
Name string `json:"name"`
|
|
Output struct {
|
|
Address string `json:"address"`
|
|
Options []string `json:"options"`
|
|
} `json:"output"`
|
|
Settings interface{} `json:"settings"`
|
|
Version int `json:"version"`
|
|
}
|
|
|
|
func importSnapshotInterval(value string, defval int) int {
|
|
interval, err := strconv.Atoi(value)
|
|
if err == nil {
|
|
return interval / 1000
|
|
}
|
|
|
|
duration, err := time.ParseDuration(value)
|
|
if err == nil {
|
|
return int(duration.Seconds())
|
|
}
|
|
|
|
return defval
|
|
}
|
|
|
|
var v1Environment = map[string]struct {
|
|
value string
|
|
defval string
|
|
}{
|
|
"RS_SNAPSHOT_INTERVAL": {defval: "60s"},
|
|
"RS_MODE": {defval: ""},
|
|
|
|
"RS_INPUTSTREAM": {defval: ""},
|
|
"RS_OUTPUTSTREAM": {defval: ""},
|
|
|
|
"RS_USBCAM_VIDEODEVICE": {defval: "/dev/video"},
|
|
"RS_USBCAM_FPS": {defval: "25"},
|
|
"RS_USBCAM_GOP": {defval: "50"},
|
|
"RS_USBCAM_BITRATE": {defval: "5000000"},
|
|
"RS_USBCAM_H264PRESET": {defval: "ultrafast"},
|
|
"RS_USBCAM_H264PROFILE": {defval: "baseline"},
|
|
"RS_USBCAM_WIDTH": {defval: "1280"},
|
|
"RS_USBCAM_HEIGHT": {defval: "720"},
|
|
"RS_USBCAM_AUDIO": {defval: "false"},
|
|
"RS_USBCAM_AUDIODEVICE": {defval: "0"},
|
|
"RS_USBCAM_AUDIOBITRATE": {defval: "64000"},
|
|
"RS_USBCAM_AUDIOCHANNELS": {defval: "1"},
|
|
"RS_USBCAM_AUDIOLAYOUT": {defval: "mono"},
|
|
"RS_USBCAM_AUDIOSAMPLING": {defval: "44100"},
|
|
|
|
"RS_RASPICAM_FORMAT": {defval: "h264"},
|
|
"RS_RASPICAM_FPS": {defval: "25"},
|
|
"RS_RASPICAM_WIDTH": {defval: "1920"},
|
|
"RS_RASPICAM_HEIGHT": {defval: "1080"},
|
|
"RS_RASPICAM_AUDIO": {defval: "false"},
|
|
"RS_RASPICAM_AUDIODEVICE": {defval: "0"},
|
|
"RS_RASPICAM_AUDIOBITRATE": {defval: "64000"},
|
|
"RS_RASPICAM_AUDIOCHANNELS": {defval: "1"},
|
|
"RS_RASPICAM_AUDIOLAYOUT": {defval: "mono"},
|
|
"RS_RASPICAM_AUDIOSAMPLING": {defval: "44100"},
|
|
}
|
|
|
|
func initV1Environment() {
|
|
for key, env := range v1Environment {
|
|
value, ok := os.LookupEnv(key)
|
|
if !ok {
|
|
value = v1Environment[key].defval
|
|
}
|
|
|
|
env.value = value
|
|
|
|
v1Environment[key] = env
|
|
}
|
|
}
|
|
|
|
func getV1Environment(key string) string {
|
|
value := v1Environment[key]
|
|
|
|
return value.value
|
|
}
|
|
|
|
func importConfigFromEnvironment() importConfig {
|
|
initV1Environment()
|
|
|
|
c := importConfig{
|
|
id: uuid.New().String(),
|
|
snapshotInterval: importSnapshotInterval(getV1Environment("RS_SNAPSHOT_INTERVAL"), 0),
|
|
|
|
inputstream: getV1Environment("RS_INPUTSTREAM"),
|
|
outputstream: getV1Environment("RS_OUTPUTSTREAM"),
|
|
}
|
|
|
|
mode := getV1Environment("RS_MODE")
|
|
|
|
if mode == "USBCAM" {
|
|
c.usbcam.enable = true
|
|
c.usbcam.device = getV1Environment("RS_USBCAM_VIDEODEVICE")
|
|
c.usbcam.fps = getV1Environment("RS_USBCAM_FPS")
|
|
c.usbcam.gop = getV1Environment("RS_USBCAM_GOP")
|
|
c.usbcam.bitrate = getV1Environment("RS_USBCAM_BITRATE")
|
|
c.usbcam.preset = getV1Environment("RS_USBCAM_H264PRESET")
|
|
c.usbcam.profile = getV1Environment("RS_USBCAM_H264PROFILE")
|
|
c.usbcam.width = getV1Environment("RS_USBCAM_WIDTH")
|
|
c.usbcam.height = getV1Environment("RS_USBCAM_HEIGHT")
|
|
|
|
if getV1Environment("RS_USBCAM_AUDIO") == "true" {
|
|
c.audio.enable = true
|
|
c.audio.device = getV1Environment("RS_USBCAM_AUDIODEVICE")
|
|
c.audio.bitrate = getV1Environment("RS_USBCAM_AUDIOBITRATE")
|
|
c.audio.channels = getV1Environment("RS_USBCAM_AUDIOCHANNELS")
|
|
c.audio.layout = getV1Environment("RS_USBCAM_AUDIOLAYOUT")
|
|
c.audio.sampling = getV1Environment("RS_USBCAM_AUDIOSAMPLING")
|
|
}
|
|
} else if mode == "RASPICAM" {
|
|
c.raspicam.enable = true
|
|
|
|
devices, _ := skills.DevicesV4L()
|
|
for _, device := range devices {
|
|
if strings.Contains(device.Extra, "bcm2835-v4l2") {
|
|
c.raspicam.device = device.Id
|
|
break
|
|
}
|
|
}
|
|
|
|
c.raspicam.format = getV1Environment("RS_RASPICAM_FORMAT")
|
|
c.raspicam.fps = getV1Environment("RS_RASPICAM_FPS")
|
|
c.raspicam.width = getV1Environment("RS_RASPICAM_WIDTH")
|
|
c.raspicam.height = getV1Environment("RS_RASPICAM_HEIGHT")
|
|
|
|
if getV1Environment("RS_RASPICAM_AUDIO") == "true" {
|
|
c.audio.enable = true
|
|
c.audio.device = getV1Environment("RS_RASPICAM_AUDIODEVICE")
|
|
c.audio.bitrate = getV1Environment("RS_RASPICAM_AUDIOBITRATE")
|
|
c.audio.channels = getV1Environment("RS_RASPICAM_AUDIOCHANNELS")
|
|
c.audio.layout = getV1Environment("RS_RASPICAM_AUDIOLAYOUT")
|
|
c.audio.sampling = getV1Environment("RS_RASPICAM_AUDIOSAMPLING")
|
|
}
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
type importConfig struct {
|
|
id string
|
|
snapshotInterval int
|
|
|
|
binary string
|
|
|
|
inputstream string
|
|
outputstream string
|
|
|
|
usbcam importConfigUSBCam
|
|
raspicam importConfigRaspiCam
|
|
audio importConfigAudio
|
|
}
|
|
|
|
type importConfigUSBCam struct {
|
|
enable bool
|
|
device string
|
|
fps string
|
|
gop string
|
|
bitrate string
|
|
preset string
|
|
profile string
|
|
width string
|
|
height string
|
|
}
|
|
|
|
type importConfigRaspiCam struct {
|
|
enable bool
|
|
device string
|
|
format string
|
|
fps string
|
|
width string
|
|
height string
|
|
}
|
|
|
|
type importConfigAudio struct {
|
|
enable bool
|
|
device string
|
|
bitrate string
|
|
channels string
|
|
layout string
|
|
sampling string
|
|
}
|
|
|
|
func importV1(path string, cfg importConfig) (store.StoreData, error) {
|
|
if len(cfg.id) == 0 {
|
|
cfg.id = uuid.New().String()
|
|
}
|
|
|
|
r := store.NewStoreData()
|
|
|
|
jsondata, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return r, fmt.Errorf("failed to read data from %s: %w", path, err)
|
|
}
|
|
|
|
var v1data storeDataV1
|
|
|
|
if err := gojson.Unmarshal(jsondata, &v1data); err != nil {
|
|
return r, json.FormatError(jsondata, err)
|
|
}
|
|
|
|
if len(v1data.Addresses.Input) == 0 {
|
|
if len(cfg.inputstream) != 0 {
|
|
v1data.Addresses.Input = cfg.inputstream
|
|
v1data.Addresses.Streams.Video = nil
|
|
v1data.Addresses.Streams.Audio = nil
|
|
v1data.States.Ingest.Type = "connected"
|
|
v1data.Actions.Ingest = "start"
|
|
|
|
if len(v1data.Addresses.Output) == 0 {
|
|
v1data.Addresses.Output = cfg.outputstream
|
|
v1data.States.Egress.Type = "connected"
|
|
v1data.Actions.Egress = "stop"
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.usbcam.enable || cfg.raspicam.enable {
|
|
if cfg.usbcam.enable {
|
|
if v1data.Options.Video.Codec == "copy" {
|
|
v1data.Options.Video.Codec = "h264"
|
|
v1data.Options.Video.Preset = cfg.usbcam.preset
|
|
|
|
if bitrate, err := strconv.Atoi(cfg.usbcam.bitrate); err == nil {
|
|
v1data.Options.Video.Bitrate = strconv.Itoa(bitrate / 1000)
|
|
} else {
|
|
v1data.Options.Video.Bitrate = "2048"
|
|
}
|
|
|
|
v1data.Options.Video.FPS = cfg.usbcam.fps
|
|
v1data.Options.Video.Profile = cfg.usbcam.profile
|
|
v1data.Options.Video.Tune = "zerolatency"
|
|
}
|
|
}
|
|
|
|
if v1data.Options.Audio.Codec == "silence" || v1data.Options.Audio.Codec == "none" {
|
|
cfg.audio.enable = false
|
|
}
|
|
|
|
if cfg.audio.enable {
|
|
v1data.Options.Audio.Codec = "aac"
|
|
v1data.Options.Audio.Preset = "device"
|
|
|
|
if bitrate, err := strconv.Atoi(cfg.audio.bitrate); err == nil {
|
|
v1data.Options.Audio.Bitrate = strconv.Itoa(bitrate / 1000)
|
|
} else {
|
|
v1data.Options.Audio.Bitrate = "64"
|
|
}
|
|
|
|
v1data.Options.Audio.Channels = "mono"
|
|
if cfg.audio.channels == "2" {
|
|
v1data.Options.Audio.Channels = "stereo"
|
|
}
|
|
|
|
v1data.Options.Audio.Sampling = cfg.audio.sampling
|
|
}
|
|
}
|
|
|
|
process := &app.Process{
|
|
ID: "restreamer-ui:ingest:" + cfg.id,
|
|
Reference: cfg.id,
|
|
CreatedAt: time.Now().Unix(),
|
|
Order: "stop",
|
|
}
|
|
|
|
if v1data.Actions.Ingest == "start" {
|
|
process.Order = "start"
|
|
}
|
|
|
|
config := &app.Config{
|
|
ID: "restreamer-ui:ingest:" + cfg.id,
|
|
Reference: cfg.id,
|
|
Reconnect: true,
|
|
ReconnectDelay: 15,
|
|
Autostart: true,
|
|
StaleTimeout: 30,
|
|
Options: []string{
|
|
"-err_detect",
|
|
"ignore_err",
|
|
},
|
|
}
|
|
|
|
// UI Settings
|
|
|
|
ui := restreamerUIIngest{
|
|
License: "CC BY 4.0",
|
|
Player: restreamerUIPlayer{},
|
|
Profiles: []restreamerUIProfile{},
|
|
Sources: []restreamerUISource{},
|
|
Version: 1,
|
|
Imported: true,
|
|
}
|
|
|
|
ui.Control.HLS.LHLS = false
|
|
ui.Control.HLS.SegmentDuration = 2
|
|
ui.Control.HLS.ListSize = 6
|
|
|
|
ui.Control.Process.Autostart = true
|
|
ui.Control.Process.Reconnect = true
|
|
ui.Control.Process.Delay = 15
|
|
ui.Control.Process.StaleTimeout = 30
|
|
|
|
ui.Control.Snapshot.Enable = false
|
|
ui.Control.Snapshot.Interval = 60
|
|
if cfg.snapshotInterval != 0 {
|
|
ui.Control.Snapshot.Enable = true
|
|
ui.Control.Snapshot.Interval = cfg.snapshotInterval
|
|
}
|
|
|
|
ui.Meta.Author.Description = ""
|
|
ui.Meta.Author.Name = ""
|
|
|
|
ui.Meta.Description = "Live from earth. Powered by datarhei Restreamer."
|
|
ui.Meta.Name = "Livestream"
|
|
|
|
ui.Player.Autoplay = v1data.Options.Player.Autoplay
|
|
ui.Player.Color.Buttons = v1data.Options.Player.Color
|
|
ui.Player.Color.Seekbar = v1data.Options.Player.Color
|
|
ui.Player.Logo.Image = v1data.Options.Player.Logo.Image
|
|
ui.Player.Logo.Link = v1data.Options.Player.Logo.Link
|
|
ui.Player.Logo.Position = v1data.Options.Player.Logo.Position
|
|
ui.Player.Mute = v1data.Options.Player.Mute
|
|
ui.Player.Statistics = v1data.Options.Player.Statistics
|
|
|
|
if cfg.usbcam.enable || cfg.raspicam.enable {
|
|
source := restreamerUISource{
|
|
Type: "video4linux2",
|
|
}
|
|
|
|
settings := restreamerUISourceSettingsV4L{}
|
|
|
|
if cfg.usbcam.enable {
|
|
settings.Device = cfg.usbcam.device
|
|
settings.Format = "nv12"
|
|
} else {
|
|
settings.Device = cfg.raspicam.device
|
|
settings.Format = cfg.raspicam.format
|
|
}
|
|
|
|
if x, err := strconv.Atoi(cfg.usbcam.fps); err == nil {
|
|
settings.Framerate = x
|
|
}
|
|
|
|
var width int = 0
|
|
var height int = 0
|
|
|
|
if w, err := strconv.Atoi(cfg.usbcam.width); err == nil {
|
|
width = w
|
|
if h, err := strconv.Atoi(cfg.usbcam.height); err == nil {
|
|
height = h
|
|
settings.Size = strconv.Itoa(w) + "x" + strconv.Itoa(h)
|
|
}
|
|
}
|
|
|
|
if len(settings.Size) == 0 {
|
|
settings.Size = "1280x720"
|
|
}
|
|
|
|
source.Settings = settings
|
|
|
|
input := restreamerUISourceInput{
|
|
Address: settings.Device,
|
|
Options: []string{
|
|
"-thread_queue_size", "512",
|
|
"-f", "v4l2",
|
|
"-framerate", strconv.Itoa(settings.Framerate),
|
|
"-video_size", settings.Size,
|
|
"-input_format", settings.Format,
|
|
},
|
|
}
|
|
|
|
source.Inputs = append(source.Inputs, input)
|
|
|
|
source.Streams = append(source.Streams, restreamerUISourceStream{
|
|
URL: settings.Device,
|
|
Type: "video",
|
|
Index: 0,
|
|
Stream: 0,
|
|
Codec: "rawvideo",
|
|
Width: width,
|
|
Height: height,
|
|
PixelFormat: settings.Format,
|
|
})
|
|
|
|
ui.Sources = append(ui.Sources, source)
|
|
|
|
if cfg.audio.enable {
|
|
source := restreamerUISource{
|
|
Type: "alsa",
|
|
}
|
|
|
|
settings := restreamerUISourceSettingsALSA{
|
|
Address: "hw:" + cfg.audio.device,
|
|
Device: cfg.audio.device,
|
|
}
|
|
|
|
re := regexp.MustCompile("([0-9]+),([0-9]+)")
|
|
if !re.MatchString(cfg.audio.device) {
|
|
settings.Address = "hw:1,0"
|
|
settings.Device = "1,0"
|
|
}
|
|
|
|
if x, err := strconv.Atoi(cfg.audio.sampling); err == nil {
|
|
settings.Sampling = x
|
|
}
|
|
|
|
if x, err := strconv.Atoi(cfg.audio.channels); err == nil {
|
|
settings.Channels = x
|
|
}
|
|
|
|
source.Settings = settings
|
|
|
|
input := restreamerUISourceInput{
|
|
Address: settings.Address,
|
|
Options: []string{
|
|
"-thread_queue_size", "512",
|
|
"-f", "alsa",
|
|
"-ac", cfg.audio.channels,
|
|
"-ar", cfg.audio.sampling,
|
|
},
|
|
}
|
|
|
|
source.Inputs = append(source.Inputs, input)
|
|
|
|
source.Streams = append(source.Streams, restreamerUISourceStream{
|
|
URL: settings.Address,
|
|
Type: "audio",
|
|
Index: 0,
|
|
Stream: 0,
|
|
Codec: "pcm_u8",
|
|
Layout: cfg.audio.layout,
|
|
Channels: settings.Channels,
|
|
Sampling: settings.Sampling,
|
|
Format: "alsa",
|
|
})
|
|
|
|
ui.Sources = append(ui.Sources, source)
|
|
}
|
|
} else {
|
|
var inputURL *url.URL = nil
|
|
|
|
if len(v1data.Addresses.Input) != 0 {
|
|
var err error
|
|
inputURL, err = url.Parse(v1data.Addresses.Input)
|
|
if err != nil {
|
|
inputURL = nil
|
|
}
|
|
}
|
|
|
|
if inputURL == nil {
|
|
return r, nil
|
|
}
|
|
|
|
source := restreamerUISource{
|
|
Type: "network",
|
|
}
|
|
|
|
settings := restreamerUISourceSettingsNetwork{
|
|
Address: v1data.Addresses.Input,
|
|
Mode: "pull",
|
|
}
|
|
|
|
settings.General.Flags = []string{
|
|
"genpts",
|
|
}
|
|
settings.General.ThreadQueueSize = 512
|
|
|
|
settings.HTTP.ForceFramerate = false
|
|
settings.HTTP.Framerate = 25
|
|
settings.HTTP.ReadNative = true
|
|
|
|
settings.RTSP.STimeout = 5000000
|
|
settings.RTSP.UDP = !v1data.Options.RTSPTCP
|
|
|
|
source.Settings = settings
|
|
|
|
input := restreamerUISourceInput{
|
|
Address: v1data.Addresses.Input,
|
|
Options: []string{
|
|
"-fflags",
|
|
"+genpts",
|
|
"-thread_queue_size",
|
|
"512",
|
|
},
|
|
}
|
|
|
|
if strings.HasPrefix(v1data.Addresses.Input, "rtsp://") {
|
|
input.Options = append(input.Options, "-stimeout", "5000000")
|
|
if v1data.Options.RTSPTCP {
|
|
input.Options = append(input.Options, "-rtsp_transport", "tcp")
|
|
}
|
|
} else if strings.HasPrefix(v1data.Addresses.Input, "http://") || strings.HasPrefix(v1data.Addresses.Input, "https://") {
|
|
input.Options = append(input.Options, "-analyzeduration", "20000000")
|
|
input.Options = append(input.Options, "-re")
|
|
} else if strings.HasPrefix(v1data.Addresses.Input, "rtmp://") {
|
|
input.Options = append(input.Options, "-analyzeduration", "3000000")
|
|
}
|
|
|
|
source.Inputs = append(source.Inputs, input)
|
|
|
|
if v1data.Addresses.Streams.Video == nil {
|
|
// In case there's no video stream info (pre 0.6.7 database, RS_INPUTSTREAM) we have to probe the stream.
|
|
// If this doesn't work, we have to make some assumptions.
|
|
v1data.Addresses.Streams.Video = &storeDataV1Stream{
|
|
Index: 0,
|
|
Type: "video",
|
|
Codec: "h264",
|
|
Width: 1280,
|
|
Height: 720,
|
|
Format: "yuv420p",
|
|
}
|
|
|
|
config := app.Config{
|
|
ID: "process",
|
|
Input: []app.ConfigIO{
|
|
{
|
|
ID: "in",
|
|
Address: input.Address,
|
|
Options: input.Options,
|
|
},
|
|
},
|
|
Output: []app.ConfigIO{
|
|
{
|
|
ID: "out",
|
|
Address: "-",
|
|
Options: []string{
|
|
"-codec",
|
|
"copy",
|
|
"-f",
|
|
"null",
|
|
},
|
|
},
|
|
},
|
|
Reconnect: true,
|
|
ReconnectDelay: 10,
|
|
Autostart: false,
|
|
StaleTimeout: 0,
|
|
}
|
|
|
|
probe := probeInput(cfg.binary, config)
|
|
|
|
for _, stream := range probe.Streams {
|
|
if stream.Type == "video" && v1data.Addresses.Streams.Video == nil {
|
|
v1data.Addresses.Streams.Video = &storeDataV1Stream{
|
|
Index: int(stream.Index),
|
|
Type: "video",
|
|
Codec: stream.Codec,
|
|
Width: int(stream.Width),
|
|
Height: int(stream.Height),
|
|
Format: stream.Format,
|
|
}
|
|
} else if stream.Type == "audio" && v1data.Addresses.Streams.Audio == nil {
|
|
v1data.Addresses.Streams.Video = &storeDataV1Stream{
|
|
Index: int(stream.Index),
|
|
Type: "audio",
|
|
Codec: stream.Codec,
|
|
Layout: stream.Layout,
|
|
Channels: int(stream.Channels),
|
|
Sampling: strconv.FormatUint(stream.Sampling, 10),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
source.Streams = append(source.Streams, restreamerUISourceStream{
|
|
URL: v1data.Addresses.Input,
|
|
Type: "video",
|
|
Index: 0,
|
|
Stream: v1data.Addresses.Streams.Video.Index,
|
|
Codec: v1data.Addresses.Streams.Video.Codec,
|
|
Width: v1data.Addresses.Streams.Video.Width,
|
|
Height: v1data.Addresses.Streams.Video.Height,
|
|
PixelFormat: v1data.Addresses.Streams.Video.Format,
|
|
})
|
|
|
|
if v1data.Addresses.Streams.Audio != nil {
|
|
sampling, _ := strconv.Atoi(v1data.Addresses.Streams.Audio.Sampling)
|
|
|
|
source.Streams = append(source.Streams, restreamerUISourceStream{
|
|
URL: v1data.Addresses.Input,
|
|
Type: "audio",
|
|
Index: 0,
|
|
Stream: v1data.Addresses.Streams.Audio.Index,
|
|
Codec: v1data.Addresses.Streams.Audio.Codec,
|
|
Layout: v1data.Addresses.Streams.Audio.Layout,
|
|
Channels: v1data.Addresses.Streams.Audio.Channels,
|
|
Sampling: sampling,
|
|
})
|
|
}
|
|
|
|
ui.Sources = append(ui.Sources, source)
|
|
}
|
|
|
|
// Audio
|
|
|
|
if v1data.Options.Audio.Codec == "auto" {
|
|
if v1data.Addresses.Streams.Audio == nil {
|
|
v1data.Options.Audio.Codec = "aac"
|
|
v1data.Options.Audio.Preset = "silence"
|
|
v1data.Options.Audio.Bitrate = "32"
|
|
v1data.Options.Audio.Channels = "stereo"
|
|
v1data.Options.Audio.Sampling = "44100"
|
|
} else {
|
|
if v1data.Addresses.Streams.Audio.Codec == "aac" {
|
|
v1data.Options.Audio.Codec = "copy"
|
|
} else {
|
|
v1data.Options.Audio.Codec = "aac"
|
|
v1data.Options.Audio.Preset = "encode"
|
|
}
|
|
|
|
if v1data.Options.Audio.Sampling != "inherit" {
|
|
v1data.Options.Audio.Sampling = v1data.Addresses.Streams.Audio.Sampling
|
|
}
|
|
|
|
if v1data.Options.Audio.Channels != "inherit" {
|
|
v1data.Options.Audio.Channels = v1data.Addresses.Streams.Audio.Layout
|
|
}
|
|
}
|
|
}
|
|
|
|
if v1data.Options.Audio.Preset == "silence" {
|
|
if v1data.Options.Audio.Codec == "aac" || v1data.Options.Audio.Codec == "mp3" {
|
|
source := restreamerUISource{
|
|
Settings: restreamerUISourceSettingsVirtualAudio{
|
|
Amplitude: 1,
|
|
BeepFactor: 4,
|
|
Color: "white",
|
|
Frequency: 440,
|
|
Layout: "stereo",
|
|
Sampling: 44100,
|
|
Source: "silence",
|
|
},
|
|
Streams: []restreamerUISourceStream{},
|
|
Type: "virtualaudio",
|
|
}
|
|
|
|
source.Inputs = append(source.Inputs, restreamerUISourceInput{
|
|
Address: "anullsrc=r=44100:cl=stereo",
|
|
Options: []string{
|
|
"-f",
|
|
"lavfi",
|
|
},
|
|
})
|
|
|
|
source.Streams = append(source.Streams, restreamerUISourceStream{
|
|
Bitrate: 0,
|
|
Channels: 2,
|
|
Codec: "pcm_u8",
|
|
Coder: "",
|
|
Duration: 0,
|
|
Format: "lavfi",
|
|
FPS: 0,
|
|
Height: 0,
|
|
Index: 0,
|
|
Language: "",
|
|
Layout: "stereo",
|
|
PixelFormat: "",
|
|
Sampling: 44100,
|
|
Stream: 0,
|
|
Type: "audio",
|
|
URL: "anullsrc=r=44100:cl=stereo",
|
|
Width: 0,
|
|
})
|
|
|
|
ui.Sources = append(ui.Sources, source)
|
|
}
|
|
}
|
|
|
|
profile := restreamerUIProfile{}
|
|
|
|
profile.Video.Source = 0
|
|
profile.Video.Stream = v1data.Addresses.Streams.Video.Index
|
|
profile.Video.Decoder = restreamerUIProfileAV{
|
|
Coder: "default",
|
|
Mapping: []string{},
|
|
Settings: restreamerUIProfileCoderSettingsCopy{},
|
|
}
|
|
profile.Video.Encoder = restreamerUIProfileAV{
|
|
Coder: "copy",
|
|
Codec: "h264",
|
|
Mapping: []string{
|
|
"-codec:v",
|
|
"copy",
|
|
"-vsync",
|
|
"0",
|
|
"-copyts",
|
|
"-start_at_zero",
|
|
},
|
|
Settings: restreamerUIProfileCoderSettingsCopy{},
|
|
}
|
|
|
|
if v1data.Options.Video.Codec != "copy" {
|
|
fps, err := strconv.ParseFloat(v1data.Options.Video.FPS, 64)
|
|
if err != nil {
|
|
fps = 25
|
|
}
|
|
|
|
r := strconv.FormatFloat(fps, 'f', 0, 64)
|
|
if math.Floor(fps) != fps {
|
|
r = strconv.FormatFloat(fps, 'f', 2, 64)
|
|
}
|
|
|
|
profile.Video.Encoder.Coder = "libx264"
|
|
profile.Video.Encoder.Codec = "h264"
|
|
profile.Video.Encoder.Mapping = []string{
|
|
"-codec:v",
|
|
"libx264",
|
|
"-preset:v", v1data.Options.Video.Preset,
|
|
"-b:v", v1data.Options.Video.Bitrate + "k",
|
|
"-maxrate:v", v1data.Options.Video.Bitrate + "k",
|
|
"-bufsize:v", v1data.Options.Video.Bitrate + "k",
|
|
"-r", r,
|
|
"-g", strconv.FormatFloat(fps*2, 'f', 0, 64),
|
|
"-pix_fmt", "yuv420p",
|
|
"-vsync", "1",
|
|
}
|
|
|
|
if v1data.Options.Video.Profile != "auto" {
|
|
profile.Video.Encoder.Mapping = append(profile.Video.Encoder.Mapping, "-profile:v", v1data.Options.Video.Profile)
|
|
}
|
|
|
|
if v1data.Options.Video.Tune != "none" {
|
|
profile.Video.Encoder.Mapping = append(profile.Video.Encoder.Mapping, "-tune:v", v1data.Options.Video.Tune)
|
|
}
|
|
|
|
profile.Video.Encoder.Settings = restreamerUIProfileCoderSettingsX264{
|
|
Bitrate: v1data.Options.Video.Bitrate,
|
|
FPS: v1data.Options.Video.FPS,
|
|
Preset: v1data.Options.Video.Preset,
|
|
Profile: v1data.Options.Video.Profile,
|
|
Tune: v1data.Options.Video.Tune,
|
|
}
|
|
}
|
|
|
|
profile.Audio.Source = -1
|
|
profile.Audio.Stream = -1
|
|
profile.Audio.Decoder = restreamerUIProfileAV{
|
|
Coder: "default",
|
|
Mapping: []string{},
|
|
Settings: restreamerUIProfileCoderSettingsCopy{},
|
|
}
|
|
profile.Audio.Encoder = restreamerUIProfileAV{
|
|
Coder: "none",
|
|
Codec: "none",
|
|
Mapping: []string{},
|
|
Settings: restreamerUIProfileCoderSettingsCopy{},
|
|
}
|
|
|
|
if v1data.Options.Audio.Codec != "none" {
|
|
if v1data.Options.Audio.Codec == "copy" {
|
|
profile.Audio.Encoder.Coder = "copy"
|
|
profile.Audio.Encoder.Codec = "copy"
|
|
profile.Audio.Encoder.Mapping = []string{
|
|
"-codec:a", "copy",
|
|
}
|
|
profile.Audio.Source = 0
|
|
profile.Audio.Stream = v1data.Addresses.Streams.Audio.Index
|
|
} else if v1data.Options.Audio.Codec == "aac" {
|
|
profile.Audio.Encoder.Coder = "aac"
|
|
profile.Audio.Encoder.Codec = "aac"
|
|
profile.Audio.Encoder.Mapping = []string{
|
|
"-codec:a", profile.Audio.Encoder.Coder,
|
|
"-b:a", v1data.Options.Audio.Bitrate + "k",
|
|
"-shortest",
|
|
"-af", "aresample=osr=" + v1data.Options.Audio.Sampling + ":ocl=" + v1data.Options.Audio.Channels,
|
|
}
|
|
|
|
if v1data.Options.Audio.Preset == "encode" {
|
|
if v1data.Addresses.Streams.Audio.Codec == "aac" {
|
|
profile.Audio.Encoder.Mapping = append(profile.Audio.Encoder.Mapping, "-bsf:a", "aac_adtstoasc")
|
|
}
|
|
profile.Audio.Source = 0
|
|
profile.Audio.Stream = v1data.Addresses.Streams.Audio.Index
|
|
} else { // silence or device
|
|
profile.Audio.Source = 1
|
|
profile.Audio.Stream = 0
|
|
}
|
|
|
|
channels := "1"
|
|
if v1data.Options.Audio.Channels == "stereo" {
|
|
channels = "2"
|
|
}
|
|
|
|
profile.Audio.Encoder.Settings = restreamerUIProfileCoderSettingsAAC{
|
|
Bitrate: v1data.Options.Audio.Bitrate,
|
|
Channels: channels,
|
|
Layout: v1data.Options.Audio.Channels,
|
|
Sampling: v1data.Options.Audio.Sampling,
|
|
}
|
|
} else if v1data.Options.Audio.Codec == "mp3" {
|
|
profile.Audio.Encoder.Coder = "libmp3lame"
|
|
profile.Audio.Encoder.Codec = "mp3"
|
|
profile.Audio.Encoder.Mapping = []string{
|
|
"-codec:a", profile.Audio.Encoder.Coder,
|
|
"-b:a", v1data.Options.Audio.Bitrate + "k",
|
|
"-shortest",
|
|
"-af", "aresample=osr=" + v1data.Options.Audio.Sampling + ":ocl=" + v1data.Options.Audio.Channels,
|
|
}
|
|
|
|
if v1data.Options.Audio.Preset == "encode" {
|
|
profile.Audio.Source = 0
|
|
profile.Audio.Stream = v1data.Addresses.Streams.Audio.Index
|
|
} else { // silence or device
|
|
profile.Audio.Source = 1
|
|
profile.Audio.Stream = 0
|
|
}
|
|
|
|
channels := "1"
|
|
if v1data.Options.Audio.Channels == "stereo" {
|
|
channels = "2"
|
|
}
|
|
|
|
profile.Audio.Encoder.Settings = restreamerUIProfileCoderSettingsAAC{
|
|
Bitrate: v1data.Options.Audio.Bitrate,
|
|
Channels: channels,
|
|
Layout: v1data.Options.Audio.Channels,
|
|
Sampling: v1data.Options.Audio.Sampling,
|
|
}
|
|
}
|
|
}
|
|
|
|
ui.Profiles = append(ui.Profiles, profile)
|
|
|
|
// Input
|
|
|
|
config.Input = append(config.Input, app.ConfigIO{
|
|
ID: "input_0",
|
|
Address: ui.Sources[0].Inputs[0].Address,
|
|
Options: ui.Sources[0].Inputs[0].Options,
|
|
})
|
|
|
|
if ui.Profiles[0].Audio.Source == 1 {
|
|
config.Input = append(config.Input, app.ConfigIO{
|
|
ID: "input_1",
|
|
Address: ui.Sources[1].Inputs[0].Address,
|
|
Options: ui.Sources[1].Inputs[0].Options,
|
|
})
|
|
}
|
|
|
|
// Output
|
|
|
|
output := app.ConfigIO{
|
|
ID: "output_0",
|
|
Address: "{memfs}/" + cfg.id + ".m3u8",
|
|
Options: []string{
|
|
"-dn",
|
|
"-sn",
|
|
"-map", strconv.Itoa(ui.Profiles[0].Video.Source) + ":" + strconv.Itoa(ui.Profiles[0].Video.Stream),
|
|
},
|
|
}
|
|
|
|
output.Options = append(output.Options, ui.Profiles[0].Video.Encoder.Mapping...)
|
|
|
|
if ui.Profiles[0].Audio.Source != -1 {
|
|
output.Options = append(output.Options, "-map", strconv.Itoa(ui.Profiles[0].Audio.Source)+":"+strconv.Itoa(ui.Profiles[0].Audio.Stream))
|
|
output.Options = append(output.Options, ui.Profiles[0].Audio.Encoder.Mapping...)
|
|
} else {
|
|
output.Options = append(output.Options, "-an")
|
|
}
|
|
|
|
output.Options = append(output.Options,
|
|
"-f", "hls",
|
|
"-start_number", "0",
|
|
"-hls_time", "2",
|
|
"-hls_list_size", "6",
|
|
"-hls_flags", "append_list+delete_segments",
|
|
"-hls_segment_filename", "{memfs}/"+cfg.id+"_%04d.ts",
|
|
"-y",
|
|
"-method", "PUT",
|
|
)
|
|
|
|
config.Output = append(config.Output, output)
|
|
|
|
process.Config = config
|
|
r.Process[process.ID] = process
|
|
|
|
r.Metadata.Process["restreamer-ui:ingest:"+cfg.id] = make(map[string]interface{})
|
|
r.Metadata.Process["restreamer-ui:ingest:"+cfg.id]["restreamer-ui"] = ui
|
|
|
|
// Snapshot
|
|
|
|
if ui.Control.Snapshot.Enable {
|
|
snapshotProcess := &app.Process{
|
|
ID: "restreamer-ui:ingest:" + cfg.id + "_snapshot",
|
|
Reference: cfg.id,
|
|
CreatedAt: time.Now().Unix(),
|
|
Order: "stop",
|
|
}
|
|
|
|
if v1data.Actions.Ingest == "start" {
|
|
process.Order = "start"
|
|
}
|
|
|
|
snapshotConfig := &app.Config{
|
|
ID: "restreamer-ui:ingest:" + cfg.id + "_snapshot",
|
|
Reference: cfg.id,
|
|
Reconnect: true,
|
|
ReconnectDelay: uint64(ui.Control.Snapshot.Interval),
|
|
Autostart: true,
|
|
StaleTimeout: 30,
|
|
Options: []string{
|
|
"-err_detect",
|
|
"ignore_err",
|
|
},
|
|
}
|
|
|
|
snapshotInput := app.ConfigIO{
|
|
ID: "input_0",
|
|
Address: "#restreamer-ui:ingest:" + cfg.id + ":output=output_0",
|
|
Options: []string{},
|
|
}
|
|
|
|
snapshotConfig.Input = append(snapshotConfig.Input, snapshotInput)
|
|
|
|
snapshotOutput := app.ConfigIO{
|
|
ID: "output_0",
|
|
Address: "{memfs}/" + cfg.id + ".jpg",
|
|
Options: []string{
|
|
"-vframes", "1",
|
|
"-f", "image2",
|
|
"-update", "1",
|
|
},
|
|
}
|
|
|
|
snapshotConfig.Output = append(snapshotConfig.Output, snapshotOutput)
|
|
|
|
snapshotProcess.Config = snapshotConfig
|
|
r.Process[snapshotProcess.ID] = snapshotProcess
|
|
|
|
r.Metadata.Process["restreamer-ui:ingest:"+cfg.id+"_snapshot"] = nil
|
|
}
|
|
|
|
// Optional publication
|
|
|
|
var outputURL *url.URL = nil
|
|
|
|
if v1data.Options.Output.Type != "rtmp" && v1data.Options.Output.Type != "hls" {
|
|
v1data.Addresses.Output = ""
|
|
}
|
|
|
|
if len(v1data.Addresses.Output) != 0 {
|
|
var err error
|
|
outputURL, err = url.Parse(v1data.Addresses.Output)
|
|
if err == nil {
|
|
if !strings.HasPrefix(outputURL.Scheme, "http") && !strings.HasPrefix(outputURL.Scheme, "https") && !strings.HasPrefix(outputURL.Scheme, "rtmp") && !strings.HasPrefix(outputURL.Scheme, "rtmps") {
|
|
outputURL = nil
|
|
}
|
|
} else {
|
|
outputURL = nil
|
|
}
|
|
}
|
|
|
|
if outputURL != nil {
|
|
egressId := "restreamer-ui:egress:rtmp:" + cfg.id
|
|
if v1data.Options.Output.Type == "hls" {
|
|
egressId = "restreamer-ui:egress:hls:" + cfg.id
|
|
}
|
|
|
|
process := &app.Process{
|
|
ID: egressId,
|
|
Reference: cfg.id,
|
|
CreatedAt: time.Now().Unix(),
|
|
Order: "stop",
|
|
}
|
|
|
|
if v1data.Actions.Egress == "start" {
|
|
process.Order = "start"
|
|
}
|
|
|
|
egress := restreamerUIEgress{
|
|
Version: 1,
|
|
}
|
|
|
|
if v1data.Options.Output.Type == "hls" {
|
|
egress.Name = "HLS"
|
|
|
|
settings := restreamerUIEgressSettingsHLS{
|
|
Address: strings.TrimPrefix(v1data.Addresses.Output, outputURL.Scheme+"://"),
|
|
Protocol: outputURL.Scheme + "://",
|
|
}
|
|
|
|
settings.Options.DeleteThreshold = "1"
|
|
settings.Options.Flags = []string{"delete_segments", "append_list"}
|
|
settings.Options.InitTime = "0"
|
|
settings.Options.ListSize = v1data.Options.Output.HLS.ListSize
|
|
settings.Options.SegmentType = "mpegts"
|
|
settings.Options.Time = v1data.Options.Output.HLS.Time
|
|
settings.Options.Method = v1data.Options.Output.HLS.Method
|
|
settings.Options.StartNumber = "0"
|
|
settings.Options.Timeout = v1data.Options.Output.HLS.Timeout
|
|
settings.Options.StartNumberSource = "generic"
|
|
settings.Options.AllowCache = "0"
|
|
settings.Options.Enc = "0"
|
|
settings.Options.IgnoreIOErrors = "0"
|
|
settings.Options.HTTPersistent = "0"
|
|
|
|
egress.Settings = settings
|
|
|
|
egress.Output.Address = v1data.Addresses.Output
|
|
egress.Output.Options = []string{
|
|
"-codec",
|
|
"copy",
|
|
"-f",
|
|
"hls",
|
|
"-hls_init_time",
|
|
settings.Options.InitTime,
|
|
"-hls_time",
|
|
settings.Options.Time,
|
|
"-hls_list_size",
|
|
settings.Options.ListSize,
|
|
"-hls_delete_threshold",
|
|
settings.Options.DeleteThreshold,
|
|
"-hls_start_number_source",
|
|
settings.Options.StartNumberSource,
|
|
"-start_number",
|
|
settings.Options.StartNumber,
|
|
"-hls_allow_cache",
|
|
settings.Options.AllowCache,
|
|
"-hls_enc",
|
|
settings.Options.Enc,
|
|
"-hls_segment_type",
|
|
settings.Options.SegmentType,
|
|
"-hls_flags",
|
|
strings.Join(settings.Options.Flags, ","),
|
|
"-method",
|
|
settings.Options.Method,
|
|
"-http_persistent",
|
|
settings.Options.HTTPersistent,
|
|
"-timeout",
|
|
settings.Options.Timeout,
|
|
"-ignore_io_errors",
|
|
settings.Options.IgnoreIOErrors,
|
|
}
|
|
|
|
} else if v1data.Options.Output.Type == "rtmp" {
|
|
egress.Name = "RTMP"
|
|
|
|
settings := restreamerUIEgressSettingsRTMP{
|
|
Address: strings.TrimPrefix(v1data.Addresses.Output, outputURL.Scheme+"://"),
|
|
Protocol: outputURL.Scheme + "://",
|
|
}
|
|
|
|
egress.Settings = settings
|
|
|
|
egress.Output.Address = v1data.Addresses.Output
|
|
egress.Output.Options = []string{
|
|
"-codec",
|
|
"copy",
|
|
"-f",
|
|
"flv",
|
|
"-rtmp_flashver",
|
|
"FMLE/3.0",
|
|
}
|
|
}
|
|
|
|
egress.Control.Process.Autostart = true
|
|
egress.Control.Process.Reconnect = true
|
|
egress.Control.Process.Delay = 30
|
|
egress.Control.Process.StaleTimeout = 30
|
|
|
|
config := &app.Config{
|
|
ID: egressId,
|
|
Reference: cfg.id,
|
|
Reconnect: egress.Control.Process.Reconnect,
|
|
ReconnectDelay: uint64(egress.Control.Process.Delay),
|
|
Autostart: egress.Control.Process.Autostart,
|
|
StaleTimeout: uint64(egress.Control.Process.StaleTimeout),
|
|
Options: []string{
|
|
"-err_detect",
|
|
"ignore_err",
|
|
},
|
|
}
|
|
|
|
config.Input = append(config.Input, app.ConfigIO{
|
|
ID: "input_0",
|
|
Address: "#restreamer-ui:ingest:" + cfg.id + ":output=output_0",
|
|
Options: []string{"-re"},
|
|
})
|
|
|
|
output := app.ConfigIO{
|
|
ID: "output_0",
|
|
Address: v1data.Addresses.Output,
|
|
Options: egress.Output.Options,
|
|
}
|
|
|
|
config.Output = append(config.Output, output)
|
|
|
|
process.Config = config
|
|
r.Process[process.ID] = process
|
|
|
|
r.Metadata.Process[egressId] = make(map[string]interface{})
|
|
r.Metadata.Process[egressId]["restreamer-ui"] = egress
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func probeInput(binary string, config app.Config) app.Probe {
|
|
ffmpeg, err := ffmpeg.New(ffmpeg.Config{
|
|
Binary: binary,
|
|
})
|
|
if err != nil {
|
|
return app.Probe{}
|
|
}
|
|
|
|
rs, err := restream.New(restream.Config{
|
|
FFmpeg: ffmpeg,
|
|
Store: store.NewDummyStore(store.DummyConfig{}),
|
|
})
|
|
if err != nil {
|
|
return app.Probe{}
|
|
}
|
|
|
|
rs.AddProcess(&config)
|
|
probe := rs.Probe(config.ID)
|
|
rs.DeleteProcess(config.ID)
|
|
|
|
return probe
|
|
}
|