9e69203db77e0ed468ad070aa0fa026f976b179f
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

1) #! /usr/bin/env python
2) 
3) # MPlayer synchronizer
4) # Copyright 2014 Stefan Schuermans <stefan@schuermans.info>
5) # Copyleft: GNU public license - http://www.gnu.org/copyleft/gpl.html
6) 
7) import datetime
8) import fcntl
9) import os
10) import re
11) import select
12) import socket
13) import struct
14) import subprocess
15) import sys
16) import time
17) 
18) scriptdir = os.path.dirname(os.path.abspath(__file__))
19) 
20) verbose = False
21) 
22) class Synchronizer:
23) 
24)   def __init__(self):
25)     """construct an MPlayer Synchronizer object"""
26)     # set constants
27)     self.info_timeout     = 1.0   # timeout (s) for info from MPlayer or PoSy
28)     self.max_equal_offset = 0.1   # maximum offset tolerated as equal
29)     self.min_cmd_delay    = 0.1   # minimum time (s) between MPlayer commands
30)     self.min_seek_offset  = 0.5   # minimum offset (s) required for a seek
31)     self.select_timeout   = 0.1   # timeout (s) for select syscall
32)     self.speed_change     = 0.05  # change of MPlayer speed for catching up
33)     # create static objects
34)     self.re_mplayer_pos = re.compile(r"[AV]: *([0-9]+.[0-9]+) *\([0-9:.]*\) of .*")
Stefan Schuermans change local file name pref...

Stefan Schuermans authored 10 years ago

35)     self.re_ignore_prefix = re.compile(r"[0-9a-zA-Z_]+__(.*)")
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

36)     # create member variables
37)     self.mplayer = None
38)     self.mplayer_buf_stdout = ""
39)     self.mplayer_buf_stderr = ""
40)     self.mplayer_last_cmd_timestamp = None
41)     self.mplayer_name = None
42)     self.mplayer_pause = None
43)     self.mplayer_pos = None
44)     self.mplayer_speed = None
45)     self.mplayer_timestamp = None
46)     self.offset_samples = []
47)     self.playlist = []
48)     self.playlist_idx = None
49)     self.posy_name = None
50)     self.posy_pause = None
51)     self.posy_pos = None
52)     self.posy_timestamp = None
53)     self.sock = None
54)     self.verbose = False
55)     # startup
56)     self.sockSetup()
57) 
58)   def __del__(self):
59)     """deconstruct object"""
60)     self.mplayerStop()
61)     self.sockClose()
62) 
63)   def dbg_print(self, txt):
64)     """output debug information in verbose mode"""
65)     if self.verbose:
66)       print >>sys.stderr, txt
67) 
68)   def mplayerClear(self):
69)     """clear MPlayer buffers and information"""
70)     self.mplayer_buf_stdin = ""
71)     self.mplayer_buf_stderr = ""
72)     self.mplayer_last_cmd_timestamp = None
73)     self.mplayer_name = None
74)     self.mplayer_pause = None
75)     self.mplayer_pos = None
76)     self.mplayer_speed = None
77)     self.mplayer_timestamp = None
78)     self.offset_samples = []
79) 
80)   def mplayerExit(self):
81)     """react to MPlayer exit"""
82)     # close pipes
83)     self.mplayer.stdin.close()
84)     self.mplayer.stdout.close()
85)     self.mplayer.stderr.close()
86)     # close process
87)     self.mplayer.wait()
88)     self.mplayer = None
89)     # clear buffers and information
90)     self.mplayerClear()
91)     # play next file
92)     self.playNext()
93) 
94)   def mplayerLine(self, line, err):
95)     """process line from MPlayer stdout (err = False) or stderr (err = True)"""
96)     if len(line) > 0:
97)       if err:
98)         self.dbg_print("MPlayer stderr: " + line)
99)       else:
100)         self.dbg_print("MPlayer stdout: " + line)
101)     if not err:
102)       # MPlayer position information
103)       m_mplayer_pos = self.re_mplayer_pos.match(line)
104)       if m_mplayer_pos:
105)         self.mplayer_timestamp = datetime.datetime.now()
106)         self.mplayer_pos = float(m_mplayer_pos.group(1))
107)         # synchronize
108)         self.sync()
109) 
110)   def mplayerPause(self, pause):
111)     """pause/unpause MPlayer"""
112)     # leave if no MPlayer running
113)     if self.mplayer is None:
114)       return
115)     # switch to pause mode
116)     if pause:
117)       self.dbg_print("MPlayer stdin: pausing seek 0 0")
118)       self.mplayer.stdin.write("pausing seek 0 0\n") # rel seek 0s, then pause
119)       self.mplayer_pause = True
120)     # continue playing
121)     else:
122)       self.dbg_print("MPlayer stdin: seek 0 0")
123)       self.mplayer.stdin.write("seek 0 0\n") # realtive seek of 0s
124)       self.mplayer_pause = False
125)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
126) 
127)   def mplayerSetPos(self, pos):
128)     """set MPlayer position"""
129)     # leave if no MPlayer running
130)     if self.mplayer is None:
131)       return
132)     # sanitize position to avoid mplayer crashes in any case
133)     if pos < 0.0:
134)       pos = 0.0
135)     # set new position
136)     self.dbg_print("MPlayer stdin: seek %5.3f 2" % pos)
137)     self.mplayer.stdin.write("seek %5.3f 2\n" % pos) # 2 means absolute pos
138)     self.mplayer_pos = pos
139)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
140) 
141)   def mplayerSetSpeed(self, speed):
142)     """set MPlayer speed"""
143)     # leave if no MPlayer running
144)     if self.mplayer is None:
145)       return
146)     # sanitize speed to avoid mplayer crashes in any case
147)     if speed < 0.5:
148)       speed = 0.5
149)     if speed > 2.0:
150)       speed = 2.0
151)     # set new speed
152)     self.dbg_print("MPlayer stdin: speed_set %5.3f" % speed)
153)     self.mplayer.stdin.write("speed_set %5.3f\n" % speed)
154)     self.mplayer_speed = speed
155)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
156) 
157)   def mplayerStart(self, filename):
158)     """start MPlayer process in background"""
159)     # stop old MPlayer
160)     self.mplayerStop()
161)     # start MPlayer
162)     cmd = [ "mplayer", "-slave", "-af", "scaletempo", filename ]
163)     print >>sys.stderr, "starting background process: " + " ".join(cmd)
164)     self.mplayer = subprocess.Popen(cmd, stdin = subprocess.PIPE,
165)                                          stdout = subprocess.PIPE,
166)                                          stderr = subprocess.PIPE)
167)     # make output pipes nonblocking
168)     fcntl.fcntl(self.mplayer.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
169)     fcntl.fcntl(self.mplayer.stderr, fcntl.F_SETFL, os.O_NONBLOCK)
170)     # set initial information
171)     self.mplayer_name = filename
172)     self.mplayer_pause = False
173)     self.mplayer_speed = 1.0
174)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
175) 
176)   def mplayerStdouterr(self, err):
177)     """process data from MPlayer stdout (err = False) or stderr (err = True)"""
178)     if self.mplayer is None:
179)       return
180)     # receive data
181)     if err:
182)       txt = self.mplayer.stderr.read()
183)     else:
184)       txt = self.mplayer.stdout.read()
185)     # check if MPlayer exited
186)     if len(txt) == 0:
187)       self.mplayerExit()
188)       return
189)     # MPlayer did not exit
190)     # replace CRs with LFs add data to buffer
191)     txt = txt.replace("\r", "\n")
192)     if err:
193)       buf = self.mplayer_buf_stderr + txt
194)     else:
195)       buf = self.mplayer_buf_stdout + txt
196)     # process complete lines and store remaining data in buffer
197)     lines = buf.split("\n")
198)     for line in lines[:-1]:
199)       self.mplayerLine(line, err)
200)     if err:
201)       self.mplayer_buf_stderr = buf[-1]
202)     else:
203)       self.mplayer_buf_stdout = buf[-1]
204) 
205)   def mplayerStop(self):
206)     """stop MPlayer process in background"""
207)     if self.mplayer is not None:
208)       # send quit command
209)       self.mplayer.stdin.write("quit\n")
210)       # close pipes
211)       self.mplayer.stdin.close()
212)       self.mplayer.stdout.close()
213)       self.mplayer.stderr.close()
214)       # terminate process
215)       self.mplayer.terminate()
216)       self.mplayer.kill()
217)       # close process
218)       self.mplayer.wait()
219)       self.mplayer = None
220)     # clear buffers and information
221)     self.mplayerClear()
222) 
223)   def playlistFind(self, posy_name):
224)     """find file in playlist by PoSy name"""
225)     idx = 0
226)     for file_name in self.playlist:
227)       if self.posyCheckName(posy_name, file_name):
228)         return idx;
229)       idx += 1
230)     return None
231) 
232)   def playlistRead(self, playlist):
233)     """read playlist file"""
234)     # read filenames from playlist
235)     filenames = []
236)     try:
237)       with open(playlist, "rt") as f:
238)         filenames = f.readlines()
239)     except:
240)       return False
241)     filenames = [filename.strip() for filename in filenames]
242)     # convert filenames to absolute paths
243)     playlistdir = os.path.dirname(playlist)
244)     filenames = [os.path.join(playlistdir, filename) for filename in filenames]
245)     # replace playlist
246)     self.playlist = filenames
247)     self.playlist_idx = None
248)     return True
249) 
250)   def playNext(self):
251)     """play next file in playlist"""
252)     # playlist empty -> stop MPlayer
253)     if len(self.playlist) == 0:
254)       self.playlist_idx = None
255)       self.mplayerStop()
256)     # playlist not empty -> play next file
257)     else:
258)       if self.playlist_idx is None:
259)         self.playlist_idx = 0
260)       else:
261)         self.playlist_idx += 1
262)         if self.playlist_idx >= len(self.playlist):
263)           self.playlist_idx = 0
264)       self.mplayerStart(self.playlist[self.playlist_idx])
265) 
266)   def playPosyName(self, posy_name):
267)     """play file by PoSy name (if found)"""
268)     # find file in playlist
269)     idx = self.playlistFind(posy_name)
270)     # file not found -> stop MPlayer
271)     if idx is None:
272)       self.playlist_idx = None
273)       self.mplayerStop()
274)     # file found -> (re-)start MPlayer
275)     else:
276)       self.playlist_idx = idx
277)       self.mplayerStart(self.playlist[idx])
278) 
279)   def posyCheckName(self, posy_name, file_name):
280)     """check if filename matches PoSyName"""
281)     # remove directory part of file name and check
282)     file_name = os.path.basename(file_name)
283)     if file_name == posy_name:
284)       return True
285)     # remove extension and check
286)     file_name = os.path.splitext(file_name)[0]
287)     if file_name == posy_name:
288)       return True
Stefan Schuermans change local file name pref...

Stefan Schuermans authored 10 years ago

289)     # remove ignore prefix and check
290)     m_ignore_prefix = self.re_ignore_prefix.match(file_name)
291)     if m_ignore_prefix and m_ignore_prefix.group(1) == posy_name: