Skip to content

Commit

Permalink
ffmpeg position aware codec selection (#2533)
Browse files Browse the repository at this point in the history
* ffmpeg: position aware codec selection

* ffmpeg: added `ActionDecodableCodecs` and `ActionEncodableCodecs`

---------

Co-authored-by: rsteube <rsteube@users.noreply.github.com>
  • Loading branch information
jopejoe1 and rsteube authored Sep 21, 2024
1 parent f1f048f commit 14541ad
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 96 deletions.
207 changes: 120 additions & 87 deletions completers/ffmpeg_completer/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,103 +272,136 @@ func actionFlags() carapace.Action {
}

func actionFlagArguments(flag string) carapace.Action {
splitted := strings.Split(strings.TrimLeft(flag, "-"), ":")
switch splitted[0] {
case "ab":
return carapace.ActionValues("16", "32", "64", "128", "192", "256", "320")
case "acodec":
return carapace.Batch(
ffmpeg.ActionCodecs(ffmpeg.CodecOpts{Audio: true}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Audio: true}),
).ToA()
case "af":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
return carapace.ActionMultiParts("=", func(c carapace.Context) carapace.Action {
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
splitted := strings.Split(strings.TrimLeft(flag, "-"), ":")
beforeInput := true
for _, arg := range c.Args {
if arg == "-i" {
beforeInput = false
break
}
}

switch splitted[0] {
case "ab":
return carapace.ActionValues("16", "32", "64", "128", "192", "256", "320")
case "acodec":
if beforeInput {
return carapace.Batch(
ffmpeg.ActionDecodableCodecs(ffmpeg.CodecOpts{Audio: true}),
ffmpeg.ActionDecoders(ffmpeg.DecoderOpts{Audio: true}),
).ToA()
}
return carapace.Batch(
ffmpeg.ActionEncodableCodecs(ffmpeg.CodecOpts{Audio: true}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Audio: true}),
).ToA()
case "af":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
return carapace.ActionMultiParts("=", func(c carapace.Context) carapace.Action {
switch len(c.Parts) {
case 0:
return ffmpeg.ActionFilters().NoSpace()
default:
return carapace.ActionValues()
}
})
})
case "ar":
return carapace.ActionValues("22050", "44100", "48000")
case "b":
if len(splitted) > 1 {
switch splitted[1] {
case "a":
return carapace.ActionValues("16", "32", "64", "128", "192", "256", "320")
case "v":
return carapace.ActionValues() // video bitrate
}
}
return carapace.ActionValues() // TODO invalid flag (missing a/v))
case "c", "codec":
audio := true
subtitle := true
video := true
if len(splitted) > 1 {
audio = splitted[1] == "a"
subtitle = splitted[1] == "s"
video = splitted[1] == "v"
}
if beforeInput {
return carapace.Batch(
ffmpeg.ActionDecodableCodecs(ffmpeg.CodecOpts{Audio: audio, Subtitle: subtitle, Video: video}),
ffmpeg.ActionDecoders(ffmpeg.DecoderOpts{Audio: audio, Subtitle: subtitle, Video: video}),
).ToA()
}
return carapace.Batch(
ffmpeg.ActionEncodableCodecs(ffmpeg.CodecOpts{Audio: audio, Subtitle: subtitle, Video: video}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Audio: audio, Subtitle: subtitle, Video: video}),
).ToA()
case "f":
return ffmpeg.ActionFormats()
case "h", "?", "help":
return ffmpeg.ActionHelpTopics()
case "hwaccel":
return ffmpeg.ActionHardwareAccelerations()
case "i":
return carapace.ActionFiles()
case "loglevel":
return ffmpeg.ActionLogLevels()
case "scodec":
if beforeInput {
return carapace.Batch(
ffmpeg.ActionDecodableCodecs(ffmpeg.CodecOpts{Subtitle: true}),
ffmpeg.ActionDecoders(ffmpeg.DecoderOpts{Subtitle: true}),
).ToA()
}
return carapace.Batch(
ffmpeg.ActionEncodableCodecs(ffmpeg.CodecOpts{Subtitle: true}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Subtitle: true}),
).ToA()
case "sinks":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
switch len(c.Parts) {
case 0:
return ffmpeg.ActionFilters().NoSpace()
return ffmpeg.ActionDevices(ffmpeg.DeviceOpts{Demuxing: true}).NoSpace()
default:
return carapace.ActionValues()
}
})
})
case "ar":
return carapace.ActionValues("22050", "44100", "48000")
case "b":
if len(splitted) > 1 {
switch splitted[1] {
case "a":
return carapace.ActionValues("16", "32", "64", "128", "192", "256", "320")
case "v":
return carapace.ActionValues() // video bitrate
}
}
return carapace.ActionValues() // TODO invalid flag (missing a/v))
case "c", "codec":
audio := true
subtitle := true
video := true
if len(splitted) > 1 {
audio = splitted[1] == "a"
subtitle = splitted[1] == "s"
video = splitted[1] == "v"
}
return carapace.Batch(
ffmpeg.ActionCodecs(ffmpeg.CodecOpts{Audio: audio, Subtitle: subtitle, Video: video}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Audio: audio, Subtitle: subtitle, Video: video}),
).ToA()
case "f":
return ffmpeg.ActionFormats()
case "h", "?", "help":
return ffmpeg.ActionHelpTopics()
case "hwaccel":
return ffmpeg.ActionHardwareAccelerations()
case "i":
return carapace.ActionFiles()
case "loglevel":
return ffmpeg.ActionLogLevels()
case "scodec":
return carapace.Batch(
ffmpeg.ActionCodecs(ffmpeg.CodecOpts{Subtitle: true}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Subtitle: true}),
).ToA()
case "sinks":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
switch len(c.Parts) {
case 0:
return ffmpeg.ActionDevices(ffmpeg.DeviceOpts{Demuxing: true}).NoSpace()
default:
return carapace.ActionValues()
}
})
case "sources":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
switch len(c.Parts) {
case 0:
return ffmpeg.ActionDevices(ffmpeg.DeviceOpts{Muxing: true}).NoSpace()
default:
return carapace.ActionValues()
}
})
case "vcodec":
return carapace.Batch(
ffmpeg.ActionCodecs(ffmpeg.CodecOpts{Video: true}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Video: true}),
).ToA()

case "vf":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
return carapace.ActionMultiParts("=", func(c carapace.Context) carapace.Action {
case "sources":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
switch len(c.Parts) {
case 0:
return ffmpeg.ActionFilters().NoSpace()
return ffmpeg.ActionDevices(ffmpeg.DeviceOpts{Muxing: true}).NoSpace()
default:
return carapace.ActionValues()
}
})
})
default:
//return carapace.ActionValues() // TODO
return carapace.ActionFiles() // default file completion for now (positional)
}
case "vcodec":
if beforeInput {
return carapace.Batch(
ffmpeg.ActionDecodableCodecs(ffmpeg.CodecOpts{Video: true}),
ffmpeg.ActionDecoders(ffmpeg.DecoderOpts{Video: true}),
).ToA()
}
return carapace.Batch(
ffmpeg.ActionEncodableCodecs(ffmpeg.CodecOpts{Video: true}),
ffmpeg.ActionEncoders(ffmpeg.EncoderOpts{Video: true}),
).ToA()
case "vf":
return carapace.ActionMultiParts(",", func(c carapace.Context) carapace.Action {
return carapace.ActionMultiParts("=", func(c carapace.Context) carapace.Action {
switch len(c.Parts) {
case 0:
return ffmpeg.ActionFilters().NoSpace()
default:
return carapace.ActionValues()
}
})
})
default:
//return carapace.ActionValues() // TODO
return carapace.ActionFiles() // default file completion for now (positional)
}
})
}
61 changes: 52 additions & 9 deletions pkg/actions/tools/ffmpeg/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import (
)

type CodecOpts struct {
Audio bool
Subtitle bool
Video bool
Attachment bool
Audio bool
Data bool
Subtitle bool
Video bool
}

func (o CodecOpts) Default() CodecOpts {
o.Attachment = true
o.Audio = true
o.Data = true
o.Subtitle = true
o.Video = true
return o
Expand All @@ -26,35 +30,74 @@ func (o CodecOpts) Default() CodecOpts {
// 4gv (4GV (Fourth Generation Vocoder))
// 4xm (4X Movie)
func ActionCodecs(opts CodecOpts) carapace.Action {
return actionCodecs(opts, nil)
}

// ActionEncodableCodecs completes codecs with encoding support
//
// amv (AMV Video)
// anull (Null audio codec)
func ActionEncodableCodecs(opts CodecOpts) carapace.Action {
return actionCodecs(opts, func(s string) bool {
return s[1] != 'E'
})
}

// ActionDecodableCodecs completes codecs with decoding support
//
// avrn (Avid AVI Codec)
// avrp (Avid 1:1 10-bit RGB Packer)
func ActionDecodableCodecs(opts CodecOpts) carapace.Action {
return actionCodecs(opts, func(s string) bool {
return s[0] != 'D'
})
}

func actionCodecs(opts CodecOpts, filter func(s string) bool) carapace.Action {
return carapace.ActionExecCommand("ffmpeg", "-hide_banner", "-codecs")(func(output []byte) carapace.Action {
_, content, ok := strings.Cut(string(output), " -------")
if !ok {
return carapace.ActionMessage("failed to parse codecs")
}

lines := strings.Split(content, "\n")
r := regexp.MustCompile(`^ .{2}(?P<type>.).{3} (?P<codec>[^ ]+) +(?P<description>.*)$`)
r := regexp.MustCompile(`^ (?P<decoding>.)(?P<encoding>.)(?P<type>.).{3} (?P<codec>[^ ]+) +(?P<description>.*)$`)

vals := make([]string, 0)
for _, line := range lines[10 : len(lines)-1] {
if matches := r.FindStringSubmatch(line); matches != nil {
switch matches[1] {
if filter != nil && filter(line[1:7]) {
continue
}

switch matches[3] {
case "A":
if opts.Audio {
vals = append(vals, matches[2], matches[3], style.Yellow)
vals = append(vals, matches[4], matches[5], style.Yellow)
}
case "D":
if opts.Data {
vals = append(vals, matches[4], matches[5], style.Cyan)
}
case "S":
if opts.Subtitle {
vals = append(vals, matches[2], matches[3], style.Magenta)
vals = append(vals, matches[4], matches[5], style.Magenta)
}
case "T":
if opts.Attachment {
vals = append(vals, matches[4], matches[5], style.Green)
}
case "V":
if opts.Video {
vals = append(vals, matches[2], matches[3], style.Blue)
vals = append(vals, matches[4], matches[5], style.Blue)
}
}
}
}
vals = append(vals, "copy", "copy the codec of the input", style.Default)

if filter == nil || !filter("D ") {
vals = append(vals, "copy", "copy the codec of the input", style.Default)
}
return carapace.ActionStyledValuesDescribed(vals...)
}).Tag("codecs")
}

0 comments on commit 14541ad

Please sign in to comment.