LabCodeHub öffentliche Ansicht
Anmelden
Küpper / ehc-tvwall öffentlich
Branch: main
ehc-tvwall / internal / mpv / mpv.go
Verlauf Rohdaten
R Rüdiger Küpper fix: v1.0.0
14518d05 vor 11 Tagen
internal/mpv/mpv.go 243 Zeilen · 5.5 KB · Go
1
// Package mpv spricht ueber den JSON-IPC-Socket mit einem laufenden mpv.
2
package mpv
3
4
import (
5
	"bufio"
6
	"encoding/json"
7
	"errors"
8
	"fmt"
9
	"net"
10
	"sync"
11
	"time"
12
)
13
14
// ErrClosed wird geliefert, wenn die Verbindung nicht mehr steht.
15
var ErrClosed = errors.New("mpv: verbindung geschlossen")
16
17
// Event ist eine von mpv gesendete Statusmeldung.
18
type Event struct {
19
	Name  string          `json:"event"`
20
	ID    int             `json:"id"`
21
	Data  json.RawMessage `json:"data"`
22
	Name2 string          `json:"name"` // bei property-change: Name der Eigenschaft
23
24
	// Raw ist die vollstaendige Meldung - fuer Felder, die je nach Ereignis
25
	// unterschiedlich heissen (etwa "reason" bei end-file).
26
	Raw json.RawMessage `json:"-"`
27
}
28
29
type response struct {
30
	Error     string          `json:"error"`
31
	Data      json.RawMessage `json:"data"`
32
	RequestID int             `json:"request_id"`
33
}
34
35
// Client ist eine Verbindung zum IPC-Socket von mpv.
36
type Client struct {
37
	conn net.Conn
38
39
	mu      sync.Mutex
40
	nextID  int
41
	pending map[int]chan response
42
	closed  bool
43
44
	events chan Event
45
	done   chan struct{}
46
}
47
48
// Dial verbindet sich mit dem Socket und wartet dabei bis zu timeout, weil mpv
49
// den Socket erst kurz nach dem Start anlegt.
50
func Dial(socketPath string, timeout time.Duration) (*Client, error) {
51
	deadline := time.Now().Add(timeout)
52
	var lastErr error
53
	for {
54
		conn, err := net.Dial("unix", socketPath)
55
		if err == nil {
56
			c := &Client{
57
				conn:    conn,
58
				pending: make(map[int]chan response),
59
				events:  make(chan Event, 64),
60
				done:    make(chan struct{}),
61
			}
62
			go c.readLoop()
63
			return c, nil
64
		}
65
		lastErr = err
66
		if time.Now().After(deadline) {
67
			return nil, fmt.Errorf("mpv-socket %s nicht erreichbar: %w", socketPath, lastErr)
68
		}
69
		time.Sleep(150 * time.Millisecond)
70
	}
71
}
72
73
// Events liefert den Kanal mit den Statusmeldungen von mpv.
74
func (c *Client) Events() <-chan Event { return c.events }
75
76
// Done wird geschlossen, sobald die Verbindung abbricht.
77
func (c *Client) Done() <-chan struct{} { return c.done }
78
79
// Close beendet die Verbindung.
80
func (c *Client) Close() error {
81
	c.mu.Lock()
82
	if c.closed {
83
		c.mu.Unlock()
84
		return nil
85
	}
86
	c.closed = true
87
	c.mu.Unlock()
88
	return c.conn.Close()
89
}
90
91
func (c *Client) readLoop() {
92
	defer func() {
93
		c.mu.Lock()
94
		c.closed = true
95
		for id, ch := range c.pending {
96
			close(ch)
97
			delete(c.pending, id)
98
		}
99
		c.mu.Unlock()
100
		close(c.done)
101
		close(c.events)
102
	}()
103
104
	sc := bufio.NewScanner(c.conn)
105
	sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
106
	for sc.Scan() {
107
		line := sc.Bytes()
108
		if len(line) == 0 {
109
			continue
110
		}
111
		// Eine Zeile ist entweder ein Event oder die Antwort auf ein Kommando.
112
		var probe struct {
113
			Event     string `json:"event"`
114
			RequestID *int   `json:"request_id"`
115
		}
116
		if json.Unmarshal(line, &probe) != nil {
117
			continue
118
		}
119
		if probe.Event != "" {
120
			var ev Event
121
			if json.Unmarshal(line, &ev) == nil {
122
				ev.Raw = append(json.RawMessage(nil), line...)
123
				select {
124
				case c.events <- ev:
125
				default: // Ereignisse sind unkritisch: lieber verwerfen als blockieren
126
				}
127
			}
128
			continue
129
		}
130
		if probe.RequestID == nil {
131
			continue
132
		}
133
		var resp response
134
		if json.Unmarshal(line, &resp) != nil {
135
			continue
136
		}
137
		c.mu.Lock()
138
		ch, ok := c.pending[resp.RequestID]
139
		if ok {
140
			delete(c.pending, resp.RequestID)
141
		}
142
		c.mu.Unlock()
143
		if ok {
144
			ch <- resp
145
			close(ch)
146
		}
147
	}
148
}
149
150
// Command schickt ein Kommando an mpv und wartet auf die Antwort.
151
func (c *Client) Command(args ...any) (json.RawMessage, error) {
152
	c.mu.Lock()
153
	if c.closed {
154
		c.mu.Unlock()
155
		return nil, ErrClosed
156
	}
157
	c.nextID++
158
	id := c.nextID
159
	ch := make(chan response, 1)
160
	c.pending[id] = ch
161
	c.mu.Unlock()
162
163
	payload, err := json.Marshal(map[string]any{"command": args, "request_id": id})
164
	if err != nil {
165
		c.forget(id)
166
		return nil, err
167
	}
168
	if err := c.conn.SetWriteDeadline(time.Now().Add(3 * time.Second)); err == nil {
169
		defer c.conn.SetWriteDeadline(time.Time{})
170
	}
171
	if _, err := c.conn.Write(append(payload, '\n')); err != nil {
172
		c.forget(id)
173
		return nil, err
174
	}
175
176
	select {
177
	case resp, ok := <-ch:
178
		if !ok {
179
			return nil, ErrClosed
180
		}
181
		if resp.Error != "" && resp.Error != "success" {
182
			return resp.Data, fmt.Errorf("mpv: %s", resp.Error)
183
		}
184
		return resp.Data, nil
185
	case <-time.After(5 * time.Second):
186
		c.forget(id)
187
		return nil, fmt.Errorf("mpv: zeitueberschreitung bei %v", args)
188
	}
189
}
190
191
func (c *Client) forget(id int) {
192
	c.mu.Lock()
193
	delete(c.pending, id)
194
	c.mu.Unlock()
195
}
196
197
// SetProperty setzt eine mpv-Eigenschaft.
198
func (c *Client) SetProperty(name string, value any) error {
199
	_, err := c.Command("set_property", name, value)
200
	return err
201
}
202
203
// GetString liest eine Eigenschaft als Zeichenkette.
204
func (c *Client) GetString(name string) (string, error) {
205
	data, err := c.Command("get_property", name)
206
	if err != nil {
207
		return "", err
208
	}
209
	var s string
210
	if json.Unmarshal(data, &s) != nil {
211
		return string(data), nil
212
	}
213
	return s, nil
214
}
215
216
// GetBool liest eine Eigenschaft als Wahrheitswert.
217
func (c *Client) GetBool(name string) (bool, error) {
218
	data, err := c.Command("get_property", name)
219
	if err != nil {
220
		return false, err
221
	}
222
	var b bool
223
	return b, json.Unmarshal(data, &b)
224
}
225
226
// GetInt liest eine Eigenschaft als Ganzzahl.
227
func (c *Client) GetInt(name string) (int, error) {
228
	data, err := c.Command("get_property", name)
229
	if err != nil {
230
		return 0, err
231
	}
232
	var n float64
233
	if err := json.Unmarshal(data, &n); err != nil {
234
		return 0, err
235
	}
236
	return int(n), nil
237
}
238
239
// ObserveProperty laesst mpv Aenderungen an einer Eigenschaft melden.
240
func (c *Client) ObserveProperty(id int, name string) error {
241
	_, err := c.Command("observe_property", id, name)
242
	return err
243
}