Add GStreamer preview backend
This commit is contained in:
parent
27ba4317a0
commit
88bc5ad4d4
18
internal/player/frame_player.go
Normal file
18
internal/player/frame_player.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package player
|
||||
|
||||
import (
|
||||
"image"
|
||||
"time"
|
||||
)
|
||||
|
||||
type framePlayer interface {
|
||||
Load(path string, offset time.Duration) error
|
||||
Play() error
|
||||
Pause() error
|
||||
SeekToTime(offset time.Duration) error
|
||||
SeekToFrame(frame int64) error
|
||||
GetCurrentTime() time.Duration
|
||||
GetFrameImage() (*image.RGBA, error)
|
||||
SetVolume(level float64) error
|
||||
Close()
|
||||
}
|
||||
7
internal/player/frame_player_default.go
Normal file
7
internal/player/frame_player_default.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !gstreamer
|
||||
|
||||
package player
|
||||
|
||||
func newFramePlayer(config Config) (framePlayer, error) {
|
||||
return NewUnifiedPlayer(config), nil
|
||||
}
|
||||
10
internal/player/frame_player_gstreamer.go
Normal file
10
internal/player/frame_player_gstreamer.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//go:build gstreamer
|
||||
|
||||
package player
|
||||
|
||||
func newFramePlayer(config Config) (framePlayer, error) {
|
||||
if gstPlayer, err := NewGStreamerPlayer(config); err == nil {
|
||||
return gstPlayer, nil
|
||||
}
|
||||
return NewUnifiedPlayer(config), nil
|
||||
}
|
||||
332
internal/player/gstreamer_player.go
Normal file
332
internal/player/gstreamer_player.go
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
//go:build gstreamer
|
||||
|
||||
package player
|
||||
|
||||
/*
|
||||
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0
|
||||
#include <gst/gst.h>
|
||||
#include <gst/app/gstappsink.h>
|
||||
#include <gst/video/video.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static void vt_gst_set_str(GstElement* elem, const char* name, const char* value) {
|
||||
g_object_set(G_OBJECT(elem), name, value, NULL);
|
||||
}
|
||||
static void vt_gst_set_bool(GstElement* elem, const char* name, gboolean value) {
|
||||
g_object_set(G_OBJECT(elem), name, value, NULL);
|
||||
}
|
||||
static void vt_gst_set_int(GstElement* elem, const char* name, gint value) {
|
||||
g_object_set(G_OBJECT(elem), name, value, NULL);
|
||||
}
|
||||
static void vt_gst_set_float(GstElement* elem, const char* name, gdouble value) {
|
||||
g_object_set(G_OBJECT(elem), name, value, NULL);
|
||||
}
|
||||
static void vt_gst_set_obj(GstElement* elem, const char* name, gpointer value) {
|
||||
g_object_set(G_OBJECT(elem), name, value, NULL);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var gstInitOnce sync.Once
|
||||
|
||||
type GStreamerPlayer struct {
|
||||
mu sync.Mutex
|
||||
pipeline *C.GstElement
|
||||
appsink *C.GstElement
|
||||
paused bool
|
||||
volume float64
|
||||
preview bool
|
||||
width int
|
||||
height int
|
||||
fps float64
|
||||
}
|
||||
|
||||
func NewGStreamerPlayer(config Config) (*GStreamerPlayer, error) {
|
||||
var initErr error
|
||||
gstInitOnce.Do(func() {
|
||||
if C.gst_init_check(nil, nil, nil) == 0 {
|
||||
initErr = errors.New("gstreamer init failed")
|
||||
}
|
||||
})
|
||||
if initErr != nil {
|
||||
return nil, initErr
|
||||
}
|
||||
|
||||
return &GStreamerPlayer{
|
||||
paused: true,
|
||||
volume: config.Volume,
|
||||
preview: config.PreviewMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) Load(path string, offset time.Duration) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.closeLocked()
|
||||
|
||||
playbinName := C.CString("playbin")
|
||||
playbin := C.gst_element_factory_make(playbinName, nil)
|
||||
C.free(unsafe.Pointer(playbinName))
|
||||
if playbin == nil {
|
||||
return errors.New("gstreamer playbin unavailable")
|
||||
}
|
||||
|
||||
appsinkName := C.CString("appsink")
|
||||
appsink := C.gst_element_factory_make(appsinkName, nil)
|
||||
C.free(unsafe.Pointer(appsinkName))
|
||||
if appsink == nil {
|
||||
C.gst_object_unref(C.gpointer(playbin))
|
||||
return errors.New("gstreamer appsink unavailable")
|
||||
}
|
||||
|
||||
capsStr := C.CString("video/x-raw,format=RGBA")
|
||||
caps := C.gst_caps_from_string(capsStr)
|
||||
C.free(unsafe.Pointer(capsStr))
|
||||
if caps != nil {
|
||||
capsName := C.CString("caps")
|
||||
C.vt_gst_set_obj(appsink, capsName, C.gpointer(caps))
|
||||
C.free(unsafe.Pointer(capsName))
|
||||
C.gst_caps_unref(caps)
|
||||
}
|
||||
emitSignals := C.CString("emit-signals")
|
||||
C.vt_gst_set_bool(appsink, emitSignals, C.gboolean(0))
|
||||
C.free(unsafe.Pointer(emitSignals))
|
||||
syncName := C.CString("sync")
|
||||
C.vt_gst_set_bool(appsink, syncName, C.gboolean(0))
|
||||
C.free(unsafe.Pointer(syncName))
|
||||
maxBuffers := C.CString("max-buffers")
|
||||
C.vt_gst_set_int(appsink, maxBuffers, C.gint(2))
|
||||
C.free(unsafe.Pointer(maxBuffers))
|
||||
dropName := C.CString("drop")
|
||||
C.vt_gst_set_bool(appsink, dropName, C.gboolean(1))
|
||||
C.free(unsafe.Pointer(dropName))
|
||||
|
||||
var audioSink *C.GstElement
|
||||
if p.preview {
|
||||
fakeName := C.CString("fakesink")
|
||||
audioSink = C.gst_element_factory_make(fakeName, nil)
|
||||
C.free(unsafe.Pointer(fakeName))
|
||||
} else {
|
||||
autoName := C.CString("autoaudiosink")
|
||||
audioSink = C.gst_element_factory_make(autoName, nil)
|
||||
C.free(unsafe.Pointer(autoName))
|
||||
}
|
||||
if audioSink == nil {
|
||||
C.gst_object_unref(C.gpointer(playbin))
|
||||
C.gst_object_unref(C.gpointer(appsink))
|
||||
return errors.New("gstreamer audio sink unavailable")
|
||||
}
|
||||
|
||||
uri := fileURI(path)
|
||||
uriC := C.CString(uri)
|
||||
uriName := C.CString("uri")
|
||||
C.vt_gst_set_str(playbin, uriName, uriC)
|
||||
C.free(unsafe.Pointer(uriName))
|
||||
C.free(unsafe.Pointer(uriC))
|
||||
videoSinkName := C.CString("video-sink")
|
||||
C.vt_gst_set_obj(playbin, videoSinkName, C.gpointer(appsink))
|
||||
C.free(unsafe.Pointer(videoSinkName))
|
||||
audioSinkName := C.CString("audio-sink")
|
||||
C.vt_gst_set_obj(playbin, audioSinkName, C.gpointer(audioSink))
|
||||
C.free(unsafe.Pointer(audioSinkName))
|
||||
|
||||
if p.volume <= 0 {
|
||||
p.volume = 1.0
|
||||
}
|
||||
volumeName := C.CString("volume")
|
||||
C.vt_gst_set_float(playbin, volumeName, C.gdouble(p.volume))
|
||||
C.free(unsafe.Pointer(volumeName))
|
||||
|
||||
p.pipeline = playbin
|
||||
p.appsink = appsink
|
||||
p.paused = true
|
||||
|
||||
C.gst_element_set_state(playbin, C.GST_STATE_PAUSED)
|
||||
if offset > 0 {
|
||||
_ = p.seekLocked(offset)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) Play() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.pipeline == nil {
|
||||
return errors.New("no pipeline loaded")
|
||||
}
|
||||
C.gst_element_set_state(p.pipeline, C.GST_STATE_PLAYING)
|
||||
p.paused = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) Pause() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.pipeline == nil {
|
||||
return errors.New("no pipeline loaded")
|
||||
}
|
||||
C.gst_element_set_state(p.pipeline, C.GST_STATE_PAUSED)
|
||||
p.paused = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) SeekToTime(offset time.Duration) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.seekLocked(offset)
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) seekLocked(offset time.Duration) error {
|
||||
if p.pipeline == nil {
|
||||
return errors.New("no pipeline loaded")
|
||||
}
|
||||
nanos := C.gint64(offset.Nanoseconds())
|
||||
flags := C.GST_SEEK_FLAG_FLUSH | C.GST_SEEK_FLAG_KEY_UNIT
|
||||
if C.gst_element_seek_simple(p.pipeline, C.GST_FORMAT_TIME, flags, nanos) == 0 {
|
||||
return errors.New("gstreamer seek failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) SeekToFrame(frame int64) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.fps <= 0 {
|
||||
return nil
|
||||
}
|
||||
seconds := float64(frame) / p.fps
|
||||
return p.seekLocked(time.Duration(seconds * float64(time.Second)))
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) GetCurrentTime() time.Duration {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.pipeline == nil {
|
||||
return 0
|
||||
}
|
||||
var pos C.gint64
|
||||
if C.gst_element_query_position(p.pipeline, C.GST_FORMAT_TIME, &pos) == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(pos)
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) GetFrameImage() (*image.RGBA, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.appsink == nil {
|
||||
return nil, errors.New("gstreamer appsink unavailable")
|
||||
}
|
||||
sample := C.gst_app_sink_try_pull_sample((*C.GstAppSink)(unsafe.Pointer(p.appsink)), 0)
|
||||
if sample == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer C.gst_sample_unref(sample)
|
||||
|
||||
caps := C.gst_sample_get_caps(sample)
|
||||
if caps == nil {
|
||||
return nil, errors.New("gstreamer caps unavailable")
|
||||
}
|
||||
str := C.gst_caps_get_structure(caps, 0)
|
||||
var width C.gint
|
||||
var height C.gint
|
||||
widthName := C.CString("width")
|
||||
C.gst_structure_get_int(str, widthName, &width)
|
||||
C.free(unsafe.Pointer(widthName))
|
||||
heightName := C.CString("height")
|
||||
C.gst_structure_get_int(str, heightName, &height)
|
||||
C.free(unsafe.Pointer(heightName))
|
||||
if width > 0 && height > 0 {
|
||||
p.width = int(width)
|
||||
p.height = int(height)
|
||||
}
|
||||
var fpsNum C.gint
|
||||
var fpsDen C.gint
|
||||
fpsName := C.CString("framerate")
|
||||
if C.gst_structure_get_fraction(str, fpsName, &fpsNum, &fpsDen) != 0 && fpsDen != 0 {
|
||||
p.fps = float64(fpsNum) / float64(fpsDen)
|
||||
}
|
||||
C.free(unsafe.Pointer(fpsName))
|
||||
|
||||
buffer := C.gst_sample_get_buffer(sample)
|
||||
if buffer == nil {
|
||||
return nil, errors.New("gstreamer buffer unavailable")
|
||||
}
|
||||
var mapInfo C.GstMapInfo
|
||||
if C.gst_buffer_map(buffer, &mapInfo, C.GST_MAP_READ) == 0 {
|
||||
return nil, errors.New("gstreamer buffer map failed")
|
||||
}
|
||||
defer C.gst_buffer_unmap(buffer, &mapInfo)
|
||||
|
||||
if p.width == 0 || p.height == 0 {
|
||||
return nil, errors.New("invalid frame size")
|
||||
}
|
||||
frameSize := p.width * p.height * 4
|
||||
if int(mapInfo.size) < frameSize {
|
||||
return nil, errors.New("incomplete frame")
|
||||
}
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, p.width, p.height))
|
||||
data := unsafe.Slice((*byte)(unsafe.Pointer(mapInfo.data)), frameSize)
|
||||
copy(img.Pix, data)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) SetVolume(level float64) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.volume = level
|
||||
if p.pipeline != nil {
|
||||
volumeName := C.CString("volume")
|
||||
C.vt_gst_set_float(p.pipeline, volumeName, C.gdouble(level))
|
||||
C.free(unsafe.Pointer(volumeName))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.closeLocked()
|
||||
}
|
||||
|
||||
func (p *GStreamerPlayer) closeLocked() {
|
||||
if p.pipeline != nil {
|
||||
C.gst_element_set_state(p.pipeline, C.GST_STATE_NULL)
|
||||
C.gst_object_unref(C.gpointer(p.pipeline))
|
||||
p.pipeline = nil
|
||||
}
|
||||
if p.appsink != nil {
|
||||
C.gst_object_unref(C.gpointer(p.appsink))
|
||||
p.appsink = nil
|
||||
}
|
||||
}
|
||||
|
||||
func fileURI(path string) string {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
abs = path
|
||||
}
|
||||
abs = filepath.ToSlash(abs)
|
||||
if runtime.GOOS == "windows" && len(abs) >= 2 && abs[1] == ':' {
|
||||
abs = "/" + abs
|
||||
}
|
||||
u := url.URL{Scheme: "file", Path: abs}
|
||||
return u.String()
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ import (
|
|||
// UnifiedPlayerAdapter wraps UnifiedPlayer to provide playSession interface compatibility
|
||||
// This allows seamless replacement of the dual-process player with UnifiedPlayer
|
||||
type UnifiedPlayerAdapter struct {
|
||||
// Core UnifiedPlayer
|
||||
player *UnifiedPlayer
|
||||
// Frame-capable player backend
|
||||
player framePlayer
|
||||
|
||||
// Interface compatibility fields (from playSession)
|
||||
path string
|
||||
|
|
@ -64,7 +64,7 @@ func NewUnifiedPlayerAdapter(path string, width, height int, fps, duration float
|
|||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
// Create UnifiedPlayer with proper configuration
|
||||
// Create frame-capable player with proper configuration
|
||||
config := Config{
|
||||
Backend: BackendAuto, // Use auto for UnifiedPlayer
|
||||
WindowX: 0,
|
||||
|
|
@ -83,23 +83,10 @@ func NewUnifiedPlayerAdapter(path string, width, height int, fps, duration float
|
|||
LogLevel: 3, // Debug
|
||||
}
|
||||
|
||||
adapter.player = NewUnifiedPlayer(config)
|
||||
|
||||
// Set up callbacks for progress and frame updates
|
||||
adapter.player.SetTimeCallback(func(d time.Duration) {
|
||||
seconds := d.Seconds()
|
||||
adapter.current = seconds
|
||||
if adapter.prog != nil {
|
||||
adapter.prog(seconds)
|
||||
}
|
||||
})
|
||||
|
||||
adapter.player.SetFrameCallback(func(frame int64) {
|
||||
adapter.frameN = int(frame)
|
||||
if adapter.frameFunc != nil {
|
||||
adapter.frameFunc(int(frame))
|
||||
}
|
||||
})
|
||||
playerBackend, err := newFramePlayer(config)
|
||||
if err == nil {
|
||||
adapter.player = playerBackend
|
||||
}
|
||||
|
||||
return adapter
|
||||
}
|
||||
|
|
@ -328,7 +315,11 @@ func (p *UnifiedPlayerAdapter) startFrameDisplayLoop() {
|
|||
|
||||
go func() {
|
||||
// Display at frame rate
|
||||
frameDuration := time.Second / time.Duration(p.fps)
|
||||
fps := p.fps
|
||||
if fps <= 0 {
|
||||
fps = 24
|
||||
}
|
||||
frameDuration := time.Second / time.Duration(fps)
|
||||
ticker := time.NewTicker(frameDuration)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,17 @@ mkdir -p "$GOCACHE" "$GOMODCACHE"
|
|||
if [ -d "$PROJECT_ROOT/vendor" ] && [ ! -f "$PROJECT_ROOT/vendor/modules.txt" ]; then
|
||||
export GOFLAGS="${GOFLAGS:-} -mod=mod"
|
||||
fi
|
||||
GST_TAG=""
|
||||
if [ -n "$VT_GSTREAMER" ]; then
|
||||
GST_TAG="gstreamer"
|
||||
elif command -v pkg-config &> /dev/null; then
|
||||
if pkg-config --exists gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0; then
|
||||
GST_TAG="gstreamer"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$GST_TAG" ]; then
|
||||
export GOFLAGS="${GOFLAGS:-} -tags ${GST_TAG}"
|
||||
fi
|
||||
if go build -o "$BUILD_OUTPUT" .; then
|
||||
echo "Build successful! (VideoTools $APP_VERSION)"
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ case "$OS" in
|
|||
|
||||
echo "Building VideoTools $APP_VERSION for Windows..."
|
||||
export CGO_ENABLED=1
|
||||
if [ -n "$VT_GSTREAMER" ] || command -v gst-launch-1.0 &> /dev/null; then
|
||||
export GOFLAGS="${GOFLAGS:-} -tags gstreamer"
|
||||
fi
|
||||
if [ -d "$PROJECT_ROOT/vendor" ] && [ ! -f "$PROJECT_ROOT/vendor/modules.txt" ]; then
|
||||
export GOFLAGS="${GOFLAGS:-} -mod=mod"
|
||||
fi
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user