diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb02864 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ +# Build outputs +dist/ +.vscode/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9fa3272 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ + +build-cmd: + cd cmd/go-gst && go build -o ../../dist/go-gst + +ARGS ?= +run-cmd: build-cmd + dist/go-gst $(ARGS) diff --git a/README.md b/README.md index e12bc28..0e933b7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ # go-gst -Gstreamer bindings and utilities for golang + +Go bindings for the gstreamer C library + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-rounded)](https://pkg.go.dev/github.com/tinyzimmer/go-gst/gst) +[![godoc reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-rounded)](https://godoc.org/github.com/tinyzimmer/go-gst/gst) + +This package was originally written to aid the audio support in [`kvdi`](https://github.com/tinyzimmer/kvdi). +But it made sense to turn it into an independent, consumable package. The intention now is to progressively implement the entire API. + +See the go.dev reference for documentation and examples. + +For other examples see the command line implementation [here](cmd/go-gst). + +_TODO: Write examples on programatically building the pipeline yourself_ + +## Requirements + +For building applications with this library you need the following: + + - `cgo`: You must set `CGO_ENABLED=1` in your environment when building + - `pkg-config` + - `libgstreamer-1.0-dev`: This package name may be different depending on your OS. You need the `gst.h` header files. + +For running applications with this library you'll need to have `libgstreamer-1.0` installed. Again, this package may be different depending on your OS. + + +## CLI + +There is a CLI utility included with this package that demonstrates some of the things you can do. + +For now the functionality is limitted to GIF encoing and other arbitrary pipelines. +If I extend it further I'll publish releases, but for now, you can retrieve it with `go get`. + +```bash +go get github.com/tinyzimmer/go-gst-launch/cmd/go-gst-launch +``` + +The usage is described below: + +``` +Go-gst is a CLI utility aiming to implement the core functionality +of the core gstreamer-tools. It's primary purpose is to showcase the functionality of +the underlying go-gst library. + +There are also additional commands showing some of the things you can do with the library, +such as websocket servers reading/writing to/from local audio servers and audio/video/image +encoders/decoders. + +Usage: + go-gst [command] + +Available Commands: + completion Generate completion script + gif Encodes the given video to GIF format + help Help about any command + inspect Inspect the elements of the given pipeline string + launch Run a generic pipeline + websocket Run a websocket audio proxy for streaming audio from a pulse server + and optionally recording to a virtual mic. + +Flags: + -I, --from-stdin Write to the pipeline from stdin. If this is specified, then -i is ignored. + -h, --help help for go-gst + -i, --input string An input file, defaults to the first element in the pipeline. + -o, --output string An output file, defaults to the last element in the pipeline. + -O, --to-stdout Writes the results from the pipeline to stdout. If this is specified, then -o is ignored. + -v, --verbose Verbose output. This is ignored when used with --to-stdout. + +Use "go-gst [command] --help" for more information about a command. +``` \ No newline at end of file diff --git a/cmd/go-gst/colors.go b/cmd/go-gst/colors.go new file mode 100644 index 0000000..2b4657c --- /dev/null +++ b/cmd/go-gst/colors.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "io" + "strings" +) + +type color string + +var ( + colorReset color = "\033[0m" + colorBlack color = "\033[0;30m" + colorRed color = "\033[0;31m" + colorGreen color = "\033[0;32m" + colorOrange color = "\033[0;33m" + colorBlue color = "\033[0;34m" + colorPurple color = "\033[0;35m" + colorCyan color = "\033[0;36m" + colorLightGray color = "\033[0;37m" + colorDarkGray color = "\033[1;30m" + colorLightRed color = "\033[1;31m" + colorLightGreen color = "\033[1;32m" + colorYellow color = "\033[1;33m" + colorLightBlue color = "\033[1;34m" + colorLightPurple color = "\033[1;35m" + colorLightCyan color = "\033[1;36m" + colorWhite color = "\033[1;37m" +) + +func disableColor() { + colorReset = "" + colorBlack = "" + colorRed = "" + colorGreen = "" + colorOrange = "" + colorBlue = "" + colorPurple = "" + colorCyan = "" + colorLightGray = "" + colorDarkGray = "" + colorLightRed = "" + colorLightGreen = "" + colorYellow = "" + colorLightBlue = "" + colorLightPurple = "" + colorLightCyan = "" + colorWhite = "" +} + +func (c color) print(s string) { fmt.Printf("%s%s%s", c, s, colorReset) } +func (c color) printIndent(i int, s string) { c.print(fmt.Sprintf("%s%s", strings.Repeat(" ", i), s)) } +func (c color) printf(f string, args ...interface{}) { c.print(fmt.Sprintf(f, args...)) } +func (c color) printfIndent(i int, f string, args ...interface{}) { + c.printf(fmt.Sprintf("%s%s", strings.Repeat(" ", i), fmt.Sprintf(f, args...))) +} +func (c color) fprint(w io.Writer, s string) { fmt.Fprintf(w, fmt.Sprintf("%s%s%s", c, s, colorReset)) } +func (c color) fprintf(w io.Writer, f string, args ...interface{}) { + c.fprint(w, fmt.Sprintf(f, args...)) +} +func (c color) fprintIndent(w io.Writer, i int, s string) { + c.fprint(w, fmt.Sprintf("%s%s", strings.Repeat(" ", i), s)) +} +func (c color) fprintfIndent(w io.Writer, i int, f string, args ...interface{}) { + c.fprint(w, fmt.Sprintf("%s%s", strings.Repeat(" ", i), fmt.Sprintf(f, args...))) +} diff --git a/cmd/go-gst/completion.go b/cmd/go-gst/completion.go new file mode 100644 index 0000000..b80434b --- /dev/null +++ b/cmd/go-gst/completion.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(completionCmd) +} + +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: `To load completions: + +Bash: + +$ source <(yourprogram completion bash) + +# To load completions for each session, execute once: +Linux: + $ yourprogram completion bash > /etc/bash_completion.d/yourprogram +MacOS: + $ yourprogram completion bash > /usr/local/etc/bash_completion.d/yourprogram + +Zsh: + +# If shell completion is not already enabled in your environment you will need +# to enable it. You can execute the following once: + +$ echo "autoload -U compinit; compinit" >> ~/.zshrc + +# To load completions for each session, execute once: +$ yourprogram completion zsh > "${fpath[1]}/_yourprogram" + +# You will need to start a new shell for this setup to take effect. + +Fish: + +$ yourprogram completion fish | source + +# To load completions for each session, execute once: +$ yourprogram completion fish > ~/.config/fish/completions/yourprogram.fish +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.ExactValidArgs(1), + Run: func(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + cmd.Root().GenPowerShellCompletion(os.Stdout) + } + }, +} diff --git a/cmd/go-gst/gif.go b/cmd/go-gst/gif.go new file mode 100644 index 0000000..53ffb6f --- /dev/null +++ b/cmd/go-gst/gif.go @@ -0,0 +1,144 @@ +package main + +import ( + "errors" + "fmt" + "image" + "image/color/palette" + "image/gif" + "image/jpeg" + "image/png" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/tinyzimmer/go-gst-launch/gst" + + "github.com/spf13/cobra" +) + +var framesPerSecond int +var imageFormat string + +func init() { + gifCmd.PersistentFlags().IntVarP(&framesPerSecond, "frame-rate", "r", 10, "The number of frames per-second to encode into the GIF") + gifCmd.PersistentFlags().StringVarP(&imageFormat, "format", "f", "png", "The image format to encode frames to") + + rootCmd.AddCommand(gifCmd) +} + +var gifCmd = &cobra.Command{ + Use: "gif", + Short: "Encodes the given video to GIF format", + Long: `Look at the available options to change the compression levels and format. + +Requires libav be installed.`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if srcFile == "" && !fromStdin { + return errors.New("No input provided") + } + if destFile == "" && !toStdout { + return errors.New("No output provided") + } + return nil + }, + RunE: gifEncode, +} + +func gifEncode(cmd *cobra.Command, args []string) error { + + var imageEncoder string + var decoder func(io.Reader) (image.Image, error) + switch strings.ToLower(imageFormat) { + case "png": + imageEncoder = "pngenc" + decoder = png.Decode + case "jpg": + imageEncoder = "jpegenc" + decoder = jpeg.Decode + case "jpeg": + imageEncoder = "jpegenc" + decoder = jpeg.Decode + default: + return fmt.Errorf("Invalid image format %s: Valid options [ png | jpg ]", strings.ToLower(imageFormat)) + } + + tmpDir, err := ioutil.TempDir("", "") + defer os.RemoveAll(tmpDir) + + launchStr := fmt.Sprintf( + `filesrc location=%s ! decodebin ! videoconvert ! videoscale ! videorate ! video/x-raw,framerate=%d/1 ! %s ! multifilesink location="%s/%%04d.%s"`, + srcFile, framesPerSecond, imageEncoder, tmpDir, strings.ToLower(imageFormat), + ) + + logInfo("gif", "Converting video to image frames") + + gstPipeline, err := gst.NewPipelineFromLaunchString(launchStr, gst.PipelineInternalOnly) + if err != nil { + return err + } + defer gstPipeline.Close() + + if err := gstPipeline.Start(); err != nil { + return err + } + + if verbose { + setupVerbosePipelineListeners(gstPipeline, "gif") + } + + gst.Wait(gstPipeline) + + logInfo("gif", "Building output gif:", destFile) + + dest, err := getDestFile() + if err != nil { + return err + } + + files, err := ioutil.ReadDir(tmpDir) + if err != nil { + return err + } + + outGif := &gif.GIF{ + Image: make([]*image.Paletted, 0), + Delay: make([]int, 0), + } + + numFrames := len(files) + for idx, file := range files { + if !toStdout { + fmt.Printf("\rEncoding frame %d/%d", idx, numFrames) + } + f, err := os.Open(filepath.Join(tmpDir, file.Name())) + if err != nil { + return err + } + img, err := decoder(f) + if err != nil { + return err + } + frame := image.NewPaletted(img.Bounds(), palette.Plan9) + for x := 1; x <= img.Bounds().Dx(); x++ { + for y := 1; y <= img.Bounds().Dy(); y++ { + frame.Set(x, y, img.At(x, y)) + } + } + outGif.Image = append(outGif.Image, frame) + outGif.Delay = append(outGif.Delay, 0) + } + + if err := gif.EncodeAll(dest, outGif); err != nil { + return err + } + + if !toStdout { + fmt.Println() + } + + logInfo("gif", "If the command reached this state and you see a GStreamer-CRITICAL error, you can ignore it") + return nil +} diff --git a/cmd/go-gst/inspect.go b/cmd/go-gst/inspect.go new file mode 100644 index 0000000..44b7f61 --- /dev/null +++ b/cmd/go-gst/inspect.go @@ -0,0 +1,325 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "strings" + "text/tabwriter" + + "github.com/gotk3/gotk3/glib" + "github.com/spf13/cobra" + "github.com/tinyzimmer/go-gst-launch/gst" +) + +func init() { + rootCmd.AddCommand(inspectCmd) +} + +var inspectCmd = &cobra.Command{ + Use: "inspect", + Short: "Inspect the elements of the given pipeline string", + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("You must specify an object to inspect") + } + return nil + }, + RunE: inspect, +} + +func inspect(cmd *cobra.Command, args []string) error { + + name := args[0] + + // load the registry + registry := gst.GetRegistry() + // get the factory for the element + factory := gst.Find(name) + + if factory == nil { + return errors.New("Could not get details for factory") + } + defer factory.Unref() + + // assume it's an element for now, can implement more later + elem, err := gst.NewElement(name) + if err != nil { + return err + } + defer elem.Unref() + + var maxLevel int + + // dump info about the element + + printFactoryDetails(registry, factory) + printPluginDetails(registry, factory) + printHierarchy(elem.TypeFromInstance(), 0, &maxLevel) + printInterfaces(elem) + printPadTemplates(elem) + printClockingInfo(elem) + printURIHandlerInfo(elem) + printPadInfo(elem) + printElementPropertiesInfo(elem) + printSignalInfo(elem) + printChildrenInfo(elem) + printPresentList(elem) + + return nil +} + +func printFactoryDetails(registry *gst.Registry, factory *gst.ElementFactory) { + + // initialize tabwriter + w := new(tabwriter.Writer) + buf := new(bytes.Buffer) + + w.Init( + buf, + 40, // minwidth + 30, // tabwith + 0, // padding + ' ', // padchar + 0, // flags + ) + + colorOrange.fprint(w, "Factory Details:\n") + for _, key := range factory.GetMetadataKeys() { + colorBlue.fprintfIndent(w, 2, "%s \t ", strings.Title(key)) + colorLightGray.fprint(w, factory.GetMetadata(key)) + colorReset.fprint(w, "\n") + } + + w.Flush() + fmt.Print(buf.String()) + fmt.Println() +} + +func printPluginDetails(registry *gst.Registry, factory *gst.ElementFactory) { + + // initialize tabwriter + w := new(tabwriter.Writer) + buf := new(bytes.Buffer) + + w.Init( + buf, + 40, // minwidth + 30, // tabwith + 0, // padding + ' ', // padchar + 0, // flags + ) + + pluginFeature, err := registry.LookupFeature(factory.Name()) + if err != nil { + return + } + plugin := pluginFeature.GetPlugin() + if plugin == nil { + return + } + + defer pluginFeature.Unref() + defer plugin.Unref() + + colorOrange.fprint(w, "Plugin Details:\n") + + colorBlue.fprintIndent(w, 2, "Name \t ") + colorLightGray.fprintf(w, "%s\n", pluginFeature.GetPluginName()) + + colorBlue.fprintIndent(w, 2, "Description \t ") + colorLightGray.fprintf(w, "%s\n", plugin.Description()) + + colorBlue.fprintIndent(w, 2, "Filename \t ") + colorLightGray.fprintf(w, "%s\n", plugin.Filename()) + + colorBlue.fprintIndent(w, 2, "Version \t ") + colorLightGray.fprintf(w, "%s\n", plugin.Version()) + + colorBlue.fprintIndent(w, 2, "License \t ") + colorLightGray.fprintf(w, "%s\n", plugin.License()) + + colorBlue.fprintIndent(w, 2, "Source module \t ") + colorLightGray.fprintf(w, "%s\n", plugin.Source()) + + colorBlue.fprintIndent(w, 2, "Binary package \t ") + colorLightGray.fprintf(w, "%s\n", plugin.Package()) + + colorBlue.fprintIndent(w, 2, "Origin URLs \t ") + colorLightGray.fprintf(w, "%s\n", plugin.Origin()) + + w.Flush() + fmt.Print(buf.String()) + + fmt.Println() +} + +func printHierarchy(gtype glib.Type, level int, maxLevel *int) { + parent := gtype.Parent() + + *maxLevel = *maxLevel + 1 + level++ + + if parent > 0 { + printHierarchy(parent, level, maxLevel) + } + + for i := 1; i < *maxLevel-level; i++ { + colorReset.print(" ") + } + + if *maxLevel-level > 0 { + colorLightPurple.print(" +----") + } + + colorGreen.printf("%s\n", gtype.Name()) + +} + +func printInterfaces(elem *gst.Element) { + fmt.Println() + + if ifaces := elem.Interfaces(); len(ifaces) > 0 { + colorOrange.print("Implemented Interfaces:") + for _, iface := range ifaces { + colorGreen.printfIndent(2, "%s\n", iface) + } + } +} + +func printPadTemplates(elem *gst.Element) { + fmt.Println() + + tmpls := elem.GetPadTemplates() + if len(tmpls) == 0 { + return + } + colorOrange.print("Pad templates:\n") + for _, tmpl := range tmpls { + colorBlue.printfIndent(2, "%s template", strings.ToUpper(tmpl.Name())) + colorReset.print(": ") + colorBlue.printf("'%s'\n", strings.ToLower(tmpl.Direction().String())) + + colorBlue.printIndent(4, "Availability") + colorReset.print(": ") + colorLightGray.print(strings.Title(tmpl.Presence().String())) + colorReset.print("\n") + + colorBlue.printIndent(4, "Capabilities") + colorReset.print(": ") + + caps := tmpl.Caps() + if len(caps) == 0 { + colorOrange.printIndent(6, "ANY") + } else { + printCaps(&caps, 6) + } + } + fmt.Println() + fmt.Println() +} + +func printClockingInfo(elem *gst.Element) { + if !elem.Has(gst.ElementFlagRequireClock) && !elem.Has(gst.ElementFlagProvideClock) { + colorLightGray.print("Element has no clocking capabilities.\n") + return + } + fmt.Printf("%sClocking Interactions:%s\n", colorOrange, colorReset) + + if elem.Has(gst.ElementFlagRequireClock) { + colorLightGray.printIndent(2, "element requires a clock\n") + } + + if elem.Has(gst.ElementFlagProvideClock) { + clock := elem.GetClock() + if clock == nil { + colorLightGray.printIndent(2, "selement is supposed to provide a clock but returned NULL%s\n") + } else { + defer clock.Unref() + colorLightGray.printIndent(2, "element provides a clock: ") + colorCyan.printf(clock.Name()) + } + } + + fmt.Println() +} + +func printURIHandlerInfo(elem *gst.Element) { + if !elem.IsURIHandler() { + colorLightGray.print("Element has no URI handling capabilities.\n") + fmt.Println() + } + + fmt.Println() + colorOrange.print("URI handling capabilities:\n") + colorLightGray.printfIndent(2, "Element can act as %s.\n", strings.ToLower(elem.GetURIType().String())) + + protos := elem.GetURIProtocols() + + if len(protos) == 0 { + fmt.Println() + return + } + + colorLightGray.printIndent(2, "Supported URI protocols:\n") + + for _, proto := range protos { + colorCyan.printfIndent(4, "%s\n", proto) + } + + fmt.Println() +} + +func printPadInfo(elem *gst.Element) { + + colorOrange.print("Pads:\n") + pads := elem.GetPads() + if len(pads) == 0 { + colorCyan.printIndent(2, "none\n") + return + } + for _, pad := range elem.GetPads() { + defer pad.Unref() + + colorBlue.printIndent(2, strings.ToUpper(pad.Direction().String())) + colorReset.print(": ") + colorLightGray.printf("'%s'\n", pad.Name()) + + if tmpl := pad.Template(); tmpl != nil { + defer tmpl.Unref() + colorBlue.printIndent(4, "Pad Template") + colorReset.print(": ") + colorLightGray.printf("'%s'\n", tmpl.Name()) + } + + if caps := pad.CurrentCaps(); caps != nil { + colorBlue.printIndent(2, "Capabilities:\n") + printCaps(&caps, 4) + } + } + + fmt.Println() +} + +func printElementPropertiesInfo(elem *gst.Element) { + printObjectPropertiesInfo(elem.Object, "Element Properties") +} + +func printSignalInfo(elem *gst.Element) {} +func printChildrenInfo(elem *gst.Element) {} +func printPresentList(elem *gst.Element) {} + +func printCaps(caps *gst.Caps, indent int) { + for _, cap := range *caps { + colorReset.print("\n") + colorOrange.printfIndent(indent, "%s", cap.Name) + for k, v := range cap.Data { + colorReset.print("\n") + colorOrange.printfIndent(indent+2, "%s", k) + colorReset.print(": ") + colorLightGray.print(fmt.Sprint(v)) + } + } + fmt.Println() +} diff --git a/cmd/go-gst/launch.go b/cmd/go-gst/launch.go new file mode 100644 index 0000000..83736fd --- /dev/null +++ b/cmd/go-gst/launch.go @@ -0,0 +1,73 @@ +package main + +import ( + "errors" + "io" + "strings" + + "github.com/spf13/cobra" + "github.com/tinyzimmer/go-gst-launch/gst" +) + +func init() { + rootCmd.AddCommand(launchCmd) +} + +var launchCmd = &cobra.Command{ + Use: "launch", + Short: "Run a generic pipeline", + Long: `Uses the provided pipeline string to encode/decode the data in the pipeline.`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("The pipeline string cannot be empty") + } + return nil + }, + RunE: launch, +} + +func launch(cmd *cobra.Command, args []string) error { + + src, dest, err := getCLIFiles() + if err != nil { + return err + } + + var flags gst.PipelineFlags + if src != nil { + flags = flags | gst.PipelineWrite + } + if dest != nil { + flags = flags | gst.PipelineRead + } + + pipelineString := strings.Join(args, " ") + + logInfo("pipeline", "Creating pipeline") + gstPipeline, err := gst.NewPipelineFromLaunchString(pipelineString, flags) + if err != nil { + return err + } + + defer gstPipeline.Close() + + if verbose { + setupVerbosePipelineListeners(gstPipeline, "pipeline") + } + + logInfo("pipeline", "Starting pipeline") + if err := gstPipeline.Start(); err != nil { + return err + } + + if src != nil { + go io.Copy(gstPipeline, src) + } + if dest != nil { + go io.Copy(dest, gstPipeline) + } + + gst.Wait(gstPipeline) + + return nil +} diff --git a/cmd/go-gst/log.go b/cmd/go-gst/log.go new file mode 100644 index 0000000..9460a0c --- /dev/null +++ b/cmd/go-gst/log.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "log" +) + +func logInfo(name string, args ...interface{}) { + nArgs := []interface{}{fmt.Sprintf("[%s]", name)} + nArgs = append(nArgs, args...) + if !toStdout { + log.Println(nArgs...) + } +} diff --git a/cmd/go-gst/main.go b/cmd/go-gst/main.go new file mode 100644 index 0000000..c3c4c34 --- /dev/null +++ b/cmd/go-gst/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "log" + + "github.com/spf13/cobra" + "github.com/tinyzimmer/go-gst-launch/gst" +) + +var ( + srcFile, destFile, pipelineStr string + verbose, fromStdin, toStdout bool + + rootCmd = &cobra.Command{ + Use: "go-gst", + Short: "A command-line audio/video encoder and decoder based on gstreamer", + Long: `Go-gst is a CLI utility aiming to implement the core functionality +of the core gstreamer-tools. It's primary purpose is to showcase the functionality of +the underlying go-gst library. + +There are also additional commands showing some of the things you can do with the library, +such as websocket servers reading/writing to/from local audio servers and audio/video/image +encoders/decoders. +`, + } +) + +func init() { + gst.Init() + + rootCmd.PersistentFlags().StringVarP(&srcFile, "input", "i", "", "An input file, defaults to the first element in the pipeline.") + rootCmd.PersistentFlags().StringVarP(&destFile, "output", "o", "", "An output file, defaults to the last element in the pipeline.") + rootCmd.PersistentFlags().BoolVarP(&fromStdin, "from-stdin", "I", false, "Write to the pipeline from stdin. If this is specified, then -i is ignored.") + rootCmd.PersistentFlags().BoolVarP(&toStdout, "to-stdout", "O", false, "Writes the results from the pipeline to stdout. If this is specified, then -o is ignored.") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output. This is ignored when used with --to-stdout.") +} + +// Execute executes the root command. +func Execute() error { + return rootCmd.Execute() +} + +func main() { + if err := Execute(); err != nil { + log.Println("ERROR:", err.Error()) + } +} diff --git a/cmd/go-gst/print_object_properties.go b/cmd/go-gst/print_object_properties.go new file mode 100644 index 0000000..4c63d36 --- /dev/null +++ b/cmd/go-gst/print_object_properties.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + + "github.com/gotk3/gotk3/glib" + "github.com/tinyzimmer/go-gst-launch/gst" +) + +func printFieldType(s string) { + colorGreen.printIndent(24, s) +} + +func printFieldName(s string) { + colorOrange.print(s) + colorReset.print(": ") +} + +func printFieldValue(s string) { + colorCyan.print(s) +} + +func printObjectPropertiesInfo(obj *gst.Object, description string) { + colorOrange.printf("%s:\n", description) + + // for now this function only handles elements + + for _, param := range obj.ListProperties() { + colorBlue.printfIndent(2, "%-20s", param.Name) + colorReset.printf(": %s", param.Blurb) + + colorReset.print("\n") + + colorOrange.printIndent(24, "flags") + colorReset.print(": ") + colorCyan.print(param.Flags.GstFlagsString()) + + colorReset.print("\n") + + switch param.ValueType { + + case glib.TYPE_STRING: + printFieldType("String. ") + printFieldName("Default") + if param.DefaultValue == nil { + printFieldValue("null") + } else { + str, _ := param.DefaultValue.GetString() + printFieldValue(str) + } + + case glib.TYPE_BOOLEAN: + val, err := param.DefaultValue.GoValue() + var valStr string + if err != nil { + valStr = "unknown" // edge case + } else { + b := val.(bool) + valStr = fmt.Sprintf("%t", b) + } + printFieldType("Boolean. ") + printFieldName("Default") + printFieldValue(valStr) + + case glib.TYPE_ULONG: + printFieldType("Unsigned Long. ") + + case glib.TYPE_LONG: + printFieldType("Long. ") + + case glib.TYPE_UINT: + printFieldType("Unsigned Integer. ") + + case glib.TYPE_INT: + printFieldType("Integer. ") + + case glib.TYPE_UINT64: + printFieldType("Unsigned Integer64. ") + + case glib.TYPE_INT64: + printFieldType("Integer64. ") + + case glib.TYPE_FLOAT: + printFieldType("Float. ") + + case glib.TYPE_DOUBLE: + printFieldType("Double. ") + + default: + + } + + colorReset.print("\n") + + } + + fmt.Println() +} diff --git a/cmd/go-gst/util.go b/cmd/go-gst/util.go new file mode 100644 index 0000000..750e600 --- /dev/null +++ b/cmd/go-gst/util.go @@ -0,0 +1,98 @@ +package main + +import ( + "os" + + "github.com/tinyzimmer/go-gst-launch/gst" +) + +func getSrcFile() (*os.File, error) { + if fromStdin || srcFile != "" { + if fromStdin { + if verbose { + logInfo("file", "Reading media data from stdin") + } + return os.Stdin, nil + } + if verbose { + logInfo("file", "Reading media data from", srcFile) + } + return os.Open(srcFile) + } + // Commands should do internal checking before calling this command + return nil, nil +} + +func getDestFile() (*os.File, error) { + if toStdout || destFile != "" { + if toStdout { + if verbose { + logInfo("file", "Writing media output to stdout") + } + return os.Stdout, nil + } + if verbose { + logInfo("file", "Writing media output to", destFile) + } + return os.Create(destFile) + } + // Commands should do internal checking before calling this command + return nil, nil +} + +func getCLIFiles() (src, dest *os.File, err error) { + src, err = getSrcFile() + if err != nil { + return nil, nil, err + } + dest, err = getDestFile() + if err != nil { + src.Close() + return nil, nil, err + } + // Commands should do internal checking before calling this command + return src, dest, nil +} + +func setupVerbosePipelineListeners(gstPipeline *gst.Pipeline, name string) { + logInfo(name, "Starting message listeners") + go func() { + var currentState gst.State + for msg := range gstPipeline.GetBus().MessageChan() { + + defer msg.Unref() + + switch msg.Type() { + + case gst.MessageStreamStart: + logInfo(name, "Stream has started") + case gst.MessageEOS: + logInfo(name, "Stream has ended") + case gst.MessageStateChanged: + if currentState != gstPipeline.GetState() { + logInfo(name, "New pipeline state:", gstPipeline.GetState().String()) + currentState = gstPipeline.GetState() + } + case gst.MessageInfo: + info := msg.ParseInfo() + logInfo(name, info.Message()) + for k, v := range info.Details() { + logInfo(name, k, ":", v) + } + case gst.MessageWarning: + info := msg.ParseWarning() + logInfo(name, "WARNING:", info.Message()) + for k, v := range info.Details() { + logInfo(name, k, ":", v) + } + + case gst.MessageError: + err := msg.ParseError() + logInfo(name, "ERROR:", err.Error()) + logInfo(name, "DEBUG:", err.DebugString()) + + } + } + }() + +} diff --git a/cmd/go-gst/websocket.go b/cmd/go-gst/websocket.go new file mode 100644 index 0000000..007001e --- /dev/null +++ b/cmd/go-gst/websocket.go @@ -0,0 +1,308 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net/http" + "os/exec" + "os/user" + "strings" + + "github.com/spf13/cobra" + "github.com/tinyzimmer/go-gst-launch/gst" + "golang.org/x/net/websocket" +) + +var ( + websocketHost string + websocketPort int + pulseServer, pulseMonitor, encoding, micName, micFifo, micFormat string + micSampleRate, micChannels int +) + +func init() { + user, err := user.Current() + var defaultPulseServer, defaultPulseMonitor string + if err == nil { + defaultPulseServer = fmt.Sprintf("/run/user/%s/pulse/native", user.Uid) + } + defaultMonitor, err := exec.Command("/bin/sh", "-c", "pactl list sources | grep Name | head -n1 | cut -d ' ' -f2").Output() + if err == nil { + defaultPulseMonitor = strings.TrimSpace(string(defaultMonitor)) + } + websocketCmd.PersistentFlags().StringVarP(&websocketHost, "host", "H", "127.0.0.1", "The host to listen on for websocket connections.") + websocketCmd.PersistentFlags().IntVarP(&websocketPort, "port", "P", 8080, "The port to listen on for websocket connections.") + websocketCmd.PersistentFlags().StringVarP(&pulseServer, "pulse-server", "p", defaultPulseServer, "The path to the PulseAudio socket.") + websocketCmd.PersistentFlags().StringVarP(&pulseMonitor, "pulse-monitor", "d", defaultPulseMonitor, "The monitor device to connect to on the Pulse server. The default device is selected if omitted.") + websocketCmd.PersistentFlags().StringVarP(&encoding, "encoding", "e", "", `The audio encoding to send to websocket connections. The options are: + + opus (default) + Serves audio data in webm/opus. + The MediaSource can consume this format by specifying "audio/webm". + + vorbis + Serves audio data in ogg/vorbis. +`) + websocketCmd.PersistentFlags().StringVarP(&micFifo, "mic-path", "m", "", "A mic FIFO to write received audio data to, by default, nothing is done with received data.") + websocketCmd.PersistentFlags().StringVarP(&micName, "mic-name", "n", "virtmic", "The name of the mic fifo device in pulse audio.") + websocketCmd.PersistentFlags().StringVarP(&micFormat, "mic-format", "f", "S16LE", "The audio format pulse audio expects on the fifo.") + websocketCmd.PersistentFlags().IntVarP(&micSampleRate, "mic-sample-rate", "r", 16000, "The sample rate pulse audio expects on the fifo.") + websocketCmd.PersistentFlags().IntVarP(&micChannels, "mic-channels", "c", 1, "The number of channels pulse audio expects on the fifo.") + + rootCmd.AddCommand(websocketCmd) +} + +var websocketCmd = &cobra.Command{ + Use: "websocket", + Short: `Run a websocket audio proxy for streaming audio from a pulse server + and optionally recording to a virtual mic.`, + Long: `Starts a websocket server with the given configurations. + +This currently only works with PulseAudio or an input file, but may be expanded to be cross-platform. + +This command may be expanded to include video support via RFB or RTP. + +To use with the mic support, you should first set up a virtual device with something like: + + pactl load-module module-pipe-source source_name=virtmic file=/tmp/mic.fifo format=s16le rate=16000 channels=1 + +And then you can run this command with --mic-path /tmp/mic.fifo. The received data will be expected to +be in the same format as specified with --encoding. +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if pulseServer == "" { + return errors.New("Could not determine pulse server, you should use --pulse-server") + } + switch encoding { + case "opus": + case "vorbis": + case "": + encoding = "opus" + default: + return fmt.Errorf("Not a valid audio encoder: %s", encoding) + } + return nil + }, + RunE: websocketProxy, +} + +func websocketProxy(cmd *cobra.Command, args []string) error { + addr := fmt.Sprintf("%s:%d", websocketHost, websocketPort) + server := &http.Server{ + Handler: &websocket.Server{ + Handshake: func(*websocket.Config, *http.Request) error { return nil }, + Handler: handleWebsocketConnection, + }, + Addr: addr, + } + logInfo("websocket", "Listening on", addr) + return server.ListenAndServe() +} + +func handleWebsocketConnection(wsconn *websocket.Conn) { + defer func() { + wsconn.Close() + logInfo("websocket", "Connection to", wsconn.Request().RemoteAddr, "closed") + }() + + logInfo("websocket", "New connection from", wsconn.Request().RemoteAddr) + wsconn.PayloadType = websocket.BinaryFrame + + var playbackPipeline, recordingPipeline, sinkPipeline *gst.Pipeline + var err error + + playbackPipeline, err = newPlaybackPipeline() + if err != nil { + logInfo("websocket", "ERROR:", err.Error()) + return + } + + playbackPipeline.SetAutoFlush(true) + + logInfo("websocket", "Starting playback pipeline") + + if err = playbackPipeline.Start(); err != nil { + logInfo("websocket", "ERROR:", err.Error()) + return + } + + if verbose { + setupVerbosePipelineListeners(playbackPipeline, "playback") + } + + if micFifo != "" { + recordingPipeline, err = newRecordingPipeline() + if err != nil { + logInfo("websocket", "Could not open pipeline for recording:", err.Error()) + return + } + defer recordingPipeline.Close() + sinkPipeline, err = newSinkPipeline() + if err != nil { + logInfo("websocket", "Could not open null sink pipeling. Disabling recording.") + return + } + defer sinkPipeline.Close() + } + + if recordingPipeline != nil && sinkPipeline != nil { + logInfo("websocket", "Starting recording pipeline") + if err = recordingPipeline.Start(); err != nil { + logInfo("websocket", "Could not start recording pipeline") + return + } + logInfo("websocket", "Starting sink pipeline") + if err = sinkPipeline.Start(); err != nil { + logInfo("websocket", "Could not start sink pipeline") + return + } + + if verbose { + setupVerbosePipelineListeners(sinkPipeline, "mic-null-sink") + } + + var runMicFunc func() + runMicFunc = func() { + if verbose { + setupVerbosePipelineListeners(recordingPipeline, "recorder") + } + go io.Copy(recordingPipeline, wsconn) + go func() { + var lastState gst.State + for msg := range recordingPipeline.GetBus().MessageChan() { + defer msg.Unref() + switch msg.Type() { + case gst.MessageStateChanged: + if lastState == gst.StatePlaying && recordingPipeline.GetState() != gst.StatePlaying { + var nerr error + recordingPipeline.Close() + recordingPipeline, nerr = newRecordingPipeline() + if nerr != nil { + logInfo("websocket", "Could not create new recording pipeline, stopping input stream") + return + } + logInfo("websocket", "Restarting recording pipeline") + if nerr = recordingPipeline.Start(); nerr != nil { + logInfo("websocket", "Could not start new recording pipeline, stopping input stream") + } + runMicFunc() + return + } + lastState = recordingPipeline.GetState() + } + } + }() + } + runMicFunc() + } + + go func() { + io.Copy(wsconn, playbackPipeline) + playbackPipeline.Close() + }() + + if srcFile != "" { + srcFile, err := getSrcFile() + if err != nil { + return + } + defer srcFile.Close() + stat, err := srcFile.Stat() + if err != nil { + return + } + appSrc := playbackPipeline.GetAppSrc() + appSrc.SetSize(stat.Size()) + appSrc.PushBuffer(srcFile) + for { + if ret := appSrc.EndStream(); ret == gst.FlowOK { + break + } + } + + } + + gst.Wait(playbackPipeline) +} + +func newPlaybackPipelineFromString() (*gst.Pipeline, error) { + pipelineString := "decodebin ! audioconvert ! audioresample" + + switch encoding { + case "opus": + pipelineString = fmt.Sprintf("%s ! cutter ! opusenc ! webmmux", pipelineString) + case "vorbis": + pipelineString = fmt.Sprintf("%s ! vorbisenc ! oggmux", pipelineString) + } + + if verbose { + logInfo("playback", "Using pipeline string", pipelineString) + } + + return gst.NewPipelineFromLaunchString(pipelineString, gst.PipelineReadWrite|gst.PipelineUseGstApp) +} + +func newPlaybackPipeline() (*gst.Pipeline, error) { + + if srcFile != "" { + return newPlaybackPipelineFromString() + } + + cfg := &gst.PipelineConfig{Elements: []*gst.PipelineElement{}} + + pulseSrc := &gst.PipelineElement{ + Name: "pulsesrc", + Data: map[string]interface{}{"server": pulseServer}, + SinkCaps: gst.NewRawCaps("S16LE", 24000, 2), + } + + if pulseMonitor != "" { + pulseSrc.Data["device"] = pulseMonitor + } + + cfg.Elements = append(cfg.Elements, pulseSrc) + + switch encoding { + case "opus": + cfg.Elements = append(cfg.Elements, &gst.PipelineElement{Name: "cutter"}) + cfg.Elements = append(cfg.Elements, &gst.PipelineElement{Name: "opusenc"}) + cfg.Elements = append(cfg.Elements, &gst.PipelineElement{Name: "webmmux"}) + case "vorbis": + cfg.Elements = append(cfg.Elements, &gst.PipelineElement{Name: "vorbisenc"}) + cfg.Elements = append(cfg.Elements, &gst.PipelineElement{Name: "oggmux"}) + } + + return gst.NewPipelineFromConfig(cfg, gst.PipelineRead|gst.PipelineUseGstApp, nil) +} + +func newRecordingPipeline() (*gst.Pipeline, error) { + return gst.NewPipelineFromLaunchString(newPipelineStringFromOpts(), gst.PipelineWrite) +} + +func newPipelineStringFromOpts() string { + return fmt.Sprintf( + "decodebin ! audioconvert ! audioresample ! audio/x-raw, format=%s, rate=%d, channels=%d ! filesink location=%s append=true", + micFormat, + micSampleRate, + micChannels, + micFifo, + ) +} + +func newSinkPipeline() (*gst.Pipeline, error) { + cfg := &gst.PipelineConfig{ + Elements: []*gst.PipelineElement{ + { + Name: "pulsesrc", + Data: map[string]interface{}{"server": pulseServer, "device": micName}, + SinkCaps: gst.NewRawCaps(micFormat, micSampleRate, micChannels), + }, + { + Name: "fakesink", + Data: map[string]interface{}{"sync": false}, + }, + }, + } + return gst.NewPipelineFromConfig(cfg, gst.PipelineInternalOnly, nil) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3a2fb02 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/tinyzimmer/go-gst-launch + +go 1.15 + +require ( + github.com/gotk3/gotk3 v0.4.0 + github.com/spf13/cobra v1.0.0 + golang.org/x/net v0.0.0-20190522155817-f3200d17e092 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..38ec652 --- /dev/null +++ b/go.sum @@ -0,0 +1,130 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gotk3/gotk3 v0.4.0 h1:TIuhyQitGeRTxOQIV3AJlYtEWWJpC74JHwAIsxlH8MU= +github.com/gotk3/gotk3 v0.4.0/go.mod h1:Eew3QBwAOBTrfFFDmsDE5wZWbcagBL1NUslj1GhRveo= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/gst/c_util.go b/gst/c_util.go new file mode 100644 index 0000000..f975c20 --- /dev/null +++ b/gst/c_util.go @@ -0,0 +1,97 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +*/ +import "C" +import ( + "errors" + "unsafe" +) + +// Init runs `gst_init`. It currently does not support arguments. This should +// be called before building any pipelines. +func Init() { + C.gst_init(nil, nil) +} + +// gobool provides an easy type conversion between C.gboolean and a go bool. +func gobool(b C.gboolean) bool { + return b != 0 +} + +// gboolean converts a go bool to a C.gboolean. +func gboolean(b bool) C.gboolean { + if b { + return C.gboolean(1) + } + return C.gboolean(0) +} + +// structureToGoMap converts a GstStructure into a Go map of strings. +func structureToGoMap(st *C.GstStructure) map[string]string { + goDetails := make(map[string]string) + numFields := int(C.gst_structure_n_fields((*C.GstStructure)(st))) + for i := 0; i < numFields-1; i++ { + fieldName := C.gst_structure_nth_field_name((*C.GstStructure)(st), (C.guint)(i)) + fieldValue := C.gst_structure_get_value((*C.GstStructure)(st), (*C.gchar)(fieldName)) + strValueDup := C.g_strdup_value_contents((*C.GValue)(fieldValue)) + goDetails[C.GoString(fieldName)] = C.GoString(strValueDup) + } + return goDetails +} + +// MessageType is an alias to the C equivalent of GstMessageType. +type MessageType C.GstMessageType + +// Type casting of GstMessageTypes +const ( + MessageAny MessageType = C.GST_MESSAGE_ANY + MessageStreamStart = C.GST_MESSAGE_STREAM_START + MessageEOS = C.GST_MESSAGE_EOS + MessageInfo = C.GST_MESSAGE_INFO + MessageWarning = C.GST_MESSAGE_WARNING + MessageError = C.GST_MESSAGE_ERROR + MessageStateChanged = C.GST_MESSAGE_STATE_CHANGED + MessageElement = C.GST_MESSAGE_ELEMENT + MessageStreamStatus = C.GST_MESSAGE_STREAM_STATUS + MessageBuffering = C.GST_MESSAGE_BUFFERING + MessageLatency = C.GST_MESSAGE_LATENCY + MessageNewClock = C.GST_MESSAGE_NEW_CLOCK + MessageAsyncDone = C.GST_MESSAGE_ASYNC_DONE + MessageTag = C.GST_MESSAGE_TAG +) + +func iteratorToElementSlice(iterator *C.GstIterator) ([]*Element, error) { + elems := make([]*Element, 0) + gval := new(C.GValue) + + for { + switch C.gst_iterator_next((*C.GstIterator)(iterator), (*C.GValue)(unsafe.Pointer(gval))) { + case C.GST_ITERATOR_DONE: + C.gst_iterator_free((*C.GstIterator)(iterator)) + return elems, nil + case C.GST_ITERATOR_RESYNC: + C.gst_iterator_resync((*C.GstIterator)(iterator)) + case C.GST_ITERATOR_OK: + cElemVoid := C.g_value_get_object((*C.GValue)(gval)) + cElem := (*C.GstElement)(cElemVoid) + elems = append(elems, wrapElement(cElem)) + C.g_value_reset((*C.GValue)(gval)) + default: + return nil, errors.New("Element iterator failed") + } + } +} + +func goStrings(argc C.int, argv **C.gchar) []string { + length := int(argc) + tmpslice := (*[1 << 30]*C.gchar)(unsafe.Pointer(argv))[:length:length] + gostrings := make([]string, length) + for i, s := range tmpslice { + gostrings[i] = C.GoString(s) + } + return gostrings +} diff --git a/gst/doc.go b/gst/doc.go new file mode 100644 index 0000000..61dfbea --- /dev/null +++ b/gst/doc.go @@ -0,0 +1,141 @@ +/* +Package gst provides wrappers for building gstreamer pipelines and then +reading and/or writing from either end of the pipeline. + +It uses cgo to interface with the gstreamer-1.0 C API. + +A simple opus/webm encoder created from a launch string could look like this: + + import ( + "os" + "github.com/tinyzimmer/go-gst-launch/gst" + ) + + func main() { + gst.Init() + encoder, err := gst.NewPipelineFromLaunchString("opusenc ! webmmux", gst.PipelineReadWrite) + if err != nil { + panic(err) + } + + // You should close even if you don't start the pipeline, since this + // will free resources created by gstreamer. + defer encoder.Close() + + if err := encoder.Start() ; err != nil { + panic(err) + } + + go func() { + encoder.Write(...) // Write raw audio data to the pipeline + }() + + // don't actually do this - copy encoded audio to stdout + if _, err := io.Copy(os.Stdout, encoder) ; err != nil { + panic(err) + } + } + +You can accomplish the same thing using the "configuration" functionality provided by NewPipelineFromConfig(). +Here is an example that will record from a pulse server and make opus/webm data available on the Reader. + + import ( + "io" + "os" + "github.com/tinyzimmer/go-gst-launch/gst" + ) + + func main() { + gst.Init() + encoder, err := gst.NewPipelineFromConfig(&gst.PipelineConfig{ + Plugins: []*gst.Plugin{ + { + Name: "pulsesrc", + Data: map[string]interface{}{ + "server": "/run/user/1000/pulse/native", + "device": "playback-device.monitor", + }, + SinkCaps: gst.NewRawCaps("S16LE", 24000, 2), + }, + { + Name: "opusenc", + }, + { + Name: "webmmux", + }, + }, + }, gst.PipelineRead, nil) + if err != nil { + panic(err) + } + + defer encoder.Close() + + if err := encoder.Start() ; err != nil { + panic(err) + } + + // Create an output file + f, err := os.Create("out.opus") + if err != nil { + panic(err) + } + + // Copy the data from the pipeline to the file + if err := io.Copy(f, encoder) ; err != nil { + panic(err) + } + + } + + +There are two channels exported for listening for messages from the pipeline. +An example of listening to messages on a fake pipeline for 10 seconds: + + package main + + import ( + "fmt" + "time" + + "github.com/tinyzimmer/go-gst-launch/gst" + ) + + func main() { + gst.Init() + + pipeline, err := gst.NewPipelineFromLaunchString("audiotestsrc ! fakesink", gst.PipelineInternalOnly) + if err != nil { + panic(err) + } + + defer pipeline.Close() + + go func() { + for msg := range pipeline.MessageChan() { + fmt.Println("Got message:", msg.TypeName()) + } + }() + + go func() { + for msg := range pipeline.ErrorChan() { + fmt.Println("Got error:", err) + } + }() + + if err := pipeline.Start(); err != nil { + fmt.Println("Pipeline failed to start") + return + } + + time.Sleep(time.Second * 10) + } + + +The package also exposes some low level functionality for building pipelines +and doing dynamic linking yourself. See the NewPipeline() function for creating an +empty pipeline that you can then build out using the other structs and methods provided +by this package. + +*/ +package gst diff --git a/gst/gst.go.h b/gst/gst.go.h new file mode 100644 index 0000000..d738552 --- /dev/null +++ b/gst/gst.go.h @@ -0,0 +1,506 @@ +#include +#include +#include + +/* + Utilitits +*/ +static GObjectClass * +getGObjectClass(void * p) { + return G_OBJECT_GET_CLASS (p); +} + +static int +sizeOfGCharArray(gchar ** arr) { + int i; + for (i = 0 ; 1 ; i = i + 1) { + if (arr[i] == NULL) { return i; }; + } +} + +static gboolean +gstObjectFlagIsSet(GstObject * obj, GstElementFlags flags) +{ + return GST_OBJECT_FLAG_IS_SET (obj, flags); +} + +static gboolean +gstElementIsURIHandler(GstElement * elem) +{ + return GST_IS_URI_HANDLER (elem); +} + +/* + Type Castings +*/ + +static GstUri * +toGstURI(void *p) +{ + return (GST_URI(p)); +} + +static GstURIHandler * +toGstURIHandler(void *p) +{ + return (GST_URI_HANDLER(p)); +} + +static GstRegistry * +toGstRegistry(void *p) +{ + return (GST_REGISTRY(p)); +} + +static GstPlugin * +toGstPlugin(void *p) +{ + return (GST_PLUGIN(p)); +} + +static GstPluginFeature * +toGstPluginFeature(void *p) +{ + return (GST_PLUGIN_FEATURE(p)); +} + +static GstObject * +toGstObject(void *p) +{ + return (GST_OBJECT(p)); +} + +static GstElementFactory * +toGstElementFactory(void *p) +{ + return (GST_ELEMENT_FACTORY(p)); +} + +static GstElement * +toGstElement(void *p) +{ + return (GST_ELEMENT(p)); +} + +static GstAppSink * +toGstAppSink(void *p) +{ + return (GST_APP_SINK(p)); +} + +static GstAppSrc * +toGstAppSrc(void *p) +{ + return (GST_APP_SRC(p)); +} + +static GstBin * +toGstBin(void *p) +{ + return (GST_BIN(p)); +} + +static GstBus * +toGstBus(void *p) +{ + return (GST_BUS(p)); +} + +static GstMessage * +toGstMessage(void *p) +{ + return (GST_MESSAGE(p)); +} + +static GstPipeline * +toGstPipeline(void *p) +{ + return (GST_PIPELINE(p)); +} + +static GstPad * +toGstPad(void *p) +{ + return (GST_PAD(p)); +} + +static GstPadTemplate * +toGstPadTemplate(void *p) +{ + return (GST_PAD_TEMPLATE(p)); +} + +static GstStructure * +toGstStructure(void *p) +{ + return (GST_STRUCTURE(p)); +} + +static GstClock * +toGstClock(void *p) +{ + return (GST_CLOCK(p)); +} + +// /* obj will be NULL if we're printing properties of pad template pads */ +// static void +// print_object_properties_info (GObject * obj, GObjectClass * obj_class, +// const gchar * desc) +// { +// GParamSpec **property_specs; +// guint num_properties, i; +// gboolean readable; +// gboolean first_flag; + +// property_specs = g_object_class_list_properties (obj_class, &num_properties); +// g_qsort_with_data (property_specs, num_properties, sizeof (gpointer), +// (GCompareDataFunc) sort_gparamspecs, NULL); + +// n_print ("%s%s%s:\n", HEADING_COLOR, desc, RESET_COLOR); + +// push_indent (); + +// for (i = 0; i < num_properties; i++) { +// GValue value = { 0, }; +// GParamSpec *param = property_specs[i]; +// GType owner_type = param->owner_type; + +// /* We're printing pad properties */ +// if (obj == NULL && (owner_type == G_TYPE_OBJECT +// || owner_type == GST_TYPE_OBJECT || owner_type == GST_TYPE_PAD)) +// continue; + +// g_value_init (&value, param->value_type); + +// n_print ("%s%-20s%s: %s%s%s\n", PROP_NAME_COLOR, +// g_param_spec_get_name (param), RESET_COLOR, PROP_VALUE_COLOR, +// g_param_spec_get_blurb (param), RESET_COLOR); + +// push_indent_n (11); + +// first_flag = TRUE; +// n_print ("%sflags%s: ", PROP_ATTR_NAME_COLOR, RESET_COLOR); +// readable = ! !(param->flags & G_PARAM_READABLE); +// if (readable && obj != NULL) { +// g_object_get_property (obj, param->name, &value); +// } else { +// /* if we can't read the property value, assume it's set to the default +// * (which might not be entirely true for sub-classes, but that's an +// * unlikely corner-case anyway) */ +// g_param_value_set_default (param, &value); +// } +// if (readable) { +// g_print ("%s%s%s%s", (first_flag) ? "" : ", ", PROP_ATTR_VALUE_COLOR, +// _("readable"), RESET_COLOR); +// first_flag = FALSE; +// } +// if (param->flags & G_PARAM_WRITABLE) { +// g_print ("%s%s%s%s", (first_flag) ? "" : ", ", PROP_ATTR_VALUE_COLOR, +// _("writable"), RESET_COLOR); +// first_flag = FALSE; +// } +// if (param->flags & G_PARAM_DEPRECATED) { +// g_print ("%s%s%s%s", (first_flag) ? "" : ", ", PROP_ATTR_VALUE_COLOR, +// _("deprecated"), RESET_COLOR); +// first_flag = FALSE; +// } +// if (param->flags & GST_PARAM_CONTROLLABLE) { +// g_print (", %s%s%s", PROP_ATTR_VALUE_COLOR, _("controllable"), +// RESET_COLOR); +// first_flag = FALSE; +// } +// if (param->flags & GST_PARAM_CONDITIONALLY_AVAILABLE) { +// g_print (", %s%s%s", PROP_ATTR_VALUE_COLOR, _("conditionally available"), +// RESET_COLOR); +// first_flag = FALSE; +// } +// if (param->flags & GST_PARAM_MUTABLE_PLAYING) { +// g_print (", %s%s%s", PROP_ATTR_VALUE_COLOR, +// _("changeable in NULL, READY, PAUSED or PLAYING state"), RESET_COLOR); +// } else if (param->flags & GST_PARAM_MUTABLE_PAUSED) { +// g_print (", %s%s%s", PROP_ATTR_VALUE_COLOR, +// _("changeable only in NULL, READY or PAUSED state"), RESET_COLOR); +// } else if (param->flags & GST_PARAM_MUTABLE_READY) { +// g_print (", %s%s%s", PROP_ATTR_VALUE_COLOR, +// _("changeable only in NULL or READY state"), RESET_COLOR); +// } +// if (param->flags & ~KNOWN_PARAM_FLAGS) { +// g_print ("%s0x%s%0x%s", (first_flag) ? "" : ", ", PROP_ATTR_VALUE_COLOR, +// param->flags & ~KNOWN_PARAM_FLAGS, RESET_COLOR); +// } +// g_print ("\n"); + +// switch (G_VALUE_TYPE (&value)) { +// case G_TYPE_STRING: +// { +// const char *string_val = g_value_get_string (&value); + +// n_print ("%sString%s. ", DATATYPE_COLOR, RESET_COLOR); + +// if (string_val == NULL) +// g_print ("%sDefault%s: %snull%s", PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, RESET_COLOR); +// else +// g_print ("%sDefault%s: %s\"%s\"%s", PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, string_val, RESET_COLOR); +// break; +// } +// case G_TYPE_BOOLEAN: +// { +// gboolean bool_val = g_value_get_boolean (&value); + +// n_print ("%sBoolean%s. %sDefault%s: %s%s%s", DATATYPE_COLOR, +// RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, bool_val ? "true" : "false", RESET_COLOR); +// break; +// } +// case G_TYPE_ULONG: +// { +// GParamSpecULong *pulong = G_PARAM_SPEC_ULONG (param); + +// n_print +// ("%sUnsigned Long%s. %sRange%s: %s%lu - %lu%s %sDefault%s: %s%lu%s ", +// DATATYPE_COLOR, RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, pulong->minimum, pulong->maximum, +// RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, g_value_get_ulong (&value), RESET_COLOR); + +// GST_ERROR ("%s: property '%s' of type ulong: consider changing to " +// "uint/uint64", G_OBJECT_CLASS_NAME (obj_class), +// g_param_spec_get_name (param)); +// break; +// } +// case G_TYPE_LONG: +// { +// GParamSpecLong *plong = G_PARAM_SPEC_LONG (param); + +// n_print ("%sLong%s. %sRange%s: %s%ld - %ld%s %sDefault%s: %s%ld%s ", +// DATATYPE_COLOR, RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, plong->minimum, plong->maximum, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// g_value_get_long (&value), RESET_COLOR); + +// GST_ERROR ("%s: property '%s' of type long: consider changing to " +// "int/int64", G_OBJECT_CLASS_NAME (obj_class), +// g_param_spec_get_name (param)); +// break; +// } +// case G_TYPE_UINT: +// { +// GParamSpecUInt *puint = G_PARAM_SPEC_UINT (param); + +// n_print +// ("%sUnsigned Integer%s. %sRange%s: %s%u - %u%s %sDefault%s: %s%u%s ", +// DATATYPE_COLOR, RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, puint->minimum, puint->maximum, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// g_value_get_uint (&value), RESET_COLOR); +// break; +// } +// case G_TYPE_INT: +// { +// GParamSpecInt *pint = G_PARAM_SPEC_INT (param); + +// n_print ("%sInteger%s. %sRange%s: %s%d - %d%s %sDefault%s: %s%d%s ", +// DATATYPE_COLOR, RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, pint->minimum, pint->maximum, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// g_value_get_int (&value), RESET_COLOR); +// break; +// } +// case G_TYPE_UINT64: +// { +// GParamSpecUInt64 *puint64 = G_PARAM_SPEC_UINT64 (param); + +// n_print ("%sUnsigned Integer64%s. %sRange%s: %s%" G_GUINT64_FORMAT " - " +// "%" G_GUINT64_FORMAT "%s %sDefault%s: %s%" G_GUINT64_FORMAT "%s ", +// DATATYPE_COLOR, RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, puint64->minimum, puint64->maximum, +// RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, g_value_get_uint64 (&value), RESET_COLOR); +// break; +// } +// case G_TYPE_INT64: +// { +// GParamSpecInt64 *pint64 = G_PARAM_SPEC_INT64 (param); + +// n_print ("%sInteger64%s. %sRange%s: %s%" G_GINT64_FORMAT " - %" +// G_GINT64_FORMAT "%s %sDefault%s: %s%" G_GINT64_FORMAT "%s ", +// DATATYPE_COLOR, RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, pint64->minimum, pint64->maximum, +// RESET_COLOR, PROP_ATTR_NAME_COLOR, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, g_value_get_int64 (&value), RESET_COLOR); +// break; +// } +// case G_TYPE_FLOAT: +// { +// GParamSpecFloat *pfloat = G_PARAM_SPEC_FLOAT (param); + +// n_print ("%sFloat%s. %sRange%s: %s%15.7g - %15.7g%s " +// "%sDefault%s: %s%15.7g%s ", DATATYPE_COLOR, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// pfloat->minimum, pfloat->maximum, RESET_COLOR, PROP_ATTR_NAME_COLOR, +// RESET_COLOR, PROP_ATTR_VALUE_COLOR, g_value_get_float (&value), +// RESET_COLOR); +// break; +// } +// case G_TYPE_DOUBLE: +// { +// GParamSpecDouble *pdouble = G_PARAM_SPEC_DOUBLE (param); + +// n_print ("%sDouble%s. %sRange%s: %s%15.7g - %15.7g%s " +// "%sDefault%s: %s%15.7g%s ", DATATYPE_COLOR, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// pdouble->minimum, pdouble->maximum, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// g_value_get_double (&value), RESET_COLOR); +// break; +// } +// case G_TYPE_CHAR: +// case G_TYPE_UCHAR: +// GST_ERROR ("%s: property '%s' of type char: consider changing to " +// "int/string", G_OBJECT_CLASS_NAME (obj_class), +// g_param_spec_get_name (param)); +// /* fall through */ +// default: +// if (param->value_type == GST_TYPE_CAPS) { +// const GstCaps *caps = gst_value_get_caps (&value); + +// if (!caps) +// n_print ("%sCaps%s (NULL)", DATATYPE_COLOR, RESET_COLOR); +// else { +// print_caps (caps, " "); +// } +// } else if (G_IS_PARAM_SPEC_ENUM (param)) { +// GEnumValue *values; +// guint j = 0; +// gint enum_value; +// const gchar *value_nick = ""; + +// values = G_ENUM_CLASS (g_type_class_ref (param->value_type))->values; +// enum_value = g_value_get_enum (&value); + +// while (values[j].value_name) { +// if (values[j].value == enum_value) +// value_nick = values[j].value_nick; +// j++; +// } + +// n_print ("%sEnum \"%s\"%s %sDefault%s: %s%d, \"%s\"%s", +// DATATYPE_COLOR, g_type_name (G_VALUE_TYPE (&value)), RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// enum_value, value_nick, RESET_COLOR); + +// j = 0; +// while (values[j].value_name) { +// g_print ("\n"); +// n_print (" %s(%d)%s: %s%-16s%s - %s%s%s", +// PROP_ATTR_NAME_COLOR, values[j].value, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, values[j].value_nick, RESET_COLOR, +// DESC_COLOR, values[j].value_name, RESET_COLOR); +// j++; +// } +// /* g_type_class_unref (ec); */ +// } else if (G_IS_PARAM_SPEC_FLAGS (param)) { +// GParamSpecFlags *pflags = G_PARAM_SPEC_FLAGS (param); +// GFlagsValue *vals; +// gchar *cur; + +// vals = pflags->flags_class->values; + +// cur = flags_to_string (vals, g_value_get_flags (&value)); + +// n_print ("%sFlags \"%s\"%s %sDefault%s: %s0x%08x, \"%s\"%s", +// DATATYPE_COLOR, g_type_name (G_VALUE_TYPE (&value)), RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// g_value_get_flags (&value), cur, RESET_COLOR); + +// while (vals[0].value_name) { +// g_print ("\n"); +// n_print (" %s(0x%08x)%s: %s%-16s%s - %s%s%s", +// PROP_ATTR_NAME_COLOR, vals[0].value, RESET_COLOR, +// PROP_ATTR_VALUE_COLOR, vals[0].value_nick, RESET_COLOR, +// DESC_COLOR, vals[0].value_name, RESET_COLOR); +// ++vals; +// } + +// g_free (cur); +// } else if (G_IS_PARAM_SPEC_OBJECT (param)) { +// n_print ("%sObject of type%s %s\"%s\"%s", PROP_VALUE_COLOR, +// RESET_COLOR, DATATYPE_COLOR, +// g_type_name (param->value_type), RESET_COLOR); +// } else if (G_IS_PARAM_SPEC_BOXED (param)) { +// n_print ("%sBoxed pointer of type%s %s\"%s\"%s", PROP_VALUE_COLOR, +// RESET_COLOR, DATATYPE_COLOR, +// g_type_name (param->value_type), RESET_COLOR); +// if (param->value_type == GST_TYPE_STRUCTURE) { +// const GstStructure *s = gst_value_get_structure (&value); +// if (s) { +// g_print ("\n"); +// gst_structure_foreach (s, print_field, +// (gpointer) " "); +// } +// } +// } else if (G_IS_PARAM_SPEC_POINTER (param)) { +// if (param->value_type != G_TYPE_POINTER) { +// n_print ("%sPointer of type%s %s\"%s\"%s.", PROP_VALUE_COLOR, +// RESET_COLOR, DATATYPE_COLOR, g_type_name (param->value_type), +// RESET_COLOR); +// } else { +// n_print ("%sPointer.%s", PROP_VALUE_COLOR, RESET_COLOR); +// } +// } else if (param->value_type == G_TYPE_VALUE_ARRAY) { +// GParamSpecValueArray *pvarray = G_PARAM_SPEC_VALUE_ARRAY (param); + +// if (pvarray->element_spec) { +// n_print ("%sArray of GValues of type%s %s\"%s\"%s", +// PROP_VALUE_COLOR, RESET_COLOR, DATATYPE_COLOR, +// g_type_name (pvarray->element_spec->value_type), RESET_COLOR); +// } else { +// n_print ("%sArray of GValues%s", PROP_VALUE_COLOR, RESET_COLOR); +// } +// } else if (GST_IS_PARAM_SPEC_FRACTION (param)) { +// GstParamSpecFraction *pfraction = GST_PARAM_SPEC_FRACTION (param); + +// n_print ("%sFraction%s. %sRange%s: %s%d/%d - %d/%d%s " +// "%sDefault%s: %s%d/%d%s ", DATATYPE_COLOR, RESET_COLOR, +// PROP_ATTR_NAME_COLOR, RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// pfraction->min_num, pfraction->min_den, pfraction->max_num, +// pfraction->max_den, RESET_COLOR, PROP_ATTR_NAME_COLOR, +// RESET_COLOR, PROP_ATTR_VALUE_COLOR, +// gst_value_get_fraction_numerator (&value), +// gst_value_get_fraction_denominator (&value), RESET_COLOR); +// } else if (param->value_type == GST_TYPE_ARRAY) { +// GstParamSpecArray *parray = GST_PARAM_SPEC_ARRAY_LIST (param); + +// if (parray->element_spec) { +// n_print ("%sGstValueArray of GValues of type%s %s\"%s\"%s", +// PROP_VALUE_COLOR, RESET_COLOR, DATATYPE_COLOR, +// g_type_name (parray->element_spec->value_type), RESET_COLOR); +// } else { +// n_print ("%sGstValueArray of GValues%s", PROP_VALUE_COLOR, +// RESET_COLOR); +// } +// } else { +// n_print ("%sUnknown type %ld%s %s\"%s\"%s", PROP_VALUE_COLOR, +// (glong) param->value_type, RESET_COLOR, DATATYPE_COLOR, +// g_type_name (param->value_type), RESET_COLOR); +// } +// break; +// } +// if (!readable) +// g_print (" %sWrite only%s\n", PROP_VALUE_COLOR, RESET_COLOR); +// else +// g_print ("\n"); + +// pop_indent_n (11); + +// g_value_reset (&value); +// } +// if (num_properties == 0) +// n_print ("%snone%s\n", PROP_VALUE_COLOR, RESET_COLOR); + +// pop_indent (); + +// g_free (property_specs); +// } diff --git a/gst/gst_app_sink.go b/gst/gst_app_sink.go new file mode 100644 index 0000000..4bd2633 --- /dev/null +++ b/gst/gst_app_sink.go @@ -0,0 +1,100 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -Wno-unused-function -g +#include +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "bytes" + "errors" + "io" + "unsafe" +) + +// AppSink wraps an Element object with additional methods for pulling samples. +type AppSink struct{ *Element } + +// NewAppSink returns a new appsink element. Unref after usage. +func NewAppSink() (*AppSink, error) { + elem, err := NewElement("appsink") + if err != nil { + return nil, err + } + return wrapAppSink(elem), nil +} + +// Instance returns the native GstAppSink instance. +func (a *AppSink) Instance() *C.GstAppSink { return C.toGstAppSink(a.unsafe()) } + +// ErrEOS represents that the stream has ended. +var ErrEOS = errors.New("Pipeline has reached end-of-stream") + +// IsEOS returns true if this AppSink has reached the end-of-stream. +func (a *AppSink) IsEOS() bool { + return gobool(C.gst_app_sink_is_eos((*C.GstAppSink)(a.Instance()))) +} + +// BlockPullSample will block until a sample becomes available or the stream +// is ended. +func (a *AppSink) BlockPullSample() (*Sample, error) { + for { + if a.IsEOS() { + return nil, ErrEOS + } + // This function won't block if the entire pipeline is waiting for data + sample := C.gst_app_sink_pull_sample((*C.GstAppSink)(a.Instance())) + if sample == nil { + continue + } + return NewSample(sample), nil + } +} + +// PullSample will try to pull a sample or return nil if none is available. +func (a *AppSink) PullSample() (*Sample, error) { + if a.IsEOS() { + return nil, ErrEOS + } + sample := C.gst_app_sink_try_pull_sample( + (*C.GstAppSink)(a.Instance()), + C.GST_SECOND, + ) + if sample != nil { + return NewSample(sample), nil + } + return nil, nil +} + +// Sample is a go wrapper around a GstSample object. +type Sample struct { + sample *C.GstSample +} + +// NewSample creates a new Sample from the given *GstSample. +func NewSample(sample *C.GstSample) *Sample { return &Sample{sample: sample} } + +// Instance returns the underlying *GstSample instance. +func (s *Sample) Instance() *C.GstSample { return s.sample } + +// Unref calls gst_sample_unref on the sample. +func (s *Sample) Unref() { C.gst_sample_unref((*C.GstSample)(s.Instance())) } + +// GetBuffer returns a Reader for the buffer inside this sample. +func (s *Sample) GetBuffer() io.Reader { + buffer := C.gst_sample_get_buffer((*C.GstSample)(s.Instance())) + var mapInfo C.GstMapInfo + C.gst_buffer_map( + (*C.GstBuffer)(buffer), + (*C.GstMapInfo)(unsafe.Pointer(&mapInfo)), + C.GST_MAP_READ, + ) + defer C.gst_buffer_unmap((*C.GstBuffer)(buffer), (*C.GstMapInfo)(unsafe.Pointer(&mapInfo))) + return bytes.NewBuffer(C.GoBytes(unsafe.Pointer(mapInfo.data), (C.int)(mapInfo.size))) +} + +func wrapAppSink(elem *Element) *AppSink { return &AppSink{elem} } diff --git a/gst/gst_app_src.go b/gst/gst_app_src.go new file mode 100644 index 0000000..2298428 --- /dev/null +++ b/gst/gst_app_src.go @@ -0,0 +1,84 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include +#include "gst.go.h" +*/ +import "C" +import ( + "io" + "io/ioutil" + "time" + "unsafe" +) + +// AppSrc wraps an Element object with additional methods for pushing samples. +type AppSrc struct{ *Element } + +// NewAppSrc returns a new AppSrc element. +func NewAppSrc() (*AppSrc, error) { + elem, err := NewElement("appsrc") + if err != nil { + return nil, err + } + return wrapAppSrc(elem), nil +} + +// Instance returns the native GstAppSink instance. +func (a *AppSrc) Instance() *C.GstAppSrc { return C.toGstAppSrc(a.unsafe()) } + +// SetSize sets the size of the source stream in bytes. You should call this for +// streams of fixed length. +func (a *AppSrc) SetSize(size int64) { + C.gst_app_src_set_size((*C.GstAppSrc)(a.Instance()), (C.gint64)(size)) +} + +// SetDuration sets the duration of the source stream. You should call +// this if the value is known. +func (a *AppSrc) SetDuration(dur time.Duration) { + C.gst_app_src_set_duration((*C.GstAppSrc)(a.Instance()), (C.ulong)(dur.Nanoseconds())) +} + +// EndStream signals to the app source that the stream has ended after the last queued +// buffer. +func (a *AppSrc) EndStream() FlowReturn { + ret := C.gst_app_src_end_of_stream((*C.GstAppSrc)(a.Instance())) + return FlowReturn(ret) +} + +// SetLive sets whether or not this is a live stream. +func (a *AppSrc) SetLive(b bool) error { return a.Set("is-live", b) } + +// PushBuffer pushes a buffer to the appsrc. Currently by default this will block +// until the source is ready to accept the buffer. +func (a *AppSrc) PushBuffer(data io.Reader) FlowReturn { + out, err := ioutil.ReadAll(data) + if err != nil { + return FlowError + } + str := string(out) + p := unsafe.Pointer(C.CString(str)) + defer C.free(p) + buf := C.gst_buffer_new_wrapped((C.gpointer)(p), C.ulong(len(str))) + ret := C.gst_app_src_push_buffer((*C.GstAppSrc)(a.Instance()), (*C.GstBuffer)(buf)) + return FlowReturn(ret) +} + +// FlowReturn is go type casting for GstFlowReturn. +type FlowReturn C.GstFlowReturn + +// Type casting of the GstFlowReturn types. Custom ones are omitted for now. +const ( + FlowOK FlowReturn = C.GST_FLOW_OK // Data passing was ok + FlowNotLinked = C.GST_FLOW_NOT_LINKED // Pad is not linked + FlowFlushing = C.GST_FLOW_FLUSHING // Pad is flushing + FlowEOS = C.GST_FLOW_EOS // Pad is EOS + FlowNotNegotiated = C.GST_FLOW_NOT_NEGOTIATED // Pad is not negotiated + FlowError = C.GST_FLOW_ERROR // Some (fatal) error occurred + FlowNotSupported = C.GST_FLOW_NOT_SUPPORTED // The operation is not supported. +) + +func wrapAppSrc(elem *Element) *AppSrc { return &AppSrc{elem} } diff --git a/gst/gst_bin.go b/gst/gst_bin.go new file mode 100644 index 0000000..afd3b6d --- /dev/null +++ b/gst/gst_bin.go @@ -0,0 +1,72 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +// Bin is a go wrapper arounds a GstBin. +type Bin struct{ *Element } + +// Instance returns the underlying GstBin instance. +func (b *Bin) Instance() *C.GstBin { return C.toGstBin(b.unsafe()) } + +// GetElementByName returns the element with the given name. Unref after usage. +func (b *Bin) GetElementByName(name string) (*Element, error) { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + elem := C.gst_bin_get_by_name((*C.GstBin)(b.Instance()), (*C.gchar)(cName)) + if elem == nil { + return nil, fmt.Errorf("Could not find element with name %s", name) + } + return wrapElement(elem), nil +} + +// GetElements returns a list of the elements added to this pipeline. Unref +// elements after usage. +func (b *Bin) GetElements() ([]*Element, error) { + iterator := C.gst_bin_iterate_elements((*C.GstBin)(b.Instance())) + return iteratorToElementSlice(iterator) +} + +// GetSourceElements returns a list of all the source elements in this pipeline. Unref +// elements after usafe. +func (b *Bin) GetSourceElements() ([]*Element, error) { + iterator := C.gst_bin_iterate_sources((*C.GstBin)(b.Instance())) + return iteratorToElementSlice(iterator) +} + +// GetSinkElements returns a list of all the sink elements in this pipeline. Unref +// elements after usage. +func (b *Bin) GetSinkElements() ([]*Element, error) { + iterator := C.gst_bin_iterate_sinks((*C.GstBin)(b.Instance())) + return iteratorToElementSlice(iterator) +} + +// Add wraps `gst_bin_add`. +func (b *Bin) Add(elem *Element) error { + if ok := C.gst_bin_add((*C.GstBin)(b.Instance()), (*C.GstElement)(elem.Instance())); !gobool(ok) { + return fmt.Errorf("Failed to add element to pipeline: %s", elem.Name()) + } + return nil +} + +// AddMany is a go implementation of `gst_bin_add_many` to compensate for the inability +// to use variadic functions in cgo. +func (b *Bin) AddMany(elems ...*Element) error { + for _, elem := range elems { + if err := b.Add(elem); err != nil { + return err + } + } + return nil +} + +func wrapBin(bin *C.GstBin) *Bin { return &Bin{wrapElement(&bin.element)} } diff --git a/gst/gst_bus.go b/gst/gst_bus.go new file mode 100644 index 0000000..8a6fc54 --- /dev/null +++ b/gst/gst_bus.go @@ -0,0 +1,78 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +import "sync" + +// Bus is a Go wrapper around a GstBus. It provides convenience methods for +// popping messages from the queue. +type Bus struct { + *Object + + msgChannels []chan *Message + mux sync.Mutex +} + +// Instance returns the underlying GstBus instance. +func (b *Bus) Instance() *C.GstBus { return C.toGstBus(b.unsafe()) } + +func (b *Bus) deliverMessages() { + for { + msg := b.BlockPopMessage() + if msg == nil { + return + } + b.mux.Lock() + for _, ch := range b.msgChannels { + ch <- msg.Ref() + } + b.mux.Unlock() + msg.Unref() + } +} + +// MessageChan returns a new channel to listen for messages asynchronously. Messages +// should be unreffed after each usage. Messages are delivered to channels in the +// order in which this function was called. +// +// While a message is being delivered to created channels, there is a lock on creating +// new ones. +func (b *Bus) MessageChan() chan *Message { + b.mux.Lock() + defer b.mux.Unlock() + ch := make(chan *Message) + b.msgChannels = append(b.msgChannels, ch) + if len(b.msgChannels) == 1 { + go b.deliverMessages() + } + return ch +} + +// BlockPopMessage blocks until a message is available on the bus and then returns it. +// This function can return nil if the bus is closed. The message should be unreffed +// after usage. +func (b *Bus) BlockPopMessage() *Message { + // I think this is ok since no other main loop is running + msg := C.gst_bus_poll( + (*C.GstBus)(b.Instance()), + C.GST_MESSAGE_ANY, + C.GST_CLOCK_TIME_NONE, + ) + if msg == nil { + return nil + } + return wrapMessage(msg) +} + +func wrapBus(bus *C.GstBus) *Bus { + return &Bus{ + Object: wrapObject(&bus.object), + msgChannels: make([]chan *Message, 0), + } +} diff --git a/gst/gst_caps.go b/gst/gst_caps.go new file mode 100644 index 0000000..78ac5df --- /dev/null +++ b/gst/gst_caps.go @@ -0,0 +1,106 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "fmt" + "strings" + "unsafe" + + "github.com/gotk3/gotk3/glib" +) + +// Caps is a wrapper around GstCaps. It provides a function for easy type +// conversion. +type Caps []*Structure + +// NewRawCaps returns new GstCaps with the given format, sample-rate, and channels. +func NewRawCaps(format string, rate, channels int) Caps { + return Caps{ + { + Name: "audio/x-raw", + Data: map[string]interface{}{ + "format": format, + "rate": rate, + "channels": channels, + }, + }, + } +} + +// FromGstCaps converts a C GstCaps objects to a go type. +func FromGstCaps(caps *C.GstCaps) Caps { + out := make(Caps, 0) + size := int(C.gst_caps_get_size((*C.GstCaps)(caps))) + for i := 0; i < size-1; i++ { + s := C.gst_caps_get_structure((*C.GstCaps)(caps), (C.guint(i))) + out = append(out, FromGstStructure(s)) + } + return out +} + +// ToGstCaps returns the GstCaps representation of this Caps instance. +func (g Caps) ToGstCaps() *C.GstCaps { + // create a new empty caps object + caps := C.gst_caps_new_empty() + if caps == nil { + // extra nil check but this would only happen when larger issues are present + return nil + } + for _, st := range g { + // append the structure to the caps + C.gst_caps_append_structure((*C.GstCaps)(caps), (*C.GstStructure)(st.ToGstStructure())) + } + return caps +} + +// Structure is a go implementation of a C GstStructure. +type Structure struct { + Name string + Data map[string]interface{} +} + +// ToGstStructure converts this structure to a C GstStructure. +func (s *Structure) ToGstStructure() *C.GstStructure { + var structStr string + structStr = s.Name + // build a structure string from the data + if s.Data != nil { + elems := make([]string, 0) + for k, v := range s.Data { + elems = append(elems, fmt.Sprintf("%s=%v", k, v)) + } + structStr = fmt.Sprintf("%s, %s", s.Name, strings.Join(elems, ", ")) + } + // convert the structure string to a cstring + cstr := C.CString(structStr) + defer C.free(unsafe.Pointer(cstr)) + // a small buffer for garbage + p := C.malloc(C.size_t(128)) + defer C.free(p) + // create a structure from the string + cstruct := C.gst_structure_from_string((*C.gchar)(cstr), (**C.gchar)(p)) + return cstruct +} + +// FromGstStructure converts the given C GstStructure into a go structure. +func FromGstStructure(s *C.GstStructure) *Structure { + v := &Structure{} + v.Name = C.GoString((*C.char)(C.gst_structure_get_name((*C.GstStructure)(s)))) + n := uint(C.gst_structure_n_fields(s)) + v.Data = make(map[string]interface{}) + for i := uint(0); i < n; i++ { + fn := C.gst_structure_nth_field_name((*C.GstStructure)(s), C.guint(i)) + fv := glib.ValueFromNative(unsafe.Pointer(C.gst_structure_id_get_value((*C.GstStructure)(s), C.g_quark_from_string(fn)))) + val, _ := fv.GoValue() + v.Data[C.GoString((*C.char)(fn))] = val + } + return v +} diff --git a/gst/gst_clock.go b/gst/gst_clock.go new file mode 100644 index 0000000..ea60583 --- /dev/null +++ b/gst/gst_clock.go @@ -0,0 +1,22 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" +import ( + "unsafe" +) + +// Clock is a go wrapper around a GstClock. +type Clock struct{ *Object } + +// Instance returns the underlying GstClock instance. +func (c *Clock) Instance() *C.GstClock { return C.toGstClock(c.unsafe()) } + +func wrapClock(c *C.GstClock) *Clock { + return &Clock{wrapObject(C.toGstObject(unsafe.Pointer(c)))} +} diff --git a/gst/gst_element.go b/gst/gst_element.go new file mode 100644 index 0000000..c9ca0a5 --- /dev/null +++ b/gst/gst_element.go @@ -0,0 +1,192 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "errors" + "fmt" + "unsafe" + + "github.com/gotk3/gotk3/glib" +) + +// Element is a Go wrapper around a GstElement. +type Element struct{ *Object } + +// ElementLinkMany is a go implementation of `gst_element_link_many` to compensate for +// no variadic functions in cgo. +func ElementLinkMany(elems ...*Element) error { + for idx, elem := range elems { + if idx == 0 { + // skip the first one as the loop always links previous to current + continue + } + if err := elems[idx-1].Link(elem); err != nil { + return err + } + } + return nil +} + +// Instance returns the underlying GstElement instance. +func (e *Element) Instance() *C.GstElement { return C.toGstElement(e.unsafe()) } + +// Link wraps gst_element_link and links this element to the given one. +func (e *Element) Link(elem *Element) error { + if ok := C.gst_element_link((*C.GstElement)(e.Instance()), (*C.GstElement)(elem.Instance())); !gobool(ok) { + return fmt.Errorf("Failed to link %s to %s", e.Name(), elem.Name()) + } + return nil +} + +// LinkFiltered wraps gst_element_link_filtered and link this element to the given one +// using the provided sink caps. +func (e *Element) LinkFiltered(elem *Element, caps Caps) error { + if ok := C.gst_element_link_filtered((*C.GstElement)(e.Instance()), (*C.GstElement)(elem.Instance()), (*C.GstCaps)(caps.ToGstCaps())); !gobool(ok) { + return fmt.Errorf("Failed to link %s to %s with provider caps", e.Name(), elem.Name()) + } + return nil +} + +// GetBus returns the GstBus for retrieving messages from this element. +func (e *Element) GetBus() (*Bus, error) { + bus := C.gst_element_get_bus((*C.GstElement)(e.Instance())) + if bus == nil { + return nil, errors.New("Could not retrieve bus from element") + } + return wrapBus(bus), nil +} + +// GetState returns the current state of this element. +func (e *Element) GetState() State { + return State(e.Instance().current_state) +} + +// SetState sets the target state for this element. +func (e *Element) SetState(state State) error { + stateRet := C.gst_element_set_state((*C.GstElement)(e.Instance()), C.GstState(state)) + if stateRet == C.GST_STATE_CHANGE_FAILURE { + return fmt.Errorf("Failed to change state to %s", state.String()) + } + return nil +} + +// BlockSetState is like SetState except it will block until the transition +// is complete. +func (e *Element) BlockSetState(state State) error { + stateRet := C.gst_element_set_state((*C.GstElement)(e.Instance()), C.GST_STATE_PLAYING) + if stateRet == C.GST_STATE_CHANGE_FAILURE { + return fmt.Errorf("Failed to change state to %s", state.String()) + } + var curState C.GstState + C.gst_element_get_state( + (*C.GstElement)(e.Instance()), + (*C.GstState)(unsafe.Pointer(&curState)), + (*C.GstState)(unsafe.Pointer(&state)), + C.GST_CLOCK_TIME_NONE, + ) + return nil +} + +// GetFactory returns the factory that created this element. No refcounting is needed. +func (e *Element) GetFactory() *ElementFactory { + factory := C.gst_element_get_factory((*C.GstElement)(e.Instance())) + if factory == nil { + return nil + } + return wrapElementFactory(factory) +} + +// GetPads retrieves a list of pads associated with the element. +func (e *Element) GetPads() []*Pad { + goList := glib.WrapList(uintptr(unsafe.Pointer(e.Instance().pads))) + out := make([]*Pad, 0) + goList.Foreach(func(item interface{}) { + pt := item.(unsafe.Pointer) + out = append(out, wrapPad(C.toGstPad(pt))) + }) + return out +} + +// GetPadTemplates retrieves a list of the pad templates associated with this element. +// The list must not be modified by the calling code. +func (e *Element) GetPadTemplates() []*PadTemplate { + glist := C.gst_element_get_pad_template_list((*C.GstElement)(e.Instance())) + if glist == nil { + return nil + } + goList := glib.WrapList(uintptr(unsafe.Pointer(glist))) + out := make([]*PadTemplate, 0) + goList.Foreach(func(item interface{}) { + pt := item.(unsafe.Pointer) + out = append(out, wrapPadTemplate(C.toGstPadTemplate(pt))) + }) + return out +} + +// GetClock returns the clock for this element or nil. Unref after usage. +func (e *Element) GetClock() *Clock { + clock := C.gst_element_get_clock((*C.GstElement)(e.Instance())) + if clock == nil { + return nil + } + return wrapClock(clock) +} + +// Has returns true if this element has the given flags. +func (e *Element) Has(flags ElementFlags) bool { + return gobool(C.gstObjectFlagIsSet(C.toGstObject(e.unsafe()), C.GstElementFlags(flags))) +} + +// IsURIHandler returns true if this element can handle URIs. +func (e *Element) IsURIHandler() bool { + return gobool(C.gstElementIsURIHandler(e.Instance())) +} + +func (e *Element) uriHandler() *C.GstURIHandler { return C.toGstURIHandler(e.unsafe()) } + +// GetURIType returns the type of URI this element can handle. +func (e *Element) GetURIType() URIType { + if !e.IsURIHandler() { + return URIUnknown + } + ty := C.gst_uri_handler_get_uri_type((*C.GstURIHandler)(e.uriHandler())) + return URIType(ty) +} + +// GetURIProtocols returns the protocols this element can handle. +func (e *Element) GetURIProtocols() []string { + if !e.IsURIHandler() { + return nil + } + protocols := C.gst_uri_handler_get_protocols((*C.GstURIHandler)(e.uriHandler())) + if protocols == nil { + return nil + } + size := C.sizeOfGCharArray(protocols) + return goStrings(size, protocols) +} + +func wrapElement(elem *C.GstElement) *Element { + return &Element{wrapObject(&elem.object)} +} + +// ElementFlags casts C GstElementFlags to a go type +type ElementFlags C.GstElementFlags + +// Type casting of element flags +const ( + ElementFlagLockedState ElementFlags = C.GST_ELEMENT_FLAG_LOCKED_STATE // (16) – ignore state changes from parent + ElementFlagSink = C.GST_ELEMENT_FLAG_SINK // (32) – the element is a sink + ElementFlagSource = C.GST_ELEMENT_FLAG_SOURCE // (64) – the element is a source. + ElementFlagProvideClock = C.GST_ELEMENT_FLAG_PROVIDE_CLOCK // (128) – the element can provide a clock + ElementFlagRequireClock = C.GST_ELEMENT_FLAG_REQUIRE_CLOCK // (256) – the element requires a clock + ElementFlagIndexable = C.GST_ELEMENT_FLAG_INDEXABLE // (512) – the element can use an index + ElementFlagLast = C.GST_ELEMENT_FLAG_LAST // (16384) – offset to define more flags +) diff --git a/gst/gst_element_factory.go b/gst/gst_element_factory.go new file mode 100644 index 0000000..57fc810 --- /dev/null +++ b/gst/gst_element_factory.go @@ -0,0 +1,101 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "fmt" + "unsafe" +) + +// NewElement is a generic wrapper around `gst_element_factory_make`. +func NewElement(name string) (*Element, error) { + elemName := C.CString(name) + defer C.free(unsafe.Pointer(elemName)) + elem := C.gst_element_factory_make((*C.gchar)(elemName), nil) + if elem == nil { + return nil, fmt.Errorf("Could not create element: %s", name) + } + return wrapElement(elem), nil +} + +// NewElementMany is a convenience wrapper around building many GstElements in a +// single function call. It returns an error if the creation of any element fails. A +// map containing the ordinal of the argument to the Element created is returned. +func NewElementMany(elemNames ...string) (map[int]*Element, error) { + elemMap := make(map[int]*Element) + for idx, name := range elemNames { + elem, err := NewElement(name) + if err != nil { + return nil, err + } + elemMap[idx] = elem + } + return elemMap, nil +} + +// ElementFactory wraps the GstElementFactory +type ElementFactory struct{ *Object } + +// Find returns the factory for the given plugin, or nil if it doesn't exist. Unref after usage. +func Find(name string) *ElementFactory { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + factory := C.gst_element_factory_find((*C.gchar)(cName)) + if factory == nil { + return nil + } + return wrapElementFactory(factory) +} + +// Instance returns the C GstFactory instance +func (e *ElementFactory) Instance() *C.GstElementFactory { return C.toGstElementFactory(e.unsafe()) } + +// CanSinkAllCaps checks if the factory can sink all possible capabilities. +func (e *ElementFactory) CanSinkAllCaps(caps *C.GstCaps) bool { + return gobool(C.gst_element_factory_can_sink_all_caps((*C.GstElementFactory)(e.Instance()), (*C.GstCaps)(caps))) +} + +// CanSinkAnyCaps checks if the factory can sink any possible capability. +func (e *ElementFactory) CanSinkAnyCaps(caps *C.GstCaps) bool { + return gobool(C.gst_element_factory_can_sink_any_caps((*C.GstElementFactory)(e.Instance()), (*C.GstCaps)(caps))) +} + +// CanSourceAllCaps checks if the factory can src all possible capabilities. +func (e *ElementFactory) CanSourceAllCaps(caps *C.GstCaps) bool { + return gobool(C.gst_element_factory_can_src_all_caps((*C.GstElementFactory)(e.Instance()), (*C.GstCaps)(caps))) +} + +// CanSourceAnyCaps checks if the factory can src any possible capability. +func (e *ElementFactory) CanSourceAnyCaps(caps *C.GstCaps) bool { + return gobool(C.gst_element_factory_can_src_any_caps((*C.GstElementFactory)(e.Instance()), (*C.GstCaps)(caps))) +} + +// GetMetadata gets the metadata on this factory with key. +func (e *ElementFactory) GetMetadata(key string) string { + ckey := C.CString(key) + defer C.free(unsafe.Pointer(ckey)) + res := C.gst_element_factory_get_metadata((*C.GstElementFactory)(e.Instance()), (*C.gchar)(ckey)) + defer C.free(unsafe.Pointer(res)) + return C.GoString(res) +} + +// GetMetadataKeys gets the available keys for the metadata on this factory. +func (e *ElementFactory) GetMetadataKeys() []string { + keys := C.gst_element_factory_get_metadata_keys((*C.GstElementFactory)(e.Instance())) + if keys == nil { + return nil + } + defer C.g_strfreev(keys) + size := C.sizeOfGCharArray(keys) + return goStrings(size, keys) +} + +func wrapElementFactory(factory *C.GstElementFactory) *ElementFactory { + return &ElementFactory{wrapObject(C.toGstObject(unsafe.Pointer(factory)))} +} diff --git a/gst/gst_message.go b/gst/gst_message.go new file mode 100644 index 0000000..77d0bb1 --- /dev/null +++ b/gst/gst_message.go @@ -0,0 +1,156 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "strings" + "unsafe" +) + +// Message is a Go wrapper around a GstMessage. It provides convenience methods for +// unref-ing and parsing the underlying messages. +type Message struct { + msg *C.GstMessage +} + +// wrapMessage returns a new Message from the given GstMessage. +func wrapMessage(msg *C.GstMessage) *Message { return &Message{msg: msg} } + +// Native returns the underlying GstMessage object. +func (m *Message) Native() *C.GstMessage { return C.toGstMessage(unsafe.Pointer(m.msg)) } + +// Type returns the MessageType of the message. +func (m *Message) Type() MessageType { + return MessageType(m.Native()._type) +} + +// TypeName returns a Go string of the GstMessageType name. +func (m *Message) TypeName() string { + return C.GoString(C.gst_message_type_get_name((C.GstMessageType)(m.Type()))) +} + +// getStructure returns the GstStructure in this message, using the type of the message +// to determine the method to use. +func (m *Message) getStructure() map[string]string { + var st *C.GstStructure + + switch m.Type() { + case MessageError: + C.gst_message_parse_error_details((*C.GstMessage)(m.Native()), (**C.GstStructure)(unsafe.Pointer(&st))) + case MessageInfo: + C.gst_message_parse_info_details((*C.GstMessage)(m.Native()), (**C.GstStructure)(unsafe.Pointer(&st))) + case MessageWarning: + C.gst_message_parse_warning_details((*C.GstMessage)(m.Native()), (**C.GstStructure)(unsafe.Pointer(&st))) + } + + // if no structure was returned, immediately return nil + if st == nil { + return nil + } + + // The returned structure must not be freed. Applies to all methods. + // https://gstreamer.freedesktop.org/documentation/gstreamer/gstmessage.html#gst_message_parse_error_details + return structureToGoMap(st) +} + +// parseToError returns a new GoGError from this message instance. There are multiple +// message types that parse to this interface. +func (m *Message) parseToError() *GoGError { + var gerr *C.GError + var debugInfo *C.gchar + + switch m.Type() { + case MessageError: + C.gst_message_parse_error((*C.GstMessage)(m.Native()), (**C.GError)(unsafe.Pointer(&gerr)), (**C.gchar)(unsafe.Pointer(&debugInfo))) + case MessageInfo: + C.gst_message_parse_info((*C.GstMessage)(m.Native()), (**C.GError)(unsafe.Pointer(&gerr)), (**C.gchar)(unsafe.Pointer(&debugInfo))) + case MessageWarning: + C.gst_message_parse_warning((*C.GstMessage)(m.Native()), (**C.GError)(unsafe.Pointer(&gerr)), (**C.gchar)(unsafe.Pointer(&debugInfo))) + } + + // if error was nil return immediately + if gerr == nil { + return nil + } + + // cleanup the C error immediately and let the garbage collector + // take over from here. + defer C.g_error_free((*C.GError)(gerr)) + defer C.g_free((C.gpointer)(debugInfo)) + return &GoGError{ + errMsg: C.GoString(gerr.message), + details: m.getStructure(), + debugStr: strings.TrimSpace(C.GoString((*C.gchar)(debugInfo))), + } +} + +// ParseInfo is identical to ParseError. The returned types are the same. However, +// this is intended for use with GstMessageType `GST_MESSAGE_INFO`. +func (m *Message) ParseInfo() *GoGError { + return m.parseToError() +} + +// ParseWarning is identical to ParseError. The returned types are the same. However, +// this is intended for use with GstMessageType `GST_MESSAGE_WARNING`. +func (m *Message) ParseWarning() *GoGError { + return m.parseToError() +} + +// ParseError will return a GoGError from the contents of this message. This will only work +// if the GstMessageType is `GST_MESSAGE_ERROR`. +func (m *Message) ParseError() *GoGError { + return m.parseToError() +} + +// ParseStateChanged will return the old and new states as Go strings. This will only work +// if the GstMessageType is `GST_MESSAGE_STATE_CHANGED`. +func (m *Message) ParseStateChanged() (oldState, newState State) { + var gOldState, gNewState C.GstState + C.gst_message_parse_state_changed((*C.GstMessage)(m.Native()), (*C.GstState)(unsafe.Pointer(&gOldState)), (*C.GstState)(unsafe.Pointer(&gNewState)), nil) + oldState = State(gOldState) + newState = State(gNewState) + return +} + +// Unref will call `gst_message_unref` on the underlying GstMessage, freeing it from memory. +func (m *Message) Unref() { C.gst_message_unref((*C.GstMessage)(m.Native())) } + +// Ref will increase the ref count on this message. This increases the total amount of times +// Unref needs to be called before the object is freed from memory. It returns the underlying +// message object for convenience. +func (m *Message) Ref() *Message { + C.gst_message_ref((*C.GstMessage)(m.Native())) + return m +} + +// Copy will copy this object into a new Message that can be Unrefed separately. +func (m *Message) Copy() *Message { + newNative := C.gst_message_copy((*C.GstMessage)(m.Native())) + return wrapMessage(newNative) +} + +// GoGError is a Go wrapper for a C GError. It implements the error interface +// and provides additional functions for retrieving debug strings and details. +type GoGError struct { + errMsg, debugStr string + details map[string]string +} + +// Message is an alias to `Error()`. It's for clarity when this object +// is parsed from a `GST_MESSAGE_INFO` or `GST_MESSAGE_WARNING`. +func (e *GoGError) Message() string { return e.Error() } + +// Error implements the error interface and returns the error message. +func (e *GoGError) Error() string { return e.errMsg } + +// DebugString returns any debug info alongside the error. +func (e *GoGError) DebugString() string { return e.debugStr } + +// Details contains additional metadata about the error if available. +func (e *GoGError) Details() map[string]string { return e.details } diff --git a/gst/gst_object.go b/gst/gst_object.go new file mode 100644 index 0000000..edcd24c --- /dev/null +++ b/gst/gst_object.go @@ -0,0 +1,192 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "strings" + "unsafe" + + "github.com/gotk3/gotk3/glib" +) + +// Object is a go representation of a GstObject. Type casting stops here +// and we do not descend into the glib library. +type Object struct{ *glib.InitiallyUnowned } + +// native returns the pointer to the underlying object. +func (o *Object) unsafe() unsafe.Pointer { return unsafe.Pointer(o.InitiallyUnowned.Native()) } + +// Instance returns the native C GstObject. +func (o *Object) Instance() *C.GstObject { return C.toGstObject(o.unsafe()) } + +// Class returns the GObjectClass of this instance. +func (o *Object) Class() *C.GObjectClass { return C.getGObjectClass(o.unsafe()) } + +// Name returns the name of this object. +func (o *Object) Name() string { + cName := C.gst_object_get_name((*C.GstObject)(o.Instance())) + defer C.free(unsafe.Pointer(cName)) + return C.GoString(cName) +} + +// Interfaces returns the interfaces associated with this object. +func (o *Object) Interfaces() []string { + var size C.guint + ifaces := C.g_type_interfaces(C.ulong(o.TypeFromInstance()), &size) + if int(size) == 0 { + return nil + } + defer C.g_free((C.gpointer)(ifaces)) + out := make([]string, int(size)) + for _, t := range (*[1 << 30]int)(unsafe.Pointer(ifaces))[:size:size] { + out = append(out, glib.Type(t).Name()) + } + return out +} + +// ListProperties returns a list of the properties associated with this object. +// The default values assumed in the parameter spec reflect the values currently +// set in this object. +func (o *Object) ListProperties() []*ParameterSpec { + var size C.guint + props := C.g_object_class_list_properties((*C.GObjectClass)(o.Class()), &size) + if props == nil { + return nil + } + defer C.g_free((C.gpointer)(props)) + out := make([]*ParameterSpec, 0) + for _, prop := range (*[1 << 30]*C.GParamSpec)(unsafe.Pointer(props))[:size:size] { + var gval C.GValue + flags := ParameterFlags(prop.flags) + if flags.Has(ParameterReadable) { + C.g_object_get_property((*C.GObject)(o.unsafe()), prop.name, &gval) + } else { + C.g_param_value_set_default((*C.GParamSpec)(prop), &gval) + } + out = append(out, &ParameterSpec{ + Name: C.GoString(C.g_param_spec_get_name(prop)), + Blurb: C.GoString(C.g_param_spec_get_blurb(prop)), + Flags: flags, + ValueType: glib.Type(prop.value_type), + OwnerType: glib.Type(prop.owner_type), + DefaultValue: glib.ValueFromNative(unsafe.Pointer(&gval)), + }) + } + return out +} + +func wrapObject(o *C.GstObject) *Object { + obj := &Object{&glib.InitiallyUnowned{Object: glib.Take(unsafe.Pointer(o))}} + obj.RefSink() + return obj +} + +// ParameterSpec is a go representation of a C GParamSpec +type ParameterSpec struct { + param *C.GParamSpec + Name string + Blurb string + Flags ParameterFlags + ValueType glib.Type + OwnerType glib.Type + DefaultValue *glib.Value +} + +// ParameterFlags is a go cast of GParamFlags. +type ParameterFlags C.GParamFlags + +// Has returns true if these flags contain the provided ones. +func (p ParameterFlags) Has(b ParameterFlags) bool { return p&b != 0 } + +// Type casting of GParamFlags +const ( + ParameterReadable ParameterFlags = C.G_PARAM_READABLE // the parameter is readable + ParameterWritable = C.G_PARAM_WRITABLE // the parameter is writable + ParameterConstruct = C.G_PARAM_CONSTRUCT // the parameter will be set upon object construction + ParameterConstructOnly = C.G_PARAM_CONSTRUCT_ONLY // the parameter can only be set upon object construction + ParameterLaxValidation = C.G_PARAM_LAX_VALIDATION // upon parameter conversion (see g_param_value_convert()) strict validation is not required + ParameterStaticName = C.G_PARAM_STATIC_NAME // the string used as name when constructing the parameter is guaranteed to remain valid and unmodified for the lifetime of the parameter. Since 2.8 + ParameterStaticNick = C.G_PARAM_STATIC_NICK // the string used as nick when constructing the parameter is guaranteed to remain valid and unmmodified for the lifetime of the parameter. Since 2.8 + ParameterStaticBlurb = C.G_PARAM_STATIC_BLURB // the string used as blurb when constructing the parameter is guaranteed to remain valid and unmodified for the lifetime of the parameter. Since 2.8 + ParameterExplicitNotify = C.G_PARAM_EXPLICIT_NOTIFY // calls to g_object_set_property() for this property will not automatically result in a "notify" signal being emitted: the implementation must call g_object_notify() themselves in case the property actually changes. Since: 2.42. + ParameterDeprecated = C.G_PARAM_DEPRECATED // the parameter is deprecated and will be removed in a future version. A warning will be generated if it is used while running with G_ENABLE_DIAGNOSTIC=1. Since 2.26 + ParameterControllable = C.GST_PARAM_CONTROLLABLE + ParameterMutablePlaying = C.GST_PARAM_MUTABLE_PLAYING + ParameterMutablePaused = C.GST_PARAM_MUTABLE_PAUSED + ParameterMutableReady = C.GST_PARAM_MUTABLE_READY +) + +var allFlags = []ParameterFlags{ + ParameterReadable, + ParameterWritable, + ParameterConstruct, + ParameterConstructOnly, + ParameterLaxValidation, + ParameterStaticName, + ParameterStaticNick, + ParameterStaticBlurb, + ParameterExplicitNotify, + ParameterDeprecated, + ParameterControllable, + ParameterMutablePlaying, + ParameterMutablePaused, + ParameterMutableReady, +} + +var allFlagStrings = []string{ + "readable", + "writable", + "construct", + "construct only", + "lax validation", + "static name", + "static nick", + "static blurb", + "explicity notify", + "deprecated", + "controllable", + "changeable in NULL, READY, PAUSED or PLAYING state", + "changeable only in NULL, READY or PAUSED state", + "changeable only in NULL or READY state", +} + +func (p ParameterFlags) String() string { + out := make([]string, 0) + for idx, param := range allFlags { + if p.Has(param) { + out = append(out, allFlagStrings[idx]) + } + } + return strings.Join(out, ", ") +} + +// GstFlagsString returns a string of the flags that are relevant specifically +// to gstreamer. +func (p ParameterFlags) GstFlagsString() string { + out := make([]string, 0) + if p.Has(ParameterReadable) { + out = append(out, "readable") + } + if p.Has(ParameterWritable) { + out = append(out, "writable") + } + if p.Has(ParameterControllable) { + out = append(out, "controllable") + } + if p.Has(ParameterMutablePlaying) { + out = append(out, "changeable in NULL, READY, PAUSED or PLAYING state") + } + if p.Has(ParameterMutablePaused) { + out = append(out, "changeable only in NULL, READY or PAUSED state") + } + if p.Has(ParameterMutableReady) { + out = append(out, "changeable only in NULL or READY state") + } + return strings.Join(out, ", ") +} diff --git a/gst/gst_pad.go b/gst/gst_pad.go new file mode 100644 index 0000000..923c5f5 --- /dev/null +++ b/gst/gst_pad.go @@ -0,0 +1,106 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" +import "unsafe" + +// Pad is a go representation of a GstPad +type Pad struct{ *Object } + +// Instance returns the underlying C GstPad. +func (p *Pad) Instance() *C.GstPad { return C.toGstPad(p.unsafe()) } + +// Direction returns the direction of this pad. +func (p *Pad) Direction() PadDirection { + return PadDirection(C.gst_pad_get_direction((*C.GstPad)(p.Instance()))) +} + +// Template returns the template for this pad or nil. +func (p *Pad) Template() *PadTemplate { return wrapPadTemplate(p.Instance().padtemplate) } + +// CurrentCaps returns the caps for this Pad or nil. +func (p *Pad) CurrentCaps() Caps { + caps := C.gst_pad_get_current_caps((*C.GstPad)(p.Instance())) + if caps == nil { + return nil + } + defer C.gst_caps_unref(caps) + return FromGstCaps(caps) +} + +func wrapPad(p *C.GstPad) *Pad { + return &Pad{wrapObject(C.toGstObject(unsafe.Pointer(p)))} +} + +// PadTemplate is a go representation of a GstPadTemplate +type PadTemplate struct{ *Object } + +// Instance returns the underlying C GstPadTemplate. +func (p *PadTemplate) Instance() *C.GstPadTemplate { return C.toGstPadTemplate(p.unsafe()) } + +// Name returns the name of the pad template. +func (p *PadTemplate) Name() string { return C.GoString(p.Instance().name_template) } + +// Direction returns the direction of the pad template. +func (p *PadTemplate) Direction() PadDirection { return PadDirection(p.Instance().direction) } + +// Presence returns the presence of the pad template. +func (p *PadTemplate) Presence() PadPresence { return PadPresence(p.Instance().presence) } + +// Caps returns the caps of the pad template. +func (p *PadTemplate) Caps() Caps { return FromGstCaps(p.Instance().caps) } + +func wrapPadTemplate(p *C.GstPadTemplate) *PadTemplate { + return &PadTemplate{wrapObject(C.toGstObject(unsafe.Pointer(p)))} +} + +// PadDirection is a cast of GstPadDirection to a go type. +type PadDirection C.GstPadDirection + +// Type casting of pad directions +const ( + PadUnknown PadDirection = C.GST_PAD_UNKNOWN // (0) - the direction is unknown + PadSource = C.GST_PAD_SRC // (1) - the pad is a source pad + PadSink = C.GST_PAD_SINK // (2) - the pad is a sink pad +) + +// String implements a Stringer on PadDirection. +func (p PadDirection) String() string { + switch p { + case PadUnknown: + return "Unknown" + case PadSource: + return "Src" + case PadSink: + return "Sink" + } + return "" +} + +// PadPresence is a cast of GstPadPresence to a go type. +type PadPresence C.GstPadPresence + +// Type casting of pad presences +const ( + PadAlways PadPresence = C.GST_PAD_ALWAYS // (0) - the pad is always available + PadSometimes = C.GST_PAD_SOMETIMES // (1) - the pad will become available depending on the media stream + PadRequest = C.GST_PAD_REQUEST // (2) - the pad is only available on request with gst_element_request_pad. +) + +// String implements a stringer on PadPresence. +func (p PadPresence) String() string { + switch p { + case PadAlways: + return "Always" + case PadSometimes: + return "Sometimes" + case PadRequest: + return "Request" + } + return "" +} diff --git a/gst/gst_pipeline.go b/gst/gst_pipeline.go new file mode 100644 index 0000000..f121665 --- /dev/null +++ b/gst/gst_pipeline.go @@ -0,0 +1,528 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -Wno-incompatible-pointer-types -g +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + "unsafe" +) + +// PipelineFlags represents arguments passed to a new Pipeline. +type PipelineFlags int + +const ( + // PipelineInternalOnly signals that this pipeline only handles data internally. + PipelineInternalOnly PipelineFlags = 1 << iota + // PipelineRead signals that the Read() method can be used on the end of this pipeline. + PipelineRead + // PipelineWrite signals that the Write() method can be used on the start of this pipeline. + PipelineWrite + // PipelineUseGstApp signals the desire to use an AppSink or AppSrc instead of the default + // os pipes, fdsrc, and fdsink. + // When using this flag, you should interact with the pipeline using the GetAppSink and + // GetAppSrc methods. + PipelineUseGstApp + // PipelineReadWrite signals that this pipeline can be both read and written to. + PipelineReadWrite = PipelineRead | PipelineWrite +) + +// has returns true if these flags contain the given flag. +func (p PipelineFlags) has(b PipelineFlags) bool { return p&b != 0 } + +// State is a type cast of the C GstState +type State int + +// Type casting for GstStates +const ( + VoidPending State = C.GST_STATE_VOID_PENDING // (0) – no pending state. + StateNull = C.GST_STATE_NULL // (1) – the NULL state or initial state of an element. + StateReady = C.GST_STATE_READY // (2) – the element is ready to go to PAUSED. + StatePaused = C.GST_STATE_PAUSED // (3) – the element is PAUSED, it is ready to accept and process data. Sink elements however only accept one buffer and then block. + StatePlaying = C.GST_STATE_PLAYING // (4) – the element is PLAYING, the GstClock is running and the data is flowing. +) + +func (s State) String() string { + return C.GoString(C.gst_element_state_get_name((C.GstState)(s))) +} + +// Pipeline is the base implementation of a GstPipeline using CGO to wrap +// gstreamer API calls. It provides methods to be inherited by the extending +// PlaybackPipeline and RecordingPipeline objects. The struct itself implements +// a ReadWriteCloser. +type Pipeline struct { + *Bin + + // a local reference to the bus so duplicates aren't created + // when retrieved by the user + bus *Bus + + // The buffers backing the Read and Write methods + destBuf *bufio.Reader + srcBuf *bufio.Writer + + // used with PipelineWrite + srcReader, srcWriter *os.File + // used with PipelineRead + destReader, destWriter *os.File + + // used with PipelineWrite AND PipelineGstApp + appSrc *AppSrc + // used with PipelineRead AND PipelineGstApp + appSink *AppSink + autoFlush bool // when set to true, the contents of the app sink are automatically flushed to the read buffer. + + // The element that represents the source/dest pipeline + // and any caps to apply to it. + srcElement *Element + srcCaps Caps + destElement *Element + + // whether or not the pipeline was built from a string. this is checked when + // starting to see who is responsible for build and linking the buffers. + pipelineFromHelper bool + + // A channel where a caller can listen for errors asynchronously. + errCh chan error + // A channel where a caller can listen for messages + msgCh []chan *Message +} + +func newEmptyPipeline() (*C.GstPipeline, error) { + pipeline := C.gst_pipeline_new((*C.gchar)(nil)) + if pipeline == nil { + return nil, errors.New("Could not create new pipeline") + } + return C.toGstPipeline(unsafe.Pointer(pipeline)), nil +} + +func newPipelineFromString(launchv string) (*C.GstPipeline, error) { + if len(strings.Split(launchv, "!")) < 2 { + return nil, fmt.Errorf("Given string is too short for a pipeline: %s", launchv) + } + cLaunchv := C.CString(launchv) + defer C.free(unsafe.Pointer(cLaunchv)) + var gerr *C.GError + pipeline := C.gst_parse_launch((*C.gchar)(cLaunchv), (**C.GError)(&gerr)) + if gerr != nil { + defer C.g_error_free((*C.GError)(gerr)) + errMsg := C.GoString(gerr.message) + return nil, errors.New(errMsg) + } + return C.toGstPipeline(unsafe.Pointer(pipeline)), nil +} + +// NewPipeline builds and returns a new empty Pipeline instance. +func NewPipeline(flags PipelineFlags) (*Pipeline, error) { + pipelineElement, err := newEmptyPipeline() + if err != nil { + return nil, err + } + + pipeline := wrapPipeline(pipelineElement) + + if err := applyFlags(pipeline, flags); err != nil { + return nil, err + } + + return pipeline, nil +} + +func applyFlags(pipeline *Pipeline, flags PipelineFlags) error { + // If the user wants to be able to write to the pipeline, set up the + // write-buffers + if flags.has(PipelineWrite) { + // Set up a pipe + if err := pipeline.setupWriters(); err != nil { + return err + } + } + + // If the user wants to be able to read from the pipeline, setup the + // read-buffers. + if flags.has(PipelineRead) { + if err := pipeline.setupReaders(); err != nil { + return err + } + } + + return nil +} + +func wrapPipeline(elem *C.GstPipeline) *Pipeline { return &Pipeline{Bin: wrapBin(&elem.bin)} } + +func (p *Pipeline) setupWriters() error { + var err error + p.srcReader, p.srcWriter, err = os.Pipe() + if err != nil { + return err + } + p.srcBuf = bufio.NewWriter(p.srcWriter) + return nil +} + +func (p *Pipeline) setupReaders() error { + var err error + p.destReader, p.destWriter, err = os.Pipe() + if err != nil { + return err + } + p.destBuf = bufio.NewReader(p.destReader) + return nil +} + +// Instance returns the native GstPipeline instance. +func (p *Pipeline) Instance() *C.GstPipeline { return C.toGstPipeline(p.unsafe()) } + +// Read implements a Reader and returns data from the read buffer. +func (p *Pipeline) Read(b []byte) (int, error) { + if p.destBuf == nil { + return 0, io.ErrClosedPipe + } + return p.destBuf.Read(b) +} + +// readerFd returns the file descriptor for the read buffer, or 0 if +// there isn't one. It returns the file descriptor that can be written to +// by gstreamer. +func (p *Pipeline) readerFd() uintptr { + if p.destWriter == nil { + return 0 + } + return p.destWriter.Fd() +} + +// Write implements a Writer and places data in the write buffer. +func (p *Pipeline) Write(b []byte) (int, error) { + if p.srcBuf == nil { + return 0, io.ErrClosedPipe + } + return p.srcBuf.Write(b) +} + +// writerFd returns the file descriptor for the write buffer, or 0 if +// there isn't one. It returns the file descriptor that can be read from +// by gstreamer. +func (p *Pipeline) writerFd() uintptr { + if p.srcWriter == nil { + return 0 + } + return p.srcReader.Fd() +} + +// SetWriterCaps sets the caps on the write-buffer. You will usually want to call this +// on a custom pipeline, unless you are using downstream elements that do dynamic pad +// linking. +func (p *Pipeline) SetWriterCaps(caps Caps) { p.srcCaps = caps } + +// LinkWriterTo links the write buffer on this Pipeline to the given element. This must +// be called when the pipeline is constructed with PipelineWrite or PipelineReadWrite. +func (p *Pipeline) LinkWriterTo(elem *Element) { p.srcElement = elem } + +// LinkReaderTo links the read buffer on this Pipeline to the given element. This must +// be called when the pipeline is constructed with PipelineRead or PipelineReadWrite. +func (p *Pipeline) LinkReaderTo(elem *Element) { p.destElement = elem } + +// IsUsingGstApp returns true if the current pipeline is using GstApp instead of file descriptors. +func (p *Pipeline) IsUsingGstApp() bool { + return p.appSrc != nil || p.appSink != nil +} + +// GetAppSrc returns the AppSrc for this pipeline if created with PipelineUseGstApp. +// Unref after usage. +func (p *Pipeline) GetAppSrc() *AppSrc { + if p.appSrc == nil { + return nil + } + // increases the ref count on the element + return wrapAppSrc(p.appSrc.Element) +} + +// GetAppSink returns the AppSink for this pipeline if created with PipelineUseGstApp. +// Unref after usage. +func (p *Pipeline) GetAppSink() *AppSink { + if p.appSink == nil { + return nil + } + // increases the ref count + return wrapAppSink(p.appSink.Element) +} + +// GetBus returns the message bus for this pipeline. +func (p *Pipeline) GetBus() *Bus { + if p.bus == nil { + cBus := C.gst_pipeline_get_bus((*C.GstPipeline)(p.Instance())) + p.bus = wrapBus(cBus) + } + return p.bus +} + +// SetAutoFlush sets whether or not samples should be automatically flushed to the read-buffer +// (default for pipelines not built with PipelineUseGstApp) and if messages should be flushed +// on the bus when the pipeline is stopped. +func (p *Pipeline) SetAutoFlush(b bool) { + p.Set("auto-flush-bus", b) + p.autoFlush = b +} + +// AutoFlush returns true if the pipeline is using a GstAppSink and is configured to autoflush to the +// read-buffer. +func (p *Pipeline) AutoFlush() bool { return p.IsUsingGstApp() && p.autoFlush } + +// Flush flushes the app sink to the read buffer. It is usually more desirable to interface +// with the PullSample and BlockPullSample methods on the AppSink interface directly. Or +// to set autoflush to true. +func (p *Pipeline) Flush() error { + sample, err := p.appSink.PullSample() + if err != nil { // err signals end of stream + return err + } + if sample == nil { + return nil + } + defer sample.Unref() + if _, err := io.Copy(p.destWriter, sample.GetBuffer()); err != nil { + return err + } + return nil +} + +// BlockFlush is like Flush but it blocks until a sample is available. This is intended for +// use with PipelineUseGstApp. +func (p *Pipeline) BlockFlush() error { + sample, err := p.appSink.BlockPullSample() + if err != nil { // err signals end of stream + return err + } + if sample == nil { + return nil + } + defer sample.Unref() + if _, err := io.Copy(p.destWriter, sample.GetBuffer()); err != nil { + return err + } + return nil +} + +// setupSrc sets up a source element with the given configuration. +func (p *Pipeline) setupSrc(pluginName string, args map[string]interface{}) (*Element, error) { + elem, err := NewElement(pluginName) + if err != nil { + return nil, err + } + for k, v := range args { + if err := elem.Set(k, v); err != nil { + return nil, err + } + } + if err := p.Add(elem); err != nil { + return nil, err + } + if p.srcCaps != nil { + return elem, elem.LinkFiltered(p.srcElement, p.srcCaps) + } + return elem, elem.Link(p.srcElement) +} + +// setupFdSrc will setup a fdsrc as the source of the pipeline. +func (p *Pipeline) setupFdSrc() error { + _, err := p.setupSrc("fdsrc", map[string]interface{}{ + "fd": p.writerFd(), + }) + return err +} + +// setupAppSrc sets up an appsrc as the source of the pipeline +func (p *Pipeline) setupAppSrc() error { + appSrc, err := p.setupSrc("appsrc", map[string]interface{}{ + "block": true, // TODO: make this configurable + "emit-signals": false, // https://gstreamer.freedesktop.org/documentation/app/appsrc.html?gi-language=c + }) + if err != nil { + return err + } + p.appSrc = &AppSrc{appSrc} + return nil +} + +// setupSrcElement will setup the source element when the pipeline is constructed with +// PipelineWrite. +func (p *Pipeline) setupSrcElement() error { + if p.srcElement == nil { + return errors.New("Pipeline was constructed with PipelineWrite but LinkWriterTo was never called") + } + if p.IsUsingGstApp() { + return p.setupAppSrc() + } + return p.setupFdSrc() +} + +// setupSink sets up a sink element with the given congifuration. +func (p *Pipeline) setupSink(pluginName string, args map[string]interface{}) (*Element, error) { + elem, err := NewElement(pluginName) + if err != nil { + return nil, err + } + for k, v := range args { + if err := elem.Set(k, v); err != nil { + return nil, err + } + } + if err := p.Add(elem); err != nil { + return nil, err + } + return elem, p.destElement.Link(elem) +} + +// setupFdSink sets up a fdsink as the sink of the pipeline. +func (p *Pipeline) setupFdSink() error { + _, err := p.setupSink("fdsink", map[string]interface{}{ + "fd": p.readerFd(), + }) + return err +} + +// setupAppSink sets up an appsink as the sink of the pipeline. +func (p *Pipeline) setupAppSink() error { + appSink, err := p.setupSink("appsink", map[string]interface{}{ + "emit-signals": false, + }) + if err != nil { + return err + } + p.appSink = wrapAppSink(appSink) + return nil +} + +// setupDestElement will setup the destination (sink) element when the pipeline is constructed with +// PipelineRead. +func (p *Pipeline) setupDestElement() error { + if p.destElement == nil { + return errors.New("Pipeline was constructed with PipelineRead but LinkReaderTo was never called") + } + if p.IsUsingGstApp() { + return p.setupAppSink() + } + return p.setupFdSink() +} + +// Start will start the GstPipeline. It is asynchronous so it does not need to be +// called within a goroutine, however, it is still safe to do so. +func (p *Pipeline) Start() error { + // If there is a write buffer on this pipeline, set up an fdsrc + if p.srcBuf != nil && !p.pipelineFromHelper { + if err := p.setupSrcElement(); err != nil { + return err + } + } + + // If there is a read buffer on this pipeline, set up an fdsink + if p.destBuf != nil && !p.pipelineFromHelper { + if err := p.setupDestElement(); err != nil { + return err + } + } + + return p.startPipeline() +} + +func (p *Pipeline) closeBuffers() error { + if p.srcBuf != nil && p.srcReader != nil && p.srcWriter != nil { + if err := p.srcReader.Close(); err != nil { + return err + } + if err := p.srcWriter.Close(); err != nil { + return err + } + p.srcBuf = nil + } + if p.destBuf != nil && p.destReader != nil && p.destWriter != nil { + if err := p.destReader.Close(); err != nil { + return err + } + if err := p.destWriter.Close(); err != nil { + return err + } + p.destBuf = nil + } + return nil +} + +// ReadBufferSize returns the current size of the unread portion of the read-buffer. +func (p *Pipeline) ReadBufferSize() int { + if p.destBuf == nil { + return 0 + } + return p.destBuf.Buffered() +} + +// WriteBufferSize returns the current size of the unread portion of the write-buffer. +func (p *Pipeline) WriteBufferSize() int { + if p.srcBuf == nil { + return 0 + } + return p.srcBuf.Buffered() +} + +// TotalBufferSize returns the sum of the Read and Write buffer unread portions. +func (p *Pipeline) TotalBufferSize() int { return p.WriteBufferSize() + p.ReadBufferSize() } + +// Close implements a Closer and closes all buffers. +func (p *Pipeline) Close() error { + defer p.Unref() + if err := p.closeBuffers(); err != nil { + return err + } + return p.SetState(StateNull) +} + +// startPipeline will set the GstPipeline to the PLAYING state. +func (p *Pipeline) startPipeline() error { + if err := p.SetState(StatePlaying); err != nil { + return err + } + // If using GstApp with autoflush + if p.AutoFlush() { + go func() { + for { + if err := p.BlockFlush(); err != nil { + // err signals end of stream + return + } + } + }() + } + return nil +} + +// Wait waits for the given pipeline to reach end of stream. +func Wait(p *Pipeline) { + if p.Instance() == nil { + return + } + msgCh := p.GetBus().MessageChan() + for { + select { + default: + if p.Instance() == nil || p.GetState() == StateNull { + return + } + case msg := <-msgCh: + defer msg.Unref() + switch msg.Type() { + case MessageEOS: + return + } + } + } +} diff --git a/gst/gst_pipeline_config.go b/gst/gst_pipeline_config.go new file mode 100644 index 0000000..013305a --- /dev/null +++ b/gst/gst_pipeline_config.go @@ -0,0 +1,194 @@ +package gst + +import "fmt" + +// PipelineConfig represents a list of elements and their configurations +// to be used with NewPipelineFromConfig. +type PipelineConfig struct { + Elements []*PipelineElement +} + +// GetElementByName returns the Element configuration for the given name. +func (p *PipelineConfig) GetElementByName(name string) *PipelineElement { + for _, elem := range p.Elements { + if name == elem.GetName() { + return elem + } + } + return nil +} + +// ElementNames returns a string slice of the names of all the plugins. +func (p *PipelineConfig) ElementNames() []string { + names := make([]string, 0) + for _, elem := range p.Elements { + names = append(names, elem.GetName()) + } + return names +} + +// pushPluginToTop pushes a plugin to the top of the list. +func (p *PipelineConfig) pushPluginToTop(elem *PipelineElement) { + newSlc := []*PipelineElement{elem} + newSlc = append(newSlc, p.Elements...) + p.Elements = newSlc +} + +// PipelineElement represents an `GstElement` in a `GstPipeline` when building a Pipeline with `NewPipelineFromConfig`. +// The Name should coorespond to a valid gstreamer plugin name. The data are additional +// fields to set on the element. If SinkCaps is non-nil, they are applied to the sink of this +// element. +type PipelineElement struct { + Name string + SinkCaps Caps + Data map[string]interface{} +} + +// GetName returns the name to use when creating Elements from this configuration. +func (p *PipelineElement) GetName() string { return p.Name } + +// NewPipelineFromConfig builds a new pipeline from the given PipelineConfig. The plugins provided +// in the configuration will be linked in the order they are given. +// If using PipelineWrite, you can optionally pass a Caps object to filter between the write-buffer +// and the start of the pipeline. +func NewPipelineFromConfig(cfg *PipelineConfig, flags PipelineFlags, caps Caps) (pipeline *Pipeline, err error) { + // create a new empty pipeline instance + pipeline, err = NewPipeline(flags) + if err != nil { + return nil, err + } + // if any error happens while setting up the pipeline, immediately free it + defer func() { + if err != nil { + if cerr := pipeline.Close(); cerr != nil { + fmt.Println("Failed to close pipeline:", err) + } + } + }() + + if cfg.Elements == nil { + cfg.Elements = make([]*PipelineElement, 0) + } + + if flags.has(PipelineWrite) { + if flags.has(PipelineUseGstApp) { + cfg.pushPluginToTop(&PipelineElement{ + Name: "appsrc", + Data: map[string]interface{}{ + "block": true, // TODO: make these all configurable + "emit-signals": false, // https://gstreamer.freedesktop.org/documentation/app/appsrc.html?gi-language=c + "is-live": true, + "max-bytes": 200000, + // "size": 0, // If this is known we should specify it + }, + SinkCaps: caps, + }) + } else { + cfg.pushPluginToTop(&PipelineElement{ + Name: "fdsrc", + Data: map[string]interface{}{ + "fd": pipeline.writerFd(), + }, + SinkCaps: caps, + }) + } + } + + if flags.has(PipelineRead) { + if flags.has(PipelineUseGstApp) { + cfg.Elements = append(cfg.Elements, &PipelineElement{ + Name: "appsink", + Data: map[string]interface{}{ + "emit-signals": false, + }, + }) + } else { + cfg.Elements = append(cfg.Elements, &PipelineElement{ + Name: "fdsink", + Data: map[string]interface{}{ + "fd": pipeline.readerFd(), + }, + }) + } + } + + // retrieve a list of the plugin names + pluginNames := cfg.ElementNames() + + // build all the elements + var elements map[int]*Element + elements, err = NewElementMany(pluginNames...) + if err != nil { + return + } + + // iterate the plugin names and add them to the pipeline + for idx, name := range pluginNames { + // get the current plugin and element + currentPlugin := cfg.GetElementByName(name) + currentElem := elements[idx] + + // Iterate any data with the plugin and set it on the element + for key, value := range currentPlugin.Data { + if err = currentElem.Set(key, value); err != nil { + return + } + } + + // Add the element to the pipeline + if err = pipeline.Add(currentElem); err != nil { + return + } + + // If this is the first element continue + if idx == 0 { + continue + } + + // get the last element in the chain + lastPluginName := pluginNames[idx-1] + lastElem := elements[idx-1] + lastPlugin := cfg.GetElementByName(lastPluginName) + + if lastPlugin == nil { + // this should never happen, since only used internally, + // but safety from panic + continue + } + + // If this is the second element and we are configuring writing + // call link on the last element + if idx == 1 && flags.has(PipelineWrite) { + pipeline.LinkWriterTo(lastElem) + if flags.has(PipelineUseGstApp) { + pipeline.appSrc = wrapAppSrc(lastElem) + } + } + + // If this is the last element and we are configuring reading + // call link on the element + if idx == len(pluginNames)-1 && flags.has(PipelineRead) { + pipeline.LinkReaderTo(currentElem) + if flags.has(PipelineUseGstApp) { + pipeline.appSink = wrapAppSink(currentElem) + } + } + + // If there are sink caps on the last element, do a filtered link to this one and continue + if lastPlugin.SinkCaps != nil { + if err = lastElem.LinkFiltered(currentElem, lastPlugin.SinkCaps); err != nil { + return + } + continue + } + + // link the last element to this element + if err = lastElem.Link(currentElem); err != nil { + return + } + } + + pipeline.pipelineFromHelper = true + + return +} diff --git a/gst/gst_pipeline_string.go b/gst/gst_pipeline_string.go new file mode 100644 index 0000000..2035c3c --- /dev/null +++ b/gst/gst_pipeline_string.go @@ -0,0 +1,142 @@ +package gst + +import ( + "errors" + "fmt" + "strings" +) + +// NewPipelineFromLaunchString returns a new GstPipeline from the given launch string. If flags +// contain PipelineRead or PipelineWrite, the launch string is further formatted accordingly. +// +// If using PipelineWrite, you should generally start your pipeline with the caps of the source. +func NewPipelineFromLaunchString(launchStr string, flags PipelineFlags) (*Pipeline, error) { + // reformat the string to point at the writerFd + if flags.has(PipelineWrite) { + + if flags.has(PipelineUseGstApp) { + if launchStr == "" { + launchStr = "appsrc" + } else { + launchStr = fmt.Sprintf("appsrc ! %s", launchStr) + } + } else { + if launchStr == "" { + launchStr = "fdsrc" + } else { + launchStr = fmt.Sprintf("fdsrc ! %s", launchStr) + } + } + + } + + if flags.has(PipelineRead) { + + if flags.has(PipelineUseGstApp) { + if launchStr == "" { + launchStr = "appsink emit-signals=false" + } else { + launchStr = fmt.Sprintf("%s ! appsink emit-signals=false", launchStr) + } + } else { + if launchStr == "" { + launchStr = "fdsink" + } else { + launchStr = fmt.Sprintf("%s ! fdsink", launchStr) + } + } + + } + + pipelineElement, err := newPipelineFromString(launchStr) + if err != nil { + return nil, err + } + + pipeline := wrapPipeline(pipelineElement) + + if err := applyFlags(pipeline, flags); err != nil { + return nil, err + } + + if flags.has(PipelineWrite) { + + sources, err := pipeline.GetSourceElements() + if err != nil { + return nil, err + } + + var srcType string + if flags.has(PipelineUseGstApp) { + srcType = "appsrc" + } else { + srcType = "fdsrc" + } + + var pipelineSrc *Element + for _, src := range sources { + if strings.Contains(src.Name(), srcType) { + pipelineSrc = src + } else { + src.Unref() + } + } + + if pipelineSrc == nil { + return nil, errors.New("Could not detect pipeline source") + } + + defer pipelineSrc.Unref() + + if flags.has(PipelineUseGstApp) { + pipeline.appSrc = wrapAppSrc(pipelineSrc) + } else { + if err := pipelineSrc.Set("fd", pipeline.writerFd()); err != nil { + return nil, err + } + } + } + + if flags.has(PipelineRead) { + sinks, err := pipeline.GetSinkElements() + if err != nil { + return nil, err + } + + var sinkType string + if flags.has(PipelineUseGstApp) { + sinkType = "appsink" + } else { + sinkType = "fdsink" + } + + var pipelineSink *Element + for _, sink := range sinks { + if strings.Contains(sink.Name(), sinkType) { + pipelineSink = sink + } else { + sink.Unref() + } + } + + if pipelineSink == nil { + return nil, errors.New("Could not detect pipeline sink") + } + + defer pipelineSink.Unref() + + if flags.has(PipelineUseGstApp) { + pipeline.appSink = wrapAppSink(pipelineSink) + } else { + if err := pipelineSink.Set("fd", pipeline.readerFd()); err != nil { + return nil, err + } + } + + } + + // signal that this pipeline was made from a string and therefore already linked + pipeline.pipelineFromHelper = true + + return pipeline, err +} diff --git a/gst/gst_plugin.go b/gst/gst_plugin.go new file mode 100644 index 0000000..3f91815 --- /dev/null +++ b/gst/gst_plugin.go @@ -0,0 +1,114 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +import ( + "unsafe" +) + +// PluginFeature is a go representation of a GstPluginFeature +type PluginFeature struct{ *Object } + +// Instance returns the underlying GstPluginFeature instance +func (p *PluginFeature) Instance() *C.GstPluginFeature { return C.toGstPluginFeature(p.unsafe()) } + +func wrapPluginFeature(p *C.GstPluginFeature) *PluginFeature { + return &PluginFeature{wrapObject(C.toGstObject(unsafe.Pointer(p)))} +} + +// GetPlugin returns the plugin that provides this feature or nil. Unref after usage. +func (p *PluginFeature) GetPlugin() *Plugin { + plugin := C.gst_plugin_feature_get_plugin((*C.GstPluginFeature)(p.Instance())) + if plugin == nil { + return nil + } + return wrapPlugin(plugin) +} + +// GetPluginName returns the name of the plugin that provides this feature. +func (p *PluginFeature) GetPluginName() string { + pluginName := C.gst_plugin_feature_get_plugin_name((*C.GstPluginFeature)(p.Instance())) + if pluginName == nil { + return "" + } + return C.GoString(pluginName) +} + +// Plugin is a go representation of a GstPlugin. +type Plugin struct{ *Object } + +// Instance returns the underlying GstPlugin instance. +func (p *Plugin) Instance() *C.GstPlugin { return C.toGstPlugin(p.unsafe()) } + +func wrapPlugin(p *C.GstPlugin) *Plugin { + return &Plugin{wrapObject(C.toGstObject(unsafe.Pointer(p)))} +} + +// Description returns the description for this plugin. +func (p *Plugin) Description() string { + ret := C.gst_plugin_get_description((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} + +// Filename returns the filename for this plugin. +func (p *Plugin) Filename() string { + ret := C.gst_plugin_get_filename((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} + +// Version returns the version for this plugin. +func (p *Plugin) Version() string { + ret := C.gst_plugin_get_version((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} + +// License returns the license for this plugin. +func (p *Plugin) License() string { + ret := C.gst_plugin_get_license((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} + +// Source returns the source module for this plugin. +func (p *Plugin) Source() string { + ret := C.gst_plugin_get_source((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} + +// Package returns the binary package for this plugin. +func (p *Plugin) Package() string { + ret := C.gst_plugin_get_package((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} + +// Origin returns the origin URL for this plugin. +func (p *Plugin) Origin() string { + ret := C.gst_plugin_get_origin((*C.GstPlugin)(p.Instance())) + if ret == nil { + return "" + } + return C.GoString(ret) +} diff --git a/gst/gst_registry.go b/gst/gst_registry.go new file mode 100644 index 0000000..9f9e651 --- /dev/null +++ b/gst/gst_registry.go @@ -0,0 +1,53 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +// Registry is a go representation of a GstRegistry. +type Registry struct{ *Object } + +// GetRegistry returns the default global GstRegistry. +func GetRegistry() *Registry { + registry := C.gst_registry_get() + return wrapRegistry(registry) +} + +// Instance returns the underlying GstRegistry instance. +func (r *Registry) Instance() *C.GstRegistry { + return C.toGstRegistry(r.unsafe()) +} + +// FindPlugin retrieves the plugin by the given name. Unref after usage. +func (r *Registry) FindPlugin(name string) (*Plugin, error) { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + plugin := C.gst_registry_find_plugin((*C.GstRegistry)(r.Instance()), (*C.gchar)(cName)) + if plugin == nil { + return nil, fmt.Errorf("No plugin named %s found", name) + } + return wrapPlugin(plugin), nil +} + +// LookupFeature looks up the given plugin feature by name. Unref after usage. +func (r *Registry) LookupFeature(name string) (*PluginFeature, error) { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + feat := C.gst_registry_lookup_feature((*C.GstRegistry)(r.Instance()), (*C.gchar)(cName)) + if feat == nil { + return nil, fmt.Errorf("No feature named %s found", name) + } + return wrapPluginFeature(feat), nil +} + +func wrapRegistry(reg *C.GstRegistry) *Registry { + return &Registry{wrapObject(C.toGstObject(unsafe.Pointer(reg)))} +} diff --git a/gst/gst_uri.go b/gst/gst_uri.go new file mode 100644 index 0000000..f99c0a7 --- /dev/null +++ b/gst/gst_uri.go @@ -0,0 +1,31 @@ +package gst + +/* +#cgo pkg-config: gstreamer-1.0 +#cgo CFLAGS: -Wno-deprecated-declarations -g -Wall +#include +#include "gst.go.h" +*/ +import "C" + +// URIType casts C GstURIType to a go type +type URIType C.GstURIType + +// Type cast URI types +const ( + URIUnknown URIType = C.GST_URI_UNKNOWN // (0) – The URI direction is unknown + URISink = C.GST_URI_SINK // (1) – The URI is a consumer. + URISource = C.GST_URI_SRC // (2) - The URI is a producer. +) + +func (u URIType) String() string { + switch u { + case URIUnknown: + return "Unknown" + case URISink: + return "Sink" + case URISource: + return "Source" + } + return "" +}