Giter Club home page Giter Club logo

m3u8's Introduction

image

image

m3u8

Python m3u8 parser.

Documentation

Loading a playlist

To load a playlist into an object from uri, file path or directly from string, use the load/loads functions:

import m3u8

playlist = m3u8.load('http://videoserver.com/playlist.m3u8')  # this could also be an absolute filename
print(playlist.segments)
print(playlist.target_duration)

# if you already have the content as string, use

playlist = m3u8.loads('#EXTM3U8 ... etc ... ')

Dumping a playlist

To dump a playlist from an object to the console or a file, use the dump/dumps functions:

import m3u8

playlist = m3u8.load('http://videoserver.com/playlist.m3u8')
print(playlist.dumps())

# if you want to write a file from its content

playlist.dump('playlist.m3u8')

Supported tags

Encryption keys

The segments may or may not be encrypted. The keys attribute list will be a list with all the different keys as described with #EXT-X-KEY:

Each key has the next properties:

If no #EXT-X-KEY is found, the keys list will have a unique element None. Multiple keys are supported.

If unencrypted and encrypted segments are mixed in the M3U8 file, then the list will contain a None element, with one or more keys afterwards.

To traverse the list of keys available:

import m3u8

m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
len(m3u8_obj.keys)  # => returns the number of keys available in the list (normally 1)
for key in m3u8_obj.keys:
   if key:  # First one could be None
      key.uri
      key.method
      key.iv

Getting segments encrypted with one key

There are cases where listing segments for a given key is important. It's possible to retrieve the list of segments encrypted with one key via by_key method in the segments list.

Example of getting the segments with no encryption:

import m3u8

m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
segmk1 = m3u8_obj.segments.by_key(None)

# Get the list of segments encrypted using last key
segm = m3u8_obj.segments.by_key( m3u8_obj.keys[-1] )

With this method, is now possible also to change the key from some of the segments programmatically:

import m3u8

m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')

# Create a new Key and replace it
new_key = m3u8.Key("AES-128", "/encrypted/newkey.bin", None, iv="0xf123ad23f22e441098aa87ee")
for segment in m3u8_obj.segments.by_key( m3u8_obj.keys[-1] ):
    segment.key = new_key
# Remember to sync the key from the list as well
m3u8_obj.keys[-1] = new_key

Variant playlists (variable bitrates)

A playlist can have a list to other playlist files, this is used to represent multiple bitrates videos, and it's called variant streams. See an example here.

variant_m3u8 = m3u8.loads('#EXTM3U8 ... contains a variant stream ...')
variant_m3u8.is_variant    # in this case will be True

for playlist in variant_m3u8.playlists:
    playlist.uri
    playlist.stream_info.bandwidth

the playlist object used in the for loop above has a few attributes:

  • uri: the url to the stream
  • stream_info: a StreamInfo object (actually a namedtuple) with all the attributes available to #EXT-X-STREAM-INF
  • media: a list of related Media objects with all the attributes available to #EXT-X-MEDIA
  • playlist_type: the type of the playlist, which can be one of VOD (video on demand) or EVENT

NOTE: the following attributes are not implemented yet, follow issue 4 for updates

  • alternative_audios: its an empty list, unless it's a playlist with Alternative audio, in this case it's a list with Media objects with all the attributes available to #EXT-X-MEDIA
  • alternative_videos: same as alternative_audios

A variant playlist can also have links to I-frame playlists, which are used to specify where the I-frames are in a video. See Apple's documentation on this for more information. These I-frame playlists can be accessed in a similar way to regular playlists.

variant_m3u8 = m3u8.loads('#EXTM3U ... contains a variant stream ...')

for iframe_playlist in variant_m3u8.iframe_playlists:
    iframe_playlist.uri
    iframe_playlist.iframe_stream_info.bandwidth

The iframe_playlist object used in the for loop above has a few attributes:

  • uri: the url to the I-frame playlist
  • base_uri: the base uri of the variant playlist (if given)
  • iframe_stream_info: a StreamInfo object (same as a regular playlist)

Custom tags

Quoting the documentation:

Lines that start with the character '#' are either comments or tags.
Tags begin with #EXT.  They are case-sensitive.  All other lines that
begin with '#' are comments and SHOULD be ignored.

This library ignores all the non-standard tags by default. If you want them to be collected while loading the file content, you need to pass a function to the load/loads functions, following the example below:

import m3u8

def get_movie(line, lineno, data, state):
    if line.startswith('#MOVIE-NAME:'):
        custom_tag = line.split(':')
        data['movie'] = custom_tag[1].strip()

m3u8_obj = m3u8.load('http://videoserver.com/playlist.m3u8', custom_tags_parser=get_movie)
print(m3u8_obj.data['movie'])  #  million dollar baby

Also you are able to override parsing of existing standard tags. To achieve this your custom_tags_parser function have to return boolean True - it will mean that you fully implement parsing of current line therefore 'main parser' can go to next line.

import re
import m3u8
from m3u8 import protocol
from m3u8.parser import save_segment_custom_value


def parse_iptv_attributes(line, lineno, data, state):
    # Customize parsing #EXTINF
    if line.startswith(protocol.extinf):
        title = ''
        chunks = line.replace(protocol.extinf + ':', '').split(',', 1)
        if len(chunks) == 2:
            duration_and_props, title = chunks
        elif len(chunks) == 1:
            duration_and_props = chunks[0]

        additional_props = {}
        chunks = duration_and_props.strip().split(' ', 1)
        if len(chunks) == 2:
            duration, raw_props = chunks
            matched_props = re.finditer(r'([\w\-]+)="([^"]*)"', raw_props)
            for match in matched_props:
                additional_props[match.group(1)] = match.group(2)
        else:
            duration = duration_and_props

        if 'segment' not in state:
            state['segment'] = {}
        state['segment']['duration'] = float(duration)
        state['segment']['title'] = title

        # Helper function for saving custom values
        save_segment_custom_value(state, 'extinf_props', additional_props)

        # Tell 'main parser' that we expect an URL on next lines
        state['expect_segment'] = True

        # Tell 'main parser' that it can go to next line, we've parsed current fully.
        return True


if __name__ == '__main__':
    PLAYLIST = """#EXTM3U
    #EXTINF:-1 timeshift="0" catchup-days="7" catchup-type="flussonic" tvg-id="channel1" group-title="Group1",Channel1
    http://str00.iptv.domain/7331/mpegts?token=longtokenhere
    """

    parsed = m3u8.loads(PLAYLIST, custom_tags_parser=parse_iptv_attributes)

    first_segment_props = parsed.segments[0].custom_parser_values['extinf_props']
    print(first_segment_props['tvg-id'])  # 'channel1'
    print(first_segment_props['group-title'])  # 'Group1'
    print(first_segment_props['catchup-type'])  # 'flussonic'

Helper functions get_segment_custom_value() and save_segment_custom_value() are intended for getting/storing your parsed values per segment into Segment class. After that all custom values will be accessible via property custom_parser_values of Segment instance.

Using different HTTP clients

If you don't want to use urllib to download playlists, having more control on how objects are fetched over the internet, you can use your own client. requests is a well known Python HTTP library and it can be used with `m3u8`:

import m3u8
import requests

class RequestsClient():
    def download(self, uri, timeout=None, headers={}, verify_ssl=True):
        o = requests.get(uri, timeout=timeout, headers=headers)
        return o.text, o.url

playlist = m3u8.load('http://videoserver.com/playlist.m3u8', http_client=RequestsClient())
print(playlist.dumps())

The advantage of using a custom HTTP client is to refine SSL verification, proxies, performance, flexibility, etc.

Playlists behind proxies

In case you need to use a proxy but can't use a system wide proxy (HTTP/HTTPS proxy environment variables), you can pass your HTTP/HTTPS proxies as a dict to the load function.

import m3u8

proxies = {
    'http': 'http://10.10.1.10:3128',
    'https': 'http://10.10.1.10:1080',
}

http_client = m3u8.httpclient.DefaultHTTPClient(proxies)
playlist = m3u8.load('http://videoserver.com/playlist.m3u8', http_client=http_client)  # this could also be an absolute filename
print(playlist.dumps())

It works with the default client only. Custom HTTP clients must implement this feature.

Running Tests

$ ./runtests

Contributing

All contributions are welcome, but we will merge a pull request if, and only if, it

  • has tests
  • follows the code conventions

If you plan to implement a new feature or something that will take more than a few minutes, please open an issue to make sure we don't work on the same thing.

m3u8's People

Contributors

ahardwick avatar baco avatar bbayles avatar birme avatar blokfyuh avatar bvc3at avatar cdunklau avatar chrippa avatar colde avatar dantasb avatar daveisfera avatar davemevans avatar feuvan avatar flavioribeiro avatar grumpyoldtroll avatar guthypeter avatar hltbra avatar igorsobreira avatar jbochi avatar leandromoreira avatar lqs avatar mauricioabreu avatar mvmocanu avatar neon-dlea avatar newmaniese avatar onovy avatar prendo93 avatar sim4n6 avatar thenewguy avatar ziima avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

m3u8's Issues

Add Python3 Support

I want to use m3u8 in my project which is written in Python 3. Sadly I found that m3u8 itself doesn't support Python 3. Is there a plan to support Python 3 in the future?

Segments in playlist (through EXT-X-STREAM-INF)

Parsing .m3u8 file with referencing other .m3u8 files, indicating that alternative renditions
of the content are available for playback of that variant stream. The current implementation cannot handle it.

Given a TOS-trailer.m3u8 file:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,RESOLUTION=426x240
TOS-trailer.hls/p0.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=214000,RESOLUTION=426x240
TOS-trailer.hls/p1.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=414000,RESOLUTION=426x240
TOS-trailer.hls/p2.m3u8

With TOS-trailer.hls/p0.m3u8 like:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:6
#EXTINF:5.083333,
p0/stream_0000.ts
#EXTINF:5.000000,
p0/stream_0001.ts
#EXTINF:5.000000,
p0/stream_0002.ts

code snippets:

>>> import m3u8
>>> m3u8_obj = m3u8.load('/tmp/TOS-trailer.m3u8')
>>> p0 = m3u8_obj.playlists[0]
>>> p0.stream_info
StreamInfo(bandwidth='128000', program_id='1', resolution=(426, 240), codecs=None)
>>> p0.media
[]
>>> p0.segments
AttributeError: 'Playlist' object has no attribute 'segments'

Remove arrow depencency

I think this looks like a nice and simple library for parsing m3u8 playlists, but one thing that kind of ruined that impression was when I realized that it depended on arrow. I'm not saying arrow is a bad library, but it's way to big (with all it's own dependencies) for what it's used for in this project.

Currently arrow is used for two operations:

  • One in parser.py: arrow.get(value).datetime...
  • ... and one in models.py: arrow.get(self.program_date_time).isoformat()

So it's basically parsing and formatting date and times in ISO 8601 format. A more lightweight library without additional dependencies for parsing ISO 8601 is https://pypi.python.org/pypi/iso8601. The operations currently done with arrow can be done like this with iso8601:

  • iso8601.parse_date(value)
  • self.program_date_time.isoformat()

Completely removing parsing (just providing the value as a string) or making it optional is another way to go.

the behavior of M3U8(builtins.object).files

>>> import m3u8
>>> p=m3u8.load('http://qv.gaiamount.com/playlist/u116/v810_mp4.m3u8?e=1477373423&token=_6yhTpZI-AHtfULMSiW7eOCvrNEnOS_YcLV9GNfH:a6ZdAsiLXdfy10QBiWguUyq26yU=')
>>> p.files
['http://www.gaiamount.com/keys/3958/4k', '/seg/u116/v810_mp4_0000.ts', '/seg/u116/v810_mp4_0001.ts', '/seg/u116/v810_mp4_0002.ts', '/seg/u116/v810_mp4_0003.ts', '/seg/u116/v810_mp4_0004.ts', '/seg/u116/v810_mp4_0005.ts', '/seg/u116/v810_mp4_0006.ts', '/seg/u116/v810_mp4_0007.ts', '/seg/u116/v810_mp4_0008.ts', '/seg/u116/v810_mp4_0009.ts', '/seg/u116/v810_mp4_0010.ts', '/seg/u116/v810_mp4_0011.ts', '/seg/u116/v810_mp4_0012.ts', '/seg/u116/v810_mp4_0013.ts', '/seg/u116/v810_mp4_0014.ts', '/seg/u116/v810_mp4_0015.ts', '/seg/u116/v810_mp4_0016.ts', '/seg/u116/v810_mp4_0017.ts', '/seg/u116/v810_mp4_0018.ts', '/seg/u116/v810_mp4_0019.ts', '/seg/u116/v810_mp4_0020.ts', '/seg/u116/v810_mp4_0021.ts', '/seg/u116/v810_mp4_0022.ts', '/seg/u116/v810_mp4_0023.ts', '/seg/u116/v810_mp4_0024.ts', '/seg/u116/v810_mp4_0025.ts', '/seg/u116/v810_mp4_0026.ts', '/seg/u116/v810_mp4_0027.ts', '/seg/u116/v810_mp4_0028.ts', '/seg/u116/v810_mp4_0029.ts', '/seg/u116/v810_mp4_0030.ts']
>>>

(1) what should I do to get the absolute URIs of all the segments?
(2) I think M3U8(builtins.object).files should return all the URIs of the segments , but why present the key uri along with them ? This makes the result difficult to use , for example if I want to use the returned result to download all the chunks, then I have to judge whether the first element is a key uri or not, so annoying! It would be better to create another method like M3U8(builtins.object).key_uri to specifically return the key uri .

Incorrect child absolute_uri values generated from base_uri.

Passing the original url as the base_uri input parameter to M3U8 constructor (as the description in its help text describes) makes incorrect absolute_uri paths for the child objects.

This code exercises the problem against one of Apple's sample streams:

$ python
Python 2.7.6 (default, Sep  9 2014, 15:04:36) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import urllib2, m3u8, urlparse
>>> url='https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8'
>>> text=urllib2.urlopen(url).read()
>>> m=m3u8.M3U8(content=text, base_uri=url)
>>> m.playlists[0].absolute_uri
'https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8/gear1/prog_index.m3u8'

My expectation is that m.playlist[0].absolute_uri would be "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8", as you would get from urlparse.urljoin, which is the actual location of the playlist that a video player would retrieve (a relative path derived from the original uri, NOT a sub-path of the original uri). Continuing on, that url that I was expecting does in fact succeed:

>>> urlparse.urljoin(url, m.playlists[0].uri)
'https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8'
>>> subtext=urllib2.urlopen(urlparse.urljoin(url, m.playlists[0].uri)).read()
>>> len(subtext)
6701
>>> sub_m=m3u8.M3U8(subtext)
>>> len(sub_m.segments)
181

Whereas the absolute_uri from the playlist fails:

>>> m.playlists[0].absolute_uri
'https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8/gear1/prog_index.m3u8'
>>> m.playlists[0].uri
'gear1/prog_index.m3u8'
>>> subtext=urllib2.urlopen(m.playlists[0].absolute_uri)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 127, in urlopen
    return _opener.open(url, data, timeout)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 410, in open
    response = meth(req, response)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 523, in http_response
    'http', request, response, code, msg, hdrs)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 448, in error
    return self._call_chain(*args)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 382, in _call_chain
    result = func(*args)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 531, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 404: Not Found

For reference, in case it goes down or changes, the present contents of that master playlist url are here:

#EXTM3U


#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=232370,CODECS="mp4a.40.2, avc1.4d4015"
gear1/prog_index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=649879,CODECS="mp4a.40.2, avc1.4d401e"
gear2/prog_index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=991714,CODECS="mp4a.40.2, avc1.4d401e"
gear3/prog_index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1927833,CODECS="mp4a.40.2, avc1.4d401f"
gear4/prog_index.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=41457,CODECS="mp4a.40.2"
gear0/prog_index.m3u8

Package installation fails

Downloading/unpacking m3u8 (from -r requirements-dev.txt (line 4))
  Downloading m3u8-0.1.3.tar.gz
  Running setup.py egg_info for package m3u8
    Traceback (most recent call last):
      File "<string>", line 14, in <module>
      File "/Users/juarez.bochi/.virtualenvs/nginx/build/m3u8/setup.py", line 4, in <module>
        long_description = codecs.open('README.rst', 'r', 'utf-8').read()
      File "/Users/juarez.bochi/.virtualenvs/nginx/lib/python2.7/codecs.py", line 881, in open
        file = __builtin__.open(filename, mode, buffering)
    IOError: [Errno 2] No such file or directory: 'README.rst'
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):

  File "<string>", line 14, in <module>

  File "/Users/juarez.bochi/.virtualenvs/nginx/build/m3u8/setup.py", line 4, in <module>

    long_description = codecs.open('README.rst', 'r', 'utf-8').read()

  File "/Users/juarez.bochi/.virtualenvs/nginx/lib/python2.7/codecs.py", line 881, in open

    file = __builtin__.open(filename, mode, buffering)

IOError: [Errno 2] No such file or directory: 'README.rst'

----------------------------------------
Command python setup.py egg_info failed with error code 1 in /Users/juarez.bochi/.virtualenvs/nginx/build/m3u8

Undeclared dependency on non-default package "arrow"

==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_model.py _____________________
tests/test_model.py:9: in <module>
    import arrow
E   ImportError: No module named arrow
--------------- coverage: platform darwin, python 2.7.6-final-0 ----------------
Name            Stmts   Miss  Cover   Missing
---------------------------------------------
m3u8/__init__      44      6    86%   16-19, 52, 68
m3u8/model        313     81    74%   14-15, 156, 163, 171-174, 178, 188-192, 207, 216, 218, 220, 222, 224, 226, 228, 231, 242, 250-253, 256-261, 268, 270, 278, 282-284, 289-290, 295-296, 348-369, 372, 381-382, 415-427, 430, 438, 601, 611, 613, 628, 638, 653
m3u8/parser       160     13    92%   23, 27-28, 31, 76, 79, 103, 130-131, 196-198, 208
m3u8/protocol      16      0   100%   
---------------------------------------------
TOTAL             533    100    81%   
================ 27 passed, 9 xfailed, 1 error in 0.44 seconds =================

See:
f50cad5#diff-e71ff7deeaabd2314ed6d0843bde8ae5R9

This seems to be just inside the tests, but there is no new dependency added to setup.py for the arrow package, but the tests no longer run successfully without installing it after the referenced commit.

broken python3 support

4e3e672

has broken python3 support

import exceptions

is not needed and "exceptions.Exception" is now simply "Exception"

in parser.py

Modify strictness

The 'strict' parameter in parser and M3U8 class has little effect, with very bold strictness checking being applied while parsing the input M3U8 manifest.

How 'strict' must the strict parameter force the parser to be? I can think about several levels of strictness checking:

  • Check deprecated or non-supported tags based on version.
  • Check for tag's format and mandatory parameters.
  • Check for overall M3U8 format: EXTM3U tag is mandatory, never checked to be present.

More than an issue, this intends to be a question to open a debate about the model to be implemented. Ideas?

M3U8 doesn't understand relative paths

We need to handle segments that have a relative path to the m3u8.

For instance, this is a m3u8 created by FMS 4.5.1:

#EXTM3U
#EXT-X-MEDIA-SEQUENCE:182498
#EXT-X-ALLOW-CACHE:NO
#EXT-X-VERSION:2
#EXT-X-TARGETDURATION:8
#EXTINF:8,
../../../../hls-live/streams/live_hls/events/testevent/testevent/teststreamNum182498.ts
#EXTINF:8,
../../../../hls-live/streams/live_hls/events/testevent/testevent/teststreamNum182499.ts
#EXTINF:8,
../../../../hls-live/streams/live_hls/events/testevent/testevent/teststreamNum182500.ts
#EXTINF:8,
../../../../hls-live/streams/live_hls/events/testevent/testevent/teststreamNum182501.ts
#EXTINF:8,
../../../../hls-live/streams/live_hls/events/testevent/testevent/teststreamNum182502.ts
#EXTINF:8,
../../../../hls-live/streams/live_hls/events/testevent/testevent/teststreamNum182503.ts

Handle quotes in parse_stream_inf

Hi,
I've found a m3u8 file like that

EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=645000,RESOLUTION=512x288,CODECS="avc1.77.30, mp4a.40.5"

parse stream_inf line around 84 fails because it splits CODECS="avc1.77.30, mp4a.40.5" at the comma
CODECS="avc1.77.30,
mp4a.40.5"

I've tried to use csv, but it does not work, I think because the quoting is only for the part after = and not the whole string between , ,.

Any idea for an elegant solution?

How to handle encryption

Hi,

I've been using this m3u8 library to download .m3u8 files.
Now I found one with encryption

http://lb.cdn.m6web.fr/s/cu/prime/vod/protected/7/e/b/Enquete-exclusive_c11529756_Daesh-la-multinat/Enquete-exclusive_c11529756_Daesh-la-multinat_400k.mp4.m3u8

the top looks like this

EXTM3U

EXT-X-VERSION:3

EXT-X-TARGETDURATION:7

EXT-X-PLAYLIST-TYPE:VOD

EXT-X-FAXS-CM:URI="Enquete-exclusive_c11529756_Daesh-la-multinat_400k.mp4.drm"

EXT-X-KEY:METHOD=AES-128,URI="faxs://faxs.adobe.com",IV=0x5a8ddab79f4fff6a8333ea8ff64d3cd4

EXT-X-MEDIA-TIME:0.0

EXTINF:6,

Enquete-exclusive_c11529756_Daesh-la-multinat_400k.mp4.0_6000.ts?trackid=1,2

I can download the .drm which seems to be related to Flash.
I can download the individual pieces, but they seem to be encrypted.
Anybody knows how the process works?

BANDWIDTH format change on Twitch playlists

It appears that Twitch.tv is breaking/changing specification by producing master playlists that have bandwidth described by a floating point number instead of an integer.

An example of such a playlist can be seen here.

Is it possible to accommodate that change by changing the expected bandwidth format in the module?

Comments are not preserved through loads() + dumps()

The hls spec permits comments (c.f. https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.1, 3rd paragraph).

Some tools use comments to provide extensions to compatible players (such as brightcove zencoder). Preserving comments in their proper locations would interoperate better with such tools.

Here's an example of a playlist with a brightcove-like comment, and which fails the test_dumps_should_build_same_string test:

PLAYLIST_WITH_COMMENT = '''
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:8
#EXTINF:8,
http://media.example.com/entire.ts
#ZEN-TOTAL-DURATION:8
#EXT-X-ENDLIST
'''

(This example also fails because VOD and TARGETDURATION get reordered, a separate issue.)

int type for bandwidth and program-id fields

The spec states that both bandwidth and program-id are of type "decimal-integer". Wouldn't it be best to a) validate and 2) parse these as ints rather than str which they are currently?

m3u8 load() function should discover if parameter is URL or content

I personally don't like two functions to load m3u8 (loads() and load()). I think we might just have one function that discovers if the argument is a m3u8 URL or its content.

We can deprecate one of the functions on the next version and remove it after two or three tags.

if EXT-X-KEY:METHOD=NONE, get TypeError: __init__() takes at least 4 arguments (3 given)

in model.py > Class Segment() > init() for the following line:

self.key = Key(base_uri=base_uri,**key) if key else None

if a segment has "#EXT-X-KEY:METHOD=NONE", it is missing the uri parameter needed for class Key(), its init function:

def __init__(self, method, uri, base_uri, iv=None, keyformat=None, keyformatversions=None):

I believe uri should also be an optional parameter according to this documentation:

URI

   The value is a quoted-string containing a URI that specifies how to
   obtain the key.  This attribute is REQUIRED unless the METHOD is
   NONE.

http://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.2.4

The fix would be the following code for Key().init() where uri=None is the main different making it optional:

def __init__(self, method, base_uri, uri=None, iv=None, keyformat=None, keyformatversions=None):

Ability to write m3u8 file

It would be useful to be able to dynamically create an object representing a stream (with substreams), and output it to a file or stdout. This could be used to create custom HLS servers with non-standard behaviour.

error when upgrade/install via pip

py2.7, win7

  Downloading from URL http://pypi.python.org/packages/source/m/m3u8/m3u8-0.1.3.tar.gz#md5=c20776c36ce936d7556a9e54335482db (from http://pypi.python.org/simple/m3u8/)
  Running setup.py egg_info for package m3u8

    Traceback (most recent call last):

      File "<string>", line 14, in <module>

      File "C:\Users\kutu\build\m3u8\setup.py", line 4, in <module>

        long_description = codecs.open('README.rst', 'r', 'utf-8').read()

      File "D:\Python27\lib\codecs.py", line 881, in open

        file = __builtin__.open(filename, mode, buffering)

    IOError: [Errno 2] No such file or directory: 'README.rst'

    Complete output from command python setup.py egg_info:

    Traceback (most recent call last):

  File "<string>", line 14, in <module>

  File "C:\Users\kutu\build\m3u8\setup.py", line 4, in <module>

    long_description = codecs.open('README.rst', 'r', 'utf-8').read()

  File "D:\Python27\lib\codecs.py", line 881, in open

    file = __builtin__.open(filename, mode, buffering)

IOError: [Errno 2] No such file or directory: 'README.rst'

Add function to merge segment playlists

In case I want to play live stream, I download the playlist from one location and get few segments from continuing stream. I'd like have an function which merges these chunks to single playlist.

Playlist examples:

#EXTM3U
#EXT-X-MEDIA-SEQUENCE:58877187
#EXT-X-TARGETDURATION:8
#EXT-X-PROGRAM-DATE-TIME:2015-12-05T14:11:36Z
#EXTINF:8
1502/58877187.ts
#EXTINF:8
1502/58877188.ts
#EXTINF:8
1502/58877189.ts
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:58877188
#EXT-X-TARGETDURATION:8
#EXT-X-PROGRAM-DATE-TIME:2015-12-05T14:11:44Z
#EXTINF:8
1502/58877188.ts
#EXTINF:8
1502/58877189.ts
#EXTINF:8
1502/58877190.ts

tests in latest master branch are failing even with arrow (media_sequence and cue_out)

=================================== FAILURES ===================================
________________________ test_segment_cue_out_attribute ________________________

    def test_segment_cue_out_attribute():
        obj = m3u8.M3U8(playlists.CUE_OUT_PLAYLIST)
        segments = obj.segments

>       assert segments[0].cue_out == True
E       assert <m3u8.model.Segment object at 0x10e4becd0>.cue_out == True

tests/test_model.py:64: AssertionError
__________________ test_dump_should_work_for_iframe_playlists __________________

    def test_dump_should_work_for_iframe_playlists():
        obj = m3u8.M3U8(playlists.IFRAME_PLAYLIST)

        expected = playlists.IFRAME_PLAYLIST.strip()

>       assert expected == obj.dumps().strip()
E       assert '#EXTM3U\n#EX...EXT-X-ENDLIST' == '#EXTM3U\n#EXT...EXT-X-ENDLIST'
E           #EXTM3U
E         - #EXT-X-MEDIA-SEQUENCE:0
E           #EXT-X-VERSION:4
E           #EXT-X-TARGETDURATION:10
E           #EXT-X-PLAYLIST-TYPE:VOD
E           #EXT-X-I-FRAMES-ONLY
E           #EXTINF:4.12,
E           #EXT-X-BYTERANGE:9400@376
E           segment1.ts
E           #EXTINF:3.56,
E           #EXT-X-BYTERANGE:7144@47000
E           segment1.ts
E           #EXTINF:3.82,
E           #EXT-X-BYTERANGE:10340@1880
E           segment2.ts
E           #EXT-X-ENDLIST

tests/test_model.py:376: AssertionError
_____________ test_dump_should_work_for_playlists_using_byteranges _____________

    def test_dump_should_work_for_playlists_using_byteranges():
        obj = m3u8.M3U8(playlists.PLAYLIST_USING_BYTERANGES)

        expected = playlists.PLAYLIST_USING_BYTERANGES.strip()

>       assert expected == obj.dumps().strip()
E       assert '#EXTM3U\n#EX...EXT-X-ENDLIST' == '#EXTM3U\n#EXT...EXT-X-ENDLIST'
E           #EXTM3U
E         - #EXT-X-MEDIA-SEQUENCE:0
E           #EXT-X-VERSION:4
E           #EXT-X-TARGETDURATION:11
E           #EXTINF:10,
E           #EXT-X-BYTERANGE:76242@0
E           segment.ts
E           #EXTINF:10,
E           #EXT-X-BYTERANGE:83442@762421
E           segment.ts
E           #EXTINF:10,
E           #EXT-X-BYTERANGE:69864@834421
E           segment.ts
E           #EXT-X-ENDLIST

tests/test_model.py:388: AssertionError
_____________________ test_0_media_sequence_added_to_file ______________________

    def test_0_media_sequence_added_to_file():
        obj = m3u8.M3U8()
        obj.media_sequence = 0
        result = obj.dumps()
        expected = '#EXTM3U\n#EXT-X-MEDIA-SEQUENCE:0\n'
>       assert result == expected
E       assert '#EXTM3U\n' == '#EXTM3U\n#EXT-X-MEDIA-SEQUENCE:0\n'
E           #EXTM3U
E         + #EXT-X-MEDIA-SEQUENCE:0

tests/test_model.py:495: AssertionError
--------------- coverage: platform darwin, python 2.7.6-final-0 ----------------
Name            Stmts   Miss  Cover   Missing
---------------------------------------------
m3u8/__init__      44      6    86%   16-19, 52, 68
m3u8/model        313     17    95%   14-15, 163, 207, 261, 268, 270, 357, 360, 372, 423, 425, 601, 611, 613, 628, 638
m3u8/parser       140      4    97%   94, 165-167
m3u8/protocol      16      0   100%   
---------------------------------------------
TOTAL             513     27    95%   
================ 4 failed, 75 passed, 9 xfailed in 0.44 seconds ================
Test server stdout on tests/server.stdout

This output follows from a clean run: mkdir tmp; cd tmp; mkvirtualenv test1; git clone https://github.com/globocom/m3u8.git; pip install arrow; cd m3u8; ./runtests

should be able to create m3u8 from scratch

Today we need to provide a m3u8 as string to create a m3u8.M3U8 object. We should be able to create one from scratch:

example:

import m3u8

playlist_low = m3u8.Playlist('http://videoserver.com/low.m3u8', stream_info={'bandwidth': '1280000'})
playlist_high = m3u8.Playlist('http://videoserver.com/high.m3u8', stream_info={'bandwidth': '3000000'})

obj = m3u8.M3U8()
obj.add_playlist(playlist_low)
obj.add_playlist(playlist_high)

print obj.dumps()

The main usage will be to create variant streams index files.

Ignore blank lines in m3u8 file

I came across a stream with blank lines in the m3u8 file, between each stream info line and uri. In the protocol for these files, it states the blank lines should be ignored. When I ran the parser on it though the uri was being set as an empty string. If I add the following to the for loop in the parse function (in parser.py) it ignores these lines:

# Ignore blank lines
if line == "":
continue

In the HTTP live streaming protocol internet draft, the following appears in section 4.1 Definition of a playlist:
"Each line is a URI, blank, or starts with the character '#'. Blank lines are ignored."

See the document here:
https://tools.ietf.org/html/draft-pantos-http-live-streaming-18#section-4.1

Is this a reasonable thing for me to submit a patch for?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.