From ed351842cd16f7fdc9a18ba398079c23e4357326 Mon Sep 17 00:00:00 2001 From: Iain Munro Date: Sun, 2 Apr 2017 01:26:58 +0200 Subject: [PATCH 1/2] Added support for extended EXTINF format with properties --- Pantomime.xcodeproj/project.pbxproj | 4 ++++ PantomimeTests/PantomimeTests.swift | 14 +++++++++++++- PantomimeTests/ReaderTests.swift | 2 ++ PantomimeTests/media.m3u8 | 4 +++- PantomimeTests/media3.m3u8 | 4 ++++ sources/ManifestBuilder.swift | 27 +++++++++++++++++++++++++-- sources/MediaPlaylist.swift | 3 +++ sources/MediaSegment.swift | 1 + 8 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 PantomimeTests/media3.m3u8 diff --git a/Pantomime.xcodeproj/project.pbxproj b/Pantomime.xcodeproj/project.pbxproj index 2236fe4..17755ab 100755 --- a/Pantomime.xcodeproj/project.pbxproj +++ b/Pantomime.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 27EDFCB21E9061B400945246 /* media3.m3u8 in Resources */ = {isa = PBXBuildFile; fileRef = 27EDFCB11E9061B400945246 /* media3.m3u8 */; }; 5ED0327E1D6F1710006DE1F3 /* ManifestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */; }; 5ED0327F1D6F1710006DE1F3 /* MediaSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */; }; 5ED032801D6F1710006DE1F3 /* MediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */; }; @@ -69,6 +70,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 27EDFCB11E9061B400945246 /* media3.m3u8 */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = media3.m3u8; sourceTree = ""; }; 5ED032791D6F1710006DE1F3 /* ManifestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ManifestBuilder.swift; path = sources/ManifestBuilder.swift; sourceTree = ""; }; 5ED0327A1D6F1710006DE1F3 /* MediaSegment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaSegment.swift; path = sources/MediaSegment.swift; sourceTree = ""; }; 5ED0327B1D6F1710006DE1F3 /* MediaPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaPlaylist.swift; path = sources/MediaPlaylist.swift; sourceTree = ""; }; @@ -188,6 +190,7 @@ 9EDCE3F31C09D211002FA4A7 /* PantomimeTests.swift */, 7FBCA10A0EB4EAA0CD4AB5EF /* ReaderTests.swift */, 7FBCAFC77C9DA4C80FAEA0C2 /* PlaylistTests.swift */, + 27EDFCB11E9061B400945246 /* media3.m3u8 */, ); path = PantomimeTests; sourceTree = ""; @@ -354,6 +357,7 @@ buildActionMask = 2147483647; files = ( 5ED0328F1D6F17BF006DE1F3 /* media2.m3u8 in Resources */, + 27EDFCB21E9061B400945246 /* media3.m3u8 in Resources */, 5ED032961D6F7618006DE1F3 /* master.m3u8 in Resources */, 5ED032901D6F17BF006DE1F3 /* media.m3u8 in Resources */, ); diff --git a/PantomimeTests/PantomimeTests.swift b/PantomimeTests/PantomimeTests.swift index b0638fd..04dee73 100644 --- a/PantomimeTests/PantomimeTests.swift +++ b/PantomimeTests/PantomimeTests.swift @@ -10,6 +10,18 @@ import XCTest @testable import Pantomime class PantomimeTests: XCTestCase { + + func test3ParseMediaPlaylist() { + let bundle = Bundle(for: type(of: self)) + let path = bundle.path(forResource: "media3", ofType: "m3u8")! + + let manifestBuilder = ManifestBuilder() + let mediaPlaylist = manifestBuilder.parseMediaPlaylistFromFile(path) + + XCTAssert(mediaPlaylist.segments.count == 1) + XCTAssert(mediaPlaylist.segments[0].title == "Hey this is working!") + XCTAssert(mediaPlaylist.segments[0].properties?["tvg-name"] == "example") + } func testParseMediaPlaylist() { let bundle = Bundle(for: type(of: self)) @@ -23,7 +35,7 @@ class PantomimeTests: XCTestCase { XCTAssert(mediaPlaylist.targetDuration == 10) XCTAssert(mediaPlaylist.mediaSequence == 0) - XCTAssert(mediaPlaylist.segments.count == 3) + XCTAssert(mediaPlaylist.segments.count == 4) XCTAssert(mediaPlaylist.segments[0].title == " no desc") XCTAssert(mediaPlaylist.segments[0].subrangeLength == 100) XCTAssert(mediaPlaylist.segments[0].subrangeStart == 40) diff --git a/PantomimeTests/ReaderTests.swift b/PantomimeTests/ReaderTests.swift index c99b7fc..8496a30 100644 --- a/PantomimeTests/ReaderTests.swift +++ b/PantomimeTests/ReaderTests.swift @@ -25,6 +25,8 @@ class ReaderTests: XCTestCase { for _ in 1...10 { _ = fileReader.readLine()! } + XCTAssertEqual("#EXTINF:-1 tvg-ID=\"\" tvg-name=\"example\" custom-attribute=\"\" group-title=\"another example\",title: of channel", fileReader.readLine()) + XCTAssertEqual("http://media.example.com/fourth.ts", fileReader.readLine()) XCTAssertNil(fileReader.readLine()) let httpReader = try ReaderBuilder.createReader(.httpreader, reference: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8") diff --git a/PantomimeTests/media.m3u8 b/PantomimeTests/media.m3u8 index b2a7a68..fa18c88 100644 --- a/PantomimeTests/media.m3u8 +++ b/PantomimeTests/media.m3u8 @@ -9,4 +9,6 @@ http://media.example.com/first.ts #EXT-X-BYTERANGE:100 http://media.example.com/second.ts #EXTINF:3.003, -http://media.example.com/third.ts \ No newline at end of file +http://media.example.com/third.ts +#EXTINF:-1 tvg-ID="" tvg-name="example" custom-attribute="" group-title="another example",title: of channel +http://media.example.com/fourth.ts diff --git a/PantomimeTests/media3.m3u8 b/PantomimeTests/media3.m3u8 new file mode 100644 index 0000000..14136c0 --- /dev/null +++ b/PantomimeTests/media3.m3u8 @@ -0,0 +1,4 @@ +#EXTM3U +#This is a comment +#EXTINF:-1 tvg-ID="" tvg-name="example" custom-attribute="" group-title="another example",Hey this is working! +http://media.example.com/fourth.ts diff --git a/sources/ManifestBuilder.swift b/sources/ManifestBuilder.swift index bc12e09..7e64768 100644 --- a/sources/ManifestBuilder.swift +++ b/sources/ManifestBuilder.swift @@ -121,8 +121,12 @@ open class ManifestBuilder { } else if line.hasPrefix("#EXTINF") { currentSegment = MediaSegment() do { - let segmentDurationString = try line.replace("(.*):(\\d.*),(.*)", replacement: "$2") - let segmentTitle = try line.replace("(.*):(\\d.*),(.*)", replacement: "$3") + let generalRegex = "(.*):(-?[0-9]\\d*(\\.\\d+)?)(.*),(.*)" + let segmentDurationString = try line.replace(generalRegex, replacement: "$2") + let rawProperties = try line.replace(generalRegex, replacement: "$4") + let segmentTitle = try line.replace(generalRegex, replacement: "$5") + + currentSegment!.properties = getProperties(in: rawProperties) currentSegment!.duration = Float(segmentDurationString) currentSegment!.title = segmentTitle } catch { @@ -172,6 +176,25 @@ open class ManifestBuilder { return mediaPlaylist } + func getProperties(in text: String) -> [String:String]? { + do { + let regex = try NSRegularExpression(pattern: "([a-zA-z-]*)=\"([a-zA-z0-9 ]*+)\"") + let nsString = text as NSString + let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length)) + let keys = results.map { nsString.substring(with: $0.rangeAt(1))} + let values = results.map { nsString.substring(with: $0.rangeAt(2))} + var result = [String:String]() + for (index, value) in values.enumerated() { + result[keys[index]] = value + } + return result +// return results.map { nsString.substring(with: $0.range)} + } catch let error { + print("Error: \(error.localizedDescription)") + return nil + } + } + /** * Parses the master playlist manifest from a string document. * diff --git a/sources/MediaPlaylist.swift b/sources/MediaPlaylist.swift index 7c884c8..7ed3200 100644 --- a/sources/MediaPlaylist.swift +++ b/sources/MediaPlaylist.swift @@ -38,6 +38,9 @@ open class MediaPlaylist { open func duration() -> Float { var dur: Float = 0.0 for item in segments { + if item.duration == nil || item.duration! <= 0 { + continue + } dur = dur + item.duration! } return dur diff --git a/sources/MediaSegment.swift b/sources/MediaSegment.swift index d5e3af0..f1a8184 100644 --- a/sources/MediaSegment.swift +++ b/sources/MediaSegment.swift @@ -14,6 +14,7 @@ open class MediaSegment { open var title: String? open var discontinuity: Bool = false open var path: String? + open var properties: [String:String]? public init() { From 6568d074c746701d66120d4f0708f6a0ab188154 Mon Sep 17 00:00:00 2001 From: Iain Munro Date: Sun, 2 Apr 2017 03:14:12 +0200 Subject: [PATCH 2/2] Improved properties regex --- PantomimeTests/media3.m3u8 | 2 ++ sources/ManifestBuilder.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/PantomimeTests/media3.m3u8 b/PantomimeTests/media3.m3u8 index 14136c0..55b2030 100644 --- a/PantomimeTests/media3.m3u8 +++ b/PantomimeTests/media3.m3u8 @@ -2,3 +2,5 @@ #This is a comment #EXTINF:-1 tvg-ID="" tvg-name="example" custom-attribute="" group-title="another example",Hey this is working! http://media.example.com/fourth.ts +#EXTINF:-1 tvg-ID="" tvg-name="Kingsman: The Secret Service" tvg-logo="https://images-na.ssl-images-amazon.com/images/M/MV5BMTkxMjgwMDM4Ml5BMl5BanBnXkFtZTgwMTk3NTIwNDE@._V1_SY1000_CR0,0,675,1000_AL_.jpg" group-title="VOD: Action",Kingsman: The Secret Service +http://client-proiptv.com:8080/movie/AwC7WSEgSy/C1IxqxuT2k/9745.mkv diff --git a/sources/ManifestBuilder.swift b/sources/ManifestBuilder.swift index 7e64768..9847460 100644 --- a/sources/ManifestBuilder.swift +++ b/sources/ManifestBuilder.swift @@ -178,7 +178,7 @@ open class ManifestBuilder { func getProperties(in text: String) -> [String:String]? { do { - let regex = try NSRegularExpression(pattern: "([a-zA-z-]*)=\"([a-zA-z0-9 ]*+)\"") + let regex = try NSRegularExpression(pattern: "([a-zA-z-]*)=\"(.*?)\"") let nsString = text as NSString let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length)) let keys = results.map { nsString.substring(with: $0.rangeAt(1))}