Skip to content

Commit 72f26d0

Browse files
committed
introduce audio and video filtergraph customization
1 parent 62da332 commit 72f26d0

File tree

4 files changed

+109
-15
lines changed

4 files changed

+109
-15
lines changed

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ vod:
119119
- "-tune:v=ull" # can be passed either as combined args, and will be split
120120
- "-rc:v" # or parameter ...
121121
- "cbr" # ... and value on separate lines
122+
# Optional filtergraph, start each chain with the special `[vin]` pad and end them in `[vout]`
123+
filtergraph:
124+
- "[vin]split=2[v1][v2]" # duplicate input
125+
- "[v1]crop=iw/2:ih:0:0,hflip[left]" # left half mirrored horizontally
126+
- "[v2]crop=iw/2:ih:iw/2:0,vflip[right]" # right half flipped vertically
127+
- "[left][right]hstack[vout]" # join halves back together
128+
122129
# HLS-VOD segment behaviour (optional)
123130
segment-length: 4 # nominal segment length in seconds
124131
segment-offset: 1 # allowed +/- tolerance in seconds
@@ -133,6 +140,9 @@ vod:
133140
audio-profile:
134141
encoder: aac # default "aac", but "copy" is an alternative
135142
bitrate: 192 # kbps
143+
# Optional filtergraph, start each chain with the special `[ain]` pad and end them in `[aout]`
144+
# filtergraph:
145+
# - "[ain]asetrate=48000*1.5,aresample=48000[aout]" # Pitch the audio up by ~50 % (makes everyone sound like that famous mouse!)
136146
# If cache is enabled
137147
cache: true
138148
# If dir is empty, cache will be stored in the same directory as media source
@@ -147,6 +157,57 @@ hls-proxy:
147157
my_server: http://192.168.1.34:9981
148158
```
149159
160+
## Defining filter graphs on video and audio streams
161+
162+
You can optionally define filtergraphs on video and audio profiles. This
163+
allows you to modify the streams during the transcoding process.
164+
165+
If you don't specify any filtergraphs, you get the video scaled to the
166+
dimensions you specified and the first audio track from the input video.
167+
168+
When you do supply a filtergraph:
169+
170+
* start the chain at the source pads `[vin]` (video) or `[ain]` (audio)
171+
* end the chain at `[vout]` or `[aout]` – these pads are what `-map` picks up
172+
173+
Examples:
174+
175+
```yaml
176+
vod:
177+
video-profiles:
178+
1080p:
179+
width: 1920
180+
height: 1080
181+
bitrate: 5000
182+
filtergraph:
183+
- "[vin]format=pix_fmts=yuv420p[vout]" # change pixel format to yuv420p
184+
```
185+
186+
```yaml
187+
vod:
188+
audio-profile:
189+
filtergraph:
190+
- "[ain][0:a:1]amix=inputs=2[aout]" # mix second audio track into the first
191+
```
192+
193+
### Implementation
194+
195+
The transcoder always assembles a single FFmpeg `-filter_complex` that already contains **one video and one audio chain**:
196+
197+
1. `[0:v]scale=…[vin]` – scales the first video stream and stores the result in pad `[vin]`.
198+
2. `[0:a]anull[ain]` – passes the first audio stream through unchanged into pad `[ain]`.
199+
3. If *no* extra filtergraph is supplied the code auto-adds `[vin]null[vout] ; [ain]anull[aout]` so the outputs exist.
200+
201+
Both pads are then selected with:
202+
203+
```sh
204+
-map [vout] -map [aout]?
205+
```
206+
207+
`-map` tells FFmpeg exactly which streams (by pad name or by input index) should
208+
be written to the current output file. Being explicit prevents surprises when
209+
inputs carry multiple audio/video streams.
210+
150211
## Transcoding profiles for live streams
151212

152213
go-transcode supports any formats that ffmpeg likes. We provide profiles out-of-the-box for h264+aac (mp4 container) for 360p, 540p, 720p and 1080p resolutions: `h264_360p`, `h264_540p`, `h264_720p` and `h264_1080p`. Profiles can have any name, but must match regex: `^[0-9A-Za-z_-]+$`

hlsvod/transcode.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ type VideoProfile struct {
3333
Profile string
3434
Level string
3535
ExtraArgs []string
36+
FilterGraph []string
3637
}
3738

3839
type AudioProfile struct {
3940
Encoder string // audio encoder (e.g., "aac", "copy", "libopus")
4041
Bitrate int // in kilobytes (0 means use encoder default)
42+
FilterGraph []string // optional audio filtergraph chains
4143
}
4244

4345
// returns a channel, that delivers name of the segments as they are encoded
@@ -86,17 +88,45 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod
8688
"-sn", // No subtitles
8789
}...)
8890

89-
// Video specs
91+
// Filtergraph (scaling + optional user graph)
9092
if config.VideoProfile != nil {
9193
profile := config.VideoProfile
9294

93-
var scale string
95+
// Build scale expression producing [vin] source pad
96+
var scaleExpr string
9497
if profile.Width >= profile.Height {
95-
scale = fmt.Sprintf("scale=-2:%d", profile.Height)
98+
scaleExpr = fmt.Sprintf("[0:v]scale=-2:%d[vin]", profile.Height)
9699
} else {
97-
scale = fmt.Sprintf("scale=%d:-2", profile.Width)
100+
scaleExpr = fmt.Sprintf("[0:v]scale=%d:-2[vin]", profile.Width)
98101
}
99102

103+
// Source audio pad
104+
audioIn := "[0:a]anull[ain]"
105+
106+
graphParts := []string{scaleExpr, audioIn}
107+
108+
// Video filters
109+
if len(profile.FilterGraph) > 0 {
110+
graphParts = append(graphParts, profile.FilterGraph...)
111+
} else {
112+
graphParts = append(graphParts, "[vin]null[vout]")
113+
}
114+
// Audio filters
115+
if config.AudioProfile != nil && len(config.AudioProfile.FilterGraph) > 0 {
116+
graphParts = append(graphParts, config.AudioProfile.FilterGraph...)
117+
} else {
118+
graphParts = append(graphParts, "[ain]anull[aout]")
119+
}
120+
combinedFG := strings.Join(graphParts, ";")
121+
// Add filter graph and explicit stream mapping (video & audio)
122+
args = append(args, "-filter_complex", combinedFG)
123+
args = append(args, "-map", "[vout]", "-map", "[aout]?")
124+
}
125+
126+
// Video specs
127+
if config.VideoProfile != nil {
128+
profile := config.VideoProfile
129+
100130
// apply defaults if empty
101131
encoder := profile.Encoder
102132
if encoder == "" {
@@ -116,7 +146,6 @@ func TranscodeSegments(ctx context.Context, ffmpegBinary string, config Transcod
116146
}
117147

118148
args = append(args, []string{
119-
"-vf", scale,
120149
"-c:v", encoder,
121150
"-preset", preset,
122151
"-profile:v", prof,

internal/api/hlsvod.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,20 @@ func (a *ApiManagerCtx) HlsVod(r chi.Router) {
144144
SegmentPrefix: profileID,
145145

146146
VideoProfile: &hlsvod.VideoProfile{
147-
Width: profile.Width,
148-
Height: profile.Height,
149-
Bitrate: profile.Bitrate,
150-
Encoder: profile.Encoder,
151-
Preset: profile.Preset,
152-
Profile: profile.Profile,
153-
Level: profile.Level,
154-
ExtraArgs: profile.ExtraArgs,
147+
Width: profile.Width,
148+
Height: profile.Height,
149+
Bitrate: profile.Bitrate,
150+
Encoder: profile.Encoder,
151+
Preset: profile.Preset,
152+
Profile: profile.Profile,
153+
Level: profile.Level,
154+
FilterGraph: profile.FilterGraph,
155+
ExtraArgs: profile.ExtraArgs,
155156
},
156157
VideoKeyframes: a.config.Vod.VideoKeyframes,
157158
AudioProfile: &hlsvod.AudioProfile{
158159
Bitrate: a.config.Vod.AudioProfile.Bitrate,
160+
FilterGraph: a.config.Vod.AudioProfile.FilterGraph,
159161
},
160162

161163
SegmentLength: a.config.Vod.SegmentLength,

internal/config/config.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ type VideoProfile struct {
5454
Profile string `mapstructure:"profile"`
5555
Level string `mapstructure:"level"`
5656
ExtraArgs []string `mapstructure:"extra-args"`
57+
FilterGraph []string `mapstructure:"filtergraph"`
5758
}
5859

5960
type AudioProfile struct {
60-
Encoder string `mapstructure:"encoder"`
61-
Bitrate int `mapstructure:"bitrate"` // in kilobytes
61+
Encoder string `mapstructure:"encoder"`
62+
Bitrate int `mapstructure:"bitrate"` // in kilobytes
63+
FilterGraph []string `mapstructure:"filtergraph"`
6264
}
6365

6466
type VOD struct {

0 commit comments

Comments
 (0)