From 6cd31fc71562318f816a61acf939fc50c0c48427 Mon Sep 17 00:00:00 2001 From: milarin Date: Wed, 20 Dec 2023 20:50:11 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + chapter.go | 26 +++++++++ go.mod | 5 ++ go.sum | 2 + props_get.go | 133 +++++++++++++++++++++++++++++++++++++++++++++++ props_observe.go | 77 +++++++++++++++++++++++++++ props_set.go | 19 +++++++ send_command.go | 121 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 385 insertions(+) create mode 100644 .gitignore create mode 100644 chapter.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 props_get.go create mode 100644 props_observe.go create mode 100644 props_set.go create mode 100644 send_command.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e88b65f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +*_test.go \ No newline at end of file diff --git a/chapter.go b/chapter.go new file mode 100644 index 0000000..b43c405 --- /dev/null +++ b/chapter.go @@ -0,0 +1,26 @@ +package mpvipc + +import ( + "encoding/json" + "time" +) + +type Chapter struct { + Title string `json:"title"` + Time time.Duration `json:"time"` +} + +func (c *Chapter) UnmarshalJSON(data []byte) error { + m := &struct { + Title string `json:"title"` + Time float64 `json:"time"` + }{} + + if err := json.Unmarshal(data, m); err != nil { + return err + } + + c.Title = m.Title + c.Time = time.Duration(m.Time * float64(time.Second)) + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5c74ef7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.milar.in/milarin/mpvipc + +go 1.21.5 + +require git.milar.in/milarin/channel v0.1.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..62d476d --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +git.milar.in/milarin/channel v0.1.1 h1:s8+BdiOMmuRUDmChQ2i4G5GWsDCK9tKNHt1knLJx9zM= +git.milar.in/milarin/channel v0.1.1/go.mod h1:We83LTI8S7u7II3pD+A2ChCDWJfCkcBUCUqii9HjTtM= diff --git a/props_get.go b/props_get.go new file mode 100644 index 0000000..ea62585 --- /dev/null +++ b/props_get.go @@ -0,0 +1,133 @@ +package mpvipc + +import "time" + +// list of (almost) all properties can be found in official docs: +// https://mpv.io/manual/master/#properties + +func IsFullscreen(socket string) (bool, error) { + return GetProperty[bool](socket, "fullscreen") +} + +func IsPaused(socket string) (bool, error) { + return GetProperty[bool](socket, "pause") +} + +func GetFilename(socket string) (string, error) { + return GetProperty[string](socket, "filename") +} + +func GetEstimatedFrameCount(socket string) (int, error) { + return GetProperty[int](socket, "estimated-frame-count") +} + +func GetEstimatedFrameNumber(socket string) (int, error) { + return GetProperty[int](socket, "estimated-frame-number") +} + +func GetPID(socket string) (int, error) { + return GetProperty[int](socket, "pid") +} + +func GetPath(socket string) (string, error) { + return GetProperty[string](socket, "path") +} + +func GetMediaTitle(socket string) (string, error) { + return GetProperty[string](socket, "media-title") +} + +func GetFileFormat(socket string) (string, error) { + return GetProperty[string](socket, "file-format") +} + +func GetDuration(socket string) (time.Duration, error) { + durationInSeconds, err := GetProperty[float64](socket, "duration") + if err != nil { + return 0, err + } + + return time.Duration(durationInSeconds * float64(time.Second)), nil +} + +func GetPercentPos(socket string) (float64, error) { + return GetProperty[float64](socket, "percent-pos") +} + +func GetTimePos(socket string) (time.Duration, error) { + durationInSeconds, err := GetProperty[float64](socket, "time-pos") + if err != nil { + return 0, err + } + + return time.Duration(durationInSeconds * float64(time.Second)), nil +} + +func GetTimeRemaining(socket string) (time.Duration, error) { + durationInSeconds, err := GetProperty[float64](socket, "time-remaining") + if err != nil { + return 0, err + } + + return time.Duration(durationInSeconds * float64(time.Second)), nil +} + +func IsSeeking(socket string) (bool, error) { + return GetProperty[bool](socket, "seeking") +} + +func GetVideoFormat(socket string) (string, error) { + return GetProperty[string](socket, "video-format") +} + +func GetVideoSize(socket string) (int, int, error) { + width, err := GetProperty[int](socket, "width") + if err != nil { + return 0, 0, err + } + + height, err := GetProperty[int](socket, "height") + if err != nil { + return 0, 0, err + } + + return width, height, nil +} + +func GetVideoDisplaySize(socket string) (int, int, error) { + width, err := GetProperty[int](socket, "dwidth") + if err != nil { + return 0, 0, err + } + + height, err := GetProperty[int](socket, "dheight") + if err != nil { + return 0, 0, err + } + + return width, height, nil +} + +func GetWindowID(socket string) (int64, error) { + return GetProperty[int64](socket, "window-id") +} + +func GetChapterList(socket string) ([]Chapter, error) { + return GetProperty[[]Chapter](socket, "chapter-list") +} + +func IsSeekable(socket string) (bool, error) { + return GetProperty[bool](socket, "seekable") +} + +func IsPartiallySeekable(socket string) (bool, error) { + return GetProperty[bool](socket, "partially-seekable") +} + +func IsPlaybackAborted(socket string) (bool, error) { + return GetProperty[bool](socket, "playback-abort") +} + +func GetPropertyList(socket string) ([]string, error) { + return GetProperty[[]string](socket, "property-list") +} diff --git a/props_observe.go b/props_observe.go new file mode 100644 index 0000000..6496e9a --- /dev/null +++ b/props_observe.go @@ -0,0 +1,77 @@ +package mpvipc + +import ( + "context" + "time" + + "git.milar.in/milarin/channel" +) + +func ObserveFullscreen(ctx context.Context, socket string) (<-chan bool, error) { + return ObserveProperty[bool](ctx, socket, "fullscreen") +} + +func ObservePaused(ctx context.Context, socket string) (<-chan bool, error) { + return ObserveProperty[bool](ctx, socket, "pause") +} + +func ObserveFilename(ctx context.Context, socket string) (<-chan string, error) { + return ObserveProperty[string](ctx, socket, "filename") +} + +func ObservePath(ctx context.Context, socket string) (<-chan string, error) { + return ObserveProperty[string](ctx, socket, "path") +} + +func ObserveMediaTitle(ctx context.Context, socket string) (<-chan string, error) { + return ObserveProperty[string](ctx, socket, "media-title") +} + +func ObserveFileFormat(ctx context.Context, socket string) (<-chan string, error) { + return ObserveProperty[string](ctx, socket, "file-format") +} + +func ObserveDuration(ctx context.Context, socket string) (<-chan time.Duration, error) { + out, err := ObserveProperty[float64](ctx, socket, "duration") + if err != nil { + return nil, err + } + + return channel.MapSuccessive(out, func(v float64) time.Duration { + return time.Duration(v * float64(time.Second)) + }), nil +} + +func ObservePercentPos(ctx context.Context, socket string) (<-chan float64, error) { + return ObserveProperty[float64](ctx, socket, "percent-pos") +} + +func ObserveTimePos(ctx context.Context, socket string) (<-chan time.Duration, error) { + out, err := ObserveProperty[float64](ctx, socket, "time-pos") + if err != nil { + return nil, err + } + + return channel.MapSuccessive(out, func(v float64) time.Duration { + return time.Duration(v * float64(time.Second)) + }), nil +} + +func ObserveTimeRemaining(ctx context.Context, socket string) (<-chan time.Duration, error) { + out, err := ObserveProperty[float64](ctx, socket, "time-remaining") + if err != nil { + return nil, err + } + + return channel.MapSuccessive(out, func(v float64) time.Duration { + return time.Duration(v * float64(time.Second)) + }), nil +} + +func ObserveVideoFormat(ctx context.Context, socket string) (<-chan string, error) { + return ObserveProperty[string](ctx, socket, "video-format") +} + +func ObservePlaybackAborted(ctx context.Context, socket string) (<-chan bool, error) { + return ObserveProperty[bool](ctx, socket, "playback-abort") +} diff --git a/props_set.go b/props_set.go new file mode 100644 index 0000000..c0dc0d1 --- /dev/null +++ b/props_set.go @@ -0,0 +1,19 @@ +package mpvipc + +import "time" + +func SetFullscreen(socket string, fullscreen bool) error { + return SetProperty[bool](socket, "fullscreen", fullscreen) +} + +func SetPause(socket string, pause bool) error { + return SetProperty[bool](socket, "pause", pause) +} + +func SetTimePos(socket string, timePos time.Duration) error { + return SetProperty[float64](socket, "time-pos", float64(timePos)/float64(time.Second)) +} + +func SetPercentPos(socket string, percentPos float64) error { + return SetProperty[float64](socket, "percent-pos", percentPos) +} diff --git a/send_command.go b/send_command.go new file mode 100644 index 0000000..51b1b80 --- /dev/null +++ b/send_command.go @@ -0,0 +1,121 @@ +package mpvipc + +import ( + "context" + "encoding/json" + "errors" + "net" +) + +type Command struct { + Command []interface{} `json:"command"` +} + +type Response[T any] struct { + Data T `json:"data"` + RequestID int `json:"request_id"` + Error string `json:"error"` +} + +type Event[T any] struct { + Data T `json:"data"` + ID int `json:"id"` + Name string `json:"name"` + Error string `json:"error"` +} + +func SendCommand[T any](socket string, cmd *Command) (*Response[T], error) { + conn, err := net.Dial("unix", socket) + if err != nil { + return nil, err + } + defer conn.Close() + + if err := json.NewEncoder(conn).Encode(cmd); err != nil { + return nil, err + } + + resp := &Response[T]{} + if err := json.NewDecoder(conn).Decode(resp); err != nil { + return nil, err + } + + return resp, nil +} + +func GetProperty[T any](socket string, propertyName string) (T, error) { + cmd := &Command{[]interface{}{"get_property", propertyName}} + resp, err := SendCommand[T](socket, cmd) + if err != nil { + return *new(T), err + } + + if resp.Error != "success" { + return *new(T), errors.New(resp.Error) + } + + return resp.Data, nil +} + +func SetProperty[T any](socket string, propertyName string, propertyValue T) error { + cmd := &Command{[]interface{}{"set_property", propertyName, propertyValue}} + resp, err := SendCommand[T](socket, cmd) + if err != nil { + return err + } + + if resp.Error != "success" { + return errors.New(resp.Error) + } + + return nil +} + +func ObserveProperty[T any](ctx context.Context, socket string, propertyName string) (<-chan T, error) { + out := make(chan T, 10) + + conn, err := net.Dial("unix", socket) + if err != nil { + return nil, err + } + + enc := json.NewEncoder(conn) + dec := json.NewDecoder(conn) + + cmd := &Command{[]interface{}{"observe_property", 1, propertyName}} + if err := enc.Encode(cmd); err != nil { + conn.Close() + return nil, err + } + + resp := &Response[T]{} + if err := json.NewDecoder(conn).Decode(resp); err != nil { + conn.Close() + return nil, err + } + + if resp.Error != "success" { + conn.Close() + return nil, errors.New(resp.Error) + } + + go func() { + defer conn.Close() + defer enc.Encode(&Command{[]interface{}{"unobserve_property", 0}}) + <-ctx.Done() + }() + + go func() { + defer close(out) + + for ctx.Err() == nil { + event := Event[T]{} + if err := dec.Decode(&event); err != nil { + break + } + out <- event.Data + } + }() + + return out, nil +}