anonstream/anonstream/segments.py

146 行
4.2 KiB
Python
Raw 通常表示 履歴

2022-03-07 23:51:59 +09:00
# SPDX-FileCopyrightText: 2022 n9k [https://git.076.ne.jp/ninya9k]
# SPDX-License-Identifier: AGPL-3.0-or-later
import asyncio
import os
import time
import aiofiles
import m3u8
from quart import current_app
from anonstream.wrappers import ttl_cache, with_timestamp
CONFIG = current_app.config
class Offline(Exception):
pass
class Stale(Exception):
pass
class UnsafePath(Exception):
pass
def get_mtime():
try:
mtime = os.path.getmtime(CONFIG['SEGMENT_PLAYLIST'])
except FileNotFoundError as e:
raise Stale from e
else:
if time.time() - mtime >= CONFIG['SEGMENT_PLAYLIST_STALE_THRESHOLD']:
raise Stale
return mtime
@ttl_cache(CONFIG['SEGMENT_PLAYLIST_CACHE_LIFETIME'])
def get_playlist():
#print(f'[debug @ {time.time():.3f}] get_playlist()')
try:
mtime = get_mtime()
except Stale as e:
raise Offline from e
else:
playlist = m3u8._load_from_file(CONFIG['SEGMENT_PLAYLIST'])
if playlist.is_endlist:
raise Offline
if len(playlist.segments) == 0:
raise Offline
return playlist, mtime
def get_starting_segment():
'''
Instead of choosing the most recent segment, try choosing a segment that
preceeds the most recent one by a little bit. Doing this increases the
buffer of initially available video, which makes playback more stable.
'''
print(f'[debug @ {time.time():.3f}] get_starting_segment()')
playlist, _ = get_playlist()
index = max(0, len(playlist.segments) - CONFIG['SEGMENT_STREAM_INITIAL_BUFFER'])
return playlist.segments[index]
def get_next_segment(uri):
'''
Look for the segment with uri `uri` and return the segment that
follows it, or None if no such segment exists.
'''
#print(f'[debug @ {time.time():.3f}] get_next_segment({uri!r})')
playlist, _ = get_playlist()
found = False
for segment in playlist.segments:
if found:
break
elif segment.uri == uri:
found = True
else:
segment = None
return segment
async def get_segment_uris():
try:
segment = get_starting_segment()
except Offline:
return
else:
yield segment.init_section.uri
while True:
yield segment.uri
t0 = time.monotonic()
while True:
try:
next_segment = get_next_segment(segment.uri)
except Offline:
return
else:
if next_segment is not None:
segment = next_segment
break
elif time.monotonic() - t0 >= CONFIG['SEGMENT_SEARCH_TIMEOUT']:
return
else:
await asyncio.sleep(CONFIG['SEGMENT_SEARCH_COOLDOWN'])
def path_for(uri):
path = os.path.normpath(
os.path.join(CONFIG['SEGMENT_DIRECTORY'], uri)
)
if os.path.dirname(path) != CONFIG['SEGMENT_DIRECTORY']:
raise UnsafePath(path)
return path
async def segments(segment_read_hook=lambda uri: None, token=None):
print(f'[debug @ {time.time():.3f}: {token=}] entering segment generator')
uri = None
async for uri in get_segment_uris():
#print(f'[debug @ {time.time():.3f}: {token=}] {uri=}')
try:
path = path_for(uri)
except UnsafePath as e:
unsafe_path, *_ = e.args
print(
f'[debug @ {time.time():.3f}: {token=}] '
f'segment {uri=} has unsafe {path=}'
)
break
segment_read_hook(uri)
try:
async with aiofiles.open(path, 'rb') as fp:
while chunk := await fp.read(8192):
yield chunk
except FileNotFoundError:
print(
f'[debug @ {time.time():.3f}: {token=}] '
f'segment {uri=} at {path=} unexpectedly does not exist'
)
break
else:
print(
f'[debug @ {time.time():.3f}: {token=}] '
f'could not find segment following {uri=} after at least '
f'{CONFIG["SEGMENT_SEARCH_TIMEOUT"]} seconds'
)
print(f'[debug @ {time.time():.3f}: {token=}] exiting segment generator')