From 6d4a86dd5e7416f6a12fccfc5439b57ea9879182 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Tue, 17 Jun 2025 11:26:06 +0200 Subject: [PATCH 1/9] make VOD segments configurable via config or CLI --- README.md | 5 ++++ hlsvod/manager.go | 21 +++++++++++++--- hlsvod/types.go | 6 +++++ internal/api/hlsvod.go | 5 ++++ internal/config/config.go | 51 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d6fb0b2..db98e00 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,11 @@ vod: width: 1920 height: 1080 bitrate: 5000 + # 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 # 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 diff --git a/hlsvod/manager.go b/hlsvod/manager.go index 662c41d..03105d5 100644 --- a/hlsvod/manager.go +++ b/hlsvod/manager.go @@ -53,15 +53,28 @@ 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 + } 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, diff --git a/hlsvod/types.go b/hlsvod/types.go index 7c4e651..18c78c5 100644 --- a/hlsvod/types.go +++ b/hlsvod/types.go @@ -14,6 +14,12 @@ type Config struct { VideoKeyframes bool AudioProfile *AudioProfile + // HLS-VOD segment parameters (override defaults from server) + SegmentLength float64 + SegmentOffset float64 + SegmentBufferMin int + SegmentBufferMax 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..791d2b5 100644 --- a/internal/api/hlsvod.go +++ b/internal/api/hlsvod.go @@ -153,6 +153,11 @@ func (a *ApiManagerCtx) HlsVod(r chi.Router) { 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, + Cache: a.config.Vod.Cache, CacheDir: a.config.Vod.CacheDir, diff --git a/internal/config/config.go b/internal/config/config.go index 532aae2..25c21e2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,8 +61,15 @@ 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"` + + FFmpegBinary string `mapstructure:"ffmpeg-binary"` + FFprobeBinary string `mapstructure:"ffprobe-binary"` } type Enigma2 struct { @@ -142,6 +149,27 @@ 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 + } + return nil } @@ -177,8 +205,27 @@ func (s *Server) Set() { panic(err) } + // segment parameters populated from viper + s.Vod.SegmentLength = viper.GetFloat64("vod.segment-length") + s.Vod.SegmentOffset = viper.GetFloat64("vod.segment-offset") + s.Vod.SegmentBufferMin = viper.GetInt("vod.segment-buffer-min") + s.Vod.SegmentBufferMax = viper.GetInt("vod.segment-buffer-max") + // defaults + if s.Vod.SegmentLength == 0 { + s.Vod.SegmentLength = 4 + } + if s.Vod.SegmentOffset == 0 { + s.Vod.SegmentOffset = 1 + } + if s.Vod.SegmentBufferMin == 0 { + s.Vod.SegmentBufferMin = 3 + } + if s.Vod.SegmentBufferMax == 0 { + s.Vod.SegmentBufferMax = 5 + } + if s.Vod.TranscodeDir == "" { var err error s.Vod.TranscodeDir, err = os.MkdirTemp(os.TempDir(), "go-transcode-vod") From bdac3fcff357233a635f039e17fba258bbf4a3a4 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Tue, 17 Jun 2025 11:38:19 +0200 Subject: [PATCH 2/9] make ffmpeg video encoding settings configurable --- README.md | 8 ++++++++ hlsvod/transcode.go | 38 ++++++++++++++++++++++++++++++++++---- internal/api/hlsvod.go | 5 +++++ internal/config/config.go | 26 +++++++++++++++++++++++++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index db98e00..1f0298b 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,14 @@ vod: width: 640 # px height: 360 # px bitrate: 800 # kbps + # Optional ffmpeg overrides + encoder: libx264 # video encoder (e.g. libx264, h264_nvenc) + preset: faster # default "faster" + profile: high # default "high" + level: "4.0" # default "4.0" + extra-args: + - "-x264opts" + - "keyint=48:min-keyint=48" 540p: width: 960 height: 540 diff --git a/hlsvod/transcode.go b/hlsvod/transcode.go index 9bbe037..a2b70d3 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go @@ -26,6 +26,13 @@ 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 { @@ -89,14 +96,37 @@ 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 { + args = append(args, profile.ExtraArgs...) + } } // Audio specs diff --git a/internal/api/hlsvod.go b/internal/api/hlsvod.go index 791d2b5..1a50984 100644 --- a/internal/api/hlsvod.go +++ b/internal/api/hlsvod.go @@ -147,6 +147,11 @@ 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{ diff --git a/internal/config/config.go b/internal/config/config.go index 25c21e2..27b1efc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,6 +47,13 @@ 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 { @@ -211,7 +218,7 @@ func (s *Server) Set() { s.Vod.SegmentBufferMin = viper.GetInt("vod.segment-buffer-min") s.Vod.SegmentBufferMax = viper.GetInt("vod.segment-buffer-max") - // defaults + // defaults (HLS-VOD segment) if s.Vod.SegmentLength == 0 { s.Vod.SegmentLength = 4 @@ -258,6 +265,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 // From 9aba8968978f391809b4154cd5e583a255ca2453 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Wed, 18 Jun 2025 22:44:07 +0200 Subject: [PATCH 3/9] rename encoder option to codec --- README.md | 14 ++++++++------ hlsvod/transcode.go | 10 +++++----- internal/api/hlsvod.go | 2 +- internal/config/config.go | 6 +++--- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1f0298b..c238766 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,15 @@ vod: height: 360 # px bitrate: 800 # kbps # Optional ffmpeg overrides - encoder: libx264 # video encoder (e.g. libx264, h264_nvenc) - preset: faster # default "faster" - profile: high # default "high" - level: "4.0" # default "4.0" + codec: h264_nvenc # default "libx264" + preset: p1 # default "faster" + profile: high # default "high" + level: auto # default "4.0" extra-args: - - "-x264opts" - - "keyint=48:min-keyint=48" + - "-tune:v" + - "ull" + - "-rc:v" + - "cbr" 540p: width: 960 height: 540 diff --git a/hlsvod/transcode.go b/hlsvod/transcode.go index a2b70d3..05ddb22 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go @@ -28,7 +28,7 @@ type VideoProfile struct { Bitrate int // in kilobytes // Optional FFmpeg overrides - Encoder string + Codec string Preset string Profile string Level string @@ -97,9 +97,9 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod } // apply defaults if empty - encoder := profile.Encoder - if encoder == "" { - encoder = "libx264" + codec := profile.Codec + if codec == "" { + codec = "libx264" } preset := profile.Preset if preset == "" { @@ -116,7 +116,7 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod args = append(args, []string{ "-vf", scale, - "-c:v", encoder, + "-c:v", codec, "-preset", preset, "-profile:v", prof, "-level:v", lvl, diff --git a/internal/api/hlsvod.go b/internal/api/hlsvod.go index 1a50984..47248ba 100644 --- a/internal/api/hlsvod.go +++ b/internal/api/hlsvod.go @@ -147,7 +147,7 @@ func (a *ApiManagerCtx) HlsVod(r chi.Router) { Width: profile.Width, Height: profile.Height, Bitrate: profile.Bitrate, - Encoder: profile.Encoder, + Codec: profile.Codec, Preset: profile.Preset, Profile: profile.Profile, Level: profile.Level, diff --git a/internal/config/config.go b/internal/config/config.go index 27b1efc..f7ec7e5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,7 +49,7 @@ type VideoProfile struct { Bitrate int `mapstructure:"bitrate"` // in kilobytes // Optional FFmpeg overrides - Encoder string `mapstructure:"encoder"` + Codec string `mapstructure:"codec"` Preset string `mapstructure:"preset"` Profile string `mapstructure:"profile"` Level string `mapstructure:"level"` @@ -267,8 +267,8 @@ func (s *Server) Set() { // apply defaults to each video profile for k, vp := range s.Vod.VideoProfiles { - if vp.Encoder == "" { - vp.Encoder = "libx264" + if vp.Codec == "" { + vp.Codec = "libx264" } if vp.Preset == "" { vp.Preset = "faster" From 6e5994fca7cc53da128e9324680367459ddf0940 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Thu, 19 Jun 2025 10:33:17 +0200 Subject: [PATCH 4/9] allow passing in combined extra args to the video encoder --- README.md | 11 +++++------ hlsvod/transcode.go | 11 ++++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c238766..55b4c7c 100644 --- a/README.md +++ b/README.md @@ -94,16 +94,15 @@ vod: width: 640 # px height: 360 # px bitrate: 800 # kbps - # Optional ffmpeg overrides + # Optional ffmpeg video overrides codec: h264_nvenc # default "libx264" preset: p1 # default "faster" profile: high # default "high" level: auto # default "4.0" - extra-args: - - "-tune:v" - - "ull" - - "-rc:v" - - "cbr" + 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 540p: width: 960 height: 540 diff --git a/hlsvod/transcode.go b/hlsvod/transcode.go index 05ddb22..662d1f6 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go @@ -125,7 +125,16 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod // extra args if len(profile.ExtraArgs) > 0 { - args = append(args, profile.ExtraArgs...) + 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...) } } From c5e67ebaeabd4a24aa4da37af608df27ac74d7dd Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Thu, 19 Jun 2025 10:46:44 +0200 Subject: [PATCH 5/9] allow configuration of VOD audio codec --- README.md | 3 ++- hlsvod/transcode.go | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 55b4c7c..c3f87dd 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,8 @@ vod: video-keyframes: false # Single audio profile used audio-profile: - bitrate: 192 # kbps + codec: 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/transcode.go b/hlsvod/transcode.go index 662d1f6..5613fae 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go @@ -36,7 +36,8 @@ type VideoProfile struct { } type AudioProfile struct { - Bitrate int // in kilobytes + Codec string // audio codec (e.g., "aac", "copy", "libopus") + Bitrate int // in kilobytes (0 means use codec default) } // returns a channel, that delivers name of the segments as they are encoded @@ -141,11 +142,12 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod // 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.Codec != "" { + args = append(args, "-c:a", profile.Codec) + if profile.Bitrate > 0 { + args = append(args, "-b:a", fmt.Sprintf("%dk", profile.Bitrate)) + } + } } // Segmenting specs From 6d276ecf4289b1c9003a8828141d74b5fe30a8db Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Fri, 4 Jul 2025 13:10:15 +0200 Subject: [PATCH 6/9] move encoder reconfiguration example into separate profile --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c3f87dd..c740940 100644 --- a/README.md +++ b/README.md @@ -94,15 +94,6 @@ vod: width: 640 # px height: 360 # px bitrate: 800 # kbps - # Optional ffmpeg video overrides - codec: 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 540p: width: 960 height: 540 @@ -115,14 +106,28 @@ vod: width: 1920 height: 1080 bitrate: 5000 + 1080p_nvidia_gpu: + width: 1920 + height: 1080 + bitrate: 5000 + # Optional ffmpeg video overrides + codec: 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 + # 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: From 62da332fa39accaa915eab03f3383029a4c2bb99 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Fri, 4 Jul 2025 13:14:44 +0200 Subject: [PATCH 7/9] rename video/audio profile config 'codec' to 'encoder' The earlier change to 'codec' was based on a misunderstanding. This actually tells ffmpeg which encoder to use, so this identifier is more accurate. Example: 'copy' ("don't change anything") is a valid encoder, but it's clearly not a codec. --- README.md | 4 ++-- hlsvod/transcode.go | 18 +++++++++--------- internal/api/hlsvod.go | 2 +- internal/config/config.go | 9 +++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c740940..73ebaa5 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ vod: height: 1080 bitrate: 5000 # Optional ffmpeg video overrides - codec: h264_nvenc # default "libx264" + encoder: h264_nvenc # default "libx264" preset: p1 # default "faster" profile: high # default "high" level: auto # default "4.0" @@ -131,7 +131,7 @@ vod: video-keyframes: false # Single audio profile used audio-profile: - codec: aac # default "aac", but "copy" is an alternative + encoder: aac # default "aac", but "copy" is an alternative bitrate: 192 # kbps # If cache is enabled cache: true diff --git a/hlsvod/transcode.go b/hlsvod/transcode.go index 5613fae..422deb1 100644 --- a/hlsvod/transcode.go +++ b/hlsvod/transcode.go @@ -28,7 +28,7 @@ type VideoProfile struct { Bitrate int // in kilobytes // Optional FFmpeg overrides - Codec string + Encoder string Preset string Profile string Level string @@ -36,8 +36,8 @@ type VideoProfile struct { } type AudioProfile struct { - Codec string // audio codec (e.g., "aac", "copy", "libopus") - Bitrate int // in kilobytes (0 means use codec default) + 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 @@ -98,9 +98,9 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod } // apply defaults if empty - codec := profile.Codec - if codec == "" { - codec = "libx264" + encoder := profile.Encoder + if encoder == "" { + encoder = "libx264" } preset := profile.Preset if preset == "" { @@ -117,7 +117,7 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod args = append(args, []string{ "-vf", scale, - "-c:v", codec, + "-c:v", encoder, "-preset", preset, "-profile:v", prof, "-level:v", lvl, @@ -142,8 +142,8 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod // Audio specs if config.AudioProfile != nil { profile := config.AudioProfile - if profile.Codec != "" { - args = append(args, "-c:a", profile.Codec) + if profile.Encoder != "" { + args = append(args, "-c:a", profile.Encoder) if profile.Bitrate > 0 { args = append(args, "-b:a", fmt.Sprintf("%dk", profile.Bitrate)) } diff --git a/internal/api/hlsvod.go b/internal/api/hlsvod.go index 47248ba..0d74921 100644 --- a/internal/api/hlsvod.go +++ b/internal/api/hlsvod.go @@ -147,7 +147,7 @@ func (a *ApiManagerCtx) HlsVod(r chi.Router) { Width: profile.Width, Height: profile.Height, Bitrate: profile.Bitrate, - Codec: profile.Codec, + Encoder: profile.Encoder, Preset: profile.Preset, Profile: profile.Profile, Level: profile.Level, diff --git a/internal/config/config.go b/internal/config/config.go index f7ec7e5..ea03fa3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,7 +49,7 @@ type VideoProfile struct { Bitrate int `mapstructure:"bitrate"` // in kilobytes // Optional FFmpeg overrides - Codec string `mapstructure:"codec"` + Encoder string `mapstructure:"encoder"` Preset string `mapstructure:"preset"` Profile string `mapstructure:"profile"` Level string `mapstructure:"level"` @@ -57,7 +57,8 @@ type VideoProfile struct { } type AudioProfile struct { - Bitrate int `mapstructure:"bitrate"` // in kilobytes + Encoder string `mapstructure:"encoder"` + Bitrate int `mapstructure:"bitrate"` // in kilobytes } type VOD struct { @@ -267,8 +268,8 @@ func (s *Server) Set() { // apply defaults to each video profile for k, vp := range s.Vod.VideoProfiles { - if vp.Codec == "" { - vp.Codec = "libx264" + if vp.Encoder == "" { + vp.Encoder = "libx264" } if vp.Preset == "" { vp.Preset = "faster" From 4d55fdae03e912fb28f909e904cdec0c15f5ec0a Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Fri, 18 Jul 2025 11:55:10 +0200 Subject: [PATCH 8/9] allow configuration of VOD transcoding timeouts --- README.md | 5 +++++ hlsvod/manager.go | 16 ++++++++-------- hlsvod/types.go | 3 +++ internal/config/config.go | 25 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 73ebaa5..5459234 100644 --- a/README.md +++ b/README.md @@ -119,12 +119,17 @@ vod: - "-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 diff --git a/hlsvod/manager.go b/hlsvod/manager.go index 03105d5..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 @@ -66,6 +60,12 @@ func New(config Config) *ManagerCtx { 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(), @@ -135,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 @@ -621,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/types.go b/hlsvod/types.go index 18c78c5..d5c3942 100644 --- a/hlsvod/types.go +++ b/hlsvod/types.go @@ -20,6 +20,9 @@ type Config struct { 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/config/config.go b/internal/config/config.go index ea03fa3..9d008d9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -76,6 +76,9 @@ type VOD struct { 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"` } @@ -174,6 +177,19 @@ func (Server) Init(cmd *cobra.Command) error { } 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 } @@ -218,6 +234,8 @@ func (s *Server) Set() { s.Vod.SegmentOffset = viper.GetFloat64("vod.segment-offset") s.Vod.SegmentBufferMin = viper.GetInt("vod.segment-buffer-min") s.Vod.SegmentBufferMax = viper.GetInt("vod.segment-buffer-max") + s.Vod.ReadyTimeout = viper.GetInt("vod.ready-timeout") + s.Vod.TranscodeTimeout = viper.GetInt("vod.transcode-timeout") // defaults (HLS-VOD segment) @@ -234,6 +252,13 @@ func (s *Server) Set() { s.Vod.SegmentBufferMax = 5 } + if s.Vod.ReadyTimeout == 0 { + s.Vod.ReadyTimeout = 80 + } + if s.Vod.TranscodeTimeout == 0 { + s.Vod.TranscodeTimeout = 10 + } + if s.Vod.TranscodeDir == "" { var err error s.Vod.TranscodeDir, err = os.MkdirTemp(os.TempDir(), "go-transcode-vod") From b5563a8cb591d8dd315935977b6bf27ffc279c54 Mon Sep 17 00:00:00 2001 From: Henrik Heimbuerger Date: Tue, 22 Jul 2025 11:07:41 +0200 Subject: [PATCH 9/9] fix VOD config processing --- internal/api/hlsvod.go | 3 ++ internal/config/config.go | 58 +++++++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/internal/api/hlsvod.go b/internal/api/hlsvod.go index 0d74921..ecd8e79 100644 --- a/internal/api/hlsvod.go +++ b/internal/api/hlsvod.go @@ -163,6 +163,9 @@ func (a *ApiManagerCtx) HlsVod(r chi.Router) { 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 9d008d9..d8bbb76 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -225,39 +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) + } } - // segment parameters populated from viper - s.Vod.SegmentLength = viper.GetFloat64("vod.segment-length") - s.Vod.SegmentOffset = viper.GetFloat64("vod.segment-offset") - s.Vod.SegmentBufferMin = viper.GetInt("vod.segment-buffer-min") - s.Vod.SegmentBufferMax = viper.GetInt("vod.segment-buffer-max") - s.Vod.ReadyTimeout = viper.GetInt("vod.ready-timeout") - s.Vod.TranscodeTimeout = viper.GetInt("vod.transcode-timeout") - - // defaults (HLS-VOD segment) - + // Set default values for VOD settings if they're not set in the config file if s.Vod.SegmentLength == 0 { - s.Vod.SegmentLength = 4 + 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 = 1 + 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 = 3 + 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 = 5 + 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 = 80 - } - if s.Vod.TranscodeTimeout == 0 { - s.Vod.TranscodeTimeout = 10 - } + 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