// This example demonstrates a filesrc plugin implemented in Go. // // Every element in a Gstreamer pipeline is provided by plugins. Some are builtin while // others are provided by third-parties or distributed privately. The plugins are built // around the GObject type system. // // Go-gst offers loose bindings around the GObject type system to provide the necessary // functionality to implement these plugins. The example in this code produces an element // that can read from a file on the local system. // // In order to build the plugin for use by GStreamer, you can do the following: // // $ go generate // $ go build -o libgstgofilesrc.so -buildmode c-shared . // // +plugin:Name=gofilesrc // +plugin:Description=File plugins written in go // +plugin:Version=v0.0.1 // +plugin:License=gst.LicenseLGPL // +plugin:Source=go-gst // +plugin:Package=examples // +plugin:Origin=https://github.com/go-gst/go-gst // +plugin:ReleaseDate=2021-01-04 // // +element:Name=gofilesrc // +element:Rank=gst.RankNone // +element:Impl=fileSrc // +element:Subclass=base.ExtendsBaseSrc // +element:Interfaces=gst.InterfaceURIHandler // //go:generate gst-plugin-gen package main import ( "errors" "fmt" "io" "os" "strings" "github.com/go-gst/go-glib/glib" "github.com/go-gst/go-gst/gst" "github.com/go-gst/go-gst/gst/base" ) // main is left unimplemented since these files are compiled to c-shared. func main() {} // CAT is the log category for the gofilesrc. It is safe to define GStreamer objects as globals // without calling gst.Init, since in the context of a loaded plugin all initialization has // already been taken care of by the loading application. var CAT = gst.NewDebugCategory( "gofilesrc", gst.DebugColorNone, "GoFileSrc Element", ) // Here we define a list of ParamSpecs that will make up the properties for our element. // This element only has a single property, the location of the file to read from. // When getting and setting properties later on, you will reference them by their index in // this list. var properties = []*glib.ParamSpec{ glib.NewStringParam( "location", // The name of the parameter "File Location", // The long name for the parameter "Location of the file to read from", // A blurb about the parameter nil, // A default value for the parameter glib.ParameterReadWrite, // Flags for the parameter ), } // Here we declare a private struct to hold our internal state. type state struct { // Whether the element is started or not started bool // The file the element is reading from file *os.File // The information about the file retrieved from stat fileInfo os.FileInfo // The current position in the file position uint64 } // This is another private struct where we hold the parameter values set on our // element. type settings struct { location string } // Finally a structure is defined that implements (at a minimum) the gst.GoElement interface. // It is possible to signal to the bindings to inherit from other classes or implement other // interfaces via the registration and TypeInit processes. type FileSrc struct { // The settings for the element settings *settings // The current state of the element state *state } // Private methods only used internally by the plugin // setLocation is a simple method to check the validity of a provided file path and set the // local value with it. func (f *FileSrc) setLocation(path string) error { if f.state.started { return errors.New("changing the `location` property on a started `GoFileSrc` is not supported") } f.settings.location = strings.TrimPrefix(path, "file://") // should obviously use url.URL and do actual parsing return nil } // The ObjectSubclass implementations below are for registering the various aspects of our // element and its capabilities with the type system. These are the minimum methods that // should be implemented by an element. // Every element needs to provide its own constructor that returns an initialized // glib.GoObjectSubclass and state objects. func (f *FileSrc) New() glib.GoObjectSubclass { CAT.Log(gst.LevelLog, "Initializing new fileSrc object") return &FileSrc{ settings: &settings{}, state: &state{}, } } // The ClassInit method should specify the metadata for this element and add any pad templates // and properties. func (f *FileSrc) ClassInit(klass *glib.ObjectClass) { CAT.Log(gst.LevelLog, "Initializing gofilesrc class") class := gst.ToElementClass(klass) class.SetMetadata( "File Source", "Source/File", "Read stream from a file", "Avi Zimmerman ", ) CAT.Log(gst.LevelLog, "Adding src pad template and properties to class") class.AddPadTemplate(gst.NewPadTemplate( "src", gst.PadDirectionSource, gst.PadPresenceAlways, gst.NewAnyCaps(), )) class.InstallProperties(properties) } // Object implementations are used during the initialization of an element. The // methods are called once the object is constructed and its properties are read // and written to. These and the rest of the methods described below are documented // in interfaces in the bindings, however only individual methods needs from those // interfaces need to be implemented. When left unimplemented, the behavior of the parent // class is inherited. // SetProperty is called when a `value` is set to the property at index `id` in the // properties slice that we installed during ClassInit. It should attempt to register // the value locally or signal any errors that occur in the process. func (f *FileSrc) SetProperty(self *glib.Object, id uint, value *glib.Value) { param := properties[id] switch param.Name() { case "location": var val string if value == nil { val = "" } else { val, _ = value.GetString() } if err := f.setLocation(val); err != nil { gst.ToElement(self).ErrorMessage(gst.DomainLibrary, gst.LibraryErrorSettings, fmt.Sprintf("Could not set location on object: %s", err.Error()), "", ) return } gst.ToElement(self).Log(CAT, gst.LevelInfo, fmt.Sprintf("Set `location` to %s", f.settings.location)) } } // GetProperty is called to retrieve the value of the property at index `id` in the properties // slice provided at ClassInit. func (f *FileSrc) GetProperty(self *glib.Object, id uint) *glib.Value { param := properties[id] switch param.Name() { case "location": if f.settings.location == "" { return nil } val, err := glib.GValue(f.settings.location) if err == nil { return val } gst.ToElement(self).ErrorMessage(gst.DomainLibrary, gst.LibraryErrorFailed, fmt.Sprintf("Could not convert %s to GValue", f.settings.location), err.Error(), ) } return nil } // Constructed is called when the type system is done constructing the object. Any finalizations required // during the initialization process can be performed here. In this example, we set the format on our // underlying GstBaseSrc to bytes. func (f *FileSrc) Constructed(self *glib.Object) { base.ToGstBaseSrc(self).Log(CAT, gst.LevelLog, "Setting format of GstBaseSrc to bytes") base.ToGstBaseSrc(self).SetFormat(gst.FormatBytes) } // GstBaseSrc implementations are optional methods to implement from the base.GstBaseSrcImpl interface. // If the method is not overridden by the implementing struct, it will be inherited from the parent class. // IsSeekable returns that we are, in fact, seekable. func (f *FileSrc) IsSeekable(*base.GstBaseSrc) bool { return true } // GetSize will return the total size of the file at the configured location. func (f *FileSrc) GetSize(self *base.GstBaseSrc) (bool, int64) { if !f.state.started { return false, 0 } return true, f.state.fileInfo.Size() } // Start is called to start this element. In this example, the configured file is opened for reading, // and any error encountered in the process is posted to the pipeline. func (f *FileSrc) Start(self *base.GstBaseSrc) bool { if f.state.started { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorSettings, "GoFileSrc is already started", "") return false } if f.settings.location == "" { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorSettings, "File location is not defined", "") return false } stat, err := os.Stat(f.settings.location) if err != nil { if os.IsNotExist(err) { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorOpenRead, fmt.Sprintf("%s does not exist", f.settings.location), "") return false } self.ErrorMessage(gst.DomainResource, gst.ResourceErrorOpenRead, fmt.Sprintf("Could not stat %s, err: %s", f.settings.location, err.Error()), "") return false } if stat.IsDir() { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorOpenRead, fmt.Sprintf("%s is a directory", f.settings.location), "") return false } f.state.fileInfo = stat self.Log(CAT, gst.LevelDebug, fmt.Sprintf("file stat - name: %s size: %d mode: %v modtime: %v", stat.Name(), stat.Size(), stat.Mode(), stat.ModTime())) self.Log(CAT, gst.LevelDebug, fmt.Sprintf("Opening file %s for reading", f.settings.location)) f.state.file, err = os.Open(f.settings.location) if err != nil { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorOpenRead, fmt.Sprintf("Could not open file %s for reading", f.settings.location), err.Error()) return false } f.state.position = 0 f.state.started = true self.StartComplete(gst.FlowOK) self.Log(CAT, gst.LevelInfo, "GoFileSrc has started") return true } // Stop is called to stop the element. The file is closed and the local values are zeroed out. func (f *FileSrc) Stop(self *base.GstBaseSrc) bool { if !f.state.started { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorSettings, "FileSrc is not started", "") return false } if err := f.state.file.Close(); err != nil { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorClose, "Failed to close the source file", err.Error()) return false } f.state.file = nil f.state.position = 0 f.state.started = false self.Log(CAT, gst.LevelInfo, "GoFileSrc has stopped") return true } // Fill is called to fill a pre-allocated buffer with the data at offset up to the given size. // Since we declared that we are seekable, we need to support the provided offset not necessarily matching // where we currently are in the file. This is why we store the position in the file locally. func (f *FileSrc) Fill(self *base.GstBaseSrc, offset uint64, size uint, buffer *gst.Buffer) gst.FlowReturn { if !f.state.started || f.state.file == nil { self.ErrorMessage(gst.DomainCore, gst.CoreErrorFailed, "Not started yet", "") return gst.FlowError } self.Log(CAT, gst.LevelLog, fmt.Sprintf("Request to fill buffer from offset %v with size %v", offset, size)) if f.state.position != offset { self.Log(CAT, gst.LevelDebug, fmt.Sprintf("Seeking to new position at offset %v from previous position at offset %v", offset, f.state.position)) if _, err := f.state.file.Seek(int64(offset), 0); err != nil { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorSeek, fmt.Sprintf("Failed to seek to %d in file", offset), err.Error()) return gst.FlowError } f.state.position = offset } bufmap := buffer.Map(gst.MapWrite) if bufmap == nil { self.ErrorMessage(gst.DomainLibrary, gst.LibraryErrorFailed, "Failed to map buffer", "") return gst.FlowError } defer buffer.Unmap() self.Log(CAT, gst.LevelLog, fmt.Sprintf("Reading %v bytes from offset %v in file into buffer at %v", size, f.state.position, bufmap.Data())) if _, err := io.CopyN(bufmap.Writer(), f.state.file, int64(size)); err != nil { self.ErrorMessage(gst.DomainResource, gst.ResourceErrorRead, fmt.Sprintf("Failed to read %d bytes from file at %d into buffer", size, offset), err.Error()) return gst.FlowError } buffer.SetSize(int64(size)) f.state.position = f.state.position + uint64(size) self.Log(CAT, gst.LevelLog, fmt.Sprintf("Incremented current position to %v", f.state.position)) return gst.FlowOK } // URIHandler implementations are the methods required by the GstURIHandler interface. // GetURI returns the currently configured URI func (f *FileSrc) GetURI() string { return fmt.Sprintf("file://%s", f.settings.location) } // GetURIType returns the types of URI this element supports. func (f *FileSrc) GetURIType() gst.URIType { return gst.URISource } // GetProtocols returns the protcols this element supports. func (f *FileSrc) GetProtocols() []string { return []string{"file"} } // SetURI should set the URI that this element is working on. func (f *FileSrc) SetURI(uri string) (bool, error) { if uri == "file://" { return true, nil } err := f.setLocation(uri) if err != nil { return false, err } CAT.Log(gst.LevelInfo, fmt.Sprintf("Set `location` to %s via URIHandler", f.settings.location)) return true, nil }