diff --git a/config/config.go b/config/config.go index 3451e2860..2ed4abe66 100644 --- a/config/config.go +++ b/config/config.go @@ -197,6 +197,15 @@ func (c *config) GetVideoStreamQualities() []StreamQuality { return _default.VideoSettings.StreamQualities } +// GetFramerate returns the framerate or default +func (q *StreamQuality) GetFramerate() int { + if q.Framerate > 0 { + return q.Framerate + } + + return _default.VideoSettings.StreamQualities[0].Framerate +} + //Load tries to load the configuration file func Load(filePath string, versionInfo string) error { Config = new(config) diff --git a/config/defaults.go b/config/defaults.go index ded829273..0c89e5353 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -21,6 +21,7 @@ func getDefaults() config { IsAudioPassthrough: true, VideoBitrate: 1200, EncoderPreset: "veryfast", + Framerate: 24, } defaults.VideoSettings.StreamQualities = []StreamQuality{defaultQuality} diff --git a/core/ffmpeg/transcoder.go b/core/ffmpeg/transcoder.go index 68d83a39f..8172376ba 100644 --- a/core/ffmpeg/transcoder.go +++ b/core/ffmpeg/transcoder.go @@ -33,7 +33,7 @@ type HLSVariant struct { videoSize VideoSize // Resizes the video via scaling framerate int // The output framerate - videoBitrate string // The output bitrate + videoBitrate int // The output bitrate isVideoPassthrough bool // Override all settings and just copy the video stream audioBitrate string // The audio bitrate @@ -164,11 +164,11 @@ func getVariantFromConfigQuality(quality config.StreamQuality, index int) HLSVar variant.encoderPreset = "veryfast" } - variant.SetVideoBitrate(strconv.Itoa(quality.VideoBitrate) + "k") + variant.SetVideoBitrate(quality.VideoBitrate) variant.SetAudioBitrate(strconv.Itoa(quality.AudioBitrate) + "k") variant.SetVideoScalingWidth(quality.ScaledWidth) variant.SetVideoScalingHeight(quality.ScaledHeight) - variant.SetVideoFramerate(quality.Framerate) + variant.SetVideoFramerate(quality.GetFramerate()) return variant } @@ -196,13 +196,6 @@ func NewTranscoder() Transcoder { transcoder.segmentLengthSeconds = config.Config.GetVideoSegmentSecondsLength() qualities := config.Config.VideoSettings.StreamQualities - if len(qualities) == 0 { - defaultQuality := config.StreamQuality{} - defaultQuality.VideoBitrate = 1000 - defaultQuality.EncoderPreset = "superfast" - qualities = append(qualities, defaultQuality) - } - for index, quality := range qualities { variant := getVariantFromConfigQuality(quality, index) transcoder.AddVariant(variant) @@ -212,9 +205,9 @@ func NewTranscoder() 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 { +func (v *HLSVariant) getVariantString(t *Transcoder) string { variantEncoderCommands := []string{ - v.getVideoQualityString(), + v.getVideoQualityString(t), v.getAudioQualityString(), } @@ -222,17 +215,7 @@ func (v *HLSVariant) getVariantString() string { variantEncoderCommands = append(variantEncoderCommands, v.getScalingString()) } - if v.framerate == 0 { - v.framerate = 30 - } - - if v.framerate != 0 { - variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-r %d", v.framerate)) - // Insert a keyframe every 2 seconds. - // 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)) - } + variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-r %d", v.framerate)) if v.encoderPreset != "" { variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", v.encoderPreset)) @@ -247,7 +230,7 @@ func (t *Transcoder) getVariantsString() string { var variantsStreamMaps = " -var_stream_map \"" for _, variant := range t.variants { - variantsCommandFlags = variantsCommandFlags + " " + variant.getVariantString() + variantsCommandFlags = variantsCommandFlags + " " + variant.getVariantString(t) variantsStreamMaps = variantsStreamMaps + fmt.Sprintf("v:%d,a:%d ", variant.index, variant.index) } variantsCommandFlags = variantsCommandFlags + " " + variantsStreamMaps + "\"" @@ -278,17 +261,40 @@ func (v *HLSVariant) getScalingString() string { // Video Quality // SetVideoBitrate will set the output bitrate of this variant's video -func (v *HLSVariant) SetVideoBitrate(bitrate string) { +func (v *HLSVariant) SetVideoBitrate(bitrate int) { v.videoBitrate = bitrate } -func (v *HLSVariant) getVideoQualityString() string { +func (v *HLSVariant) getVideoQualityString(t *Transcoder) 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) + + // -1 to work around segments being generated slightly larger than expected. + // https://trac.ffmpeg.org/ticket/6915?replyto=58#comment:57 + gop := (t.segmentLengthSeconds * v.framerate) - 1 + + // For limiting the output bitrate + // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate + // https://developer.apple.com/documentation/http_live_streaming/about_apple_s_http_live_streaming_tools + // Adjust the max & buffer size until the output bitrate doesn't exceed the ~+10% that Apple's media validator + // complains about. + maxBitrate := int(float64(v.videoBitrate) * 1.06) // Max is a ~+10% over specified bitrate. + bufferSize := int(float64(v.videoBitrate) * 1.2) // How often it checks the bitrate of encoded segments to see if it's too high/low. + + cmd := []string{ + "-map v:0", + fmt.Sprintf("-c:v:%d %s", v.index, encoderCodec), // Video codec used for this variant + fmt.Sprintf("-b:v:%d %dk", v.index, v.videoBitrate), // The average bitrate for this variant + fmt.Sprintf("-maxrate:v:%d %dk", v.index, maxBitrate), // The max bitrate allowed for this variant + fmt.Sprintf("-bufsize:v:%d %dk", v.index, bufferSize), // How often the encoder checks the bitrate in order to meet average/max values + fmt.Sprintf("-g:v:%d %d", v.index, gop), // How often i-frames are encoded into the segments + fmt.Sprintf("-x264-params:v:%d \"scenecut=0:open_gop=0:min-keyint=%d:keyint=%d\"", v.index, gop, gop), // How often i-frames are encoded into the segments + } + + return strings.Join(cmd, " ") } // SetVideoFramerate will set the output framerate of this variant's video diff --git a/core/ffmpeg/transcoder_test.go b/core/ffmpeg/transcoder_test.go index 8488aa0de..58e0f7cbb 100644 --- a/core/ffmpeg/transcoder_test.go +++ b/core/ffmpeg/transcoder_test.go @@ -13,7 +13,7 @@ func TestFFmpegCommand(t *testing.T) { transcoder.SetHLSPlaylistLength(10) variant := HLSVariant{} - variant.videoBitrate = "1200k" + variant.videoBitrate = 1200 variant.isAudioPassthrough = true variant.encoderPreset = "veryfast" variant.SetVideoFramerate(30) @@ -22,7 +22,7 @@ func TestFFmpegCommand(t *testing.T) { cmd := transcoder.getString() - expected := "cat fakecontent.flv | /fake/path/ffmpeg -hide_banner -i pipe: -map v:0 -c:v:0 libx264 -b:v:0 1200k -map a:0 -c:a:0 copy -r 30 -g 60 -keyint_min 60 -preset veryfast -var_stream_map \"v:0,a:0 \" -f hls -hls_time 4 -hls_list_size 10 -hls_delete_threshold 10 -hls_flags delete_segments+program_date_time+temp_file -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename fakeOutput/%v/stream-%s.ts -max_muxing_queue_size 400 fakeOutput/%v/stream.m3u8 2> transcoder.log" + expected := `cat fakecontent.flv | /fake/path/ffmpeg -hide_banner -i pipe: -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 119 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=119:keyint=119" -map a:0 -c:a:0 copy -r 30 -preset veryfast -var_stream_map "v:0,a:0 " -f hls -hls_time 4 -hls_list_size 10 -hls_delete_threshold 10 -hls_flags delete_segments+program_date_time+temp_file -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename fakeOutput/%v/stream-%s.ts -max_muxing_queue_size 400 fakeOutput/%v/stream.m3u8 2> transcoder.log` if cmd != expected { t.Errorf("ffmpeg command does not match expected. Got %s, want: %s", cmd, expected)