|
|
@ -1,4 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
import errno
|
|
|
|
import errno
|
|
|
|
import functools
|
|
|
|
import functools
|
|
|
|
import json
|
|
|
|
import json
|
|
|
@ -85,6 +86,17 @@ def list_segments(stream, variant, hour):
|
|
|
|
return json.dumps(listdir(path, error=False))
|
|
|
|
return json.dumps(listdir(path, error=False))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def time_range_for_variant(stream, variant):
|
|
|
|
|
|
|
|
"""Returns earliest and latest times that the given variant has segments for
|
|
|
|
|
|
|
|
(up to hour resolution), or 404 if it doesn't exist / is empty."""
|
|
|
|
|
|
|
|
hours = listdir(os.path.join(app.static_folder, stream, variant))
|
|
|
|
|
|
|
|
if not hours:
|
|
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
first, last = min(hours), max(hours)
|
|
|
|
|
|
|
|
# note last hour parses to _start_ of that hour, so we add 1h to go to end of that hour
|
|
|
|
|
|
|
|
return dateutil.parser.parse(first), dateutil.parser.parse(last) + datetime.timedelta(hours=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/playlist/<stream>.m3u8')
|
|
|
|
@app.route('/playlist/<stream>.m3u8')
|
|
|
|
@has_path_args
|
|
|
|
@has_path_args
|
|
|
|
def generate_master_playlist(stream):
|
|
|
|
def generate_master_playlist(stream):
|
|
|
@ -93,24 +105,61 @@ def generate_master_playlist(stream):
|
|
|
|
start, end: The time to begin and end the stream at.
|
|
|
|
start, end: The time to begin and end the stream at.
|
|
|
|
See generate_media_playlist for details.
|
|
|
|
See generate_media_playlist for details.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
start = dateutil.parser.parse(request.args['start']) if 'start' in request.args else None
|
|
|
|
|
|
|
|
end = dateutil.parser.parse(request.args['end']) if 'end' in request.args else None
|
|
|
|
variants = listdir(os.path.join(app.static_folder, stream))
|
|
|
|
variants = listdir(os.path.join(app.static_folder, stream))
|
|
|
|
playlists = {
|
|
|
|
|
|
|
|
variant: url_for('generate_media_playlist', stream=stream, variant=variant, **request.args)
|
|
|
|
playlists = {}
|
|
|
|
for variant in variants
|
|
|
|
for variant in variants:
|
|
|
|
}
|
|
|
|
# If start or end are given, try to restrict offered variants to ones which exist for that
|
|
|
|
|
|
|
|
# time range.
|
|
|
|
|
|
|
|
if start is not None or end is not None:
|
|
|
|
|
|
|
|
first, last = time_range_for_variant(stream, variant)
|
|
|
|
|
|
|
|
if start is not None and last < start:
|
|
|
|
|
|
|
|
continue # last time for variant is before our start time, don't offer variant
|
|
|
|
|
|
|
|
if end is not None and end < first:
|
|
|
|
|
|
|
|
continue # our end time is before first time for variant, don't offer variant
|
|
|
|
|
|
|
|
playlists[variant] = url_for(
|
|
|
|
|
|
|
|
'generate_media_playlist', stream=stream, variant=variant, **request.args
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return generate_hls.generate_master(playlists)
|
|
|
|
return generate_hls.generate_master(playlists)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/playlist/<stream>/<variant>.m3u8')
|
|
|
|
@app.route('/playlist/<stream>/<variant>.m3u8')
|
|
|
|
@has_path_args
|
|
|
|
@has_path_args
|
|
|
|
def generate_media_playlist(stream, variant):
|
|
|
|
def generate_media_playlist(stream, variant):
|
|
|
|
#TODO handle no start/end
|
|
|
|
"""Returns a HLS media playlist for the given stream and variant.
|
|
|
|
#TODO error handling of args
|
|
|
|
Takes optional params:
|
|
|
|
# TODO lots of other stuff
|
|
|
|
start, end: The time to begin and end the stream at.
|
|
|
|
start = dateutil.parser.parse(request.args['start'])
|
|
|
|
Must be in ISO 8601 format (ie. yyyy-mm-ddTHH:MM:SS).
|
|
|
|
end = dateutil.parser.parse(request.args['end'])
|
|
|
|
If not given, effectively means "infinity", ie. no start means
|
|
|
|
|
|
|
|
any time ago, no end means any time in the future.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
hours_path = os.path.join(app.static_folder, stream, variant)
|
|
|
|
hours_path = os.path.join(app.static_folder, stream, variant)
|
|
|
|
|
|
|
|
if not os.path.isdir(hours_path):
|
|
|
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start = dateutil.parser.parse(request.args['start']) if 'start' in request.args else None
|
|
|
|
|
|
|
|
end = dateutil.parser.parse(request.args['end']) if 'end' in request.args else None
|
|
|
|
|
|
|
|
if start is None or end is None:
|
|
|
|
|
|
|
|
# If start or end are not given, use the earliest/latest time available
|
|
|
|
|
|
|
|
first, last = time_range_for_variant(stream, variant)
|
|
|
|
|
|
|
|
if start is None:
|
|
|
|
|
|
|
|
start = first
|
|
|
|
|
|
|
|
if end is None:
|
|
|
|
|
|
|
|
end = last
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# get_best_segments requires start be before end, special case that as no segments
|
|
|
|
|
|
|
|
# (not an error because someone might ask for a specific start, no end, but we ended up with
|
|
|
|
|
|
|
|
# end before start because that's the latest time we have)
|
|
|
|
|
|
|
|
if start < end:
|
|
|
|
segments = get_best_segments(hours_path, start, end)
|
|
|
|
segments = get_best_segments(hours_path, start, end)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Note the None to indicate there was a "hole" at both start and end
|
|
|
|
|
|
|
|
segments = [None]
|
|
|
|
|
|
|
|
|
|
|
|
return generate_hls.generate_media(segments, os.path.join(app.static_url_path, stream, variant))
|
|
|
|
return generate_hls.generate_media(segments, os.path.join(app.static_url_path, stream, variant))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|