Skip to content
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 25 additions & 12 deletions hlsvod/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
61 changes: 51 additions & 10 deletions hlsvod/transcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions hlsvod/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions internal/api/hlsvod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
Loading