instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / Core / Video / VideoStatus.py
1 # Written by Jan David Mol, Arno Bakker
2 # see LICENSE.txt for license information
3
4 import sys
5 import time
6 from math import ceil
7 from sets import Set
8
9 from BaseLib.Core.simpledefs import *
10
11 # live streaming means wrapping around
12 LIVE_WRAPAROUND = True
13
14 DEBUG = False
15
16 class VideoStatus:
17     """ Info about the selected video and status of the playback. """
18
19     # TODO: thread safety? PiecePicker, MovieSelector and MovieOnDemandTransporter all interface this
20
21     def __init__(self,piecelen,fileinfo,videoinfo,authparams):
22         """
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
27         """
28         self.piecelen = piecelen # including signature, if any
29         self.sigsize = 0
30         self.fileinfo = fileinfo
31         self.videoinfo = videoinfo
32         self.authparams = authparams
33
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)
39
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 
43         # (min,max,step).
44         # Arno, 2010-03-10: max 50 pieces too little for 32K piece-sized
45         # VOD streams.
46         #
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
49
50         # ----- locate selected movie in fileinfo
51         index = self.videoinfo['index']
52         if index == -1:
53             index = 0
54
55         movie_offset = sum( (filesize for (_,filesize) in fileinfo[:index] if filesize) )
56         movie_name = fileinfo[index][0]
57         movie_size = fileinfo[index][1]
58
59         self.selected_movie = {
60           "offset": movie_offset,
61           "name": movie_name,
62           "size": movie_size,
63         }
64
65         # ----- derive generic movie parameters
66         movie_begin = movie_offset
67         movie_end = movie_offset + movie_size - 1
68
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
77
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) 
85
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
90         else:
91             self.dropping = False # just wait and produce flawless playback
92
93         if videoinfo['bitrate']:
94             self.set_bitrate( videoinfo['bitrate'] )
95         else:
96             self.set_bitrate( 512*1024/8 ) # default to 512 Kbit/s
97             self.bitrate_set = False
98
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
105
106         self.pausable = (VODEVENT_PAUSE in videoinfo["userevents"]) and (VODEVENT_RESUME in videoinfo["userevents"])
107
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 )
112
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
118         else:
119             return self.piecelen
120
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
126
127     def set_live_startpos( self, pos ):
128         if self.wraparound:
129             if self.live_startpos is None:
130                 oldrange = self.first_piece,self.last_piece
131             else:
132                 oldrange = self.live_get_valid_range()
133             if DEBUG:
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:
138             o( None, pos )
139
140         if self.wraparound:
141             newrange = self.live_get_valid_range()
142             if DEBUG:
143                 print >>sys.stderr,"vodstatus: set_live_pos: new",newrange
144             return self.get_range_diff(oldrange,newrange)
145         else:
146             return (Set(),[])
147
148
149     def get_live_startpos(self):
150         return self.live_startpos
151
152     # the following functions work with absolute piece numbers,
153     # so they all function within the range [first_piece,last_piece]
154
155     # the range of pieces to download is
156     # [playback_pos,numpieces) for normal downloads and
157     # [playback_pos,playback_pos+delta) for wraparound
158
159     def generate_range( self, (f, t) ):
160         if self.wraparound and f > t:
161             for x in xrange( f, self.last_piece+1 ):
162                 yield x
163             for x in xrange( self.first_piece, t ):
164                 yield x
165         else:
166             for x in xrange( f, t ):
167                 yield x
168
169     def dist_range(self, f, t):
170         """ Returns the distance between f and t """
171         if f > t:
172             return self.last_piece-f + t-self.first_piece 
173         else:
174             return t - f
175
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
179         else:
180             return f <= x < t
181
182     def inc_playback_pos( self ):
183         oldpos = self.playback_pos
184         self.playback_pos += 1
185
186         if self.playback_pos > self.last_piece:
187             if self.wraparound:
188                 self.playback_pos = self.first_piece
189             else:
190                 # Arno, 2010-01-08: Adjusted EOF condition to work well with seeking/HTTP range queries
191                 self.playback_pos = self.last_piece+1
192
193         for o in self.playback_pos_observers:
194             o( oldpos, self.playback_pos )
195
196     def in_download_range( self, x ):
197         if self.wraparound:
198             wraplen = self.playback_pos + self.wraparound_delta - self.last_piece
199             if wraplen > 0:
200                 return self.first_piece <= x < self.first_piece + wraplen or self.playback_pos <= x <= self.last_piece
201
202             return self.playback_pos <= x < self.playback_pos + self.wraparound_delta
203         else:
204             return self.first_piece <= x <= self.last_piece
205
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
210                 return True
211             else:
212                 (begin,end) = self.live_get_valid_range()
213                 ret = self.in_range(begin,end,piece)
214                 if ret == False:
215                     print >>sys.stderr,"vod: status: NOT in_valid_range:",begin,"<",piece,"<",end
216                 return ret
217         else:
218             return self.first_piece <= piece <= self.last_piece
219         
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)
223         return (begin,end)
224         
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)
228
229     def get_range_diff(self,oldrange,newrange):
230         """ Returns the diff between oldrange and newrange as a Set.
231         """
232         rlist = []
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
240                 rlist = [a,b]
241                 return (None,rlist)
242                 #return Set(range(a[0],a[1]) + range(b[0],b[1]))
243             else:
244                 # 500-100, diff is 101-499
245                 a = (newrange[1]+1,newrange[0]-1)
246                 #print >>sys.stderr,"get_range_diff: range",a
247                 rlist = [a]
248                 return (None,rlist)
249                 #return Set(xrange(a[0],a[1]))
250              
251         oldset = range2set(oldrange,self.movie_numpieces)
252         newset = range2set(newrange,self.movie_numpieces)
253         return (oldset - newset,rlist)
254     
255     def normalize( self, x ):
256         """ Caps or wraps a piece number. """
257
258         if self.first_piece <= x <= self.last_piece:
259             return x
260
261         if self.wraparound:
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
264         else:
265             return max( self.first_piece, min( x, self.last_piece ) )
266
267     def time_to_pieces( self, sec ):
268         """ Returns the number of pieces that are needed to hold "sec" seconds of content. """
269
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))
272
273     def download_range( self ):
274         """ Returns the range [first,last) of pieces we like to download. """
275
276         first = self.playback_pos
277
278         if self.wraparound:
279             wraplen = first + self.wraparound_delta + 1 - self.last_piece
280             if wraplen > 0:
281                 last = self.first_piece + wraplen
282             else:
283                 last = first + self.wraparound_delta + 1
284         else:
285             last = self.last_piece + 1
286
287         return (first,last)
288
289     def get_wraparound(self):
290         return self.wraparound
291
292     def increase_high_range(self, factor=1):
293         """
294         Increase the high priority range (effectively enlarging the buffer size)
295         """
296         assert factor > 0
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]
300         
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]
304
305         if DEBUG: print >>sys.stderr, "VideoStatus:increase_high_range", self.high_prob_curr_time, "seconds or", self.high_prob_curr_pieces, "pieces"
306
307     def decrease_high_range(self, factor=1):
308         """
309         Decrease the high priority range (effectively reducing the buffer size)
310         """
311         assert factor > 0
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]
315         
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]
319
320         if DEBUG: print >>sys.stderr, "VideoStatus:decrease_high_range", self.high_prob_curr_time, "seconds or", self.high_prob_curr_pieces, "pieces"
321
322     def set_high_range(self, seconds=None, pieces=None):
323         """
324         Set the minimum size of the high priority range. Can be given
325         in seconds of pieces.
326         """
327         if seconds: self.high_prob_curr_time = seconds
328         if pieces: self.high_prob_curr_pieces = pieces
329
330     def get_high_range(self):
331         """
332         Returns (first, last) tuple
333         """
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
339         return first, last
340
341     def in_high_range(self, piece):
342         """
343         Returns True when PIECE is in the high priority range.
344         """
345         first, last = self.get_high_range()
346         return self.in_range(first, last, piece)
347
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
352         else:
353             return last - first
354
355     def get_high_range_length(self):
356         first, last = self.get_high_range()
357         return self.get_range_length(first, last)
358
359     def generate_high_range(self):
360         """
361         Returns the high current high priority range in piece_ids
362         """
363         first, last = self.get_high_range()
364         return self.generate_range((first, last))
365
366 def range2set(range,maxrange):    
367     if range[0] <= range[1]:
368         set = Set(xrange(range[0],range[1]+1))
369     else:
370         set = Set(xrange(range[0],maxrange)) | Set(xrange(0,range[1]+1))
371     return set