1 # Written by Jan David Mol, Arno Bakker
2 # see LICENSE.txt for license information
9 from BaseLib.Core.simpledefs import *
11 # live streaming means wrapping around
12 LIVE_WRAPAROUND = True
17 """ Info about the selected video and status of the playback. """
19 # TODO: thread safety? PiecePicker, MovieSelector and MovieOnDemandTransporter all interface this
21 def __init__(self,piecelen,fileinfo,videoinfo,authparams):
23 piecelen = length of BitTorrent pieces
24 fileinfo = list of (name,length) pairs for all files in the torrent,
25 in their recorded order
26 videoinfo = videoinfo object from download engine
28 self.piecelen = piecelen # including signature, if any
30 self.fileinfo = fileinfo
31 self.videoinfo = videoinfo
32 self.authparams = authparams
34 # size of high probability set, in seconds (piecepicker varies
35 # between the limit values depending on network performance,
36 # increases and decreases are in the specified step (min,max,step)
37 self.high_prob_curr_time = 10
38 self.high_prob_curr_time_limit = (10, 180, 10)
40 # size of high probability set, in pieces (piecepicker
41 # varies between the limit values depending on network
42 # performance, increases and decreases are in the specified step
44 # Arno, 2010-03-10: max 50 pieces too little for 32K piece-sized
47 self.high_prob_curr_pieces = 5
48 self.high_prob_curr_pieces_limit = (5, 1800 ,5) # Arno, 2010-03-11: with 32K pieces and 3 mbps we're talking 10 pieces / sec
50 # ----- locate selected movie in fileinfo
51 index = self.videoinfo['index']
55 movie_offset = sum( (filesize for (_,filesize) in fileinfo[:index] if filesize) )
56 movie_name = fileinfo[index][0]
57 movie_size = fileinfo[index][1]
59 self.selected_movie = {
60 "offset": movie_offset,
65 # ----- derive generic movie parameters
66 movie_begin = movie_offset
67 movie_end = movie_offset + movie_size - 1
69 # movie_range = (bpiece,offset),(epiece,offset), inclusive
70 self.movie_range = ( (movie_begin/piecelen, movie_begin%piecelen),
71 (movie_end/piecelen, movie_end%piecelen) )
72 self.first_piecelen = piecelen - self.movie_range[0][1]
73 self.last_piecelen = self.movie_range[1][1]+1 # Arno, 2010-01-08: corrected off by one error
74 self.first_piece = self.movie_range[0][0]
75 self.last_piece = self.movie_range[1][0]
76 self.movie_numpieces = self.last_piece - self.first_piece + 1
78 # ----- live streaming settings
79 self.live_streaming = videoinfo['live']
80 self.live_startpos = None
81 self.playback_pos_observers = []
82 self.wraparound = self.live_streaming and LIVE_WRAPAROUND
83 # /8 means -12.5 % ... + 12.5 % = 25 % window
84 self.wraparound_delta = max(4,self.movie_numpieces/8)
86 # ----- generic streaming settings
87 # whether to drop packets that come in too late
88 if self.live_streaming:
89 self.dropping = True # drop, but we will autopause as well
91 self.dropping = False # just wait and produce flawless playback
93 if videoinfo['bitrate']:
94 self.set_bitrate( videoinfo['bitrate'] )
96 self.set_bitrate( 512*1024/8 ) # default to 512 Kbit/s
97 self.bitrate_set = False
99 # ----- set defaults for dynamic positions
100 self.playing = False # video has started playback
101 self.paused = False # video is paused
102 self.autoresume = False # video is paused but will resume automatically
103 self.prebuffering = True # video is prebuffering
104 self.playback_pos = self.first_piece
106 self.pausable = (VODEVENT_PAUSE in videoinfo["userevents"]) and (VODEVENT_RESUME in videoinfo["userevents"])
108 def add_playback_pos_observer( self, observer ):
109 """ Add a function to be called when the playback position changes. Is called as follows:
110 observer( oldpos, newpos ). In case of initialisation: observer( None, startpos ). """
111 self.playback_pos_observers.append( observer )
113 def real_piecelen( self, x ):
114 if x == self.first_piece:
115 return self.first_piecelen
116 elif x == self.last_piece:
117 return self.last_piecelen
121 def set_bitrate( self, bitrate ):
122 #print >>sys.stderr,"vodstatus: set_bitrate",bitrate
123 self.bitrate_set = True
124 self.bitrate = bitrate
125 self.sec_per_piece = 1.0 * bitrate / self.piecelen
127 def set_live_startpos( self, pos ):
129 if self.live_startpos is None:
130 oldrange = self.first_piece,self.last_piece
132 oldrange = self.live_get_valid_range()
134 print >>sys.stderr,"vodstatus: set_live_pos: old",oldrange
135 self.live_startpos = pos
136 self.playback_pos = pos
137 for o in self.playback_pos_observers:
141 newrange = self.live_get_valid_range()
143 print >>sys.stderr,"vodstatus: set_live_pos: new",newrange
144 return self.get_range_diff(oldrange,newrange)
149 def get_live_startpos(self):
150 return self.live_startpos
152 # the following functions work with absolute piece numbers,
153 # so they all function within the range [first_piece,last_piece]
155 # the range of pieces to download is
156 # [playback_pos,numpieces) for normal downloads and
157 # [playback_pos,playback_pos+delta) for wraparound
159 def generate_range( self, (f, t) ):
160 if self.wraparound and f > t:
161 for x in xrange( f, self.last_piece+1 ):
163 for x in xrange( self.first_piece, t ):
166 for x in xrange( f, t ):
169 def dist_range(self, f, t):
170 """ Returns the distance between f and t """
172 return self.last_piece-f + t-self.first_piece
176 def in_range( self, f, t, x ):
177 if self.wraparound and f > t:
178 return self.first_piece <= x < t or f <= x <= self.last_piece
182 def inc_playback_pos( self ):
183 oldpos = self.playback_pos
184 self.playback_pos += 1
186 if self.playback_pos > self.last_piece:
188 self.playback_pos = self.first_piece
190 # Arno, 2010-01-08: Adjusted EOF condition to work well with seeking/HTTP range queries
191 self.playback_pos = self.last_piece+1
193 for o in self.playback_pos_observers:
194 o( oldpos, self.playback_pos )
196 def in_download_range( self, x ):
198 wraplen = self.playback_pos + self.wraparound_delta - self.last_piece
200 return self.first_piece <= x < self.first_piece + wraplen or self.playback_pos <= x <= self.last_piece
202 return self.playback_pos <= x < self.playback_pos + self.wraparound_delta
204 return self.first_piece <= x <= self.last_piece
206 def in_valid_range(self,piece):
207 if self.live_streaming:
208 if self.live_startpos is None:
209 # Haven't hooked in yet
212 (begin,end) = self.live_get_valid_range()
213 ret = self.in_range(begin,end,piece)
215 print >>sys.stderr,"vod: status: NOT in_valid_range:",begin,"<",piece,"<",end
218 return self.first_piece <= piece <= self.last_piece
220 def live_get_valid_range(self):
221 begin = self.normalize(self.playback_pos - self.wraparound_delta)
222 end = self.normalize(self.playback_pos + self.wraparound_delta)
225 def live_piece_to_invalidate(self):
226 #print >>sys.stderr,"vod: live_piece_to_inval:",self.playback_pos,self.wraparound_delta,self.movie_numpieces
227 return self.normalize(self.playback_pos - self.wraparound_delta)
229 def get_range_diff(self,oldrange,newrange):
230 """ Returns the diff between oldrange and newrange as a Set.
233 if oldrange[0] == 0 and oldrange[1] == self.movie_numpieces-1:
234 # Optimize for case where there is no playback pos yet, for STB.
235 if newrange[0] < newrange[1]:
236 # 100-500, diff is 0-99 + 501-7200
237 a = (oldrange[0],newrange[0]-1)
238 b = (newrange[1]+1,oldrange[1])
239 #print >>sys.stderr,"get_range_diff: ranges",a,b
242 #return Set(range(a[0],a[1]) + range(b[0],b[1]))
244 # 500-100, diff is 101-499
245 a = (newrange[1]+1,newrange[0]-1)
246 #print >>sys.stderr,"get_range_diff: range",a
249 #return Set(xrange(a[0],a[1]))
251 oldset = range2set(oldrange,self.movie_numpieces)
252 newset = range2set(newrange,self.movie_numpieces)
253 return (oldset - newset,rlist)
255 def normalize( self, x ):
256 """ Caps or wraps a piece number. """
258 if self.first_piece <= x <= self.last_piece:
262 # in Python, -1 % 3 == 2, so modulo will do our work for us if x < first_piece
263 return (x - self.first_piece) % self.movie_numpieces + self.first_piece
265 return max( self.first_piece, min( x, self.last_piece ) )
267 def time_to_pieces( self, sec ):
268 """ Returns the number of pieces that are needed to hold "sec" seconds of content. """
270 # TODO: take first and last piece into account, as they can have a different size
271 return int(ceil(sec * self.sec_per_piece))
273 def download_range( self ):
274 """ Returns the range [first,last) of pieces we like to download. """
276 first = self.playback_pos
279 wraplen = first + self.wraparound_delta + 1 - self.last_piece
281 last = self.first_piece + wraplen
283 last = first + self.wraparound_delta + 1
285 last = self.last_piece + 1
289 def get_wraparound(self):
290 return self.wraparound
292 def increase_high_range(self, factor=1):
294 Increase the high priority range (effectively enlarging the buffer size)
297 self.high_prob_curr_time += factor * self.high_prob_curr_time_limit[2]
298 if self.high_prob_curr_time > self.high_prob_curr_time_limit[1]:
299 self.high_prob_curr_time = self.high_prob_curr_time_limit[1]
301 self.high_prob_curr_pieces += int(factor * self.high_prob_curr_pieces_limit[2])
302 if self.high_prob_curr_pieces > self.high_prob_curr_pieces_limit[1]:
303 self.high_prob_curr_pieces = self.high_prob_curr_pieces_limit[1]
305 if DEBUG: print >>sys.stderr, "VideoStatus:increase_high_range", self.high_prob_curr_time, "seconds or", self.high_prob_curr_pieces, "pieces"
307 def decrease_high_range(self, factor=1):
309 Decrease the high priority range (effectively reducing the buffer size)
312 self.high_prob_curr_time -= factor * self.high_prob_curr_time_limit[2]
313 if self.high_prob_curr_time < self.high_prob_curr_time_limit[0]:
314 self.high_prob_curr_time = self.high_prob_curr_time_limit[0]
316 self.high_prob_curr_pieces -= int(factor * self.high_prob_curr_pieces_limit[2])
317 if self.high_prob_curr_pieces < self.high_prob_curr_pieces_limit[0]:
318 self.high_prob_curr_pieces = self.high_prob_curr_pieces_limit[0]
320 if DEBUG: print >>sys.stderr, "VideoStatus:decrease_high_range", self.high_prob_curr_time, "seconds or", self.high_prob_curr_pieces, "pieces"
322 def set_high_range(self, seconds=None, pieces=None):
324 Set the minimum size of the high priority range. Can be given
325 in seconds of pieces.
327 if seconds: self.high_prob_curr_time = seconds
328 if pieces: self.high_prob_curr_pieces = pieces
330 def get_high_range(self):
332 Returns (first, last) tuple
334 first, _ = self.download_range()
335 number_of_pieces = self.time_to_pieces(self.high_prob_curr_time)
336 last = min(self.last_piece, # last piece
337 1 + first + max(number_of_pieces, self.high_prob_curr_pieces), # based on time OR pieces
338 1 + first + self.high_prob_curr_pieces_limit[1]) # hard-coded buffer maximum
341 def in_high_range(self, piece):
343 Returns True when PIECE is in the high priority range.
345 first, last = self.get_high_range()
346 return self.in_range(first, last, piece)
348 def get_range_length(self, first, last):
349 if self.wraparound and first > last:
350 return self.last_piece - first + \
351 last - self.first_piece
355 def get_high_range_length(self):
356 first, last = self.get_high_range()
357 return self.get_range_length(first, last)
359 def generate_high_range(self):
361 Returns the high current high priority range in piece_ids
363 first, last = self.get_high_range()
364 return self.generate_range((first, last))
366 def range2set(range,maxrange):
367 if range[0] <= range[1]:
368 set = Set(xrange(range[0],range[1]+1))
370 set = Set(xrange(range[0],maxrange)) | Set(xrange(0,range[1]+1))