Skip to content

Commit

Permalink
Add support for ext x image tag
Browse files Browse the repository at this point in the history
  • Loading branch information
darkbaboon authored Jul 25, 2023
1 parent a4d6f22 commit 98a4bdb
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 21 deletions.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ Supported tags
* `#EXT-X-BITRATE`_
* `#EXT-X-BYTERANGE`_
* `#EXT-X-I-FRAME-STREAM-INF`_
* `#EXT-X-IMAGES-ONLY`_
* `#EXT-X-IMAGE-STREAM-INF`_
* `#EXT-X-TILES`_
* `#EXT-X-DISCONTINUITY`_
* #EXT-X-CUE-OUT
* #EXT-X-CUE-OUT-CONT
Expand Down Expand Up @@ -376,6 +379,9 @@ the same thing.
.. _#EXT-X-MEDIA: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
.. _#EXT-X-STREAM-INF: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
.. _#EXT-X-I-FRAME-STREAM-INF: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
.. _#EXT-X-IMAGES-ONLY: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf
.. _#EXT-X-IMAGE-STREAM-INF: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf
.. _#EXT-X-TILES: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf
.. _#EXT-X-SESSION-DATA: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
.. _#EXT-X-SESSION-KEY: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
.. _#EXT-X-INDEPENDENT-SEGMENTS: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
Expand Down
7 changes: 6 additions & 1 deletion m3u8/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# coding: utf-8
# Copyright 2014 Globo.com Player authors. All rights reserved.
# Copyright 2023 Ronan RABOUIN
# Use of this source code is governed by a MIT License
# license that can be found in the LICENSE file.

Expand Down Expand Up @@ -31,6 +32,8 @@
DateRange,
DateRangeList,
ContentSteering,
ImagePlaylist,
Tiles
)
from m3u8.parser import parse, ParseError

Expand All @@ -57,10 +60,12 @@
"DateRange",
"DateRangeList",
"ContentSteering",
"ImagePlaylist",
"Tiles",
"loads",
"load",
"parse",
"ParseError",
"ParseError"
)


Expand Down
213 changes: 211 additions & 2 deletions m3u8/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# coding: utf-8
# Copyright 2014 Globo.com Player authors. All rights reserved.
# Copyright 2023 Ronan RABOUIN
# Use of this source code is governed by a MIT License
# license that can be found in the LICENSE file.
import decimal
Expand Down Expand Up @@ -131,6 +132,13 @@ class M3U8(object):
Returns true if EXT-X-INDEPENDENT-SEGMENTS tag present in M3U8.
https://tools.ietf.org/html/draft-pantos-http-live-streaming-13#section-3.4.16
`image_playlists`
If this is a variant playlist (`is_variant` is True), returns a list of
ImagePlaylist objects
`is_images_only`
Returns true if EXT-X-IMAGES-ONLY tag present in M3U8.
https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf
"""

simple_attributes = (
Expand All @@ -146,6 +154,7 @@ class M3U8(object):
("allow_cache", "allow_cache"),
("playlist_type", "playlist_type"),
("discontinuity_sequence", "discontinuity_sequence"),
("is_images_only", "is_images_only")
)

def __init__(
Expand Down Expand Up @@ -225,6 +234,16 @@ def _initialize_attributes(self):
)
)

self.image_playlists = PlaylistList()
for img_pl in self.data.get('image_playlists', []):
self.image_playlists.append(
ImagePlaylist(
base_uri=self.base_uri,
uri=img_pl["uri"],
image_stream_info=img_pl["image_stream_info"]
)
)

start = self.data.get("start", None)
self.start = start and Start(**start)

Expand Down Expand Up @@ -282,6 +301,7 @@ def base_uri(self, new_base_uri):
self.iframe_playlists.base_uri = new_base_uri
self.segments.base_uri = new_base_uri
self.rendition_reports.base_uri = new_base_uri
self.image_playlists.base_uri = new_base_uri
for key in self.keys:
if key:
key.base_uri = new_base_uri
Expand Down Expand Up @@ -315,6 +335,7 @@ def _update_base_path(self):
self.segments.base_path = self._base_path
self.playlists.base_path = self._base_path
self.iframe_playlists.base_path = self._base_path
self.image_playlists.base_path = self._base_path
self.rendition_reports.base_path = self._base_path
if self.preload_hint:
self.preload_hint.base_path = self._base_path
Expand All @@ -330,6 +351,11 @@ def add_iframe_playlist(self, iframe_playlist):
self.is_variant = True
self.iframe_playlists.append(iframe_playlist)

def add_image_playlist(self, image_playlist):
if image_playlist is not None:
self.is_variant = True
self.image_playlists.append(image_playlist)

def add_media(self, media):
self.media.append(media)

Expand All @@ -347,8 +373,6 @@ def dumps(self):
output = ["#EXTM3U"]
if self.content_steering:
output.append(str(self.content_steering))
if self.is_independent_segments:
output.append("#EXT-X-INDEPENDENT-SEGMENTS")
if self.media_sequence:
output.append("#EXT-X-MEDIA-SEQUENCE:" + str(self.media_sequence))
if self.discontinuity_sequence:
Expand All @@ -359,6 +383,8 @@ def dumps(self):
output.append("#EXT-X-ALLOW-CACHE:" + self.allow_cache.upper())
if self.version:
output.append("#EXT-X-VERSION:" + str(self.version))
if self.is_independent_segments:
output.append("#EXT-X-INDEPENDENT-SEGMENTS")
if self.target_duration:
output.append(
"#EXT-X-TARGETDURATION:" + number_to_string(self.target_duration)
Expand All @@ -369,6 +395,8 @@ def dumps(self):
output.append(str(self.start))
if self.is_i_frames_only:
output.append("#EXT-X-I-FRAMES-ONLY")
if self.is_images_only:
output.append("#EXT-X-IMAGES-ONLY")
if self.server_control:
output.append(str(self.server_control))
if self.is_variant:
Expand All @@ -377,6 +405,8 @@ def dumps(self):
output.append(str(self.playlists))
if self.iframe_playlists:
output.append(str(self.iframe_playlists))
if self.image_playlists:
output.append(str(self.image_playlists))
if self.part_inf:
output.append(str(self.part_inf))
if self.skip:
Expand Down Expand Up @@ -1477,6 +1507,185 @@ def __str__(self):
return self.dumps()


class ImagePlaylist(BasePathMixin):
"""
ImagePlaylist object representing a link to a
variant M3U8 image playlist with a specific bitrate.
Attributes:
`image_stream_info` is a named tuple containing the attributes:
`bandwidth`, `resolution` which is a tuple (w, h) of integers and `codecs`,
More info: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf
"""

def __init__(self, base_uri, uri, image_stream_info):
self.uri = uri
self.base_uri = base_uri

resolution = image_stream_info.get("resolution")
if resolution is not None:
values = resolution.split("x")
resolution_pair = (int(values[0]), int(values[1]))
else:
resolution_pair = None

self.image_stream_info = StreamInfo(
bandwidth=image_stream_info.get("bandwidth"),
average_bandwidth=image_stream_info.get("average_bandwidth"),
video=image_stream_info.get("video"),
# Audio, subtitles, closed captions, video range and hdcp level should not exist in
# EXT-X-IMAGE-STREAM-INF, so just hardcode them to None.
audio=None,
subtitles=None,
closed_captions=None,
program_id=image_stream_info.get("program_id"),
resolution=resolution_pair,
codecs=image_stream_info.get("codecs"),
video_range=None,
hdcp_level=None,
frame_rate=None,
pathway_id=image_stream_info.get("pathway_id"),
stable_variant_id=image_stream_info.get("stable_variant_id")
)

def __str__(self):
image_stream_inf = []
if self.image_stream_info.program_id:
image_stream_inf.append("PROGRAM-ID=%d" %
self.image_stream_info.program_id)
if self.image_stream_info.bandwidth:
image_stream_inf.append("BANDWIDTH=%d" %
self.image_stream_info.bandwidth)
if self.image_stream_info.average_bandwidth:
image_stream_inf.append("AVERAGE-BANDWIDTH=%d" %
self.image_stream_info.average_bandwidth)
if self.image_stream_info.resolution:
res = (str(self.image_stream_info.resolution[0]) + "x" +
str(self.image_stream_info.resolution[1]))
image_stream_inf.append("RESOLUTION=" + res)
if self.image_stream_info.codecs:
image_stream_inf.append("CODECS=" +
quoted(self.image_stream_info.codecs))
if self.uri:
image_stream_inf.append("URI=" + quoted(self.uri))
if self.image_stream_info.pathway_id:
image_stream_inf.append(
"PATHWAY-ID=" + quoted(self.image_stream_info.pathway_id)
)
if self.image_stream_info.stable_variant_id:
image_stream_inf.append(
"STABLE-VARIANT-ID=" + quoted(self.image_stream_info.stable_variant_id)
)

return "#EXT-X-IMAGE-STREAM-INF:" + ",".join(image_stream_inf)

class Tiles(BasePathMixin):
"""
Image tiles from a M3U8 playlist
`resolution`
resolution attribute from EXT-X-TILES tag
`layout`
layout attribute from EXT-X-TILES tag
`duration`
duration attribute from EXT-X-TILES tag
"""

def __init__(self, resolution, layout, duration):
self.resolution = resolution
self.layout = layout
self.duration = duration

def dumps(self):
tiles = []
tiles.append("RESOLUTION=" + self.resolution)
tiles.append("LAYOUT=" + self.layout)
tiles.append("DURATION=" + self.duration)

return "#EXT-X-TILES:" + ",".join(tiles)

def __str__(self):
return self.dumps()

class ImagePlaylist(BasePathMixin):
'''
ImagePlaylist object representing a link to a
variant M3U8 image playlist with a specific bitrate.
Attributes:
`image_stream_info` is a named tuple containing the attributes:
`bandwidth`, `resolution` which is a tuple (w, h) of integers and `codecs`,
More info: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf
'''

def __init__(self, base_uri, uri, image_stream_info):
self.uri = uri
self.base_uri = base_uri

resolution = image_stream_info.get('resolution')
if resolution is not None:
values = resolution.split('x')
resolution_pair = (int(values[0]), int(values[1]))
else:
resolution_pair = None

self.image_stream_info = StreamInfo(
bandwidth=image_stream_info.get('bandwidth'),
average_bandwidth=image_stream_info.get('average_bandwidth'),
video=image_stream_info.get('video'),
# Audio, subtitles, closed captions, video range and hdcp level should not exist in
# EXT-X-IMAGE-STREAM-INF, so just hardcode them to None.
audio=None,
subtitles=None,
closed_captions=None,
program_id=image_stream_info.get('program_id'),
resolution=resolution_pair,
codecs=image_stream_info.get('codecs'),
video_range=None,
hdcp_level=None,
frame_rate=None,
pathway_id=image_stream_info.get('pathway_id'),
stable_variant_id=image_stream_info.get('stable_variant_id')
)

def __str__(self):
image_stream_inf = []
if self.image_stream_info.program_id:
image_stream_inf.append('PROGRAM-ID=%d' %
self.image_stream_info.program_id)
if self.image_stream_info.bandwidth:
image_stream_inf.append('BANDWIDTH=%d' %
self.image_stream_info.bandwidth)
if self.image_stream_info.average_bandwidth:
image_stream_inf.append('AVERAGE-BANDWIDTH=%d' %
self.image_stream_info.average_bandwidth)
if self.image_stream_info.resolution:
res = (str(self.image_stream_info.resolution[0]) + 'x' +
str(self.image_stream_info.resolution[1]))
image_stream_inf.append('RESOLUTION=' + res)
if self.image_stream_info.codecs:
image_stream_inf.append('CODECS=' +
quoted(self.image_stream_info.codecs))
if self.uri:
image_stream_inf.append('URI=' + quoted(self.uri))
if self.image_stream_info.pathway_id:
image_stream_inf.append(
'PATHWAY-ID=' + quoted(self.image_stream_info.pathway_id)
)
if self.image_stream_info.stable_variant_id:
image_stream_inf.append(
'STABLE-VARIANT-ID=' + quoted(self.image_stream_info.stable_variant_id)
)

return '#EXT-X-IMAGE-STREAM-INF:' + ','.join(image_stream_inf)


def find_key(keydata, keylist):
if not keydata:
return None
Expand Down
Loading

0 comments on commit 98a4bdb

Please sign in to comment.