diff --git a/.coveralls.yml b/.coveralls.yml
deleted file mode 100644
index f62e437..0000000
--- a/.coveralls.yml
+++ /dev/null
@@ -1 +0,0 @@
-repo_token: YiOOP7xuzmxWqSWpUQh9xJlTFFD0DTV2g
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..a4ec022
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,40 @@
+# This is a basic workflow to help you get started with Actions
+
+name: CI
+
+# Controls when the action will run. 
+on:
+  # Triggers the workflow on push or pull request events but only for the master branch
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+  # This workflow contains a single job called "build"
+  build:
+    # The type of runner that the job will run on
+    runs-on: ubuntu-latest
+    strategy:
+      # You can use PyPy versions in python-version.
+      # For example, pypy2 and pypy3
+      matrix:
+        python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
+
+    # Steps represent a sequence of tasks that will be executed as part of the job
+    steps:
+      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      # Runs a single command using the runners shell
+      - name: Run all tests
+        run: ./runtests
+        
diff --git a/.gitignore b/.gitignore
index 26748eb..c1ca433 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,8 +6,10 @@ build/
 bin/
 include/
 lib/
+lib64/
 local/
 .coverage
 .cache
 .python-version
 .idea/
+pyvenv.cfg
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 8e836ec..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-language: python
-sudo: false
-python:
-  - "3.5"
-  - "3.6"
-  - "3.6-dev"
-  - "3.7"
-  - "3.7-dev"
-  - "3.8"
-  - "3.8-dev"
-  - "nightly"
-script: ./runtests
-after_success: coveralls
diff --git a/README.rst b/README.rst
index f16d735..d2fff83 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,4 @@
-.. image:: https://travis-ci.org/globocom/m3u8.svg
-    :target: https://travis-ci.org/globocom/m3u8
-
-.. image:: https://coveralls.io/repos/globocom/m3u8/badge.png?branch=master
-    :target: https://coveralls.io/r/globocom/m3u8?branch=master
+.. image:: https://github.com/globocom/m3u8/actions/workflows/main.yml/badge.svg
 
 .. image:: https://badge.fury.io/py/m3u8.svg
     :target: https://badge.fury.io/py/m3u8
@@ -65,6 +61,7 @@ Supported tags
 * `#EXT-X-ENDLIST`_
 * `#EXTINF`_
 * `#EXT-X-I-FRAMES-ONLY`_
+* `#EXT-X-BITRATE`_
 * `#EXT-X-BYTERANGE`_
 * `#EXT-X-I-FRAME-STREAM-INF`_
 * `#EXT-X-DISCONTINUITY`_
@@ -82,8 +79,11 @@ Supported tags
 * #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-STEERING`_
 
 Encryption keys
 ---------------
@@ -224,7 +224,7 @@ you need to pass a function to the `load/loads` functions, following the example
 
     import m3u8
 
-    def get_movie(line, data, lineno):
+    def get_movie(line, lineno, data, state):
         if line.startswith('#MOVIE-NAME:'):
             custom_tag = line.split(':')
             data['movie'] = custom_tag[1].strip()
@@ -232,6 +232,69 @@ you need to pass a function to the `load/loads` functions, following the example
     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.
+
+.. code-block:: python
+
+    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
 ----------------------------
 
@@ -296,6 +359,7 @@ the same thing.
 .. _m3u8: https://tools.ietf.org/html/rfc8216
 .. _#EXT-X-VERSION: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
 .. _#EXTINF: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
+.. _#EXT-X-BITRATE: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.8
 .. _#EXT-X-BYTERANGE: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
 .. _#EXT-X-DISCONTINUITY: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
 .. _#EXT-X-KEY: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
@@ -312,10 +376,13 @@ the same thing.
 .. _#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-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
 .. _#EXT-X-START: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
+.. _#EXT-X-PRELOAD-HINT: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.5.3
 .. _#EXT-X-DATERANGE: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
 .. _#EXT-X-GAP: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.2.7
+.. _#EXT-X-CONTENT-STEERING: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-10#section-4.4.6.6
 .. _issue 1: https://github.com/globocom/m3u8/issues/1
 .. _variant streams: https://tools.ietf.org/html/rfc8216#section-6.2.4
 .. _example here: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.5
diff --git a/debian/changelog b/debian/changelog
index 5ce88af..7998adc 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-m3u8 (2.0.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 22 May 2022 02:16:58 -0000
+
 python-m3u8 (0.8.0-2) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/m3u8/__init__.py b/m3u8/__init__.py
index b65e912..5362dda 100644
--- a/m3u8/__init__.py
+++ b/m3u8/__init__.py
@@ -12,7 +12,7 @@ from m3u8.model import (M3U8, Segment, SegmentList, PartialSegment,
                         Media, MediaList, PlaylistList, Start,
                         RenditionReport, RenditionReportList, ServerControl,
                         Skip, PartInformation, PreloadHint, DateRange,
-                        DateRangeList)
+                        DateRangeList, ContentSteering)
 from m3u8.parser import parse, is_url, ParseError
 
 
@@ -20,8 +20,8 @@ __all__ = ('M3U8', 'Segment', 'SegmentList', 'PartialSegment',
             'PartialSegmentList', 'Key', 'Playlist', 'IFramePlaylist',
             'Media', 'MediaList', 'PlaylistList', 'Start', 'RenditionReport',
             'RenditionReportList', 'ServerControl', 'Skip', 'PartInformation',
-            'PreloadHint' 'DateRange', 'DateRangeList', 'loads', 'load',
-            'parse', 'ParseError')
+            'PreloadHint' 'DateRange', 'DateRangeList', 'ContentSteering',
+            'loads', 'load', 'parse', 'ParseError')
 
 def loads(content, uri=None, custom_tags_parser=None):
     '''
diff --git a/m3u8/httpclient.py b/m3u8/httpclient.py
index d03bbab..71b8231 100644
--- a/m3u8/httpclient.py
+++ b/m3u8/httpclient.py
@@ -1,17 +1,10 @@
-import posixpath
 import ssl
-import sys
-import urllib
-from urllib.error import HTTPError
-from urllib.parse import urlparse, urljoin
 import urllib.request
+from m3u8.parser import urljoin
 
 
 def _parsed_url(url):
-    parsed_url = urlparse(url)
-    prefix = parsed_url.scheme + '://' + parsed_url.netloc
-    base_path = posixpath.normpath(parsed_url.path + '/..')
-    return urljoin(prefix, base_path)
+    return urljoin(url, '.')
 
 
 class DefaultHTTPClient:
diff --git a/m3u8/mixins.py b/m3u8/mixins.py
index acdacee..5d55168 100644
--- a/m3u8/mixins.py
+++ b/m3u8/mixins.py
@@ -1,16 +1,13 @@
 
 import os
-from m3u8.parser import is_url
-
-try:
-    import urlparse as url_parser
-except ImportError:
-    import urllib.parse as url_parser
+from m3u8.parser import is_url, urljoin
 
 
 def _urijoin(base_uri, path):
     if is_url(base_uri):
-        return url_parser.urljoin(base_uri, path)
+        if base_uri[-1] != '/':
+            base_uri += '/'
+        return urljoin(base_uri, path)
     else:
         return os.path.normpath(os.path.join(base_uri, path.strip('/')))
 
diff --git a/m3u8/model.py b/m3u8/model.py
index 19b7af0..2037693 100644
--- a/m3u8/model.py
+++ b/m3u8/model.py
@@ -5,7 +5,6 @@
 import decimal
 import os
 import errno
-import math
 
 from m3u8.protocol import ext_x_start, ext_x_key, ext_x_session_key, ext_x_map
 from m3u8.parser import parse, format_date_time
@@ -155,13 +154,16 @@ class M3U8(object):
         self._initialize_attributes()
         self.base_path = base_path
 
-
     def _initialize_attributes(self):
-        self.keys = [ Key(base_uri=self.base_uri, **params) if params else None
-                      for params in self.data.get('keys', []) ]
-        self.segments = SegmentList([ Segment(base_uri=self.base_uri, keyobject=find_key(segment.get('key', {}), self.keys), **segment)
-                                      for segment in self.data.get('segments', []) ])
-        #self.keys = get_uniques([ segment.key for segment in self.segments ])
+        self.keys = [Key(base_uri=self.base_uri, **params) if params else None
+                     for params in self.data.get('keys', [])]
+        self.segment_map = [InitializationSection(base_uri=self.base_uri, **params) if params else None
+                     for params in self.data.get('segment_map', [])]
+        self.segments = SegmentList([
+            Segment(base_uri=self.base_uri, keyobject=find_key(segment.get('key', {}), self.keys), **segment)
+            for segment in self.data.get('segments', [])
+        ])
+
         for attr, param in self.simple_attributes:
             setattr(self, attr, self.data.get(param))
 
@@ -184,7 +186,6 @@ class M3U8(object):
                                          uri=ifr_pl['uri'],
                                          iframe_stream_info=ifr_pl['iframe_stream_info'])
                                         )
-        self.segment_map = self.data.get('segment_map')
 
         start = self.data.get('start', None)
         self.start = start and Start(**start)
@@ -211,6 +212,9 @@ class M3U8(object):
         preload_hint = self.data.get('preload_hint', None)
         self.preload_hint = preload_hint and PreloadHint(base_uri=self.base_uri, **preload_hint)
 
+        content_steering = self.data.get('content_steering', None)
+        self.content_steering = content_steering and ContentSteering(base_uri=self.base_uri, **content_steering)
+
     def __unicode__(self):
         return self.dumps()
 
@@ -234,6 +238,8 @@ class M3U8(object):
                 key.base_uri = new_base_uri
         if self.preload_hint:
             self.preload_hint.base_uri = new_base_uri
+        if self.content_steering:
+            self.content_steering.base_uri = new_base_uri
 
     @property
     def base_path(self):
@@ -260,6 +266,8 @@ class M3U8(object):
         self.rendition_reports.base_path = self._base_path
         if self.preload_hint:
             self.preload_hint.base_path = self._base_path
+        if self.content_steering:
+            self.content_steering.base_path = self._base_path
 
 
     def add_playlist(self, playlist):
@@ -286,6 +294,8 @@ class M3U8(object):
         You could also use unicode(<this obj>) or str(<this obj>)
         '''
         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:
@@ -408,6 +418,9 @@ class Segment(BasePathMixin):
     `base_uri`
       uri the key comes from in URI hierarchy. ex.: http://example.com/path/to
 
+    `bitrate`
+      bitrate attribute from EXT-X-BITRATE parameter
+
     `byterange`
       byterange attribute from EXT-X-BYTERANGE parameter
 
@@ -418,20 +431,25 @@ class Segment(BasePathMixin):
       partial segments that make up this segment
 
     `dateranges`
-      any dateranges that should  preceed the segment
+      any dateranges that should  precede the segment
 
     `gap_tag`
       GAP tag indicates that a Media Segment is missing
+
+    `custom_parser_values`
+        Additional values which custom_tags_parser might store per segment
     '''
 
     def __init__(self, uri=None, base_uri=None, program_date_time=None, current_program_date_time=None,
-                 duration=None, title=None, byterange=None, cue_out=False, cue_out_start=False,
+                 duration=None, title=None, bitrate=None, byterange=None, cue_out=False, cue_out_start=False,
                  cue_in=False, discontinuity=False, key=None, scte35=None, scte35_duration=None,
-                 keyobject=None, parts=None, init_section=None, dateranges=None, gap_tag=None):
+                 keyobject=None, parts=None, init_section=None, dateranges=None, gap_tag=None,
+                 custom_parser_values=None):
         self.uri = uri
         self.duration = duration
         self.title = title
         self._base_uri = base_uri
+        self.bitrate = bitrate
         self.byterange = byterange
         self.program_date_time = program_date_time
         self.current_program_date_time = current_program_date_time
@@ -449,8 +467,7 @@ class Segment(BasePathMixin):
             self.init_section = None
         self.dateranges = DateRangeList( [ DateRange(**daterange) for daterange in dateranges ] if dateranges else [] )
         self.gap_tag = gap_tag
-
-        # Key(base_uri=base_uri, **key) if key else None
+        self.custom_parser_values = custom_parser_values or {}
 
     def add_part(self, part):
         self.parts.append(part)
@@ -458,7 +475,6 @@ class Segment(BasePathMixin):
     def dumps(self, last_segment):
         output = []
 
-
         if last_segment and self.key != last_segment.key:
             output.append(str(self.key))
             output.append('\n')
@@ -511,6 +527,9 @@ class Segment(BasePathMixin):
             if self.byterange:
                 output.append('#EXT-X-BYTERANGE:%s\n' % self.byterange)
 
+            if self.bitrate:
+                output.append('#EXT-X-BITRATE:%s\n' % self.bitrate)
+
             if self.gap_tag:
                 output.append('#EXT-X-GAP\n')
 
@@ -593,7 +612,7 @@ class PartialSegment(BasePathMixin):
       GAP attribute indicates the Partial Segment is not available
 
     `dateranges`
-      any dateranges that should preceed the partial segment
+      any dateranges that should precede the partial segment
 
     `gap_tag`
       GAP tag indicates one or more of the parent Media Segment's Partial
@@ -734,7 +753,7 @@ class InitializationSection(BasePathMixin):
         if self.uri:
             output.append('URI=' + quoted(self.uri))
         if self.byterange:
-            output.append('BYTERANGE=' + self.byterange)
+            output.append('BYTERANGE=' + quoted(self.byterange))
         return "{tag}:{attributes}".format(tag=self.tag, attributes=",".join(output))
 
     def __eq__(self, other):
@@ -789,7 +808,8 @@ class Playlist(BasePathMixin):
             codecs=stream_info.get('codecs'),
             frame_rate=stream_info.get('frame_rate'),
             video_range=stream_info.get('video_range'),
-            hdcp_level=stream_info.get('hdcp_level')
+            hdcp_level=stream_info.get('hdcp_level'),
+            pathway_id=stream_info.get('pathway_id')
         )
         self.media = []
         for media_type in ('audio', 'video', 'subtitles'):
@@ -852,7 +872,8 @@ class IFramePlaylist(BasePathMixin):
             codecs=iframe_stream_info.get('codecs'),
             video_range=iframe_stream_info.get('video_range'),
             hdcp_level=iframe_stream_info.get('hdcp_level'),
-            frame_rate=None
+            frame_rate=None,
+            pathway_id=iframe_stream_info.get('pathway_id')
         )
 
     def __str__(self):
@@ -881,6 +902,10 @@ class IFramePlaylist(BasePathMixin):
                                      self.iframe_stream_info.hdcp_level)
         if self.uri:
             iframe_stream_inf.append('URI=' + quoted(self.uri))
+        if self.iframe_stream_info.pathway_id:
+            iframe_stream_inf.append(
+                'PATHWAY-ID=' + quoted(self.iframe_stream_info.pathway_id)
+            )
 
         return '#EXT-X-I-FRAME-STREAM-INF:' + ','.join(iframe_stream_inf)
 
@@ -898,6 +923,7 @@ class StreamInfo(object):
     frame_rate = None
     video_range = None
     hdcp_level = None
+    pathway_id = None
 
     def __init__(self, **kwargs):
         self.bandwidth = kwargs.get("bandwidth")
@@ -912,6 +938,7 @@ class StreamInfo(object):
         self.frame_rate = kwargs.get("frame_rate")
         self.video_range = kwargs.get("video_range")
         self.hdcp_level = kwargs.get("hdcp_level")
+        self.pathway_id = kwargs.get("pathway_id")
 
     def __str__(self):
         stream_inf = []
@@ -936,6 +963,8 @@ class StreamInfo(object):
             stream_inf.append('VIDEO-RANGE=%s' % self.video_range)
         if self.hdcp_level is not None:
             stream_inf.append('HDCP-LEVEL=%s' % self.hdcp_level)
+        if self.pathway_id is not None:
+            stream_inf.append('PATHWAY-ID=' + quoted(self.pathway_id))
         return ",".join(stream_inf)
 
 
@@ -1253,6 +1282,24 @@ class DateRange(object):
     def __str__(self):
         return self.dumps()
 
+class ContentSteering(BasePathMixin):
+    def __init__(self, base_uri, server_uri, pathway_id = None):
+        self.base_uri = base_uri
+        self.uri = server_uri
+        self.pathway_id = pathway_id
+
+    def dumps(self):
+        steering = []
+        steering.append('SERVER-URI=' + quoted(self.uri))
+
+        if self.pathway_id is not None:
+            steering.append('PATHWAY-ID=' + quoted(self.pathway_id))
+
+        return '#EXT-X-CONTENT-STEERING:' + ','.join(steering)
+
+    def __str__(self):
+        return self.dumps()
+
 def find_key(keydata, keylist):
     if not keydata:
         return None
diff --git a/m3u8/parser.py b/m3u8/parser.py
index 0cd3490..a5f6377 100644
--- a/m3u8/parser.py
+++ b/m3u8/parser.py
@@ -7,6 +7,7 @@ import iso8601
 import datetime
 import itertools
 import re
+from urllib.parse import urljoin as _urljoin
 from m3u8 import protocol
 
 '''
@@ -14,7 +15,7 @@ http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
 http://stackoverflow.com/questions/2785755/how-to-split-but-ignore-separators-in-quoted-strings-in-python
 '''
 ATTRIBUTELISTPATTERN = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')
-
+URI_PREFIXES = ('https://', 'http://', 's3://', 's3a://', 's3n://')
 
 def cast_date_time(value):
     return iso8601.parse_date(value)
@@ -24,7 +25,6 @@ def format_date_time(value):
     return value.isoformat()
 
 
-
 class ParseError(Exception):
 
     def __init__(self, lineno, line):
@@ -56,6 +56,7 @@ def parse(content, strict=False, custom_tags_parser=None):
         'part_inf': {},
         'session_data': [],
         'session_keys': [],
+        'segment_map': []
     }
 
     state = {
@@ -70,9 +71,21 @@ def parse(content, strict=False, custom_tags_parser=None):
         lineno += 1
         line = line.strip()
 
+        # Call custom parser if needed
+        if line.startswith('#') and callable(custom_tags_parser):
+            go_to_next_line = custom_tags_parser(line, lineno, data, state)
+
+            # Do not try to parse other standard tags on this line if custom_tags_parser function returns 'True'
+            if go_to_next_line:
+                continue
+
         if line.startswith(protocol.ext_x_byterange):
             _parse_byterange(line, state)
             state['expect_segment'] = True
+            continue
+
+        if line.startswith(protocol.ext_x_bitrate):
+            _parse_bitrate(line, state)
 
         elif line.startswith(protocol.ext_x_targetduration):
             _parse_simple_parameter(line, data, float)
@@ -147,11 +160,10 @@ def parse(content, strict=False, custom_tags_parser=None):
             data['is_endlist'] = True
 
         elif line.startswith(protocol.ext_x_map):
-            quoted_parser = remove_quotes_parser('uri')
+            quoted_parser = remove_quotes_parser('uri', 'byterange')
             segment_map_info = _parse_attribute_list(protocol.ext_x_map, line, quoted_parser)
             state['current_segment_map'] = segment_map_info
-            # left for backward compatibility
-            data['segment_map'] = segment_map_info
+            data['segment_map'].append(segment_map_info)
 
         elif line.startswith(protocol.ext_x_start):
             attribute_parser = {
@@ -190,10 +202,12 @@ def parse(content, strict=False, custom_tags_parser=None):
         elif line.startswith(protocol.ext_x_gap):
             state['gap'] = True
 
-        # Comments and whitespace
-        elif line.startswith('#'):
-            if callable(custom_tags_parser):
-                custom_tags_parser(line, data, lineno)
+        elif line.startswith(protocol.ext_x_content_steering):
+            _parse_content_steering(line, data, state)
+
+        elif line.startswith(protocol.ext_m3u):
+            # We don't parse #EXTM3U, it just should to be present
+            pass
 
         elif line.strip() == '':
             # blank lines are legal
@@ -253,10 +267,9 @@ def _parse_ts_chunk(line, data, state):
     segment['cue_in'] = state.pop('cue_in', False)
     segment['cue_out'] = state.pop('cue_out', False)
     segment['cue_out_start'] = state.pop('cue_out_start', False)
-    if state.get('current_cue_out_scte35'):
-        segment['scte35'] = state['current_cue_out_scte35']
-    if state.get('current_cue_out_duration'):
-        segment['scte35_duration'] = state['current_cue_out_duration']
+    scte_op = state.pop if segment['cue_in'] else state.get
+    segment['scte35'] = scte_op('current_cue_out_scte35', None)
+    segment['scte35_duration'] = scte_op('current_cue_out_duration', None)
     segment['discontinuity'] = state.pop('discontinuity', False)
     if state.get('current_key'):
         segment['key'] = state['current_key']
@@ -289,7 +302,7 @@ def _parse_attribute_list(prefix, line, atribute_parser):
 def _parse_stream_inf(line, data, state):
     data['is_variant'] = True
     data['media_sequence'] = None
-    atribute_parser = remove_quotes_parser('codecs', 'audio', 'video', 'subtitles', 'closed_captions')
+    atribute_parser = remove_quotes_parser('codecs', 'audio', 'video', 'subtitles', 'closed_captions', 'pathway_id')
     atribute_parser["program_id"] = int
     atribute_parser["bandwidth"] = lambda x: int(float(x))
     atribute_parser["average_bandwidth"] = int
@@ -300,7 +313,7 @@ def _parse_stream_inf(line, data, state):
 
 
 def _parse_i_frame_stream_inf(line, data):
-    atribute_parser = remove_quotes_parser('codecs', 'uri')
+    atribute_parser = remove_quotes_parser('codecs', 'uri', 'pathway_id')
     atribute_parser["program_id"] = int
     atribute_parser["bandwidth"] = int
     atribute_parser["average_bandwidth"] = int
@@ -325,6 +338,10 @@ def _parse_variant_playlist(line, data, state):
 
     data['playlists'].append(playlist)
 
+def _parse_bitrate(line, state):
+    if 'segment' not in state:
+        state['segment'] = {}
+    state['segment']['bitrate'] = line.replace(protocol.ext_x_bitrate + ':', '')
 
 def _parse_byterange(line, state):
     if 'segment' not in state:
@@ -351,7 +368,10 @@ def _parse_simple_parameter(line, data, cast_to=str):
 
 
 def _parse_cueout_cont(line, state):
-    param, value = line.split(':', 1)
+    elements = line.split(':', 1)
+    if len(elements) != 2:
+        return
+    param, value = elements
     res = re.match('.*Duration=(.*),SCTE35=(.*)$', value)
     if res:
         state['current_cue_out_duration'] = res.group(1)
@@ -382,7 +402,7 @@ def _cueout_envivio(line, state, prevline):
 def _cueout_duration(line):
     # this needs to be called after _cueout_elemental
     # as it would capture those cues incompletely
-    # This was added seperately rather than modifying "simple"
+    # This was added separately rather than modifying "simple"
     param, value = line.split(':', 1)
     res = re.match(r'DURATION=(.*)', value)
     if res:
@@ -511,6 +531,12 @@ def _parse_daterange(line, date, state):
 
     state['dateranges'].append(parsed)
 
+def _parse_content_steering(line, data, state):
+    attribute_parser = remove_quotes_parser('server_uri', 'pathway_id')
+
+    data['content_steering'] = _parse_attribute_list(
+        protocol.ext_x_content_steering, line, attribute_parser
+    )
 
 def string_to_lines(string):
     return string.strip().splitlines()
@@ -541,4 +567,40 @@ def normalize_attribute(attribute):
 
 
 def is_url(uri):
-    return uri.startswith(('https://', 'http://'))
+    return uri.startswith(URI_PREFIXES)
+
+
+def urljoin(base, url):
+    base = base.replace('://', '\1')
+    url = url.replace('://', '\1')
+    while '//' in base:
+        base = base.replace('//', '/\0/')
+    while '//' in url:
+        url = url.replace('//', '/\0/')
+    return _urljoin(base.replace('\1', '://'), url.replace('\1', '://')).replace('\0', '')
+
+
+def get_segment_custom_value(state, key, default=None):
+    """
+    Helper function for getting custom values for Segment
+    Are useful with custom_tags_parser
+    """
+    if 'segment' not in state:
+        return default
+    if 'custom_parser_values' not in state['segment']:
+        return default
+    return state['segment']['custom_parser_values'].get(key, default)
+
+
+def save_segment_custom_value(state, key, value):
+    """
+    Helper function for saving custom values for Segment
+    Are useful with custom_tags_parser
+    """
+    if 'segment' not in state:
+        state['segment'] = {}
+
+    if 'custom_parser_values' not in state['segment']:
+        state['segment']['custom_parser_values'] = {}
+
+    state['segment']['custom_parser_values'][key] = value
diff --git a/m3u8/protocol.py b/m3u8/protocol.py
index 3db8b28..87e6ca1 100644
--- a/m3u8/protocol.py
+++ b/m3u8/protocol.py
@@ -3,6 +3,7 @@
 # Use of this source code is governed by a MIT License
 # license that can be found in the LICENSE file.
 
+ext_m3u = '#EXTM3U'
 ext_x_targetduration = '#EXT-X-TARGETDURATION'
 ext_x_media_sequence = '#EXT-X-MEDIA-SEQUENCE'
 ext_x_discontinuity_sequence = '#EXT-X-DISCONTINUITY-SEQUENCE'
@@ -16,6 +17,7 @@ ext_x_allow_cache = '#EXT-X-ALLOW-CACHE'
 ext_x_endlist = '#EXT-X-ENDLIST'
 extinf = '#EXTINF'
 ext_i_frames_only = '#EXT-X-I-FRAMES-ONLY'
+ext_x_bitrate = "#EXT-X-BITRATE"
 ext_x_byterange = '#EXT-X-BYTERANGE'
 ext_x_i_frame_stream_inf = '#EXT-X-I-FRAME-STREAM-INF'
 ext_x_discontinuity = '#EXT-X-DISCONTINUITY'
@@ -37,3 +39,4 @@ ext_x_session_key = '#EXT-X-SESSION-KEY'
 ext_x_preload_hint = '#EXT-X-PRELOAD-HINT'
 ext_x_daterange = "#EXT-X-DATERANGE"
 ext_x_gap = "#EXT-X-GAP"
+ext_x_content_steering = "#EXT-X-CONTENT-STEERING"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 91284d5..56806d3 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -6,4 +6,3 @@ pytest
 # which is in conflict with the version requirement 
 # defined by the python-coveralls package for coverage==4.0.3
 pytest-cov>=2.4.0,<2.6
-python-coveralls
diff --git a/runtests b/runtests
index 229e35d..7d55bd7 100755
--- a/runtests
+++ b/runtests
@@ -12,7 +12,7 @@ function start_server {
 }
 
 function stop_server {
-    ps ax | grep m3u8server.py | grep -v grep | cut -d ' ' -f 1 | xargs kill
+    pkill -9 -f m3u8server.py
     echo "Test server stdout on ${test_server_stdout}"
 }
 
@@ -32,5 +32,5 @@ function main {
 if [ -z "$1" ]; then
     main
 else
-    $@
+    "$@"
 fi
diff --git a/setup.py b/setup.py
index df6f20c..16e979f 100644
--- a/setup.py
+++ b/setup.py
@@ -11,8 +11,7 @@ install_reqs = [req for req in open(abspath(join(dirname(__file__), 'requirement
 setup(
     name="m3u8",
     author='Globo.com',
-    author_email='videos3@corp.globo.com',
-    version="0.8.0",
+    version="2.0.0",
     license='MIT',
     zip_safe=False,
     include_package_data=True,
diff --git a/tests/playlists.py b/tests/playlists.py
index d4f766a..e88fb8b 100755
--- a/tests/playlists.py
+++ b/tests/playlists.py
@@ -725,6 +725,8 @@ master2500_47232.ts
 #EXT-X-CUE-IN
 #EXTINF:7.960,
 master2500_47233.ts
+#EXTINF:7.960,
+master2500_47234.ts
 '''
 
 CUE_OUT_ENVIVIO_PLAYLIST = '''
@@ -760,6 +762,9 @@ CUE_OUT_INVALID_PLAYLIST = '''#EXTM3U
 #EXT-X-CUE-OUT:INVALID
 #EXTINF:5.76, no desc
 0.aac
+#EXT-X-CUE-OUT-CONT
+#EXTINF:5.76
+1.aac
 '''
 
 CUE_OUT_NO_DURATION_PLAYLIST = '''#EXTM3U
@@ -831,7 +836,10 @@ MAP_URI_PLAYLIST_WITH_BYTERANGE = '''#EXTM3U
 #EXT-X-INDEPENDENT-SEGMENTS
 #EXT-X-MAP:URI="main.mp4",BYTERANGE="812@0"
 #EXTINF:1,
-segment_link.mp4
+segment_link1.mp4
+#EXT-X-MAP:URI="main2.mp4",BYTERANGE="912@0"
+#EXTINF:1,
+segment_link2.mp4
 '''
 
 MULTIPLE_MAP_URI_PLAYLIST = '''#EXTM3U
@@ -874,6 +882,14 @@ http://media.example.com/entire.ts
 #EXT-X-ENDLIST
 '''
 
+IPTV_PLAYLIST_WITH_CUSTOM_TAGS = '''#EXTM3U
+#EXTVLCOPT:video-filter=invert
+#EXTGRP:ExtGroup1
+#EXTINF:-1 timeshift="0" catchup-days="7" catchup-type="flussonic" tvg-id="channel1" group-title="Group1",Channel1
+#EXTVLCOPT:param2=value2
+http://str00.iptv.domain/7331/mpegts?token=longtokenhere
+'''
+
 LOW_LATENCY_DELTA_UPDATE_PLAYLIST = '''#EXTM3U
 # Following the example above, this playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=3&_HLS_report=../1M/waitForMSN.php&_HLS_report=../4M/waitForMSN.php&_HLS_skip=YES
 #EXT-X-TARGETDURATION:4
@@ -1151,4 +1167,36 @@ segment21.mp4
 #EXT-X-DATERANGE:ID="Q"
 '''
 
+BITRATE_PLAYLIST = '''
+#EXTM3U
+#EXT-X-VERSION:3
+#EXT-X-INDEPENDENT-SEGMENTS
+#EXT-X-TARGETDURATION:10
+#EXT-X-MEDIA-SEQUENCE:55119
+#EXT-X-PROGRAM-DATE-TIME:2020-07-21T08:14:29.379Z
+#EXT-X-BITRATE:1674
+#EXTINF:9.600,
+test1.ts
+#EXT-X-BITRATE:1625
+#EXTINF:9.600,
+test2.ts
+'''
+
+CONTENT_STEERING_PLAYLIST = '''
+#EXTM3U
+#EXT-X-CONTENT-STEERING:SERVER-URI="/steering?video=00012",PATHWAY-ID="CDN-A"
+#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="A",NAME="English",DEFAULT=YES,URI="eng.m3u8",LANGUAGE="en"
+#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="B",NAME="ENGLISH",DEFAULT=YES,URI="https://b.example.com/content/videos/video12/eng.m3u8",LANGUAGE="en"
+#EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="A",PATHWAY-ID="CDN-A"
+low/video.m3u8
+#EXT-X-STREAM-INF:BANDWIDTH=7680000,AUDIO="A",PATHWAY-ID="CDN-A"
+hi/video.m3u8
+#EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="B",PATHWAY-ID="CDN-B"
+https://backup.example.com/content/videos/video12/low/video.m3u8
+#EXT-X-STREAM-INF:BANDWIDTH=7680000,AUDIO="B",PATHWAY-ID="CDN-B"
+https://backup.example.com/content/videos/video12/hi/video.m3u8
+#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=193350,CODECS="avc1.4d001f",URI="video-1200k-iframes.m3u8",PATHWAY-ID="CDN-A"
+#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=193350,CODECS="avc1.4d001f",URI="https://backup.example.com/content/videos/video12/video-1200k-iframes.m3u8",PATHWAY-ID="CDN-B"
+'''
+
 del abspath, dirname, join
diff --git a/tests/playlists/relative-playlist.m3u8 b/tests/playlists/relative-playlist.m3u8
index 2315a89..206a65e 100644
--- a/tests/playlists/relative-playlist.m3u8
+++ b/tests/playlists/relative-playlist.m3u8
@@ -14,4 +14,8 @@
 ../../entire3.ts
 #EXTINF:5220,
 entire4.ts
+#EXTINF:5220,
+//entire5.ts
+#EXTINF:5220,
+.//entire6.ts
 #EXT-X-ENDLIST
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 0167fff..768512c 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -4,10 +4,7 @@
 # license that can be found in the LICENSE file.
 
 import os
-try:
-    import urlparse as url_parser
-except ImportError:
-    import urllib.parse as url_parser
+import urllib.parse
 import m3u8
 import pytest
 import playlists
@@ -36,7 +33,7 @@ def test_load_should_create_object_from_uri():
 
 def test_load_should_remember_redirect():
     obj = m3u8.load(playlists.REDIRECT_PLAYLIST_URI)
-    urlparsed = url_parser.urlparse(playlists.SIMPLE_PLAYLIST_URI)
+    urlparsed = urllib.parse.urlparse(playlists.SIMPLE_PLAYLIST_URI)
     assert urlparsed.scheme + '://' + urlparsed.netloc + "/" == obj.base_uri
 
 
@@ -53,6 +50,10 @@ def test_load_should_create_object_from_file_with_relative_segments():
     expected_ts3_path = '../../entire3.ts'
     expected_ts4_abspath = '%s/entire4.ts' % base_uri
     expected_ts4_path = 'entire4.ts'
+    expected_ts5_abspath = '%s/entire5.ts' % base_uri
+    expected_ts5_path = '//entire5.ts'
+    expected_ts6_abspath = '%s/entire6.ts' % base_uri
+    expected_ts6_path = './/entire6.ts'
 
     assert isinstance(obj, m3u8.M3U8)
     assert expected_key_path == obj.keys[0].uri
@@ -65,16 +66,20 @@ def test_load_should_create_object_from_file_with_relative_segments():
     assert expected_ts3_abspath == obj.segments[2].absolute_uri
     assert expected_ts4_path == obj.segments[3].uri
     assert expected_ts4_abspath == obj.segments[3].absolute_uri
+    assert expected_ts5_path == obj.segments[4].uri
+    assert expected_ts5_abspath == obj.segments[4].absolute_uri
+    assert expected_ts6_path == obj.segments[5].uri
+    assert expected_ts6_abspath == obj.segments[5].absolute_uri
 
 
 def test_load_should_create_object_from_uri_with_relative_segments():
     obj = m3u8.load(playlists.RELATIVE_PLAYLIST_URI)
-    urlparsed = url_parser.urlparse(playlists.RELATIVE_PLAYLIST_URI)
+    urlparsed = urllib.parse.urlparse(playlists.RELATIVE_PLAYLIST_URI)
     base_uri = os.path.normpath(urlparsed.path + '/..')
     prefix = urlparsed.scheme + '://' + urlparsed.netloc
-    expected_key_abspath = '%s%s/key.bin' % (prefix, os.path.normpath(base_uri + '/..'))
+    expected_key_abspath = '%s%skey.bin' % (prefix, os.path.normpath(base_uri + '/..') + '/')
     expected_key_path = '../key.bin'
-    expected_ts1_abspath = '%s/entire1.ts' % (prefix)
+    expected_ts1_abspath = '%s%sentire1.ts' % (prefix, '/')
     expected_ts1_path = '/entire1.ts'
     expected_ts2_abspath = '%s%sentire2.ts' % (prefix, os.path.normpath(base_uri + '/..') + '/')
     expected_ts2_path = '../entire2.ts'
@@ -82,6 +87,10 @@ def test_load_should_create_object_from_uri_with_relative_segments():
     expected_ts3_path = '../../entire3.ts'
     expected_ts4_abspath = '%s%sentire4.ts' % (prefix, base_uri + '/')
     expected_ts4_path = 'entire4.ts'
+    expected_ts5_abspath = '%s%sentire5.ts' % (prefix, '//')
+    expected_ts5_path = '//entire5.ts'
+    expected_ts6_abspath = '%s%sentire6.ts' % (prefix, os.path.normpath(base_uri + '/.') + '//')
+    expected_ts6_path = './/entire6.ts'
 
     assert isinstance(obj, m3u8.M3U8)
     assert expected_key_path == obj.keys[0].uri
@@ -94,6 +103,10 @@ def test_load_should_create_object_from_uri_with_relative_segments():
     assert expected_ts3_abspath == obj.segments[2].absolute_uri
     assert expected_ts4_path == obj.segments[3].uri
     assert expected_ts4_abspath == obj.segments[3].absolute_uri
+    assert expected_ts5_path == obj.segments[4].uri
+    assert expected_ts5_abspath == obj.segments[4].absolute_uri
+    assert expected_ts6_path == obj.segments[5].uri
+    assert expected_ts6_abspath == obj.segments[5].absolute_uri
 
 
 def test_there_should_not_be_absolute_uris_with_loads():
diff --git a/tests/test_model.py b/tests/test_model.py
index 55b9127..a329495 100755
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -689,10 +689,10 @@ def test_multiple_map_attributes():
     assert obj.segments[2].init_section.uri == 'init3.mp4'
 
 
-def test_dump_should_include_multiple_map_attributes():
+def test_dump_should_include_multiple_map_attributes(tmpdir):
     obj = m3u8.M3U8(playlists.MULTIPLE_MAP_URI_PLAYLIST)
 
-    output = obj.dump('/tmp/d.m3u8')
+    output = obj.dump(str(tmpdir.join('d.m3u8')))
     output = obj.dumps().strip()
     assert output.count('#EXT-X-MAP:URI="init1.mp4"') == 1
     assert output.count('#EXT-X-MAP:URI="init3.mp4"') == 1
@@ -1017,12 +1017,15 @@ def test_medialist_uri_method():
 
 def test_segment_map_uri_attribute():
     obj = m3u8.M3U8(playlists.MAP_URI_PLAYLIST)
-    assert obj.segment_map['uri'] == "fileSequence0.mp4"
+    assert obj.segment_map[0].uri == "fileSequence0.mp4"
 
 
 def test_segment_map_uri_attribute_with_byterange():
     obj = m3u8.M3U8(playlists.MAP_URI_PLAYLIST_WITH_BYTERANGE)
-    assert obj.segment_map['uri'] == "main.mp4"
+    assert obj.segment_map[0].uri == "main.mp4"
+    assert obj.segment_map[0].byterange == "812@0"
+    assert obj.segment_map[1].uri == "main2.mp4"
+    assert obj.segment_map[1].byterange == "912@0"
 
 
 def test_start_with_negative_offset():
@@ -1360,7 +1363,37 @@ def test_add_skip():
 
     assert result == expected
 
+def test_content_steering():
+    obj = m3u8.M3U8(playlists.CONTENT_STEERING_PLAYLIST)
 
+    expected_content_steering_tag = '#EXT-X-CONTENT-STEERING:SERVER-URI="/steering?video=00012",PATHWAY-ID="CDN-A"'
+    result = obj.dumps().strip()
+
+    assert expected_content_steering_tag in result
+
+def test_add_content_steering():
+    obj = m3u8.ContentSteering(
+        '',
+        '/steering?video=00012',
+        'CDN-A'
+    )
+
+    expected = '#EXT-X-CONTENT-STEERING:SERVER-URI="/steering?video=00012",PATHWAY-ID="CDN-A"'
+    result = obj.dumps().strip()
+
+    assert result == expected
+
+def test_content_steering_base_path_update():
+    obj = m3u8.M3U8(playlists.CONTENT_STEERING_PLAYLIST)
+    obj.base_path = "https://another.example.com/"
+
+    assert '#EXT-X-CONTENT-STEERING:SERVER-URI="https://another.example.com/steering?video=00012",PATHWAY-ID="CDN-A"' in obj.dumps().strip()
+
+def test_add_content_steering_base_uri_update():
+    obj = m3u8.M3U8(playlists.CONTENT_STEERING_PLAYLIST)
+    obj.base_uri = "https://yet-another.example.com/"
+
+    assert obj.content_steering.absolute_uri == "https://yet-another.example.com/steering?video=00012"
 # custom asserts
 
 
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 877d8ab..7c483d9 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -2,11 +2,14 @@
 # Copyright 2014 Globo.com Player authors. All rights reserved.
 # Use of this source code is governed by a MIT License
 # license that can be found in the LICENSE file.
+import re
 
 import m3u8
 import playlists
 import pytest
-from m3u8.parser import cast_date_time, ParseError
+from m3u8.parser import cast_date_time, ParseError, save_segment_custom_value, _parse_simple_parameter_raw_value, \
+    get_segment_custom_value
+
 
 def test_should_parse_simple_playlist_from_string():
     data = m3u8.parse(playlists.SIMPLE_PLAYLIST)
@@ -323,12 +326,12 @@ def test_commaless_extinf_strict():
 
 def test_should_parse_segment_map_uri():
     data = m3u8.parse(playlists.MAP_URI_PLAYLIST)
-    assert data['segment_map']['uri'] == "fileSequence0.mp4"
+    assert data['segment_map'][0]['uri'] == "fileSequence0.mp4"
 
 
 def test_should_parse_segment_map_uri_with_byterange():
     data = m3u8.parse(playlists.MAP_URI_PLAYLIST_WITH_BYTERANGE)
-    assert data['segment_map']['uri'] == "main.mp4"
+    assert data['segment_map'][0]['uri'] == "main.mp4"
 
 
 def test_should_parse_multiple_map_attributes():
@@ -400,11 +403,14 @@ def test_simple_playlist_with_discontinuity_sequence():
     data = m3u8.parse(playlists.SIMPLE_PLAYLIST_WITH_DISCONTINUITY_SEQUENCE)
     assert data['discontinuity_sequence'] == 123
 
+
 def test_simple_playlist_with_custom_tags():
-    def get_movie(line, data, lineno):
-        custom_tag = line.split(':')
-        if len(custom_tag) == 2:
-            data['movie'] = custom_tag[1].strip()
+    def get_movie(line, lineno, data, segment):
+        if line.startswith('#EXT-X-MOVIE'):
+            custom_tag = line.split(':')
+            if len(custom_tag) == 2:
+                data['movie'] = custom_tag[1].strip()
+                return True
 
     data = m3u8.parse(playlists.SIMPLE_PLAYLIST_WITH_CUSTOM_TAGS, strict=False, custom_tags_parser=get_movie)
     assert data['movie'] == 'million dollar baby'
@@ -413,6 +419,68 @@ def test_simple_playlist_with_custom_tags():
     assert ['http://media.example.com/entire.ts'] == [c['uri'] for c in data['segments']]
     assert [5220] == [c['duration'] for c in data['segments']]
 
+
+def test_iptv_playlist_with_custom_tags():
+    def parse_iptv_attributes(line, lineno, data, state):
+        # Customize parsing #EXTINF
+        if line.startswith('#EXTINF'):
+            chunks = line.replace('#EXTINF' + ':', '').split(',', 1)
+            if len(chunks) == 2:
+                duration_and_props, title = chunks
+            elif len(chunks) == 1:
+                duration_and_props = chunks[0]
+                title = ''
+
+            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
+
+            save_segment_custom_value(state, 'extinf_props', additional_props)
+
+            state['expect_segment'] = True
+            return True
+
+        # Parse #EXTGRP
+        if line.startswith('#EXTGRP'):
+            _, value = _parse_simple_parameter_raw_value(line, str)
+            save_segment_custom_value(state, 'extgrp', value)
+            state['expect_segment'] = True
+            return True
+
+        # Parse #EXTVLCOPT
+        if line.startswith('#EXTVLCOPT'):
+            _, value = _parse_simple_parameter_raw_value(line, str)
+
+            existing_opts = get_segment_custom_value(state, 'vlcopt', [])
+            existing_opts.append(value)
+            save_segment_custom_value(state, 'vlcopt', existing_opts)
+
+            state['expect_segment'] = True
+            return True
+
+    data = m3u8.parse(playlists.IPTV_PLAYLIST_WITH_CUSTOM_TAGS, strict=False, custom_tags_parser=parse_iptv_attributes)
+
+    assert ['Channel1'] == [c['title'] for c in data['segments']]
+    assert data['segments'][0]['uri'] == 'http://str00.iptv.domain/7331/mpegts?token=longtokenhere'
+    assert data['segments'][0]['custom_parser_values']['extinf_props']['tvg-id'] == 'channel1'
+    assert data['segments'][0]['custom_parser_values']['extinf_props']['group-title'] == 'Group1'
+    assert data['segments'][0]['custom_parser_values']['extinf_props']['catchup-days'] == '7'
+    assert data['segments'][0]['custom_parser_values']['extinf_props']['catchup-type'] == 'flussonic'
+    assert data['segments'][0]['custom_parser_values']['extgrp'] == 'ExtGroup1'
+    assert data['segments'][0]['custom_parser_values']['vlcopt'] == ['video-filter=invert', 'param2=value2']
+
+
 def test_master_playlist_with_frame_rate():
     data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_FRAME_RATE)
     playlists_list = list(data['playlists'])
@@ -561,3 +629,26 @@ def test_delta_playlist_daterange_skipping():
     data = m3u8.parse(playlists.DELTA_UPDATE_SKIP_DATERANGES_PLAYLIST)
     assert data['skip']['recently_removed_dateranges'] == "1"
     assert data['server_control']['can_skip_dateranges'] == "YES"
+
+def test_bitrate():
+    data = m3u8.parse(playlists.BITRATE_PLAYLIST)
+    assert data['segments'][0]['bitrate'] == '1674'
+    assert data['segments'][1]['bitrate'] == '1625'
+
+def test_content_steering():
+    data = m3u8.parse(playlists.CONTENT_STEERING_PLAYLIST)
+    assert data['content_steering']['server_uri'] == '/steering?video=00012'
+    assert data['content_steering']['pathway_id'] == 'CDN-A'
+    assert data['playlists'][0]['stream_info']['pathway_id'] == 'CDN-A'
+    assert data['playlists'][1]['stream_info']['pathway_id'] == 'CDN-A'
+    assert data['playlists'][2]['stream_info']['pathway_id'] == 'CDN-B'
+    assert data['playlists'][3]['stream_info']['pathway_id'] == 'CDN-B'
+
+def test_cue_in_pops_scte35_data_and_duration():
+    data = m3u8.parse(playlists.CUE_OUT_ELEMENTAL_PLAYLIST)
+    assert data['segments'][9]['cue_in'] == True
+    assert data['segments'][9]['scte35'] == '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg=='
+    assert data['segments'][9]['scte35_duration'] == '50'
+    assert data['segments'][10]['cue_in'] == False
+    assert data['segments'][10]['scte35'] == None
+    assert data['segments'][10]['scte35_duration'] == None