refactored to channel and quality

pull/67/head
Christopher Usher 5 years ago
parent 720684a388
commit b959853593

@ -28,7 +28,7 @@ def unpadded_b64_decode(s):
class SegmentInfo( class SegmentInfo(
namedtuple('SegmentInfoBase', [ namedtuple('SegmentInfoBase', [
'path', 'stream', 'variant', 'start', 'duration', 'type', 'hash' 'path', 'channel', 'quality', 'start', 'duration', 'type', 'hash'
]) ])
): ):
"""Info parsed from a segment path, including original path. """Info parsed from a segment path, including original path.
@ -48,7 +48,7 @@ def parse_segment_path(path):
# left-pad parts with None up to 4 parts # left-pad parts with None up to 4 parts
parts = [None] * (4 - len(parts)) + parts parts = [None] * (4 - len(parts)) + parts
# pull info out of path parts # pull info out of path parts
stream, variant, hour, filename = parts[-4:] channel, quality, hour, filename = parts[-4:]
# split filename, which should be TIME-DURATION-TYPE-HASH.ts # split filename, which should be TIME-DURATION-TYPE-HASH.ts
try: try:
if not filename.endswith('.ts'): if not filename.endswith('.ts'):
@ -63,8 +63,8 @@ def parse_segment_path(path):
hash = None if type == 'temp' else unpadded_b64_decode(hash) hash = None if type == 'temp' else unpadded_b64_decode(hash)
return SegmentInfo( return SegmentInfo(
path = path, path = path,
stream = stream, channel = channel,
variant = variant, quality = quality,
start = datetime.datetime.strptime("{}:{}".format(hour, time), "%Y-%m-%dT%H:%M:%S.%f"), start = datetime.datetime.strptime("{}:{}".format(hour, time), "%Y-%m-%dT%H:%M:%S.%f"),
duration = datetime.timedelta(seconds=float(duration)), duration = datetime.timedelta(seconds=float(duration)),
type = type, type = type,

@ -29,8 +29,8 @@ The restreamer is a simple http api for listing available segments and generatin
HLS playlists for them. HLS playlists for them.
The segments themselves are ideally to be served by some external webserver The segments themselves are ideally to be served by some external webserver
under "/segments/<stream>/<variant>/<hour>/<filename>" (ie. with BASE_DIR under "/segments"), under "/segments/<channel>/<quality>/<hour>/<filename>" (ie. with BASE_DIR
though this server will also serve them if requested. under "/segments"), though this server will also serve them if requested.
""" """
@ -89,62 +89,62 @@ def metrics():
@app.route('/files') @app.route('/files')
@stats @stats
def list_streams(): def list_channels():
"""Returns a JSON list of streams for which there may be segments available. """Returns a JSON list of channels for which there may be segments available.
Returns empty list if no streams are available. Returns empty list if no channels are available.
""" """
path = app.static_folder path = app.static_folder
return json.dumps(listdir(path, error=False)) return json.dumps(listdir(path, error=False))
@app.route('/files/<stream>') @app.route('/files/<channel>')
@stats @stats
@has_path_args @has_path_args
def list_variants(stream): def list_qualities(channel):
"""Returns a JSON list of variants for the given stream for which there may """Returns a JSON list of qualities for the given channel for which there
be segments available. Returns empty list on non-existent streams, etc. may be segments available. Returns empty list on non-existent channels, etc."""
"""
path = os.path.join( path = os.path.join(
app.static_folder, app.static_folder,
stream, channel,
) )
return json.dumps(listdir(path, error=False)) return json.dumps(listdir(path, error=False))
@app.route('/files/<stream>/<variant>') @app.route('/files/<channel>/<quality>')
@stats @stats
@has_path_args @has_path_args
def list_hours(stream, variant): def list_hours(channel, quality):
"""Returns a JSON list of hours for the given stream and variant for which """Returns a JSON list of hours for the given channel and quality for which
there may be segments available. Returns empty list on non-existent streams, etc. there may be segments available. Returns empty list on non-existent
channels, etc.
""" """
path = os.path.join( path = os.path.join(
app.static_folder, app.static_folder,
stream, channel,
variant, quality,
) )
return json.dumps(listdir(path, error=False)) return json.dumps(listdir(path, error=False))
@app.route('/files/<stream>/<variant>/<hour>') @app.route('/files/<channel>/<quality>/<hour>')
@stats @stats
@has_path_args @has_path_args
def list_segments(stream, variant, hour): def list_segments(channel, quality, hour):
"""Returns a JSON list of segment files for a given stream, variant and hour. """Returns a JSON list of segment files for a given channel, quality and
Returns empty list on non-existant streams, etc. hour. Returns empty list on non-existant channels, etc.
""" """
path = os.path.join( path = os.path.join(
app.static_folder, app.static_folder,
stream, channel,
variant, quality,
hour, hour,
) )
return json.dumps(listdir(path, error=False)) return json.dumps(listdir(path, error=False))
def time_range_for_variant(stream, variant): def time_range_for_quality(channel, quality):
"""Returns earliest and latest times that the given variant has segments for """Returns earliest and latest times that the given quality has segments for
(up to hour resolution), or 404 if it doesn't exist / is empty.""" (up to hour resolution), or 404 if it doesn't exist / is empty."""
hours = listdir(os.path.join(app.static_folder, stream, variant)) hours = listdir(os.path.join(app.static_folder, channel, quality))
if not hours: if not hours:
abort(404) abort(404)
first, last = min(hours), max(hours) first, last = min(hours), max(hours)
@ -154,43 +154,43 @@ def time_range_for_variant(stream, variant):
return parse_hour(first), parse_hour(last) + datetime.timedelta(hours=1) return parse_hour(first), parse_hour(last) + datetime.timedelta(hours=1)
@app.route('/playlist/<stream>.m3u8') @app.route('/playlist/<channel>.m3u8')
@stats @stats
@has_path_args @has_path_args
def generate_master_playlist(stream): def generate_master_playlist(channel):
"""Returns a HLS master playlist for the given stream. """Returns a HLS master playlist for the given channel.
Takes optional params: Takes optional params:
start, end: The time to begin and end the stream at. start, end: The time to begin and end the channel at.
See generate_media_playlist for details. See generate_media_playlist for details.
""" """
start = common.dateutil.parse_utc_only(request.args['start']) if 'start' in request.args else None start = common.dateutil.parse_utc_only(request.args['start']) if 'start' in request.args else None
end = common.dateutil.parse_utc_only(request.args['end']) if 'end' in request.args else None end = common.dateutil.parse_utc_only(request.args['end']) if 'end' in request.args else None
variants = listdir(os.path.join(app.static_folder, stream)) qualities = listdir(os.path.join(app.static_folder, channel))
playlists = {} playlists = {}
for variant in variants: for quality in qualities:
# If start or end are given, try to restrict offered variants to ones which exist for that # If start or end are given, try to restrict offered qualities to ones which exist for that
# time range. # time range.
if start is not None or end is not None: if start is not None or end is not None:
first, last = time_range_for_variant(stream, variant) first, last = time_range_for_quality(channel, quality)
if start is not None and last < start: if start is not None and last < start:
continue # last time for variant is before our start time, don't offer variant continue # last time for quality is before our start time, don't offer quality
if end is not None and end < first: if end is not None and end < first:
continue # our end time is before first time for variant, don't offer variant continue # our end time is before first time for quality, don't offer quality
playlists[variant] = url_for( playlists[quality] = url_for(
'generate_media_playlist', stream=stream, variant=variant, **request.args 'generate_media_playlist', channel=channel, quality=quality, **request.args
) )
return generate_hls.generate_master(playlists) return generate_hls.generate_master(playlists)
@app.route('/playlist/<stream>/<variant>.m3u8') @app.route('/playlist/<channel>/<quality>.m3u8')
@stats @stats
@has_path_args @has_path_args
def generate_media_playlist(stream, variant): def generate_media_playlist(channel, quality):
"""Returns a HLS media playlist for the given stream and variant. """Returns a HLS media playlist for the given channel and quality.
Takes optional params: Takes optional params:
start, end: The time to begin and end the stream at. start, end: The time to begin and end the channel at.
Must be in ISO 8601 format (ie. yyyy-mm-ddTHH:MM:SS) and UTC. Must be in ISO 8601 format (ie. yyyy-mm-ddTHH:MM:SS) and UTC.
If not given, effectively means "infinity", ie. no start means If not given, effectively means "infinity", ie. no start means
any time ago, no end means any time in the future. any time ago, no end means any time in the future.
@ -198,7 +198,7 @@ def generate_media_playlist(stream, variant):
may start slightly before and end slightly after the given times. may start slightly before and end slightly after the given times.
""" """
hours_path = os.path.join(app.static_folder, stream, variant) hours_path = os.path.join(app.static_folder, channel, quality)
if not os.path.isdir(hours_path): if not os.path.isdir(hours_path):
abort(404) abort(404)
@ -206,7 +206,7 @@ def generate_media_playlist(stream, variant):
end = common.dateutil.parse_utc_only(request.args['end']) if 'end' in request.args else None end = common.dateutil.parse_utc_only(request.args['end']) if 'end' in request.args else None
if start is None or end is None: if start is None or end is None:
# If start or end are not given, use the earliest/latest time available # If start or end are not given, use the earliest/latest time available
first, last = time_range_for_variant(stream, variant) first, last = time_range_for_quality(channel, quality)
if start is None: if start is None:
start = first start = first
if end is None: if end is None:
@ -221,13 +221,13 @@ def generate_media_playlist(stream, variant):
# Note the None to indicate there was a "hole" at both start and end # Note the None to indicate there was a "hole" at both start and end
segments = [None] 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, channel, quality))
@app.route('/cut/<stream>/<variant>.ts') @app.route('/cut/<channel>/<quality>.ts')
@stats @stats
@has_path_args @has_path_args
def cut(stream, variant): def cut(channel, quality):
"""Return a MPEGTS video file covering the exact timestamp range. """Return a MPEGTS video file covering the exact timestamp range.
Params: Params:
start, end: Required. The start and end times, down to the millisecond. start, end: Required. The start and end times, down to the millisecond.
@ -247,7 +247,7 @@ def cut(stream, variant):
return "allow_holes must be one of: true, false", 400 return "allow_holes must be one of: true, false", 400
allow_holes = (allow_holes == "true") allow_holes = (allow_holes == "true")
hours_path = os.path.join(app.static_folder, stream, variant) hours_path = os.path.join(app.static_folder, channel, quality)
if not os.path.isdir(hours_path): if not os.path.isdir(hours_path):
abort(404) abort(404)

Loading…
Cancel
Save