mpv.el (13427B)
1 ;;; mpv.el --- control mpv for easy note-taking -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2014-2018 Johann Klähn 4 5 ;; Author: Johann Klähn <johann@jklaehn.de> 6 ;; URL: https://github.com/kljohann/mpv.el 7 ;; Package-Version: 20211228.2043 8 ;; Package-Commit: 4fd8baa508dbc1a6b42b4e40292c0dbb0f19c9b9 9 ;; Version: 0.2.0 10 ;; Keywords: tools, multimedia 11 ;; Package-Requires: ((cl-lib "0.5") (emacs "25.1") (json "1.3") (org "8.0")) 12 13 ;; This program is free software; you can redistribute it and/or modify 14 ;; it under the terms of the GNU General Public License as published by 15 ;; the Free Software Foundation, either version 3 of the License, or 16 ;; (at your option) any later version. 17 18 ;; This program is distributed in the hope that it will be useful, 19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 ;; GNU General Public License for more details. 22 23 ;; You should have received a copy of the GNU General Public License 24 ;; along with this program. If not, see <http://www.gnu.org/licenses/>. 25 26 ;;; Commentary: 27 28 ;; This package is a potpourri of helper functions to control a mpv 29 ;; process via its IPC interface. You might want to add the following 30 ;; to your init file: 31 ;; 32 ;; (org-add-link-type "mpv" #'mpv-play) 33 ;; (defun org-mpv-complete-link (&optional arg) 34 ;; (replace-regexp-in-string 35 ;; "file:" "mpv:" 36 ;; (org-file-complete-link arg) 37 ;; t t)) 38 ;; (add-hook 'org-open-at-point-functions #'mpv-seek-to-position-at-point) 39 40 ;;; Code: 41 42 (require 'cl-lib) 43 (require 'json) 44 (require 'org) 45 (require 'org-timer) 46 (require 'tq) 47 48 (defgroup mpv nil 49 "Customization group for mpv." 50 :prefix "mpv-" 51 :group 'external) 52 53 (defcustom mpv-executable "mpv" 54 "Name or path to the mpv executable." 55 :type 'file 56 :group 'mpv) 57 58 (defcustom mpv-default-options nil 59 "List of default options to be passed to mpv." 60 :type '(repeat string) 61 :group 'mpv) 62 63 (defcustom mpv-speed-step 1.10 64 "Scale factor used when adjusting playback speed." 65 :type 'number 66 :group 'mpv) 67 68 (defcustom mpv-volume-step 1.50 69 "Scale factor used when adjusting volume." 70 :type 'number 71 :group 'mpv) 72 73 (defcustom mpv-seek-step 5 74 "Step size in seconds used when seeking." 75 :type 'number 76 :group 'mpv) 77 78 (defcustom mpv-on-event-hook nil 79 "Hook to run when an event message is received. 80 The hook will be called with the parsed JSON message as its only an 81 argument. See \"List of events\" in the mpv man page." 82 :type 'hook 83 :group 'mpv) 84 85 (defcustom mpv-on-start-hook nil 86 "Hook to run when a new mpv process is started. 87 The hook will be called with the arguments passed to `mpv-start'." 88 :type 'hook 89 :group 'mpv) 90 91 (defcustom mpv-on-exit-hook nil 92 "Hook to run when the mpv process dies." 93 :type 'hook 94 :group 'mpv) 95 96 (defvar mpv--process nil) 97 (defvar mpv--queue nil) 98 99 (defun mpv-live-p () 100 "Return non-nil if inferior mpv is running." 101 (and mpv--process (eq (process-status mpv--process) 'run))) 102 103 (defun mpv-start (&rest args) 104 "Start an mpv process with the specified ARGS. 105 106 If there already is an mpv process controlled by this Emacs instance, 107 it will be killed. Options specified in `mpv-default-options' will be 108 prepended to ARGS." 109 (mpv-kill) 110 (let ((socket (make-temp-name 111 (expand-file-name "mpv-" temporary-file-directory)))) 112 (setq mpv--process 113 (apply #'start-process "mpv-player" nil mpv-executable 114 "--no-terminal" 115 (concat "--input-unix-socket=" socket) 116 (append mpv-default-options args))) 117 (set-process-query-on-exit-flag mpv--process nil) 118 (set-process-sentinel 119 mpv--process 120 (lambda (process _event) 121 (when (memq (process-status process) '(exit signal)) 122 (mpv-kill) 123 (when (file-exists-p socket) 124 (with-demoted-errors (delete-file socket))) 125 (run-hooks 'mpv-on-exit-hook)))) 126 (with-timeout 127 (0.5 (mpv-kill) 128 (error "Failed to connect to mpv")) 129 (while (not (file-exists-p socket)) 130 (sleep-for 0.05))) 131 (setq mpv--queue (tq-create 132 (make-network-process :name "mpv-socket" 133 :family 'local 134 :service socket))) 135 (set-process-filter 136 (tq-process mpv--queue) 137 (lambda (_proc string) 138 (mpv--tq-filter mpv--queue string))) 139 (run-hook-with-args 'mpv-on-start-hook args) 140 t)) 141 142 (defun mpv--as-strings (command) 143 "Convert COMMAND to a list of strings." 144 (mapcar (lambda (arg) 145 (if (numberp arg) 146 (number-to-string arg) 147 arg)) 148 command)) 149 150 (defun mpv--enqueue (command fn &optional delay-command) 151 "Add COMMAND to the transaction queue. 152 153 FN will be called with the corresponding answer. 154 If DELAY-COMMAND is non-nil, delay sending this question until 155 the process has finished replying to any previous questions. 156 This produces more reliable results with some processes. 157 158 Note that we do not use the regexp and closure arguments of 159 `tq-enqueue', see our custom implementation of `tq-process-buffer' 160 below." 161 (when (mpv-live-p) 162 (tq-enqueue 163 mpv--queue 164 (concat (json-encode `((command . ,(mpv--as-strings command)))) "\n") 165 "" nil fn delay-command) 166 t)) 167 168 (defun mpv-run-command (command &rest arguments) 169 "Send a COMMAND to mpv, passing the remaining ARGUMENTS. 170 Block while waiting for the response." 171 (when (mpv-live-p) 172 (let* ((response 173 (cl-block mpv-run-command-wait-for-response 174 (mpv--enqueue 175 (cons command arguments) 176 (lambda (response) 177 (cl-return-from mpv-run-command-wait-for-response 178 response))) 179 (while (mpv-live-p) 180 (sleep-for 0.05)))) 181 (status (alist-get 'error response)) 182 (data (alist-get 'data response))) 183 (unless (string-equal status "success") 184 (error "`%s' failed: %s" command status)) 185 data))) 186 187 (defun mpv--tq-filter (tq string) 188 "Append to the queue's buffer and process the new data. 189 190 TQ is a transaction queue created by `tq-create'. 191 STRING is the data fragment received from the process. 192 193 This is a verbatim copy of `tq-filter' that uses 194 `mpv--tq-process-buffer' instead of `tq-process-buffer'." 195 (let ((buffer (tq-buffer tq))) 196 (when (buffer-live-p buffer) 197 (with-current-buffer buffer 198 (goto-char (point-max)) 199 (insert string) 200 (mpv--tq-process-buffer tq))))) 201 202 (defun mpv--tq-process-buffer (tq) 203 "Check TQ's buffer for a JSON response. 204 205 Replacement for `tq-process-buffer' that ignores regular expressions 206 \(answers are always passed to the first handler in the queue) and 207 passes unsolicited event messages to `mpv-on-event-hook'." 208 (goto-char (point-min)) 209 (skip-chars-forward "^{") 210 (let ((answer (ignore-errors (json-read)))) 211 (when answer 212 (delete-region (point-min) (point)) 213 ;; event messages have form {"event": ...} 214 ;; answers have form {"error": ..., "data": ...} 215 (cond 216 ((assoc 'event answer) 217 (run-hook-with-args 'mpv-on-event-hook answer)) 218 ((not (tq-queue-empty tq)) 219 (unwind-protect 220 (funcall (tq-queue-head-fn tq) answer) 221 (tq-queue-pop tq)))) 222 ;; Recurse to check for further JSON messages. 223 (mpv--tq-process-buffer tq)))) 224 225 ;;;###autoload 226 (defun mpv-play (path) 227 "Start an mpv process playing the file at PATH. 228 229 You can use this with `org-add-link-type' or `org-file-apps'. 230 See `mpv-start' if you need to pass further arguments and 231 `mpv-default-options' for default options." 232 (interactive "fFile: ") 233 (mpv-start (expand-file-name path))) 234 235 ;;;###autoload 236 (defun mpv-kill () 237 "Kill the mpv process." 238 (interactive) 239 (when mpv--queue 240 (tq-close mpv--queue)) 241 (when (mpv-live-p) 242 (kill-process mpv--process)) 243 (with-timeout 244 (0.5 (error "Failed to kill mpv")) 245 (while (mpv-live-p) 246 (sleep-for 0.05))) 247 (setq mpv--process nil) 248 (setq mpv--queue nil)) 249 250 ;;;###autoload 251 (defun mpv-pause () 252 "Pause or unpause playback." 253 (interactive) 254 (mpv--enqueue '("cycle" "pause") #'ignore)) 255 256 (defun mpv-get-property (property) 257 "Return the value of the given PROPERTY." 258 (mpv-run-command "get_property" property)) 259 260 (defun mpv-set-property (property value) 261 "Set the given PROPERTY to VALUE." 262 (mpv-run-command "set_property" property value)) 263 264 (defun mpv-cycle-property (property) 265 "Cycle the given PROPERTY." 266 (mpv-run-command "cycle" property)) 267 268 (defun mpv-get-playback-position () 269 "Return the current playback position in seconds." 270 (mpv-get-property "playback-time")) 271 272 (defun mpv-get-duration () 273 "Return the estimated total duration of the current file in seconds." 274 (mpv-get-property "duration")) 275 276 ;;;###autoload 277 (defun mpv-insert-playback-position (&optional arg) 278 "Insert the current playback position at point. 279 280 When called with a non-nil ARG, insert a timer list item like `org-timer-item'." 281 (interactive "P") 282 (let ((time (mpv-get-playback-position))) 283 (funcall 284 (if arg #'mpv--position-insert-as-org-item #'insert) 285 (org-timer-secs-to-hms (round time))))) 286 287 (defun mpv--position-insert-as-org-item (time-string) 288 "Insert a description-type item with the playback position TIME-STRING. 289 290 See `org-timer-item' which this is based on." 291 (cl-letf (((symbol-function 'org-timer) 292 (lambda (&optional _restart no-insert) 293 (funcall 294 (if no-insert #'identity #'insert) 295 (concat time-string " "))))) 296 (org-timer-item))) 297 298 ;;;###autoload 299 (defun mpv-seek-to-position-at-point () 300 "Jump to playback position as inserted by `mpv-insert-playback-position'. 301 302 This can be used with the `org-open-at-point-functions' hook." 303 (interactive) 304 (save-excursion 305 (skip-chars-backward ":[:digit:]" (point-at-bol)) 306 (when (looking-at "[0-9]+:[0-9]\\{2\\}:[0-9]\\{2\\}") 307 (let ((secs (org-timer-hms-to-secs (match-string 0)))) 308 (when (>= secs 0) 309 (mpv-seek secs)))))) 310 311 ;;;###autoload 312 (defun mpv-speed-set (factor) 313 "Set playback speed to FACTOR." 314 (interactive "nFactor: ") 315 (mpv--enqueue `("set" "speed" ,(abs factor)) #'ignore)) 316 317 ;;;###autoload 318 (defun mpv-speed-increase (steps) 319 "Increase playback speed by STEPS factors of `mpv-speed-step'." 320 (interactive "p") 321 (let ((factor (if (>= steps 0) 322 (* steps mpv-speed-step) 323 (/ 1 (* (- steps) mpv-speed-step))))) 324 (mpv--enqueue `("multiply" "speed" ,factor) #'ignore))) 325 326 ;;;###autoload 327 (defun mpv-speed-decrease (steps) 328 "Decrease playback speed by STEPS factors of `mpv-speed-step'." 329 (interactive "p") 330 (mpv-speed-increase (- steps))) 331 332 ;;;###autoload 333 (defun mpv-volume-set (factor) 334 "Set playback volume to FACTOR." 335 (interactive "nFactor: ") 336 (mpv--enqueue `("set" "volume" ,(abs factor)) #'ignore)) 337 338 ;;;###autoload 339 (defun mpv-volume-increase (steps) 340 "Increase playback volume by STEPS factors of `mpv-volume-step'." 341 (interactive "p") 342 (let ((factor (if (>= steps 0) 343 (* steps mpv-volume-step) 344 (/ 1 (* (- steps) mpv-volume-step))))) 345 (mpv--enqueue `("multiply" "volume" ,factor) #'ignore))) 346 347 ;;;###autoload 348 (defun mpv-volume-decrease (steps) 349 "Decrease playback volume by STEPS factors of `mpv-volume-step'." 350 (interactive "p") 351 (mpv-volume-increase (- steps))) 352 353 (defun mpv--raw-prefix-to-seconds (arg) 354 "Convert raw prefix argument ARG to seconds using `mpv-seek-step'. 355 Numeric arguments will be treated as seconds, repeated use 356 \\[universal-argument] will be multiplied with `mpv-seek-step'." 357 (if (numberp arg) 358 arg 359 (* mpv-seek-step 360 (cl-signum (or (car arg) 1)) 361 (log (abs (or (car arg) 4)) 4)))) 362 363 ;;;###autoload 364 (defun mpv-seek (seconds) 365 "Seek to the given (absolute) time in SECONDS. 366 A negative value is interpreted relative to the end of the file." 367 (interactive "nPosition in seconds: ") 368 (mpv--enqueue `("seek" ,seconds "absolute") #'ignore)) 369 370 ;;;###autoload 371 (defun mpv-seek-forward (arg) 372 "Seek forward ARG seconds. 373 If ARG is numeric, it is used as the number of seconds. Else each use 374 of \\[universal-argument] will add another `mpv-seek-step' seconds." 375 (interactive "P") 376 (mpv--enqueue `("seek" ,(mpv--raw-prefix-to-seconds arg) "relative") #'ignore)) 377 378 ;;;###autoload 379 (defun mpv-seek-backward (arg) 380 "Seek backward ARG seconds. 381 If ARG is numeric, it is used as the number of seconds. Else each use 382 of \\[universal-argument] will add another `mpv-seek-step' seconds." 383 (interactive "P") 384 (mpv-seek-forward (- (mpv--raw-prefix-to-seconds arg)))) 385 386 ;;;###autoload 387 (defun mpv-revert-seek () 388 "Undo the previous seek command." 389 (interactive) 390 (mpv--enqueue '("revert-seek") #'ignore)) 391 392 ;;;###autoload 393 (defun mpv-playlist-next () 394 "Go to the next entry on the playlist." 395 (interactive) 396 (mpv--enqueue '("playlist-next") #'ignore)) 397 398 ;;;###autoload 399 (defun mpv-playlist-prev () 400 "Go to the previous entry on the playlist." 401 (interactive) 402 (mpv--enqueue '("playlist-prev") #'ignore)) 403 404 ;;;###autoload 405 (defun mpv-version () 406 "Return the mpv version string. 407 When called interactively, also show a more verbose version in 408 the echo area." 409 (interactive) 410 (let ((version (cadr (split-string (car (process-lines mpv-executable "--version")))))) 411 (prog1 version 412 (if (called-interactively-p 'interactive) 413 (message "mpv %s" version))))) 414 415 (provide 'mpv) 416 ;;; mpv.el ends here