f0e43476adcfb281c91ae094da9ddb66bd3b58ee
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")
127)       self.mplayer.stdin.write("pausing seek 0 0\n") # rel seek 0s, then pause
128)       self.mplayer_pause = True
129)     # continue playing
130)     else:
131)       self.dbg_print("MPlayer stdin: seek 0 0")
132)       self.mplayer.stdin.write("seek 0 0\n") # realtive seek of 0s
133)       self.mplayer_pause = False
134)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
135) 
136)   def mplayerSetPos(self, pos):
137)     """set MPlayer position"""
138)     # leave if no MPlayer running
139)     if self.mplayer is None:
140)       return
141)     # sanitize position to avoid mplayer crashes in any case
142)     if pos < 0.0:
143)       pos = 0.0
144)     # set new position
145)     self.dbg_print("MPlayer stdin: seek %5.3f 2" % pos)
146)     self.mplayer.stdin.write("seek %5.3f 2\n" % pos) # 2 means absolute pos
147)     self.mplayer_pos = pos
148)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
149) 
150)   def mplayerSetSpeed(self, speed):
151)     """set MPlayer speed"""
152)     # leave if no MPlayer running
153)     if self.mplayer is None:
154)       return
155)     # sanitize speed to avoid mplayer crashes in any case
156)     if speed < 0.5:
157)       speed = 0.5
158)     if speed > 2.0:
159)       speed = 2.0
160)     # set new speed
161)     self.dbg_print("MPlayer stdin: speed_set %5.3f" % speed)
162)     self.mplayer.stdin.write("speed_set %5.3f\n" % speed)
163)     self.mplayer_speed = speed
164)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
165) 
166)   def mplayerStart(self, filename):
167)     """start MPlayer process in background"""
168)     # stop old MPlayer
169)     self.mplayerStop()
170)     # start MPlayer
Stefan Schuermans start mplayer with higher v...

Stefan Schuermans authored 10 years ago

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

Stefan Schuermans authored 10 years ago

173)     print >>sys.stderr, "starting background process: " + " ".join(cmd)
174)     self.mplayer = subprocess.Popen(cmd, stdin = subprocess.PIPE,
175)                                          stdout = subprocess.PIPE,
176)                                          stderr = subprocess.PIPE)
177)     # make output pipes nonblocking
178)     fcntl.fcntl(self.mplayer.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
179)     fcntl.fcntl(self.mplayer.stderr, fcntl.F_SETFL, os.O_NONBLOCK)
180)     # set initial information
181)     self.mplayer_name = filename
182)     self.mplayer_pause = False
183)     self.mplayer_speed = 1.0
184)     self.mplayer_last_cmd_timestamp = datetime.datetime.now()
185) 
186)   def mplayerStdouterr(self, err):
187)     """process data from MPlayer stdout (err = False) or stderr (err = True)"""
188)     if self.mplayer is None:
189)       return
190)     # receive data
191)     if err:
192)       txt = self.mplayer.stderr.read()
193)     else:
194)       txt = self.mplayer.stdout.read()
195)     # check if MPlayer exited
196)     if len(txt) == 0:
197)       self.mplayerExit()
198)       return
199)     # MPlayer did not exit
200)     # replace CRs with LFs add data to buffer
201)     txt = txt.replace("\r", "\n")
202)     if err:
203)       buf = self.mplayer_buf_stderr + txt
204)     else:
205)       buf = self.mplayer_buf_stdout + txt
206)     # process complete lines and store remaining data in buffer
207)     lines = buf.split("\n")
208)     for line in lines[:-1]:
209)       self.mplayerLine(line, err)
210)     if err:
211)       self.mplayer_buf_stderr = buf[-1]
212)     else:
213)       self.mplayer_buf_stdout = buf[-1]
214) 
215)   def mplayerStop(self):
216)     """stop MPlayer process in background"""
217)     if self.mplayer is not None:
218)       # send quit command
219)       self.mplayer.stdin.write("quit\n")
220)       # close pipes
221)       self.mplayer.stdin.close()
222)       self.mplayer.stdout.close()
223)       self.mplayer.stderr.close()
224)       # terminate process
225)       self.mplayer.terminate()
226)       self.mplayer.kill()
227)       # close process
228)       self.mplayer.wait()
229)       self.mplayer = None
230)     # clear buffers and information
231)     self.mplayerClear()
232) 
233)   def playlistFind(self, posy_name):
234)     """find file in playlist by PoSy name"""
235)     idx = 0
236)     for file_name in self.playlist:
237)       if self.posyCheckName(posy_name, file_name):
238)         return idx;
239)       idx += 1
240)     return None
241) 
242)   def playlistRead(self, playlist):
243)     """read playlist file"""
244)     # read filenames from playlist
245)     filenames = []
246)     try:
247)       with open(playlist, "rt") as f:
248)         filenames = f.readlines()
249)     except:
250)       return False
251)     filenames = [filename.strip() for filename in filenames]
252)     # convert filenames to absolute paths
253)     playlistdir = os.path.dirname(playlist)
254)     filenames = [os.path.join(playlistdir, filename) for filename in filenames]
255)     # replace playlist
256)     self.playlist = filenames
257)     self.playlist_idx = None
258)     return True
259) 
260)   def playNext(self):
261)     """play next file in playlist"""
262)     # playlist empty -> stop MPlayer
263)     if len(self.playlist) == 0:
264)       self.playlist_idx = None
265)       self.mplayerStop()
266)     # playlist not empty -> play next file
267)     else:
268)       if self.playlist_idx is None:
269)         self.playlist_idx = 0
270)       else:
271)         self.playlist_idx += 1
272)         if self.playlist_idx >= len(self.playlist):
273)           self.playlist_idx = 0
274)       self.mplayerStart(self.playlist[self.playlist_idx])
275) 
276)   def playPosyName(self, posy_name):
277)     """play file by PoSy name (if found)"""
278)     # find file in playlist
279)     idx = self.playlistFind(posy_name)
280)     # file not found -> stop MPlayer
281)     if idx is None:
282)       self.playlist_idx = None
283)       self.mplayerStop()
284)     # file found -> (re-)start MPlayer
285)     else:
286)       self.playlist_idx = idx
287)       self.mplayerStart(self.playlist[idx])
288) 
289)   def posyCheckName(self, posy_name, file_name):
290)     """check if filename matches PoSyName"""
291)     # remove directory part of file name and check
292)     file_name = os.path.basename(file_name)
293)     if file_name == posy_name:
294)       return True
295)     # remove extension and check
296)     file_name = os.path.splitext(file_name)[0]
297)     if file_name == posy_name:
298)       return True
Stefan Schuermans change local file name pref...

Stefan Schuermans authored 10 years ago

299)     # remove ignore prefix and check
300)     m_ignore_prefix = self.re_ignore_prefix.match(file_name)
301)     if m_ignore_prefix and m_ignore_prefix.group(1) == posy_name: