aboutsummaryrefslogtreecommitdiff
path: root/emms-player-mpv.el
diff options
context:
space:
mode:
Diffstat (limited to 'emms-player-mpv.el')
-rw-r--r--emms-player-mpv.el847
1 files changed, 847 insertions, 0 deletions
diff --git a/emms-player-mpv.el b/emms-player-mpv.el
new file mode 100644
index 0000000..c4fc541
--- /dev/null
+++ b/emms-player-mpv.el
@@ -0,0 +1,847 @@
+;;; emms-player-mpv.el --- mpv support for EMMS
+;;
+;; Copyright (C) 2018 Free Software Foundation, Inc.
+
+;; Authors: Mike Kazantsev <mk.fraggod@gmail.com>
+
+;; This file is part of EMMS.
+
+;; EMMS is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License
+;; as published by the Free Software Foundation; either version 3
+;; of the License, or (at your option) any later version.
+
+;; EMMS is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;;; Commentary:
+
+;;
+;; This code provides EMMS backend for using mpv player.
+;;
+;; It works in one of two modes, depending on `emms-player-mpv-ipc-method'
+;; customizable value or installed mpv version:
+;;
+;; - Using long-running mpv instance and JSON IPC interface to switch tracks
+;; and receive player feedback/metadata - for mpv 0.7.0 2014-10-16 and later.
+;;
+;; - Starting new mpv instance for each track, using its exit
+;; as "next track" signal and --input-file interface for pause/seek.
+;; Used as a fallback for any older mpv versions (supported in all of them).
+;;
+;; In default configuration, mpv will read its configuration files
+;; (see its manpage for locations), and can display window for
+;; video, subtitles, album-art or audio visualization.
+;;
+;; Useful `emms-player-mpv-parameters' tweaks:
+;;
+;; - Ignore config file(s): (add-to-list 'emms-player-mpv-parameters "--no-config")
+;; - Disable vo window: (add-to-list 'emms-player-mpv-parameters "--vo=null")
+;; - Show simple cqt visualizer window:
+;; (add-to-list 'emms-player-mpv-parameters
+;; "--lavfi-complex=[aid1]asplit[ao][a]; [a]showcqt[vo]")
+;;
+;; See "M-x customize-group emms-player-mpv" and mpv manpage for more options.
+;;
+;; See `emms-player-mpv-event-connect-hook' and `emms-player-mpv-event-functions',
+;; as well as `emms-player-mpv-ipc-req-send' for handling more mpv events,
+;; processing more playback info and metadata from it, as well as extending
+;; control over its vast functionality.
+;;
+
+;;; Code:
+
+
+(require 'emms)
+(require 'emms-player-simple)
+(require 'json)
+(require 'cl-lib)
+
+
+(defcustom emms-player-mpv
+ (emms-player
+ #'emms-player-mpv-start
+ #'emms-player-mpv-stop
+ #'emms-player-mpv-playable-p)
+ "*Parameters for mpv player."
+ :type '(cons symbol alist)
+ :group 'emms-player-mpv)
+
+(emms-player-set emms-player-mpv 'regex
+ (apply #'emms-player-simple-regexp emms-player-base-format-list))
+
+(defcustom emms-player-mpv-command-name "mpv"
+ "mpv binary to use. Can be absolute path or just binary name."
+ :type 'file
+ :group 'emms-player-mpv)
+
+(defcustom emms-player-mpv-parameters
+ '("--quiet" "--really-quiet" "--no-audio-display")
+ "Extra command-line arguments for started mpv process(es).
+Either a list of strings or function returning such list.
+Extra arguments --idle and --input-file/--input-ipc-server
+are added automatically, depending on mpv version.
+Note that unless --no-config option is specified here,
+mpv will also use options from its configuration files.
+For mpv binary path, see `emms-player-mpv-command-name'."
+ :type '(choice (repeat :tag "List of mpv arguments" string)
+ function)
+ :group 'emms-player-mpv)
+
+(defcustom emms-player-mpv-environment ()
+ "List of extra environment variables (\"VAR=value\" strings) to pass on to mpv process.
+These are added on top of `process-environment' by default.
+Adding nil as an element to this list will discard emacs
+`process-environment' and only pass variables that are specified in the list."
+ :type '(repeat (choice string (const :tag "Start from blank environment" nil)))
+ :group 'emms-player-mpv)
+
+(defcustom emms-player-mpv-ipc-method nil
+ "Switch for which IPC method to use with mpv.
+Possible symbols: detect, ipc-server, unix-socket, file.
+Defaults to nil value, which will cause `emms-player-mpv-ipc-detect'
+to pick one based on mpv --version output.
+Using JSON-IPC variants (ipc-server and unix-socket) enables
+support for various feedback and metadata options from mpv."
+ :type '(choice
+ (const :tag "Auto-detect from mpv --version" nil)
+ (const :tag "Use --input-ipc-server JSON IPC (v0.17.0 2016-04-11)" ipc-server)
+ (const :tag "Use --input-unix-socket JSON IPC (v0.7.0 2014-10-16)" unix-socket)
+ (const :tag "Use --input-file FIFO (any mpv version)" file))
+ :group 'emms-player-mpv)
+
+(defcustom emms-player-mpv-ipc-socket
+ (concat (file-name-as-directory emms-directory)
+ "mpv-ipc.sock")
+ "Unix IPC socket or FIFO to use with mpv --input-* options,
+depending on `emms-player-mpv-ipc-method' value and/or mpv version."
+ :type 'file
+ :group 'emms-player-mpv)
+
+(defvar emms-player-mpv-ipc-proc nil) ; to avoid warnings while keeping useful defs at the top
+
+(defcustom emms-player-mpv-update-duration t
+ "Update track duration when played by mpv.
+Uses `emms-player-mpv-event-functions' hook."
+ :type 'boolean
+ :set (lambda (sym value)
+ (run-at-time 0.1 nil
+ (lambda (value)
+ (if value
+ (add-hook
+ 'emms-player-mpv-event-functions
+ #'emms-player-mpv-info-duration-event-func)
+ (remove-hook
+ 'emms-player-mpv-event-functions
+ #'emms-player-mpv-info-duration-event-func)))
+ value))
+ :group 'emms-player-mpv)
+
+(defcustom emms-player-mpv-update-metadata nil
+ "Update track info (artist, album, name, etc) from mpv events, when it is played.
+This allows to dynamically update stream info from ICY tags, for example.
+Uses `emms-player-mpv-event-connect-hook' and `emms-player-mpv-event-functions' hooks."
+ :type 'boolean
+ :set (lambda (sym value)
+ (run-at-time 0.1 nil
+ (lambda (value)
+ (if value
+ (progn
+ (add-hook
+ 'emms-player-mpv-event-connect-hook
+ #'emms-player-mpv-info-meta-connect-func)
+ (add-hook
+ 'emms-player-mpv-event-functions
+ #'emms-player-mpv-info-meta-event-func)
+ (when (process-live-p emms-player-mpv-ipc-proc)
+ (emms-player-mpv-info-meta-connect-func)))
+ (progn
+ (remove-hook
+ 'emms-player-mpv-event-connect-hook
+ #'emms-player-mpv-info-meta-connect-func)
+ (remove-hook
+ 'emms-player-mpv-event-functions
+ #'emms-player-mpv-info-meta-event-func))))
+ value))
+ :group 'emms-player-mpv)
+
+
+(defvar emms-player-mpv-proc nil
+ "Running mpv process, controlled over --input-ipc-server/--input-file sockets.")
+
+(defvar emms-player-mpv-proc-kill-delay 5
+ "Delay until SIGKILL gets sent to `emms-player-mpv-proc',
+if it refuses to exit cleanly on `emms-player-mpv-proc-stop'.")
+
+
+(defvar emms-player-mpv-ipc-proc nil
+ "Unix socket process that communicates with running `emms-player-mpv-proc' instance.")
+
+(defvar emms-player-mpv-ipc-buffer " *emms-player-mpv-ipc*"
+ "Buffer to associate with `emms-player-mpv-ipc-proc' socket/pipe process.")
+
+(defvar emms-player-mpv-ipc-connect-timer nil
+ "Timer for connection attempts to JSON IPC unix socket.")
+(defvar emms-player-mpv-ipc-connect-delays
+ '(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)
+ "List of delays before initiating socket connection for new mpv process.")
+
+(defvar emms-player-mpv-ipc-connect-command nil
+ "JSON command for `emms-player-mpv-ipc-sentinel' to run as soon as it connects to mpv.
+I.e. last command that either initiated connection or was used while connecting to mpv.
+Set by `emms-player-mpv-start' and such,
+cleared once it gets sent by `emms-player-mpv-ipc-sentinel'.")
+
+(defvar emms-player-mpv-ipc-id 1
+ "Auto-incremented value sent in JSON requests for request_id and observe_property id's.
+Use `emms-player-mpv-ipc-id-get' to get and increment this value, instead of using it directly.
+Wraps-around upon reaching `emms-player-mpv-ipc-id-max' (unlikely to ever happen).")
+
+(defvar emms-player-mpv-ipc-id-max (expt 2 30)
+ "Max value for `emms-player-mpv-ipc-id' to wrap around after.
+Should be fine with both mpv and emacs, and probably never reached anyway.")
+
+(defvar emms-player-mpv-ipc-req-table nil
+ "Auto-initialized hash table of outstanding API req_ids to their handler funcs.")
+
+(defvar emms-player-mpv-ipc-stop-command nil
+ "Internal flag to track when stop command starts/finishes before next loadfile.
+Set to either nil, t or playback start function to call on end-file event after stop command.
+This is a workaround for mpv-0.30+ behavior, when 'stop + loadfile' only runs 'stop'.")
+
+
+(defvar emms-player-mpv-event-connect-hook nil
+ "Normal hook run right after establishing new JSON IPC
+connection to mpv instance and before `emms-player-mpv-ipc-connect-command' if any.
+Best place to send any observe_property, request_log_messages, enable_event commands.
+Use `emms-player-mpv-ipc-id-get' to get unique id values for these.
+See also `emms-player-mpv-event-functions'.")
+
+(defvar emms-player-mpv-event-functions nil
+ "List of functions to call for each event emitted from JSON IPC.
+One argument is passed to each function - JSON line,
+as sent by mpv and decoded by `json-read-from-string'.
+See also `emms-player-mpv-event-connect-hook'.")
+
+
+(defvar emms-player-mpv-stopped nil
+ "Non-nil if playback was stopped by call from emms.
+Similar to `emms-player-stopped-p', but set for future async events,
+to indicate that playback should stop instead of switching to next track.")
+
+(defvar emms-player-mpv-idle-timer (timer-create)
+ "Timer to delay `emms-player-stopped' when mpv unexpectedly goes idle.")
+
+(defvar emms-player-mpv-idle-delay 0.5
+ "Delay before issuing `emms-player-stopped' when mpv unexpectedly goes idle.")
+
+
+(defvar emms-player-mpv-ipc-conn-emacs-26.1-workaround
+ (and (= emacs-major-version 26)
+ (= emacs-minor-version 1))
+ "Non-nil to enable workaround for issue #31901 in emacs 26.1.
+Emacs 26.1 fails to indicate missing socket file error for unix socket network processes
+that were started with :nowait t, so blocking connections are used there instead.")
+
+
+;; ----- helpers
+
+(defvar emms-player-mpv-debug nil
+ "Enable to print sent/received JSON lines and process
+start/stop events to *Messages* buffer using `emms-player-mpv-debug-msg'.")
+
+(defvar emms-player-mpv-debug-ts-offset nil
+ "Timestamp offset for `emms-player-mpv-debug-msg'.
+Set on first use, with intent to both shorten and obfuscate time in logs.")
+
+(defun emms-player-mpv-debug-trim (s)
+ (if (stringp s)
+ (replace-regexp-in-string "\\(^[ \t\n\r]+\\|[ \t\n\r]+$\\)" "" s t t)
+ s))
+
+(defun emms-player-mpv-debug-msg (tpl-or-msg &rest tpl-values)
+ "Print debug message to *Messages* if `emms-player-mpv-debug' is non-nil.
+Message is only formatted if TPL-VALUES is non-empty.
+Strips whitespace from start/end of TPL-OR-MSG and strings in TPL-VALUES."
+ (when emms-player-mpv-debug
+ (setq
+ tpl-or-msg (emms-player-mpv-debug-trim tpl-or-msg)
+ tpl-values (seq-map #'emms-player-mpv-debug-trim tpl-values))
+ (unless tpl-values
+ (setq tpl-or-msg (replace-regexp-in-string "%" "%%" tpl-or-msg t t)))
+ (let ((ts (float-time)))
+ (unless emms-player-mpv-debug-ts-offset (setq emms-player-mpv-debug-ts-offset ts))
+ (apply 'message
+ (concat "emms-player-mpv %.1f " tpl-or-msg)
+ (- ts emms-player-mpv-debug-ts-offset)
+ tpl-values))))
+
+(defun emms-player-mpv-ipc-fifo-p ()
+ "Returns non-nil if --input-file fifo should be used.
+Runs `emms-player-mpv-ipc-detect' to detect/set `emms-player-mpv-ipc-method' if necessary."
+ (unless emms-player-mpv-ipc-method
+ (setq emms-player-mpv-ipc-method
+ (emms-player-mpv-ipc-detect emms-player-mpv-command-name)))
+ (eq emms-player-mpv-ipc-method 'file))
+
+(defun emms-player-mpv-ipc-detect (cmd)
+ "Run mpv --version and return symbol for best IPC method supported.
+CMD should be either name of mpv binary to use or full path to it.
+Return values correspond to `emms-player-mpv-ipc-method' options.
+Error is signaled if mpv binary fails to run."
+ (with-temp-buffer
+ (let ((exit-code (call-process cmd nil '(t t)
+ nil "--version")))
+ (unless (zerop exit-code)
+ (insert (format "----- process exited with code %d -----" exit-code))
+ (error (format "Failed to run mpv binary [%s]:\n%s" cmd (buffer-string))))
+ (goto-char (point-min))
+ (pcase
+ (if (re-search-forward "^mpv\\s-+\\(\\([0-9]+\\.?\\)+\\)" nil t 1)
+ (mapconcat (lambda (n)
+ (format "%03d" n))
+ (seq-map 'string-to-number
+ (split-string (match-string-no-properties 1)
+ "\\." t))
+ ".")
+ "000.000.000")
+ ((pred (string> "000.006.999"))
+ 'file)
+ ((pred (string> "000.016.999"))
+ 'unix-socket)
+ (- 'ipc-server)))))
+
+
+;; ----- mpv process
+
+(defun emms-player-mpv-proc-playing-p (&optional proc)
+ "Return whether playback in PROC or `emms-player-mpv-proc' is started,
+which is distinct from 'start-command sent' and 'process is running' states.
+Used to signal emms via `emms-player-started' and `emms-player-stopped' calls."
+ (let ((proc (or proc emms-player-mpv-proc)))
+ (and proc (process-get proc 'mpv-playing))))
+
+(defun emms-player-mpv-proc-playing (state &optional proc)
+ "Set process mpv-playing state flag for `emms-player-mpv-proc-playing-p'."
+ (let ((proc (or proc emms-player-mpv-proc)))
+ (when proc (process-put proc 'mpv-playing state))))
+
+(defun emms-player-mpv-proc-symbol-id (sym &optional proc)
+ "Get unique process-specific id integer for SYM or nil if it was already requested."
+ (let
+ ((proc (or proc emms-player-mpv-proc))
+ (sym-id (intern (concat "mpv-sym-" (symbol-name sym)))))
+ (unless (process-get proc sym-id)
+ (let ((id (emms-player-mpv-ipc-id-get)))
+ (process-put proc sym-id id)
+ id))))
+
+(defun emms-player-mpv-proc-init-fifo (path &optional mode)
+ "Create named pipe (fifo) socket for mpv --input-file PATH, if not exists already.
+Optional MODE should be 12-bit octal integer, e.g. #o600 (safe default).
+Signals error if mkfifo exits with non-zero code."
+ (let ((attrs (file-attributes path)))
+ (when
+ (and attrs (not (string-prefix-p "p" (nth 8 attrs))))
+ (delete-file path)
+ (setq attrs nil))
+ (unless attrs
+ (unless
+ (zerop (call-process "mkfifo" nil nil nil
+ (format "--mode=%o" (or mode #o600))
+ path))
+ (error (format "Failed to run mkfifo for mpv --input-file path: %s" path))))))
+
+(defun emms-player-mpv-proc-sentinel (proc ev)
+ (let
+ ((status (process-status proc))
+ (playing (emms-player-mpv-proc-playing-p proc)))
+ (emms-player-mpv-debug-msg
+ "proc[%s]: %s (status=%s, playing=%s)" proc ev status playing)
+ (when (and (memq status '(exit signal))
+ playing)
+ (emms-player-stopped))))
+
+(defun emms-player-mpv-proc-init (&rest media-args)
+ "initialize new mpv process as `emms-player-mpv-proc'.
+MEDIA-ARGS are used instead of --idle, if specified."
+ (emms-player-mpv-proc-stop)
+ (unless (file-directory-p (file-name-directory emms-player-mpv-ipc-socket))
+ (make-directory (file-name-directory emms-player-mpv-ipc-socket)))
+ (when (emms-player-mpv-ipc-fifo-p)
+ (emms-player-mpv-proc-init-fifo emms-player-mpv-ipc-socket))
+ (let*
+ ((argv emms-player-mpv-parameters)
+ (argv (append
+ (list emms-player-mpv-command-name)
+ (if (functionp argv)
+ (funcall argv)
+ argv)
+ (list (format "--input-%s=%s"
+ emms-player-mpv-ipc-method emms-player-mpv-ipc-socket))
+ (or media-args '("--idle"))))
+ (env emms-player-mpv-environment)
+ (process-environment (append
+ (unless (seq-some 'not env)
+ process-environment)
+ (seq-filter 'identity env))))
+ (setq emms-player-mpv-proc
+ (make-process :name "emms-player-mpv"
+ :buffer nil :command argv :noquery t :sentinel #'emms-player-mpv-proc-sentinel))
+ (when (emms-player-mpv-ipc-fifo-p)
+ (emms-player-mpv-proc-playing t))
+ (emms-player-mpv-debug-msg "proc[%s]: start %s" emms-player-mpv-proc argv)))
+
+(defun emms-player-mpv-proc-stop ()
+ "Stop running `emms-player-mpv-proc' instance via SIGINT, if any.
+`delete-process' (SIGKILL) timer is started if `emms-player-mpv-proc-kill-delay' is non-nil."
+ (when emms-player-mpv-proc
+ (let ((proc emms-player-mpv-proc))
+ (emms-player-mpv-debug-msg "proc[%s]: stop" proc)
+ (if (not (process-live-p proc))
+ (delete-process proc)
+ (emms-player-mpv-proc-playing nil proc)
+ (interrupt-process proc)
+ (when emms-player-mpv-proc-kill-delay
+ (run-at-time
+ emms-player-mpv-proc-kill-delay nil
+ (lambda (proc)
+ (delete-process proc))
+ proc))))
+ (setq emms-player-mpv-proc nil)))
+
+
+;; ----- IPC socket/fifo
+
+(defun emms-player-mpv-ipc-sentinel (proc ev)
+ (emms-player-mpv-debug-msg "ipc[%s]: %s" proc ev)
+ (when (memq (process-status proc)
+ '(open run))
+ (run-hooks 'emms-player-mpv-event-connect-hook)
+ (when emms-player-mpv-ipc-connect-command
+ (let ((cmd emms-player-mpv-ipc-connect-command))
+ (setq emms-player-mpv-ipc-connect-command nil)
+ (emms-player-mpv-ipc-req-send cmd nil proc)))))
+
+(defun emms-player-mpv-ipc-filter (proc s)
+ (when (buffer-live-p (process-buffer proc))
+ (with-current-buffer (process-buffer proc)
+ (let ((moving (= (point)
+ (process-mark proc))))
+ (save-excursion
+ (goto-char (process-mark proc))
+ (insert s)
+ (set-marker (process-mark proc)
+ (point)))
+ (if moving (goto-char (process-mark proc))))
+ ;; Process/remove all complete lines of json, if any
+ (let ((p0 (point-min)))
+ (while
+ (progn
+ (goto-char p0)
+ (end-of-line)
+ (equal (following-char)
+ ?\n))
+ (let*
+ ((p1 (point))
+ (json (buffer-substring p0 p1)))
+ (delete-region p0 (+ p1 1))
+ (emms-player-mpv-ipc-recv json)))))))
+
+(defun emms-player-mpv-ipc-connect (delays)
+ "Make IPC connection attempt, rescheduling if there's no socket by (car DELAYS).
+(cdr DELAYS) gets passed to next connection attempt,
+so it can be rescheduled further until function runs out of DELAYS values.
+Sets `emms-player-mpv-ipc-proc' value to resulting process on success."
+ ;; Note - emacs handles missing unix socket files in different ways between versions:
+ ;; emacs <26 returns nil, emacs 26.1 leaves process in a stuck 'open
+ ;; state (see issue #31901), emacs 26.2+ sets 'file-missing status.
+ ;; None of these cases call sentinel function, so status must also be checked here.
+ (emms-player-mpv-debug-msg "ipc: connect-delay %s" (car delays))
+ (let ((use-nowait (not emms-player-mpv-ipc-conn-emacs-26.1-workaround)))
+ (setq emms-player-mpv-ipc-proc
+ (condition-case nil
+ (make-network-process
+ :name "emms-player-mpv-ipc"
+ :family 'local
+ :service emms-player-mpv-ipc-socket
+ :nowait use-nowait
+ :coding '(utf-8 . utf-8)
+ :buffer (get-buffer-create emms-player-mpv-ipc-buffer)
+ :noquery t
+ :filter #'emms-player-mpv-ipc-filter
+ :sentinel #'emms-player-mpv-ipc-sentinel)
+ (file-error nil)))
+ (unless (process-live-p emms-player-mpv-ipc-proc)
+ (setq emms-player-mpv-ipc-proc nil))
+ (when (and emms-player-mpv-ipc-proc (not use-nowait))
+ (emms-player-mpv-ipc-sentinel emms-player-mpv-ipc-proc 'open)))
+ (when (and (not emms-player-mpv-ipc-proc)
+ delays)
+ (run-at-time (car delays)
+ nil #'emms-player-mpv-ipc-connect (cdr delays))))
+
+(defun emms-player-mpv-ipc-connect-fifo ()
+ "Set `emms-player-mpv-ipc-proc' to process wrapper for
+writing to a named pipe (fifo) file/node or signal error."
+ (setq emms-player-mpv-ipc-proc
+ (start-process-shell-command "emms-player-mpv-input-file" nil
+ (format "cat > \"%s\"" (shell-quote-argument emms-player-mpv-ipc-socket))))
+ (set-process-query-on-exit-flag emms-player-mpv-ipc-proc nil)
+ (unless emms-player-mpv-ipc-proc (error (format
+ "Failed to start cat-pipe to fifo: %s" emms-player-mpv-ipc-socket)))
+ (when emms-player-mpv-ipc-connect-command
+ (let ((cmd emms-player-mpv-ipc-connect-command))
+ (setq emms-player-mpv-ipc-connect-command nil)
+ (emms-player-mpv-ipc-fifo-cmd cmd emms-player-mpv-ipc-proc))))
+
+(defun emms-player-mpv-ipc-init ()
+ "Initialize new mpv ipc socket/file process and associated state."
+ (emms-player-mpv-ipc-stop)
+ (emms-player-mpv-debug-msg "ipc: init")
+ (if (emms-player-mpv-ipc-fifo-p)
+ (emms-player-mpv-ipc-connect-fifo)
+ (when emms-player-mpv-ipc-connect-timer (cancel-timer emms-player-mpv-ipc-connect-timer))
+ (with-current-buffer (get-buffer-create emms-player-mpv-ipc-buffer)
+ (erase-buffer))
+ (setq
+ emms-player-mpv-ipc-id 1
+ emms-player-mpv-ipc-req-table nil
+ emms-player-mpv-ipc-connect-timer nil
+ emms-player-mpv-ipc-connect-timer
+ (run-at-time (car emms-player-mpv-ipc-connect-delays)
+ nil
+ #'emms-player-mpv-ipc-connect (cdr emms-player-mpv-ipc-connect-delays)))))
+
+(defun emms-player-mpv-ipc-stop ()
+ (when emms-player-mpv-ipc-proc
+ (emms-player-mpv-debug-msg "ipc: stop")
+ (delete-process emms-player-mpv-ipc-proc)
+ (setq emms-player-mpv-ipc-proc nil)))
+
+(defun emms-player-mpv-ipc ()
+ "Return open IPC socket/fifo process or nil, (re-)starting mpv/connection if necessary.
+Return nil when starting async process/connection, and any follow-up
+command should be stored to `emms-player-mpv-ipc-connect-command' in this case."
+ (unless
+ ;; Don't start idle processes for fifo - just ignore all ipc requests there
+ (and (not (process-live-p emms-player-mpv-proc))
+ (emms-player-mpv-ipc-fifo-p))
+ (unless (process-live-p emms-player-mpv-proc)
+ (emms-player-mpv-proc-init))
+ (unless (process-live-p emms-player-mpv-ipc-proc)
+ (emms-player-mpv-ipc-init))
+ (and
+ emms-player-mpv-ipc-proc
+ (memq (process-status emms-player-mpv-ipc-proc)
+ '(open run))
+ emms-player-mpv-ipc-proc)))
+
+
+;; ----- IPC protocol
+
+(defun emms-player-mpv-ipc-id-get ()
+ "Get new connection-unique id value, tracked via `emms-player-mpv-ipc-id'."
+ (let ((ipc-id emms-player-mpv-ipc-id))
+ (setq emms-player-mpv-ipc-id
+ (if (< emms-player-mpv-ipc-id emms-player-mpv-ipc-id-max)
+ (1+ emms-player-mpv-ipc-id)
+ 1))
+ ipc-id))
+
+(defun emms-player-mpv-ipc-req-send (cmd &optional handler proc)
+ "Send JSON IPC request and assign HANDLER to response for it, if any.
+CMD value is encoded via `json-encode'.
+HANDLER func will be called with decoded response JSON as (handler data err),
+where ERR will be either nil on \"success\", 'connection-error or whatever is in JSON.
+If HANDLER is nil, default `emms-player-mpv-ipc-req-error-printer'
+will be used to at least log errors.
+PROC can be specified to avoid `emms-player-mpv-ipc' call (e.g. from sentinel/filter funcs)."
+ (let
+ ((req-id (emms-player-mpv-ipc-id-get))
+ (req-proc (or proc (emms-player-mpv-ipc)))
+ (handler (or handler #'emms-player-mpv-ipc-req-error-printer)))
+ (unless emms-player-mpv-ipc-req-table
+ (setq emms-player-mpv-ipc-req-table (make-hash-table)))
+ (let ((json (concat (json-encode (list :command cmd :request_id req-id))
+ "\n")))
+ (emms-player-mpv-debug-msg "json >> %s" json)
+ (condition-case err
+ ;; On any disconnect, assume that mpv process is to blame and force restart.
+ (process-send-string req-proc json)
+ (error
+ (emms-player-mpv-proc-stop)
+ (funcall handler nil 'connection-error)
+ (setq handler nil))))
+ (when handler (puthash req-id handler emms-player-mpv-ipc-req-table))))
+
+(defun emms-player-mpv-ipc-req-resolve (req-id data err)
+ "Run handler-func for specified req-id."
+ (when emms-player-mpv-ipc-req-table
+ (let
+ ((handler (gethash req-id emms-player-mpv-ipc-req-table))
+ (err (if (string= err "success")
+ nil err)))
+ (remhash req-id emms-player-mpv-ipc-req-table)
+ (when handler (funcall handler data err)))))
+
+(defun emms-player-mpv-ipc-req-error-printer (data err)
+ "Simple default `emms-player-mpv-ipc-req-send' handler to log errors, if any."
+ (when err (message "emms-player-mpv ipc-error: %s" err)))
+
+(defun emms-player-mpv-ipc-recv (json)
+ "Handler for all JSON lines from mpv process.
+Only used with JSON IPC, never called with --input-file as there's no feedback there."
+ (emms-player-mpv-debug-msg "json << %s" json)
+ (let*
+ ((json-data (json-read-from-string json))
+ (req-id (alist-get 'request_id json-data))
+ (ev (alist-get 'event json-data)))
+ (when req-id
+ ;; Response to command
+ (emms-player-mpv-ipc-req-resolve req-id
+ (alist-get 'data json-data)
+ (alist-get 'error json-data)))
+ (when ev
+ ;; mpv event
+ (emms-player-mpv-event-handler json-data)
+ (run-hook-with-args 'emms-player-mpv-event-functions json-data))))
+
+(defun emms-player-mpv-ipc-fifo-cmd (cmd &optional proc)
+ "Send --input-file command string for older mpv versions.
+PROC can be specified to avoid `emms-player-mpv-ipc' call."
+ (let
+ ((proc (or proc (emms-player-mpv-ipc)))
+ (cmd-line (concat (mapconcat (lambda (s)
+ (format "%s" s))
+ cmd " ")
+ "\n")))
+ (emms-player-mpv-debug-msg "fifo >> %s" cmd-line)
+ (process-send-string proc cmd-line)))
+
+(defun emms-player-mpv-observe-property (sym)
+ "Send mpv observe_property command for property identified by SYM.
+Only sends command once per process, removing any
+potential duplication if used for same properties from different functions."
+ (let ((id (emms-player-mpv-proc-symbol-id sym)))
+ (when id (emms-player-mpv-ipc-req-send `(observe_property ,id ,sym)))))
+
+(defun emms-player-mpv-event-idle ()
+ "Delayed check for switching tracks when mpv goes idle for no good reason."
+ (emms-player-mpv-debug-msg "idle-check (stopped=%s)" emms-player-mpv-stopped)
+ (unless emms-player-mpv-stopped (emms-player-stopped)))
+
+(defun emms-player-mpv-event-handler (json-data)
+ "Handler for supported mpv events, including property changes.
+Called before `emms-player-mpv-event-functions' and does same thing as these hooks."
+ (pcase (alist-get 'event json-data)
+ ("playback-restart"
+ ;; Separate emms-player-mpv-proc-playing state is used for emms started/stopped signals,
+ ;; because start-file/end-file are also emitted after track-change and for playlists,
+ ;; and don't correspond to actual playback state.
+ (unless (emms-player-mpv-proc-playing-p)
+ (emms-player-mpv-proc-playing t)
+ (emms-player-started emms-player-mpv)))
+ ("end-file"
+ (when (emms-player-mpv-proc-playing-p)
+ (emms-player-mpv-proc-playing nil)
+ (emms-player-stopped))
+ (when emms-player-mpv-ipc-stop-command
+ (unless (eq emms-player-mpv-ipc-stop-command t)
+ (funcall emms-player-mpv-ipc-stop-command))
+ (setq emms-player-mpv-ipc-stop-command nil)))
+ ("idle"
+ ;; Can mean any kind of error before or during playback.
+ ;; Example can be access/format error, resulting in start+end without playback-restart.
+ (cancel-timer emms-player-mpv-idle-timer)
+ (setq
+ emms-player-mpv-idle-timer
+ (run-at-time emms-player-mpv-idle-delay nil #'emms-player-mpv-event-idle)
+ emms-player-mpv-ipc-stop-command nil))
+ ("start-file" (cancel-timer emms-player-mpv-idle-timer))))
+
+
+;; ----- Metadata update hooks
+
+(defun emms-player-mpv-info-meta-connect-func ()
+ "Hook function for `emms-player-mpv-event-connect-hook' to update metadata from mpv."
+ (emms-player-mpv-observe-property 'metadata))
+
+(defun emms-player-mpv-info-meta-event-func (json-data)
+ "Hook function for `emms-player-mpv-event-functions' to update metadata from mpv."
+ (when
+ (and
+ (string= (alist-get 'event json-data)
+ "property-change")
+ (string= (alist-get 'name json-data)
+ "metadata"))
+ (let ((info-alist (alist-get 'data json-data)))
+ (when info-alist (emms-player-mpv-info-meta-update-track info-alist)))))
+
+(defun emms-player-mpv-info-meta-update-track (info-alist &optional track)
+ "Update TRACK with mpv metadata from INFO-ALIST.
+`emms-playlist-current-selected-track' is used by default."
+ (mapc
+ (lambda (cc)
+ (setcar cc (intern (downcase (symbol-name (car cc))))))
+ info-alist)
+ (cl-macrolet
+ ((key (k)
+ `(alist-get ',k info-alist))
+ (set-track-info (track &rest body)
+ (cons 'progn
+ (cl-loop for (k v)
+ on body by 'cddr collect
+ `(let ((value ,v))
+ (when value
+ (emms-track-set ,track ',(intern (format "info-%s" k))
+ value)))))))
+ (unless track (setq track (emms-playlist-current-selected-track)))
+ (set-track-info track
+ title (or (key title)
+ (key icy-title))
+ artist (or (key artist)
+ (key album_artist)
+ (key icy-name))
+ album (key album)
+ tracknumber (key track)
+ year (key date)
+ genre (key genre)
+ note (key comment))
+ (emms-track-updated track)))
+
+(defun emms-player-mpv-info-duration-event-func (json-data)
+ "Hook function for `emms-player-mpv-event-functions' to update track duration from mpv."
+ (when
+ (string= (alist-get 'event json-data)
+ "playback-restart")
+ (emms-player-mpv-info-duration-check)))
+
+(defun emms-player-mpv-info-duration-check ()
+ "Check whether current mpv track has reliable duration info and request it."
+ (emms-player-mpv-ipc-req-send '(get_property stream-end)
+ (lambda (pts-end err)
+ (if err
+ (unless (and (stringp err)
+ (string= err "property unavailable"))
+ (emms-player-mpv-ipc-req-error-printer pts-end err))
+ (when pts-end
+ (emms-player-mpv-ipc-req-send '(get_property duration)
+ #'emms-player-mpv-info-duration-handler))))))
+
+(defun emms-player-mpv-info-duration-handler (duration err)
+ "Duration property request handler to update it for current emms track."
+ (if err
+ (emms-player-mpv-debug-msg "duration-req-error: %s" err)
+ ;; Duration can be nil or 0 for network streams, depending on version/stream
+ (when (and (numberp duration)
+ (> duration 0))
+ (let
+ ((duration (round duration))
+ (track (emms-playlist-current-selected-track)))
+ (emms-track-set track 'info-playing-time duration)
+ (emms-track-set track 'info-playing-time-min (/ duration 60))
+ (emms-track-set track 'info-playing-time-sec (% duration 60))))))
+
+
+;; ----- High-level EMMS interface
+
+(defun emms-player-mpv-cmd (cmd &optional handler)
+ "Send mpv command to process/connection if both are running,
+or otherwise schedule start/connect and set
+`emms-player-mpv-ipc-start-track' for `emms-player-mpv-ipc-sentinel'."
+ (setq emms-player-mpv-ipc-connect-command nil)
+ (let ((proc (emms-player-mpv-ipc)))
+ (if proc
+ (if (emms-player-mpv-ipc-fifo-p)
+ (emms-player-mpv-ipc-fifo-cmd cmd proc)
+ (emms-player-mpv-ipc-req-send cmd handler proc))
+ (setq emms-player-mpv-ipc-connect-command cmd))))
+
+(defmacro emms-player-mpv-cmd-prog (cmd &rest handler-body)
+ "Macro around `emms-player-mpv-cmd' that creates
+handler callback (see `emms-player-mpv-ipc-req-send') from HANDLER-BODY forms,
+which have following bindings:
+- mpv-cmd for CMD.
+- mpv-data for response data (decoded json, nil if none).
+- mpv-error for response error (nil if no error, decoded json or 'connection-error)."
+ `(emms-player-mpv-cmd ,cmd (apply-partially
+ (lambda (mpv-cmd mpv-data mpv-error)
+ ,@handler-body)
+ ,cmd)))
+
+
+(defun emms-player-mpv-playable-p (track)
+ (memq (emms-track-type track)
+ '(file url streamlist playlist)))
+
+(defun emms-player-mpv-start-error-handler (mpv-cmd mpv-data mpv-error)
+ "Playback-restart error handler for `emms-player-mpv-cmd',
+to restart/reconnect-to mpv and re-run MPV-CMD,
+if there was any issue when trying to start it initially."
+ (if (eq mpv-error 'connection-error)
+ ;; Reconnect and restart playback if current connection fails (e.g. mpv crash)
+ (emms-player-mpv-cmd-prog
+ (emms-player-mpv-cmd mpv-cmd)
+ (emms-player-mpv-cmd `(set pause no)))
+ (emms-player-mpv-cmd `(set pause no))))
+
+(defun emms-player-mpv-start (track)
+ (setq emms-player-mpv-stopped nil)
+ (emms-player-mpv-proc-playing nil)
+ (let
+ ((track-name (emms-track-get track 'name))
+ (track-is-playlist (memq (emms-track-get track 'type)
+ '(streamlist playlist))))
+ (if (emms-player-mpv-ipc-fifo-p)
+ (progn
+ ;; ipc-stop is to clear any buffered commands
+ (emms-player-mpv-ipc-stop)
+ (emms-player-mpv-proc-init (if track-is-playlist "--playlist" "--")
+ track-name)
+ (emms-player-started emms-player-mpv))
+ (let*
+ ((start-cmd (list (if track-is-playlist 'loadlist 'loadfile)
+ track-name 'replace))
+ (start-func `(lambda ()
+ (emms-player-mpv-cmd ',start-cmd
+ (apply-partially 'emms-player-mpv-start-error-handler ',start-cmd)))))
+ (if emms-player-mpv-ipc-stop-command
+ (setq emms-player-mpv-ipc-stop-command start-func)
+ (funcall start-func))))))
+
+(defun emms-player-mpv-stop ()
+ (setq
+ emms-player-mpv-stopped t
+ emms-player-mpv-ipc-stop-command t)
+ (emms-player-mpv-proc-playing nil)
+ (emms-player-mpv-cmd `(stop))
+ (emms-player-stopped))
+
+
+(defun emms-player-mpv-pause ()
+ (emms-player-mpv-cmd `(set pause yes)))
+
+(defun emms-player-mpv-resume ()
+ (emms-player-mpv-cmd `(set pause no)))
+
+(defun emms-player-mpv-seek (sec)
+ (emms-player-mpv-cmd `(seek ,sec relative)))
+
+(defun emms-player-mpv-seek-to (sec)
+ (emms-player-mpv-cmd `(seek ,sec absolute)))
+
+(emms-player-set emms-player-mpv 'pause #'emms-player-mpv-pause)
+(emms-player-set emms-player-mpv 'resume #'emms-player-mpv-resume)
+(emms-player-set emms-player-mpv 'seek #'emms-player-mpv-seek)
+(emms-player-set emms-player-mpv 'seek-to #'emms-player-mpv-seek-to)
+
+
+(provide 'emms-player-mpv)
+;;; emms-player-mpv.el ends here