55
66import json
77import logging
8+ import os
89import socket
910import subprocess
1011import tempfile
@@ -43,7 +44,7 @@ class MPVIPCClient:
4344
4445 def __init__ (self , socket_path : str ):
4546 self .socket_path = socket_path
46- self .socket : Optional [socket . socket ] = None
47+ self .socket : Optional [Any ] = None
4748 self ._request_id_counter = 0
4849 self ._lock = threading .Lock ()
4950
@@ -55,16 +56,55 @@ def __init__(self, socket_path: str):
5556 self ._response_dict : Dict [int , Any ] = {}
5657 self ._response_events : Dict [int , threading .Event ] = {}
5758
59+ @staticmethod
60+ def _is_windows_named_pipe (path : str ) -> bool :
61+ return path .startswith ("\\ \\ .\\ pipe\\ " )
62+
63+ @staticmethod
64+ def _supports_unix_sockets () -> bool :
65+ return hasattr (socket , "AF_UNIX" )
66+
67+ @staticmethod
68+ def _open_windows_named_pipe (path : str ):
69+ # MPV's JSON IPC on Windows uses named pipes like: \\.\pipe\mpvpipe
70+ # Opening the pipe as a binary file supports read/write.
71+ f = open (path , "r+b" , buffering = 0 )
72+
73+ class _PipeConn :
74+ def __init__ (self , fileobj ):
75+ self ._f = fileobj
76+
77+ def recv (self , n : int ) -> bytes :
78+ return self ._f .read (n )
79+
80+ def sendall (self , data : bytes ) -> None :
81+ self ._f .write (data )
82+ self ._f .flush ()
83+
84+ def close (self ) -> None :
85+ self ._f .close ()
86+
87+ return _PipeConn (f )
88+
5889 def connect (self , timeout : float = 5.0 ) -> None :
5990 """Connect to MPV IPC socket and start the reader thread."""
60- if not hasattr (socket , "AF_UNIX" ):
61- raise MPVIPCError ("Unix domain sockets are unavailable on this platform" )
6291
6392 start_time = time .time ()
6493 while time .time () - start_time < timeout :
6594 try :
66- self .socket = socket .socket (socket .AF_UNIX , socket .SOCK_STREAM )
67- self .socket .connect (self .socket_path )
95+ if self ._supports_unix_sockets () and not self ._is_windows_named_pipe (
96+ self .socket_path
97+ ):
98+ self .socket = socket .socket (socket .AF_UNIX , socket .SOCK_STREAM ) # type: ignore (type error on Windows but this code path won't be used there)
99+ self .socket .connect (self .socket_path )
100+ else :
101+ if os .name != "nt" or not self ._is_windows_named_pipe (self .socket_path ):
102+ raise MPVIPCError (
103+ "MPV IPC requires Unix domain sockets (AF_UNIX) or a Windows named pipe path "
104+ "like \\ \\ .\\ pipe\\ mpvpipe. Got: "
105+ f"{ self .socket_path } "
106+ )
107+ self .socket = self ._open_windows_named_pipe (self .socket_path )
68108 logger .info (f"Connected to MPV IPC socket at { self .socket_path } " )
69109 self ._start_reader_thread ()
70110 return
@@ -302,10 +342,6 @@ def play(
302342 def _play_with_ipc (self , player : BasePlayer , params : PlayerParams ) -> PlayerResult :
303343 """Play media using MPV IPC."""
304344 try :
305- if not hasattr (socket , "AF_UNIX" ):
306- raise MPVIPCError (
307- "MPV IPC requires Unix domain sockets, which are unavailable on this platform."
308- )
309345 self ._start_mpv_process (player , params )
310346 self ._connect_ipc ()
311347 self ._setup_event_handling ()
@@ -336,8 +372,12 @@ def _play_with_ipc(self, player: BasePlayer, params: PlayerParams) -> PlayerResu
336372
337373 def _start_mpv_process (self , player : BasePlayer , params : PlayerParams ) -> None :
338374 """Start MPV process with IPC enabled."""
339- temp_dir = Path (tempfile .gettempdir ())
340- self .socket_path = str (temp_dir / f"mpv_ipc_{ time .time ()} .sock" )
375+ if hasattr (socket , "AF_UNIX" ):
376+ temp_dir = Path (tempfile .gettempdir ())
377+ self .socket_path = str (temp_dir / f"mpv_ipc_{ time .time ()} .sock" )
378+ else :
379+ # Windows MPV IPC uses named pipes.
380+ self .socket_path = f"\\ \\ .\\ pipe\\ mpv_ipc_{ int (time .time () * 1000 )} "
341381 self .mpv_process = player .play_with_ipc (params , self .socket_path )
342382 time .sleep (1.0 )
343383
@@ -487,7 +527,11 @@ def _cleanup(self):
487527 self .mpv_process .wait (timeout = 3 )
488528 except subprocess .TimeoutExpired :
489529 self .mpv_process .kill ()
490- if self .socket_path and Path (self .socket_path ).exists ():
530+ if (
531+ self .socket_path
532+ and not self .socket_path .startswith ("\\ \\ .\\ pipe\\ " )
533+ and Path (self .socket_path ).exists ()
534+ ):
491535 Path (self .socket_path ).unlink (missing_ok = True )
492536
493537 def _get_episode (
0 commit comments