· 8 years ago · Jun 01, 2018, 12:42 AM
1;;; emms-player-mpv.el --- mpv support for EMMS
2;;
3;; Copyright (C) 2018 Free Software Foundation, Inc.
4
5;; Authors: Mike Kazantsev <mk.fraggod@gmail.com>
6
7;; This file is part of EMMS.
8
9;; EMMS is free software; you can redistribute it and/or
10;; modify it under the terms of the GNU General Public License
11;; as published by the Free Software Foundation; either version 3
12;; of the License, or (at your option) any later version.
13
14;; EMMS is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;; GNU General Public License for more details.
18
19;; You should have received a copy of the GNU General Public License
20;; along with EMMS; if not, write to the Free Software Foundation,
21;; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
22
23;;; Commentary:
24
25;;
26;; This code provides EMMS backend for using mpv player.
27;;
28;; It works in one of two modes, depending on `emms-player-mpv-ipc-method'
29;; customizable value or installed mpv version:
30;;
31;; - Using long-running mpv instance and JSON IPC interface to switch tracks
32;; and receive player feedback/metadata - for mpv 0.7.0 2014-10-16 and later.
33;;
34;; - Starting new mpv instance for each track, using its exit
35;; as "next track" signal and --input-file interface for pause/seek.
36;; Used as a fallback for any older mpv versions (supported in all of them).
37;;
38;; In default configuration, mpv will read its configuration files
39;; (see its manpage for locations), and can display window for
40;; video, subtitles, album-art or audio visualization.
41;;
42;; Useful `emms-player-mpv-parameters' tweaks:
43;;
44;; - Ignore config file(s): (add-to-list 'emms-player-mpv-parameters "--no-config")
45;; - Disable vo window: (add-to-list 'emms-player-mpv-parameters "--vo=null")
46;; - Show simple cqt visualizer window:
47;; (add-to-list 'emms-player-mpv-parameters
48;; "--lavfi-complex=[aid1]asplit[ao][a]; [a]showcqt[vo]")
49;;
50;; See "M-x customize-group emms-player-mpv" and mpv manpage for more options.
51;;
52;; See `emms-player-mpv-event-connect-hook' and `emms-player-mpv-event-functions',
53;; as well as `emms-player-mpv-ipc-req-send' for handling more mpv events,
54;; processing more playback info and metadata from it, as well as extending
55;; control over its vast functionality.
56;;
57
58;;; Code:
59
60
61(require 'emms)
62(require 'json)
63(require 'cl-lib)
64
65
66(defcustom emms-player-mpv
67 (emms-player
68 'emms-player-mpv-start
69 'emms-player-mpv-stop
70 'emms-player-mpv-playable-p)
71 "*Parameters for mpv player."
72 :type '(cons symbol alist)
73 :group 'emms-player-mpv)
74
75(defcustom emms-player-mpv-command-name "mpv"
76 "mpv binary to use. Can be absolute path or just binary name."
77 :type 'file
78 :group 'emms-player-mpv)
79
80(defcustom emms-player-mpv-parameters
81 '("--quiet" "--really-quiet" "--no-audio-display")
82 "Extra command-line arguments for started mpv process(es).
83Either a list of strings or function returning such list.
84Extra arguments --idle and --input-file/--input-ipc-server
85are added automatically, depending on mpv version.
86Note that unless --no-config option is specified here,
87mpv will also use options from its configuration files.
88For mpv binary path, see `emms-player-mpv-command-name'."
89 :type '(choice (repeat :tag "List of mpv arguments" string) function)
90 :group 'emms-player-mpv)
91
92(defcustom emms-player-mpv-environment ()
93 "List of extra environment variables (\"VAR=value\" strings) to pass on to mpv process.
94These are added on top of `process-environment' by default.
95Adding nil as an element to this list will discard emacs
96`process-environment' and only pass variables that are specified in the list."
97 :type '(repeat (choice string (const :tag "Start from blank environment" nil)))
98 :group 'emms-player-mpv)
99
100(defcustom emms-player-mpv-ipc-method nil
101 "Switch for which IPC method to use with mpv.
102Possible symbols: detect, ipc-server, unix-socket, file.
103Defaults to nil value, which will cause `emms-player-mpv-ipc-detect'
104to pick one based on mpv --version output.
105Using JSON-IPC variants (ipc-server and unix-socket) enables
106support for various feedback and metadata options from mpv."
107 :type '(choice
108 (const :tag "Auto-detect from mpv --version" nil)
109 (const :tag "Use --input-ipc-server JSON IPC (v0.17.0 2016-04-11)" ipc-server)
110 (const :tag "Use --input-unix-socket JSON IPC (v0.7.0 2014-10-16)" unix-socket)
111 (const :tag "Use --input-file FIFO (any mpv version)" file))
112 :group 'emms-player-mpv)
113
114(defcustom emms-player-mpv-ipc-socket
115 (expand-file-name (locate-user-emacs-file "emms-player-mpv-ipc.sock"))
116 "Unix IPC socket or FIFO to use with mpv --input-* options,
117depending on `emms-player-mpv-ipc-method' value and/or mpv version."
118 :type 'file
119 :group 'emms-player-mpv)
120
121(defvar emms-player-mpv-ipc-proc nil) ; to avoid warnings while keeping useful defs at the top
122
123(defcustom emms-player-mpv-update-duration t
124 "Update track duration when played by mpv.
125Uses `emms-player-mpv-event-functions' hook."
126 :type 'boolean
127 :set (lambda (sym value)
128 (run-at-time 0.1 nil
129 (lambda (value) (if value
130 (add-hook
131 'emms-player-mpv-event-functions
132 'emms-player-mpv-info-duration-event-func)
133 (remove-hook
134 'emms-player-mpv-event-functions
135 'emms-player-mpv-info-duration-event-func)))
136 value))
137 :group 'emms-player-mpv)
138
139(defcustom emms-player-mpv-update-metadata nil
140 "Update track info (artist, album, name, etc) from mpv events, when it is played.
141This allows to dynamically update stream info from ICY tags, for example.
142Uses `emms-player-mpv-event-connect-hook' and `emms-player-mpv-event-functions' hooks."
143 :type 'boolean
144 :set (lambda (sym value)
145 (run-at-time 0.1 nil
146 (lambda (value) (if value
147 (progn
148 (add-hook
149 'emms-player-mpv-event-connect-hook
150 'emms-player-mpv-info-meta-connect-func)
151 (add-hook
152 'emms-player-mpv-event-functions
153 'emms-player-mpv-info-meta-event-func)
154 (when (process-live-p emms-player-mpv-ipc-proc)
155 (emms-player-mpv-info-meta-connect-func)))
156 (progn
157 (remove-hook
158 'emms-player-mpv-event-connect-hook
159 'emms-player-mpv-info-meta-connect-func)
160 (remove-hook
161 'emms-player-mpv-event-functions
162 'emms-player-mpv-info-meta-event-func))))
163 value))
164 :group 'emms-player-mpv)
165
166
167(defvar emms-player-mpv-proc nil
168 "Running mpv process, controlled over --input-ipc-server/--input-file sockets.")
169
170(defvar emms-player-mpv-proc-kill-delay 5
171 "Delay until SIGKILL gets sent to `emms-player-mpv-proc',
172if it refuses to exit cleanly on `emms-player-mpv-proc-stop'.")
173
174
175(defvar emms-player-mpv-ipc-proc nil
176 "Unix socket process that communicates with running `emms-player-mpv-proc' instance.")
177
178(defvar emms-player-mpv-ipc-buffer " *emms-player-mpv-ipc*"
179 "Buffer to associate with `emms-player-mpv-ipc-proc' socket/pipe process.")
180
181(defvar emms-player-mpv-ipc-connect-timer nil
182 "Timer for connection attempts to JSON IPC unix socket.")
183(defvar emms-player-mpv-ipc-connect-delays
184 '(0.1 0.1 0.1 0.1 0.1 0.1 0.2 0.2 0.3 0.3 0.5 1.0 1.0 2.0)
185 "List of delays before initiating socket connection for new mpv process.")
186
187(defvar emms-player-mpv-ipc-connect-command nil
188 "JSON command for `emms-player-mpv-ipc-sentinel' to run as soon as it connects to mpv.
189I.e. last command that either initiated connection or was used while connecting to mpv.
190Set by `emms-player-mpv-start' and such, cleared once it gets sent by `emms-player-mpv-ipc-sentinel'.")
191
192(defvar emms-player-mpv-ipc-id 1
193 "Auto-incremented value sent in JSON requests for request_id and observe_property id's.
194Use `emms-player-mpv-ipc-id-get' to get and increment this value, instead of using it directly.
195Wraps-around upon reaching `emms-player-mpv-ipc-id-max' (unlikely to ever happen).")
196
197(defvar emms-player-mpv-ipc-id-max (expt 2 30)
198 "Max value for `emms-player-mpv-ipc-id' to wrap around after.
199Should be fine with both mpv and emacs, and probably never reached anyway.")
200
201(defvar emms-player-mpv-ipc-req-table nil
202 "Auto-initialized hash table of outstanding API req_ids to their handler funcs.")
203
204
205(defvar emms-player-mpv-event-connect-hook nil
206 "Normal hook run right after establishing new JSON IPC
207connection to mpv instance and before `emms-player-mpv-ipc-connect-command' if any.
208Best place to send any observe_property, request_log_messages, enable_event commands.
209Use `emms-player-mpv-ipc-id-get' to get unique id values for these.
210See also `emms-player-mpv-event-functions'.")
211
212(defvar emms-player-mpv-event-functions nil
213 "List of functions to call for each event emitted from JSON IPC.
214One argument is passed to each function - JSON line,
215as sent by mpv and decoded by `json-read-from-string'.
216See also `emms-player-mpv-event-connect-hook'.")
217
218
219(defvar emms-player-mpv-stopped nil
220 "Non-nil if playback was stopped by call from emms.
221Similar to `emms-player-stopped-p', but set for future async events,
222to indicate that playback should stop instead of switching to next track.")
223
224(defvar emms-player-mpv-idle-timer (timer-create)
225 "Timer to delay `emms-player-stopped' when mpv unexpectedly goes idle.")
226
227(defvar emms-player-mpv-idle-delay 0.5
228 "Delay before issuing `emms-player-stopped' when mpv unexpectedly goes idle.")
229
230
231;; ----- helpers
232
233(defvar emms-player-mpv-debug nil
234 "Enable to print sent/received JSON lines and process
235start/stop events to *Messages* buffer using `emms-player-mpv-debug-msg'.")
236
237(defvar emms-player-mpv-debug-ts-offset nil
238 "Timestamp offset for `emms-player-mpv-debug-msg'.
239Set on first use, with intent to both shorten and obfuscate time in logs.")
240
241(defun emms-player-mpv-debug-trim (s)
242 (if (stringp s)
243 (replace-regexp-in-string "\\(^[ \t\n\r]+\\|[ \t\n\r]+$\\)" "" s t t) s))
244
245(defun emms-player-mpv-debug-msg (tpl-or-msg &rest tpl-values)
246 "Print debug message to *Messages* if `emms-player-mpv-debug' is non-nil.
247Message is only formatted if TPL-VALUES is non-empty.
248Strips whitespace from start/end of TPL-OR-MSG and strings in TPL-VALUES."
249 (when emms-player-mpv-debug
250 (setq
251 tpl-or-msg (emms-player-mpv-debug-trim tpl-or-msg)
252 tpl-values (seq-map 'emms-player-mpv-debug-trim tpl-values))
253 (unless tpl-values
254 (setq tpl-or-msg (replace-regexp-in-string "%" "%%" tpl-or-msg t t)))
255 (let ((ts (float-time)))
256 (unless emms-player-mpv-debug-ts-offset (setq emms-player-mpv-debug-ts-offset ts))
257 (apply 'message
258 (concat "emms-player-mpv %.1f " tpl-or-msg)
259 (- ts emms-player-mpv-debug-ts-offset) tpl-values))))
260
261(defun emms-player-mpv-ipc-fifo-p ()
262 "Returns non-nil if --input-file fifo should be used.
263Runs `emms-player-mpv-ipc-detect' to detect/set `emms-player-mpv-ipc-method' if necessary."
264 (unless emms-player-mpv-ipc-method
265 (setq emms-player-mpv-ipc-method
266 (emms-player-mpv-ipc-detect emms-player-mpv-command-name)))
267 (eq emms-player-mpv-ipc-method 'file))
268
269(defun emms-player-mpv-ipc-detect (cmd)
270 "Run mpv --version and return symbol for best IPC method supported.
271CMD should be either name of mpv binary to use or full path to it.
272Return values correspond to `emms-player-mpv-ipc-method' options.
273Error is signaled if mpv binary fails to run."
274 (with-temp-buffer
275 (let ((exit-code (call-process cmd nil '(t t) nil "--version")))
276 (unless (zerop exit-code)
277 (insert (format "----- process exited with code %d -----" exit-code))
278 (error (format "Failed to run mpv binary [%s]:\n%s" cmd (buffer-string))))
279 (goto-char (point-min))
280 (pcase
281 (if (re-search-forward "^mpv\\s-+\\(\\([0-9]+\\.?\\)+\\)" nil t 1)
282 (mapconcat (lambda (n) (format "%03d" n))
283 (seq-map 'string-to-number
284 (split-string (match-string-no-properties 1) "\\." t)) ".")
285 "000.000.000")
286 ((pred (string> "000.006.999")) 'file)
287 ((pred (string> "000.016.999")) 'unix-socket)
288 (- 'ipc-server)))))
289
290
291;; ----- mpv process
292
293(defun emms-player-mpv-proc-playing-p (&optional proc)
294 "Return whether playback in PROC or `emms-player-mpv-proc' is started,
295which is distinct from 'start-command sent' and 'process is running' states.
296Used to signal emms via `emms-player-started' and `emms-player-stopped' calls."
297 (let ((proc (or proc emms-player-mpv-proc)))
298 (if proc (process-get proc 'mpv-playing) nil)))
299
300(defun emms-player-mpv-proc-playing (state &optional proc)
301 "Set process mpv-playing state flag for `emms-player-mpv-proc-playing-p'."
302 (let ((proc (or proc emms-player-mpv-proc)))
303 (when proc (process-put proc 'mpv-playing state))))
304
305(defun emms-player-mpv-proc-symbol-id (sym &optional proc)
306 "Get unique process-specific id integer for SYM or nil if it was already requested."
307 (let
308 ((proc (or proc emms-player-mpv-proc))
309 (sym-id (intern (concat "mpv-sym-" (symbol-name sym)))))
310 (unless (process-get proc sym-id)
311 (let ((id (emms-player-mpv-ipc-id-get))) (process-put proc sym-id id) id))))
312
313(defun emms-player-mpv-proc-init-fifo (path &optional mode)
314 "Create named pipe (fifo) socket for mpv --input-file PATH, if not exists already.
315Optional MODE should be 12-bit octal integer, e.g. #o600 (safe default).
316Signals error if mkfifo exits with non-zero code."
317 (let ((attrs (file-attributes path)))
318 (when ; remove non-fifo at the same path
319 (and attrs (not (string-prefix-p "p" (nth 8 attrs))))
320 (delete-file path) (setq attrs nil))
321 (unless attrs
322 (unless
323 (zerop (call-process "mkfifo" nil nil nil
324 (format "--mode=%o" (or mode #o600)) path))
325 (error (format "Failed to run mkfifo for mpv --input-file path: %s" path))))))
326
327(defun emms-player-mpv-proc-sentinel (proc ev)
328 (let
329 ((status (process-status proc))
330 (playing (emms-player-mpv-proc-playing-p proc)))
331 (emms-player-mpv-debug-msg
332 "proc[%s]: %s (status=%s, playing=%s)" proc ev status playing)
333 (when (and (memq status '(exit signal)) playing) (emms-player-stopped))))
334
335(defun emms-player-mpv-proc-init (&rest media-args)
336 "initialize new mpv process as `emms-player-mpv-proc'.
337MEDIA-ARGS are used instead of --idle, if specified."
338 (emms-player-mpv-proc-stop)
339 (when (emms-player-mpv-ipc-fifo-p)
340 (emms-player-mpv-proc-init-fifo emms-player-mpv-ipc-socket))
341 (let*
342 ((argv emms-player-mpv-parameters)
343 (argv (append
344 (list emms-player-mpv-command-name)
345 (if (functionp argv) (funcall argv) argv)
346 (list (format "--input-%s=%s"
347 emms-player-mpv-ipc-method emms-player-mpv-ipc-socket))
348 (or media-args '("--idle"))))
349 (env emms-player-mpv-environment)
350 (process-environment (append
351 (unless (seq-some 'not env) process-environment) (seq-filter 'identity env))))
352 (setq emms-player-mpv-proc
353 (make-process :name "emms-player-mpv"
354 :buffer nil :command argv :noquery t :sentinel 'emms-player-mpv-proc-sentinel))
355 (when (emms-player-mpv-ipc-fifo-p) (emms-player-mpv-proc-playing t))
356 (emms-player-mpv-debug-msg "proc[%s]: start %s" emms-player-mpv-proc argv)))
357
358(defun emms-player-mpv-proc-stop ()
359 "Stop running `emms-player-mpv-proc' instance via SIGINT, if any.
360`delete-process' (SIGKILL) timer is started if `emms-player-mpv-proc-kill-delay' is non-nil."
361 (when emms-player-mpv-proc
362 (let ((proc emms-player-mpv-proc))
363 (emms-player-mpv-debug-msg "proc[%s]: stop" proc)
364 (setq emms-player-mpv-proc nil)
365 (if (not (process-live-p proc)) (delete-process proc)
366 (emms-player-mpv-proc-playing nil proc)
367 (interrupt-process proc)
368 (when emms-player-mpv-proc-kill-delay
369 (run-at-time
370 emms-player-mpv-proc-kill-delay nil
371 (lambda (proc) (delete-process proc)) proc))))))
372
373
374;; ----- IPC socket/fifo
375
376(defun emms-player-mpv-ipc-sentinel (proc ev)
377 (emms-player-mpv-debug-msg "ipc[%s]: %s" proc ev)
378 (when (memq (process-status proc) '(open run))
379 (run-hooks 'emms-player-mpv-event-connect-hook)
380 (when emms-player-mpv-ipc-connect-command
381 (let ((cmd emms-player-mpv-ipc-connect-command))
382 (setq emms-player-mpv-ipc-connect-command nil)
383 (emms-player-mpv-ipc-req-send cmd nil proc)))))
384
385(defun emms-player-mpv-ipc-filter (proc s)
386 (when (buffer-live-p (process-buffer proc))
387 (with-current-buffer (process-buffer proc)
388 (let ((moving (= (point) (process-mark proc))))
389 (save-excursion
390 (goto-char (process-mark proc))
391 (insert s)
392 (set-marker (process-mark proc) (point)))
393 (if moving (goto-char (process-mark proc))))
394 ;; Process/remove all complete lines of json, if any
395 (let ((p0 (point-min)))
396 (while
397 (progn
398 (goto-char p0) (end-of-line)
399 (equal (following-char) #xa))
400 (let*
401 ((p1 (point))
402 (json (buffer-substring p0 p1)))
403 (delete-region p0 (+ p1 1))
404 (emms-player-mpv-ipc-recv json)))))))
405
406(defun emms-player-mpv-ipc-connect (delays)
407 "Make IPC connection attempt, rescheduling if there's no socket by (car DELAYS).
408(cdr DELAYS) gets passed to next connection attempt,
409so it can be rescheduled further until function runs out of DELAYS values.
410Sets `emms-player-mpv-ipc-proc' value to resulting process on success."
411 (emms-player-mpv-debug-msg "ipc: connect-delay %s" (car delays))
412 (setq emms-player-mpv-ipc-proc
413 (make-network-process ; returns nil if there's no socket yet
414 :name "emms-player-mpv-ipc"
415 :family 'local
416 :service emms-player-mpv-ipc-socket
417 :nowait t
418 :coding '(utf-8 . utf-8)
419 :buffer (get-buffer-create emms-player-mpv-ipc-buffer)
420 :noquery t
421 :filter 'emms-player-mpv-ipc-filter
422 :sentinel 'emms-player-mpv-ipc-sentinel))
423 (when (and (not emms-player-mpv-ipc-proc) delays)
424 (run-at-time (car delays) nil 'emms-player-mpv-ipc-connect (cdr delays))))
425
426(defun emms-player-mpv-ipc-connect-fifo ()
427 "Set `emms-player-mpv-ipc-proc' to process wrapper for
428writing to a named pipe (fifo) file/node or signal error."
429 (setq emms-player-mpv-ipc-proc
430 (start-process-shell-command "emms-player-mpv-input-file" nil
431 (format "cat > '%s'"
432 (replace-regexp-in-string "'" "'\"'\"'" emms-player-mpv-ipc-socket t t))))
433 (set-process-query-on-exit-flag emms-player-mpv-ipc-proc nil)
434 (unless emms-player-mpv-ipc-proc (error (format
435 "Failed to start cat-pipe to fifo: %s" emms-player-mpv-ipc-socket)))
436 (when emms-player-mpv-ipc-connect-command
437 (let ((cmd emms-player-mpv-ipc-connect-command))
438 (setq emms-player-mpv-ipc-connect-command nil)
439 (emms-player-mpv-ipc-fifo-cmd cmd emms-player-mpv-ipc-proc))))
440
441(defun emms-player-mpv-ipc-init ()
442 "initialize new mpv ipc socket/file process and associated state."
443 (emms-player-mpv-ipc-stop)
444 (emms-player-mpv-debug-msg "ipc: init")
445 (if (emms-player-mpv-ipc-fifo-p) (emms-player-mpv-ipc-connect-fifo)
446 (when emms-player-mpv-ipc-connect-timer (cancel-timer emms-player-mpv-ipc-connect-timer))
447 (with-current-buffer (get-buffer-create emms-player-mpv-ipc-buffer) (erase-buffer))
448 (setq
449 emms-player-mpv-ipc-id 1
450 emms-player-mpv-ipc-req-table nil
451 emms-player-mpv-ipc-connect-timer nil
452 emms-player-mpv-ipc-connect-timer
453 (run-at-time (car emms-player-mpv-ipc-connect-delays)
454 nil 'emms-player-mpv-ipc-connect (cdr emms-player-mpv-ipc-connect-delays)))))
455
456(defun emms-player-mpv-ipc-stop ()
457 (when emms-player-mpv-ipc-proc
458 (emms-player-mpv-debug-msg "ipc: stop")
459 (delete-process emms-player-mpv-ipc-proc)
460 (setq emms-player-mpv-ipc-proc nil)))
461
462(defun emms-player-mpv-ipc ()
463 "Returns open ipc socket/fifo process or nil, (re-)starting mpv/connection if necessary.
464Will return nil when starting async process/connection, and any follow-up
465command should be stored to `emms-player-mpv-ipc-connect-command' in this case."
466 (unless
467 ;; Don't start idle processes for fifo - just ignore all ipc requests there
468 (and (not (process-live-p emms-player-mpv-proc)) (emms-player-mpv-ipc-fifo-p))
469 (unless (process-live-p emms-player-mpv-proc) (emms-player-mpv-proc-init))
470 (unless (process-live-p emms-player-mpv-ipc-proc) (emms-player-mpv-ipc-init))
471 (and
472 emms-player-mpv-ipc-proc
473 (memq (process-status emms-player-mpv-ipc-proc) '(open run))
474 emms-player-mpv-ipc-proc)))
475
476
477;; ----- IPC protocol
478
479(defun emms-player-mpv-ipc-id-get ()
480 "Get new connection-unique id value, tracked via `emms-player-mpv-ipc-id'."
481 (let ((ipc-id emms-player-mpv-ipc-id))
482 (setq emms-player-mpv-ipc-id
483 (if (< emms-player-mpv-ipc-id emms-player-mpv-ipc-id-max) (1+ emms-player-mpv-ipc-id) 1))
484 ipc-id))
485
486(defun emms-player-mpv-ipc-req-send (cmd &optional handler proc)
487 "Send JSON IPC request and assign HANDLER to response for it, if any.
488CMD value is encoded via `json-encode'.
489HANDLER func will be called with decoded response JSON as (handler data err),
490where ERR will be either nil on \"success\", 'connection-error or whatever is in JSON.
491If HANDLER is nil, default `emms-player-mpv-ipc-req-error-printer' will be used to at least log errors.
492PROC can be specified to avoid `emms-player-mpv-ipc' call (e.g. from sentinel/filter funcs)."
493 (let
494 ((req-id (emms-player-mpv-ipc-id-get))
495 (req-proc (or proc (emms-player-mpv-ipc)))
496 (handler (or handler 'emms-player-mpv-ipc-req-error-printer)))
497 (unless emms-player-mpv-ipc-req-table
498 (setq emms-player-mpv-ipc-req-table (make-hash-table)))
499 (let ((json (concat (json-encode (list :command cmd :request_id req-id)) "\n")))
500 (emms-player-mpv-debug-msg "json >> %s" json)
501 (condition-case err
502 (process-send-string req-proc json) ; can disconnect at any time
503 (error
504 (emms-player-mpv-proc-stop) ; assume that mpv process is to blame and force restart
505 (funcall handler nil 'connection-error) (setq handler nil))))
506 (when handler (puthash req-id handler emms-player-mpv-ipc-req-table))))
507
508(defun emms-player-mpv-ipc-req-resolve (req-id data err)
509 "Run handler-func for specified req-id."
510 (when emms-player-mpv-ipc-req-table
511 (let
512 ((handler (gethash req-id emms-player-mpv-ipc-req-table))
513 (err (if (string= err "success") nil err)))
514 (remhash req-id emms-player-mpv-ipc-req-table)
515 (when handler (funcall handler data err)))))
516
517(defun emms-player-mpv-ipc-req-error-printer (data err)
518 "Simple default `emms-player-mpv-ipc-req-send' handler to log errors, if any."
519 (when err (message "emms-player-mpv-ipc-error: %s" err)))
520
521(defun emms-player-mpv-ipc-recv (json)
522 "Handler for all JSON lines from mpv process.
523Only used with JSON IPC, never called with --input-file as there's no feedback there."
524 (emms-player-mpv-debug-msg "json << %s" json)
525 (let*
526 ((json-data (json-read-from-string json))
527 (req-id (alist-get 'request_id json-data))
528 (ev (alist-get 'event json-data)))
529 (when req-id ; response to command
530 (emms-player-mpv-ipc-req-resolve req-id
531 (alist-get 'data json-data) (alist-get 'error json-data)))
532 (when ev ; mpv event
533 (emms-player-mpv-event-handler json-data)
534 (run-hook-with-args 'emms-player-mpv-event-functions json-data))))
535
536(defun emms-player-mpv-ipc-fifo-cmd (cmd &optional proc)
537 "Send --input-file command string for older mpv versions.
538PROC can be specified to avoid `emms-player-mpv-ipc' call."
539 (let
540 ((proc (or proc (emms-player-mpv-ipc)))
541 (cmd-line (concat (mapconcat (lambda (s) (format "%s" s)) cmd " ") "\n")))
542 (emms-player-mpv-debug-msg "fifo >> %s" cmd-line)
543 (process-send-string proc cmd-line)))
544
545(defun emms-player-mpv-observe-property (sym)
546 "Send mpv observe_property command for property identified by SYM.
547Only sends command once per process, removing any
548potential duplication if used for same properties from different functions."
549 (let ((id (emms-player-mpv-proc-symbol-id sym)))
550 (when id (emms-player-mpv-ipc-req-send `(observe_property ,id ,sym)))))
551
552(defun emms-player-mpv-event-idle ()
553 "Delayed check for switching tracks when mpv goes idle for no good reason."
554 (emms-player-mpv-debug-msg "idle-check (stopped=%s)" emms-player-mpv-stopped)
555 (unless emms-player-mpv-stopped (emms-player-stopped)))
556
557(defun emms-player-mpv-event-handler (json-data)
558 "Handler for supported mpv events, including property changes.
559Called before `emms-player-mpv-event-functions' and does same thing as these hooks."
560 (pcase (alist-get 'event json-data)
561 ("playback-restart"
562 ;; Separate emms-player-mpv-proc-playing state is used for emms started/stopped signals,
563 ;; because start-file/end-file are also emitted after track-change and for playlists,
564 ;; and don't correspond to actual playback state.
565 (unless (emms-player-mpv-proc-playing-p)
566 (emms-player-mpv-proc-playing t)
567 (emms-player-started emms-player-mpv)))
568 ("end-file"
569 (when (emms-player-mpv-proc-playing-p)
570 (emms-player-mpv-proc-playing nil)
571 (emms-player-stopped)))
572 ("idle"
573 ;; Can mean any kind of error before or during playback.
574 ;; Example can be access/format error, resulting in start+end without playback-restart.
575 (cancel-timer emms-player-mpv-idle-timer)
576 (setq emms-player-mpv-idle-timer
577 (run-at-time emms-player-mpv-idle-delay nil 'emms-player-mpv-event-idle)))
578 ("start-file" (cancel-timer emms-player-mpv-idle-timer))))
579
580
581;; ----- Metadata update hooks
582
583(defun emms-player-mpv-info-meta-connect-func ()
584 "Hook function for `emms-player-mpv-event-connect-hook' to update metadata from mpv."
585 (emms-player-mpv-observe-property 'metadata))
586
587(defun emms-player-mpv-info-meta-event-func (json-data)
588 "Hook function for `emms-player-mpv-event-functions' to update metadata from mpv."
589 (when
590 (and
591 (string= (alist-get 'event json-data) "property-change")
592 (string= (alist-get 'name json-data) "metadata"))
593 (let ((info-alist (alist-get 'data json-data)))
594 (when info-alist (emms-player-mpv-info-meta-update-track info-alist)))))
595
596(defun emms-player-mpv-info-meta-update-track (info-alist &optional track)
597 "Update TRACK with mpv metadata from INFO-ALIST.
598`emms-playlist-current-selected-track' is used by default."
599 (mapc
600 (lambda (cc) (setcar cc (intern (downcase (symbol-name (car cc))))))
601 info-alist)
602 (cl-macrolet
603 ((key (k) `(alist-get ',k info-alist))
604 (set-track-info (track &rest body) (cons 'progn
605 (cl-loop for (k v) on body by 'cddr collect
606 `(let ((value ,v)) (when value
607 (emms-track-set ,track ',(intern (format "info-%s" k)) value)))))))
608 (unless track (setq track (emms-playlist-current-selected-track)))
609 (set-track-info track
610 title (or (key title) (key icy-title))
611 artist (or (key artist) (key album_artist) (key icy-name))
612 album (key album)
613 tracknumber (key track)
614 year (key date)
615 genre (key genre)
616 note (key comment))
617 (emms-track-updated track)))
618
619(defun emms-player-mpv-info-duration-event-func (json-data)
620 "Hook function for `emms-player-mpv-event-functions' to update track duration from mpv."
621 (when
622 (string= (alist-get 'event json-data) "playback-restart")
623 (emms-player-mpv-info-duration-check)))
624
625(defun emms-player-mpv-info-duration-check ()
626 "Check whether current mpv track has reliable duration info and request it."
627 (emms-player-mpv-ipc-req-send '(get_property stream-end) (lambda (pts-end err)
628 (if err
629 (unless (and (stringp err) (string= err "property unavailable"))
630 (emms-player-mpv-ipc-req-error-printer pts-end err))
631 (when pts-end
632 (emms-player-mpv-ipc-req-send '(get_property duration)
633 'emms-player-mpv-info-duration-handler))))))
634
635(defun emms-player-mpv-info-duration-handler (duration err)
636 "Duration property request handler to update it for current emms track."
637 (if err
638 (emms-player-mpv-debug-msg "duration-req-error: %s" err)
639 (when (and (numberp duration) (> duration 0)) ; can be nil/0 for network streams
640 (let
641 ((duration (round duration))
642 (track (emms-playlist-current-selected-track)))
643 (emms-track-set track 'info-playing-time duration)
644 (emms-track-set track 'info-playing-time-min (/ duration 60))
645 (emms-track-set track 'info-playing-time-sec (% duration 60))))))
646
647
648;; ----- High-level EMMS interface
649
650(defun emms-player-mpv-cmd (cmd &optional handler)
651 "Send mpv command to process/connection if both are running,
652or otherwise schedule start/connect and set
653`emms-player-mpv-ipc-start-track' for `emms-player-mpv-ipc-sentinel'.
654PROC can be specified to avoid `emms-player-mpv-ipc' call."
655 (setq emms-player-mpv-ipc-connect-command nil)
656 (let ((proc (emms-player-mpv-ipc)))
657 (if proc
658 (if (emms-player-mpv-ipc-fifo-p)
659 (emms-player-mpv-ipc-fifo-cmd cmd proc)
660 (emms-player-mpv-ipc-req-send cmd handler proc))
661 (setq emms-player-mpv-ipc-connect-command cmd))))
662
663(defmacro emms-player-mpv-cmd-prog (cmd &rest handler-body)
664 "Macro around `emms-player-mpv-cmd' that creates
665handler callback (see `emms-player-mpv-ipc-req-send') from HANDLER-BODY forms,
666which have following bindings:
667- mpv-cmd for CMD.
668- mpv-data for response data (decoded json, nil if none).
669- mpv-error for response error (nil if no error, decoded json or 'connection-error)."
670 `(emms-player-mpv-cmd ,cmd (apply-partially
671 (lambda (mpv-cmd mpv-data mpv-error) ,@handler-body) ,cmd)))
672
673
674(defun emms-player-mpv-playable-p (track)
675 (memq (emms-track-type track) '(file url streamlist playlist)))
676
677(defun emms-player-mpv-start (track)
678 (setq emms-player-mpv-stopped nil)
679 (emms-player-mpv-proc-playing nil)
680 (let
681 ((track-name (emms-track-get track 'name))
682 (track-is-playlist (memq (emms-track-get track 'type) '(streamlist playlist))))
683 (if (emms-player-mpv-ipc-fifo-p)
684 (progn
685 (emms-player-mpv-ipc-stop) ; to clear any buffered commands
686 (emms-player-mpv-proc-init (if track-is-playlist "--playlist" "--") track-name)
687 (emms-player-started emms-player-mpv))
688 (emms-player-mpv-cmd-prog
689 (list (if track-is-playlist 'loadlist 'loadfile) track-name 'replace)
690 (if (eq mpv-error 'connection-error)
691 ;; Reconnect and restart playback if current connection fails (e.g. mpv crash)
692 (emms-player-mpv-cmd-prog
693 (emms-player-mpv-cmd mpv-cmd)
694 (emms-player-mpv-cmd `(set pause no)))
695 (emms-player-mpv-cmd `(set pause no)))))))
696
697(defun emms-player-mpv-stop ()
698 (setq emms-player-mpv-stopped t)
699 (emms-player-mpv-proc-playing nil)
700 (emms-player-mpv-cmd `(stop))
701 (emms-player-stopped))
702
703
704(defun emms-player-mpv-pause ()
705 (emms-player-mpv-cmd `(set pause yes)))
706
707(defun emms-player-mpv-resume ()
708 (emms-player-mpv-cmd `(set pause no)))
709
710(defun emms-player-mpv-seek (sec)
711 (emms-player-mpv-cmd `(seek ,sec relative)))
712
713(defun emms-player-mpv-seek-to (sec)
714 (emms-player-mpv-cmd `(seek ,sec absolute)))
715
716(emms-player-set emms-player-mpv 'pause 'emms-player-mpv-pause)
717(emms-player-set emms-player-mpv 'resume 'emms-player-mpv-resume)
718(emms-player-set emms-player-mpv 'seek 'emms-player-mpv-seek)
719(emms-player-set emms-player-mpv 'seek-to 'emms-player-mpv-seek-to)
720
721
722(provide 'emms-player-mpv)
723;;; emms-player-mpv.el ends here