Decoder for Master and Media Playlists of HTTP Live Streaming using Decodable protocol.
- Overview
- Key decoding strategy
- Data decoding strategy
- Predefined types
- Custom tags and attributes
- Custom parsing
- Combine
- Installation
- License
The example below shows how to decode a simple Media Playlist from a provided text. The type adopts Decodable so that it’s decodable using a M3U8Decoder instance.
import M3U8Decoder
struct MediaPlaylist: Decodable {
  let extm3u: Bool
  let ext_x_version: Int
  let ext_x_targetduration: Int
  let ext_x_media_sequence: Int
  let segments: [MediaSegment]
  let comments: [String]
}
let m3u8 = """
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:10
# Created with Unified Streaming Platform
#EXT-X-MEDIA-SEQUENCE:2680
#EXTINF:13.333,Sample artist - Sample title
http://example.com/low.m3u8
"""
let playlist = try M3U8Decoder().decode(MediaPlaylist.self, from: m3u8)
print(playlist)Where:
- Predefined segmentsproperty contains array ofMediaSegmentstructs with all parsed media segments tags and url, such as:#EXTINF,#EXT-X-BYTERANGE,#EXT-X-DISCONTINUITY,#EXT-X-KEY,#EXT-X-MAP#EXT-X-PROGRAM-DATE-TIME,#EXT-X-DATERANGE(For more info see Predefined types)
- Predefined commentsproperty contains all lines that begin with#.
Prints:
MediaPlaylist(
  extm3u: true, 
  ext_x_version: 7, 
  ext_x_targetduration: 10, 
  ext_x_media_sequence: 2680, 
  segments: [
    M3U8Decoder.MediaSegment(
      extinf: M3U8Decoder.EXTINF(
        duration: 13.333, 
        title: Optional("Sample artist - Sample title")
      ),
      ext_x_byterange: nil,
      ext_x_discontinuity: nil, 
      ext_x_key: nil, 
      ext_x_map: nil, 
      ext_x_program_date_time: nil, 
      ext_x_daterange: nil, 
      uri: "http://example.com/low.m3u8"
    )
  ], 
  comments: ["Created with Unified Streaming Platform"]
)M3U8Decoder can also decode from Data and URL instances both synchonously and asynchronously (async/await). For instance, decoding Master Playlist by url:
import M3U8Decoder
struct MasterPlaylist: Decodable {
  let extm3u: Bool
  let ext_x_version: Int
  let ext_x_independent_segments: Bool
  let ext_x_media: [EXT_X_MEDIA]
  let ext_x_i_frame_stream_inf: [EXT_X_I_FRAME_STREAM_INF]
  let streams: [VariantStream]
}
let url = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")!
let playlist = try M3U8Decoder().decode(MasterPlaylist.self, from: url)
print(playlist)Where:
- Predefined streamsproperty contains array ofVariantStreamstructs with parsed#EXT-X-STREAM-INFtag and url (For more info see Predefined types)
Prints:
MasterPlaylist(
  extm3u: true, 
  ext_x_version: 6, 
  ext_x_independent_segments: true, 
  ext_x_media: [
    M3U8Decoder.EXT_X_MEDIA(
      type: "AUDIO", 
      group_id: "aud1", 
      name: "English", 
      language: Optional("en"), 
      assoc_language: nil, 
      autoselect: Optional(true), 
      default: Optional(true), 
      instream_id: nil, 
      channels: Optional("2"), 
      forced: nil, 
      uri: Optional("a1/prog_index.m3u8"), 
      characteristics: nil
    ),
    ...
  ], 
  ext_x_i_frame_stream_inf: [
    M3U8Decoder.EXT_X_I_FRAME_STREAM_INF(
      bandwidth: 187492, 
      average_bandwidth: Optional(183689), 
      codecs: ["avc1.64002a"], 
      resolution: Optional(M3U8Decoder.RESOLUTION(width: 1920, height: 1080)), 
      hdcp_level: nil, 
      video: nil, 
      uri: "v7/iframe_index.m3u8"
    ),
    ...
  ], 
  streams: [
    M3U8Decoder.VariantStream(
      ext_x_stream_inf: M3U8Decoder.EXT_X_STREAM_INF(
        bandwidth: 2177116, 
        average_bandwidth: Optional(2168183), 
        codecs: ["avc1.640020", "mp4a.40.2"], 
        resolution: Optional(M3U8Decoder.RESOLUTION(width: 960, height: 540)), 
        frame_rate: Optional(60.0), 
        hdcp_level: nil, 
        audio: Optional("aud1"), 
        video: nil, 
        subtitles: Optional("sub1"), 
        closed_captions: Optional("cc1")
      ),
      uri: "v5/prog_index.m3u8"
    ),
    ...
  ]
)The strategy to use for automatically changing the value of keys before decoding.
It's default strategy to convert playlist tags and attribute names to snake case.
- Converting keys to lower case.
- Replaces all -with_.
For example: #EXT-X-TARGETDURATION becomes ext_x_targetduration.
Converting playlist tag and attribute names to camel case.
- Converting keys to lower case.
- Capitalises the word starting after each -
- Removes all -.
For example: #EXT-X-TARGETDURATION becomes extXTargetduration.
struct Media: Decodable {
  let type: String
  let groupId: String
  let name: String
  let language: String
  let instreamId: String
}
struct MasterPlaylist: Decodable {
  let extm3u: Bool
  let extXVersion: Int
  let extXIndependentSegments: Bool
  let extXMedia: [Media]
}
let m3u8 = """
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="SERVICE1",LANGUAGE="en",INSTREAM-ID="SERVICE1"
"""
let decoder = M3U8Decoder()
decoder.keyDecodingStrategy = .camelCase
let playlist = try decoder.decode(MasterPlaylist.self, from: m3u8)
print(playlist)Prints:
MasterPlaylist(
  extm3u: true, 
  extXVersion: 7, 
  extXIndependentSegments: true, 
  extXMedia: [
    Media(
      type: "CLOSED-CAPTIONS", 
      groupId: "cc", 
      name: "SERVICE1", 
      language: "en", 
      instreamId: "SERVICE1"
    )
  ]
)Provide a custom conversion from a tag or attribute name in the playlist to the keys specified by the provided function.
struct Media: Decodable {
  let type: String
  let group_id: String
  let name: String
  let language: String
  let instream_id: String
}
struct MasterPlaylist: Decodable {
  let m3u: Bool
  let version: Int
  let independent_segments: Bool
  let media: [Media]
}
let m3u8 = """
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="SERVICE1",LANGUAGE="en",INSTREAM-ID="SERVICE1"
"""
let decoder = M3U8Decoder()
// `EXT-X-INDEPENDENT-SEGMENTS` becomes `independent_segments`
decoder.keyDecodingStrategy = .custom { key in
  key
    .lowercased()
    .replacingOccurrences(of: "ext", with: "")
    .replacingOccurrences(of: "-x-", with: "")
    .replacingOccurrences(of: "-", with: "_")
}
let playlist = try decoder.decode(MasterPlaylist.self, from: m3u8)
print(playlist)Prints:
MasterPlaylist(
  m3u: true, 
  version: 7, 
  independent_segments: true, 
  media: [
    Media(
      type: "CLOSED-CAPTIONS", 
      group_id: "cc", 
      name: "SERVICE1", 
      language: "en", 
      instream_id: "SERVICE1"
    )
  ]
)The strategies to use for decoding Data values.
Decode the Data from a hex string (e.g. 0xa2c4f622...). This is the default strategy.
For instance, decoding #EXT-X-KEY tag with IV attribute where data is represented in hex string:
struct MediaPlaylist: Decodable {
  let extm3u: Bool
  let ext_x_version: Int
  let segments: [MediaSegment]
}
let m3u8 = """
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://vod.domain.com/fairplay/d1acadbf70824d178601c2e55675b3b3",IV=0X99b74007b6254e4bd1c6e03631cad15b
#EXTINF:10,
http://example.com/low.m3u8
"""
let playlist = try M3U8Decoder().decode(MediaPlaylist.self, from: m3u8)
if let iv = playlist.segments.first?.ext_x_key?.iv {
  print(iv.map { $0 } )
}
// Prints: [153, 183, 64, 7, 182, 37, 78, 75, 209, 198, 224, 54, 49, 202, 209, 91]Decoding the Data from a Base64-encoded string:
struct Playlist: Decodable {
  let extm3u: Bool
  let ext_x_version: Int
  let ext_data: Data
}
let m3u8 = """
#EXTM3U
#EXT-X-VERSION:7
#EXT-DATA:SGVsbG8gQmFzZTY0IQ==
"""
let decoder = M3U8Decoder()
decoder.dataDecodingStrategy = .base64
let playlist = try decoder.decode(Playlist.self, from: m3u8)
let text = String(data: playlist.ext_data, encoding: .utf8)!
print(text) // Prints: Hello Base64!There are a list of predefined types (with snakeCase key coding strategy) for all master/media tags and attributes from of HTTP Live Streaming document that can be used to decode playlists.
NOTE: Implementations of these types you can look at MasterPlaylist.swift and MediaPlaylist.swift but anyway you can make and use your own ones to decode your playlists.
| Type | Tag/Attribute | Description | 
|---|---|---|
| EXT_X_MAP | #EXT-X-MAP:<attribute-list> | The EXT-X-MAP tag specifies how to obtain the Media Initialization Section required to parse the applicable Media Segments. | 
| EXT_X_KEY | #EXT-X-KEY:<attribute-list>#EXT_X_SESSION_KEY:<attribute-list> | Media Segments MAY be encrypted. The EXT-X-KEY/EXT_X_SESSION_KEY tag specifies how to decrypt them. | 
| EXT_X_DATERANGE | #EXT-X-DATERANGE:<attribute-list> | The EXT-X-DATERANGE tag associates a Date Range (i.e., a range o time defined by a starting and ending date) with a set of attribute value pairs. | 
| EXTINF | #EXTINF:<duration>,[<title>] | The EXTINF tag specifies the duration of a Media Segment. | 
| EXT_X_BYTERANGE | #EXT-X-BYTERANGE:<n>[@<o>]BYTERANGE=<n>[@<o>] | The EXT-X-BYTERANGE tag indicates that a Media Segment is a sub-range of the resource identified by its URI. | 
| EXT_X_SESSION_DATA | #EXT-X-SESSION-DATA:<attribute-list> | The EXT-X-SESSION-DATA tag allows arbitrary session data to be carried in a Master Playlist. | 
| EXT_X_START | #EXT-X-START:<attribute-list> | The EXT-X-START tag indicates a preferred point at which to start playing a Playlist. | 
| EXT_X_MEDIA | #EXT-X-MEDIA:<attribute-list> | The EXT-X-MEDIA tag is used to relate Media Playlists that contain alternative Renditions of the same content. | 
| EXT_X_STREAM_INF | #EXT-X-STREAM-INF:<attribute-list> | The EXT-X-STREAM-INF tag specifies a Variant Stream, which is a set of Renditions that can be combined to play the presentation. | 
| EXT_X_I_FRAME_STREAM_INF | #EXT-X-I-FRAME-STREAM-INF:<attribute-list> | The EXT-X-I-FRAME-STREAM-INF tag identifies a Media Playlist file containing the I-frames of a multimedia presentation. | 
| RESOLUTION | RESOLUTION=<width>x<height> | The value is a decimal-resolution describing the optimal pixel resolution at which to display all the video in the Variant Stream. | 
| [String] | CODECS="codec1,codec2,..." | The value is a quoted-string containing a comma-separated list of formats, where each format specifies a media sample type that is present in one or more Renditions specified by the Variant Stream. | 
| MediaSegment | #EXTINF#EXT-X-BYTERANGE#EXT-X-DISCONTINUITY#EXT-X-KEY#EXT-X-MAP#EXT-X-PROGRAM-DATE-TIME#EXT-X-DATERANGE<URI> | Specifies a Media Segment. | 
| VariantStream | #EXT-X-STREAM-INF<URI> | Specifies a Variant Stream. | 
You can specify your types for custom tags or attributes with any key decoding strategy to decode your non-standard playlists:
let m3u8 = """
#EXTM3U
#EXT-CUSTOM-TAG1:1
#EXT-CUSTOM-TAG2:VALUE1=1,VALUE2="Text"
#EXT-CUSTOM-ARRAY:1
#EXT-CUSTOM-ARRAY:2
#EXT-CUSTOM-ARRAY:3
"""
struct CustomAttributes: Decodable {
  let value1: Int
  let value2: String
}
struct CustomPlaylist: Decodable {
  let ext_custom_tag1: Int
  let ext_custom_tag2: CustomAttributes
  let ext_custom_array: [Int]
}
let playlist = try M3U8Decoder().decode(CustomPlaylist.self, from: m3u8)
print(playlist)Prints:
CustomPlaylist(
  ext_custom_tag1: 1, 
  ext_custom_tag2: CustomAttributes(
    value1: 1,
    value2: "Text"
  ), 
  ext_custom_array: [1, 2, 3]
)M3U8Decoder automatically parses all attributes with their values of any tag in the playlist but if you use a complex format (like json etc.) it's possible to parse attributes with your code using parseHandler callback:
let m3u8 =
#"""
#EXTM3U
#EXT-CUSTOM-TAG:{"duration": 10.3, "title": "Title", "id": 12345}
"""#
struct CustomTag: Decodable {
  let duration: Double
  let title: String
  let id: Int
}
struct Playlist: Decodable {
  let ext_custom_tag: CustomTag
}
let decoder = M3U8Decoder()
decoder.parseHandler = { (tag: String, attributes: String) -> M3U8Decoder.ParseAction in
  if tag == "EXT-CUSTOM-TAG" {
    do {
      if let data = attributes.data(using: .utf8) {
        let dict = try JSONSerialization.jsonObject(with: data)
        return .apply(dict)
      }
    }
    catch {
      print(error)
    }
  }
  return .parse
}
let playlist = try decoder.decode(Playlist.self, from: m3u8)
print(playlist)Prints:
Playlist(
  ext_custom_tag: CustomTag(duration: 10.3, title: "Title", id: 12345)
)
M3U8Decoder supports TopLevelDecoder protocol and can be used with Combine framework:
struct MasterPlaylist: Decodable {
  let extm3u: Bool
  let ext_x_version: Int
  let ext_x_independent_segments: Bool
  let ext_x_media: [EXT_X_MEDIA]
  let ext_x_i_frame_stream_inf: [EXT_X_I_FRAME_STREAM_INF]
  let streams: [VariantStream]
}
let url = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")!
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
  .map(\.data)
  .decode(type: MasterPlaylist.self, decoder: M3U8Decoder())
  .sink (
    receiveCompletion: { print($0) }, // Prints: finished
    receiveValue: { playlist in
      print(playlist) // Prints: MasterPlaylist(extm3u: true, ext_x_version: 6, ext_x_independent_segments: true, ext_x_media: ...
    }
  )- Select Xcode > File > Add Packages...
- Add package repository: https://github.com/ikhvorost/M3U8Decoder.git
- Import the package in your source files: import M3U8Decoder
Add M3U8Decoder package dependency to your Package.swift file:
let package = Package(
  ...
  dependencies: [
    .package(url: "https://github.com/ikhvorost/M3U8Decoder.git", from: "1.0.0")
  ],
  targets: [
    .target(name: "YourPackage",
      dependencies: [
        .product(name: "M3U8Decoder", package: "M3U8Decoder")
      ]
    ),
  ]
)M3U8Decoder is available under the MIT license. See the LICENSE file for more info.

