Skip to content

Commit

Permalink
Merge pull request #141 from Comcast/candidate/1.6.0
Browse files Browse the repository at this point in the history
Candidate/1.6.0
  • Loading branch information
theRealRobG authored Oct 4, 2024
2 parents 2867dbe + d687e31 commit 07a909b
Show file tree
Hide file tree
Showing 19 changed files with 1,876 additions and 207 deletions.
52 changes: 52 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Build and test

on:
pull_request:
branches: [ "develop", "develop_1.x", "main", "main_1.x" ]

jobs:
define-ios-device:
name: Get iOS simulator device to run iOS tests on
runs-on: macos-latest
outputs:
device: ${{ steps.ios.outputs.device }}
steps:
- id: ios
run: echo "device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`" >> "$GITHUB_OUTPUT"

build:
name: Build and Test mamba and mambaTVOS
runs-on: macos-latest
needs: define-ios-device
strategy:
matrix:
target:
- scheme: mamba
platform: iOS Simulator
device: ${{ needs.define-ios-device.outputs.device }}
- scheme: mambaTVOS
platform: tvOS Simulator
device: Apple TV
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build
env:
scheme: ${{ matrix.target.scheme }}
platform: ${{ matrix.target.platform }}
device: ${{ matrix.target.device }}
run: |
echo "scheme = $scheme"
echo "platform = $platform"
echo "device = $device"
xcodebuild build-for-testing -scheme "$scheme" -"workspace" "mamba.xcworkspace" -destination "platform=$platform,name=$device"
- name: Test
env:
scheme: ${{ matrix.target.scheme }}
platform: ${{ matrix.target.platform }}
device: ${{ matrix.target.device }}
run: |
echo "scheme = $scheme"
echo "platform = $platform"
echo "device = $device"
xcodebuild test-without-building -scheme "$scheme" -"workspace" "mamba.xcworkspace" -destination "platform=$platform,name=$device"
2 changes: 1 addition & 1 deletion mamba.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|

s.name = "mamba"
s.version = "1.5.0"
s.version = "1.6.0"
s.license = { :type => 'Apache License, Version 2.0',
:text => <<-LICENSE
Copyright 2017 Comcast Cable Communications Management, LLC
Expand Down
54 changes: 47 additions & 7 deletions mamba.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -372,16 +372,44 @@ fileprivate struct HLSPlaylistStructureConstructor {
var currentSegmentDuration: CMTime = CMTime.invalid
var discontinuity = false
let tagDescriptor = self.tagDescriptor(forTags: tags)

// figure out our media sequence start (defaults to 1 if not specified)
let mediaSequenceTags = tags.filter{ $0.tagDescriptor == PantosTag.EXT_X_MEDIA_SEQUENCE }
if mediaSequenceTags.count > 0 {
assert(mediaSequenceTags.count == 1, "Unexpected to have more than one media sequence")
if let startMediaSequence: MediaSequence = mediaSequenceTags.first?.value(forValueIdentifier: PantosValue.sequence) {
currentMediaSequence = startMediaSequence

// collect media sequence and skip tag (if they exist) as they impact the initial media sequence value
var mediaSequenceTag: HLSTag?
var skipTag: HLSTag?
for tag in tags {
switch tag.tagDescriptor {
case PantosTag.EXT_X_MEDIA_SEQUENCE: mediaSequenceTag = tag
case PantosTag.EXT_X_SKIP: skipTag = tag
case PantosTag.Location:
// Both the EXT-X-MEDIA-SEQUNCE and the EXT-X-SKIP tag are expected to occur before any Media Segments.
//
// For EXT-X-MEDIA-SEQUNCE section 4.4.3.2 indicates:
// The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.
//
// For EXT-X-SKIP section 4.4.5.2 indicates:
// A server produces a Playlist Delta Update (Section 6.2.5.1), by replacing tags earlier than the
// Skip Boundary with an EXT-X-SKIP tag. When replacing Media Segments, the EXT-X-SKIP tag replaces
// the segment URI lines and all Media Segment Tags tags that are applied to those segments.
//
// Exiting early at the first Location helps us avoid having to loop through the entire playlist when we
// know that the tags we're looking for MUST NOT exist.
break
default: continue
}
}


// figure out our media sequence start (defaults to 0 if not specified)
if let startMediaSequence: MediaSequence = mediaSequenceTag?.value(forValueIdentifier: PantosValue.sequence) {
currentMediaSequence = startMediaSequence
}

// account for any skip tag (since a delta update replaces all segments earlier than the skip boundary, the
// SKIPPED-SEGMENTS value will effectively update the current media sequence value of the first segment, so safe
// to do this here and not within the looping through media group tags below).
if let skippedSegments: Int = skipTag?.value(forValueIdentifier: PantosValue.skippedSegments) {
currentMediaSequence += skippedSegments
}

// find the "header" portion by finding the first ".mediaSegment" scoped tag
let mediaStartIndexOptional = tags.firstIndex(where: { $0.scope() == .mediaSegment })

Expand Down
3 changes: 3 additions & 0 deletions mambaSharedFramework/HLSValidationIssue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public enum IssueDescription: String {

case HLSPlaylistRenditionGroupMatchingNAMELANGUAGEValidator = "A Playlist MAY contain multiple groups of the same TYPE in order to provide multiple encodings of each rendition. If it does so, each group of the same TYPE SHOULD contain corresponding members with the same NAME attribute, LANGUAGE attribute, and rendition."
case EXT_X_KEYValidator = "EXT-X-KEY If the encryption method is NONE, the URI, IV, KEYFORMAT and KEYFORMATVERSIONS attributes MUST NOT be present. If the encryption method is AES-128 or SAMPLE-AES, the URI attribute MUST be present."
case EXT_X_SESSION_KEYValidator = "All attributes defined for the EXT-X-KEY tag are also defined for the EXT-X-SESSION-KEY, except that the value of the METHOD attribute MUST NOT be NONE."
case EXT_X_SESSION_DATATagValidator = "Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI attribute, but not both."
case EXT_X_SESSION_DATAPlaylistValidator = "A Playlist MAY contain multiple EXT-X-SESSION-DATA tags with the same DATA-ID attribute. A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute."
case HLSPlaylistRenditionGroupMatchingPROGRAM_IDValidator = "Variant Playlists MUST contain an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag for each variant stream. Each tag identifying an encoding of the same presentation MUST have the same PROGRAM-ID attribute value."
case EXT_X_STREAM_INFRenditionGroupAUDIOValidator = "EXT-X-STREAM-INF - AUDIO The value is a quoted-string. It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is AUDIO."
case EXT_X_STREAM_INFRenditionGroupVIDEOValidator = "EXT-X-STREAM-INF - VIDEO The value is a quoted-string. It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist whose TYPE attribute is VIDEO."
Expand Down
227 changes: 224 additions & 3 deletions mambaSharedFramework/HLSValueTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,14 @@ public func ==(lhs: HLSMediaType, rhs: HLSMediaType) -> Bool {

/// Represents an encryption method
///
/// Can be initialized with a string "NONE" or "AES-128" or "SAMPLE-AES" for a valid value
/// Can be initialized with a string "NONE" or "AES-128" or "SAMPLE-AES" or "SAMPLE-AES-CTR" for a valid value
public struct HLSEncryptionMethodType: Equatable, FailableStringLiteralConvertible {
public let type: EncryptionMethod
public enum EncryptionMethod: String {
case None = "NONE"
case AES128 = "AES-128"
case SampleAES = "SAMPLE-AES"
case SampleAESCTR = "SAMPLE-AES-CTR"
}
public init?(string: String) {
self.init(encryption: string)
Expand All @@ -130,6 +131,59 @@ public func ==(lhs: HLSEncryptionMethodType, rhs: HLSEncryptionMethodType) -> Bo
return lhs.type == rhs.type
}

/// Represents a minimum required HDCP level needed to play content.
public enum HLSHDCPLevel: String, Equatable, FailableStringLiteralConvertible {
/// Indicates that the content does not require output copy protections.
case none = "NONE"
/// Indicates that the Variant Stream could fail to play unless the output is protected by High-bandwidth Digital
/// Content Protection (HDCP) Type 0 or equivalent.
case type0 = "TYPE-0"
/// Indicates that the Variant Stream could fail to play unless the output is protected by HDCP Type 1 or
/// equivalent.
case type1 = "TYPE-1"

public init?(string: String) {
self.init(rawValue: string)
}
}

/// Represents the dynamic range of the video.
///
/// This is represented by an enumeration where each case covers a group of similar opto-electronic transfer
/// characteristic functions that could have been used to encode the media file.
///
/// For example, `SDR` covers TransferCharacteristics code points 1, 6, 13, 14 and 15. More information on what each
/// code point represents can be found in _"Information technology - MPEG systems technologies - Part 8: Coding-_
/// _independent code points" ISO/IEC International Standard 23001-8, 2016_ [CICP].
public enum HLSVideoRange: String, Equatable, FailableStringLiteralConvertible {
/// The value MUST be SDR if the video in the Variant Stream is encoded using one of the following reference
/// opto-electronic transfer characteristic functions specified by the TransferCharacteristics code point: 1, 6, 13,
/// 14, 15. Note that different TransferCharacteristics code points can use the same transfer function.
case sdr = "SDR"
/// The value MUST be HLG if the video in the Variant Stream is encoded using a reference opto-electronic transfer
/// characteristic function specified by the TransferCharacteristics code point 18, or consists of such video mixed
/// with video qualifying as SDR.
case hlg = "HLG"
/// The value MUST be PQ if the video in the Variant Stream is encoded using a reference opto-electronic transfer
/// characteristic function specified by the TransferCharacteristics code point 16, or consists of such video mixed
/// with video qualifying as SDR or HLG.
case pq = "PQ"

public init?(string: String) {
self.init(rawValue: string)
}
}

/// Represents the format of the file referenced by `EXT-X-SESSION-DATA:URI`.
public enum HLSSessionDataFormat: String, Equatable, FailableStringLiteralConvertible {
case json = "JSON"
case raw = "RAW"

public init?(string: String) {
self.init(rawValue: string)
}
}

/// Represents a playlist type
///
/// Can be initialized with a string "EVENT" or "VOD" for a valid value
Expand Down Expand Up @@ -160,7 +214,6 @@ public func ==(lhs: HLSPlaylistType, rhs: HLSPlaylistType) -> Bool {
/// Represents a instreamId type
///
/// Can be initialized with a string "CC1" or "CC2" or "CC3" or "CC4" for a valid value

public enum HLSInstreamId: String, FailableStringLiteralConvertible {
case CC1 = "CC1"
case CC2 = "CC2"
Expand All @@ -173,7 +226,6 @@ public enum HLSInstreamId: String, FailableStringLiteralConvertible {

}


/// Represents a CLOSED-CAPTIONS
///
/// can be either a quoted-string or an enumerated-string with the value NONE for a valid value
Expand All @@ -189,6 +241,109 @@ public struct HLSClosedCaptions: FailableStringLiteralConvertible {
}
}

/// Represents CHANNELS
public struct HLSChannels: Equatable, FailableStringLiteralConvertible {
/// A count of audio channels, indicating the maximum number of independent, simultaneous audio channels present in
/// any Media Segment in the Rendition.
///
/// For example, an AC-3 5.1 Rendition would have a CHANNELS="6" attribute.
public let count: Int
/// Identifies the presence of spatial audio of some kind, for example, object-based audio, in the Rendition. The
/// Audio Coding Identifiers are codec-specific.
public let spatialAudioCodingIdentifiers: [String]
/// Supplementary indications of special channel usage that are necessary for informed selection and processing.
/// This parameter is an array of Special Usage Identifiers.
public let specialUsageIdentifiers: [SpecialUsageIdentifier]

public enum SpecialUsageIdentifier: RawRepresentable, Equatable {
/// The audio is binaural (either recorded or synthesized). It SHOULD NOT be dynamically spatialized. It is best
/// suited for delivery to headphones.
case binaural
/// The audio is pre-processed content that SHOULD NOT be dynamically spatialized. It is suitable to deliver to
/// either headphones or speakers.
case immersive
/// The audio is a downmix derivative of some other audio. If desired, the downmix may be used as a subtitute
/// for alternative Renditions in the same group with compatible attributes and a greater channel count. It MAY
/// be dynamically spatialized.
case downmix
/// The audio identifier is not recognized by this library; however, we provide the raw identifier string that
/// existed in the manifest.
case unrecognized(String)

public var rawValue: String {
switch self {
case .binaural: return "BINAURAL"
case .immersive: return "IMMERSIVE"
case .downmix: return "DOWNMIX"
case .unrecognized(let string): return string
}
}

public init?(rawValue: String) {
self.init(str: Substring(rawValue))
}

/// Allows `init` without having to allocate a new `String` object.
init(str: Substring) {
switch str {
case "BINAURAL": self = .binaural
case "IMMERSIVE": self = .immersive
case "DOWNMIX": self = .downmix
default: self = .unrecognized(String(str))
}
}
}

public init?(string: String) {
var count: Int?
var spatialAudioCodingIdentifiers: [String]?
var specialUsageIdentifiers: [SpecialUsageIdentifier]?
let enumeratedSplit = string.split(separator: "/").enumerated()
for (index, str) in enumeratedSplit {
switch index {
case 0: count = Self.parseChannelCount(str: str)
case 1: spatialAudioCodingIdentifiers = Self.parseSpatialAudioCodingIdentifiers(str: str)
case 2: specialUsageIdentifiers = Self.parseSpecialUsageIdentifiers(str: str)
default: break // In the future there may be more parameters defined.
}
}
// Count is required to have been parsed.
guard let count else {
return nil
}
self.count = count
self.spatialAudioCodingIdentifiers = spatialAudioCodingIdentifiers ?? []
self.specialUsageIdentifiers = specialUsageIdentifiers ?? []
}

public init(
count: Int,
spatialAudioCodingIdentifiers: [String],
specialUsageIdentifiers: [SpecialUsageIdentifier]
) {
self.count = count
self.spatialAudioCodingIdentifiers = spatialAudioCodingIdentifiers
self.specialUsageIdentifiers = specialUsageIdentifiers
}

private static func parseChannelCount(str: Substring) -> Int? {
Int(string: String(str))
}

private static func parseSpatialAudioCodingIdentifiers(str: Substring) -> [String] {
let split = str.split(separator: ",")
var identifiers = [String]()
for id in split where id != "-" {
identifiers.append(String(id))
}
return identifiers
}

private static func parseSpecialUsageIdentifiers(str: Substring) -> [SpecialUsageIdentifier] {
str.split(separator: ",").map { SpecialUsageIdentifier(str: $0) }
}
}

/// Represents a RFC6381 codec
///
/// We are currently not parsing these values further
Expand Down Expand Up @@ -266,4 +421,70 @@ public func ==(lhs: HLSCodecArray, rhs: HLSCodecArray) -> Bool {
return lhs.codecs == rhs.codecs
}

/// Represents information to assist in view presentation.
///
/// Indicates when video content in the Variant Stream requires specialized rendering to be properly displayed.
public struct HLSVideoLayout: Equatable, FailableStringLiteralConvertible {
/// Each specifier controls one aspect of the entry. That is, the specifiers are disjoint and the values for a
/// specifier are mutually exclusive.
public let layouts: [VideoLayoutIdentifier]
/// The client SHOULD assume that the order of entries reflects the most common presentation in the content.
///
/// For example, if the content is predominantly stereoscopic, with some brief sections that are monoscopic then the
/// Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-STEREO,CH-MONO"`. On the other hand, if the content
/// is predominantly monoscopic then the Multivariant Playlist SHOULD specify `REQ-VIDEO-LAYOUT="CH-MONO,CH-STEREO"`.
public let predominantLayout: VideoLayoutIdentifier

public enum VideoLayoutIdentifier: RawRepresentable, Equatable {
/// Monoscopic.
///
/// Indicates that a single image is present.
case chMono
/// Stereoscopic.
///
/// Indicates that both left and right eye images are present.
case chStereo
/// The video layout identifier is not recognized by this library; however, we provide the raw identifier string
/// that existed in the manifest.
case unrecognized(String)

public var rawValue: String {
switch self {
case .chMono: return "CH-MONO"
case .chStereo: return "CH-STEREO"
case .unrecognized(let string): return string
}
}

public init?(rawValue: String) {
self.init(str: Substring(rawValue))
}

init(str: Substring) {
switch str {
case "CH-MONO": self = .chMono
case "CH-STEREO": self = .chStereo
default: self = .unrecognized(String(str))
}
}
}

public init?(string: String) {
let layouts = string.split(separator: ",").map { VideoLayoutIdentifier(str: $0) }
guard let firstLayout = layouts.first else {
return nil
}
self.predominantLayout = firstLayout
self.layouts = layouts
}

public init?(layouts: [VideoLayoutIdentifier]) {
guard let predominantLayout = layouts.first else { return nil }
self.layouts = layouts
self.predominantLayout = predominantLayout
}

public func contains(_ layout: VideoLayoutIdentifier) -> Bool {
layouts.contains(layout)
}
}
Loading

0 comments on commit 07a909b

Please sign in to comment.