Package: m3u8
m3u8Pythonm3u8parser.DocumentationLoading a playlistTo load a playlist into an object from uri, file path or directly from string, use theload/loadsfunctions:importm3u8playlist=m3u8.load('http://videoserver.com/playlist.m3u8')# this could also be an absolute filenameprint(playlist.segments)print(playlist.target_duration)# if you already have the content as string, useplaylist=m3u8.loads('#EXTM3U8 ... etc ... ')Dumping a playlistTo dump a playlist from an object to the console or a file, use thedump/dumpsfunctions:importm3u8playlist=m3u8.load('http://videoserver.com/playlist.m3u8')print(playlist.dumps())# if you want to write a file from its contentplaylist.dump('playlist.m3u8')Supported tags#EXT-X-TARGETDURATION#EXT-X-MEDIA-SEQUENCE#EXT-X-DISCONTINUITY-SEQUENCE#EXT-X-PROGRAM-DATE-TIME#EXT-X-MEDIA#EXT-X-PLAYLIST-TYPE#EXT-X-KEY#EXT-X-STREAM-INF#EXT-X-VERSION#EXT-X-ALLOW-CACHE#EXT-X-ENDLIST#EXTINF#EXT-X-I-FRAMES-ONLY#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#EXT-X-CUE-IN#EXT-X-CUE-SPAN#EXT-OATCLS-SCTE35#EXT-X-INDEPENDENT-SEGMENTS#EXT-X-MAP#EXT-X-START#EXT-X-SERVER-CONTROL#EXT-X-PART-INF#EXT-X-PART#EXT-X-RENDITION-REPORT#EXT-X-SKIP#EXT-X-SESSION-DATA#EXT-X-PRELOAD-HINT#EXT-X-SESSION-KEY#EXT-X-DATERANGE#EXT-X-GAP#EXT-X-CONTENT-STEERINGEncryption keysThe segments may or may not be encrypted. Thekeysattribute list will
be a list  with all the different keys as described with#EXT-X-KEY:Each key has the next properties:method: ex.: “AES-128”uri: the key uri, ex.: “http://videoserver.com/key.bin”iv: the initialization vector, if available. OtherwiseNone.If no#EXT-X-KEYis found, thekeyslist will have a unique elementNone. Multiple keys are supported.If unencrypted and encrypted segments are mixed in the M3U8 file, then the list will contain aNoneelement, with one
or more keys afterwards.To traverse the list of keys available:importm3u8m3u8_obj=m3u8.loads('#EXTM3U8 ... etc ...')len(m3u8_obj.keys)# => returns the number of keys available in the list (normally 1)forkeyinm3u8_obj.keys:ifkey:# First one could be Nonekey.urikey.methodkey.ivGetting segments encrypted with one keyThere are cases where listing segments for a given key is important. It’s possible to
retrieve the list of segments encrypted with one key viaby_keymethod in thesegmentslist.Example of getting the segments with no encryption:importm3u8m3u8_obj=m3u8.loads('#EXTM3U8 ... etc ...')segmk1=m3u8_obj.segments.by_key(None)# Get the list of segments encrypted using last keysegm=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:importm3u8m3u8_obj=m3u8.loads('#EXTM3U8 ... etc ...')# Create a new Key and replace itnew_key=m3u8.Key("AES-128","/encrypted/newkey.bin",None,iv="0xf123ad23f22e441098aa87ee")forsegmentinm3u8_obj.segments.by_key(m3u8_obj.keys[-1]):segment.key=new_key# Remember to sync the key from the list as wellm3u8_obj.keys[-1]=new_keyVariant playlists (variable bitrates)A playlist can have a list to other playlist files, this is used to
represent multiple bitrates videos, and it’s calledvariant streams.
See anexample here.variant_m3u8=m3u8.loads('#EXTM3U8 ... contains a variant stream ...')variant_m3u8.is_variant# in this case will be Trueforplaylistinvariant_m3u8.playlists:playlist.uriplaylist.stream_info.bandwidththe playlist object used in the for loop above has a few attributes:uri: the url to the streamstream_info: aStreamInfoobject (actually a namedtuple) with
all the attributes available to#EXT-X-STREAM-INFmedia: a list of relatedMediaobjects with all the attributes
available to#EXT-X-MEDIAplaylist_type: the type of the playlist, which can be one ofVOD(video on demand) orEVENTNOTE: the following attributes are not implemented yet, followissue 4for updatesalternative_audios: its an empty list, unless it’s a playlist
withAlternative audio, in this case it’s a list withMediaobjects with all the attributes available to#EXT-X-MEDIAalternative_videos: same asalternative_audiosA variant playlist can also have links toI-frame playlists, which are used
to specify where the I-frames are in a video. SeeApple’s documentationon
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 ...')foriframe_playlistinvariant_m3u8.iframe_playlists:iframe_playlist.uriiframe_playlist.iframe_stream_info.bandwidthThe iframe_playlist object used in the for loop above has a few attributes:uri: the url to the I-frame playlistbase_uri: the base uri of the variant playlist (if given)iframe_stream_info: aStreamInfoobject (same as a regular playlist)Custom tagsQuoting 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 theload/loadsfunctions, following the example below:importm3u8defget_movie(line,lineno,data,state):ifline.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 babyAlso 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.importreimportm3u8fromm3u8importprotocolfromm3u8.parserimportsave_segment_custom_valuedefparse_iptv_attributes(line,lineno,data,state):# Customize parsing #EXTINFifline.startswith(protocol.extinf):title=''chunks=line.replace(protocol.extinf+':','').split(',',1)iflen(chunks)==2:duration_and_props,title=chunkseliflen(chunks)==1:duration_and_props=chunks[0]additional_props={}chunks=duration_and_props.strip().split(' ',1)iflen(chunks)==2:duration,raw_props=chunksmatched_props=re.finditer(r'([\w\-]+)="([^"]*)"',raw_props)formatchinmatched_props:additional_props[match.group(1)]=match.group(2)else:duration=duration_and_propsif'segment'notinstate:state['segment']={}state['segment']['duration']=float(duration)state['segment']['title']=title# Helper function for saving custom valuessave_segment_custom_value(state,'extinf_props',additional_props)# Tell 'main parser' that we expect an URL on next linesstate['expect_segment']=True# Tell 'main parser' that it can go to next line, we've parsed current fully.returnTrueif__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 clientsIf 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.requestsis a well known Python HTTP library and it can be used withm3u8:importm3u8importrequestsclassRequestsClient():defdownload(self,uri,timeout=None,headers={},verify_ssl=True):o=requests.get(uri,timeout=timeout,headers=headers)returno.text,o.urlplaylist=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 proxiesIn 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.importm3u8proxies={'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 filenameprint(playlist.dumps())It works with the default client only. Custom HTTP clients must implement this feature.Running Tests$./runtestsContributingAll contributions are welcome, but we will merge a pull request if, and only if, ithas testsfollows the code conventionsIf 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.