Merge branch 'master' of https://github.com/gabek/owncast
This commit is contained in:
18
config.go
18
config.go
@@ -10,14 +10,15 @@ import (
|
|||||||
|
|
||||||
// Config struct
|
// Config struct
|
||||||
type Config struct {
|
type Config struct {
|
||||||
IPFS IPFS `yaml:"ipfs"`
|
IPFS IPFS `yaml:"ipfs"`
|
||||||
PublicHLSPath string `yaml:"publicHLSPath"`
|
PublicHLSPath string `yaml:"publicHLSPath"`
|
||||||
PrivateHLSPath string `yaml:"privateHLSPath"`
|
PrivateHLSPath string `yaml:"privateHLSPath"`
|
||||||
VideoSettings VideoSettings `yaml:"videoSettings"`
|
VideoSettings VideoSettings `yaml:"videoSettings"`
|
||||||
Files Files `yaml:"files"`
|
Files Files `yaml:"files"`
|
||||||
FFMpegPath string `yaml:"ffmpegPath"`
|
FFMpegPath string `yaml:"ffmpegPath"`
|
||||||
WebServerPort int `yaml:"webServerPort"`
|
WebServerPort int `yaml:"webServerPort"`
|
||||||
S3 S3 `yaml:"s3"`
|
S3 S3 `yaml:"s3"`
|
||||||
|
EnableOfflineImage bool `yaml:"enableOfflineImage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoSettings struct {
|
type VideoSettings struct {
|
||||||
@@ -26,6 +27,7 @@ type VideoSettings struct {
|
|||||||
EncoderPreset string `yaml:"encoderPreset"`
|
EncoderPreset string `yaml:"encoderPreset"`
|
||||||
StreamQualities []StreamQuality `yaml:"streamQualities"`
|
StreamQualities []StreamQuality `yaml:"streamQualities"`
|
||||||
EnablePassthrough bool `yaml:"passthrough"`
|
EnablePassthrough bool `yaml:"passthrough"`
|
||||||
|
OfflineImage string `yaml:"offlineImage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamQuality struct {
|
type StreamQuality struct {
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ publicHLSPath: webroot/hls
|
|||||||
privateHLSPath: hls
|
privateHLSPath: hls
|
||||||
ffmpegPath: /usr/local/bin/ffmpeg
|
ffmpegPath: /usr/local/bin/ffmpeg
|
||||||
webServerPort: 8080
|
webServerPort: 8080
|
||||||
|
enableOfflineImage: true
|
||||||
|
|
||||||
videoSettings:
|
videoSettings:
|
||||||
chunkLengthInSeconds: 4
|
chunkLengthInSeconds: 4
|
||||||
streamingKey: abc123
|
streamingKey: abc123
|
||||||
encoderPreset: superfast # https://trac.ffmpeg.org/wiki/Encode/H.264
|
encoderPreset: superfast # https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||||
passthrough: true # Enabling this will ignore the below stream qualities and pass through the same quality that you're sending it
|
passthrough: true # Enabling this will ignore the below stream qualities and pass through the same quality that you're sending it
|
||||||
|
offlineImage: doc/logo.png # Is displayed when a stream ends
|
||||||
|
|
||||||
streamQualities:
|
streamQualities:
|
||||||
- bitrate: 1000 # in k
|
- bitrate: 1000 # in k
|
||||||
|
|||||||
81
ffmpeg.go
81
ffmpeg.go
@@ -11,6 +11,81 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func showStreamOfflineState(configuration Config) {
|
||||||
|
fmt.Println("----- Stream offline! Showing offline state!")
|
||||||
|
|
||||||
|
var outputDir = configuration.PublicHLSPath
|
||||||
|
var variantPlaylistPath = configuration.PublicHLSPath
|
||||||
|
|
||||||
|
if configuration.IPFS.Enabled || configuration.S3.Enabled {
|
||||||
|
outputDir = configuration.PrivateHLSPath
|
||||||
|
variantPlaylistPath = configuration.PrivateHLSPath
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDir = path.Join(outputDir, "%v")
|
||||||
|
var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8")
|
||||||
|
|
||||||
|
var videoMaps = make([]string, 0)
|
||||||
|
var streamMaps = make([]string, 0)
|
||||||
|
var videoMapsString = ""
|
||||||
|
var streamMappingString = ""
|
||||||
|
if configuration.VideoSettings.EnablePassthrough || len(configuration.VideoSettings.StreamQualities) == 0 {
|
||||||
|
fmt.Println("Enabling passthrough video")
|
||||||
|
streamMaps = append(streamMaps, fmt.Sprintf("v:%d", 0))
|
||||||
|
} else {
|
||||||
|
for index, quality := range configuration.VideoSettings.StreamQualities {
|
||||||
|
maxRate := math.Floor(float64(quality.Bitrate) * 0.8)
|
||||||
|
videoMaps = append(videoMaps, fmt.Sprintf("-map v:0 -c:v:%d libx264 -b:v:%d %dk -maxrate %dk -bufsize %dk", index, index, int(quality.Bitrate), int(maxRate), int(maxRate)))
|
||||||
|
streamMaps = append(streamMaps, fmt.Sprintf("v:%d", index))
|
||||||
|
videoMapsString = strings.Join(videoMaps, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
framerate := 25
|
||||||
|
|
||||||
|
streamMappingString = "-var_stream_map \"" + strings.Join(streamMaps, " ") + "\""
|
||||||
|
|
||||||
|
ffmpegFlags := []string{
|
||||||
|
"-hide_banner",
|
||||||
|
"-stream_loop 5000",
|
||||||
|
"-i", configuration.VideoSettings.OfflineImage,
|
||||||
|
videoMapsString, // All the different video variants
|
||||||
|
"-f hls",
|
||||||
|
"-hls_list_size " + strconv.Itoa(configuration.Files.MaxNumberInPlaylist),
|
||||||
|
"-hls_time " + strconv.Itoa(configuration.VideoSettings.ChunkLengthInSeconds),
|
||||||
|
"-strftime 1",
|
||||||
|
"-use_localtime 1",
|
||||||
|
"-hls_playlist_type", "event",
|
||||||
|
"-master_pl_name", "stream.m3u8",
|
||||||
|
"-use_localtime 1",
|
||||||
|
"-hls_flags program_date_time+temp_file",
|
||||||
|
"-tune", "zerolatency",
|
||||||
|
"-framerate " + strconv.Itoa(framerate),
|
||||||
|
"-g " + strconv.Itoa(framerate*2), " -keyint_min " + strconv.Itoa(framerate*2), // multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60
|
||||||
|
"-preset " + configuration.VideoSettings.EncoderPreset,
|
||||||
|
"-sc_threshold 0", // don't create key frames on scene change - only according to -g
|
||||||
|
"-profile:v", "high", // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080
|
||||||
|
"-movflags +faststart",
|
||||||
|
"-pix_fmt yuv420p",
|
||||||
|
|
||||||
|
streamMappingString,
|
||||||
|
"-hls_segment_filename " + path.Join(outputDir, "offline-%s.ts"),
|
||||||
|
variantPlaylistName,
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpegFlagsString := strings.Join(ffmpegFlags, " ")
|
||||||
|
|
||||||
|
ffmpegCmd := configuration.FFMpegPath + " " + ffmpegFlagsString
|
||||||
|
|
||||||
|
fmt.Println(ffmpegCmd)
|
||||||
|
|
||||||
|
_, err := exec.Command("sh", "-c", ffmpegCmd).Output()
|
||||||
|
fmt.Println(err)
|
||||||
|
verifyError(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func startFfmpeg(configuration Config) {
|
func startFfmpeg(configuration Config) {
|
||||||
var outputDir = configuration.PublicHLSPath
|
var outputDir = configuration.PublicHLSPath
|
||||||
var variantPlaylistPath = configuration.PublicHLSPath
|
var variantPlaylistPath = configuration.PublicHLSPath
|
||||||
@@ -21,13 +96,7 @@ func startFfmpeg(configuration Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
outputDir = path.Join(outputDir, "%v")
|
outputDir = path.Join(outputDir, "%v")
|
||||||
|
|
||||||
// var masterPlaylistName = path.Join(configuration.PublicHLSPath, "%v", "stream.m3u8")
|
|
||||||
var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8")
|
var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8")
|
||||||
// var variantRootPath = configuration.PublicHLSPath
|
|
||||||
|
|
||||||
// variantRootPath = path.Join(variantRootPath, "%v")
|
|
||||||
// variantPlaylistName := path.Join("%v", "stream.m3u8")
|
|
||||||
|
|
||||||
log.Printf("Starting transcoder saving to /%s.", variantPlaylistName)
|
log.Printf("Starting transcoder saving to /%s.", variantPlaylistName)
|
||||||
pipePath := getTempPipePath()
|
pipePath := getTempPipePath()
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -95,6 +95,9 @@ func streamConnected() {
|
|||||||
|
|
||||||
func streamDisconnected() {
|
func streamDisconnected() {
|
||||||
stats.StreamDisconnected()
|
stats.StreamDisconnected()
|
||||||
|
if configuration.EnableOfflineImage {
|
||||||
|
showStreamOfflineState(configuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewerAdded(clientID string) {
|
func viewerAdded(clientID string) {
|
||||||
|
|||||||
16
stats.go
16
stats.go
@@ -23,8 +23,8 @@ type Stats struct {
|
|||||||
SessionMaxViewerCount int `json:"sessionMaxViewerCount"`
|
SessionMaxViewerCount int `json:"sessionMaxViewerCount"`
|
||||||
OverallMaxViewerCount int `json:"overallMaxViewerCount"`
|
OverallMaxViewerCount int `json:"overallMaxViewerCount"`
|
||||||
LastDisconnectTime time.Time `json:"lastDisconnectTime"`
|
LastDisconnectTime time.Time `json:"lastDisconnectTime"`
|
||||||
|
lastConnectTime time.Time `json:"-"`
|
||||||
clients map[string]time.Time
|
clients map[string]time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stats) Setup() {
|
func (s *Stats) Setup() {
|
||||||
@@ -62,6 +62,17 @@ func (s *Stats) purgeStaleViewers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stats) IsStreamConnected() bool {
|
func (s *Stats) IsStreamConnected() bool {
|
||||||
|
if !s.streamConnected {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind of a hack. It takes a handful of seconds between a RTMP connection and when HLS data is available.
|
||||||
|
// So account for that with an artificial buffer.
|
||||||
|
timeSinceLastConnected := time.Since(s.lastConnectTime).Seconds()
|
||||||
|
if timeSinceLastConnected < 10 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return s.streamConnected
|
return s.streamConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +107,7 @@ func (s *Stats) ViewerDisconnected(clientID string) {
|
|||||||
|
|
||||||
func (s *Stats) StreamConnected() {
|
func (s *Stats) StreamConnected() {
|
||||||
s.streamConnected = true
|
s.streamConnected = true
|
||||||
|
s.lastConnectTime = time.Now()
|
||||||
|
|
||||||
timeSinceDisconnect := time.Since(s.LastDisconnectTime).Minutes()
|
timeSinceDisconnect := time.Since(s.LastDisconnectTime).Minutes()
|
||||||
if timeSinceDisconnect > 15 {
|
if timeSinceDisconnect > 15 {
|
||||||
|
|||||||
@@ -38,11 +38,25 @@ function setupApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getStatus() {
|
async function getStatus() {
|
||||||
let url = "https://goth.land/status";
|
let url = "/status";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const status = await response.json(); // read response body and parse as JSON
|
const status = await response.json(); // read response body and parse as JSON
|
||||||
|
|
||||||
|
if (!app.isOnline && status.online) {
|
||||||
|
// The stream was offline, but now it's online. Force start of playback after an arbitrary
|
||||||
|
// delay to make sure the stream has actual data ready to go.
|
||||||
|
setTimeout(function () {
|
||||||
|
var player = videojs('video');
|
||||||
|
player.pause()
|
||||||
|
player.src(player.src()); // Reload the same video
|
||||||
|
player.load();
|
||||||
|
player.play();
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
app.streamStatus = status.online
|
app.streamStatus = status.online
|
||||||
? "Stream is online."
|
? "Stream is online."
|
||||||
: "Stream is offline."
|
: "Stream is offline."
|
||||||
@@ -50,6 +64,7 @@ async function getStatus() {
|
|||||||
app.viewerCount = status.viewerCount;
|
app.viewerCount = status.viewerCount;
|
||||||
app.sessionMaxViewerCount = status.sessionMaxViewerCount;
|
app.sessionMaxViewerCount = status.sessionMaxViewerCount;
|
||||||
app.overallMaxViewerCount = status.overallMaxViewerCount;
|
app.overallMaxViewerCount = status.overallMaxViewerCount;
|
||||||
|
app.isOnline = status.online;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
app.streamStatus = "Stream server is offline."
|
app.streamStatus = "Stream server is offline."
|
||||||
|
|||||||
Reference in New Issue
Block a user