diff --git a/Example/DownloadToGo.xcodeproj/project.pbxproj b/Example/DownloadToGo.xcodeproj/project.pbxproj index 7f66715..4ea225b 100644 --- a/Example/DownloadToGo.xcodeproj/project.pbxproj +++ b/Example/DownloadToGo.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ CA0AA135222D198700FFF90F /* DownloadTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0AA134222D198700FFF90F /* DownloadTest.swift */; }; CA0AA138223021D900FFF90F /* items.json in Resources */ = {isa = PBXBuildFile; fileRef = CA0AA137223021BE00FFF90F /* items.json */; }; CAB012E1206CCBC400EB3F69 /* Samples.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB012E0206CCBC400EB3F69 /* Samples.swift */; }; + CAD71F59223E99CC0074CBA4 /* TestTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD71F58223E99CC0074CBA4 /* TestTools.swift */; }; + CAD71F5A223E99CC0074CBA4 /* TestTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD71F58223E99CC0074CBA4 /* TestTools.swift */; }; + CAD71F5C223EDAA40074CBA4 /* XCTestTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD71F5B223EDAA40074CBA4 /* XCTestTools.swift */; }; CAE8915B222BC015004ACD30 /* HLSLocalizerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE8915A222BC015004ACD30 /* HLSLocalizerTest.swift */; }; /* End PBXBuildFile section */ @@ -55,6 +58,8 @@ CA0AA134222D198700FFF90F /* DownloadTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTest.swift; sourceTree = ""; }; CA0AA137223021BE00FFF90F /* items.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = items.json; sourceTree = ""; }; CAB012E0206CCBC400EB3F69 /* Samples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Samples.swift; sourceTree = ""; }; + CAD71F58223E99CC0074CBA4 /* TestTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTools.swift; sourceTree = ""; }; + CAD71F5B223EDAA40074CBA4 /* XCTestTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = XCTestTools.swift; path = Tests/XCTestTools.swift; sourceTree = SOURCE_ROOT; }; CAE8915A222BC015004ACD30 /* HLSLocalizerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HLSLocalizerTest.swift; sourceTree = ""; }; DC9E0E3C7F4B72EB61E261A5 /* Pods-DownloadToGo_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DownloadToGo_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DownloadToGo_Example/Pods-DownloadToGo_Example.debug.xcconfig"; sourceTree = ""; }; EBD962729B0E7C93B9B967A2 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; @@ -124,6 +129,7 @@ 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 607FACD31AFB9204008FA782 /* Supporting Files */, CAB012E0206CCBC400EB3F69 /* Samples.swift */, + CAD71F58223E99CC0074CBA4 /* TestTools.swift */, ); name = "Example for DownloadToGo"; path = DownloadToGo; @@ -144,6 +150,7 @@ 607FACE91AFB9204008FA782 /* Supporting Files */, CAE8915A222BC015004ACD30 /* HLSLocalizerTest.swift */, CA0AA134222D198700FFF90F /* DownloadTest.swift */, + CAD71F5B223EDAA40074CBA4 /* XCTestTools.swift */, ); path = Tests; sourceTree = ""; @@ -379,6 +386,7 @@ 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, CAB012E1206CCBC400EB3F69 /* Samples.swift in Sources */, 20AD661C1F174B0900E8E6D7 /* VideoViewController.swift in Sources */, + CAD71F59223E99CC0074CBA4 /* TestTools.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -386,8 +394,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CAD71F5C223EDAA40074CBA4 /* XCTestTools.swift in Sources */, CAE8915B222BC015004ACD30 /* HLSLocalizerTest.swift in Sources */, CA0AA135222D198700FFF90F /* DownloadTest.swift in Sources */, + CAD71F5A223E99CC0074CBA4 /* TestTools.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/DownloadToGo.xcodeproj/xcshareddata/xcschemes/DownloadToGo-Example.xcscheme b/Example/DownloadToGo.xcodeproj/xcshareddata/xcschemes/DownloadToGo-Example.xcscheme index f0ecf81..a4d5242 100644 --- a/Example/DownloadToGo.xcodeproj/xcshareddata/xcschemes/DownloadToGo-Example.xcscheme +++ b/Example/DownloadToGo.xcodeproj/xcshareddata/xcschemes/DownloadToGo-Example.xcscheme @@ -42,7 +42,7 @@ buildForAnalyzing = "YES"> @@ -54,6 +54,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + codeCoverageEnabled = "YES" shouldUseLaunchSchemeArgsEnv = "YES"> - - BuildSystemType - Original - + diff --git a/Example/DownloadToGo/TestTools.swift b/Example/DownloadToGo/TestTools.swift new file mode 100644 index 0000000..9687a50 --- /dev/null +++ b/Example/DownloadToGo/TestTools.swift @@ -0,0 +1,103 @@ +// +// TestTools.swift +// DownloadToGo +// +// Created by Noam Tamim on 17/03/2019. +// Copyright © 2019 CocoaPods. All rights reserved. +// + +import PlayKit +import PlayKitProviders +import DownloadToGo + +let setSmallerOfflineDRMExpirationMinutes: Int? = 5 +//let setSmallerOfflineDRMExpirationMinutes: Int? = nil + + +let defaultEnv = "http://cdnapi.kaltura.com" + +struct ItemJSON: Codable { + let id: String + let title: String? + let partnerId: Int? + let ks: String? + let env: String = defaultEnv + + let url: String? + + let options: OptionsJSON? + + let expected: ExpectedValues? +} + +struct ExpectedValues: Codable { + let estimatedSize: Int64? + let downloadedSize: Int64? + let audioLangs: [String]? + let textLangs: [String]? +} + +struct OptionsJSON: Codable { + let audioLangs: [String]? + let allAudioLangs: Bool? + let textLangs: [String]? + let allTextLangs: Bool? + let videoCodecs: [String]? + let audioCodecs: [String]? + let videoWidth: Int? + let videoHeight: Int? + let videoBitrates: [String:Int]? + let allowInefficientCodecs: Bool? + + func toOptions() -> DTGSelectionOptions { + let opts = DTGSelectionOptions() + + opts.allAudioLanguages = allAudioLangs ?? false + opts.audioLanguages = audioLangs + + opts.allTextLanguages = allTextLangs ?? false + opts.textLanguages = textLangs + + opts.allowInefficientCodecs = allowInefficientCodecs ?? false + + if let codecs = audioCodecs { + opts.audioCodecs = codecs.compactMap({ (tag) -> DTGSelectionOptions.AudioCodec? in + switch tag { + case "mp4a": return .mp4a + case "ac3": return .ac3 + case "eac3", "ec3": return .eac3 + default: return nil + } + }) + } + + if let codecs = videoCodecs { + opts.videoCodecs = codecs.compactMap({ (tag) -> DTGSelectionOptions.VideoCodec? in + switch tag { + case "avc1": return .avc1 + case "hevc", "hvc1": return .hevc + default: return nil + } + }) + } + + opts.videoWidth = videoWidth + opts.videoHeight = videoHeight + + if let bitrates = videoBitrates { + for (codecId, bitrate) in bitrates { + let codec: DTGSelectionOptions.VideoCodec + switch codecId { + case "avc1": codec = .avc1 + case "hevc", "hvc1": codec = .hevc + default: continue + } + + opts.setMinVideoBitrate(codec, bitrate) + } + } + + return opts + } +} + diff --git a/Example/DownloadToGo/ViewController.swift b/Example/DownloadToGo/ViewController.swift index f7eb27c..0f874c0 100644 --- a/Example/DownloadToGo/ViewController.swift +++ b/Example/DownloadToGo/ViewController.swift @@ -12,103 +12,8 @@ import Toast import PlayKit import PlayKitProviders -let setSmallerOfflineDRMExpirationMinutes: Int? = 5 -//let setSmallerOfflineDRMExpirationMinutes: Int? = nil - let defaultAudioBitrateEstimation: Int = 64000 - -struct ItemJSON: Codable { - let id: String - let title: String? - let partnerId: Int? - let ks: String? - let env: String? - - let url: String? - - let options: OptionsJSON? - - func toItem() -> Item { - let item: Item - let title = self.title ?? self.id - if let partnerId = self.partnerId { - item = Item(title, id: self.id, partnerId: partnerId, ks: self.ks, env: self.env) - } else if let url = self.url { - item = Item(title, id: self.id, url: url) - } else { - fatalError("Invalid item, missing `partnerId` and `url`") - } - item.options = options?.toOptions() - - return item - } -} - -struct OptionsJSON: Codable { - let audioLangs: [String]? - let allAudioLangs: Bool? - let textLangs: [String]? - let allTextLangs: Bool? - let videoCodecs: [String]? - let audioCodecs: [String]? - let videoWidth: Int? - let videoHeight: Int? - let videoBitrates: [String:Int]? - let allowInefficientCodecs: Bool? - - func toOptions() -> DTGSelectionOptions { - let opts = DTGSelectionOptions() - - opts.allAudioLanguages = allAudioLangs ?? false - opts.audioLanguages = audioLangs - - opts.allTextLanguages = allTextLangs ?? false - opts.textLanguages = textLangs - - opts.allowInefficientCodecs = allowInefficientCodecs ?? false - - if let codecs = audioCodecs { - opts.audioCodecs = codecs.compactMap({ (tag) -> DTGSelectionOptions.AudioCodec? in - switch tag { - case "mp4a": return .mp4a - case "ac3": return .ac3 - case "eac3", "ec3": return .eac3 - default: return nil - } - }) - } - - if let codecs = videoCodecs { - opts.videoCodecs = codecs.compactMap({ (tag) -> DTGSelectionOptions.VideoCodec? in - switch tag { - case "avc1": return .avc1 - case "hevc", "hvc1": return .hevc - default: return nil - } - }) - } - - opts.videoWidth = videoWidth - opts.videoHeight = videoHeight - - if let bitrates = videoBitrates { - for (codecId, bitrate) in bitrates { - let codec: DTGSelectionOptions.VideoCodec - switch codecId { - case "avc1": codec = .avc1 - case "hevc", "hvc1": codec = .hevc - default: continue - } - - opts.setMinVideoBitrate(codec, bitrate) - } - } - - return opts - } -} - class Item { static let defaultEnv = "http://cdnapi.kaltura.com" let id: String @@ -119,7 +24,21 @@ class Item { var entry: PKMediaEntry? var options: DTGSelectionOptions? - + var expected: ExpectedValues? + + convenience init(json: ItemJSON) { + let title = json.title ?? json.id + + if let partnerId = json.partnerId { + self.init(title, id: json.id, partnerId: partnerId, ks: json.ks, env: json.env) + } else if let url = json.url { + self.init(title, id: json.id, url: url) + } else { + fatalError("Invalid item, missing `partnerId` and `url`") + } + self.options = json.options?.toOptions() + } + init(_ title: String, id: String, url: String) { self.id = id self.title = title @@ -157,6 +76,7 @@ class Item { } } + class ViewController: UIViewController { let dummyFileName = "dummyfile" let videoViewControllerSegueIdentifier = "videoViewController" @@ -214,7 +134,7 @@ class ViewController: UIViewController { let json = try! Data(contentsOf: jsonURL) let loadedItems = try! JSONDecoder().decode([ItemJSON].self, from: json) - items = loadedItems.map{$0.toItem()} + items = loadedItems.map{Item(json: $0)} let completedItems = try! self.cm.itemsByState(.completed) for (index, item) in completedItems.enumerated() { diff --git a/Example/DownloadToGo/items.json b/Example/DownloadToGo/items.json index de189ef..56d424c 100644 --- a/Example/DownloadToGo/items.json +++ b/Example/DownloadToGo/items.json @@ -4,7 +4,13 @@ "options": { "allAudioLangs": true, "allTextLangs": true, - } + }, + "expected": { + "estimatedSize": 0, + "downloadedSize": 0, + "audioLangs": ["en"], + "textLangs": [], + } }, { "title": "AES-128 multi-key", diff --git a/Example/Tests/DownloadTest.swift b/Example/Tests/DownloadTest.swift index adeb934..b7858a7 100644 --- a/Example/Tests/DownloadTest.swift +++ b/Example/Tests/DownloadTest.swift @@ -10,12 +10,16 @@ import XCTest @testable import DownloadToGo import PlayKit +import PlayKitProviders + class DownloadTest: XCTestCase, ContentManagerDelegate { var downloadedExp: XCTestExpectation? var id: String? + static var items: [ItemJSON]! + // It's not possible to play on travis because of the microphone permission issue (https://forums.developer.apple.com/thread/110423) #if targetEnvironment(simulator) static let dontPlay = FileManager.default.fileExists(atPath: "/tmp/DontPlay") @@ -23,43 +27,14 @@ class DownloadTest: XCTestCase, ContentManagerDelegate { static let dontPlay = false #endif - func item(id: String, didDownloadData totalBytesDownloaded: Int64, totalBytesEstimated: Int64?) { - print(id, "\(Double(totalBytesDownloaded)/1024/1024) / \(Double(totalBytesEstimated ?? -1)/1024/1024)") - } - func item(id: String, didChangeToState newState: DTGItemState, error: Error?) { + override class func setUp() { - print("QQQ item \(id) moved to state \(newState)") + let jsonURL = Bundle.main.url(forResource: "items", withExtension: "json")! + // let jsonURL = URL(string: "http://localhost/items.json")! + let json = try! Data(contentsOf: jsonURL) + items = try! JSONDecoder().decode([ItemJSON].self, from: json) - if let selfId = self.id { - if newState == .completed { - assert(id == selfId, "Id doesn't match") - print("QQQ item \(id) completed") - - // Check if it's in completed state - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // Only in travis, sometimes it takes a while until the state is reflected in db. - eq(self.item().state, DTGItemState.completed) - self.downloadedExp?.fulfill() - } - } - } else { - // setUp - assert(newState == .removed) - print("QQQ Removed \(id) in setUp()") - } - } - - let cm = ContentManager.shared - - func waitForDownload(_ timeout: TimeInterval = 300) { - if let e = downloadedExp { - wait(for: [e], timeout: timeout) - print("QQQ download fulfilled") - } - } - - override class func setUp() { if dontPlay { print("TRAVIS DETECTED, WILL NOT PLAY") @@ -87,6 +62,7 @@ class DownloadTest: XCTestCase, ContentManagerDelegate { } override func setUp() { + cm.delegate = self } @@ -95,6 +71,44 @@ class DownloadTest: XCTestCase, ContentManagerDelegate { try! cm.removeItem(id: id) } + + + func item(id: String, didDownloadData totalBytesDownloaded: Int64, totalBytesEstimated: Int64?) { + print(id, "\(Double(totalBytesDownloaded)/1024/1024) / \(Double(totalBytesEstimated ?? -1)/1024/1024)") + } + + func item(id: String, didChangeToState newState: DTGItemState, error: Error?) { + + print("QQQ item \(id) moved to state \(newState)") + + if let selfId = self.id { + if newState == .completed { + assert(id == selfId, "Id doesn't match") + print("QQQ item \(id) completed") + + // Check if it's in completed state + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // Only in travis, sometimes it takes a while until the state is reflected in db. + eq(self.item().state, DTGItemState.completed) + self.downloadedExp?.fulfill() + } + } + } else { + // setUp + assert(newState == .removed) + print("QQQ Removed \(id) in setUp()") + } + } + + let cm = ContentManager.shared + + func waitForDownload(_ timeout: TimeInterval = 300) { + if let e = downloadedExp { + wait(for: [e], timeout: timeout) + print("QQQ download fulfilled") + } + } + func item(_ id: String) -> DTGItem? { if let item = try! cm.itemById(id) { @@ -172,7 +186,23 @@ class DownloadTest: XCTestCase, ContentManagerDelegate { tracks.fulfill() } + let reached5sec = XCTestExpectation(description: "reached 5 seconds \(id!)") + let ended = XCTestExpectation(description: "ended \(id!)") + + player.addObserver(self, event: PlayerEvent.playheadUpdate) { (e) in + if let time = e.currentTime, time.floatValue >= 5.0 { + print("QQQ reached 5 sec!") + reached5sec.fulfill() + } + } + + player.addObserver(self, event: PlayerEvent.ended) { (e) in + print("QQQ ended!") + ended.fulfill() + } + player.addObserver(self, event: PlayerEvent.canPlay) { (e) in + print("QQQ can play!") canPlay.fulfill() } @@ -180,28 +210,57 @@ class DownloadTest: XCTestCase, ContentManagerDelegate { print("QQQ prepare \(entry)") player.prepare(MediaConfig(mediaEntry: entry)) - wait(for: [canPlay, tracks], timeout: 10) + wait(for: [canPlay, tracks], timeout: 2) + + player.play() + + wait(for: [reached5sec], timeout: 6) + + player.seek(to: player.duration - 2) + wait(for: [ended], timeout: 4) + player.destroy() - } - func testBasicDownload_1() { - newItem("http://cdntesting.qa.mkaltura.com/p/1091/sp/109100/playManifest/entryId/0_mskmqcit/format/applehttp/protocol/http/a.m3u8") - loadItem(basic()) - - eq(item().estimatedSize, 47_197_225) + func _testFromJSON() { + for it in DownloadTest.items { + + guard let url = it.url else {continue} + + newItem(url, it.id) + loadItem(it.options?.toOptions()) + + if let est = it.expected?.estimatedSize { + eq(item().estimatedSize, est) + } + + startItem() + waitForDownload() + + if let est = it.expected?.downloadedSize { + eq(item().downloadedSize, est) + } + + playItem() + } + } + + func testSmallBunny() { + newItem("https://noamtamim.com/hls-bunny/index.m3u8") + loadItem(basic().setMinVideoBitrate(.avc1, 100)) + eq(item().estimatedSize, 5066000) startItem() waitForDownload() - eq(item().downloadedSize, 47_229_736) + eq(item().downloadedSize, 5156458) playItem() } - - func check(_ id: String = #function) { - newItem("http://cdntesting.qa.mkaltura.com/p/1091/sp/109100/playManifest/entryId/0_mskmqcit/format/applehttp/protocol/http/a.m3u8", id) + + func testBasicDownload_1() { + newItem("http://cdntesting.qa.mkaltura.com/p/1091/sp/109100/playManifest/entryId/0_mskmqcit/format/applehttp/protocol/http/a.m3u8") loadItem(basic()) eq(item().estimatedSize, 47_197_225) @@ -210,38 +269,8 @@ class DownloadTest: XCTestCase, ContentManagerDelegate { waitForDownload() eq(item().downloadedSize, 47_229_736) - } - - func test1() { - check() - } - - func test2() { - check() - } - - func test3() { - check() - } - - func test4() { - check() - } - - func test5() { - check() - } - - func test6() { - check() - } - - func test7() { - check() - } - - func test8() { - check() + + playItem() } func testBasicDownload_2() { diff --git a/Example/Tests/HLSLocalizerTest.swift b/Example/Tests/HLSLocalizerTest.swift index 5a4568a..84aaccc 100644 --- a/Example/Tests/HLSLocalizerTest.swift +++ b/Example/Tests/HLSLocalizerTest.swift @@ -10,11 +10,6 @@ import XCTest @testable import DownloadToGo -public func eq(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: String = "", file: StaticString = #file, line: UInt = #line) where T : Equatable { - XCTAssertEqual(expression1, expression2, message, file: file, line: line) -} - - class HLSLocalizerTest: XCTestCase { let bundleURL = Bundle(for: HLSLocalizerTest.self).bundleURL diff --git a/Example/Tests/XCTestTools.swift b/Example/Tests/XCTestTools.swift new file mode 100644 index 0000000..ce36b10 --- /dev/null +++ b/Example/Tests/XCTestTools.swift @@ -0,0 +1,16 @@ +// +// XCTestTools.swift +// DownloadToGo_Tests +// +// Created by Noam Tamim on 17/03/2019. +// Copyright © 2019 CocoaPods. All rights reserved. +// + +import XCTest + + +public func eq(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: String = "", file: StaticString = #file, line: UInt = #line) where T : Equatable { + XCTAssertEqual(expression1, expression2, message, file: file, line: line) +} + + diff --git a/Sources/Queue.swift b/Sources/Queue.swift index 3a8560a..c584ab6 100644 --- a/Sources/Queue.swift +++ b/Sources/Queue.swift @@ -70,12 +70,4 @@ struct Queue { mutating func purge() { self.array.removeAll() } - - var front: T? { - if self.isEmpty { - return nil - } else { - return self.array[self.head] - } - } } diff --git a/jenkins-build.sh b/jenkins-build.sh new file mode 100755 index 0000000..b745b99 --- /dev/null +++ b/jenkins-build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +pushd Example +pod install + +xcodebuild test -derivedDataPath build -workspace DownloadToGo.xcworkspace -scheme DownloadToGo-Example -sdk iphonesimulator \ + ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,name=iPhone X' | tee xcodebuild.log | xcpretty -r html + +popd +mkdir out +mv Example/build/reports/tests.html Example/xcodebuild.log out +