bbae936afac92e3d7b9605fab146d0a8ed5e00bc
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
Stefan Schuermans support synchronizing video...

Stefan Schuermans authored 10 years ago

34)     self.re_mplayer_audio_pos = re.compile(r"A: *([0-9]+.[0-9]+) *\([0-9:.]*\) of .*")
35)     self.re_mplayer_video_pos = re.compile(r"A: *([0-9]+.[0-9]+) *V: *[0-9]+.[0-9]+ A-V: .*")
Stefan Schuermans change local file name pref...

Stefan Schuermans authored 10 years ago

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

Stefan Schuermans authored 10 years ago

37)     # create member variables
38)     self.mplayer = None
39)     self.mplayer_buf_stdout = ""
40)     self.mplayer_buf_stderr = ""
41)     self.mplayer_last_cmd_timestamp = None
42)     self.mplayer_name = None
43)     self.mplayer_pause = None
44)     self.mplayer_pos = None
45)     self.mplayer_speed = None
46)     self.mplayer_timestamp = None
47)     self.offset_samples = []
48)     self.playlist = []
49)     self.playlist_idx = None
50)     self.posy_name = None
51)     self.posy_pause = None
52)     self.posy_pos = None
53)     self.posy_timestamp = None
54)     self.sock = None
55)     self.verbose = False
56)     # startup
57)     self.sockSetup()
58) 
59)   def __del__(self):
60)     """deconstruct object"""
61)     self.mplayerStop()
62)     self.sockClose()
63) 
64)   def dbg_print(self, txt):
65)     """output debug information in verbose mode"""
66)     if self.verbose:
67)       print >>sys.stderr, txt
68) 
69)   def mplayerClear(self):
70)     """clear MPlayer buffers and information"""
71)     self.mplayer_buf_stdin = ""
72)     self.mplayer_buf_stderr = ""
73)     self.mplayer_last_cmd_timestamp = None
74)     self.mplayer_name = None
75)     self.mplayer_pause = None
76)     self.mplayer_pos = None
77)     self.mplayer_speed = None
78)     self.mplayer_timestamp = None
79)     self.offset_samples = []
80) 
81)   def mplayerExit(self):
82)     """react to MPlayer exit"""
83)     # close pipes
84)     self.mplayer.stdin.close()
85)     self.mplayer.stdout.close()
86)     self.mplayer.stderr.close()
87)     # close process
88)     self.mplayer.wait()
89)     self.mplayer = None
90)     # clear buffers and information
91)     self.mplayerClear()
92)     # play next file
93)     self.playNext()
94) 
95)   def mplayerLine(self, line, err):
96)     """process line from MPlayer stdout (err = False) or stderr (err = True)"""
97)     if len(line) > 0:
98)       if err:
99)         self.dbg_print("MPlayer stderr: " + line)
100)       else:
101)         self.dbg_print("MPlayer stdout: " + line)
102)     if not err:
Stefan Schuermans support synchronizing video...

Stefan Schuermans authored 10 years ago

103)       # MPlayer position information (audio)
104)       m_mplayer_audio_pos = self.re_mplayer_audio_pos.match(line)
105)       if m_mplayer_audio_pos:
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

106)         self.mplayer_timestamp = datetime.datetime.now()
Stefan Schuermans support synchronizing video...

Stefan Schuermans authored 10 years ago

107)         self.mplayer_pos = float(m_mplayer_audio_pos.group(1))
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

108)         # synchronize
109)         self.sync()
Stefan Schuermans support synchronizing video...

Stefan Schuermans authored 10 years ago

110)       else:
111)         # MPlayer position information (video)
112)         m_mplayer_video_pos = self.re_mplayer_video_pos.match(line)
113)         if m_mplayer_video_pos:
114)           self.mplayer_timestamp = datetime.datetime.now()
115)           self.mplayer_pos = float(m_mplayer_video_pos.group(1))
116)           # synchronize
117)           self.sync()
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

118) 
119)   def mplayerPause(self, pause):
120)     """pause/unpause MPlayer"""
121)     # leave if no MPlayer running
122)     if self.mplayer is None:
123)       return
124)     # switch to pause mode
125)     if pause:
126)       self.dbg_print("MPlayer stdin: pausing seek 0 0")
Stefan Schuermans avoid exception on broken p...

Stefan Schuermans authored 10 years ago

127)       try:
128)         self.mplayer.stdin.write("pausing seek 0 0\n") # rel seek 0s, then pause
129)       except:
130)         pass
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

131)       self.mplayer_pause = True
132)     # continue playing
133)     else:
134)       self.dbg_print("MPlayer stdin: seek 0 0")
Stefan Schuermans avoid exception on broken p...

Stefan Schuermans authored 10 years ago

135)       try:
136)         self.mplayer.stdin.write("seek 0 0\n") # realtive seek of 0s
137)       except:
138)         pass
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

139)       self.mplayer_pause = False
140)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
141) 
142)   def mplayerSetPos(self, pos):
143)     """set MPlayer position"""
144)     # leave if no MPlayer running
145)     if self.mplayer is None:
146)       return
147)     # sanitize position to avoid mplayer crashes in any case
148)     if pos < 0.0:
149)       pos = 0.0
150)     # set new position
151)     self.dbg_print("MPlayer stdin: seek %5.3f 2" % pos)
Stefan Schuermans avoid exception on broken p...

Stefan Schuermans authored 10 years ago

152)     try:
153)       self.mplayer.stdin.write("seek %5.3f 2\n" % pos) # 2 means absolute pos
154)     except:
155)       pass
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

156)     self.mplayer_pos = pos
157)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
158) 
159)   def mplayerSetSpeed(self, speed):
160)     """set MPlayer speed"""
161)     # leave if no MPlayer running
162)     if self.mplayer is None:
163)       return
164)     # sanitize speed to avoid mplayer crashes in any case
165)     if speed < 0.5:
166)       speed = 0.5
167)     if speed > 2.0:
168)       speed = 2.0
169)     # set new speed
170)     self.dbg_print("MPlayer stdin: speed_set %5.3f" % speed)
Stefan Schuermans avoid exception on broken p...

Stefan Schuermans authored 10 years ago

171)     try:
172)       self.mplayer.stdin.write("speed_set %5.3f\n" % speed)
173)     except:
174)       pass
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

175)     self.mplayer_speed = speed
176)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
177) 
178)   def mplayerStart(self, filename):
179)     """start MPlayer process in background"""
180)     # stop old MPlayer
181)     self.mplayerStop()
182)     # start MPlayer
Stefan Schuermans start mplayer with higher v...

Stefan Schuermans authored 10 years ago

183)     cmd = [ "mplayer", "-volume", "80", "-slave", "-af", "scaletempo",
184)             filename ]
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

185)     print >>sys.stderr, "starting background process: " + " ".join(cmd)
186)     self.mplayer = subprocess.Popen(cmd, stdin = subprocess.PIPE,
187)                                          stdout = subprocess.PIPE,
188)                                          stderr = subprocess.PIPE)
189)     # make output pipes nonblocking
190)     fcntl.fcntl(self.mplayer.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
191)     fcntl.fcntl(self.mplayer.stderr, fcntl.F_SETFL, os.O_NONBLOCK)
192)     # set initial information
193)     self.mplayer_name = filename
194)     self.mplayer_pause = False
195)     self.mplayer_speed = 1.0
196)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
197) 
198)   def mplayerStdouterr(self, err):
199)     """process data from MPlayer stdout (err = False) or stderr (err = True)"""
200)     if self.mplayer is None:
201)       return
202)     # receive data
203)     if err:
204)       txt = self.mplayer.stderr.read()
205)     else:
206)       txt = self.mplayer.stdout.read()
207)     # check if MPlayer exited
208)     if len(txt) == 0:
209)       self.mplayerExit()
210)       return
211)     # MPlayer did not exit
212)     # replace CRs with LFs add data to buffer
213)     txt = txt.replace("\r", "\n")
214)     if err:
215)       buf = self.mplayer_buf_stderr + txt
216)     else:
217)       buf = self.mplayer_buf_stdout + txt
218)     # process complete lines and store remaining data in buffer
219)     lines = buf.split("\n")
220)     for line in lines[:-1]:
221)       self.mplayerLine(line, err)
222)     if err:
223)       self.mplayer_buf_stderr = buf[-1]
224)     else:
225)       self.mplayer_buf_stdout = buf[-1]
226) 
227)   def mplayerStop(self):
228)     """stop MPlayer process in background"""
229)     if self.mplayer is not None:
230)       # send quit command
Stefan Schuermans avoid exception on broken p...

Stefan Schuermans authored 10 years ago

231)       try:
232)         self.mplayer.stdin.write("quit\n")
233)       except:
234)         pass
Stefan Schuermans script to synchronize mplay...

Stefan Schuermans authored 10 years ago

235)       # close pipes
236)       self.mplayer.stdin.close()
237)       self.mplayer.stdout.close()
238)       self.mplayer.stderr.close()
239)       # terminate process
240)       self.mplayer.terminate()
241)       self.mplayer.kill()
242)       # close process
243)       self.mplayer.wait()
244)       self.mplayer = None
245)     # clear buffers and information
246)     self.mplayerClear()
247) 
248)   def playlistFind(self, posy_name):
249)     """find file in playlist by PoSy name"""
250)     idx = 0
251)     for file_name in self.playlist:
252)       if self.posyCheckName(posy_name, file_name):
253)         return idx;
254)       idx += 1
255)     return None
256) 
257)   def playlistRead(self, playlist):
258)     """read playlist file"""
259)     # read filenames from playlist
260)     filenames = []
261)     try:
262)       with open(playlist, "rt") as f:
263)         filenames = f.readlines()
264)     except:
265)       return False
266)     filenames = [filename.strip() for filename in filenames]
267)     # convert filenames to absolute paths
268)     playlistdir = os.path.dirname(playlist)
269)     filenames = [os.path.join(playlistdir, filename) for filename in filenames]
270)     # replace playlist
271)     self.playlist = filenames
272)     self.playlist_idx = None
273)     return True
274) 
275)   def playNext(self):
276)     """play next file in playlist"""
277)     # playlist empty -> stop MPlayer
278)     if len(self.playlist) == 0:
279)       self.playlist_idx = None
280)       self.mplayerStop()
281)     # playlist not empty -> play next file
282)     else:
283)       if self.playlist_idx is None:
284)         self.playlist_idx = 0
285)       else:
286)         self.playlist_idx += 1
287)         if self.playlist_idx >= len(self.playlist):
288)           self.playlist_idx = 0
289)       self.mplayerStart(self.playlist[self.playlist_idx])
290) 
291)   def playPosyName(self, posy_name):
292)     """play file by PoSy name (if found)"""
293)     # find file in playlist
294)     idx = self.playlistFind(posy_name)
295)     # file not found -> stop MPlayer
296)     if idx is None:
297)       self.playlist_idx = None
298)       self.mplayerStop()
299)     # file found -> (re-)start MPlayer
300)     else:
301)       self.playlist_idx = idx
302)       self.mplayerStart(self.playlist[idx])
303) 
304)   def posyCheckName(self, posy_name, file_name):
305)     """check if filename matches PoSyName"""
306)     # remove directory part of file name and check
307)     file_name = os.path.basename(file_name)
308)     if file_name == posy_name:
309)       return True
310)     # remove extension and check
311)     file_name = os.path.splitext(file_name)[0]
312)     if file_name == posy_name:
313)       return True
Stefan Schuermans change local file name pref...

Stefan Schuermans authored 10 years ago

314)     # remove ignore prefix and check
315)     m_ignore_prefix = self.re_ignore_prefix.match(file_name)
316)     if m_ignore_prefix and m_ignore_prefix.group(1) == posy_name: