diff --git a/config-example.yaml b/config-example.yaml index e88d992f5..1e8a3e8bb 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -1,18 +1,25 @@ publicHLSPath: webroot/hls privateHLSPath: hls -ffmpegPath: /usr/local/bin/ffmpeg +ffmpegPath: /usr/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 + offlineContent: static/offline.m4v # Is displayed when a stream ends streamQualities: - - bitrate: 1000 # in k + # Pass through the exact video and audio that you're streaming. + - full: + videoPassthrough: true + audioPassthrough: true + + # Transcode the video to a lower bitrate and resize + # - low: + # videoBitrate: 700 + # scaledWidth: 600 + # audioPassthrough: true + # encoderPreset: superfast files: maxNumberInPlaylist: 30 @@ -27,4 +34,4 @@ s3: accessKey: ABC12342069 secret: lolomgqwtf49583949 region: us-west-2 - bucket: myvideo \ No newline at end of file + bucket: myvideo diff --git a/config/config.go b/config/config.go index 259b8d277..741953753 100644 --- a/config/config.go +++ b/config/config.go @@ -15,28 +15,41 @@ import ( var Config *config 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"` - EnableOfflineImage bool `yaml:"enableOfflineImage"` + 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"` } type videoSettings struct { ChunkLengthInSeconds int `yaml:"chunkLengthInSeconds"` StreamingKey string `yaml:"streamingKey"` - EncoderPreset string `yaml:"encoderPreset"` - StreamQualities []streamQuality `yaml:"streamQualities"` + StreamQualities []StreamQuality `yaml:"streamQualities"` + OfflineContent string `yaml:"offlineContent"` EnablePassthrough bool `yaml:"passthrough"` - OfflineImage string `yaml:"offlineImage"` } -type streamQuality struct { - Bitrate int `yaml:"bitrate"` +type StreamQuality struct { + // Enable passthrough to copy the video and/or audio directly from the + // incoming stream and disable any transcoding. It will ignore any of + // the below settings. + IsVideoPassthrough bool `yaml:"videoPassthrough"` + IsAudioPassthrough bool `yaml:"audioPassthrough"` + + VideoBitrate int `yaml:"videoBitrate"` + AudioBitrate int `yaml:"audioBitrate"` + + // Set only one of these in order to keep your current aspect ratio. + // Or set neither to not scale the video. + ScaledWidth int `yaml:"scaledWidth"` + ScaledHeight int `yaml:"scaledHeight"` + + Framerate int `yaml:"framerate"` + EncoderPreset string `yaml:"encoderPreset"` } type files struct { @@ -108,10 +121,6 @@ func (c *config) verifySettings() error { return fmt.Errorf("ffmpeg does not exist at: %s", c.FFMpegPath) } - if c.VideoSettings.EncoderPreset == "" { - return errors.New("a video encoder preset is required to be set in the config file") - } - return nil } diff --git a/core/core.go b/core/core.go index f44f0a937..80503b2ef 100644 --- a/core/core.go +++ b/core/core.go @@ -51,7 +51,9 @@ func createInitialOfflineState() error { } } - return ffmpeg.ShowStreamOfflineState() + ffmpeg.ShowStreamOfflineState() + + return nil } func resetDirectories() { diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index 477586977..5f8d6d0a3 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -1,200 +1,14 @@ package ffmpeg import ( - "fmt" - "math" - "os" - "os/exec" - "path" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" - "github.com/gabek/owncast/config" - "github.com/gabek/owncast/utils" ) //ShowStreamOfflineState generates and shows the stream's offline state -func ShowStreamOfflineState() error { - log.Println("----- Stream offline! Showing offline state!") - - var outputDir = config.Config.PublicHLSPath - var variantPlaylistPath = config.Config.PublicHLSPath - - if config.Config.IPFS.Enabled || config.Config.S3.Enabled { - outputDir = config.Config.PrivateHLSPath - variantPlaylistPath = config.Config.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 config.Config.VideoSettings.EnablePassthrough || len(config.Config.VideoSettings.StreamQualities) == 0 { - log.Println("Enabling passthrough video for offline state") - videoMapsString = "-b:v 1200k -b:a 128k" // Since we're compositing multiple sources we can't infer bitrate, so pick something reasonable. - streamMaps = append(streamMaps, fmt.Sprintf("v:%d", 0)) - } else { - for index, quality := range config.Config.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 100", - // "-fflags", "+genpts", - "-i", config.Config.VideoSettings.OfflineImage, - "-i", "webroot/thumbnail.jpg", - "-filter_complex", "\"[0:v]scale=2640:2360[bg];[bg][1:v]overlay=200:250:enable='between(t,0,3)'\"", - videoMapsString, // All the different video variants - "-f hls", - // "-hls_list_size " + strconv.Itoa(config.Config.Files.MaxNumberInPlaylist), - "-hls_time 4", // + strconv.Itoa(config.Config.VideoSettings.ChunkLengthInSeconds), - "-hls_playlist_type", "event", - "-master_pl_name", "stream.m3u8", - "-strftime 1", - "-use_localtime 1", - "-hls_flags temp_file", - "-tune", "zerolatency", - "-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 - "-framerate " + strconv.Itoa(framerate), - "-preset " + config.Config.VideoSettings.EncoderPreset, - "-sc_threshold 0", // don't create key frames on scene change - only according to -g - "-profile:v", "main", // 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"), - // "-s", "720x480", // size - variantPlaylistName, - } - - ffmpegFlagsString := strings.Join(ffmpegFlags, " ") - - ffmpegCmd := config.Config.FFMpegPath + " " + ffmpegFlagsString - - // log.Println(ffmpegCmd) - - _, err := exec.Command("sh", "-c", ffmpegCmd).Output() - - return err -} - -//Start starts the ffmpeg process -func Start() error { - var outputDir = config.Config.PublicHLSPath - var variantPlaylistPath = config.Config.PublicHLSPath - - if config.Config.IPFS.Enabled || config.Config.S3.Enabled { - outputDir = config.Config.PrivateHLSPath - variantPlaylistPath = config.Config.PrivateHLSPath - } - - outputDir = path.Join(outputDir, "%v") - var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8") - - log.Printf("Starting transcoder saving to /%s.", variantPlaylistName) - pipePath := utils.GetTemporaryPipePath() - - var videoMaps = make([]string, 0) - var streamMaps = make([]string, 0) - var audioMaps = make([]string, 0) - var videoMapsString = "" - var audioMapsString = "" - var streamMappingString = "" - var profileString = "" - - if config.Config.VideoSettings.EnablePassthrough || len(config.Config.VideoSettings.StreamQualities) == 0 { - log.Println("Enabling passthrough video for stream") - streamMaps = append(streamMaps, fmt.Sprintf("v:%d,a:%d", 0, 0)) - videoMaps = append(videoMaps, "-map v:0 -c:v copy") - videoMapsString = strings.Join(videoMaps, " ") - audioMaps = append(audioMaps, "-map a:0") - audioMapsString = strings.Join(audioMaps, " ") + " -c:a copy" // Pass through audio for all the variants, don't reencode - - } else { - for index, quality := range config.Config.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,a:%d", index, index)) - videoMapsString = strings.Join(videoMaps, " ") - audioMaps = append(audioMaps, "-map a:0") - audioMapsString = strings.Join(audioMaps, " ") + " -c:a copy" // Pass through audio for all the variants, don't reencode - profileString = "-profile:v high" // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 - } - } - - framerate := 25 - - streamMappingString = "-var_stream_map \"" + strings.Join(streamMaps, " ") + "\"" - ffmpegFlags := []string{ - "-hide_banner", - // "-re", - "-fflags", "+genpts", - "-i pipe:", - // "-vf scale=900:-2", // Re-enable in the future with a config to togging resizing? - // "-sws_flags fast_bilinear", - videoMapsString, // All the different video variants - audioMapsString, - "-master_pl_name stream.m3u8", - "-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 - // "-r 25", - "-preset " + config.Config.VideoSettings.EncoderPreset, - "-sc_threshold 0", // don't create key frames on scene change - only according to -g - profileString, - "-movflags +faststart", - "-pix_fmt yuv420p", - "-f hls", - "-hls_list_size " + strconv.Itoa(config.Config.Files.MaxNumberInPlaylist), - "-hls_delete_threshold 10", // Keep 10 unreferenced segments on disk before they're deleted. - "-hls_time " + strconv.Itoa(config.Config.VideoSettings.ChunkLengthInSeconds), - "-strftime 1", - "-use_localtime 1", - "-hls_segment_filename " + path.Join(outputDir, "stream-%Y%m%d-%s.ts"), - "-hls_flags delete_segments+program_date_time+temp_file", - "-tune zerolatency", - // "-s", "720x480", // size - - streamMappingString, - variantPlaylistName, - } - - ffmpegFlagsString := strings.Join(ffmpegFlags, " ") - - ffmpegCmd := "cat " + pipePath + " | " + config.Config.FFMpegPath + " " + ffmpegFlagsString - - // fmt.Println(ffmpegCmd) - - _, err := exec.Command("sh", "-c", ffmpegCmd).Output() - - return err -} - -//WritePlaylist writes the playlist to disk -func WritePlaylist(data string, filePath string) error { - f, err := os.Create(filePath) - if err != nil { - return err - } - defer f.Close() - - if _, err := f.WriteString(data); err != nil { - return err - } - - return nil +func ShowStreamOfflineState() { + transcoder := NewTranscoder() + transcoder.SetSegmentLength(10) + transcoder.SetAppendToStream(true) + transcoder.SetInput(config.Config.VideoSettings.OfflineContent) + transcoder.Start() } diff --git a/core/ffmpeg/transcoder.go b/core/ffmpeg/transcoder.go new file mode 100644 index 000000000..9ab5c2a45 --- /dev/null +++ b/core/ffmpeg/transcoder.go @@ -0,0 +1,315 @@ +package ffmpeg + +import ( + "fmt" + "os/exec" + "path" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/gabek/owncast/config" + "github.com/gabek/owncast/utils" +) + +// Transcoder is a single instance of a video transcoder +type Transcoder struct { + input string + segmentOutputPath string + playlistOutputPath string + variants []HLSVariant + hlsPlaylistLength int + segmentLengthSeconds int + appendToStream bool +} + +// HLSVariant is a combination of settings that results in a single HLS stream +type HLSVariant struct { + index int + + videoSize VideoSize // Resizes the video via scaling + framerate int // The output framerate + videoBitrate string // The output bitrate + isVideoPassthrough bool // Override all settings and just copy the video stream + + audioBitrate string // The audio bitrate + isAudioPassthrough bool // Override all settings and just copy the audio stream + + encoderPreset string // A collection of automatic settings for the encoder. https://trac.ffmpeg.org/wiki/Encode/H.264#crf +} + +// VideoSize is the scaled size of the video output +type VideoSize struct { + Width int + Height int +} + +// getString returns a WxH formatted getString for scaling video output +func (v *VideoSize) getString() string { + widthString := strconv.Itoa(v.Width) + heightString := strconv.Itoa(v.Height) + + if widthString != "0" && heightString != "0" { + return widthString + ":" + heightString + } else if widthString != "0" { + return widthString + ":-2" + } else if heightString != "0" { + return "-2:" + heightString + } + + return "" +} + +// Start will execute the transcoding process with the settings previously set. +func (t *Transcoder) Start() { + command := t.getString() + + log.Printf("Video transcoder started with %d stream variants.", len(t.variants)) + + _, err := exec.Command("sh", "-c", command).Output() + if err != nil { + log.Panicln(err, command) + } + + return +} + +func (t *Transcoder) getString() string { + hlsOptionFlags := []string{ + "delete_segments", + "program_date_time", + "temp_file", + } + + if t.appendToStream { + hlsOptionFlags = append(hlsOptionFlags, "append_list") + } + + ffmpegFlags := []string{ + "cat", t.input, "|", + config.Config.FFMpegPath, + "-hide_banner", + "-i pipe:", + t.getVariantsString(), + + // HLS Output + "-f", "hls", + "-hls_time", strconv.Itoa(t.segmentLengthSeconds), // Length of each segment + "-hls_list_size", strconv.Itoa(config.Config.Files.MaxNumberInPlaylist), // Max # in variant playlist + "-hls_delete_threshold", "10", // Start deleting files after hls_list_size + 10 + "-hls_flags", strings.Join(hlsOptionFlags, "+"), // Specific options in HLS generation + + // Video settings + "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment) + // "-profile:v", "high", // Main – for standard definition (SD) to 640×480, High – for high definition (HD) to 1920×1080 + "-sc_threshold", "0", // Disable scene change detection for creating segments + + // Filenames + "-master_pl_name", "stream.m3u8", + "-strftime 1", // Support the use of strftime in filenames + "-hls_segment_filename", path.Join(t.segmentOutputPath, "/%v/stream-%s.ts"), // Each segment's filename + "-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0 + path.Join(t.segmentOutputPath, "/%v/stream.m3u8"), // Each variant's playlist + } + + return strings.Join(ffmpegFlags, " ") +} + +func getVariantFromConfigQuality(quality config.StreamQuality, index int) HLSVariant { + variant := HLSVariant{} + variant.index = index + variant.isAudioPassthrough = quality.IsAudioPassthrough + variant.isVideoPassthrough = quality.IsVideoPassthrough + + // If no audio bitrate is specified then we pass through original audio + if quality.AudioBitrate == 0 { + variant.isAudioPassthrough = true + } + + if quality.VideoBitrate == 0 { + variant.isVideoPassthrough = true + } + + // If the video is being passed through then + // don't continue to set options on the variant. + if variant.isVideoPassthrough { + return variant + } + + // Set a default, reasonable preset if one is not provided. + // "superfast" and "ultrafast" are generally not recommended since they look bad. + // https://trac.ffmpeg.org/wiki/Encode/H.264 + if quality.EncoderPreset != "" { + variant.encoderPreset = quality.EncoderPreset + } else { + variant.encoderPreset = "veryfast" + } + + variant.SetVideoBitrate(strconv.Itoa(quality.VideoBitrate) + "k") + variant.SetAudioBitrate(strconv.Itoa(quality.AudioBitrate) + "k") + variant.SetVideoScalingWidth(quality.ScaledWidth) + variant.SetVideoScalingHeight(quality.ScaledHeight) + variant.SetVideoFramerate(quality.Framerate) + + return variant +} + +// NewTranscoder will return a new Transcoder, populated by the config +func NewTranscoder() Transcoder { + transcoder := new(Transcoder) + + var outputPath string + if config.Config.S3.Enabled || config.Config.IPFS.Enabled { + // Segments are not available via the local HTTP server + outputPath = config.Config.PrivateHLSPath + } else { + // Segments are available via the local HTTP server + outputPath = config.Config.PublicHLSPath + } + + transcoder.segmentOutputPath = outputPath + // Playlists are available via the local HTTP server + transcoder.playlistOutputPath = config.Config.PublicHLSPath + + transcoder.input = utils.GetTemporaryPipePath() + transcoder.segmentLengthSeconds = config.Config.VideoSettings.ChunkLengthInSeconds + + for index, quality := range config.Config.VideoSettings.StreamQualities { + variant := getVariantFromConfigQuality(quality, index) + transcoder.AddVariant(variant) + } + + return *transcoder +} + +// Uses `map` https://www.ffmpeg.org/ffmpeg-all.html#Stream-specifiers-1 https://www.ffmpeg.org/ffmpeg-all.html#Advanced-options +func (v *HLSVariant) getVariantString() string { + variantEncoderCommands := []string{ + v.getVideoQualityString(), + v.getAudioQualityString(), + } + + if v.videoSize.Width != 0 || v.videoSize.Height != 0 { + variantEncoderCommands = append(variantEncoderCommands, v.getScalingString()) + } + + if v.framerate != 0 { + variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-r %d", v.framerate)) + // multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60 + variantEncoderCommands = append(variantEncoderCommands, "-g "+strconv.Itoa(v.framerate*2)) + variantEncoderCommands = append(variantEncoderCommands, "-keyint_min "+strconv.Itoa(v.framerate*2)) + } + + if v.encoderPreset != "" { + variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", v.encoderPreset)) + } + + return strings.Join(variantEncoderCommands, " ") +} + +// Get the command flags for the variants +func (t *Transcoder) getVariantsString() string { + var variantsCommandFlags = "" + var variantsStreamMaps = " -var_stream_map \"" + + for _, variant := range t.variants { + variantsCommandFlags = variantsCommandFlags + " " + variant.getVariantString() + variantsStreamMaps = variantsStreamMaps + fmt.Sprintf("v:%d,a:%d ", variant.index, variant.index) + } + variantsCommandFlags = variantsCommandFlags + " " + variantsStreamMaps + "\"" + + return variantsCommandFlags +} + +// Video Scaling +// https://trac.ffmpeg.org/wiki/Scaling +// If we'd like to keep the aspect ratio, we need to specify only one component, either width or height. +// Some codecs require the size of width and height to be a multiple of n. You can achieve this by setting the width or height to -n. + +// SetVideoScalingWidth will set the scaled video width of this variant +func (v *HLSVariant) SetVideoScalingWidth(width int) { + v.videoSize.Width = width +} + +// SetVideoScalingHeight will set the scaled video height of this variant +func (v *HLSVariant) SetVideoScalingHeight(height int) { + v.videoSize.Height = height +} + +func (v *HLSVariant) getScalingString() string { + scalingAlgorithm := "bilinear" + return fmt.Sprintf("-filter:v:%d \"scale=%s\" -sws_flags %s", v.index, v.videoSize.getString(), scalingAlgorithm) +} + +// Video Quality + +// SetVideoBitrate will set the output bitrate of this variant's video +func (v *HLSVariant) SetVideoBitrate(bitrate string) { + v.videoBitrate = bitrate +} + +func (v *HLSVariant) getVideoQualityString() string { + if v.isVideoPassthrough { + return fmt.Sprintf("-map v:0 -c:v:%d copy", v.index) + } + + encoderCodec := "libx264" + return fmt.Sprintf("-map v:0 -c:v:%d %s -b:v:%d %s", v.index, encoderCodec, v.index, v.videoBitrate) +} + +// SetVideoFramerate will set the output framerate of this variant's video +func (v *HLSVariant) SetVideoFramerate(framerate int) { + v.framerate = framerate +} + +// SetEncoderPreset will set the video encoder preset of this variant +func (v *HLSVariant) SetEncoderPreset(preset string) { + v.encoderPreset = preset +} + +// Audio Quality + +// SetAudioBitrate will set the output framerate of this variant's audio +func (v *HLSVariant) SetAudioBitrate(bitrate string) { + v.audioBitrate = bitrate +} + +func (v *HLSVariant) getAudioQualityString() string { + if v.isAudioPassthrough { + return fmt.Sprintf("-map a:0 -c:a:%d copy", v.index) + } + + encoderCodec := "libfdk_aac" + return fmt.Sprintf("-map a:0 -c:a:%d %s -profile:a aac_he -b:a:%d %s", v.index, encoderCodec, v.index, v.audioBitrate) +} + +// AddVariant adds a new HLS variant to include in the output +func (t *Transcoder) AddVariant(variant HLSVariant) { + t.variants = append(t.variants, variant) +} + +// SetInput sets the input stream on the filesystem +func (t *Transcoder) SetInput(input string) { + t.input = input +} + +// SetOutputPath sets the root directory that should include playlists and video segments +func (t *Transcoder) SetOutputPath(output string) { + t.segmentOutputPath = output +} + +// SetHLSPlaylistLength will set the max number of items in a HLS variant's playlist +func (t *Transcoder) SetHLSPlaylistLength(length int) { + t.hlsPlaylistLength = length +} + +// SetSegmentLength Specifies the number of seconds each segment should be +func (t *Transcoder) SetSegmentLength(seconds int) { + t.segmentLengthSeconds = seconds +} + +// SetAppendToStream enables appending to the HLS stream instead of overwriting +func (t *Transcoder) SetAppendToStream(append bool) { + t.appendToStream = append +} diff --git a/core/playlist/monitor.go b/core/playlist/monitor.go index dfe847e93..4b3419278 100644 --- a/core/playlist/monitor.go +++ b/core/playlist/monitor.go @@ -13,7 +13,6 @@ import ( "github.com/radovskyb/watcher" "github.com/gabek/owncast/config" - "github.com/gabek/owncast/core/ffmpeg" "github.com/gabek/owncast/models" "github.com/gabek/owncast/utils" ) @@ -159,5 +158,5 @@ func updateVariantPlaylist(fullPath string) error { playlistString = _storage.GenerateRemotePlaylist(playlistString, variant) - return ffmpeg.WritePlaylist(playlistString, path.Join(config.Config.PublicHLSPath, relativePath)) + return WritePlaylist(playlistString, path.Join(config.Config.PublicHLSPath, relativePath)) } diff --git a/core/playlist/writer.go b/core/playlist/writer.go new file mode 100644 index 000000000..290254075 --- /dev/null +++ b/core/playlist/writer.go @@ -0,0 +1,18 @@ +package playlist + +import "os" + +//WritePlaylist writes the playlist to disk +func WritePlaylist(data string, filePath string) error { + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + + if _, err := f.WriteString(data); err != nil { + return err + } + + return nil +} diff --git a/core/rtmp/handler.go b/core/rtmp/handler.go index 4230e4d9e..450a70133 100644 --- a/core/rtmp/handler.go +++ b/core/rtmp/handler.go @@ -76,8 +76,8 @@ func (h *Handler) OnPublish(timestamp uint32, cmd *rtmpmsg.NetStreamPublish) err } h.flvEnc = enc - //TODO: why is this turned into a goroutine? - go ffmpeg.Start() + transcoder := ffmpeg.NewTranscoder() + go transcoder.Start() _isConnected = true core.SetStreamAsConnected() diff --git a/core/status.go b/core/status.go index 9d0521e76..8f7a34a50 100644 --- a/core/status.go +++ b/core/status.go @@ -47,7 +47,5 @@ func SetStreamAsDisconnected() { _stats.StreamConnected = false _stats.LastDisconnectTime = time.Now() - if config.Config.EnableOfflineImage { - ffmpeg.ShowStreamOfflineState() - } + ffmpeg.ShowStreamOfflineState() } diff --git a/scripts/build.sh b/scripts/build.sh index 674a79eff..91a68b742 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -39,15 +39,14 @@ build() { mkdir -p dist/${NAME} mkdir -p dist/${NAME}/webroot/static - mkdir -p dist/${NAME}/static # Default files cp config-example.yaml dist/${NAME}/config.yaml cp webroot/static/content-example.md dist/${NAME}/webroot/static/content.md - cp webroot/img/logo.png dist/${NAME}/static/logo.png cp -R webroot/ dist/${NAME}/webroot/ cp -R doc/ dist/${NAME}/doc/ + cp -R static/ dist/${NAME}/static cp README.md dist/${NAME} env CGO_ENABLED=0 GOOS=$OS GOARCH=$ARCH go build -ldflags "-X main.GitCommit=$GIT_COMMIT -X main.BuildVersion=$VERSION -X main.BuildType=$NAME" -a -o dist/$NAME/owncast diff --git a/static/offline.m4v b/static/offline.m4v index 0f8ce537a..3c733394b 100644 Binary files a/static/offline.m4v and b/static/offline.m4v differ diff --git a/webroot/js/player/player.js b/webroot/js/player/player.js index 43cf528c1..ec691abc0 100644 --- a/webroot/js/player/player.js +++ b/webroot/js/player/player.js @@ -1,5 +1,5 @@ -// const streamURL = '/hls/stream.m3u8'; -const streamURL = 'https://goth.land/hls/stream.m3u8'; // Uncomment me to point to remote video +const streamURL = '/hls/stream.m3u8'; +// const streamURL = 'https://goth.land/hls/stream.m3u8'; // Uncomment me to point to remote video // style hackings window.VIDEOJS_NO_DYNAMIC_STYLE = true;