2021-04-10 23:39:58 +09:00
import os
import time
2021-07-05 20:45:32 +09:00
from website . constants import CONFIG , SEGMENTS_DIR , SEGMENT_INIT , VIEW_COUNTING_PERIOD
2021-07-08 21:45:02 +09:00
from website . utils . stream import _is_segment , _segment_number , get_segments , is_online
2021-04-10 23:39:58 +09:00
2021-04-16 14:53:25 +09:00
SEGMENT = ' stream {number} .m4s '
2021-04-12 13:32:32 +09:00
CORRUPTING_SEGMENT = ' corrupt.m4s '
2021-07-17 07:02:19 +09:00
STREAM_TIMEOUT = lambda : CONFIG [ ' stream ' ] [ ' hls_time ' ] * 2 + 2 # consider the stream offline after this many seconds without a new segment
2021-04-10 23:39:58 +09:00
2021-05-17 16:58:42 +09:00
def resolve_segment_offset ( segment_offset = 1 ) :
2021-04-16 14:53:25 +09:00
'''
Returns the number of the segment at ` segment_offset ` ( 1 is most recent segment )
'''
2021-07-08 21:45:02 +09:00
segments = get_segments ( )
2021-04-16 14:53:25 +09:00
try :
segment = segments [ - min ( segment_offset , len ( segments ) ) ]
except IndexError :
2021-04-16 19:09:42 +09:00
return 0
2021-04-16 14:53:25 +09:00
return _segment_number ( segment )
def get_next_segment ( after , start_segment ) :
2021-07-08 21:45:02 +09:00
if not is_online ( ) :
raise SegmentUnavailable ( f ' stream went offline ' )
2021-04-10 23:39:58 +09:00
start = time . time ( )
while True :
2021-07-17 07:02:19 +09:00
time . sleep ( 0.5 )
2021-07-08 21:45:02 +09:00
segments = get_segments ( )
2021-04-10 23:39:58 +09:00
if after == None :
2021-04-16 18:58:37 +09:00
try :
if os . path . getsize ( os . path . join ( SEGMENTS_DIR , SEGMENT_INIT ) ) > 0 : # FFmpeg creates an empty init.mp4 and only writes to it when the first segment exists
return SEGMENT_INIT
except FileNotFoundError :
pass
2021-04-10 23:39:58 +09:00
elif after == SEGMENT_INIT :
2021-04-16 18:58:37 +09:00
if os . path . isfile ( os . path . join ( SEGMENTS_DIR , start_segment ) ) :
return start_segment
2021-04-10 23:39:58 +09:00
else :
segments = filter ( lambda segment : _segment_number ( segment ) > _segment_number ( after ) , segments )
2021-04-16 18:58:37 +09:00
try :
return min ( segments , key = _segment_number )
except ValueError :
pass
2021-07-05 20:45:32 +09:00
if time . time ( ) - start > = STREAM_TIMEOUT ( ) :
2021-04-16 18:58:37 +09:00
if after == None :
2021-07-08 21:45:02 +09:00
raise SegmentUnavailable ( f ' timeout waiting for initial segment { SEGMENT_INIT } ' )
2021-04-16 18:58:37 +09:00
elif after == SEGMENT_INIT :
raise SegmentUnavailable ( f ' timeout waiting for start segment { start_segment } ' )
else :
raise SegmentUnavailable ( f ' timeout searching after { after } ' )
2021-04-10 23:39:58 +09:00
2021-04-11 14:49:42 +09:00
class SegmentUnavailable ( Exception ) :
2021-04-10 23:39:58 +09:00
pass
class SegmentsIterator :
2021-04-16 14:53:25 +09:00
def __init__ ( self , start_segment , skip_init_segment = False ) :
self . start_segment = start_segment
2021-04-11 14:49:42 +09:00
self . segment = SEGMENT_INIT if skip_init_segment else None
2021-04-10 23:39:58 +09:00
def __iter__ ( self ) :
return self
def __next__ ( self ) :
2021-04-16 14:53:25 +09:00
self . segment = get_next_segment ( self . segment , self . start_segment )
2021-04-10 23:39:58 +09:00
return self . segment
2021-04-12 13:32:32 +09:00
2021-04-10 23:39:58 +09:00
class ConcatenatedSegments :
2021-04-16 14:53:25 +09:00
def __init__ ( self , start_number , segment_hook = None , corrupt_hook = None , should_close_connection = None ) :
# start at this segment, after SEGMENT_INIT
self . start_number = start_number
2021-07-17 07:02:19 +09:00
# run this function before sending each segment (if we do it after then if someone gets the most of a segment but then stops, that wouldn't be counted, before = 0 viewers means nobody is retrieving the stream, after = slightly more accurate viewer count but 0 viewers doesn't necessarily mean nobody is retrieving the stream)
2021-04-12 18:01:49 +09:00
self . segment_hook = segment_hook or ( lambda n : None )
2021-04-14 01:56:25 +09:00
# run this function when we send the corrupting segment
self . corrupt_hook = corrupt_hook or ( lambda : None )
2021-04-12 21:00:02 +09:00
# run this function before reading files; if it returns True, then stop
2021-04-14 01:56:25 +09:00
self . should_close_connection = should_close_connection or ( lambda : None )
2021-04-11 20:37:47 +09:00
2021-04-16 14:53:25 +09:00
start_segment = SEGMENT . format ( number = start_number )
self . segments = SegmentsIterator ( start_segment = start_segment )
2021-04-12 18:01:49 +09:00
2021-04-12 13:32:32 +09:00
self . _closed = False
2021-04-12 18:01:49 +09:00
self . segment_read_offset = 0
2021-04-16 14:53:25 +09:00
self . segment = next ( self . segments )
2021-07-17 07:02:19 +09:00
self . segment_hook ( _segment_number ( self . segment ) )
2021-04-16 14:53:25 +09:00
2021-04-10 23:39:58 +09:00
def _read ( self , n ) :
chunk = b ' '
2021-04-11 14:49:42 +09:00
while True :
2021-04-12 21:00:02 +09:00
if self . should_close_connection ( ) :
2021-04-16 18:58:37 +09:00
raise SegmentUnavailable ( f ' told to close while reading { self . segment } ' )
2021-04-12 21:00:02 +09:00
2021-04-16 18:58:37 +09:00
try :
with open ( os . path . join ( SEGMENTS_DIR , self . segment ) , ' rb ' ) as fp :
fp . seek ( self . segment_read_offset )
chunk_chunk = fp . read ( n - len ( chunk ) )
except FileNotFoundError :
raise SegmentUnavailable ( f ' deleted while reading { self . segment } ' )
2021-04-12 18:01:49 +09:00
self . segment_read_offset + = len ( chunk_chunk )
2021-04-12 13:32:32 +09:00
chunk + = chunk_chunk
2021-04-10 23:39:58 +09:00
if len ( chunk ) > = n :
break
2021-04-12 18:01:49 +09:00
self . segment_read_offset = 0
2021-07-17 07:02:19 +09:00
next_segment = next ( self . segments )
self . segment_hook ( _segment_number ( next_segment ) )
self . segment = next_segment
2021-04-10 23:39:58 +09:00
return chunk
def read ( self , n ) :
2021-04-11 02:50:50 +09:00
if self . _closed :
return b ' '
2021-04-10 23:39:58 +09:00
try :
return self . _read ( n )
2021-04-16 18:58:37 +09:00
except SegmentUnavailable as e :
2021-04-12 18:01:49 +09:00
# If a fragment gets interrupted and we start appending whole new
# fragments after it, the video will get corrupted.
# This is very likely to happen if you become extremely delayed.
# It's also likely to happen if the reason for the
# discontinuity is the livestream restarting.
2021-07-05 13:37:53 +09:00
# If you cache segments this becomes very unlikely to happen in
2021-04-12 18:01:49 +09:00
# either case. However, appending fragments from the restarted
# stream corrupts the video; and skipping ahead lots of fragments
# will make the video pause for the number of fragments that were
# skipped. TODO: figure this out.
# Until this is figured out, it's probably best to just corrupt the
# video stream so it's clear to the viewer that they have to refresh.
2021-04-16 18:58:37 +09:00
print ( ' SegmentUnavailable in ConcatenatedSegments.read: ' , * e . args )
2021-04-12 18:01:49 +09:00
return self . _corrupt ( n )
2021-04-12 13:32:32 +09:00
def _corrupt ( self , n ) :
2021-04-13 22:10:05 +09:00
# TODO: make this corrupt more reliably (maybe it has to follow a full segment?)
2021-05-16 09:24:50 +09:00
# Doesn't corrupt when directly after init.mp4
2021-04-14 01:56:25 +09:00
print ( ' ConcatenatedSegments._corrupt ' )
self . corrupt_hook ( )
2021-04-12 13:32:32 +09:00
self . close ( )
2021-04-12 18:01:49 +09:00
try :
Update website/static/external/grids-responsive-min.css, website/static/external/pure-min.css, website/static/external/video-js.css, website/static/external/video.js, website/static/external/videojs-contrib-hls.js, website/static/platform.css, website/static/platform.js, website/templates/chat-iframe.html, website/templates/comment-iframe.html, website/templates/index.html, website/templates/stream-info-iframe.html, website/utils/captcha.py, website/utils/colour.py, website/utils/stream.py, website/utils/tripcode.py, website/chat.py, website/concatenate.py, website/routes.py, website/viewership.py files
2021-04-14 23:12:40 +09:00
return open ( os . path . join ( SEGMENTS_DIR , CORRUPTING_SEGMENT ) , ' rb ' ) . read ( n )
2021-04-12 18:01:49 +09:00
except FileNotFoundError :
2021-04-12 18:04:41 +09:00
# TODO: try to read the corrupting segment earlier
return b ' '
2021-04-10 23:39:58 +09:00
def close ( self ) :
2021-05-16 09:24:50 +09:00
self . _closed = True