-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
301 lines (258 loc) · 8.27 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// Copyright ©2022 Evolution. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// Application configuration structures.
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/evolution-gaming/ease/internal/logging"
"github.com/evolution-gaming/ease/internal/tools"
"github.com/evolution-gaming/ease/internal/vqm"
)
var (
ErrInvalidConfig = errors.New("invalid configuration")
defaultReportFile = "report.csv"
)
// Config represent application configuration.
type Config struct {
FfmpegPath ConfigVal[string] `json:"ffmpeg_path,omitempty"`
FfprobePath ConfigVal[string] `json:"ffprobe_path,omitempty"`
LibvmafModelPath ConfigVal[string] `json:"libvmaf_model_path,omitempty"`
FfmpegVMAFTemplate ConfigVal[string] `json:"ffmpeg_vmaf_template,omitempty"`
ReportFileName ConfigVal[string] `json:"report_file_name,omitempty"`
}
// Verify will check that configuration is valid.
//
// Will check that configuration option values are sensible.
func (c *Config) Verify() error {
msgs := []string{}
// Check that ffmpeg exists.
if !fileExists(c.FfmpegPath.Value()) {
msgs = append(msgs, "invalid ffmpeg path")
}
// Check that ffprobe exists.
if !fileExists(c.FfprobePath.Value()) {
msgs = append(msgs, "invalid ffprobe path")
}
// Check that libvmaf model file exists.
if !fileExists(c.LibvmafModelPath.Value()) {
msgs = append(msgs, "invalid libvmaf model file path")
}
// Template should not be nil.
if c.FfmpegVMAFTemplate.IsNil() {
msgs = append(msgs, "empty ffmpeg VMAF template")
}
// Report file should not be nil.
if c.ReportFileName.IsNil() {
msgs = append(msgs, "empty report file name")
}
if len(msgs) != 0 {
return fmt.Errorf("%s: %w", strings.Join(msgs, ", "), ErrInvalidConfig)
}
return nil
}
// OverrideFrom will overwrite fields from given Config object.
//
// Only fields that are "not-nil" (as per IsNil() method) in src Config object will be
// overwritten.
func (c *Config) OverrideFrom(src Config) {
// TODO: some way to iterate over fields and set them (reflection?) otherwise need to
// remember to update this method when new fields are added.
if !src.FfmpegPath.IsNil() {
c.FfmpegPath = src.FfmpegPath
}
if !src.FfprobePath.IsNil() {
c.FfprobePath = src.FfprobePath
}
if !src.LibvmafModelPath.IsNil() {
c.LibvmafModelPath = src.LibvmafModelPath
}
if !src.FfmpegVMAFTemplate.IsNil() {
c.FfmpegVMAFTemplate = src.FfmpegVMAFTemplate
}
if !src.ReportFileName.IsNil() {
c.ReportFileName = src.ReportFileName
}
}
// loadDefaultConfig will create a default configuration.
//
// For some configuration options a default value will be specified, for others an
// auto-detection mechanism will populate option values.
func loadDefaultConfig() Config {
var cfg Config
// For default configuration attempt to locate ffmpeg binary.
ffmpeg, err := tools.FfmpegPath()
if err != nil {
ffmpeg = "not found"
}
// For default configuration attempt to locate ffprobe binary.
ffprobe, err := tools.FfprobePath()
if err != nil {
ffprobe = "not found"
}
// For default configuration attempt to locate VMAF model file.
libvmafModel, err := tools.FindLibvmafModel()
if err != nil {
libvmafModel = "not found"
}
cfg = Config{
FfmpegPath: NewConfigVal(ffmpeg),
FfprobePath: NewConfigVal(ffprobe),
LibvmafModelPath: NewConfigVal(libvmafModel),
FfmpegVMAFTemplate: NewConfigVal(vqm.DefaultFfmpegVMAFTemplate),
ReportFileName: NewConfigVal(defaultReportFile),
}
return cfg
}
// loadConfigFromFile will load configuration from file.
//
// Only JSON is supported at this point.
func loadConfigFromFile(f string) (cfg Config, err error) {
fileExt := strings.ToLower(filepath.Ext(f))
switch fileExt {
case ".json":
return loadJSON(f)
default:
return cfg, fmt.Errorf("unknown config format: %s", fileExt)
}
}
// LoadConfig will return merged default config and config from file. This is main
// function to use for config loading. Configuration file is optional e.g. can be "".
func LoadConfig(configFile string) (cfg Config, err error) {
// Initialize default configuration.
cfg = loadDefaultConfig()
// Load configuration from file and override default configuration options.
if configFile != "" {
c, err := loadConfigFromFile(configFile)
if err != nil {
return cfg, err
}
// Configuration file can specify full set or partial set of configuration
// options. So we only want to override those options that have been specified in
// config file, re st will remain as per default config.
cfg.OverrideFrom(c)
}
return cfg, nil
}
func loadJSON(f string) (cfg Config, err error) {
b, err := os.ReadFile(f)
if err != nil {
return cfg, fmt.Errorf("config from JSON file: %w", err)
}
if len(b) == 0 {
return cfg, fmt.Errorf("JSON file is empty: %w", ErrInvalidConfig)
}
if err = json.Unmarshal(b, &cfg); err != nil {
return cfg, fmt.Errorf("config from JSON document: %w", err)
}
return cfg, nil
}
// In order to support Config overriding we have to implement wrapper type for Config
// fields. Otherwise it is hard to distinguish skipped fields, for instance when loading
// partial configuration from file: in that case it would be impossible to distinguish
// between say string fields zero value and empty string values as explicitly specified in
// configuration file.
// NewConfigVal is constructor for ConfigVal. It will wrap its argument into ConfigVal.
func NewConfigVal[T any](v T) ConfigVal[T] {
return ConfigVal[T]{v: &v}
}
// ConfigVal is a wrapper for Config field value.
type ConfigVal[T any] struct {
// Store wrapped value as pointer in order to have ability to distinguish between
// unspecified ConfigVal and a value that is the same as zero value for wrapped type.
// In this case a zero value for pointer is nil.
//
// For example a zero value for string is "" which is impossible to distinguish from
// explicit empty string "".
v *T
}
// Value will return wrapped value.
//
// In case field has not been defined e.g. is zero value, then appropriate zero value of
// wrapped typw will be returned.
func (o *ConfigVal[T]) Value() T {
if o.IsNil() {
var v T
return v
}
return *o.v
}
// IsNil check if wrapped value is nil.
func (o *ConfigVal[T]) IsNil() bool {
// Zero value for pointer type is nil.
return o.v == nil
}
// UnmarshalJSON implements json.Unmarshaler interface for ConfigVal.
func (o *ConfigVal[T]) UnmarshalJSON(b []byte) error {
var val T
err := json.Unmarshal(b, &val)
if err != nil {
return err
}
o.v = &val
return nil
}
// MarshalJSON implements json.Marshaler interface for ConfigVal.
func (o ConfigVal[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(o.Value())
}
func CreateDumpConfCommand() *DumpConfApp {
longHelp := `Command "dump-conf" will print actual application configuration taking into account
configuration file provided and default configuration values.
Examples:
ease dump-conf
ease dump-conf -conf path/to/config.json`
app := &DumpConfApp{
fs: flag.NewFlagSet("dump-conf", flag.ContinueOnError),
gf: globalFlags{},
out: os.Stdout,
}
app.gf.Register(app.fs)
app.fs.Usage = func() {
printSubCommandUsage(longHelp, app.fs)
}
return app
}
// Also define command "dump-conf" here.
// DumpConfApp is subcommand application context that implements Commander interface.
// Although this is very simple application, but for consistency sake is is implemented in
// similar style as other subcommands.
type DumpConfApp struct {
out io.Writer
fs *flag.FlagSet
gf globalFlags
}
// Run is main entry point into BitrateApp execution.
func (d *DumpConfApp) Run(args []string) error {
if err := d.fs.Parse(args); err != nil {
return &AppError{
exitCode: 2,
msg: "usage error",
}
}
if d.gf.Debug {
logging.EnableDebugLogger()
}
// Load application configuration.
cfg, err := LoadConfig(d.gf.ConfFile)
if err != nil {
return &AppError{exitCode: 1, msg: err.Error()}
}
enc := json.NewEncoder(d.out)
enc.SetIndent("", " ")
if err := enc.Encode(cfg); err != nil {
return &AppError{exitCode: 1, msg: err.Error()}
}
// Also, report if configuration is valid.
if err := cfg.Verify(); err != nil {
return &AppError{exitCode: 1, msg: fmt.Sprintf("configuration validation: %s", err)}
}
return nil
}