├── LICENSE ├── README.md ├── client.go ├── examples ├── http │ └── main.go └── rpc │ └── main.go ├── http.go ├── ipc.go └── rpc.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Benedikt Lang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mpv (remote) control library 2 | ====== 3 | 4 | This library provides everything needed to (remote) control the [mpv media player](https://mpv.io/). 5 | 6 | It provides an easy api, a json api and rpc functionality. 7 | 8 | Usecases: Remote control your mediaplayer running on a raspberry pi or laptop or build a http interface for mpv 9 | 10 | Usage 11 | ----- 12 | 13 | ```bash 14 | $ go get github.com/blang/mpv 15 | ``` 16 | Note: Always vendor your dependencies or fix on a specific version tag. 17 | 18 | Start mpv: 19 | ```bash 20 | $ mpv --idle --input-ipc-server=/tmp/mpvsocket 21 | ``` 22 | 23 | Remote control: 24 | ```go 25 | import github.com/blang/mpv 26 | 27 | ipcc := mpv.NewIPCClient("/tmp/mpvsocket") // Lowlevel client 28 | c := mpv.NewClient(ipcc) // Highlevel client, can also use RPCClient 29 | 30 | c.LoadFile("movie.mp4", mpv.LoadFileModeReplace) 31 | c.SetPause(true) 32 | c.Seek(600, mpv.SeekModeAbsolute) 33 | c.SetFullscreen(true) 34 | c.SetPause(false) 35 | 36 | pos, err := c.Position() 37 | fmt.Printf("Position in Seconds: %.0f", pos) 38 | ``` 39 | 40 | Also check the [GoDocs](http://godoc.org/github.com/blang/mpv). 41 | 42 | 43 | Features 44 | ----- 45 | 46 | - Low-Level and High-Level API 47 | - RPC Server and Client (fully transparent) 48 | - HTTP Handler exposing lowlevel API (json) 49 | 50 | 51 | Contribution 52 | ----- 53 | 54 | Feel free to make a pull request. For bigger changes create a issue first to discuss about it. 55 | 56 | 57 | License 58 | ----- 59 | 60 | See [LICENSE](LICENSE) file. 61 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package mpv 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // Client is a more comfortable higher level interface 10 | // to LLClient. It can use any LLClient implementation. 11 | type Client struct { 12 | LLClient 13 | } 14 | 15 | // NewClient creates a new highlevel client based on a lowlevel client. 16 | func NewClient(llclient LLClient) *Client { 17 | return &Client{ 18 | llclient, 19 | } 20 | } 21 | 22 | // Mode options for Loadfile 23 | const ( 24 | LoadFileModeReplace = "replace" 25 | LoadFileModeAppend = "append" 26 | LoadFileModeAppendPlay = "append-play" // Starts if nothing is playing 27 | ) 28 | 29 | // Loadfile loads a file, it either replaces the currently playing file (LoadFileModeReplace), 30 | // appends to the current playlist (LoadFileModeAppend) or appends to playlist and plays if 31 | // nothing is playing right now (LoadFileModeAppendPlay) 32 | func (c *Client) Loadfile(path string, mode string) error { 33 | _, err := c.Exec("loadfile", path, mode) 34 | return err 35 | } 36 | 37 | // Mode options for Seek 38 | const ( 39 | SeekModeRelative = "relative" 40 | SeekModeAbsolute = "absolute" 41 | ) 42 | 43 | // Seek seeks to a position in the current file. 44 | // Use mode to seek relative to current position (SeekModeRelative) or absolute (SeekModeAbsolute). 45 | func (c *Client) Seek(n int, mode string) error { 46 | _, err := c.Exec("seek", strconv.Itoa(n), mode) 47 | return err 48 | } 49 | 50 | // PlaylistNext plays the next playlistitem or NOP if no item is available. 51 | func (c *Client) PlaylistNext() error { 52 | _, err := c.Exec("playlist-next", "weak") 53 | return err 54 | } 55 | 56 | // PlaylistPrevious plays the previous playlistitem or NOP if no item is available. 57 | func (c *Client) PlaylistPrevious() error { 58 | _, err := c.Exec("playlist-prev", "weak") 59 | return err 60 | } 61 | 62 | // Mode options for LoadList 63 | const ( 64 | LoadListModeReplace = "replace" 65 | LoadListModeAppend = "append" 66 | ) 67 | 68 | // LoadList loads a playlist from path. It can either replace the current playlist (LoadListModeReplace) 69 | // or append to the current playlist (LoadListModeAppend). 70 | func (c *Client) LoadList(path string, mode string) error { 71 | _, err := c.Exec("loadlist", path, mode) 72 | return err 73 | } 74 | 75 | // GetProperty reads a property by name and returns the data as a string. 76 | func (c *Client) GetProperty(name string) (string, error) { 77 | res, err := c.Exec("get_property", name) 78 | if res == nil { 79 | return "", err 80 | } 81 | return fmt.Sprintf("%#v", res.Data), err 82 | } 83 | 84 | // SetProperty sets the value of a property. 85 | func (c *Client) SetProperty(name string, value interface{}) error { 86 | _, err := c.Exec("set_property", name, value) 87 | return err 88 | } 89 | 90 | // ErrInvalidType is returned if the response data does not match the methods return type. 91 | // Use GetProperty or find matching type in mpv docs. 92 | var ErrInvalidType = errors.New("Invalid type") 93 | 94 | // GetFloatProperty reads a float property and returns the data as a float64. 95 | func (c *Client) GetFloatProperty(name string) (float64, error) { 96 | res, err := c.Exec("get_property", name) 97 | if res == nil { 98 | return 0, err 99 | } 100 | if val, found := res.Data.(float64); found { 101 | return val, err 102 | } 103 | return 0, ErrInvalidType 104 | } 105 | 106 | // GetBoolProperty reads a bool property and returns the data as a boolean. 107 | func (c *Client) GetBoolProperty(name string) (bool, error) { 108 | res, err := c.Exec("get_property", name) 109 | if res == nil { 110 | return false, err 111 | } 112 | if val, found := res.Data.(bool); found { 113 | return val, err 114 | } 115 | return false, ErrInvalidType 116 | } 117 | 118 | // Filename returns the currently playing filename 119 | func (c *Client) Filename() (string, error) { 120 | return c.GetProperty("filename") 121 | } 122 | 123 | // Path returns the currently playing path 124 | func (c *Client) Path() (string, error) { 125 | return c.GetProperty("path") 126 | } 127 | 128 | // Pause returns true if the player is paused 129 | func (c *Client) Pause() (bool, error) { 130 | return c.GetBoolProperty("pause") 131 | } 132 | 133 | // SetPause pauses or unpauses the player 134 | func (c *Client) SetPause(pause bool) error { 135 | return c.SetProperty("pause", pause) 136 | } 137 | 138 | // Idle returns true if the player is idle 139 | func (c *Client) Idle() (bool, error) { 140 | return c.GetBoolProperty("idle") 141 | } 142 | 143 | // Mute returns true if the player is muted. 144 | func (c *Client) Mute() (bool, error) { 145 | return c.GetBoolProperty("mute") 146 | } 147 | 148 | // SetMute mutes or unmutes the player. 149 | func (c *Client) SetMute(mute bool) error { 150 | return c.SetProperty("mute", mute) 151 | } 152 | 153 | // Fullscreen returns true if the player is in fullscreen mode. 154 | func (c *Client) Fullscreen() (bool, error) { 155 | return c.GetBoolProperty("fullscreen") 156 | } 157 | 158 | // SetFullscreen activates/deactivates the fullscreen mode. 159 | func (c *Client) SetFullscreen(v bool) error { 160 | return c.SetProperty("fullscreen", v) 161 | } 162 | 163 | // Volume returns the current volume level. 164 | func (c *Client) Volume() (float64, error) { 165 | return c.GetFloatProperty("volume") 166 | } 167 | 168 | // Speed returns the current playback speed. 169 | func (c *Client) Speed() (float64, error) { 170 | return c.GetFloatProperty("speed") 171 | } 172 | 173 | // Duration returns the duration of the currently playing file. 174 | func (c *Client) Duration() (float64, error) { 175 | return c.GetFloatProperty("duration") 176 | } 177 | 178 | // Position returns the current playback position in seconds. 179 | func (c *Client) Position() (float64, error) { 180 | return c.GetFloatProperty("time-pos") 181 | } 182 | 183 | // PercentPosition returns the current playback position in percent. 184 | func (c *Client) PercentPosition() (float64, error) { 185 | return c.GetFloatProperty("percent-pos") 186 | } 187 | -------------------------------------------------------------------------------- /examples/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/blang/mpv" 10 | ) 11 | 12 | func main() { 13 | mpvll := mpv.NewIPCClient("/tmp/mpvsocket") 14 | mpvc := mpv.Client{mpvll} 15 | 16 | // Build in low level json api 17 | http.Handle("/lowlevel", mpv.HTTPServerHandler(mpvll)) 18 | 19 | // Your own api based on mpv.Client 20 | http.HandleFunc("/file/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | log.Printf("Request URL: %s", r.RequestURI) 22 | path := strings.Replace(r.RequestURI, "/file/", "", 1) 23 | fmt.Fprintln(w, path) 24 | log.Println(mpvc.Loadfile(path, mpv.LoadFileModeAppendPlay)) 25 | })) 26 | http.HandleFunc("/playlist/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | log.Printf("Request URL: %s", r.RequestURI) 28 | path := strings.Replace(r.RequestURI, "/playlist/", "", 1) 29 | fmt.Fprintln(w, path) 30 | log.Println(mpvc.LoadList(path, mpv.LoadListModeReplace)) 31 | })) 32 | http.HandleFunc("/cmd/seek", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | fmt.Fprintln(w, mpvc.Seek(10, mpv.SeekModeAbsolute)) 34 | })) 35 | http.HandleFunc("/cmd/pause", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | fmt.Fprintln(w, mpvc.SetPause(true)) 37 | })) 38 | http.HandleFunc("/cmd/prev", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | fmt.Fprintln(w, mpvc.PlaylistPrevious()) 40 | })) 41 | http.HandleFunc("/cmd/next", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 | fmt.Fprintln(w, mpvc.PlaylistNext()) 43 | })) 44 | http.HandleFunc("/value/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | name := strings.Replace(r.RequestURI, "/value/", "", 1) 46 | value, err := mpvc.GetProperty(name) 47 | if err != nil { 48 | fmt.Fprintf(w, "Error: %s", err) 49 | return 50 | } 51 | fmt.Fprintf(w, "Value: %s", value) 52 | })) 53 | http.HandleFunc("/cmd/fullscreen", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | err := mpvc.SetProperty("fullscreen", true) 55 | if err != nil { 56 | fmt.Fprintf(w, "Error: %s", err) 57 | return 58 | } 59 | fmt.Fprintf(w, "ok") 60 | })) 61 | log.Fatal(http.ListenAndServe(":8080", nil)) 62 | } 63 | -------------------------------------------------------------------------------- /examples/rpc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "net/http" 7 | "net/rpc" 8 | 9 | "github.com/blang/mpv" 10 | ) 11 | 12 | func main() { 13 | ll := mpv.NewIPCClient("/tmp/mpvsocket") 14 | s := mpv.NewRPCServer(ll) 15 | rpc.Register(s) 16 | rpc.HandleHTTP() 17 | l, err := net.Listen("tcp", ":9999") 18 | if err != nil { 19 | log.Fatal("Listen error: ", err) 20 | } 21 | go http.Serve(l, nil) 22 | 23 | // Client 24 | client, err := rpc.DialHTTP("tcp", "127.0.0.1:9999") 25 | if err != nil { 26 | log.Fatal("Listen error: ", err) 27 | } 28 | 29 | rpcc := mpv.NewRPCClient(client) 30 | c := mpv.NewClient(rpcc) 31 | c.SetFullscreen(false) 32 | c.SetPause(true) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package mpv 2 | 3 | import "encoding/json" 4 | import "net/http" 5 | 6 | // JSONRequest send to the server. 7 | type JSONRequest struct { 8 | Command []interface{} `json:"command"` 9 | } 10 | 11 | // JSONResponse send from the server. 12 | type JSONResponse struct { 13 | Err string `json:"error"` 14 | Data interface{} `json:"data"` // May contain float64, bool or string 15 | } 16 | 17 | type httpServerHandler struct { 18 | llclient LLClient 19 | } 20 | 21 | // HTTPServerHandler returns a http.Handler to access a client via a lowlevel json-api. 22 | // Register as route on your server: 23 | // http.Handle("/mpv", mpv.HTTPHandler(lowlevelclient) 24 | // 25 | // Use api: 26 | // POST http://host/lowlevel Body: { "command": ["get_property", "fullscreen"] } 27 | // 28 | // Result: 29 | // {"error":"success","data":false} 30 | func HTTPServerHandler(client LLClient) http.Handler { 31 | return &httpServerHandler{ 32 | llclient: client, 33 | } 34 | } 35 | 36 | func (h *httpServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 37 | if r.Method != http.MethodPost { 38 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 39 | return 40 | } 41 | var req JSONRequest 42 | dec := json.NewDecoder(r.Body) 43 | err := dec.Decode(&req) 44 | if err != nil { 45 | http.Error(w, "Can not decode request", http.StatusBadRequest) 46 | return 47 | } 48 | resp, err := h.llclient.Exec(req.Command...) 49 | if err != nil { 50 | if err == ErrTimeoutRecv || err == ErrTimeoutSend { 51 | http.Error(w, "Timeout", http.StatusGatewayTimeout) 52 | return 53 | } 54 | // TODO: Handle error, maybe send json response 55 | http.Error(w, "Client returned unknown error", http.StatusInternalServerError) 56 | return 57 | } 58 | jsonResp := JSONResponse{ 59 | Err: resp.Err, 60 | Data: resp.Data, 61 | } 62 | b, err := json.Marshal(jsonResp) 63 | if err != nil { 64 | http.Error(w, "", http.StatusInternalServerError) 65 | return 66 | } 67 | _, err = w.Write(b) 68 | if err != nil { 69 | http.Error(w, "", http.StatusInternalServerError) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ipc.go: -------------------------------------------------------------------------------- 1 | package mpv 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "math/rand" 9 | "net" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Response received from mpv. Can be an event or a user requested response. 15 | type Response struct { 16 | Err string `json:"error"` 17 | Data interface{} `json:"data"` // May contain float64, bool or string 18 | Event string `json:"event"` 19 | RequestID int `json:"request_id"` 20 | } 21 | 22 | // request sent to mpv. Includes request_id for mapping the response. 23 | type request struct { 24 | Command []interface{} `json:"command"` 25 | RequestID int `json:"request_id"` 26 | Response chan *Response `json:"-"` 27 | } 28 | 29 | func newRequest(cmd ...interface{}) *request { 30 | return &request{ 31 | Command: cmd, 32 | RequestID: rand.Intn(10000), 33 | Response: make(chan *Response, 1), 34 | } 35 | } 36 | 37 | // LLClient is the most low level interface 38 | type LLClient interface { 39 | Exec(command ...interface{}) (*Response, error) 40 | } 41 | 42 | // IPCClient is a low-level IPC client to communicate with the mpv player via socket. 43 | type IPCClient struct { 44 | socket string 45 | timeout time.Duration 46 | comm chan *request 47 | 48 | mu sync.Mutex 49 | reqMap map[int]*request // Maps RequestIDs to Requests for response association 50 | } 51 | 52 | // NewIPCClient creates a new IPCClient connected to the given socket. 53 | func NewIPCClient(socket string) *IPCClient { 54 | c := &IPCClient{ 55 | socket: socket, 56 | timeout: 2 * time.Second, 57 | comm: make(chan *request), 58 | reqMap: make(map[int]*request), 59 | } 60 | c.run() 61 | return c 62 | } 63 | 64 | // dispatch dispatches responses to the corresponding request 65 | func (c *IPCClient) dispatch(resp *Response) { 66 | if resp.Event == "" { // No Event 67 | c.mu.Lock() 68 | defer c.mu.Unlock() 69 | if req, ok := c.reqMap[resp.RequestID]; ok { // Lookup requestID in request map 70 | delete(c.reqMap, resp.RequestID) 71 | req.Response <- resp 72 | return 73 | } 74 | // Discard response 75 | } else { // Event 76 | // TODO: Implement Event support 77 | } 78 | } 79 | 80 | func (c *IPCClient) run() { 81 | conn, err := net.Dial("unix", c.socket) 82 | if err != nil { 83 | panic(err) 84 | } 85 | go c.readloop(conn) 86 | go c.writeloop(conn) 87 | // TODO: Close connection 88 | } 89 | 90 | func (c *IPCClient) writeloop(conn io.Writer) { 91 | for { 92 | req, ok := <-c.comm 93 | if !ok { 94 | panic("Communication channel closed") 95 | } 96 | b, err := json.Marshal(req) 97 | if err != nil { 98 | // TODO: Discard request, maybe send error downstream 99 | // log.Printf("Discard request %v with error: %s", req, err) 100 | continue 101 | } 102 | c.mu.Lock() 103 | c.reqMap[req.RequestID] = req 104 | c.mu.Unlock() 105 | b = append(b, '\n') 106 | _, err = conn.Write(b) 107 | if err != nil { 108 | // TODO: Discard request, maybe send error downstream 109 | // TODO: Remove from reqMap? 110 | } 111 | } 112 | } 113 | 114 | func (c *IPCClient) readloop(conn io.Reader) { 115 | rd := bufio.NewReader(conn) 116 | for { 117 | data, err := rd.ReadBytes('\n') 118 | if err != nil { 119 | // TODO: Handle error 120 | continue 121 | } 122 | var resp Response 123 | err = json.Unmarshal(data, &resp) 124 | if err != nil { 125 | // TODO: Handle error 126 | continue 127 | } 128 | c.dispatch(&resp) 129 | } 130 | } 131 | 132 | // Timeout errors while communicating via IPC 133 | var ( 134 | ErrTimeoutSend = errors.New("Timeout while sending command") 135 | ErrTimeoutRecv = errors.New("Timeout while receiving response") 136 | ) 137 | 138 | // Exec executes a command via ipc and returns the response. 139 | // A request can timeout while sending or while waiting for the response. 140 | // An error is only returned if there was an error in the communication. 141 | // The client has to check for `response.Error` in case the server returned 142 | // an error. 143 | func (c *IPCClient) Exec(command ...interface{}) (*Response, error) { 144 | req := newRequest(command...) 145 | select { 146 | case c.comm <- req: 147 | case <-time.After(c.timeout): 148 | return nil, ErrTimeoutSend 149 | } 150 | 151 | select { 152 | case res, ok := <-req.Response: 153 | if !ok { 154 | panic("Response channel closed") 155 | } 156 | return res, nil 157 | case <-time.After(c.timeout): 158 | return nil, ErrTimeoutRecv 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /rpc.go: -------------------------------------------------------------------------------- 1 | package mpv 2 | 3 | import "net/rpc" 4 | 5 | // RPCServer publishes a LLClient over RPC. 6 | type RPCServer struct { 7 | llclient LLClient 8 | } 9 | 10 | // NewRPCServer creates a new RPCServer based on lowlevel client. 11 | func NewRPCServer(client LLClient) *RPCServer { 12 | return &RPCServer{ 13 | llclient: client, 14 | } 15 | } 16 | 17 | // Exec exposes llclient.Exec via RPC. Not intended to be used directly. 18 | func (s *RPCServer) Exec(args *[]interface{}, res *Response) error { 19 | resp, err := s.llclient.Exec(*args...) 20 | *res = *resp 21 | return err 22 | } 23 | 24 | // RPCClient represents a LLClient over RPC. 25 | type RPCClient struct { 26 | client *rpc.Client 27 | } 28 | 29 | // NewRPCClient creates a new RPCClient based on rpc.Client 30 | func NewRPCClient(client *rpc.Client) *RPCClient { 31 | return &RPCClient{ 32 | client: client, 33 | } 34 | } 35 | 36 | // Exec executes a command over rpc. 37 | func (s *RPCClient) Exec(command ...interface{}) (*Response, error) { 38 | var res Response 39 | err := s.client.Call("RPCServer.Exec", &command, &res) 40 | return &res, err 41 | } 42 | --------------------------------------------------------------------------------