mirror of
https://github.com/go-gst/go-gst.git
synced 2025-11-01 12:02:34 +08:00
add support for GstPromise
This commit is contained in:
@@ -275,9 +275,18 @@ func goLogFunction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// goDestroyBusWatch frees the go memory associated with the bus watch.
|
// goUnrefGopointerUserData is a GDestroyNotify used to unref the gopointer from the userdata, used for callback functions
|
||||||
//
|
//
|
||||||
//export goDestroyBusWatch
|
//export goUnrefGopointerUserData
|
||||||
func goDestroyBusWatch(fPtr C.gpointer) {
|
func goUnrefGopointerUserData(fPtr C.gpointer) {
|
||||||
gopointer.Unref(unsafe.Pointer(fPtr))
|
gopointer.Unref(unsafe.Pointer(fPtr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// goPromiseChangeFunc is the function the GstPromise calls when it changes state
|
||||||
|
//
|
||||||
|
//export goPromiseChangeFunc
|
||||||
|
func goPromiseChangeFunc(_ *C.GstPromise, fPtr C.gpointer) {
|
||||||
|
f := gopointer.Restore(unsafe.Pointer(fPtr)).(func())
|
||||||
|
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ package gst
|
|||||||
|
|
||||||
extern GstBusSyncReply goBusSyncHandler (GstBus * bus, GstMessage * message, gpointer user_data);
|
extern GstBusSyncReply goBusSyncHandler (GstBus * bus, GstMessage * message, gpointer user_data);
|
||||||
extern gboolean goBusFunc (GstBus * bus, GstMessage * msg, gpointer user_data);
|
extern gboolean goBusFunc (GstBus * bus, GstMessage * msg, gpointer user_data);
|
||||||
extern void goDestroyBusWatch (gpointer);
|
extern void goUnrefGopointerUserData (gpointer);
|
||||||
|
|
||||||
gboolean cgoBusFunc (GstBus * bus, GstMessage * msg, gpointer user_data)
|
gboolean cgoBusFunc (GstBus * bus, GstMessage * msg, gpointer user_data)
|
||||||
{
|
{
|
||||||
return goBusFunc(bus, msg, user_data);
|
return goBusFunc(bus, msg, user_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cgoDestroyBusWatch (gpointer data)
|
void cgoUnrefGopointerUserData (gpointer data)
|
||||||
{
|
{
|
||||||
goDestroyBusWatch(data);
|
goUnrefGopointerUserData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
GstBusSyncReply cgoBusSyncHandler (GstBus * bus, GstMessage * message, gpointer user_data)
|
GstBusSyncReply cgoBusSyncHandler (GstBus * bus, GstMessage * message, gpointer user_data)
|
||||||
@@ -141,7 +141,7 @@ func (b *Bus) AddWatch(busFunc BusWatchFunc) bool {
|
|||||||
C.G_PRIORITY_DEFAULT,
|
C.G_PRIORITY_DEFAULT,
|
||||||
C.GstBusFunc(C.cgoBusFunc),
|
C.GstBusFunc(C.cgoBusFunc),
|
||||||
(C.gpointer)(unsafe.Pointer(fPtr)),
|
(C.gpointer)(unsafe.Pointer(fPtr)),
|
||||||
C.GDestroyNotify(C.cgoDestroyBusWatch),
|
C.GDestroyNotify(C.cgoUnrefGopointerUserData),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ func (b *Bus) SetSyncHandler(f BusSyncHandler) {
|
|||||||
b.Instance(),
|
b.Instance(),
|
||||||
C.GstBusSyncHandler(C.cgoBusSyncHandler),
|
C.GstBusSyncHandler(C.cgoBusSyncHandler),
|
||||||
(C.gpointer)(unsafe.Pointer(ptr)),
|
(C.gpointer)(unsafe.Pointer(ptr)),
|
||||||
C.GDestroyNotify(C.cgoDestroyBusWatch),
|
C.GDestroyNotify(C.cgoUnrefGopointerUserData),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
202
gst/gst_promise.go
Normal file
202
gst/gst_promise.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package gst
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "gst.go.h"
|
||||||
|
|
||||||
|
extern void goPromiseChangeFunc (GstPromise*, gpointer user_data);
|
||||||
|
extern void cgoUnrefGopointerUserData (gpointer);
|
||||||
|
|
||||||
|
void cgoPromiseChangeFunc (GstPromise *promise, gpointer data)
|
||||||
|
{
|
||||||
|
goPromiseChangeFunc(promise, data);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/go-gst/go-glib/glib"
|
||||||
|
gopointer "github.com/mattn/go-pointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PromiseResult int
|
||||||
|
|
||||||
|
func (pr PromiseResult) String() string {
|
||||||
|
switch pr {
|
||||||
|
case PromiseResultPending:
|
||||||
|
return "PENDING"
|
||||||
|
case PromiseResultInterrupted:
|
||||||
|
return "INTERRUPTED"
|
||||||
|
case PromiseResultReplied:
|
||||||
|
return "REPLIED"
|
||||||
|
case PromiseResultExpired:
|
||||||
|
return "EXPIRED"
|
||||||
|
default:
|
||||||
|
return "UNKNOWN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
//Initial state. Waiting for transition to any other state.
|
||||||
|
PromiseResultPending = C.GST_PROMISE_RESULT_PENDING
|
||||||
|
// Interrupted by the consumer as it doesn't want the value anymore.
|
||||||
|
PromiseResultInterrupted = C.GST_PROMISE_RESULT_INTERRUPTED
|
||||||
|
// A producer marked a reply
|
||||||
|
PromiseResultReplied = C.GST_PROMISE_RESULT_REPLIED
|
||||||
|
// The promise expired (the carrying object lost all refs) and the promise will never be fulfilled.
|
||||||
|
PromiseResultExpired = C.GST_PROMISE_RESULT_EXPIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
// Promise is a go wrapper around a GstPromise.
|
||||||
|
// See: https://gstreamer.freedesktop.org/documentation/gstreamer/gstpromise.html
|
||||||
|
//
|
||||||
|
// it can be awaited on-blocking using Await, given the promise was constructed in go and not received from FFI.
|
||||||
|
type Promise struct {
|
||||||
|
ptr *C.GstPromise
|
||||||
|
|
||||||
|
// done will be closed when the GstPromise has changed state
|
||||||
|
done <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPromise() *Promise {
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
fPtr := gopointer.Save(func() {
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
cprom := C.gst_promise_new_with_change_func(
|
||||||
|
C.GstPromiseChangeFunc(C.cgoPromiseChangeFunc),
|
||||||
|
C.gpointer(fPtr),
|
||||||
|
C.GDestroyNotify(C.cgoUnrefGopointerUserData),
|
||||||
|
)
|
||||||
|
|
||||||
|
prom := &Promise{
|
||||||
|
ptr: cprom,
|
||||||
|
done: done,
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.SetFinalizer(prom, func(prom *Promise) {
|
||||||
|
prom.Unref()
|
||||||
|
})
|
||||||
|
|
||||||
|
return prom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Promise) Instance() *C.GstPromise {
|
||||||
|
return p.ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ref increases the ref count on the promise. Exposed for completeness sake. Should not be called
|
||||||
|
// by application code
|
||||||
|
func (p *Promise) Ref() {
|
||||||
|
C.gst_promise_ref(p.ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unref decreases the ref count on the promise. Exposed for completeness sake. Should not be called
|
||||||
|
// by application code
|
||||||
|
func (p *Promise) Unref() {
|
||||||
|
C.gst_promise_unref(p.ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrPromiseNotReplied = errors.New("promise was not replied")
|
||||||
|
var ErrNilPromiseReply = errors.New("promise returned a nil reply")
|
||||||
|
|
||||||
|
// ErrCannotAwaitPromise signifies that we do not have a channel that we can await.
|
||||||
|
//
|
||||||
|
// this happens if the promise was marshaled from a GValue coming from C
|
||||||
|
var ErrCannotAwaitPromise = errors.New("promises received from FFI cannot be awaited")
|
||||||
|
|
||||||
|
// Await awaits the promise without blocking the thread. It returns the reply returned by the GstPromise
|
||||||
|
//
|
||||||
|
// its implementation is preferred over the blocking gst_promise_wait, which would lock a thread until the
|
||||||
|
// promise has changed state.
|
||||||
|
func (p *Promise) Await(ctx context.Context) (*Structure, error) {
|
||||||
|
if p.done == nil {
|
||||||
|
return nil, ErrCannotAwaitPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-p.done:
|
||||||
|
}
|
||||||
|
|
||||||
|
// gst_promise_wait will not block here, because the promise has already changed state
|
||||||
|
result := PromiseResult(C.gst_promise_wait(p.ptr))
|
||||||
|
|
||||||
|
if result != PromiseResultReplied {
|
||||||
|
return nil, fmt.Errorf("%w: got %s", ErrPromiseNotReplied, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
structure := p.GetReply()
|
||||||
|
|
||||||
|
if structure == nil {
|
||||||
|
return nil, ErrNilPromiseReply
|
||||||
|
}
|
||||||
|
|
||||||
|
return structure, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReply wraps gst_promise_get_reply and returns the structure, which can be nil.
|
||||||
|
func (p *Promise) GetReply() *Structure {
|
||||||
|
cstruct := C.gst_promise_get_reply(p.ptr)
|
||||||
|
|
||||||
|
if cstruct == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
structure := wrapStructure(cstruct)
|
||||||
|
|
||||||
|
// the structure is owned by the promise, so we keep the promise alive
|
||||||
|
// until the structure gets GC'ed
|
||||||
|
p.Ref()
|
||||||
|
runtime.SetFinalizer(structure, func(_ *Structure) {
|
||||||
|
p.Unref()
|
||||||
|
})
|
||||||
|
|
||||||
|
return structure
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire wraps gst_promise_expire
|
||||||
|
func (p *Promise) Expire() {
|
||||||
|
C.gst_promise_expire(p.ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt wraps gst_promise_interrupt
|
||||||
|
func (p *Promise) Interrupt() {
|
||||||
|
C.gst_promise_interrupt(p.ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply wraps gst_promise_reply
|
||||||
|
func (p *Promise) Reply(answer *Structure) {
|
||||||
|
C.gst_promise_reply(p.ptr, answer.Instance())
|
||||||
|
}
|
||||||
|
|
||||||
|
var TypePromise = glib.Type(C.GST_TYPE_PROMISE)
|
||||||
|
|
||||||
|
// ToGValue implements glib.ValueTransformer
|
||||||
|
func (p *Promise) ToGValue() (*glib.Value, error) {
|
||||||
|
val, err := glib.ValueInit(TypePromise)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
val.SetInstance(unsafe.Pointer(p.Instance()))
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalPromise(p unsafe.Pointer) (interface{}, error) {
|
||||||
|
c := C.g_value_get_object(toGValue(p))
|
||||||
|
obj := (*C.GstPromise)(unsafe.Pointer(c))
|
||||||
|
|
||||||
|
prom := &Promise{
|
||||||
|
ptr: obj,
|
||||||
|
done: nil, // cannot be awaited if received from FFI
|
||||||
|
}
|
||||||
|
|
||||||
|
return prom, nil
|
||||||
|
}
|
||||||
123
gst/gst_promise_test.go
Normal file
123
gst/gst_promise_test.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package gst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:noinline
|
||||||
|
func awaitGC() {
|
||||||
|
type tmp struct{ v string }
|
||||||
|
|
||||||
|
setup := make(chan struct{})
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
v := &tmp{"foo"}
|
||||||
|
|
||||||
|
runtime.SetFinalizer(v, func(v *tmp) {
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
close(setup)
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-setup
|
||||||
|
runtime.GC()
|
||||||
|
<-done
|
||||||
|
runtime.GC()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPromise(t *testing.T) {
|
||||||
|
initOnce.Do(func() {
|
||||||
|
Init(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
prom := NewPromise()
|
||||||
|
cprom := prom.Instance()
|
||||||
|
|
||||||
|
reply := NewStructure("foo/bar")
|
||||||
|
errchan := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
res, err := prom.Await(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errchan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// even though we don't use the promise, the result structure should be still accessible.
|
||||||
|
// the returned structure is owned by the promise, so the promise must not get GC'ed until
|
||||||
|
// we don't use the structure anymore
|
||||||
|
awaitGC()
|
||||||
|
|
||||||
|
if res.Name() != reply.Name() {
|
||||||
|
errchan <- errors.New("name mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
runtime.GC()
|
||||||
|
runtime.GC()
|
||||||
|
|
||||||
|
close(errchan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
prom.Reply(reply)
|
||||||
|
|
||||||
|
err := <-errchan
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitGC()
|
||||||
|
|
||||||
|
if cprom.parent.refcount != 1 {
|
||||||
|
panic("refcount too high")
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.KeepAlive(prom)
|
||||||
|
|
||||||
|
awaitGC()
|
||||||
|
}
|
||||||
|
|
||||||
|
var initOnce sync.Once
|
||||||
|
|
||||||
|
func TestPromiseMarshal(t *testing.T) {
|
||||||
|
initOnce.Do(func() {
|
||||||
|
Init(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
prom := NewPromise()
|
||||||
|
|
||||||
|
gv, err := prom.ToGValue()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedPromI, err := marshalPromise(unsafe.Pointer(gv.GValue))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedProm, ok := receivedPromI.(*Promise)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("could not cast")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awaiting received promise should error immediately
|
||||||
|
_, err = receivedProm.Await(context.Background())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,6 +234,10 @@ func registerMarshalers() {
|
|||||||
T: TypeValueList,
|
T: TypeValueList,
|
||||||
F: marshalValueList,
|
F: marshalValueList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
T: TypePromise,
|
||||||
|
F: marshalPromise,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
T: glib.Type(C.gst_sample_get_type()),
|
T: glib.Type(C.gst_sample_get_type()),
|
||||||
F: marshalSample,
|
F: marshalSample,
|
||||||
|
|||||||
Reference in New Issue
Block a user