Swift: 5.10, 5.9, 5.8, 5.7 Platforms: iOS, macOS, tvOS, visionOS, watchOS Swift Package Manager: compatible Build codecov Swift Doc Coverage

Donate

M3U8Decoder: Flexible M3U8 playlist parsing for Swift.

M3U8Decoder

使用 Decodable 协议对 HTTP Live Streaming 的 Master 和 Media 播放列表进行解码。

概述

下面的示例展示了如何从提供的文本中解码一个简单的 Media 播放列表。 该类型遵循 Decodable 协议,因此可以使用 M3U8Decoder 实例进行解码。

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)

其中

打印

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 还可以同步和异步(async/await)地从 DataURL 实例进行解码。 例如,通过 URL 解码 Master 播放列表

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)

其中

打印

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"
    ),
    ...
  ]
)

Key 解码策略

用于在解码之前自动更改键值的策略。

snakeCase

这是将播放列表标签和属性名称转换为蛇形命名法的**默认**策略。

  1. 将键转换为小写。
  2. _ 替换所有 -

例如:#EXT-X-TARGETDURATION 变为 ext_x_targetduration

camelCase

将播放列表标签和属性名称转换为驼峰命名法。

  1. 将键转换为小写。
  2. 将每个 - 后面的单词的首字母大写。
  3. 删除所有 -

例如:#EXT-X-TARGETDURATION 变为 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)

打印

MasterPlaylist(
  extm3u: true, 
  extXVersion: 7, 
  extXIndependentSegments: true, 
  extXMedia: [
    Media(
      type: "CLOSED-CAPTIONS", 
      groupId: "cc", 
      name: "SERVICE1", 
      language: "en", 
      instreamId: "SERVICE1"
    )
  ]
)

custom((_ key: String) -> String)

提供从播放列表中的标签或属性名称到提供的函数指定的键的自定义转换。

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)

打印

MasterPlaylist(
  m3u: true, 
  version: 7, 
  independent_segments: true, 
  media: [
    Media(
      type: "CLOSED-CAPTIONS", 
      group_id: "cc", 
      name: "SERVICE1", 
      language: "en", 
      instream_id: "SERVICE1"
    )
  ]
)

Data 解码策略

用于解码 Data 值的策略。

hex

从十六进制字符串(例如 0xa2c4f622...)解码 Data。 这是默认策略。

例如,使用 IV 属性解码 #EXT-X-KEY 标签,其中数据以十六进制字符串表示

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

从 Base64 编码的字符串解码 Data

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!

预定义类型

这里有一个预定义类型列表(带有 snakeCase 键编码策略),适用于 HTTP Live Streaming 文档中的所有 master/media 标签和属性,可用于解码播放列表。

注意:这些类型的实现您可以查看 MasterPlaylist.swiftMediaPlaylist.swift,但无论如何,您可以创建和使用您自己的类型来解码您的播放列表。

类型 标签/属性 描述
EXT_X_MAP #EXT-X-MAP:<attribute-list> EXT-X-MAP 标签指定如何获取解析适用 Media Segments 所需的 Media Initialization Section。
EXT_X_KEY #EXT-X-KEY:<attribute-list>
#EXT_X_SESSION_KEY:<attribute-list>
Media Segments 可能会被加密。 EXT-X-KEY/EXT_X_SESSION_KEY 标签指定如何解密它们。
EXT_X_DATERANGE #EXT-X-DATERANGE:<attribute-list> EXT-X-DATERANGE 标签将日期范围(即,由起始和结束日期定义的时间范围)与一组属性值对相关联。
EXTINF #EXTINF:<duration>,[<title>] EXTINF 标签指定 Media Segment 的持续时间。
EXT_X_BYTERANGE #EXT-X-BYTERANGE:<n>[@<o>]
BYTERANGE=<n>[@<o>]
EXT-X-BYTERANGE 标签指示 Media Segment 是由其 URI 标识的资源的子范围。
EXT_X_SESSION_DATA #EXT-X-SESSION-DATA:<attribute-list> EXT-X-SESSION-DATA 标签允许在 Master 播放列表中携带任意会话数据。
EXT_X_START #EXT-X-START:<attribute-list> EXT-X-START 标签指示播放播放列表的首选起始点。
EXT_X_MEDIA #EXT-X-MEDIA:<attribute-list> EXT-X-MEDIA 标签用于关联包含相同内容的备用呈现的 Media 播放列表。
EXT_X_STREAM_INF #EXT-X-STREAM-INF:<attribute-list> EXT-X-STREAM-INF 标签指定一个 Variant Stream,它是一组可以组合在一起播放 presentation 的 Renditions。
EXT_X_I_FRAME_STREAM_INF #EXT-X-I-FRAME-STREAM-INF:<attribute-list> EXT-X-I-FRAME-STREAM-INF 标签标识包含多媒体 presentation 的 I 帧的 Media 播放列表文件。
RESOLUTION RESOLUTION=<width>x<height> 该值是一个十进制分辨率,描述了显示 Variant Stream 中所有视频的最佳像素分辨率。
[String] CODECS="codec1,codec2,..." 该值是一个带引号的字符串,包含一个逗号分隔的格式列表,其中每个格式指定 Variant Stream 指定的一个或多个 Renditions 中存在的媒体样本类型。
MediaSegment #EXTINF
#EXT-X-BYTERANGE
#EXT-X-DISCONTINUITY
#EXT-X-KEY
#EXT-X-MAP
#EXT-X-PROGRAM-DATE-TIME
#EXT-X-DATERANGE
<URI>
指定一个 Media Segment。
VariantStream #EXT-X-STREAM-INF
<URI>
指定一个 Variant Stream。

自定义标签和属性

您可以为自定义标签或属性指定带有任何键解码策略的类型,以解码您的非标准播放列表

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)

打印

CustomPlaylist(
  ext_custom_tag1: 1, 
  ext_custom_tag2: CustomAttributes(
    value1: 1,
    value2: "Text"
  ), 
  ext_custom_array: [1, 2, 3]
)

自定义解析

M3U8Decoder 自动解析播放列表中任何标签的所有属性及其值,但是如果您使用复杂的格式(例如 json 等),则可以使用 parseHandler 回调通过您的代码解析属性

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)

打印

Playlist(
  ext_custom_tag: CustomTag(duration: 10.3, title: "Title", id: 12345)
)

Combine

M3U8Decoder 支持 TopLevelDecoder 协议,可以与 Combine 框架一起使用

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

  1. 选择 Xcode > File > Add Packages...
  2. 添加包仓库:https://github.com/ikhvorost/M3U8Decoder.git
  3. 在您的源文件中导入包:import M3U8Decoder

Swift Package

M3U8Decoder 包依赖项添加到您的 Package.swift 文件中

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 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。

Donate