diff --git a/README.md b/README.md index d6fb0b2..5459234 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,38 @@ vod: width: 1920 height: 1080 bitrate: 5000 + 1080p_nvidia_gpu: + width: 1920 + height: 1080 + bitrate: 5000 + # Optional ffmpeg video overrides + encoder: h264_nvenc # default "libx264" + preset: p1 # default "faster" + profile: high # default "high" + level: auto # default "4.0" + extra-args: # optionally, additional ffmpeg video encoder arguments + - "-tune:v=ull" # can be passed either as combined args, and will be split + - "-rc:v" # or parameter ... + - "cbr" # ... and value on separate lines + + # HLS-VOD segment behaviour (optional) + segment-length: 4 # nominal segment length in seconds + segment-offset: 1 # allowed +/- tolerance in seconds + segment-buffer-min: 3 # min segments ahead of playhead + segment-buffer-max: 5 # max segments transcoded at once + + # Timeout reconfiguration (optional) + ready-timeout: 80 # timeout for VOD manager to get ready + transcode-timeout: 10 # timeout waiting for a segment to transcode + # Use video keyframes as existing reference for chunks split # Using this might cause long probing times in order to get - # all keyframes - therefore they should be cached + # all keyframes - therefore they should be cached video-keyframes: false # Single audio profile used audio-profile: - bitrate: 192 # kbps + encoder: aac # default "aac", but "copy" is an alternative + bitrate: 192 # kbps # If cache is enabled cache: true # If dir is empty, cache will be stored in the same directory as media source diff --git a/hlsvod/manager.go b/hlsvod/manager.go index 662c41d..af98962 100644 --- a/hlsvod/manager.go +++ b/hlsvod/manager.go @@ -18,12 +18,6 @@ import ( "github.com/rs/zerolog/log" ) -// how long can it take for transcode to be ready -const readyTimeout = 80 * time.Second - -// how long can it take for transcode to return first data -const transcodeTimeout = 10 * time.Second - type ManagerCtx struct { mu sync.Mutex logger zerolog.Logger @@ -53,15 +47,34 @@ type ManagerCtx struct { } func New(config Config) *ManagerCtx { + // apply defaults if zero + if config.SegmentLength == 0 { + config.SegmentLength = 4 + } + if config.SegmentOffset == 0 { + config.SegmentOffset = 1 + } + if config.SegmentBufferMin == 0 { + config.SegmentBufferMin = 3 + } + if config.SegmentBufferMax == 0 { + config.SegmentBufferMax = 5 + } + if config.ReadyTimeout == 0 { + config.ReadyTimeout = 80 + } + if config.TranscodeTimeout == 0 { + config.TranscodeTimeout = 10 + } ctx, cancel := context.WithCancel(context.Background()) return &ManagerCtx{ logger: log.With().Str("module", "hlsvod").Str("submodule", "manager").Logger(), config: config, - segmentLength: 4, - segmentOffset: 1, - segmentBufferMin: 3, - segmentBufferMax: 5, + segmentLength: config.SegmentLength, + segmentOffset: config.SegmentOffset, + segmentBufferMin: config.SegmentBufferMin, + segmentBufferMax: config.SegmentBufferMax, ctx: ctx, cancel: cancel, @@ -122,7 +135,7 @@ func (m *ManagerCtx) httpEnsureReady(w http.ResponseWriter) bool { m.logger.Warn().Msg("manager load failed because of shutdown") http.Error(w, "500 manager not available", http.StatusInternalServerError) return false - case <-time.After(readyTimeout): + case <-time.After(time.Duration(m.config.ReadyTimeout) * time.Second): m.logger.Warn().Msg("manager load timeouted") http.Error(w, "504 manager timeout", http.StatusGatewayTimeout) return false @@ -608,7 +621,7 @@ func (m *ManagerCtx) ServeMedia(w http.ResponseWriter, r *http.Request) { m.logger.Warn().Msg("media transcode failed because of shutdown") http.Error(w, "500 media not available", http.StatusInternalServerError) return - case <-time.After(transcodeTimeout): + case <-time.After(time.Duration(m.config.TranscodeTimeout) * time.Second): m.logger.Warn().Msg("media transcode timeouted") http.Error(w, "504 media timeout", http.StatusGatewayTimeout) return diff --git a/hlsvod/transcode.go b/hlsvod/transcode.go index 9bbe037..422deb1 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go @@ -26,10 +26,18 @@ type VideoProfile struct { Width int Height int Bitrate int // in kilobytes + + // Optional FFmpeg overrides + Encoder string + Preset string + Profile string + Level string + ExtraArgs []string } type AudioProfile struct { - Bitrate int // in kilobytes + Encoder string // audio encoder (e.g., "aac", "copy", "libopus") + Bitrate int // in kilobytes (0 means use encoder default) } // returns a channel, that delivers name of the segments as they are encoded @@ -89,24 +97,57 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod scale = fmt.Sprintf("scale=%d:-2", profile.Width) } + // apply defaults if empty + encoder := profile.Encoder + if encoder == "" { + encoder = "libx264" + } + preset := profile.Preset + if preset == "" { + preset = "faster" + } + prof := profile.Profile + if prof == "" { + prof = "high" + } + lvl := profile.Level + if lvl == "" { + lvl = "4.0" + } + args = append(args, []string{ "-vf", scale, - "-c:v", "libx264", - "-preset", "faster", - "-profile:v", "high", - "-level:v", "4.0", + "-c:v", encoder, + "-preset", preset, + "-profile:v", prof, + "-level:v", lvl, "-b:v", fmt.Sprintf("%dk", profile.Bitrate), }...) + + // extra args + if len(profile.ExtraArgs) > 0 { + extraArgs := make([]string, 0, len(profile.ExtraArgs)) + for _, arg := range profile.ExtraArgs { + // Split combined args like "-tune:v=ull" into "-tune:v", "ull" + if strings.Contains(arg, "=") { + extraArgs = append(extraArgs, strings.SplitN(arg, "=", 2)...) + } else { + extraArgs = append(extraArgs, arg) + } + } + args = append(args, extraArgs...) + } } // Audio specs if config.AudioProfile != nil { profile := config.AudioProfile - - args = append(args, []string{ - "-c:a", "aac", - "-b:a", fmt.Sprintf("%dk", profile.Bitrate), - }...) + if profile.Encoder != "" { + args = append(args, "-c:a", profile.Encoder) + if profile.Bitrate > 0 { + args = append(args, "-b:a", fmt.Sprintf("%dk", profile.Bitrate)) + } + } } // Segmenting specs diff --git a/hlsvod/types.go b/hlsvod/types.go index 7c4e651..d5c3942 100644 --- a/hlsvod/types.go +++ b/hlsvod/types.go @@ -14,6 +14,15 @@ type Config struct { VideoKeyframes bool AudioProfile *AudioProfile + // HLS-VOD segment parameters (override defaults from server) + SegmentLength float64 + SegmentOffset float64 + SegmentBufferMin int + SegmentBufferMax int + + ReadyTimeout int + TranscodeTimeout int + Cache bool CacheDir string // If not empty, cache will folder will be used instead of media path diff --git a/internal/api/hlsvod.go b/internal/api/hlsvod.go index 4c96481..ecd8e79 100644 --- a/internal/api/hlsvod.go +++ b/internal/api/hlsvod.go @@ -147,12 +147,25 @@ func (a *ApiManagerCtx) HlsVod(r chi.Router) { Width: profile.Width, Height: profile.Height, Bitrate: profile.Bitrate, + Encoder: profile.Encoder, + Preset: profile.Preset, + Profile: profile.Profile, + Level: profile.Level, + ExtraArgs: profile.ExtraArgs, }, VideoKeyframes: a.config.Vod.VideoKeyframes, AudioProfile: &hlsvod.AudioProfile{ Bitrate: a.config.Vod.AudioProfile.Bitrate, }, + SegmentLength: a.config.Vod.SegmentLength, + SegmentOffset: a.config.Vod.SegmentOffset, + SegmentBufferMin: a.config.Vod.SegmentBufferMin, + SegmentBufferMax: a.config.Vod.SegmentBufferMax, + + ReadyTimeout: a.config.Vod.ReadyTimeout, + TranscodeTimeout: a.config.Vod.TranscodeTimeout, + Cache: a.config.Vod.Cache, CacheDir: a.config.Vod.CacheDir, diff --git a/internal/config/config.go b/internal/config/config.go index 532aae2..d8bbb76 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,10 +47,18 @@ type VideoProfile struct { Width int `mapstructure:"width"` Height int `mapstructure:"height"` Bitrate int `mapstructure:"bitrate"` // in kilobytes + + // Optional FFmpeg overrides + Encoder string `mapstructure:"encoder"` + Preset string `mapstructure:"preset"` + Profile string `mapstructure:"profile"` + Level string `mapstructure:"level"` + ExtraArgs []string `mapstructure:"extra-args"` } type AudioProfile struct { - Bitrate int `mapstructure:"bitrate"` // in kilobytes + Encoder string `mapstructure:"encoder"` + Bitrate int `mapstructure:"bitrate"` // in kilobytes } type VOD struct { @@ -61,8 +69,18 @@ type VOD struct { AudioProfile AudioProfile `mapstructure:"audio-profile"` Cache bool `mapstructure:"cache"` CacheDir string `mapstructure:"cache-dir"` - FFmpegBinary string `mapstructure:"ffmpeg-binary"` - FFprobeBinary string `mapstructure:"ffprobe-binary"` + + // HLS-VOD segment parameters + SegmentLength float64 `mapstructure:"segment-length"` + SegmentOffset float64 `mapstructure:"segment-offset"` + SegmentBufferMin int `mapstructure:"segment-buffer-min"` + SegmentBufferMax int `mapstructure:"segment-buffer-max"` + + ReadyTimeout int `mapstructure:"ready-timeout"` + TranscodeTimeout int `mapstructure:"transcode-timeout"` + + FFmpegBinary string `mapstructure:"ffmpeg-binary"` + FFprobeBinary string `mapstructure:"ffprobe-binary"` } type Enigma2 struct { @@ -142,6 +160,40 @@ func (Server) Init(cmd *cobra.Command) error { return err } + // HLS-VOD segment flags + cmd.PersistentFlags().Float64("vod-segment-length", 4, "HLS-VOD segment length in seconds") + if err := viper.BindPFlag("vod.segment-length", cmd.PersistentFlags().Lookup("vod-segment-length")); err != nil { + return err + } + + cmd.PersistentFlags().Float64("vod-segment-offset", 1, "HLS-VOD allowed deviation from segment length in seconds") + if err := viper.BindPFlag("vod.segment-offset", cmd.PersistentFlags().Lookup("vod-segment-offset")); err != nil { + return err + } + + cmd.PersistentFlags().Int("vod-segment-buffer-min", 3, "HLS-VOD minimum number of future segments maintained") + if err := viper.BindPFlag("vod.segment-buffer-min", cmd.PersistentFlags().Lookup("vod-segment-buffer-min")); err != nil { + return err + } + + cmd.PersistentFlags().Int("vod-segment-buffer-max", 5, "HLS-VOD maximum number of segments transcoded in a batch") + if err := viper.BindPFlag("vod.segment-buffer-max", cmd.PersistentFlags().Lookup("vod-segment-buffer-max")); err != nil { + return err + } + + // VOD timeouts + cmd.PersistentFlags().Int("vod-ready-timeout", 80, "HLS-VOD timeout (seconds) for manager to become ready") + if err := viper.BindPFlag("vod.ready-timeout", cmd.PersistentFlags().Lookup("vod-ready-timeout")); err != nil { + return err + } + cmd.PersistentFlags().Int("vod-transcode-timeout", 10, "HLS-VOD timeout (seconds) for a segment to transcode") + if err := viper.BindPFlag("vod.transcode-timeout", cmd.PersistentFlags().Lookup("vod-transcode-timeout")); err != nil { + return err + } + if err := viper.BindPFlag("vod.segment-buffer-max", cmd.PersistentFlags().Lookup("vod-segment-buffer-max")); err != nil { + return err + } + return nil } @@ -173,11 +225,55 @@ func (s *Server) Set() { // // VOD // - if err := viper.UnmarshalKey("vod", &s.Vod); err != nil { - panic(err) + // Unmarshal the VOD section from the config file + if viper.IsSet("vod") { + if err := viper.UnmarshalKey("vod", &s.Vod); err != nil { + panic(err) + } } - // defaults + // Set default values for VOD settings if they're not set in the config file + if s.Vod.SegmentLength == 0 { + s.Vod.SegmentLength = viper.GetFloat64("vod.segment-length") + if s.Vod.SegmentLength == 0 { + s.Vod.SegmentLength = 4 // default value + } + } + + if s.Vod.SegmentOffset == 0 { + s.Vod.SegmentOffset = viper.GetFloat64("vod.segment-offset") + if s.Vod.SegmentOffset == 0 { + s.Vod.SegmentOffset = 1 // default value + } + } + + if s.Vod.SegmentBufferMin == 0 { + s.Vod.SegmentBufferMin = viper.GetInt("vod.segment-buffer-min") + if s.Vod.SegmentBufferMin == 0 { + s.Vod.SegmentBufferMin = 3 // default value + } + } + + if s.Vod.SegmentBufferMax == 0 { + s.Vod.SegmentBufferMax = viper.GetInt("vod.segment-buffer-max") + if s.Vod.SegmentBufferMax == 0 { + s.Vod.SegmentBufferMax = 5 // default value + } + } + + if s.Vod.ReadyTimeout == 0 { + s.Vod.ReadyTimeout = viper.GetInt("vod.ready-timeout") + if s.Vod.ReadyTimeout == 0 { + s.Vod.ReadyTimeout = 80 // default value + } + } + + if s.Vod.TranscodeTimeout == 0 { + s.Vod.TranscodeTimeout = viper.GetInt("vod.transcode-timeout") + if s.Vod.TranscodeTimeout == 0 { + s.Vod.TranscodeTimeout = 10 // default value + } + } if s.Vod.TranscodeDir == "" { var err error @@ -211,6 +307,23 @@ func (s *Server) Set() { s.Vod.FFprobeBinary = "ffprobe" } + // apply defaults to each video profile + for k, vp := range s.Vod.VideoProfiles { + if vp.Encoder == "" { + vp.Encoder = "libx264" + } + if vp.Preset == "" { + vp.Preset = "faster" + } + if vp.Profile == "" { + vp.Profile = "high" + } + if vp.Level == "" { + vp.Level = "4.0" + } + s.Vod.VideoProfiles[k] = vp + } + // // HLS PROXY //