From 5bc3555206caf12eda2612773792b93fbceac050 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Mon, 15 Jun 2020 16:27:58 -0700 Subject: [PATCH] Stitch in an offline slate when the RTMP stream is dropped --- config.go | 18 +++++---- config/config-example.yaml | 2 + ffmpeg.go | 83 +++++++++++++++++++++++++++++++++++--- main.go | 3 ++ 4 files changed, 92 insertions(+), 14 deletions(-) diff --git a/config.go b/config.go index 0f0369900..97c917c5a 100644 --- a/config.go +++ b/config.go @@ -10,14 +10,15 @@ import ( // Config struct type Config struct { - IPFS IPFS `yaml:"ipfs"` - PublicHLSPath string `yaml:"publicHLSPath"` - PrivateHLSPath string `yaml:"privateHLSPath"` - VideoSettings VideoSettings `yaml:"videoSettings"` - Files Files `yaml:"files"` - FFMpegPath string `yaml:"ffmpegPath"` - WebServerPort int `yaml:"webServerPort"` - S3 S3 `yaml:"s3"` + IPFS IPFS `yaml:"ipfs"` + PublicHLSPath string `yaml:"publicHLSPath"` + PrivateHLSPath string `yaml:"privateHLSPath"` + VideoSettings VideoSettings `yaml:"videoSettings"` + Files Files `yaml:"files"` + FFMpegPath string `yaml:"ffmpegPath"` + WebServerPort int `yaml:"webServerPort"` + S3 S3 `yaml:"s3"` + EnableOfflineImage bool `yaml:"enableOfflineImage"` } type VideoSettings struct { @@ -26,6 +27,7 @@ type VideoSettings struct { EncoderPreset string `yaml:"encoderPreset"` StreamQualities []StreamQuality `yaml:"streamQualities"` EnablePassthrough bool `yaml:"passthrough"` + OfflineImage string `yaml:"offlineImage"` } type StreamQuality struct { diff --git a/config/config-example.yaml b/config/config-example.yaml index 76849c3e6..e88d992f5 100644 --- a/config/config-example.yaml +++ b/config/config-example.yaml @@ -2,12 +2,14 @@ publicHLSPath: webroot/hls privateHLSPath: hls ffmpegPath: /usr/local/bin/ffmpeg webServerPort: 8080 +enableOfflineImage: true videoSettings: chunkLengthInSeconds: 4 streamingKey: abc123 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 + offlineImage: doc/logo.png # Is displayed when a stream ends streamQualities: - bitrate: 1000 # in k diff --git a/ffmpeg.go b/ffmpeg.go index 72f0ce3c5..033efab42 100644 --- a/ffmpeg.go +++ b/ffmpeg.go @@ -11,6 +11,83 @@ import ( "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, " ") + "\"" + + // ffmpeg -hide_banner -stream_loop 500 -i doc/logo.png -c:v libx264 -map v:0 -c:v:0 libx264 -b:v:0 1500k -f hls -master_pl_name stream.m3u8 -use_localtime 1 -hls_flags program_date_time+temp_file+append_list -tune zerolatency -var_stream_map "v:0" -hls_segment_filename hls/%v/stream-%Y%m%d-%s.ts hls/%v/stream.m3u8 + + 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-%Y%m%d-%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) { var outputDir = configuration.PublicHLSPath var variantPlaylistPath = configuration.PublicHLSPath @@ -21,13 +98,7 @@ func startFfmpeg(configuration Config) { } outputDir = path.Join(outputDir, "%v") - - // var masterPlaylistName = path.Join(configuration.PublicHLSPath, "%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) pipePath := getTempPipePath() diff --git a/main.go b/main.go index 0896f7187..975671a0c 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,9 @@ func streamConnected() { func streamDisconnected() { stats.StreamDisconnected() + if configuration.EnableOfflineImage { + showStreamOfflineState(configuration) + } } func viewerAdded(clientID string) {