├── .gitignore
├── .swiftlint.yml
├── .travis.yml
├── CHANGELOG.md
├── Info.plist
├── LICENSE
├── Package.swift
├── Pantomime.podspec
├── Pantomime.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ ├── Pantomime (OSX).xcscheme
│ ├── Pantomime (iOS).xcscheme
│ └── Pantomime (tvOS).xcscheme
├── PantomimeTests
├── Info.plist
├── PantomimeTests.swift
├── PlaylistTests.swift
├── ReaderTests.swift
├── master.m3u8
├── media.m3u8
└── media2.m3u8
├── README.md
└── sources
├── BufferedReader.swift
├── FileBufferedReader.swift
├── ManifestBuilder.swift
├── MasterPlaylist.swift
├── MediaPlaylist.swift
├── MediaSegment.swift
├── NSURLExtension.swift
├── ReaderBuilder.swift
├── StreamReader.swift
├── StringBufferedReader.swift
├── StringExtensions.swift
└── URLBufferedReader.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # for Xcode
2 | ###########
3 |
4 | .DS_Store
5 | .DS_Store?
6 | Icon?
7 |
8 | Secrets.*
9 |
10 | ## Build generated
11 | build/
12 | DerivedData
13 |
14 | ## Various settings
15 | *.pbxuser
16 | !default.pbxuser
17 | *.mode1v3
18 | !default.mode1v3
19 | *.mode2v3
20 | !default.mode2v3
21 | *.perspectivev3
22 | !default.perspectivev3
23 | xcuserdata
24 | .idea
25 |
26 | ## Other
27 | *.xccheckout
28 | *.moved-aside
29 | *.xcuserstate
30 | *.xcscmblueprint
31 |
32 | ## Obj-C/Swift specific
33 | *.hmap
34 | *.ipa
35 |
36 | ## Playgrounds
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 | # Swift Package Manager
41 | #
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | # Packages/
44 | .build/
45 |
46 | # CocoaPods
47 | #
48 | # We recommend against adding the Pods directory to your .gitignore. However
49 | # you should judge for yourself, the pros and cons are mentioned at:
50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
51 | #
52 | # Pods/
53 |
54 | # Carthage
55 | #
56 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
57 | # Carthage/Checkouts
58 |
59 | Carthage/
60 |
61 | # fastlane
62 | #
63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
64 | # screenshots whenever they are needed.
65 | # For more information about the recommended setup visit:
66 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
67 |
68 | fastlane/report.xml
69 | fastlane/screenshots
70 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/realm/SwiftLint
2 | #
3 | # run on CLI (or as build phase):
4 | #
5 | # swiftlint rules / autocorrect / lint (default)
6 | #
7 | #
8 |
9 | disabled_rules: # rule identifiers to exclude from running
10 | - valid_docs # /// is valid and used by Xcode
11 | - cyclomatic_complexity
12 | - variable_name
13 |
14 | opt_in_rules: # some rules are only opt-in
15 | - empty_count
16 | #- missing_docs
17 |
18 | # Find all the available rules by running:
19 | # swiftlint rules
20 |
21 | included: # paths to include during linting. `--path` is ignored if present.
22 | - sources
23 | - SwiftyBeaver
24 | - SwiftyBeaverTests
25 |
26 | excluded: # paths to ignore during linting. Takes precedence over `included`.
27 | - Carthage
28 |
29 | line_length: 121
30 |
31 | function_parameter_count:
32 | warning: 10
33 | error: 15
34 |
35 | function_body_length:
36 | warning: 80
37 | error: 100
38 |
39 | type_body_length:
40 | warning: 700
41 | error: 1000
42 |
43 | file_length:
44 | warning: 1200
45 | error: 1500
46 |
47 | variable_name:
48 | min_length: 2
49 | excluded:
50 | - translatesAutoresizingMaskIntoConstraints
51 |
52 | type_name:
53 | min_length: 2
54 |
55 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle)
56 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | xcode_project: Pantomime.xcodeproj
3 | xcode_scheme: Pantomime (iOS)
4 | osx_image: xcode7.3
5 | xcode_sdk: iphonesimulator9.3
6 | before_install:
7 | - gem install xcpretty
8 | script:
9 | - set -o pipefail # required to let xcpretty use the same exit code like xcodebuild
10 | - xcodebuild -project Pantomime.xcodeproj -scheme "Pantomime (iOS)" -destination name="iPhone 6" -destination name="iPhone 6" test | xcpretty -c
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file following the style described at [Keep a Changelog](http://keepachangelog.com) by [@olivierlacan](https://github.com/olivierlacan).
4 | This project adheres to [Semantic Versioning](http://semver.org/).
5 |
6 | ----
7 | ## 0.1.4 (2016-11-21)
8 |
9 | ##### Fixed
10 |
11 | - "fatal error: Index out of range" on getSegment and getPlaylist.
12 |
13 |
14 |
15 | ## 0.1.3 (2016-08-29)
16 |
17 | ##### Fixed
18 |
19 | - "fatal error: Index out of range" on getSegment and getPlaylist.
20 |
21 |
22 |
23 | ## 0.1.2 (2016-08-29)
24 |
25 | ##### Fixed
26 |
27 | - public constructors, properties and methods added
28 |
29 |
30 | ## 0.1.1 (2016-08-28)
31 |
32 | ##### Updated
33 |
34 | - Docs (readme)
35 |
36 | ##### Fixed
37 |
38 | - public constructor was missing in ManifestBuilder
39 |
40 |
41 | ## 0.1.0 (2016-08-28)
42 |
43 | ##### Added
44 |
45 | - The first release of the HLS Manifest parser
46 | - Reads and parses MediaPlaylist and MediaSegments
47 | - Can read full manifest with loading of referrenced manifests
48 |
49 |
--------------------------------------------------------------------------------
/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 0.1.4
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Thomas Christensen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "Pantomime"
5 | )
6 |
--------------------------------------------------------------------------------
/Pantomime.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "Pantomime"
3 | s.version = "0.1.4"
4 | s.summary = "Parsing of M3U8 manifest files for Swift"
5 |
6 | s.description = <<-DESC
7 | M3U8Parser4Swift reads and writes HTTP Live Streaming manifest files.
8 | Use it to fetch a Master manifest and for parsing it. Supports the
9 | Internet-Draft version 7. Can be used to throw events when various elements
10 | have been parsed. Use it to contruct a new manifest from scratch.
11 | Supports Master and Media playlist manifest files.
12 | DESC
13 |
14 | s.homepage = "https://github.com/thomaschristensen/Pantomime"
15 | s.license = "MIT"
16 | s.author = { "Thomas Christensen" => "tchristensen@nordija.com" }
17 | s.ios.deployment_target = "8.0"
18 | s.tvos.deployment_target = "9.0"
19 | s.osx.deployment_target = "10.10"
20 | s.source = { :git => "https://github.com/thomaschristensen/Pantomime.git", :tag => "0.1.4" }
21 | s.source_files = "sources"
22 | end
23 |
--------------------------------------------------------------------------------
/Pantomime.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 5ED0327E1D6F1710006DE1F3 /* ManifestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */; };
11 | 5ED0327F1D6F1710006DE1F3 /* MediaSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */; };
12 | 5ED032801D6F1710006DE1F3 /* MediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */; };
13 | 5ED032811D6F1710006DE1F3 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327C1D6F1710006DE1F3 /* StringExtensions.swift */; };
14 | 5ED032821D6F1710006DE1F3 /* StreamReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327D1D6F1710006DE1F3 /* StreamReader.swift */; };
15 | 5ED032831D6F1721006DE1F3 /* ManifestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */; };
16 | 5ED032841D6F1722006DE1F3 /* ManifestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */; };
17 | 5ED032851D6F1728006DE1F3 /* MediaSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */; };
18 | 5ED032861D6F1728006DE1F3 /* MediaSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */; };
19 | 5ED032871D6F172B006DE1F3 /* MediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */; };
20 | 5ED032881D6F172C006DE1F3 /* MediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */; };
21 | 5ED032891D6F1731006DE1F3 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327C1D6F1710006DE1F3 /* StringExtensions.swift */; };
22 | 5ED0328A1D6F1731006DE1F3 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327C1D6F1710006DE1F3 /* StringExtensions.swift */; };
23 | 5ED0328B1D6F1734006DE1F3 /* StreamReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327D1D6F1710006DE1F3 /* StreamReader.swift */; };
24 | 5ED0328C1D6F1735006DE1F3 /* StreamReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327D1D6F1710006DE1F3 /* StreamReader.swift */; };
25 | 5ED0328F1D6F17BF006DE1F3 /* media2.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 5ED0328D1D6F17BF006DE1F3 /* media2.m3u8 */; };
26 | 5ED032901D6F17BF006DE1F3 /* media.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 5ED0328E1D6F17BF006DE1F3 /* media.m3u8 */; };
27 | 5ED032921D6F7539006DE1F3 /* MasterPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032911D6F7539006DE1F3 /* MasterPlaylist.swift */; };
28 | 5ED032931D6F7539006DE1F3 /* MasterPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032911D6F7539006DE1F3 /* MasterPlaylist.swift */; };
29 | 5ED032941D6F7539006DE1F3 /* MasterPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032911D6F7539006DE1F3 /* MasterPlaylist.swift */; };
30 | 5ED032961D6F7618006DE1F3 /* master.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 5ED032951D6F7618006DE1F3 /* master.m3u8 */; };
31 | 7FBCA08CA592BD0DC04B8FE5 /* StringBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA22C61E0D299DF542F4B /* StringBufferedReader.swift */; };
32 | 7FBCA092389AE5D6DF3A72EB /* ReaderBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAFFB2E9DF6E75D5AF46D /* ReaderBuilder.swift */; };
33 | 7FBCA48C54BDA0AB70B73393 /* FileBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA03857771FD1265B089F /* FileBufferedReader.swift */; };
34 | 7FBCA552A6090C2DF269D9E9 /* NSURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA9CB6FB8AA516DB9C538 /* NSURLExtension.swift */; };
35 | 7FBCA65DDE8C32438FE4E9C8 /* FileBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA03857771FD1265B089F /* FileBufferedReader.swift */; };
36 | 7FBCA6A7E5981681AB2E5E48 /* ReaderBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAFFB2E9DF6E75D5AF46D /* ReaderBuilder.swift */; };
37 | 7FBCA81E63E9441F3AC22BB7 /* ReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA10A0EB4EAA0CD4AB5EF /* ReaderTests.swift */; };
38 | 7FBCA8DD1928C9CD881780F6 /* BufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAAE76466B9E53DA130F3 /* BufferedReader.swift */; };
39 | 7FBCAB4AB49726BF5F8EB6C8 /* BufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAAE76466B9E53DA130F3 /* BufferedReader.swift */; };
40 | 7FBCABD199BB1B8440EC750E /* FileBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA03857771FD1265B089F /* FileBufferedReader.swift */; };
41 | 7FBCABE9DE7114831052FA9D /* BufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAAE76466B9E53DA130F3 /* BufferedReader.swift */; };
42 | 7FBCAC77789A6B78BFB3B2BE /* ReaderBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAFFB2E9DF6E75D5AF46D /* ReaderBuilder.swift */; };
43 | 7FBCACF2178BC111C07423B5 /* URLBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAAA79D01BF5CEBA7261A /* URLBufferedReader.swift */; };
44 | 7FBCAD486D18CBDBCA89C3F6 /* NSURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA9CB6FB8AA516DB9C538 /* NSURLExtension.swift */; };
45 | 7FBCAD4D0E61F2913881E262 /* PlaylistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAFC77C9DA4C80FAEA0C2 /* PlaylistTests.swift */; };
46 | 7FBCAD932FF257481CDB95DE /* StringBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA22C61E0D299DF542F4B /* StringBufferedReader.swift */; };
47 | 7FBCADB79746F44D9AC5F47C /* URLBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAAA79D01BF5CEBA7261A /* URLBufferedReader.swift */; };
48 | 7FBCAE6AB452BA202FB48490 /* NSURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA9CB6FB8AA516DB9C538 /* NSURLExtension.swift */; };
49 | 7FBCAEE343068A84C3A08C26 /* StringBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCA22C61E0D299DF542F4B /* StringBufferedReader.swift */; };
50 | 7FBCAF63D24C6183A51D09D8 /* URLBufferedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBCAAA79D01BF5CEBA7261A /* URLBufferedReader.swift */; };
51 | 9E6CAA481C148EC1009D9093 /* Pantomime.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 9E6CAA471C148EC1009D9093 /* Pantomime.podspec */; };
52 | 9E6CAA4A1C149579009D9093 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9E6CAA491C149579009D9093 /* LICENSE */; };
53 | 9EDCE3EF1C09D211002FA4A7 /* Pantomime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9EDCE3E41C09D211002FA4A7 /* Pantomime.framework */; };
54 | 9EDCE3F41C09D211002FA4A7 /* PantomimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDCE3F31C09D211002FA4A7 /* PantomimeTests.swift */; };
55 | ACE5978F1C18830D0031451F /* Pantomime.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 9E6CAA471C148EC1009D9093 /* Pantomime.podspec */; };
56 | ACE597901C18830D0031451F /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9E6CAA491C149579009D9093 /* LICENSE */; };
57 | ACE597A01C18832E0031451F /* Pantomime.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 9E6CAA471C148EC1009D9093 /* Pantomime.podspec */; };
58 | ACE597A11C18832E0031451F /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9E6CAA491C149579009D9093 /* LICENSE */; };
59 | /* End PBXBuildFile section */
60 |
61 | /* Begin PBXContainerItemProxy section */
62 | 9EDCE3F01C09D211002FA4A7 /* PBXContainerItemProxy */ = {
63 | isa = PBXContainerItemProxy;
64 | containerPortal = 9EDCE3DB1C09D211002FA4A7 /* Project object */;
65 | proxyType = 1;
66 | remoteGlobalIDString = 9EDCE3E31C09D211002FA4A7;
67 | remoteInfo = Pantomime;
68 | };
69 | /* End PBXContainerItemProxy section */
70 |
71 | /* Begin PBXFileReference section */
72 | 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ManifestBuilder.swift; path = sources/ManifestBuilder.swift; sourceTree = ""; };
73 | 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaSegment.swift; path = sources/MediaSegment.swift; sourceTree = ""; };
74 | 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaPlaylist.swift; path = sources/MediaPlaylist.swift; sourceTree = ""; };
75 | 5ED0327C1D6F1710006DE1F3 /* StringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringExtensions.swift; path = sources/StringExtensions.swift; sourceTree = ""; };
76 | 5ED0327D1D6F1710006DE1F3 /* StreamReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StreamReader.swift; path = sources/StreamReader.swift; sourceTree = ""; };
77 | 5ED0328D1D6F17BF006DE1F3 /* media2.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = media2.m3u8; sourceTree = ""; };
78 | 5ED0328E1D6F17BF006DE1F3 /* media.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = media.m3u8; sourceTree = ""; };
79 | 5ED032911D6F7539006DE1F3 /* MasterPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MasterPlaylist.swift; path = sources/MasterPlaylist.swift; sourceTree = ""; };
80 | 5ED032951D6F7618006DE1F3 /* master.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = master.m3u8; sourceTree = ""; };
81 | 7FBCA03857771FD1265B089F /* FileBufferedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FileBufferedReader.swift; path = sources/FileBufferedReader.swift; sourceTree = ""; };
82 | 7FBCA10A0EB4EAA0CD4AB5EF /* ReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderTests.swift; sourceTree = ""; };
83 | 7FBCA22C61E0D299DF542F4B /* StringBufferedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringBufferedReader.swift; path = sources/StringBufferedReader.swift; sourceTree = ""; };
84 | 7FBCA9CB6FB8AA516DB9C538 /* NSURLExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NSURLExtension.swift; path = sources/NSURLExtension.swift; sourceTree = ""; };
85 | 7FBCAAA79D01BF5CEBA7261A /* URLBufferedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLBufferedReader.swift; path = sources/URLBufferedReader.swift; sourceTree = ""; };
86 | 7FBCAAE76466B9E53DA130F3 /* BufferedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BufferedReader.swift; path = sources/BufferedReader.swift; sourceTree = ""; };
87 | 7FBCAFC77C9DA4C80FAEA0C2 /* PlaylistTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaylistTests.swift; sourceTree = ""; };
88 | 7FBCAFFB2E9DF6E75D5AF46D /* ReaderBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReaderBuilder.swift; path = sources/ReaderBuilder.swift; sourceTree = ""; };
89 | 9E195B101C6B766D00D924CB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
90 | 9E4BBDFA1C09EB7F00220716 /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = ""; };
91 | 9E4BBDFE1C0A01D000220716 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
92 | 9E6CAA471C148EC1009D9093 /* Pantomime.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Pantomime.podspec; sourceTree = ""; };
93 | 9E6CAA491C149579009D9093 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
94 | 9E74FEA81CD74AFA0024DD76 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; };
95 | 9EDCE3E41C09D211002FA4A7 /* Pantomime.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pantomime.framework; sourceTree = BUILT_PRODUCTS_DIR; };
96 | 9EDCE3EE1C09D211002FA4A7 /* PantomimeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PantomimeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
97 | 9EDCE3F31C09D211002FA4A7 /* PantomimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PantomimeTests.swift; sourceTree = ""; };
98 | 9EDCE3F51C09D211002FA4A7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
99 | ACE597951C18830D0031451F /* Pantomime.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pantomime.framework; sourceTree = BUILT_PRODUCTS_DIR; };
100 | ACE597A61C18832E0031451F /* Pantomime.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pantomime.framework; sourceTree = BUILT_PRODUCTS_DIR; };
101 | /* End PBXFileReference section */
102 |
103 | /* Begin PBXFrameworksBuildPhase section */
104 | 9EDCE3E01C09D211002FA4A7 /* Frameworks */ = {
105 | isa = PBXFrameworksBuildPhase;
106 | buildActionMask = 2147483647;
107 | files = (
108 | );
109 | runOnlyForDeploymentPostprocessing = 0;
110 | };
111 | 9EDCE3EB1C09D211002FA4A7 /* Frameworks */ = {
112 | isa = PBXFrameworksBuildPhase;
113 | buildActionMask = 2147483647;
114 | files = (
115 | 9EDCE3EF1C09D211002FA4A7 /* Pantomime.framework in Frameworks */,
116 | );
117 | runOnlyForDeploymentPostprocessing = 0;
118 | };
119 | ACE5978C1C18830D0031451F /* Frameworks */ = {
120 | isa = PBXFrameworksBuildPhase;
121 | buildActionMask = 2147483647;
122 | files = (
123 | );
124 | runOnlyForDeploymentPostprocessing = 0;
125 | };
126 | ACE5979D1C18832E0031451F /* Frameworks */ = {
127 | isa = PBXFrameworksBuildPhase;
128 | buildActionMask = 2147483647;
129 | files = (
130 | );
131 | runOnlyForDeploymentPostprocessing = 0;
132 | };
133 | /* End PBXFrameworksBuildPhase section */
134 |
135 | /* Begin PBXGroup section */
136 | 9E31D62A1C185ED0004E0980 /* sources */ = {
137 | isa = PBXGroup;
138 | children = (
139 | 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */,
140 | 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */,
141 | 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */,
142 | 5ED0327C1D6F1710006DE1F3 /* StringExtensions.swift */,
143 | 5ED0327D1D6F1710006DE1F3 /* StreamReader.swift */,
144 | 5ED032911D6F7539006DE1F3 /* MasterPlaylist.swift */,
145 | 7FBCAAE76466B9E53DA130F3 /* BufferedReader.swift */,
146 | 7FBCA03857771FD1265B089F /* FileBufferedReader.swift */,
147 | 7FBCAFFB2E9DF6E75D5AF46D /* ReaderBuilder.swift */,
148 | 7FBCA22C61E0D299DF542F4B /* StringBufferedReader.swift */,
149 | 7FBCAAA79D01BF5CEBA7261A /* URLBufferedReader.swift */,
150 | 7FBCA9CB6FB8AA516DB9C538 /* NSURLExtension.swift */,
151 | );
152 | name = sources;
153 | sourceTree = "";
154 | };
155 | 9EDCE3DA1C09D211002FA4A7 = {
156 | isa = PBXGroup;
157 | children = (
158 | 9E4BBDFA1C09EB7F00220716 /* .gitignore */,
159 | 9E6CAA491C149579009D9093 /* LICENSE */,
160 | 9E195B101C6B766D00D924CB /* Info.plist */,
161 | 9E74FEA81CD74AFA0024DD76 /* CHANGELOG.md */,
162 | 9E4BBDFE1C0A01D000220716 /* README.md */,
163 | 9E6CAA471C148EC1009D9093 /* Pantomime.podspec */,
164 | 9E31D62A1C185ED0004E0980 /* sources */,
165 | 9EDCE3F21C09D211002FA4A7 /* PantomimeTests */,
166 | 9EDCE3E51C09D211002FA4A7 /* Products */,
167 | );
168 | sourceTree = "";
169 | };
170 | 9EDCE3E51C09D211002FA4A7 /* Products */ = {
171 | isa = PBXGroup;
172 | children = (
173 | 9EDCE3E41C09D211002FA4A7 /* Pantomime.framework */,
174 | 9EDCE3EE1C09D211002FA4A7 /* PantomimeTests.xctest */,
175 | ACE597951C18830D0031451F /* Pantomime.framework */,
176 | ACE597A61C18832E0031451F /* Pantomime.framework */,
177 | );
178 | name = Products;
179 | sourceTree = "";
180 | };
181 | 9EDCE3F21C09D211002FA4A7 /* PantomimeTests */ = {
182 | isa = PBXGroup;
183 | children = (
184 | 5ED032951D6F7618006DE1F3 /* master.m3u8 */,
185 | 5ED0328D1D6F17BF006DE1F3 /* media2.m3u8 */,
186 | 5ED0328E1D6F17BF006DE1F3 /* media.m3u8 */,
187 | 9EDCE3F51C09D211002FA4A7 /* Info.plist */,
188 | 9EDCE3F31C09D211002FA4A7 /* PantomimeTests.swift */,
189 | 7FBCA10A0EB4EAA0CD4AB5EF /* ReaderTests.swift */,
190 | 7FBCAFC77C9DA4C80FAEA0C2 /* PlaylistTests.swift */,
191 | );
192 | path = PantomimeTests;
193 | sourceTree = "";
194 | };
195 | /* End PBXGroup section */
196 |
197 | /* Begin PBXHeadersBuildPhase section */
198 | 9EDCE3E11C09D211002FA4A7 /* Headers */ = {
199 | isa = PBXHeadersBuildPhase;
200 | buildActionMask = 2147483647;
201 | files = (
202 | );
203 | runOnlyForDeploymentPostprocessing = 0;
204 | };
205 | ACE5978D1C18830D0031451F /* Headers */ = {
206 | isa = PBXHeadersBuildPhase;
207 | buildActionMask = 2147483647;
208 | files = (
209 | );
210 | runOnlyForDeploymentPostprocessing = 0;
211 | };
212 | ACE5979E1C18832E0031451F /* Headers */ = {
213 | isa = PBXHeadersBuildPhase;
214 | buildActionMask = 2147483647;
215 | files = (
216 | );
217 | runOnlyForDeploymentPostprocessing = 0;
218 | };
219 | /* End PBXHeadersBuildPhase section */
220 |
221 | /* Begin PBXNativeTarget section */
222 | 9EDCE3E31C09D211002FA4A7 /* Pantomime (iOS) */ = {
223 | isa = PBXNativeTarget;
224 | buildConfigurationList = 9EDCE3F81C09D211002FA4A7 /* Build configuration list for PBXNativeTarget "Pantomime (iOS)" */;
225 | buildPhases = (
226 | 5ED032971D6F82B8006DE1F3 /* ShellScript */,
227 | 9EDCE3DF1C09D211002FA4A7 /* Sources */,
228 | 9EDCE3E01C09D211002FA4A7 /* Frameworks */,
229 | 9EDCE3E11C09D211002FA4A7 /* Headers */,
230 | 9EDCE3E21C09D211002FA4A7 /* Resources */,
231 | );
232 | buildRules = (
233 | );
234 | dependencies = (
235 | );
236 | name = "Pantomime (iOS)";
237 | productName = Pantomime;
238 | productReference = 9EDCE3E41C09D211002FA4A7 /* Pantomime.framework */;
239 | productType = "com.apple.product-type.framework";
240 | };
241 | 9EDCE3ED1C09D211002FA4A7 /* PantomimeTests */ = {
242 | isa = PBXNativeTarget;
243 | buildConfigurationList = 9EDCE3FB1C09D211002FA4A7 /* Build configuration list for PBXNativeTarget "PantomimeTests" */;
244 | buildPhases = (
245 | 9EDCE3EA1C09D211002FA4A7 /* Sources */,
246 | 9EDCE3EB1C09D211002FA4A7 /* Frameworks */,
247 | 9EDCE3EC1C09D211002FA4A7 /* Resources */,
248 | 9EB9C85B1C92C271005C0541 /* ShellScript */,
249 | );
250 | buildRules = (
251 | );
252 | dependencies = (
253 | 9EDCE3F11C09D211002FA4A7 /* PBXTargetDependency */,
254 | );
255 | name = PantomimeTests;
256 | productName = PantomimeTests;
257 | productReference = 9EDCE3EE1C09D211002FA4A7 /* PantomimeTests.xctest */;
258 | productType = "com.apple.product-type.bundle.unit-test";
259 | };
260 | ACE597861C18830D0031451F /* Pantomime (tvOS) */ = {
261 | isa = PBXNativeTarget;
262 | buildConfigurationList = ACE597921C18830D0031451F /* Build configuration list for PBXNativeTarget "Pantomime (tvOS)" */;
263 | buildPhases = (
264 | ACE597871C18830D0031451F /* Sources */,
265 | ACE5978C1C18830D0031451F /* Frameworks */,
266 | ACE5978D1C18830D0031451F /* Headers */,
267 | ACE5978E1C18830D0031451F /* Resources */,
268 | );
269 | buildRules = (
270 | );
271 | dependencies = (
272 | );
273 | name = "Pantomime (tvOS)";
274 | productName = Pantomime;
275 | productReference = ACE597951C18830D0031451F /* Pantomime.framework */;
276 | productType = "com.apple.product-type.framework";
277 | };
278 | ACE597971C18832E0031451F /* Pantomime (OSX) */ = {
279 | isa = PBXNativeTarget;
280 | buildConfigurationList = ACE597A31C18832E0031451F /* Build configuration list for PBXNativeTarget "Pantomime (OSX)" */;
281 | buildPhases = (
282 | ACE597981C18832E0031451F /* Sources */,
283 | ACE5979D1C18832E0031451F /* Frameworks */,
284 | ACE5979E1C18832E0031451F /* Headers */,
285 | ACE5979F1C18832E0031451F /* Resources */,
286 | );
287 | buildRules = (
288 | );
289 | dependencies = (
290 | );
291 | name = "Pantomime (OSX)";
292 | productName = Pantomime;
293 | productReference = ACE597A61C18832E0031451F /* Pantomime.framework */;
294 | productType = "com.apple.product-type.framework";
295 | };
296 | /* End PBXNativeTarget section */
297 |
298 | /* Begin PBXProject section */
299 | 9EDCE3DB1C09D211002FA4A7 /* Project object */ = {
300 | isa = PBXProject;
301 | attributes = {
302 | LastSwiftUpdateCheck = 0730;
303 | LastUpgradeCheck = 1000;
304 | ORGANIZATIONNAME = "Thomas Christensen";
305 | TargetAttributes = {
306 | 9EDCE3E31C09D211002FA4A7 = {
307 | CreatedOnToolsVersion = 7.1.1;
308 | DevelopmentTeam = W9ZVWEMJZF;
309 | LastSwiftMigration = 1000;
310 | };
311 | 9EDCE3ED1C09D211002FA4A7 = {
312 | CreatedOnToolsVersion = 7.1.1;
313 | DevelopmentTeam = W9ZVWEMJZF;
314 | LastSwiftMigration = 1000;
315 | };
316 | ACE597861C18830D0031451F = {
317 | DevelopmentTeam = W9ZVWEMJZF;
318 | LastSwiftMigration = 0800;
319 | };
320 | ACE597971C18832E0031451F = {
321 | DevelopmentTeam = W9ZVWEMJZF;
322 | LastSwiftMigration = 0800;
323 | ProvisioningStyle = Automatic;
324 | };
325 | };
326 | };
327 | buildConfigurationList = 9EDCE3DE1C09D211002FA4A7 /* Build configuration list for PBXProject "Pantomime" */;
328 | compatibilityVersion = "Xcode 3.2";
329 | developmentRegion = English;
330 | hasScannedForEncodings = 0;
331 | knownRegions = (
332 | en,
333 | );
334 | mainGroup = 9EDCE3DA1C09D211002FA4A7;
335 | productRefGroup = 9EDCE3E51C09D211002FA4A7 /* Products */;
336 | projectDirPath = "";
337 | projectRoot = "";
338 | targets = (
339 | 9EDCE3E31C09D211002FA4A7 /* Pantomime (iOS) */,
340 | 9EDCE3ED1C09D211002FA4A7 /* PantomimeTests */,
341 | ACE597861C18830D0031451F /* Pantomime (tvOS) */,
342 | ACE597971C18832E0031451F /* Pantomime (OSX) */,
343 | );
344 | };
345 | /* End PBXProject section */
346 |
347 | /* Begin PBXResourcesBuildPhase section */
348 | 9EDCE3E21C09D211002FA4A7 /* Resources */ = {
349 | isa = PBXResourcesBuildPhase;
350 | buildActionMask = 2147483647;
351 | files = (
352 | 9E6CAA481C148EC1009D9093 /* Pantomime.podspec in Resources */,
353 | 9E6CAA4A1C149579009D9093 /* LICENSE in Resources */,
354 | );
355 | runOnlyForDeploymentPostprocessing = 0;
356 | };
357 | 9EDCE3EC1C09D211002FA4A7 /* Resources */ = {
358 | isa = PBXResourcesBuildPhase;
359 | buildActionMask = 2147483647;
360 | files = (
361 | 5ED0328F1D6F17BF006DE1F3 /* media2.m3u8 in Resources */,
362 | 5ED032961D6F7618006DE1F3 /* master.m3u8 in Resources */,
363 | 5ED032901D6F17BF006DE1F3 /* media.m3u8 in Resources */,
364 | );
365 | runOnlyForDeploymentPostprocessing = 0;
366 | };
367 | ACE5978E1C18830D0031451F /* Resources */ = {
368 | isa = PBXResourcesBuildPhase;
369 | buildActionMask = 2147483647;
370 | files = (
371 | ACE5978F1C18830D0031451F /* Pantomime.podspec in Resources */,
372 | ACE597901C18830D0031451F /* LICENSE in Resources */,
373 | );
374 | runOnlyForDeploymentPostprocessing = 0;
375 | };
376 | ACE5979F1C18832E0031451F /* Resources */ = {
377 | isa = PBXResourcesBuildPhase;
378 | buildActionMask = 2147483647;
379 | files = (
380 | ACE597A01C18832E0031451F /* Pantomime.podspec in Resources */,
381 | ACE597A11C18832E0031451F /* LICENSE in Resources */,
382 | );
383 | runOnlyForDeploymentPostprocessing = 0;
384 | };
385 | /* End PBXResourcesBuildPhase section */
386 |
387 | /* Begin PBXShellScriptBuildPhase section */
388 | 5ED032971D6F82B8006DE1F3 /* ShellScript */ = {
389 | isa = PBXShellScriptBuildPhase;
390 | buildActionMask = 2147483647;
391 | files = (
392 | );
393 | inputPaths = (
394 | );
395 | outputPaths = (
396 | );
397 | runOnlyForDeploymentPostprocessing = 0;
398 | shellPath = /bin/sh;
399 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
400 | };
401 | 9EB9C85B1C92C271005C0541 /* ShellScript */ = {
402 | isa = PBXShellScriptBuildPhase;
403 | buildActionMask = 2147483647;
404 | files = (
405 | );
406 | inputPaths = (
407 | );
408 | outputPaths = (
409 | );
410 | runOnlyForDeploymentPostprocessing = 0;
411 | shellPath = /bin/sh;
412 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint autocorrect\n swiftlint\nfi";
413 | };
414 | /* End PBXShellScriptBuildPhase section */
415 |
416 | /* Begin PBXSourcesBuildPhase section */
417 | 9EDCE3DF1C09D211002FA4A7 /* Sources */ = {
418 | isa = PBXSourcesBuildPhase;
419 | buildActionMask = 2147483647;
420 | files = (
421 | 5ED032821D6F1710006DE1F3 /* StreamReader.swift in Sources */,
422 | 5ED032811D6F1710006DE1F3 /* StringExtensions.swift in Sources */,
423 | 5ED0327F1D6F1710006DE1F3 /* MediaSegment.swift in Sources */,
424 | 5ED032921D6F7539006DE1F3 /* MasterPlaylist.swift in Sources */,
425 | 5ED0327E1D6F1710006DE1F3 /* ManifestBuilder.swift in Sources */,
426 | 5ED032801D6F1710006DE1F3 /* MediaPlaylist.swift in Sources */,
427 | 7FBCABE9DE7114831052FA9D /* BufferedReader.swift in Sources */,
428 | 7FBCA48C54BDA0AB70B73393 /* FileBufferedReader.swift in Sources */,
429 | 7FBCA6A7E5981681AB2E5E48 /* ReaderBuilder.swift in Sources */,
430 | 7FBCA08CA592BD0DC04B8FE5 /* StringBufferedReader.swift in Sources */,
431 | 7FBCAF63D24C6183A51D09D8 /* URLBufferedReader.swift in Sources */,
432 | 7FBCAE6AB452BA202FB48490 /* NSURLExtension.swift in Sources */,
433 | );
434 | runOnlyForDeploymentPostprocessing = 0;
435 | };
436 | 9EDCE3EA1C09D211002FA4A7 /* Sources */ = {
437 | isa = PBXSourcesBuildPhase;
438 | buildActionMask = 2147483647;
439 | files = (
440 | 9EDCE3F41C09D211002FA4A7 /* PantomimeTests.swift in Sources */,
441 | 7FBCA81E63E9441F3AC22BB7 /* ReaderTests.swift in Sources */,
442 | 7FBCAD4D0E61F2913881E262 /* PlaylistTests.swift in Sources */,
443 | );
444 | runOnlyForDeploymentPostprocessing = 0;
445 | };
446 | ACE597871C18830D0031451F /* Sources */ = {
447 | isa = PBXSourcesBuildPhase;
448 | buildActionMask = 2147483647;
449 | files = (
450 | 5ED0328C1D6F1735006DE1F3 /* StreamReader.swift in Sources */,
451 | 5ED032891D6F1731006DE1F3 /* StringExtensions.swift in Sources */,
452 | 5ED032851D6F1728006DE1F3 /* MediaSegment.swift in Sources */,
453 | 5ED032931D6F7539006DE1F3 /* MasterPlaylist.swift in Sources */,
454 | 5ED032831D6F1721006DE1F3 /* ManifestBuilder.swift in Sources */,
455 | 5ED032881D6F172C006DE1F3 /* MediaPlaylist.swift in Sources */,
456 | 7FBCA8DD1928C9CD881780F6 /* BufferedReader.swift in Sources */,
457 | 7FBCABD199BB1B8440EC750E /* FileBufferedReader.swift in Sources */,
458 | 7FBCAC77789A6B78BFB3B2BE /* ReaderBuilder.swift in Sources */,
459 | 7FBCAEE343068A84C3A08C26 /* StringBufferedReader.swift in Sources */,
460 | 7FBCADB79746F44D9AC5F47C /* URLBufferedReader.swift in Sources */,
461 | 7FBCAD486D18CBDBCA89C3F6 /* NSURLExtension.swift in Sources */,
462 | );
463 | runOnlyForDeploymentPostprocessing = 0;
464 | };
465 | ACE597981C18832E0031451F /* Sources */ = {
466 | isa = PBXSourcesBuildPhase;
467 | buildActionMask = 2147483647;
468 | files = (
469 | 5ED0328B1D6F1734006DE1F3 /* StreamReader.swift in Sources */,
470 | 5ED0328A1D6F1731006DE1F3 /* StringExtensions.swift in Sources */,
471 | 5ED032861D6F1728006DE1F3 /* MediaSegment.swift in Sources */,
472 | 5ED032941D6F7539006DE1F3 /* MasterPlaylist.swift in Sources */,
473 | 5ED032841D6F1722006DE1F3 /* ManifestBuilder.swift in Sources */,
474 | 5ED032871D6F172B006DE1F3 /* MediaPlaylist.swift in Sources */,
475 | 7FBCAB4AB49726BF5F8EB6C8 /* BufferedReader.swift in Sources */,
476 | 7FBCA65DDE8C32438FE4E9C8 /* FileBufferedReader.swift in Sources */,
477 | 7FBCA092389AE5D6DF3A72EB /* ReaderBuilder.swift in Sources */,
478 | 7FBCAD932FF257481CDB95DE /* StringBufferedReader.swift in Sources */,
479 | 7FBCACF2178BC111C07423B5 /* URLBufferedReader.swift in Sources */,
480 | 7FBCA552A6090C2DF269D9E9 /* NSURLExtension.swift in Sources */,
481 | );
482 | runOnlyForDeploymentPostprocessing = 0;
483 | };
484 | /* End PBXSourcesBuildPhase section */
485 |
486 | /* Begin PBXTargetDependency section */
487 | 9EDCE3F11C09D211002FA4A7 /* PBXTargetDependency */ = {
488 | isa = PBXTargetDependency;
489 | target = 9EDCE3E31C09D211002FA4A7 /* Pantomime (iOS) */;
490 | targetProxy = 9EDCE3F01C09D211002FA4A7 /* PBXContainerItemProxy */;
491 | };
492 | /* End PBXTargetDependency section */
493 |
494 | /* Begin XCBuildConfiguration section */
495 | 9EDCE3F61C09D211002FA4A7 /* Debug */ = {
496 | isa = XCBuildConfiguration;
497 | buildSettings = {
498 | ALWAYS_SEARCH_USER_PATHS = NO;
499 | APPLICATION_EXTENSION_API_ONLY = YES;
500 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
501 | CLANG_CXX_LIBRARY = "libc++";
502 | CLANG_ENABLE_MODULES = YES;
503 | CLANG_ENABLE_OBJC_ARC = YES;
504 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
505 | CLANG_WARN_BOOL_CONVERSION = YES;
506 | CLANG_WARN_COMMA = YES;
507 | CLANG_WARN_CONSTANT_CONVERSION = YES;
508 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
509 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
510 | CLANG_WARN_EMPTY_BODY = YES;
511 | CLANG_WARN_ENUM_CONVERSION = YES;
512 | CLANG_WARN_INFINITE_RECURSION = YES;
513 | CLANG_WARN_INT_CONVERSION = YES;
514 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
515 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
516 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
517 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
518 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
519 | CLANG_WARN_STRICT_PROTOTYPES = YES;
520 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
521 | CLANG_WARN_UNREACHABLE_CODE = YES;
522 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
523 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
524 | COPY_PHASE_STRIP = NO;
525 | CURRENT_PROJECT_VERSION = 1;
526 | DEBUG_INFORMATION_FORMAT = dwarf;
527 | ENABLE_STRICT_OBJC_MSGSEND = YES;
528 | ENABLE_TESTABILITY = YES;
529 | GCC_C_LANGUAGE_STANDARD = gnu99;
530 | GCC_DYNAMIC_NO_PIC = NO;
531 | GCC_NO_COMMON_BLOCKS = YES;
532 | GCC_OPTIMIZATION_LEVEL = 0;
533 | GCC_PREPROCESSOR_DEFINITIONS = (
534 | "DEBUG=1",
535 | "$(inherited)",
536 | );
537 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
538 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
539 | GCC_WARN_UNDECLARED_SELECTOR = YES;
540 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
541 | GCC_WARN_UNUSED_FUNCTION = YES;
542 | GCC_WARN_UNUSED_VARIABLE = YES;
543 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
544 | MTL_ENABLE_DEBUG_INFO = YES;
545 | ONLY_ACTIVE_ARCH = YES;
546 | SDKROOT = iphoneos;
547 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
548 | SWIFT_VERSION = 4.0;
549 | VALID_ARCHS = "arm64 armv7 armv7s x86_64";
550 | VERSIONING_SYSTEM = "apple-generic";
551 | VERSION_INFO_PREFIX = "";
552 | };
553 | name = Debug;
554 | };
555 | 9EDCE3F71C09D211002FA4A7 /* Release */ = {
556 | isa = XCBuildConfiguration;
557 | buildSettings = {
558 | ALWAYS_SEARCH_USER_PATHS = NO;
559 | APPLICATION_EXTENSION_API_ONLY = YES;
560 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
561 | CLANG_CXX_LIBRARY = "libc++";
562 | CLANG_ENABLE_MODULES = YES;
563 | CLANG_ENABLE_OBJC_ARC = YES;
564 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
565 | CLANG_WARN_BOOL_CONVERSION = YES;
566 | CLANG_WARN_COMMA = YES;
567 | CLANG_WARN_CONSTANT_CONVERSION = YES;
568 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
569 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
570 | CLANG_WARN_EMPTY_BODY = YES;
571 | CLANG_WARN_ENUM_CONVERSION = YES;
572 | CLANG_WARN_INFINITE_RECURSION = YES;
573 | CLANG_WARN_INT_CONVERSION = YES;
574 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
575 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
576 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
577 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
578 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
579 | CLANG_WARN_STRICT_PROTOTYPES = YES;
580 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
581 | CLANG_WARN_UNREACHABLE_CODE = YES;
582 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
583 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
584 | COPY_PHASE_STRIP = NO;
585 | CURRENT_PROJECT_VERSION = 1;
586 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
587 | ENABLE_NS_ASSERTIONS = NO;
588 | ENABLE_STRICT_OBJC_MSGSEND = YES;
589 | GCC_C_LANGUAGE_STANDARD = gnu99;
590 | GCC_NO_COMMON_BLOCKS = YES;
591 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
592 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
593 | GCC_WARN_UNDECLARED_SELECTOR = YES;
594 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
595 | GCC_WARN_UNUSED_FUNCTION = YES;
596 | GCC_WARN_UNUSED_VARIABLE = YES;
597 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
598 | MTL_ENABLE_DEBUG_INFO = NO;
599 | ONLY_ACTIVE_ARCH = NO;
600 | SDKROOT = iphoneos;
601 | SWIFT_VERSION = 4.0;
602 | VALIDATE_PRODUCT = YES;
603 | VALID_ARCHS = "arm64 armv7 armv7s x86_64";
604 | VERSIONING_SYSTEM = "apple-generic";
605 | VERSION_INFO_PREFIX = "";
606 | };
607 | name = Release;
608 | };
609 | 9EDCE3F91C09D211002FA4A7 /* Debug */ = {
610 | isa = XCBuildConfiguration;
611 | buildSettings = {
612 | APPLICATION_EXTENSION_API_ONLY = YES;
613 | CLANG_ENABLE_MODULES = YES;
614 | CODE_SIGN_IDENTITY = "";
615 | DEFINES_MODULE = YES;
616 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
617 | DYLIB_COMPATIBILITY_VERSION = 1;
618 | DYLIB_CURRENT_VERSION = 1;
619 | DYLIB_INSTALL_NAME_BASE = "@rpath";
620 | INFOPLIST_FILE = Info.plist;
621 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
622 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
623 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.Pantomime;
624 | PRODUCT_NAME = Pantomime;
625 | SKIP_INSTALL = YES;
626 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
627 | SWIFT_VERSION = 4.2;
628 | };
629 | name = Debug;
630 | };
631 | 9EDCE3FA1C09D211002FA4A7 /* Release */ = {
632 | isa = XCBuildConfiguration;
633 | buildSettings = {
634 | APPLICATION_EXTENSION_API_ONLY = YES;
635 | CLANG_ENABLE_MODULES = YES;
636 | CODE_SIGN_IDENTITY = "";
637 | DEFINES_MODULE = YES;
638 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
639 | DYLIB_COMPATIBILITY_VERSION = 1;
640 | DYLIB_CURRENT_VERSION = 1;
641 | DYLIB_INSTALL_NAME_BASE = "@rpath";
642 | INFOPLIST_FILE = Info.plist;
643 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
644 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
645 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.Pantomime;
646 | PRODUCT_NAME = Pantomime;
647 | SKIP_INSTALL = YES;
648 | SWIFT_OPTIMIZATION_LEVEL = "-O";
649 | SWIFT_VERSION = 4.2;
650 | };
651 | name = Release;
652 | };
653 | 9EDCE3FC1C09D211002FA4A7 /* Debug */ = {
654 | isa = XCBuildConfiguration;
655 | buildSettings = {
656 | APPLICATION_EXTENSION_API_ONLY = NO;
657 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
658 | INFOPLIST_FILE = PantomimeTests/Info.plist;
659 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
660 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.PantomimeTests;
661 | PRODUCT_NAME = "$(TARGET_NAME)";
662 | SWIFT_VERSION = 4.2;
663 | };
664 | name = Debug;
665 | };
666 | 9EDCE3FD1C09D211002FA4A7 /* Release */ = {
667 | isa = XCBuildConfiguration;
668 | buildSettings = {
669 | APPLICATION_EXTENSION_API_ONLY = NO;
670 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
671 | INFOPLIST_FILE = PantomimeTests/Info.plist;
672 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
673 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.PantomimeTests;
674 | PRODUCT_NAME = "$(TARGET_NAME)";
675 | SWIFT_OPTIMIZATION_LEVEL = "-O";
676 | SWIFT_VERSION = 4.2;
677 | };
678 | name = Release;
679 | };
680 | ACE597931C18830D0031451F /* Debug */ = {
681 | isa = XCBuildConfiguration;
682 | buildSettings = {
683 | CLANG_ENABLE_MODULES = YES;
684 | CODE_SIGN_IDENTITY = "iPhone Developer";
685 | DEFINES_MODULE = YES;
686 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
687 | DYLIB_COMPATIBILITY_VERSION = 1;
688 | DYLIB_CURRENT_VERSION = 1;
689 | DYLIB_INSTALL_NAME_BASE = "@rpath";
690 | ENABLE_BITCODE = YES;
691 | INFOPLIST_FILE = Info.plist;
692 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
693 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
694 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.Pantomime;
695 | PRODUCT_NAME = Pantomime;
696 | SDKROOT = appletvos;
697 | SKIP_INSTALL = YES;
698 | SUPPORTED_PLATFORMS = "appletvsimulator appletvos";
699 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
700 | SWIFT_VERSION = 4.0;
701 | VALID_ARCHS = "$(ARCHS_STANDARD)";
702 | };
703 | name = Debug;
704 | };
705 | ACE597941C18830D0031451F /* Release */ = {
706 | isa = XCBuildConfiguration;
707 | buildSettings = {
708 | CLANG_ENABLE_MODULES = YES;
709 | CODE_SIGN_IDENTITY = "";
710 | DEFINES_MODULE = YES;
711 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
712 | DYLIB_COMPATIBILITY_VERSION = 1;
713 | DYLIB_CURRENT_VERSION = 1;
714 | DYLIB_INSTALL_NAME_BASE = "@rpath";
715 | ENABLE_BITCODE = YES;
716 | INFOPLIST_FILE = Info.plist;
717 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
718 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
719 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.Pantomime;
720 | PRODUCT_NAME = Pantomime;
721 | SDKROOT = appletvos;
722 | SKIP_INSTALL = YES;
723 | SUPPORTED_PLATFORMS = "appletvsimulator appletvos";
724 | SWIFT_OPTIMIZATION_LEVEL = "-O";
725 | SWIFT_VERSION = 4.0;
726 | VALID_ARCHS = "$(ARCHS_STANDARD)";
727 | };
728 | name = Release;
729 | };
730 | ACE597A41C18832E0031451F /* Debug */ = {
731 | isa = XCBuildConfiguration;
732 | buildSettings = {
733 | CLANG_ENABLE_MODULES = YES;
734 | CODE_SIGN_IDENTITY = "Mac Developer";
735 | CODE_SIGN_STYLE = Automatic;
736 | COMBINE_HIDPI_IMAGES = YES;
737 | DEFINES_MODULE = YES;
738 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
739 | DYLIB_COMPATIBILITY_VERSION = 1;
740 | DYLIB_CURRENT_VERSION = 1;
741 | DYLIB_INSTALL_NAME_BASE = "@rpath";
742 | INFOPLIST_FILE = Info.plist;
743 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
744 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
745 | MACOSX_DEPLOYMENT_TARGET = 10.10;
746 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.Pantomime;
747 | PRODUCT_NAME = Pantomime;
748 | PROVISIONING_PROFILE_SPECIFIER = "";
749 | SDKROOT = macosx;
750 | SKIP_INSTALL = YES;
751 | SUPPORTED_PLATFORMS = macosx;
752 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
753 | SWIFT_VERSION = 4.0;
754 | VALID_ARCHS = "i386 x86_64";
755 | };
756 | name = Debug;
757 | };
758 | ACE597A51C18832E0031451F /* Release */ = {
759 | isa = XCBuildConfiguration;
760 | buildSettings = {
761 | CLANG_ENABLE_MODULES = YES;
762 | CODE_SIGN_IDENTITY = "Mac Developer";
763 | CODE_SIGN_STYLE = Automatic;
764 | COMBINE_HIDPI_IMAGES = YES;
765 | DEFINES_MODULE = YES;
766 | DEVELOPMENT_TEAM = W9ZVWEMJZF;
767 | DYLIB_COMPATIBILITY_VERSION = 1;
768 | DYLIB_CURRENT_VERSION = 1;
769 | DYLIB_INSTALL_NAME_BASE = "@rpath";
770 | INFOPLIST_FILE = Info.plist;
771 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
772 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
773 | MACOSX_DEPLOYMENT_TARGET = 10.10;
774 | PRODUCT_BUNDLE_IDENTIFIER = com.nordija.Pantomime;
775 | PRODUCT_NAME = Pantomime;
776 | PROVISIONING_PROFILE_SPECIFIER = "";
777 | SDKROOT = macosx;
778 | SKIP_INSTALL = YES;
779 | SUPPORTED_PLATFORMS = macosx;
780 | SWIFT_OPTIMIZATION_LEVEL = "-O";
781 | SWIFT_VERSION = 4.0;
782 | VALID_ARCHS = "i386 x86_64";
783 | };
784 | name = Release;
785 | };
786 | /* End XCBuildConfiguration section */
787 |
788 | /* Begin XCConfigurationList section */
789 | 9EDCE3DE1C09D211002FA4A7 /* Build configuration list for PBXProject "Pantomime" */ = {
790 | isa = XCConfigurationList;
791 | buildConfigurations = (
792 | 9EDCE3F61C09D211002FA4A7 /* Debug */,
793 | 9EDCE3F71C09D211002FA4A7 /* Release */,
794 | );
795 | defaultConfigurationIsVisible = 0;
796 | defaultConfigurationName = Release;
797 | };
798 | 9EDCE3F81C09D211002FA4A7 /* Build configuration list for PBXNativeTarget "Pantomime (iOS)" */ = {
799 | isa = XCConfigurationList;
800 | buildConfigurations = (
801 | 9EDCE3F91C09D211002FA4A7 /* Debug */,
802 | 9EDCE3FA1C09D211002FA4A7 /* Release */,
803 | );
804 | defaultConfigurationIsVisible = 0;
805 | defaultConfigurationName = Release;
806 | };
807 | 9EDCE3FB1C09D211002FA4A7 /* Build configuration list for PBXNativeTarget "PantomimeTests" */ = {
808 | isa = XCConfigurationList;
809 | buildConfigurations = (
810 | 9EDCE3FC1C09D211002FA4A7 /* Debug */,
811 | 9EDCE3FD1C09D211002FA4A7 /* Release */,
812 | );
813 | defaultConfigurationIsVisible = 0;
814 | defaultConfigurationName = Release;
815 | };
816 | ACE597921C18830D0031451F /* Build configuration list for PBXNativeTarget "Pantomime (tvOS)" */ = {
817 | isa = XCConfigurationList;
818 | buildConfigurations = (
819 | ACE597931C18830D0031451F /* Debug */,
820 | ACE597941C18830D0031451F /* Release */,
821 | );
822 | defaultConfigurationIsVisible = 0;
823 | defaultConfigurationName = Release;
824 | };
825 | ACE597A31C18832E0031451F /* Build configuration list for PBXNativeTarget "Pantomime (OSX)" */ = {
826 | isa = XCConfigurationList;
827 | buildConfigurations = (
828 | ACE597A41C18832E0031451F /* Debug */,
829 | ACE597A51C18832E0031451F /* Release */,
830 | );
831 | defaultConfigurationIsVisible = 0;
832 | defaultConfigurationName = Release;
833 | };
834 | /* End XCConfigurationList section */
835 | };
836 | rootObject = 9EDCE3DB1C09D211002FA4A7 /* Project object */;
837 | }
838 |
--------------------------------------------------------------------------------
/Pantomime.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Pantomime.xcodeproj/xcshareddata/xcschemes/Pantomime (OSX).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Pantomime.xcodeproj/xcshareddata/xcschemes/Pantomime (iOS).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/Pantomime.xcodeproj/xcshareddata/xcschemes/Pantomime (tvOS).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/PantomimeTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/PantomimeTests/PantomimeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PantomimeTests.swift
3 | // PantomimeTests
4 | //
5 | // Created by Thomas Christensen on 24/08/16.
6 | // Copyright © 2016 Nordija A/S. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Pantomime
11 |
12 | class PantomimeTests: XCTestCase {
13 |
14 | func testParseMediaPlaylist() {
15 | let bundle = Bundle(for: type(of: self))
16 | let path = bundle.path(forResource: "media", ofType: "m3u8")!
17 |
18 | let manifestBuilder = ManifestBuilder()
19 | let mediaPlaylist = manifestBuilder.parseMediaPlaylistFromFile(path, onMediaSegment: {
20 | (segment: MediaSegment) -> Void in
21 | XCTAssertNotNil(segment.sequence)
22 | })
23 |
24 | XCTAssert(mediaPlaylist.targetDuration == 10)
25 | XCTAssert(mediaPlaylist.mediaSequence == 0)
26 | XCTAssert(mediaPlaylist.segments.count == 3)
27 | XCTAssert(mediaPlaylist.segments[0].title == " no desc")
28 | XCTAssert(mediaPlaylist.segments[0].subrangeLength == 100)
29 | XCTAssert(mediaPlaylist.segments[0].subrangeStart == 40)
30 | XCTAssert(mediaPlaylist.segments[1].subrangeLength == 100)
31 | XCTAssert(mediaPlaylist.segments[1].subrangeStart == nil)
32 | XCTAssert(mediaPlaylist.segments[2].duration == Float(3.003))
33 | XCTAssert(mediaPlaylist.segments[2].path! == "http://media.example.com/third.ts")
34 | XCTAssert(mediaPlaylist.duration() == Float(21.021))
35 |
36 | if let path2 = bundle.path(forResource: "media2", ofType: "m3u8") {
37 | let mediaPlaylist2 = manifestBuilder.parseMediaPlaylistFromFile(path2, onMediaSegment: {
38 | (segment: MediaSegment) -> Void in
39 | XCTAssertNotNil(segment.sequence)
40 | })
41 | XCTAssertEqual(12, mediaPlaylist2.targetDuration, "Should have been read as 12 seconds")
42 | }
43 | }
44 |
45 | func testParseMasterPlaylist() {
46 | let bundle = Bundle(for: type(of: self))
47 | let path = bundle.path(forResource: "master", ofType: "m3u8")!
48 |
49 | let manifestBuilder = ManifestBuilder()
50 |
51 | let masterPlaylist = manifestBuilder.parseMasterPlaylistFromFile(path,
52 | onMediaPlaylist: {
53 | (playlist: MediaPlaylist) -> Void in
54 | XCTAssertNotNil(playlist.programId)
55 | XCTAssertNotNil(playlist.bandwidth)
56 | XCTAssertNotNil(playlist.path)
57 | })
58 |
59 | XCTAssert(masterPlaylist.playlists.count == 4)
60 | }
61 |
62 | /**
63 | * This would be the typical set up. Users will use Alomofire or similar libraries to
64 | * fetch the manifest files and then parse the text.
65 | */
66 | func testParseFromString() {
67 | let bundle = Bundle(for: type(of: self))
68 | let file = bundle.path(forResource: "master", ofType: "m3u8")!
69 | let path = URL(fileURLWithPath: file)
70 | do {
71 |
72 | let manifestText = try String(contentsOf: path, encoding: String.Encoding.utf8)
73 | let manifestBuilder = ManifestBuilder()
74 | let masterPlaylist = manifestBuilder.parseMasterPlaylistFromString(manifestText)
75 | XCTAssert(masterPlaylist.playlists.count == 4)
76 |
77 | } catch {
78 | XCTFail("Failed to read master playlist file")
79 | }
80 | }
81 |
82 | func testRealWorldParsing() {
83 | let manifestBuilder = ManifestBuilder()
84 |
85 | // Keep baseURL separate to contruct the nested media playlist URL's
86 | let baseURL = "http://devimages.apple.com/iphone/samples/bipbop"
87 | let path = "bipbopall.m3u8"
88 | let URL = Foundation.URL(string: baseURL + "/" + path)!
89 | XCTAssertEqual("http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8", URL.absoluteString)
90 |
91 | let expectation = self.expectation(description: "Testing parsing of the apple bipbop HTTP Live Stream sample")
92 |
93 | let session = URLSession.shared
94 |
95 | // Request master playlist
96 | let task = session.dataTask(with: URL, completionHandler: {
97 | data, response, error in
98 |
99 | XCTAssertNotNil(data, "data should not be nil")
100 | XCTAssertNil(error, "error should be nil")
101 |
102 | if let httpResponse = response as? HTTPURLResponse,
103 | let responseURL = httpResponse.url,
104 | let mimeType = httpResponse.mimeType {
105 |
106 | XCTAssertEqual(responseURL.absoluteString, URL.absoluteString, "No redirect expected")
107 | XCTAssertEqual(httpResponse.statusCode, 200, "HTTP response status code should be 200")
108 | XCTAssertEqual(mimeType, "audio/x-mpegurl", "HTTP response content type should be text/html")
109 |
110 | // Parse master playlist and perform verification of it
111 | if let dataFound = data, let manifestText = String(data: dataFound, encoding: String.Encoding.utf8) {
112 |
113 | let masterPlaylist = manifestBuilder.parseMasterPlaylistFromString(manifestText,
114 | onMediaPlaylist: {
115 | (mep: MediaPlaylist) -> Void in
116 |
117 | // Deduct full media playlist URL from path
118 | if let path = mep.path, let mepURL = Foundation.URL(string: baseURL + "/" + path) {
119 |
120 | // Request each found media playlist
121 | let mepTask = session.dataTask(with: mepURL, completionHandler: {
122 | mepData, mepResponse, mepError in
123 |
124 | XCTAssertNotNil(mepData, "data should not be nil")
125 | XCTAssertNil(mepError, "error should be nil")
126 |
127 | // Parse the media playlist and perform validation
128 | if let mepDataFound = mepData,
129 | let mepManifest = String(data: mepDataFound, encoding: String.Encoding.utf8) {
130 | let mediaPlaylist = manifestBuilder.parseMediaPlaylistFromString(mepManifest)
131 | XCTAssertEqual(181, mediaPlaylist.segments.count)
132 | }
133 |
134 | // In case we have requested, parsed and validated the last one
135 | if path.contains("gear4/prog_index.m3u8") {
136 | expectation.fulfill()
137 | }
138 | })
139 |
140 | mepTask.resume()
141 | }
142 | })
143 |
144 | XCTAssertEqual(4, masterPlaylist.playlists.count)
145 | }
146 | } else {
147 | XCTFail("Response was not NSHTTPURLResponse")
148 | }
149 | })
150 |
151 | task.resume()
152 |
153 | waitForExpectations(timeout: task.originalRequest!.timeoutInterval) {
154 | error in
155 | if let error = error {
156 | XCTFail("Error: \(error.localizedDescription)")
157 | }
158 | task.cancel()
159 | }
160 | }
161 |
162 | func testParsingJustUsingStringSource() {
163 | let builder = ManifestBuilder()
164 |
165 | // Check using String with contentsOfURL
166 | do {
167 | if let url = URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8") {
168 | let content = try String(contentsOf: url, encoding: String.Encoding.utf8)
169 | let master = builder.parseMasterPlaylistFromString(content)
170 | XCTAssertEqual(4, master.playlists.count, "Number of media playlists in master does not match")
171 | } else {
172 | XCTFail("Failed to create plain URL to bipbopall,m3u8")
173 | }
174 | } catch {
175 | XCTFail("Failed to just use string to read from various sources")
176 | }
177 | }
178 |
179 | func testSimpleFullParse() {
180 | let builder = ManifestBuilder()
181 | if let url = URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8") {
182 | let manifest = builder.parse(url)
183 | XCTAssertEqual(4, manifest.playlists.count)
184 | }
185 | }
186 |
187 | func testFullParse() {
188 | let builder = ManifestBuilder()
189 | if let url = URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8") {
190 | let manifest = builder.parse(url, onMediaPlaylist: {
191 | (media: MediaPlaylist) -> Void in
192 | XCTAssertNotNil(media.path)
193 | }, onMediaSegment: {
194 | (segment: MediaSegment) -> Void in
195 | let mediaManifestURL = url.URLByReplacingLastPathComponent(segment.mediaPlaylist!.path!)
196 | let segmentURL = mediaManifestURL!.URLByReplacingLastPathComponent(segment.path!)
197 | XCTAssertNotNil(segmentURL!.absoluteString)
198 | })
199 | XCTAssertEqual(4, manifest.playlists.count, "Number of media playlists in master does not match")
200 | XCTAssertEqual(181, manifest.playlists[3].segments.count, "Segments not correctly parsed")
201 | }
202 | }
203 |
204 | func testFullParseWithFullPathInManifests() {
205 | let builder = ManifestBuilder()
206 | if let url = URL(string: "https://mnmedias.api.telequebec.tv/m3u8/29880.m3u8") {
207 | let manifest = builder.parse(url, onMediaPlaylist: {
208 | (media: MediaPlaylist) -> Void in
209 | XCTAssertNotNil(media.path)
210 | }, onMediaSegment: {
211 | (segment: MediaSegment) -> Void in
212 | let mediaManifestURL = url.URLByReplacingLastPathComponent(segment.mediaPlaylist!.path!)
213 | let segmentURL = mediaManifestURL!.URLByReplacingLastPathComponent(segment.path!)
214 | XCTAssertNotNil(segmentURL!.absoluteString)
215 | })
216 | XCTAssertEqual(7, manifest.playlists.count, "Number of media playlists in master does not match")
217 | XCTAssertEqual(105, manifest.playlists[3].segments.count, "Segments not correctly parsed")
218 | }
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/PantomimeTests/PlaylistTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 29/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | import Pantomime
8 |
9 | class PlaylistTests: XCTestCase {
10 |
11 | func testMasterPlaylist() {
12 | let master = MasterPlaylist()
13 | XCTAssertNil(master.getPlaylist(0))
14 | XCTAssertNil(master.getPlaylist(5))
15 | let media = MediaPlaylist()
16 | master.addPlaylist(media)
17 | XCTAssertEqual(1, master.getPlaylistCount())
18 | XCTAssertNotNil(master.getPlaylist(0))
19 | XCTAssert(media === master.getPlaylist(0))
20 | master.path = "hello"
21 | XCTAssertEqual("hello", master.path)
22 | }
23 |
24 | func testMediaPlaylist() {
25 | let media = MediaPlaylist()
26 | let segment = MediaSegment()
27 | XCTAssertNil(media.getSegment(0))
28 | XCTAssertNil(media.getSegment(2))
29 | media.addSegment(segment)
30 | XCTAssertEqual(1, media.getSegmentCount())
31 | XCTAssertNotNil(media.getSegment(0))
32 | XCTAssert(segment === media.getSegment(0))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/PantomimeTests/ReaderTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 26/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import XCTest
7 | @testable import Pantomime
8 |
9 | class ReaderTests: XCTestCase {
10 | func testReaderBuilder() {
11 | do {
12 | let stringReader = try ReaderBuilder.createReader(.stringreader, reference: "This is a line\nThis is another")
13 | XCTAssert(stringReader is StringBufferedReader)
14 | XCTAssertEqual("This is a line", stringReader.readLine())
15 | XCTAssertEqual("This is another", stringReader.readLine())
16 | XCTAssertNil(stringReader.readLine())
17 | XCTAssertNil(stringReader.readLine())
18 |
19 | let bundle = Bundle(for: type(of: self))
20 | let path = bundle.path(forResource: "media", ofType: "m3u8")!
21 | let fileReader = try ReaderBuilder.createReader(.filereader, reference: path)
22 | XCTAssert(fileReader is FileBufferedReader)
23 | XCTAssertEqual("#EXTM3U", fileReader.readLine())
24 | XCTAssertEqual("#This is a comment", fileReader.readLine())
25 | for _ in 1...10 {
26 | _ = fileReader.readLine()!
27 | }
28 | XCTAssertNil(fileReader.readLine())
29 |
30 | let httpReader = try ReaderBuilder.createReader(.httpreader, reference: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")
31 | XCTAssert(httpReader is URLBufferedReader)
32 | XCTAssertEqual("#EXTM3U", httpReader.readLine())
33 | XCTAssertEqual("#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000", httpReader.readLine())
34 | XCTAssertEqual("gear1/prog_index.m3u8", httpReader.readLine())
35 | XCTAssertEqual("#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=311111", httpReader.readLine())
36 | XCTAssertEqual("gear2/prog_index.m3u8", httpReader.readLine())
37 | XCTAssertEqual("#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=484444", httpReader.readLine())
38 | XCTAssertEqual("gear3/prog_index.m3u8", httpReader.readLine())
39 | XCTAssertEqual("#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=737777", httpReader.readLine())
40 | XCTAssertEqual("gear4/prog_index.m3u8", httpReader.readLine())
41 | } catch {
42 | XCTFail("Not able to construct valid buffered reader instances")
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/PantomimeTests/master.m3u8:
--------------------------------------------------------------------------------
1 | #EXTM3U
2 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000
3 | gear1/prog_index.m3u8
4 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=311111
5 | gear2/prog_index.m3u8
6 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=484444
7 | gear3/prog_index.m3u8
8 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=737777
9 | gear4/prog_index.m3u8
10 |
11 |
--------------------------------------------------------------------------------
/PantomimeTests/media.m3u8:
--------------------------------------------------------------------------------
1 | #EXTM3U
2 | #This is a comment
3 | #EXT-X-TARGETDURATION:10
4 | #EXT-X-MEDIA-SEQUENCE:0
5 | #EXTINF:9.009, no desc
6 | #EXT-X-BYTERANGE:100@40
7 | http://media.example.com/first.ts
8 | #EXTINF:9.009,
9 | #EXT-X-BYTERANGE:100
10 | http://media.example.com/second.ts
11 | #EXTINF:3.003,
12 | http://media.example.com/third.ts
--------------------------------------------------------------------------------
/PantomimeTests/media2.m3u8:
--------------------------------------------------------------------------------
1 | #EXTM3U
2 | #EXT-X-VERSION:3
3 | #EXT-X-ALLOW-CACHE:NO
4 | #EXT-X-TARGETDURATION:12
5 | #EXT-X-MEDIA-SEQUENCE:29945
6 | #EXTINF:10.04,
7 | media_w346121679_b864000_sleng_29945.ts
8 | #EXTINF:9.92,
9 | media_w346121679_b864000_sleng_29946.ts
10 | #EXTINF:10.08,
11 | media_w346121679_b864000_sleng_29947.ts
12 | #EXTINF:10.08,
13 | media_w346121679_b864000_sleng_29948.ts
14 | #EXTINF:10.08,
15 | media_w346121679_b864000_sleng_29949.ts
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pantomime
2 |
3 | Pantomime is a lightweight framework for iOS, OSX and tvOS that can read and parse HTTP Live Streaming manifests.
4 |
5 | ## Latest build
6 |
7 | [](https://travis-ci.org/thomaschristensen/Pantomime)
8 |
9 | ## Installation
10 |
11 | You can use [Carthage](https://github.com/Carthage/Carthage) to install Pantomime by adding that to your Cartfile:
12 |
13 | ```
14 | github "thomaschristensen/Pantomime"
15 | ```
16 |
17 | #### via CocoaPods
18 |
19 | To use [CocoaPods](https://cocoapods.org) just add this to your Podfile:
20 |
21 | ``` Ruby
22 | pod 'Pantomime'
23 | ```
24 |
25 | #### via Swift Package Manager
26 |
27 | To use Pantomime as a [Swift Package Manager](https://swift.org/package-manager/) package just add the following in your Package.swift file.
28 |
29 | ``` Swift
30 | import PackageDescription
31 |
32 | let package = Package(
33 | name: "HelloWorld",
34 | dependencies: [
35 | .Package(url: "https://github.com/thomaschristensen/Pantomime", majorVersion: 0)
36 | ]
37 | )
38 | ```
39 |
40 |
41 |
42 | ## Usage
43 |
44 | To use the parser just do the following:
45 |
46 | ``` Swift
47 | import Pantomime
48 |
49 | let builder = ManifestBuilder()
50 | if let url = NSURL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8") {
51 | let manifest = builder.parse(url)
52 | }
53 | ```
54 |
55 | The ManifestBuilder's parse method expects a master playlist manifest
56 | to be found at the other end of the URL. Once this has been parsed
57 | all Media Playlist Manifests referred to in the master will also
58 | be fetched and parsed.
59 |
60 | ## Core Classes
61 |
62 | #### ManifestBuilder
63 | The manifest builder can parse both Master and Media playlist manifests.
64 | You can choose to let ManifestBuilder parse master and it's media
65 | playlists, or you can parse either the master or media playlists only.
66 |
67 | #### MasterPlaylist
68 | Represents a master playlist and it holds a reference to a list of
69 | media playlist objects
70 |
71 | #### MediaPlaylist
72 | The media playlist object contains a list of all video segments and
73 | other properties like target duration (max duration of each segment),
74 | path, version, bandwidth, program-id and the starting media sequence
75 | number.
76 |
77 | #### MediaSegment
78 | This object holds a reference to the actual video file (path), it's
79 | actual duration, sequence number and optional title.
80 |
81 | ## Helper Classes
82 |
83 | #### BufferedReader
84 | This is a protocol that defines how these text manifest documents can
85 | be read line by line. Various implementations of this protocol exist
86 | helping reading the documents from a File, a URL or from a String.
87 | These implementations are FileBufferedReader, URLBufferedReader and
88 | StringBufferedReader.
89 |
90 | #### ReaderBuilder
91 | Is a utility class that can be used to construct the actual
92 | implementation of BufferedReader by specifying which type is required.
93 | (This class is already deprecated. Use the implementations of
94 | BufferedReader directly)
95 |
96 | #### NSURLExtension
97 | An extension to the NSURL class has been made to assist in constructing
98 | the right URL when given relative paths in the various manifest files.
99 |
100 | ``` Swift
101 |
102 | let masterManifest = "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"
103 | let mediaManifest = "gear1/prog_index.m3u8"
104 |
105 | if let masterManifestURL = NSURL(string: masterManifest) {
106 | let mediaManifestURL = masterManifestURL.URLByReplacingLastPathComponent(mediaManifest)
107 | // mediaManifestURL now = http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8
108 | }
109 |
110 | ```
111 |
112 |
113 | ## License
114 |
115 | The Pantomime Framework is released under the [MIT License](https://github.com/thomaschristensen/Pantomime/blob/master/LICENSE).
116 |
117 | ## Todo
118 |
119 | * Construct Master and Media Playlist objects and write them as files
--------------------------------------------------------------------------------
/sources/BufferedReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 26/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | public protocol BufferedReader {
9 | func close()
10 |
11 | // Return next line or nil if no more lines can be read
12 | func readLine() -> String?
13 | }
14 |
--------------------------------------------------------------------------------
/sources/FileBufferedReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 26/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | open class FileBufferedReader: BufferedReader {
9 | var _fileName: String
10 | var streamReader: StreamReader
11 |
12 | public init(path: String) {
13 | _fileName = path
14 | streamReader = StreamReader(path: _fileName)!
15 | }
16 |
17 | open func close() {
18 | streamReader.close()
19 | }
20 |
21 | open func readLine() -> String? {
22 | return streamReader.nextLine()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sources/ManifestBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 25/08/16.
3 | // Copyright (c) 2016 Nordija A/S. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | /**
9 | * Parses HTTP Live Streaming manifest files
10 | * Use a BufferedReader to let the parser read from various sources.
11 | */
12 | open class ManifestBuilder {
13 |
14 | public init() {}
15 |
16 | /**
17 | * Parses Master playlist manifests
18 | */
19 | fileprivate func parseMasterPlaylist(_ reader: BufferedReader, onMediaPlaylist:
20 | ((_ playlist: MediaPlaylist) -> Void)?) -> MasterPlaylist {
21 | var masterPlaylist = MasterPlaylist()
22 | var currentMediaPlaylist: MediaPlaylist?
23 |
24 | defer {
25 | reader.close()
26 | }
27 | while let line = reader.readLine() {
28 | if line.isEmpty {
29 | // Skip empty lines
30 |
31 | } else if line.hasPrefix("#EXT") {
32 |
33 | // Tags
34 | if line.hasPrefix("#EXTM3U") {
35 | // Ok Do nothing
36 |
37 | } else if line.hasPrefix("#EXT-X-STREAM-INF") {
38 | // #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000
39 | currentMediaPlaylist = MediaPlaylist()
40 | do {
41 | let programIdString = try line.replace("(.*)=(\\d+),(.*)", replacement: "$2")
42 | let bandwidthString = try line.replace("(.*),(.*)=(\\d+)(.*)", replacement: "$3")
43 | if let currentMediaPlaylistExist = currentMediaPlaylist {
44 | currentMediaPlaylistExist.programId = Int(programIdString)!
45 | currentMediaPlaylistExist.bandwidth = Int(bandwidthString)!
46 | }
47 | } catch {
48 | print("Failed to parse program-id and bandwidth on master playlist. Line = \(line)")
49 | }
50 |
51 | }
52 | } else if line.hasPrefix("#") {
53 | // Comments are ignored
54 |
55 | } else {
56 | // URI - must be
57 | if let currentMediaPlaylistExist = currentMediaPlaylist {
58 | currentMediaPlaylistExist.path = line
59 | currentMediaPlaylistExist.masterPlaylist = masterPlaylist
60 | masterPlaylist.addPlaylist(currentMediaPlaylistExist)
61 | if let callableOnMediaPlaylist = onMediaPlaylist {
62 | callableOnMediaPlaylist(currentMediaPlaylistExist)
63 | }
64 | }
65 | }
66 | }
67 |
68 | return masterPlaylist
69 | }
70 |
71 | /**
72 | * Parses Media Playlist manifests
73 | */
74 | fileprivate func parseMediaPlaylist(_ reader: BufferedReader,
75 | mediaPlaylist: MediaPlaylist = MediaPlaylist(),
76 | onMediaSegment: ((_ segment: MediaSegment) -> Void)?) -> MediaPlaylist {
77 | var currentSegment: MediaSegment?
78 | var currentURI: String?
79 | var currentSequence = 0
80 |
81 | defer {
82 | reader.close()
83 | }
84 |
85 | while let line = reader.readLine() {
86 | if line.isEmpty {
87 | // Skip empty lines
88 |
89 | } else if line.hasPrefix("#EXT") {
90 |
91 | // Tags
92 | if line.hasPrefix("#EXTM3U") {
93 |
94 | // Ok Do nothing
95 | } else if line.hasPrefix("#EXT-X-VERSION") {
96 | do {
97 | let version = try line.replace("(.*):(\\d+)(.*)", replacement: "$2")
98 | mediaPlaylist.version = Int(version)
99 | } catch {
100 | print("Failed to parse the version of media playlist. Line = \(line)")
101 | }
102 |
103 | } else if line.hasPrefix("#EXT-X-TARGETDURATION") {
104 | do {
105 | let durationString = try line.replace("(.*):(\\d+)(.*)", replacement: "$2")
106 | mediaPlaylist.targetDuration = Int(durationString)
107 | } catch {
108 | print("Failed to parse the target duration of media playlist. Line = \(line)")
109 | }
110 |
111 | } else if line.hasPrefix("#EXT-X-MEDIA-SEQUENCE") {
112 | do {
113 | let mediaSequence = try line.replace("(.*):(\\d+)(.*)", replacement: "$2")
114 | if let mediaSequenceExtracted = Int(mediaSequence) {
115 | mediaPlaylist.mediaSequence = mediaSequenceExtracted
116 | currentSequence = mediaSequenceExtracted
117 | }
118 | } catch {
119 | print("Failed to parse the media sequence in media playlist. Line = \(line)")
120 | }
121 |
122 | } else if line.hasPrefix("#EXTINF") {
123 | currentSegment = MediaSegment()
124 | do {
125 | let segmentDurationString = try line.replace("(.*):(\\d.*),(.*)", replacement: "$2")
126 | let segmentTitle = try line.replace("(.*):(\\d.*),(.*)", replacement: "$3")
127 | currentSegment!.duration = Float(segmentDurationString)
128 | currentSegment!.title = segmentTitle
129 | } catch {
130 | print("Failed to parse the segment duration and title. Line = \(line)")
131 | }
132 | } else if line.hasPrefix("#EXT-X-BYTERANGE") {
133 | if line.contains("@") {
134 | do {
135 | let subrangeLength = try line.replace("(.*):(\\d.*)@(.*)", replacement: "$2")
136 | let subrangeStart = try line.replace("(.*):(\\d.*)@(.*)", replacement: "$3")
137 | currentSegment!.subrangeLength = Int(subrangeLength)
138 | currentSegment!.subrangeStart = Int(subrangeStart)
139 | } catch {
140 | print("Failed to parse byte range. Line = \(line)")
141 | }
142 | } else {
143 | do {
144 | let subrangeLength = try line.replace("(.*):(\\d.*)", replacement: "$2")
145 | currentSegment!.subrangeLength = Int(subrangeLength)
146 | currentSegment!.subrangeStart = nil
147 | } catch {
148 | print("Failed to parse the byte range. Line = \(line)")
149 | }
150 | }
151 | } else if line.hasPrefix("#EXT-X-DISCONTINUITY") {
152 | currentSegment!.discontinuity = true
153 | }
154 |
155 | } else if line.hasPrefix("#") {
156 | // Comments are ignored
157 |
158 | } else {
159 | // URI - must be
160 | if let currentSegmentExists = currentSegment {
161 | currentSegmentExists.mediaPlaylist = mediaPlaylist
162 | currentSegmentExists.path = line
163 | currentSegmentExists.sequence = currentSequence
164 | currentSequence += 1
165 | mediaPlaylist.addSegment(currentSegmentExists)
166 | if let callableOnMediaSegment = onMediaSegment {
167 | callableOnMediaSegment(currentSegmentExists)
168 | }
169 | }
170 | }
171 | }
172 |
173 | return mediaPlaylist
174 | }
175 |
176 | /**
177 | * Parses the master playlist manifest from a string document.
178 | *
179 | * Convenience method that uses a StringBufferedReader as source for the manifest.
180 | */
181 | open func parseMasterPlaylistFromString(_ string: String, onMediaPlaylist:
182 | ((_ playlist: MediaPlaylist) -> Void)? = nil) -> MasterPlaylist {
183 | return parseMasterPlaylist(StringBufferedReader(string: string), onMediaPlaylist: onMediaPlaylist)
184 | }
185 |
186 | /**
187 | * Parses the master playlist manifest from a file.
188 | *
189 | * Convenience method that uses a FileBufferedReader as source for the manifest.
190 | */
191 | open func parseMasterPlaylistFromFile(_ path: String, onMediaPlaylist:
192 | ((_ playlist: MediaPlaylist) -> Void)? = nil) -> MasterPlaylist {
193 | return parseMasterPlaylist(FileBufferedReader(path: path), onMediaPlaylist: onMediaPlaylist)
194 | }
195 |
196 | /**
197 | * Parses the master playlist manifest requested synchronous from a URL
198 | *
199 | * Convenience method that uses a URLBufferedReader as source for the manifest.
200 | */
201 | open func parseMasterPlaylistFromURL(_ url: URL, onMediaPlaylist:
202 | ((_ playlist: MediaPlaylist) -> Void)? = nil) -> MasterPlaylist {
203 | return parseMasterPlaylist(URLBufferedReader(uri: url), onMediaPlaylist: onMediaPlaylist)
204 | }
205 |
206 | /**
207 | * Parses the media playlist manifest from a string document.
208 | *
209 | * Convenience method that uses a StringBufferedReader as source for the manifest.
210 | */
211 | open func parseMediaPlaylistFromString(_ string: String,
212 | mediaPlaylist: MediaPlaylist = MediaPlaylist(),
213 | onMediaSegment:((_ segment: MediaSegment) -> Void)? = nil) -> MediaPlaylist {
214 | return parseMediaPlaylist(StringBufferedReader(string: string),
215 | mediaPlaylist: mediaPlaylist, onMediaSegment: onMediaSegment)
216 | }
217 |
218 | /**
219 | * Parses the media playlist manifest from a file document.
220 | *
221 | * Convenience method that uses a FileBufferedReader as source for the manifest.
222 | */
223 | open func parseMediaPlaylistFromFile(_ path: String,
224 | mediaPlaylist: MediaPlaylist = MediaPlaylist(),
225 | onMediaSegment: ((_ segment: MediaSegment) -> Void)? = nil) -> MediaPlaylist {
226 | return parseMediaPlaylist(FileBufferedReader(path: path),
227 | mediaPlaylist: mediaPlaylist, onMediaSegment: onMediaSegment)
228 | }
229 |
230 | /**
231 | * Parses the media playlist manifest requested synchronous from a URL
232 | *
233 | * Convenience method that uses a URLBufferedReader as source for the manifest.
234 | */
235 | @discardableResult
236 | open func parseMediaPlaylistFromURL(_ url: URL,
237 | mediaPlaylist: MediaPlaylist = MediaPlaylist(),
238 | onMediaSegment: ((_ segment: MediaSegment) -> Void)? = nil) -> MediaPlaylist {
239 | return parseMediaPlaylist(URLBufferedReader(uri: url),
240 | mediaPlaylist: mediaPlaylist, onMediaSegment: onMediaSegment)
241 | }
242 |
243 | /**
244 | * Parses the master manifest found at the URL and all the referenced media playlist manifests recursively.
245 | */
246 | open func parse(_ url: URL,
247 | onMediaPlaylist: ((_ playlist: MediaPlaylist) -> Void)? = nil,
248 | onMediaSegment: ((_ segment: MediaSegment) -> Void)? = nil) -> MasterPlaylist {
249 | // Parse master
250 | let master = parseMasterPlaylistFromURL(url, onMediaPlaylist: onMediaPlaylist)
251 | for playlist in master.playlists {
252 | if let path = playlist.path {
253 |
254 | // Detect if manifests are referred to with protocol
255 | if path.hasPrefix("http") || path.hasPrefix("file") {
256 | // Full path used
257 | if let mediaURL = URL(string: path) {
258 | parseMediaPlaylistFromURL(mediaURL,
259 | mediaPlaylist: playlist, onMediaSegment: onMediaSegment)
260 | }
261 | } else {
262 | // Relative path used
263 | if let mediaURL = url.URLByReplacingLastPathComponent(path) {
264 | parseMediaPlaylistFromURL(mediaURL,
265 | mediaPlaylist: playlist, onMediaSegment: onMediaSegment)
266 | }
267 | }
268 | }
269 | }
270 | return master
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/sources/MasterPlaylist.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MasterPlaylist.swift
3 | // Pantomime
4 | //
5 | // Created by Thomas Christensen on 25/08/16.
6 | // Copyright © 2016 Sebastian Kreutzberger. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class MasterPlaylist {
12 | var playlists = [MediaPlaylist]()
13 | open var path: String?
14 |
15 | public init() {}
16 |
17 | open func addPlaylist(_ playlist: MediaPlaylist) {
18 | playlists.append(playlist)
19 | }
20 |
21 | open func getPlaylist(_ index: Int) -> MediaPlaylist? {
22 | if index >= playlists.count {
23 | return nil
24 | }
25 | return playlists[index]
26 | }
27 |
28 | open func getPlaylistCount() -> Int {
29 | return playlists.count
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sources/MediaPlaylist.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 24/08/16.
3 | // Copyright (c) 2016 Nordija A/S. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | open class MediaPlaylist {
9 | var masterPlaylist: MasterPlaylist?
10 |
11 | open var programId: Int = 0
12 | open var bandwidth: Int = 0
13 | open var path: String?
14 | open var version: Int?
15 | open var targetDuration: Int?
16 | open var mediaSequence: Int?
17 | var segments = [MediaSegment]()
18 |
19 | public init() {
20 |
21 | }
22 |
23 | open func addSegment(_ segment: MediaSegment) {
24 | segments.append(segment)
25 | }
26 |
27 | open func getSegment(_ index: Int) -> MediaSegment? {
28 | if index >= segments.count {
29 | return nil
30 | }
31 | return segments[index]
32 | }
33 |
34 | open func getSegmentCount() -> Int {
35 | return segments.count
36 | }
37 |
38 | open func duration() -> Float {
39 | var dur: Float = 0.0
40 | for item in segments {
41 | dur += item.duration!
42 | }
43 | return dur
44 | }
45 |
46 | open func getMaster() -> MasterPlaylist? {
47 | return self.masterPlaylist
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/sources/MediaSegment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 24/08/16.
3 | // Copyright (c) 2016 Nordija A/S. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | open class MediaSegment {
9 | var mediaPlaylist: MediaPlaylist?
10 | open var duration: Float?
11 | open var sequence: Int = 0
12 | open var subrangeLength: Int?
13 | open var subrangeStart: Int?
14 | open var title: String?
15 | open var discontinuity: Bool = false
16 | open var path: String?
17 |
18 | public init() {
19 |
20 | }
21 |
22 | open func getMediaPlaylist() -> MediaPlaylist? {
23 | return self.mediaPlaylist
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sources/NSURLExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 27/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | // Extend the NSURL object with helpers
9 |
10 | public extension URL {
11 | /**
12 | Replaces the last path component of the URL with the path component and returns a new URL or nil.
13 |
14 | - Parameter pathComponent: path component to replace last path component with
15 |
16 | - Returns: A new URL object or nil
17 | */
18 | @available(iOS 4.0, *)
19 | public func URLByReplacingLastPathComponent(_ pathComponent: String) -> URL? {
20 | let tmpurl = self.deletingLastPathComponent()
21 | return tmpurl.appendingPathComponent(pathComponent)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sources/ReaderBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 26/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | open class ReaderBuilder {
9 |
10 | enum ReaderBuilderError: Error {
11 | case illegalReference(reference:String)
12 | }
13 |
14 | enum ReaderTypes {
15 | case stringreader, httpreader, filereader
16 | }
17 |
18 | static func createURLReader(_ reference: URL) -> BufferedReader {
19 | return URLBufferedReader(uri: reference)
20 | }
21 |
22 | static func createStringReader(_ reference: String) -> BufferedReader {
23 | return StringBufferedReader(string: reference)
24 | }
25 |
26 | static func createFileReader(_ reference: String) -> BufferedReader? {
27 | return FileBufferedReader(path: reference)
28 | }
29 |
30 | static func createReader(_ reader: ReaderTypes, reference: String) throws -> BufferedReader {
31 |
32 | switch reader {
33 | case .stringreader:
34 | return StringBufferedReader(string: reference)
35 | case .filereader:
36 | return FileBufferedReader(path: reference)
37 | case .httpreader:
38 | if let uriOK = URL(string: reference) {
39 | return URLBufferedReader(uri: uriOK)
40 | } else {
41 | throw ReaderBuilderError.illegalReference(reference: reference)
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/sources/StreamReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 24/08/16.
3 | // Copyright (c) 2016 Nordija A/S. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | class StreamReader {
9 |
10 | let encoding: String.Encoding
11 | let chunkSize: Int
12 |
13 | var fileHandle: FileHandle!
14 | let buffer: NSMutableData!
15 | let delimData: Data!
16 | var atEof: Bool = false
17 |
18 | init?(path: String, delimiter: String = "\n", encoding: String.Encoding = String.Encoding.utf8,
19 | chunkSize: Int = 4096) {
20 | self.chunkSize = chunkSize
21 | self.encoding = encoding
22 |
23 | if let fileHandle = FileHandle(forReadingAtPath: path),
24 | let delimData = delimiter.data(using: encoding),
25 | let buffer = NSMutableData(capacity: chunkSize) {
26 | self.fileHandle = fileHandle
27 | self.delimData = delimData
28 | self.buffer = buffer
29 | } else {
30 | self.fileHandle = nil
31 | self.delimData = nil
32 | self.buffer = nil
33 | return nil
34 | }
35 | }
36 |
37 | deinit {
38 | self.close()
39 | }
40 |
41 | /// Return next line, or nil on EOF.
42 | func nextLine() -> String? {
43 | precondition(fileHandle != nil, "Attempt to read from closed file")
44 |
45 | if atEof {
46 | return nil
47 | }
48 |
49 | // Read data chunks from file until a line delimiter is found:
50 |
51 | var range = buffer.range(of: delimData, options: [], in: NSRange(location: 0, length: buffer.length))
52 | while range.location == NSNotFound {
53 | let tmpData = fileHandle.readData(ofLength: chunkSize)
54 | if tmpData.isEmpty {
55 | // EOF or read error.
56 | atEof = true
57 | if buffer.length > 0 {
58 | // Buffer contains last line in file (not terminated by delimiter).
59 | let line = NSString(data: buffer as Data, encoding: encoding.rawValue)
60 |
61 | buffer.length = 0
62 | return line as String?
63 | }
64 | // No more lines.
65 | return nil
66 | }
67 | buffer.append(tmpData)
68 | range = buffer.range(of: delimData, options: [], in: NSRange(location: 0, length: buffer.length))
69 | }
70 |
71 | // Convert complete line (excluding the delimiter) to a string:
72 | let line = NSString(data: buffer.subdata(with: NSRange(location: 0, length: range.location)),
73 | encoding: encoding.rawValue)
74 | // Remove line (and the delimiter) from the buffer:
75 | buffer.replaceBytes( in: NSRange(location: 0, length: range.location + range.length),
76 | withBytes: nil, length: 0)
77 |
78 | return line as String?
79 | }
80 |
81 | /// Start reading from the beginning of file.
82 | func rewind() {
83 | fileHandle.seek(toFileOffset: 0)
84 | buffer.length = 0
85 | atEof = false
86 | }
87 |
88 | /// Close the underlying file. No reading must be done after calling this method.
89 | func close() {
90 | fileHandle?.closeFile()
91 | fileHandle = nil
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/sources/StringBufferedReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 26/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | /**
9 | * Uses a string as a stream and reads it line by line.
10 | */
11 |
12 | open class StringBufferedReader: BufferedReader {
13 | var _buffer: [String]
14 | var _line: Int
15 |
16 | public init(string: String) {
17 | _line = 0
18 | _buffer = string.components(separatedBy: CharacterSet.newlines)
19 | }
20 |
21 | open func close() {
22 | }
23 |
24 | open func readLine() -> String? {
25 | if _buffer.isEmpty || _buffer.count <= _line {
26 | return nil
27 | }
28 | let result = _buffer[_line]
29 | _line += 1
30 | return result
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sources/StringExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 24/08/16.
3 | // Copyright (c) 2016 Nordija A/S. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | // Extend the String object with helpers
9 | extension String {
10 |
11 | // String.replace(); similar to JavaScript's String.replace() and Ruby's String.gsub()
12 | func replace(_ pattern: String, replacement: String) throws -> String {
13 |
14 | let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
15 |
16 | return regex.stringByReplacingMatches(
17 | in: self,
18 | options: [.withTransparentBounds],
19 | range: NSRange(location: 0, length: self.count),
20 | withTemplate: replacement
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sources/URLBufferedReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Thomas Christensen on 26/08/16.
3 | // Copyright (c) 2016 Sebastian Kreutzberger. All rights reserved.
4 | //
5 |
6 | import Foundation
7 |
8 | /**
9 | * Reads the document found at the specified URL in one chunk synchonous
10 | * and then lets the readLine function pick it line by line.
11 | */
12 | open class URLBufferedReader: BufferedReader {
13 | var _uri: URL
14 | var _stringReader: StringBufferedReader
15 |
16 | public init(uri: URL) {
17 | _uri = uri
18 | _stringReader = StringBufferedReader(string: "")
19 | let request1: URLRequest = URLRequest(url: _uri)
20 | let response: AutoreleasingUnsafeMutablePointer? = nil
21 | do {
22 | let dataVal = try NSURLConnection.sendSynchronousRequest(request1, returning: response)
23 | let text = String(data: dataVal, encoding: String.Encoding.utf8)!
24 | _stringReader = StringBufferedReader(string: text)
25 | } catch {
26 | print("Failed to make request for content at \(_uri)")
27 | }
28 | }
29 |
30 | open func close() {
31 | _stringReader.close()
32 | }
33 |
34 | open func readLine() -> String? {
35 | return _stringReader.readLine()
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------