├── .github └── workflows │ └── swift.yml ├── .gitignore ├── Example ├── iOS Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── iOS Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── PlayerView.swift │ └── ViewController.swift ├── LICENSE.md ├── Package.swift ├── README.md ├── RxAVFoundation.podspec ├── Sources └── RxAVFoundation │ ├── AVAsynchronousKeyValueLoading+Rx.swift │ ├── AVPlayer+Rx.swift │ ├── AVPlayerItem+Rx.swift │ └── AVPlayerLayer+Rx.swift └── Tests └── RxAVFoundationTests ├── RxAVAsynchronousKeyValueLoadingTests.swift ├── RxAVPlayerItemTests.swift ├── RxAVPlayerLayerTests.swift ├── RxAVPlayerTests.swift └── TestHelpers.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | ## Playgrounds 29 | timeline.xctimeline 30 | playground.xcworkspace 31 | 32 | # Swift Package Manager 33 | # 34 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 35 | # Packages/ 36 | .swiftpm 37 | .build/ 38 | Package.resolved 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | Pods/ 47 | Example/Pods/ 48 | 49 | # Carthage 50 | # 51 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 52 | # Carthage/Checkouts 53 | 54 | Carthage/Build 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 59 | # screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 62 | 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | 68 | .DS_Store 69 | -------------------------------------------------------------------------------- /Example/iOS Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 40547DFF1CB0787D000F5383 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40547DFE1CB0787D000F5383 /* AppDelegate.swift */; }; 11 | 40547E011CB0787D000F5383 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40547E001CB0787D000F5383 /* ViewController.swift */; }; 12 | 40547E041CB0787D000F5383 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 40547E021CB0787D000F5383 /* Main.storyboard */; }; 13 | 40547E061CB0787D000F5383 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 40547E051CB0787D000F5383 /* Assets.xcassets */; }; 14 | 40547E091CB0787D000F5383 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 40547E071CB0787D000F5383 /* LaunchScreen.storyboard */; }; 15 | 40547E151CB07943000F5383 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40547E141CB07943000F5383 /* PlayerView.swift */; }; 16 | CD106F5B2525AF48002D44AC /* RxAVFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CD106F5A2525AF48002D44AC /* RxAVFoundation */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 40547E131CB07921000F5383 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 40547DFB1CB0787D000F5383 /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 40547DFE1CB0787D000F5383 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 40547E001CB0787D000F5383 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 36 | 40547E031CB0787D000F5383 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 37 | 40547E051CB0787D000F5383 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 40547E081CB0787D000F5383 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | 40547E0A1CB0787D000F5383 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 40547E141CB07943000F5383 /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; 41 | CA58B08B0B9BA4964FBB9832 /* Pods_iOS_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 40547DF81CB0787D000F5383 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | CD106F5B2525AF48002D44AC /* RxAVFoundation in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 40547DF21CB0787D000F5383 = { 57 | isa = PBXGroup; 58 | children = ( 59 | 40547DFD1CB0787D000F5383 /* iOS Example */, 60 | 40547DFC1CB0787D000F5383 /* Products */, 61 | DC91C328CD701522A1078E96 /* Frameworks */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 40547DFC1CB0787D000F5383 /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 40547DFB1CB0787D000F5383 /* iOS Example.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 40547DFD1CB0787D000F5383 /* iOS Example */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 40547DFE1CB0787D000F5383 /* AppDelegate.swift */, 77 | 40547E001CB0787D000F5383 /* ViewController.swift */, 78 | 40547E021CB0787D000F5383 /* Main.storyboard */, 79 | 40547E051CB0787D000F5383 /* Assets.xcassets */, 80 | 40547E071CB0787D000F5383 /* LaunchScreen.storyboard */, 81 | 40547E0A1CB0787D000F5383 /* Info.plist */, 82 | 40547E141CB07943000F5383 /* PlayerView.swift */, 83 | ); 84 | path = "iOS Example"; 85 | sourceTree = ""; 86 | }; 87 | DC91C328CD701522A1078E96 /* Frameworks */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | CA58B08B0B9BA4964FBB9832 /* Pods_iOS_Example.framework */, 91 | ); 92 | name = Frameworks; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 40547DFA1CB0787D000F5383 /* iOS Example */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 40547E0D1CB0787D000F5383 /* Build configuration list for PBXNativeTarget "iOS Example" */; 101 | buildPhases = ( 102 | 40547DF71CB0787D000F5383 /* Sources */, 103 | 40547DF81CB0787D000F5383 /* Frameworks */, 104 | 40547DF91CB0787D000F5383 /* Resources */, 105 | 40547E131CB07921000F5383 /* Embed Frameworks */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = "iOS Example"; 112 | packageProductDependencies = ( 113 | CD106F5A2525AF48002D44AC /* RxAVFoundation */, 114 | ); 115 | productName = "iOS Example"; 116 | productReference = 40547DFB1CB0787D000F5383 /* iOS Example.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | 40547DF31CB0787D000F5383 /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | LastSwiftUpdateCheck = 0730; 126 | LastUpgradeCheck = 0810; 127 | ORGANIZATIONNAME = YayNext; 128 | TargetAttributes = { 129 | 40547DFA1CB0787D000F5383 = { 130 | CreatedOnToolsVersion = 7.3; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 40547DF61CB0787D000F5383 /* Build configuration list for PBXProject "iOS Example" */; 135 | compatibilityVersion = "Xcode 3.2"; 136 | developmentRegion = English; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | English, 140 | en, 141 | Base, 142 | ); 143 | mainGroup = 40547DF21CB0787D000F5383; 144 | packageReferences = ( 145 | CD106F592525AF48002D44AC /* XCRemoteSwiftPackageReference "RxAVFoundation" */, 146 | ); 147 | productRefGroup = 40547DFC1CB0787D000F5383 /* Products */; 148 | projectDirPath = ""; 149 | projectRoot = ""; 150 | targets = ( 151 | 40547DFA1CB0787D000F5383 /* iOS Example */, 152 | ); 153 | }; 154 | /* End PBXProject section */ 155 | 156 | /* Begin PBXResourcesBuildPhase section */ 157 | 40547DF91CB0787D000F5383 /* Resources */ = { 158 | isa = PBXResourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 40547E091CB0787D000F5383 /* LaunchScreen.storyboard in Resources */, 162 | 40547E061CB0787D000F5383 /* Assets.xcassets in Resources */, 163 | 40547E041CB0787D000F5383 /* Main.storyboard in Resources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXResourcesBuildPhase section */ 168 | 169 | /* Begin PBXSourcesBuildPhase section */ 170 | 40547DF71CB0787D000F5383 /* Sources */ = { 171 | isa = PBXSourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | 40547E151CB07943000F5383 /* PlayerView.swift in Sources */, 175 | 40547E011CB0787D000F5383 /* ViewController.swift in Sources */, 176 | 40547DFF1CB0787D000F5383 /* AppDelegate.swift in Sources */, 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | }; 180 | /* End PBXSourcesBuildPhase section */ 181 | 182 | /* Begin PBXVariantGroup section */ 183 | 40547E021CB0787D000F5383 /* Main.storyboard */ = { 184 | isa = PBXVariantGroup; 185 | children = ( 186 | 40547E031CB0787D000F5383 /* Base */, 187 | ); 188 | name = Main.storyboard; 189 | sourceTree = ""; 190 | }; 191 | 40547E071CB0787D000F5383 /* LaunchScreen.storyboard */ = { 192 | isa = PBXVariantGroup; 193 | children = ( 194 | 40547E081CB0787D000F5383 /* Base */, 195 | ); 196 | name = LaunchScreen.storyboard; 197 | sourceTree = ""; 198 | }; 199 | /* End PBXVariantGroup section */ 200 | 201 | /* Begin XCBuildConfiguration section */ 202 | 40547E0B1CB0787D000F5383 /* Debug */ = { 203 | isa = XCBuildConfiguration; 204 | buildSettings = { 205 | ALWAYS_SEARCH_USER_PATHS = NO; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_WARN_BOOL_CONVERSION = YES; 212 | CLANG_WARN_CONSTANT_CONVERSION = YES; 213 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 220 | CLANG_WARN_UNREACHABLE_CODE = YES; 221 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 222 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 223 | COPY_PHASE_STRIP = NO; 224 | DEBUG_INFORMATION_FORMAT = dwarf; 225 | ENABLE_STRICT_OBJC_MSGSEND = YES; 226 | ENABLE_TESTABILITY = YES; 227 | GCC_C_LANGUAGE_STANDARD = gnu99; 228 | GCC_DYNAMIC_NO_PIC = NO; 229 | GCC_NO_COMMON_BLOCKS = YES; 230 | GCC_OPTIMIZATION_LEVEL = 0; 231 | GCC_PREPROCESSOR_DEFINITIONS = ( 232 | "DEBUG=1", 233 | "$(inherited)", 234 | ); 235 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 236 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 237 | GCC_WARN_UNDECLARED_SELECTOR = YES; 238 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 239 | GCC_WARN_UNUSED_FUNCTION = YES; 240 | GCC_WARN_UNUSED_VARIABLE = YES; 241 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 242 | MTL_ENABLE_DEBUG_INFO = YES; 243 | ONLY_ACTIVE_ARCH = YES; 244 | SDKROOT = iphoneos; 245 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 246 | SWIFT_VERSION = 4.2; 247 | TARGETED_DEVICE_FAMILY = "1,2"; 248 | }; 249 | name = Debug; 250 | }; 251 | 40547E0C1CB0787D000F5383 /* Release */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ALWAYS_SEARCH_USER_PATHS = NO; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_WARN_BOOL_CONVERSION = YES; 261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 263 | CLANG_WARN_EMPTY_BODY = YES; 264 | CLANG_WARN_ENUM_CONVERSION = YES; 265 | CLANG_WARN_INFINITE_RECURSION = YES; 266 | CLANG_WARN_INT_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu99; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | SDKROOT = iphoneos; 287 | SWIFT_COMPILATION_MODE = wholemodule; 288 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 289 | SWIFT_VERSION = 4.2; 290 | TARGETED_DEVICE_FAMILY = "1,2"; 291 | VALIDATE_PRODUCT = YES; 292 | }; 293 | name = Release; 294 | }; 295 | 40547E0E1CB0787D000F5383 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | INFOPLIST_FILE = "iOS Example/Info.plist"; 300 | LD_RUNPATH_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "@executable_path/Frameworks", 303 | ); 304 | PRODUCT_BUNDLE_IDENTIFIER = "com.YayNext.iOS-Example"; 305 | PRODUCT_NAME = "$(TARGET_NAME)"; 306 | SWIFT_VERSION = 4.2; 307 | }; 308 | name = Debug; 309 | }; 310 | 40547E0F1CB0787D000F5383 /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 314 | INFOPLIST_FILE = "iOS Example/Info.plist"; 315 | LD_RUNPATH_SEARCH_PATHS = ( 316 | "$(inherited)", 317 | "@executable_path/Frameworks", 318 | ); 319 | PRODUCT_BUNDLE_IDENTIFIER = "com.YayNext.iOS-Example"; 320 | PRODUCT_NAME = "$(TARGET_NAME)"; 321 | SWIFT_VERSION = 4.2; 322 | }; 323 | name = Release; 324 | }; 325 | /* End XCBuildConfiguration section */ 326 | 327 | /* Begin XCConfigurationList section */ 328 | 40547DF61CB0787D000F5383 /* Build configuration list for PBXProject "iOS Example" */ = { 329 | isa = XCConfigurationList; 330 | buildConfigurations = ( 331 | 40547E0B1CB0787D000F5383 /* Debug */, 332 | 40547E0C1CB0787D000F5383 /* Release */, 333 | ); 334 | defaultConfigurationIsVisible = 0; 335 | defaultConfigurationName = Release; 336 | }; 337 | 40547E0D1CB0787D000F5383 /* Build configuration list for PBXNativeTarget "iOS Example" */ = { 338 | isa = XCConfigurationList; 339 | buildConfigurations = ( 340 | 40547E0E1CB0787D000F5383 /* Debug */, 341 | 40547E0F1CB0787D000F5383 /* Release */, 342 | ); 343 | defaultConfigurationIsVisible = 0; 344 | defaultConfigurationName = Release; 345 | }; 346 | /* End XCConfigurationList section */ 347 | 348 | /* Begin XCRemoteSwiftPackageReference section */ 349 | CD106F592525AF48002D44AC /* XCRemoteSwiftPackageReference "RxAVFoundation" */ = { 350 | isa = XCRemoteSwiftPackageReference; 351 | repositoryURL = "../"; 352 | requirement = { 353 | kind = upToNextMajorVersion; 354 | minimumVersion = 2.0.0; 355 | }; 356 | }; 357 | /* End XCRemoteSwiftPackageReference section */ 358 | 359 | /* Begin XCSwiftPackageProductDependency section */ 360 | CD106F5A2525AF48002D44AC /* RxAVFoundation */ = { 361 | isa = XCSwiftPackageProductDependency; 362 | package = CD106F592525AF48002D44AC /* XCRemoteSwiftPackageReference "RxAVFoundation" */; 363 | productName = RxAVFoundation; 364 | }; 365 | /* End XCSwiftPackageProductDependency section */ 366 | }; 367 | rootObject = 40547DF31CB0787D000F5383 /* Project object */; 368 | } 369 | -------------------------------------------------------------------------------- /Example/iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/iOS Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS Example 4 | // 5 | // Created by Patrick Mick on 4/2/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 16 | return true 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/iOS Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Example/iOS Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Example/iOS Example/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/iOS Example/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerView.swift 3 | // iOS Example 4 | // 5 | // Created by Patrick Mick on 4/2/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | class PlayerView: UIView { 13 | var playerLayer: AVPlayerLayer { 14 | return self.layer as! AVPlayerLayer 15 | } 16 | 17 | override class var layerClass: AnyClass { 18 | return AVPlayerLayer.self 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/iOS Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // iOS Example 4 | // 5 | // Created by Patrick Mick on 4/2/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxAVFoundation 11 | import RxSwift 12 | import RxCocoa 13 | import AVFoundation 14 | 15 | class ViewController: UIViewController { 16 | 17 | @IBOutlet var progressView: UIProgressView! 18 | @IBOutlet var playerView: PlayerView! 19 | @IBOutlet var activityIndicator: UIActivityIndicatorView! 20 | 21 | let player = AVPlayer() 22 | let disposeBag = DisposeBag() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | let item = AVPlayerItem(url: URL(string: "https://i.imgur.com/9rGrj10.mp4")!) 28 | player.replaceCurrentItem(with: item) 29 | 30 | playerView.playerLayer.videoGravity = .resizeAspectFill 31 | playerView.playerLayer.player = player 32 | 33 | setupProgressObservation(item: item) 34 | 35 | // TODO: Build out this example by hiding loading indicator and pausing 36 | // adding a gesture recognizer to pause/resume playback etc. 37 | 38 | player.rx.status 39 | .filter { $0 == .readyToPlay } 40 | .subscribe(onNext: { [unowned self] status in 41 | print("item ready to play") 42 | self.player.play() 43 | }).disposed(by: disposeBag) 44 | } 45 | 46 | private func setupProgressObservation(item: AVPlayerItem) { 47 | let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) 48 | player.rx.periodicTimeObserver(interval: interval) 49 | .map { [unowned self] in self.progress(currentTime: $0, duration: item.duration) } 50 | .bind(to: progressView.rx.progress) 51 | .disposed(by: disposeBag) 52 | } 53 | 54 | private func progress(currentTime: CMTime, duration: CMTime) -> Float { 55 | if !duration.isValid || !currentTime.isValid { 56 | return 0 57 | } 58 | 59 | let totalSeconds = duration.seconds 60 | let currentSeconds = currentTime.seconds 61 | 62 | if !totalSeconds.isFinite || !currentSeconds.isFinite { 63 | return 0 64 | } 65 | 66 | let p = Float(min(currentSeconds/totalSeconds, 1)) 67 | return p 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Patrick Mick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "RxAVFoundation", 7 | platforms: [ 8 | .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v3) 9 | ], 10 | products: [ 11 | .library( 12 | name: "RxAVFoundation", 13 | targets: ["RxAVFoundation"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "RxAVFoundation", 21 | dependencies: ["RxSwift", "RxCocoa"]), 22 | .testTarget( 23 | name: "RxAVFoundationTests", 24 | dependencies: ["RxAVFoundation"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxAVPlayer 2 | 3 | RxSwift extensions for some AVFoundation classes 4 | 5 | ## Installation 6 | 7 | Add the following to your podfile to use the framework. 8 | 9 | ``` 10 | pod 'RxAVFoundation' 11 | ``` 12 | 13 | ## Currently Supported Classes 14 | 15 | ### Playback 16 | 17 | - [x] AVPlayer 18 | - [x] AVPlayerItem 19 | - [x] AVPlayerLayer 20 | 21 | ### Reading, Writing, and Reencoding Assets 22 | 23 | - [x] AVAsynchronousKeyValueLoading 24 | - [ ] ALAssetsLibrary: this isn't part of AVFoundation - likely for another pod or might already exist 25 | - [ ] AVAssetExportSession 26 | 27 | ### Thumbnails 28 | 29 | - [ ] AVAssetImageGenerator 30 | 31 | ### Editing 32 | 33 | ### Still and Video Media Capture 34 | -------------------------------------------------------------------------------- /RxAVFoundation.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint ProgressController.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = "RxAVFoundation" 11 | s.version = "4.0.0" 12 | s.summary = "Functional Reactive (RxSwift) extensions for AVFoundations" 13 | s.homepage = "https://github.com/pmick/RxAVFoundation" 14 | s.license = 'MIT' 15 | s.author = { "Patrick Mick" => "patrickmick1@gmail.com" } 16 | s.source = { :git => "https://github.com/pmick/RxAVFoundation.git", :tag => s.version.to_s } 17 | s.social_media_url = "http://twitter.com/patrickmick" 18 | 19 | s.swift_version = '5.0' 20 | s.ios.deployment_target = '9.0' 21 | s.tvos.deployment_target = '9.0' 22 | s.requires_arc = true 23 | 24 | s.source_files = 'Sources/RxAVFoundation/*.swift' 25 | 26 | s.dependency 'RxSwift', '~> 6.0' 27 | s.dependency 'RxCocoa', '~> 6.0' 28 | end 29 | -------------------------------------------------------------------------------- /Sources/RxAVFoundation/AVAsynchronousKeyValueLoading+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVAsynchronousKeyValueLoading+Rx.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/1/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import RxSwift 12 | import RxCocoa 13 | 14 | extension Reactive where Base: AVAsynchronousKeyValueLoading { 15 | public func loadValuesForKeys(_ keys: [String]) -> Observable { 16 | return Observable.create { observer in 17 | self.base.loadValuesAsynchronously(forKeys: keys) { 18 | // TODO: Test statusOfValueForKey for every key that was loaded and 19 | // return some kind of error model if any keys failed to load 20 | 21 | observer.onNext(()) 22 | observer.onCompleted() 23 | } 24 | 25 | return Disposables.create() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/RxAVFoundation/AVPlayer+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPlayer+Rx.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 3/30/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import RxSwift 12 | import RxCocoa 13 | 14 | extension Reactive where Base: AVPlayer { 15 | public var rate: Observable { 16 | return self.observe(Float.self, #keyPath(AVPlayer.rate)) 17 | .map { $0 ?? 0 } 18 | } 19 | 20 | public var currentItem: Observable { 21 | return observe(AVPlayerItem.self, #keyPath(AVPlayer.currentItem)) 22 | } 23 | 24 | public var status: Observable { 25 | return self.observe(AVPlayer.Status.self, #keyPath(AVPlayer.status)) 26 | .map { $0 ?? .unknown } 27 | } 28 | 29 | public var error: Observable { 30 | return self.observe(NSError.self, #keyPath(AVPlayer.error)) 31 | } 32 | 33 | @available(iOS 10.0, tvOS 10.0, macOS 10.12, *) 34 | public var reasonForWaitingToPlay: Observable { 35 | return self.observe(AVPlayer.WaitingReason.self, #keyPath(AVPlayer.reasonForWaitingToPlay)) 36 | } 37 | 38 | @available(iOS 10.0, tvOS 10.0, *, OSX 10.12, *) 39 | public var timeControlStatus: Observable { 40 | return self.observe(AVPlayer.TimeControlStatus.self, #keyPath(AVPlayer.timeControlStatus)) 41 | .map { $0 ?? .waitingToPlayAtSpecifiedRate } 42 | } 43 | 44 | public func periodicTimeObserver(interval: CMTime) -> Observable { 45 | return Observable.create { observer in 46 | let t = self.base.addPeriodicTimeObserver(forInterval: interval, queue: nil) { time in 47 | observer.onNext(time) 48 | } 49 | 50 | return Disposables.create { self.base.removeTimeObserver(t) } 51 | } 52 | } 53 | 54 | public func boundaryTimeObserver(times: [CMTime]) -> Observable { 55 | return Observable.create { observer in 56 | let timeValues = times.map() { NSValue(time: $0) } 57 | let t = self.base.addBoundaryTimeObserver(forTimes: timeValues, queue: nil) { 58 | observer.onNext(()) 59 | } 60 | 61 | return Disposables.create { self.base.removeTimeObserver(t) } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/RxAVFoundation/AVPlayerItem+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPlayerItem+Rx.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/1/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import RxSwift 12 | import RxCocoa 13 | 14 | extension Reactive where Base: AVPlayerItem { 15 | public var status: Observable { 16 | return self.observe(AVPlayerItem.Status.self, #keyPath(AVPlayerItem.status)) 17 | .map { $0 ?? .unknown } 18 | } 19 | 20 | public var error: Observable { 21 | return self.observe(NSError.self, #keyPath(AVPlayerItem.error)) 22 | } 23 | 24 | public var duration: Observable { 25 | return self.observe(CMTime.self, #keyPath(AVPlayerItem.duration)) 26 | .map { $0 ?? .zero } 27 | } 28 | 29 | public var playbackLikelyToKeepUp: Observable { 30 | return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp)) 31 | .map { $0 ?? false } 32 | } 33 | 34 | public var playbackBufferFull: Observable { 35 | return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackBufferFull)) 36 | .map { $0 ?? false } 37 | } 38 | 39 | public var playbackBufferEmpty: Observable { 40 | return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackBufferEmpty)) 41 | .map { $0 ?? false } 42 | } 43 | 44 | public var didPlayToEnd: Observable { 45 | let ns = NotificationCenter.default 46 | return ns.rx.notification(.AVPlayerItemDidPlayToEndTime, object: base) 47 | } 48 | 49 | public var loadedTimeRanges: Observable<[CMTimeRange]> { 50 | return self.observe([NSValue].self, #keyPath(AVPlayerItem.loadedTimeRanges)) 51 | .map { $0 ?? [] } 52 | .map { values in values.map { $0.timeRangeValue } } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/RxAVFoundation/AVPlayerLayer+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPlayerLayer+Rx.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/4/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import RxSwift 12 | import RxCocoa 13 | 14 | extension Reactive where Base: AVPlayerLayer { 15 | public var isReadyForDisplay: Observable { 16 | return self.observe(Bool.self, #keyPath(AVPlayerLayer.isReadyForDisplay)) 17 | .map { $0 ?? false } 18 | } 19 | 20 | @available(*, deprecated, renamed: "isReadyForDisplay") 21 | public var readyForDisplay: Observable { 22 | isReadyForDisplay 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/RxAVFoundationTests/RxAVAsynchronousKeyValueLoadingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxAVAsynchronousKeyValueLoadingTests.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/1/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import AVFoundation 11 | 12 | class RxAVAsynchronousKeyValueLoadingTests: XCTestCase { 13 | func testCallingLoadValuesForKeys_CallsTheAsynchronousLoadingFunction() { 14 | class MockAsset: AVURLAsset { 15 | var calledWithKeys: [String]! 16 | fileprivate override func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)?) { 17 | self.calledWithKeys = keys 18 | } 19 | } 20 | 21 | let asset = MockAsset(url: URL.test) 22 | 23 | let keys = ["duration"] 24 | asset.rx.loadValuesForKeys(keys) 25 | .subscribe(onNext: {}) 26 | .dispose() 27 | 28 | XCTAssertEqual(asset.calledWithKeys, keys) 29 | } 30 | 31 | func testNextAndCompletedShouldBeEmitted_WhenCompletionIsCalled() { 32 | class MockAsset: AVURLAsset { 33 | fileprivate override func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)?) { 34 | handler!() 35 | } 36 | } 37 | 38 | let asset = MockAsset(url: URL.test) 39 | var nextCalled = false 40 | var completedCalled = false 41 | 42 | let keys = ["duration"] 43 | let o = asset.rx.loadValuesForKeys(keys) 44 | o.subscribe(onNext: { nextCalled = true }) 45 | .dispose() 46 | o.subscribe(onCompleted: { completedCalled = true }) 47 | .dispose() 48 | 49 | XCTAssertTrue(nextCalled) 50 | XCTAssertTrue(completedCalled) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/RxAVFoundationTests/RxAVPlayerItemTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxAVPlayerItemTests.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/1/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import RxAVFoundation 11 | import AVFoundation 12 | 13 | class RxAVPlayerItemTests: XCTestCase { 14 | let asset = AVURLAsset(url: URL(string: "www.google.com")!) 15 | 16 | //MARK: Status 17 | 18 | func testPlayerItem_ShouldAllowObservationOfStatus() { 19 | let sut = AVPlayerItem(asset: asset) 20 | var capturedStatus: AVPlayerItem.Status? 21 | sut.rx.status 22 | .subscribe(onNext: { capturedStatus = $0 }) 23 | .dispose() 24 | 25 | XCTAssertEqual(capturedStatus, AVPlayerItem.Status.unknown) 26 | } 27 | 28 | func testPlayerItem_ShouldUpdateRxStatus() { 29 | class MockItem: AVPlayerItem { 30 | fileprivate override var status: AVPlayerItem.Status { 31 | return .readyToPlay 32 | } 33 | } 34 | 35 | let sut = MockItem(asset: asset) 36 | var capturedStatus: AVPlayerItem.Status? 37 | sut.rx.status 38 | .subscribe(onNext: { capturedStatus = $0 }) 39 | .dispose() 40 | 41 | XCTAssertEqual(capturedStatus, AVPlayerItem.Status.readyToPlay) 42 | } 43 | 44 | // MARK: Error 45 | 46 | func testPlayerItem_ShouldAllowObservationOfError() { 47 | let sut = AVPlayerItem(asset: asset) 48 | var capturedError: NSError? 49 | sut.rx.error 50 | .subscribe(onNext: { capturedError = $0 }) 51 | .dispose() 52 | 53 | XCTAssertNil(capturedError) 54 | } 55 | 56 | func testPlayerItem_ShouldUpdateRxError() { 57 | class MockItem: AVPlayerItem { 58 | fileprivate override var error: Error? { 59 | return NSError.test 60 | } 61 | } 62 | 63 | let sut = MockItem(asset: asset) 64 | var capturedError: NSError? 65 | sut.rx.error 66 | .subscribe(onNext: { capturedError = $0 }) 67 | .dispose() 68 | 69 | XCTAssertEqual(capturedError, NSError.test) 70 | } 71 | 72 | //MARK: Duration 73 | 74 | func testPlayerItem_ShouldAllowObservationOfDuration() { 75 | let sut = AVPlayerItem(asset: asset) 76 | var capturedDuration: CMTime? 77 | sut.rx.duration 78 | .subscribe(onNext: { capturedDuration = $0 }) 79 | .dispose() 80 | 81 | XCTAssertEqual(capturedDuration?.value, 0) 82 | } 83 | 84 | func testPlayerItem_ShouldUpdateRxDuration() { 85 | class MockItem: AVPlayerItem { 86 | // Using flag to test because player item does something odd and doesn't 87 | // return our mocked 5 second duration even though it's accessed 88 | var durationChecked = false 89 | fileprivate override var duration: CMTime { 90 | durationChecked = true 91 | return CMTime(seconds: 5, preferredTimescale: 1) 92 | } 93 | } 94 | 95 | let sut = MockItem(asset: asset) 96 | sut.rx.duration 97 | .subscribe(onNext: { duration in }) 98 | .dispose() 99 | 100 | XCTAssertTrue(sut.durationChecked) 101 | } 102 | 103 | // MARK: PlaybackLikelyToKeepUp 104 | 105 | func testPlayerItem_ShouldAllowObservationOfPlaybackLikelyToKeepUp() { 106 | let sut = AVPlayerItem(asset: asset) 107 | var capturedFlag: Bool! 108 | sut.rx.playbackLikelyToKeepUp 109 | .subscribe(onNext: { capturedFlag = $0 }) 110 | .dispose() 111 | 112 | XCTAssertFalse(capturedFlag) 113 | } 114 | 115 | func testPlayerItem_ShouldUpdateRxPlaybackLikelyToKeepUp() { 116 | class MockItem: AVPlayerItem { 117 | fileprivate override var isPlaybackLikelyToKeepUp: Bool { 118 | return true 119 | } 120 | } 121 | 122 | let sut = MockItem(asset: asset) 123 | var capturedFlag: Bool! 124 | sut.rx.playbackLikelyToKeepUp 125 | .subscribe(onNext: { capturedFlag = $0 }) 126 | .dispose() 127 | 128 | XCTAssertTrue(capturedFlag) 129 | } 130 | 131 | // MARK: PlaybackBufferFull 132 | 133 | func testPlayerItem_ShouldAllowObservationOfPlaybackBufferFull() { 134 | let sut = AVPlayerItem(asset: asset) 135 | var capturedFlag: Bool! 136 | sut.rx.playbackBufferFull 137 | .subscribe(onNext: { capturedFlag = $0 }) 138 | .dispose() 139 | 140 | XCTAssertFalse(capturedFlag) 141 | } 142 | 143 | func testPlayerItem_ShouldUpdateRxPlaybackBufferFull() { 144 | class MockItem: AVPlayerItem { 145 | fileprivate override var isPlaybackBufferFull: Bool { 146 | return true 147 | } 148 | } 149 | 150 | let sut = MockItem(asset: asset) 151 | var capturedFlag: Bool! 152 | sut.rx.playbackBufferFull 153 | .subscribe(onNext: { capturedFlag = $0 }) 154 | .dispose() 155 | 156 | XCTAssertTrue(capturedFlag) 157 | } 158 | 159 | // MARK: PlaybackBufferEmpty 160 | 161 | func testPlayerItem_ShouldAllowObservationOfPlaybackBufferEmpty() { 162 | let sut = AVPlayerItem(asset: asset) 163 | var capturedFlag: Bool! 164 | sut.rx.playbackBufferEmpty 165 | .subscribe(onNext: { capturedFlag = $0 }) 166 | .dispose() 167 | 168 | XCTAssertTrue(capturedFlag) 169 | } 170 | 171 | func testPlayerItem_ShouldUpdateRxPlaybackBufferEmpty() { 172 | class MockItem: AVPlayerItem { 173 | fileprivate override var isPlaybackBufferEmpty: Bool { 174 | return true 175 | } 176 | } 177 | 178 | let sut = MockItem(asset: asset) 179 | var capturedFlag: Bool! 180 | sut.rx.playbackBufferEmpty 181 | .subscribe(onNext: { capturedFlag = $0 }) 182 | .dispose() 183 | 184 | XCTAssertTrue(capturedFlag) 185 | } 186 | 187 | // MARK: DidReachEnd 188 | 189 | func testPlayerItem_ShouldAllowObservationOfDidReachEnd() { 190 | let sut = AVPlayerItem(asset: asset) 191 | var called: Bool = false 192 | sut.rx.didPlayToEnd 193 | .subscribe(onNext: { note in called = true }) 194 | .dispose() 195 | 196 | XCTAssertFalse(called) 197 | } 198 | 199 | func testPlayerItem_ShouldUpdateRxDidPlayToEnd() { 200 | let sut = AVPlayerItem(asset: asset) 201 | var called: Bool = false 202 | let observer = sut.rx.didPlayToEnd 203 | .subscribe(onNext: { note in called = true }) 204 | 205 | let ns = NotificationCenter.default 206 | ns.post(name: .AVPlayerItemDidPlayToEndTime, object: sut) 207 | 208 | observer.dispose() 209 | 210 | XCTAssertTrue(called) 211 | } 212 | 213 | // MARK: LoadedTimeRanges 214 | 215 | func testPlayerItem_ShouldAllowObservationOfLoadedTimeRanges() { 216 | let sut = AVPlayerItem(asset: asset) 217 | var capturedRanges: [CMTimeRange]? 218 | sut.rx.loadedTimeRanges 219 | .subscribe(onNext: { capturedRanges = $0 }) 220 | .dispose() 221 | 222 | XCTAssertEqual(capturedRanges!, []) 223 | } 224 | 225 | func testPlayerItem_WhenLoadedTimeRangesHasATimeRange_ShouldProduceThatRange() { 226 | class MockItem: AVPlayerItem { 227 | let range = CMTimeRange(start: CMTime.zero, duration: CMTime(seconds: 5, preferredTimescale: 1)) 228 | fileprivate override var loadedTimeRanges: [NSValue] { 229 | return [NSValue(timeRange: range)] 230 | } 231 | } 232 | 233 | let sut = MockItem(asset: asset) 234 | var capturedRanges: [CMTimeRange]? 235 | sut.rx.loadedTimeRanges 236 | .subscribe(onNext: { capturedRanges = $0 }) 237 | .dispose() 238 | 239 | XCTAssertEqual(capturedRanges!, [sut.range]) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Tests/RxAVFoundationTests/RxAVPlayerLayerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxAVPlayerLayerTests.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/4/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import RxAVFoundation 11 | import AVFoundation 12 | 13 | class RxAVPlayerLayerTests: XCTestCase { 14 | func testPlayerLayer_ShouldAllowObservationOfReadyForDisplay() { 15 | let sut = AVPlayerLayer() 16 | var capturedFlag: Bool! 17 | sut.rx.isReadyForDisplay 18 | .subscribe(onNext: { capturedFlag = $0 }) 19 | .dispose() 20 | 21 | XCTAssertFalse(capturedFlag) 22 | } 23 | 24 | // TODO: fix this test. if observation is set on isReadyToDisplay rather than 25 | // 'readyToDisplay' this will work correctly, but that's not the key path 26 | // we want to observe. This is failing because of that custom getter name 27 | // We could override observeValueForKey to capture arguments but that's 28 | // called a lot internally and fails. 29 | // func testPlayerLayer_ShouldUpdateRxReadyForDisplay() { 30 | // class MockLayer: AVPlayerLayer { 31 | // private override var readyForDisplay: Bool { 32 | // return true 33 | // } 34 | // } 35 | // 36 | // let sut = MockLayer() 37 | // var capturedFlag = false 38 | // sut.rx.readyForDisplay 39 | // .subscribeNext { capturedFlag = $0 } 40 | // .dispose() 41 | // 42 | // XCTAssertTrue(capturedFlag) 43 | // } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/RxAVFoundationTests/RxAVPlayerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxAVPlayerTests.swift 3 | // RxAVPlayerTests 4 | // 5 | // Created by Patrick Mick on 3/30/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import RxAVFoundation 11 | import AVFoundation 12 | import RxSwift 13 | import RxCocoa 14 | 15 | class RxAVPlayerRateTests: XCTestCase { 16 | let player = AVPlayer() 17 | var capturedRate: Float? 18 | 19 | func testObserving_ShouldReturnTheDefaultRateOfZero() { 20 | player.rx.rate 21 | .subscribe(onNext: { self.capturedRate = $0 }) 22 | .dispose() 23 | 24 | XCTAssertEqual(capturedRate, 0) 25 | } 26 | 27 | func testObservingAPlayerWithARateOfOne_ShouldReturnOne() { 28 | let sut = player.rx.rate.subscribe(onNext: { self.capturedRate = $0 }) 29 | player.rate = 1 30 | sut.dispose() 31 | 32 | XCTAssertEqual(capturedRate, 1) 33 | } 34 | } 35 | 36 | class RxAVPlayerCurrentItemTests: XCTestCase { 37 | let player = AVPlayer() 38 | var capturedCurrentItem: AVPlayerItem? 39 | 40 | func testObservingCurrentItem_ShouldReturnTheDefaultNilItem() { 41 | player.rx.currentItem 42 | .subscribe(onNext: { self.capturedCurrentItem = $0 }) 43 | .dispose() 44 | 45 | XCTAssertNil(capturedCurrentItem) 46 | } 47 | 48 | func testObservingCurrentItem_ShouldReturnTheItemWhenSet() { 49 | let url = URL(string: "https://example.com") 50 | let item = AVPlayerItem(url: url!) 51 | player.replaceCurrentItem(with: item) 52 | 53 | player.rx.currentItem 54 | .subscribe(onNext: { self.capturedCurrentItem = $0 }) 55 | .dispose() 56 | 57 | XCTAssertEqual(capturedCurrentItem, item) 58 | } 59 | } 60 | 61 | class RxAVPlayerStatusTests: XCTestCase { 62 | func testObservingStatus_ShouldReturnTheDefaultUnknown() { 63 | let player = AVPlayer() 64 | var capturedStatus: AVPlayer.Status? 65 | player.rx.status 66 | .subscribe(onNext: { capturedStatus = $0 }) 67 | .dispose() 68 | 69 | XCTAssertEqual(capturedStatus, .unknown) 70 | } 71 | 72 | func testObservingStatus_WhenItChangesToReadyToPlay_ShouldUpdateTheObserver() { 73 | // Makes it so that we can update the readonly property 74 | class MockPlayer: AVPlayer { 75 | var changeableStatus: AVPlayer.Status = .unknown { 76 | willSet { self.willChangeValue(forKey: "status") } 77 | didSet { self.didChangeValue(forKey: "status") } 78 | } 79 | fileprivate override var status: AVPlayer.Status { return changeableStatus } 80 | } 81 | 82 | let player = MockPlayer() 83 | var capturedStatus: AVPlayer.Status? 84 | let sut = player.rx.status.subscribe(onNext: { capturedStatus = $0 }) 85 | player.changeableStatus = .readyToPlay 86 | sut.dispose() 87 | 88 | XCTAssertEqual(capturedStatus, .readyToPlay) 89 | } 90 | } 91 | 92 | class RxAVPlayerErrorTests: XCTestCase { 93 | func testObservingStatus_ShouldReturnTheDefaultUnknown() { 94 | let player = AVPlayer() 95 | var capturedError: NSError? 96 | player.rx.error 97 | .subscribe(onNext: { capturedError = $0 }) 98 | .dispose() 99 | 100 | XCTAssertNil(capturedError) 101 | } 102 | 103 | func testObservingStatus_WhenItChangesToReadyToPlay_ShouldUpdateTheObserver() { 104 | // Makes it so that we can update the readonly property 105 | class MockPlayer: AVPlayer { 106 | var changeableError: NSError? = nil { 107 | willSet { self.willChangeValue(forKey: "error") } 108 | didSet { self.didChangeValue(forKey: "error") } 109 | } 110 | fileprivate override var error: Error? { return changeableError } 111 | } 112 | 113 | let player = MockPlayer() 114 | var capturedError: NSError? 115 | let sut = player.rx.error.subscribe(onNext: { capturedError = $0 }) 116 | player.changeableError = NSError.test 117 | sut.dispose() 118 | 119 | XCTAssertEqual(capturedError, NSError.test) 120 | } 121 | } 122 | 123 | @available(iOS 10.0, tvOS 10.0, OSX 10.12, *) 124 | class RxAVPlayerReasonForWaitingToPlayTests: XCTestCase { 125 | func testObservingWaitingReason_ShouldReturnNilByDefault() { 126 | let player = AVPlayer() 127 | var capturedReason: AVPlayer.WaitingReason? 128 | player.rx.reasonForWaitingToPlay 129 | .subscribe(onNext: { capturedReason = $0 }) 130 | .dispose() 131 | 132 | XCTAssertNil(capturedReason) 133 | } 134 | 135 | func testObservingWaitingReason_WhenItChangesToMinimizeStalls_ShouldUpdateTheObserver() { 136 | // Makes it so that we can update the readonly property 137 | class MockPlayer: AVPlayer { 138 | var changeableReasonForWaitingToPlay: AVPlayer.WaitingReason? = nil { 139 | willSet { self.willChangeValue(forKey: "reasonForWaitingToPlay") } 140 | didSet { self.didChangeValue(forKey: "reasonForWaitingToPlay") } 141 | } 142 | fileprivate override var reasonForWaitingToPlay: AVPlayer.WaitingReason? { return changeableReasonForWaitingToPlay } 143 | } 144 | 145 | let player = MockPlayer() 146 | var capturedReason: AVPlayer.WaitingReason? 147 | let sut = player.rx.reasonForWaitingToPlay.subscribe(onNext: { capturedReason = $0 }) 148 | player.changeableReasonForWaitingToPlay = .toMinimizeStalls 149 | sut.dispose() 150 | 151 | XCTAssertEqual(capturedReason, .toMinimizeStalls) 152 | } 153 | } 154 | 155 | @available(iOS 10.0, tvOS 10.0, OSX 10.12, *) 156 | class RxAVPlayerTimeControlStatusTests: XCTestCase { 157 | func testObservingTimeControlStatus_ShouldReturnPausedByDefault() { 158 | let player = AVPlayer() 159 | var capturedTimeControlStatus: AVPlayer.TimeControlStatus? 160 | player.rx.timeControlStatus 161 | .subscribe(onNext: { capturedTimeControlStatus = $0 }) 162 | .dispose() 163 | 164 | // by default you will get paused for a player 165 | XCTAssertEqual(capturedTimeControlStatus, .paused) 166 | } 167 | 168 | func testObservingTimeControlStatus_WhenItChangesToPlaying_ShouldUpdateTheObserver() { 169 | // Makes it so that we can update the readonly property 170 | class MockPlayer: AVPlayer { 171 | var changeableTimeControlStats: AVPlayer.TimeControlStatus = .waitingToPlayAtSpecifiedRate { 172 | willSet { self.willChangeValue(forKey: "timeControlStatus") } 173 | didSet { self.didChangeValue(forKey: "timeControlStatus") } 174 | } 175 | fileprivate override var timeControlStatus: AVPlayer.TimeControlStatus { return changeableTimeControlStats } 176 | } 177 | 178 | let player = MockPlayer() 179 | var capturedTimeControlStatus: AVPlayer.TimeControlStatus? 180 | let sut = player.rx.timeControlStatus.subscribe(onNext: { capturedTimeControlStatus = $0 }) 181 | player.changeableTimeControlStats = .playing 182 | sut.dispose() 183 | 184 | XCTAssertEqual(capturedTimeControlStatus, .playing) 185 | } 186 | } 187 | 188 | 189 | class RxAVPlayerPeriodicTimeObserverTests: XCTestCase { 190 | func testPediodicTimeObserver_AddsAnObserverToTheAVPlayer() { 191 | class MockPlayer: AVPlayer { 192 | var interval: CMTime! 193 | 194 | fileprivate override func addPeriodicTimeObserver(forInterval interval: CMTime, queue: DispatchQueue?, using block: @escaping (CMTime) -> Void) -> Any { 195 | self.interval = interval 196 | return "" 197 | } 198 | 199 | fileprivate override func removeTimeObserver(_ observer: Any) { } 200 | } 201 | 202 | let player = MockPlayer() 203 | let interval = CMTime(seconds: 1, preferredTimescale: CMTimeScale(1)) 204 | player.rx.periodicTimeObserver(interval: interval) 205 | .subscribe(onNext: { time in }) 206 | .dispose() 207 | 208 | XCTAssertEqual(player.interval, interval) 209 | } 210 | 211 | func testPeriodicTimeObserver_NotifiesObserversWhenItsBlockIsCalled() { 212 | class MockPlayer: AVPlayer { 213 | fileprivate override func addPeriodicTimeObserver(forInterval interval: CMTime, queue: DispatchQueue?, using block: @escaping (CMTime) -> Void) -> Any { 214 | block(CMTime(seconds: 2, preferredTimescale: CMTimeScale(1))) 215 | return "" 216 | } 217 | 218 | fileprivate override func removeTimeObserver(_ observer: Any) { } 219 | } 220 | 221 | let player = MockPlayer() 222 | var capturedTime: CMTime! 223 | let interval = CMTime(seconds: 1, preferredTimescale: CMTimeScale(1)) 224 | player.rx.periodicTimeObserver(interval: interval) 225 | .subscribe(onNext: { time in capturedTime = time }) 226 | .dispose() 227 | 228 | let time = CMTime(seconds: 2, preferredTimescale: CMTimeScale(1)) 229 | 230 | XCTAssertEqual(capturedTime, time) 231 | } 232 | 233 | func testPeriodicTimeObserver_ShouldRemoveTheTimeObserverWhenDisposed() { 234 | class MockPlayer: AVPlayer { 235 | var capturedRemove: String! 236 | 237 | fileprivate override func addPeriodicTimeObserver(forInterval interval: CMTime, queue: DispatchQueue?, using block: @escaping (CMTime) -> Void) -> Any { 238 | return "test" 239 | } 240 | 241 | fileprivate override func removeTimeObserver(_ observer: Any) { 242 | capturedRemove = observer as? String 243 | } 244 | } 245 | 246 | let player = MockPlayer() 247 | let interval = CMTime(seconds: 1, preferredTimescale: CMTimeScale(1)) 248 | player.rx.periodicTimeObserver(interval: interval) 249 | .subscribe(onNext: { time in }) 250 | .dispose() 251 | 252 | XCTAssertEqual("test", player.capturedRemove) 253 | } 254 | } 255 | 256 | class RxAVPlayerPeriodicBoundaryObserverTests: XCTestCase { 257 | func testBoundaryTimeObserver_AddsAnObserverToTheAVPlayer() { 258 | class MockPlayer: AVPlayer { 259 | var times: [CMTime]! 260 | 261 | fileprivate override func addBoundaryTimeObserver(forTimes times: [NSValue], queue: DispatchQueue?, using block: @escaping () -> Void) -> Any { 262 | self.times = times.map { $0.timeValue } 263 | return "" 264 | } 265 | 266 | fileprivate override func removeTimeObserver(_ observer: Any) { } 267 | } 268 | 269 | let player = MockPlayer() 270 | let time = CMTime(seconds: 1, preferredTimescale: CMTimeScale(1)) 271 | player.rx.boundaryTimeObserver(times: [time]) 272 | .subscribe(onNext: { }) 273 | .dispose() 274 | 275 | XCTAssertEqual(player.times.first, time) 276 | } 277 | 278 | func testBoundaryTimeObserver_NotifiesObserversWhenItsBlockIsCalled() { 279 | class MockPlayer: AVPlayer { 280 | fileprivate override func addBoundaryTimeObserver(forTimes times: [NSValue], queue: DispatchQueue?, using block: @escaping () -> Void) -> Any { 281 | block() 282 | return "" 283 | } 284 | 285 | fileprivate override func removeTimeObserver(_ observer: Any) { } 286 | } 287 | 288 | let player = MockPlayer() 289 | let time = CMTime(seconds: 1, preferredTimescale: CMTimeScale(1)) 290 | var closureCalled = false 291 | player.rx.boundaryTimeObserver(times: [time]) 292 | .subscribe(onNext: { closureCalled = true }) 293 | .dispose() 294 | 295 | XCTAssertTrue(closureCalled) 296 | } 297 | 298 | func testBoundaryTimeObserver_ShouldRemoveTheTimeObserverWhenDisposed() { 299 | class MockPlayer: AVPlayer { 300 | var capturedRemove: String! 301 | 302 | fileprivate override func addBoundaryTimeObserver(forTimes times: [NSValue], queue: DispatchQueue?, using block: @escaping () -> Void) -> Any { 303 | return "test" 304 | } 305 | 306 | fileprivate override func removeTimeObserver(_ observer: Any) { 307 | capturedRemove = observer as? String 308 | } 309 | } 310 | 311 | let player = MockPlayer() 312 | let time = CMTime(seconds: 1, preferredTimescale: CMTimeScale(1)) 313 | player.rx.boundaryTimeObserver(times: [time]) 314 | .subscribe(onNext: { }) 315 | .dispose() 316 | 317 | XCTAssertEqual("test", player.capturedRemove) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Tests/RxAVFoundationTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // RxAVPlayer 4 | // 5 | // Created by Patrick Mick on 4/1/16. 6 | // Copyright © 2016 YayNext. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSError { 12 | class var test: NSError { 13 | return NSError(domain: "test", code: 0, userInfo: nil) 14 | } 15 | } 16 | 17 | extension URL { 18 | static var test: URL { 19 | return URL(string: "www.google.com")! 20 | } 21 | } 22 | 23 | struct GenericTestingError: Error, Equatable { 24 | public static func ==(lhs: GenericTestingError, rhs: GenericTestingError) -> Bool { 25 | return true 26 | } 27 | } 28 | --------------------------------------------------------------------------------