Decoder for Master and Media Playlists of HTTP Live Streaming using Decodable
protocol.
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:
segments
property contains array of MediaSegment
structs 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)comments
property 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:
streams
property contains array of VariantStream
structs with parsed #EXT-X-STREAM-INF
tag 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.
snakeCase
It's default strategy to convert playlist tags and attribute names to snake case.
-
with _
.For example: #EXT-X-TARGETDURATION
becomes ext_x_targetduration
.
camelCase
Converting playlist tag and attribute names to camel case.
-
-
.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"
)
]
)
custom((_ key: String) -> String)
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.
hex
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]
base64
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: ...
}
)
Xcode > File > Add Packages...
https://github.com/ikhvorost/M3U8Decoder.git
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.