diff --git a/.gitignore b/.gitignore index d477ec05..2ca6ff0b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ project.xcworkspace .gradle local.properties *.hprof +.project +.settings +.classpath # node.js # diff --git a/examples/basic/yarn.lock b/examples/basic/yarn.lock index 50d8d52e..ec0a6edd 100644 --- a/examples/basic/yarn.lock +++ b/examples/basic/yarn.lock @@ -2093,6 +2093,11 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +eme-encryption-scheme-polyfill@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.3.tgz#2ca6e06480e06cceb5e50efd27943ac46c959878" + integrity sha512-44CNFMsqzHdKHrzWxlS7xZ8KUHn5XutBqpmCuWzNIynmAyFInHrrD3ozv/RvK9ZhgV6QY6Easx8EWAmxteNodg== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -5160,7 +5165,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -5238,11 +5243,11 @@ react-is@^16.8.4, react-is@^16.8.6: integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== "react-native-video@file:../..": - version "5.0.1" + version "5.1.1" dependencies: keymirror "^0.1.1" - prop-types "^15.5.10" - shaka-player "^2.4.4" + prop-types "^15.7.2" + shaka-player "^2.5.9" react-native@0.60.5: version "0.60.5" @@ -5774,10 +5779,12 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -shaka-player@^2.4.4: - version "2.5.2" - resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-2.5.2.tgz#3e639f8f5dfdb1a0afff2ff1f87cddf481f93fe4" - integrity sha512-gpRfXVLAZi33kw9Egop18MkZ/EVRS0soeN6ocR+Btq/J5IoCC56MxwwHzAGna+PgBW9Ox458HRefJ6HB0BoADA== +shaka-player@^2.5.9: + version "2.5.23" + resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-2.5.23.tgz#db92d1c6cf2314f0180a2cec11b0e2f2560336f5" + integrity sha512-3MC9k0OXJGw8AZ4n/ZNCZS2yDxx+3as5KgH6Tx4Q5TRboTBBCu6dYPI5vp1DxKeyU12MBN1Zcbs7AKzXv2EnCg== + dependencies: + eme-encryption-scheme-polyfill "^2.0.1" shebang-command@^1.2.0: version "1.2.0" diff --git a/ios/RCTVideo.xcodeproj/project.pbxproj b/ios/RCTVideo.xcodeproj/project.pbxproj index 4e675ac7..e601ea80 100644 --- a/ios/RCTVideo.xcodeproj/project.pbxproj +++ b/ios/RCTVideo.xcodeproj/project.pbxproj @@ -7,14 +7,12 @@ objects = { /* Begin PBXBuildFile section */ - D1107C0A2110259000073188 /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C032110259000073188 /* UIView+FindUIViewController.m */; }; - D1107C0B2110259000073188 /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C032110259000073188 /* UIView+FindUIViewController.m */; }; - D1107C0C2110259000073188 /* RCTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C052110259000073188 /* RCTVideo.m */; }; - D1107C0D2110259000073188 /* RCTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C052110259000073188 /* RCTVideo.m */; }; - D1107C0E2110259000073188 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C062110259000073188 /* RCTVideoManager.m */; }; - D1107C0F2110259000073188 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C062110259000073188 /* RCTVideoManager.m */; }; - D1107C102110259000073188 /* RCTVideoPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C082110259000073188 /* RCTVideoPlayerViewController.m */; }; - D1107C112110259000073188 /* RCTVideoPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C082110259000073188 /* RCTVideoPlayerViewController.m */; }; + 0177D39A27170A7A00F5BE18 /* RCTVideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */; }; + 0177D39B27170A7A00F5BE18 /* UIView+FindUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39327170A7A00F5BE18 /* UIView+FindUIViewController.swift */; }; + 0177D39C27170A7A00F5BE18 /* RCTVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */; }; + 0177D39D27170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */; }; + 0177D39E27170A7A00F5BE18 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */; }; + 0177D39F27170A7A00F5BE18 /* RCTVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39927170A7A00F5BE18 /* RCTVideo.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -39,17 +37,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01450CB5271D5738005D8F6B /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 01489050272001A100E69940 /* DataStructures */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DataStructures; path = Video/DataStructures; sourceTree = ""; }; + 01489051272001A100E69940 /* Features */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Features; path = Video/Features; sourceTree = ""; }; + 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideoManager.swift; path = Video/RCTVideoManager.swift; sourceTree = ""; }; + 0177D39327170A7A00F5BE18 /* UIView+FindUIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+FindUIViewController.swift"; path = "Video/UIView+FindUIViewController.swift"; sourceTree = ""; }; + 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideoPlayerViewController.swift; path = Video/RCTVideoPlayerViewController.swift; sourceTree = ""; }; + 0177D39527170A7A00F5BE18 /* RCTSwiftLog */ = {isa = PBXFileReference; lastKnownFileType = folder; name = RCTSwiftLog; path = Video/RCTSwiftLog; sourceTree = ""; }; + 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideoPlayerViewControllerDelegate.swift; path = Video/RCTVideoPlayerViewControllerDelegate.swift; sourceTree = ""; }; + 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideoManager.m; path = Video/RCTVideoManager.m; sourceTree = ""; }; + 0177D39827170A7A00F5BE18 /* RCTVideo-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RCTVideo-Bridging-Header.h"; path = "Video/RCTVideo-Bridging-Header.h"; sourceTree = ""; }; + 0177D39927170A7A00F5BE18 /* RCTVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideo.swift; path = Video/RCTVideo.swift; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 641E28441F0EEC8500443AF6 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; - D1107C012110259000073188 /* RCTVideoPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoPlayerViewController.h; path = Video/RCTVideoPlayerViewController.h; sourceTree = ""; }; - D1107C022110259000073188 /* RCTVideoPlayerViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoPlayerViewControllerDelegate.h; path = Video/RCTVideoPlayerViewControllerDelegate.h; sourceTree = ""; }; - D1107C032110259000073188 /* UIView+FindUIViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIView+FindUIViewController.m"; path = "Video/UIView+FindUIViewController.m"; sourceTree = ""; }; - D1107C042110259000073188 /* UIView+FindUIViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIView+FindUIViewController.h"; path = "Video/UIView+FindUIViewController.h"; sourceTree = ""; }; - D1107C052110259000073188 /* RCTVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideo.m; path = Video/RCTVideo.m; sourceTree = ""; }; - D1107C062110259000073188 /* RCTVideoManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideoManager.m; path = Video/RCTVideoManager.m; sourceTree = ""; }; - D1107C072110259000073188 /* RCTVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideo.h; path = Video/RCTVideo.h; sourceTree = ""; }; - D1107C082110259000073188 /* RCTVideoPlayerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideoPlayerViewController.m; path = Video/RCTVideoPlayerViewController.m; sourceTree = ""; }; - D1107C092110259000073188 /* RCTVideoManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoManager.h; path = Video/RCTVideoManager.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,18 +87,19 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( - D1107C072110259000073188 /* RCTVideo.h */, - D1107C052110259000073188 /* RCTVideo.m */, - D1107C092110259000073188 /* RCTVideoManager.h */, - D1107C062110259000073188 /* RCTVideoManager.m */, - D1107C012110259000073188 /* RCTVideoPlayerViewController.h */, - D1107C082110259000073188 /* RCTVideoPlayerViewController.m */, - D1107C022110259000073188 /* RCTVideoPlayerViewControllerDelegate.h */, - D1107C042110259000073188 /* UIView+FindUIViewController.h */, - D1107C032110259000073188 /* UIView+FindUIViewController.m */, + 01489050272001A100E69940 /* DataStructures */, + 01489051272001A100E69940 /* Features */, + 0177D39527170A7A00F5BE18 /* RCTSwiftLog */, + 0177D39927170A7A00F5BE18 /* RCTVideo.swift */, + 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */, + 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */, + 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */, + 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */, + 0177D39327170A7A00F5BE18 /* UIView+FindUIViewController.swift */, + 0177D39827170A7A00F5BE18 /* RCTVideo-Bridging-Header.h */, 134814211AA4EA7D00B7C361 /* Products */, - 641E28441F0EEC8500443AF6 /* libRCTVideo.a */, 49E995712048B4CE00EA7890 /* Frameworks */, + 01450CB5271D5738005D8F6B /* libRCTVideo.a */, ); sourceTree = ""; }; @@ -137,7 +137,7 @@ ); name = "RCTVideo-tvOS"; productName = "RCTVideo-tvOS"; - productReference = 641E28441F0EEC8500443AF6 /* libRCTVideo.a */; + productReference = 01450CB5271D5738005D8F6B /* libRCTVideo.a */; productType = "com.apple.product-type.library.static"; }; /* End PBXNativeTarget section */ @@ -151,9 +151,11 @@ TargetAttributes = { 58B511DA1A9E6C8500147676 = { CreatedOnToolsVersion = 6.1.1; + LastSwiftMigration = 1300; }; 641E28431F0EEC8500443AF6 = { CreatedOnToolsVersion = 8.3.3; + LastSwiftMigration = 1300; ProvisioningStyle = Automatic; }; }; @@ -163,6 +165,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 58B511D21A9E6C8500147676; @@ -181,10 +184,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D1107C0A2110259000073188 /* UIView+FindUIViewController.m in Sources */, - D1107C102110259000073188 /* RCTVideoPlayerViewController.m in Sources */, - D1107C0E2110259000073188 /* RCTVideoManager.m in Sources */, - D1107C0C2110259000073188 /* RCTVideo.m in Sources */, + 0177D39D27170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift in Sources */, + 0177D39C27170A7A00F5BE18 /* RCTVideoPlayerViewController.swift in Sources */, + 0177D39B27170A7A00F5BE18 /* UIView+FindUIViewController.swift in Sources */, + 0177D39F27170A7A00F5BE18 /* RCTVideo.swift in Sources */, + 0177D39E27170A7A00F5BE18 /* RCTVideoManager.m in Sources */, + 0177D39A27170A7A00F5BE18 /* RCTVideoManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -192,10 +197,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D1107C0B2110259000073188 /* UIView+FindUIViewController.m in Sources */, - D1107C112110259000073188 /* RCTVideoPlayerViewController.m in Sources */, - D1107C0F2110259000073188 /* RCTVideoManager.m in Sources */, - D1107C0D2110259000073188 /* RCTVideo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -280,30 +281,39 @@ 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/Vendor/SPTPersistentCache/include/**", ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Video/RCTVideo-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/Vendor/SPTPersistentCache/include/**", ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Video/RCTVideo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -312,15 +322,19 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; DEBUG_INFORMATION_FORMAT = dwarf; GCC_NO_COMMON_BLOCKS = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = Debug; @@ -330,16 +344,19 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_NO_COMMON_BLOCKS = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = Release; diff --git a/ios/Video/DataStructures/DRMParams.swift b/ios/Video/DataStructures/DRMParams.swift new file mode 100644 index 00000000..64add7e0 --- /dev/null +++ b/ios/Video/DataStructures/DRMParams.swift @@ -0,0 +1,30 @@ +struct DRMParams { + let type: String? + let licenseServer: String? + let headers: Dictionary? + let contentId: String? + let certificateUrl: String? + let base64Certificate: Bool? + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = nil + self.licenseServer = nil + self.contentId = nil + self.certificateUrl = nil + self.base64Certificate = nil + self.headers = nil + return + } + self.json = json + self.type = json["type"] as? String + self.licenseServer = json["licenseServer"] as? String + self.contentId = json["contentId"] as? String + self.certificateUrl = json["certificateUrl"] as? String + self.base64Certificate = json["base64Certificate"] as? Bool + self.headers = json["headers"] as? Dictionary + } +} diff --git a/ios/Video/DataStructures/SelectedTrackCriteria.swift b/ios/Video/DataStructures/SelectedTrackCriteria.swift new file mode 100644 index 00000000..7d97b8f2 --- /dev/null +++ b/ios/Video/DataStructures/SelectedTrackCriteria.swift @@ -0,0 +1,18 @@ +struct SelectedTrackCriteria { + let type: String + let value: Any? + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = "" + self.value = nil + return + } + self.json = json + self.type = json["type"] as? String ?? "" + self.value = json["value"] + } +} diff --git a/ios/Video/DataStructures/TextTrack.swift b/ios/Video/DataStructures/TextTrack.swift new file mode 100644 index 00000000..b0bdad80 --- /dev/null +++ b/ios/Video/DataStructures/TextTrack.swift @@ -0,0 +1,25 @@ + +struct TextTrack { + let type: String + let language: String + let title: String + let uri: String + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = "" + self.language = "" + self.title = "" + self.uri = "" + return + } + self.json = json + self.type = json["type"] as? String ?? "" + self.language = json["language"] as? String ?? "" + self.title = json["title"] as? String ?? "" + self.uri = json["uri"] as? String ?? "" + } +} diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift new file mode 100644 index 00000000..20ab7158 --- /dev/null +++ b/ios/Video/DataStructures/VideoSource.swift @@ -0,0 +1,31 @@ + +struct VideoSource { + let type: String? + let uri: String? + let isNetwork: Bool + let isAsset: Bool + let shouldCache: Bool + let requestHeaders: Dictionary? + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = nil + self.uri = nil + self.isNetwork = false + self.isAsset = false + self.shouldCache = false + self.requestHeaders = nil + return + } + self.json = json + self.type = json["type"] as? String + self.uri = json["uri"] as? String + self.isNetwork = json["isNetwork"] as? Bool ?? false + self.isAsset = json["isAsset"] as? Bool ?? false + self.shouldCache = json["shouldCache"] as? Bool ?? false + self.requestHeaders = json["requestHeaders"] as? Dictionary + } +} diff --git a/ios/Video/Features/RCTPictureInPicture.swift b/ios/Video/Features/RCTPictureInPicture.swift new file mode 100644 index 00000000..be9812c4 --- /dev/null +++ b/ios/Video/Features/RCTPictureInPicture.swift @@ -0,0 +1,75 @@ +import AVFoundation +import AVKit +import MediaAccessibility +import React +import Foundation + +#if TARGET_OS_IOS +class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate { + private var _onPictureInPictureStatusChanged: RCTDirectEventBlock? + private var _onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock? + private var _restoreUserInterfaceForPIPStopCompletionHandler:((Bool) -> Void)? = nil + private var _pipController:AVPictureInPictureController? + private var _isActive:Bool = false + + init(_ onPictureInPictureStatusChanged: @escaping RCTDirectEventBlock, _ onRestoreUserInterfaceForPictureInPictureStop: @escaping RCTDirectEventBlock) { + _onPictureInPictureStatusChanged = onPictureInPictureStatusChanged + _onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop + } + + func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } + + _onPictureInPictureStatusChanged([ "isActive": NSNumber(value: true)]) + } + + func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } + + _onPictureInPictureStatusChanged([ "isActive": NSNumber(value: false)]) + } + + func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + + assert(_restoreUserInterfaceForPIPStopCompletionHandler == nil, "restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited.") + + guard let _onRestoreUserInterfaceForPictureInPictureStop = _onRestoreUserInterfaceForPictureInPictureStop else { return } + + _onRestoreUserInterfaceForPictureInPictureStop([:]) + + _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler + } + + func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore:Bool) { + guard let _restoreUserInterfaceForPIPStopCompletionHandler = _restoreUserInterfaceForPIPStopCompletionHandler else { return } + _restoreUserInterfaceForPIPStopCompletionHandler(restore) + self._restoreUserInterfaceForPIPStopCompletionHandler = nil + } + + func setupPipController(_ playerLayer: AVPlayerLayer?) { + guard playerLayer != nil && AVPictureInPictureController.isPictureInPictureSupported() else { return } + // Create new controller passing reference to the AVPlayerLayer + _pipController = AVPictureInPictureController(playerLayer:playerLayer!) + _pipController?.delegate = self + } + + func setPictureInPicture(_ isActive:Bool) { + if _isActive == isActive { + return + } + _isActive = isActive + + guard let _pipController = _pipController else { return } + + if _isActive && !_pipController.isPictureInPictureActive { + DispatchQueue.main.async(execute: { + _pipController.startPictureInPicture() + }) + } else if !_isActive && _pipController.isPictureInPictureActive { + DispatchQueue.main.async(execute: { + _pipController.stopPictureInPicture() + }) + } + } +} +#endif diff --git a/ios/Video/Features/RCTPlayerObserver.swift b/ios/Video/Features/RCTPlayerObserver.swift new file mode 100644 index 00000000..d17b27da --- /dev/null +++ b/ios/Video/Features/RCTPlayerObserver.swift @@ -0,0 +1,212 @@ +import AVFoundation +import AVKit +import Foundation + +@objc +protocol RCTPlayerObserverHandlerObjc { + func handleDidFailToFinishPlaying(notification:NSNotification!) + func handlePlaybackStalled(notification:NSNotification!) + func handlePlayerItemDidReachEnd(notification:NSNotification!) + // unused +// func handleAVPlayerAccess(notification:NSNotification!) +} + +protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc { + func handleTimeUpdate(time:CMTime) + func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) + func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) + func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange) + func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange) + func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange) +} + +class RCTPlayerObserver: NSObject { + + var _handlers: RCTPlayerObserverHandler! + + var player:AVPlayer? { + didSet { + if player == nil { + removePlayerObservers() + removePlayerTimeObserver() + } else { + addPlayerObservers() + addPlayerTimeObserver() + } + } + } + var playerItem:AVPlayerItem? { + didSet { + if playerItem == nil { + removePlayerItemObservers() + } else { + addPlayerItemObservers() + } + } + } + var playerViewController:AVPlayerViewController? { + didSet { + if playerViewController == nil { + removePlayerViewControllerObservers() + } else { + addPlayerViewControllerObservers() + } + } + } + var playerLayer:AVPlayerLayer? { + didSet { + if playerLayer == nil { + removePlayerLayerObserver() + } else { + addPlayerLayerObserver() + } + } + } + + private var _progressUpdateInterval:TimeInterval = 250 + private var _timeObserver:Any? + + private var _playerRateChangeObserver:NSKeyValueObservation? + private var _playerExpernalPlaybackActiveObserver:NSKeyValueObservation? + private var _playerItemStatusObserver:NSKeyValueObservation? + private var _playerPlaybackBufferEmptyObserver:NSKeyValueObservation? + private var _playerPlaybackLikelyToKeepUpObserver:NSKeyValueObservation? + private var _playerTimedMetadataObserver:NSKeyValueObservation? + private var _playerViewControllerReadyForDisplayObserver:NSKeyValueObservation? + private var _playerLayerReadyForDisplayObserver:NSKeyValueObservation? + private var _playerViewControllerOverlayFrameObserver:NSKeyValueObservation? + + deinit { + NotificationCenter.default.removeObserver(_handlers) + } + + func addPlayerObservers() { + guard let player = player else { + return + } + + _playerRateChangeObserver = player.observe(\.rate, changeHandler: _handlers.handlePlaybackRateChange) + _playerExpernalPlaybackActiveObserver = player.observe(\.isExternalPlaybackActive, changeHandler: _handlers.handleExternalPlaybackActiveChange) + } + + func removePlayerObservers() { + _playerRateChangeObserver?.invalidate() + _playerExpernalPlaybackActiveObserver?.invalidate() + } + + func addPlayerItemObservers() { + guard let playerItem = playerItem else { return } + + _playerItemStatusObserver = playerItem.observe(\.status, options: [.new, .old], changeHandler: _handlers.handlePlayerItemStatusChange) + _playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty) + _playerPlaybackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .old], changeHandler: _handlers.handlePlaybackLikelyToKeepUp) + _playerTimedMetadataObserver = playerItem.observe(\.timedMetadata, options: [.new], changeHandler: _handlers.handleTimeMetadataChange) + } + + func removePlayerItemObservers() { + _playerItemStatusObserver?.invalidate() + _playerPlaybackBufferEmptyObserver?.invalidate() + _playerPlaybackLikelyToKeepUpObserver?.invalidate() + _playerTimedMetadataObserver?.invalidate() + } + + func addPlayerViewControllerObservers() { + guard let playerViewController = playerViewController else { return } + + _playerViewControllerReadyForDisplayObserver = playerViewController.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) + + _playerViewControllerOverlayFrameObserver = playerViewController.contentOverlayView?.observe(\.frame, options: [.new, .old], changeHandler: _handlers.handleViewControllerOverlayViewFrameChange) + } + + func removePlayerViewControllerObservers() { + _playerViewControllerReadyForDisplayObserver?.invalidate() + _playerViewControllerOverlayFrameObserver?.invalidate() + } + + func addPlayerLayerObserver() { + _playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) + } + + func removePlayerLayerObserver() { + _playerLayerReadyForDisplayObserver?.invalidate() + } + + func addPlayerTimeObserver() { + removePlayerTimeObserver() + let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000 + // @see endScrubbing in AVPlayerDemoPlaybackViewController.m + // of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html + _timeObserver = player?.addPeriodicTimeObserver( + forInterval: CMTimeMakeWithSeconds(progressUpdateIntervalMS, preferredTimescale: Int32(NSEC_PER_SEC)), + queue:nil, + using:_handlers.handleTimeUpdate + ) + } + + /* Cancels the previously registered time observer. */ + func removePlayerTimeObserver() { + if let timeObserver = _timeObserver { + player?.removeTimeObserver(timeObserver) + _timeObserver = nil + } + } + + func addTimeObserverIfNotSet() { + if (_timeObserver == nil) { + addPlayerTimeObserver() + } + } + + func replaceTimeObserverIfSet(_ newUpdateInterval:Float64? = nil) { + if let newUpdateInterval = newUpdateInterval { + _progressUpdateInterval = newUpdateInterval + } + if (_timeObserver != nil) { + addPlayerTimeObserver() + } + } + + func attachPlayerEventListeners() { + + NotificationCenter.default.removeObserver(_handlers, + name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object:player?.currentItem) + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handlePlayerItemDidReachEnd(notification:)), + name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object:player?.currentItem) + + NotificationCenter.default.removeObserver(_handlers, + name:NSNotification.Name.AVPlayerItemPlaybackStalled, + object:nil) + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handlePlaybackStalled(notification:)), + name:NSNotification.Name.AVPlayerItemPlaybackStalled, + object:nil) + NotificationCenter.default.removeObserver(_handlers, + name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, + object:nil) + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handleDidFailToFinishPlaying(notification:)), + name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, + object:nil) + // Unused +// NotificationCenter.default.removeObserver(_handlers, +// name:NSNotification.Name.AVPlayerItemNewAccessLogEntry, +// object:nil) +// NotificationCenter.default.addObserver(_handlers, +// selector: #selector(RCTPlayerObserverHandler.handleAVPlayerAccess(notification:)), +// name:NSNotification.Name.AVPlayerItemNewAccessLogEntry, +// object:nil) + + } + + func clearPlayer() { + player = nil + playerItem = nil + NotificationCenter.default.removeObserver(_handlers) + } +} diff --git a/ios/Video/Features/RCTPlayerOperations.swift b/ios/Video/Features/RCTPlayerOperations.swift new file mode 100644 index 00000000..8d70f46d --- /dev/null +++ b/ios/Video/Features/RCTPlayerOperations.swift @@ -0,0 +1,160 @@ +import AVFoundation +import MediaAccessibility + +let RCTVideoUnset = -1 + +/*! + * Collection of mutating functions + */ +enum RCTPlayerOperations { + + static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) { + let type = criteria?.type + let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player) + + // The first few tracks will be audio & video track + let firstTextIndex:Int = 0 + for firstTextIndex in 0..<(player?.currentItem?.tracks.count ?? 0) { + if player?.currentItem?.tracks[firstTextIndex].assetTrack?.hasMediaCharacteristic(.legible) ?? false { + break + } + } + + var selectedTrackIndex:Int = RCTVideoUnset + + if (type == "disabled") { + // Do nothing. We want to ensure option is nil + } else if (type == "language") { + let selectedValue = criteria?.value as? String + for i in 0.. index { + selectedTrackIndex = index + } + } + } + + // in the situation that a selected text track is not available (eg. specifies a textTrack not available) + if (type != "disabled") && selectedTrackIndex == RCTVideoUnset { + let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) as! CFArray + let captionSettings = captioningMediaCharacteristics as? [AnyHashable] + if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) { + selectedTrackIndex = 0 // If we can't find a match, use the first available track + let systemLanguage = NSLocale.preferredLanguages.first + for i in 0.. index { + mediaOption = group.options[index] + } + } + } else { // default. invalid type or "system" + player?.currentItem?.selectMediaOptionAutomatically(in: group) + return + } + + // If a match isn't found, option will be nil and text tracks will be disabled + player?.currentItem?.select(mediaOption, in:group) + } + + static func setMediaSelectionTrackForCharacteristic(player:AVPlayer?, characteristic:AVMediaCharacteristic, criteria:SelectedTrackCriteria?) { + let type = criteria?.type + let group:AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) + var mediaOption:AVMediaSelectionOption! + + if (type == "disabled") { + // Do nothing. We want to ensure option is nil + } else if (type == "language") || (type == "title") { + let value = criteria?.value as? String + for i in 0.. index { + mediaOption = group.options[index] + } + } + } else if let group = group { // default. invalid type or "system" + player?.currentItem?.selectMediaOptionAutomatically(in: group) + return + } + + if let group = group { + // If a match isn't found, option will be nil and text tracks will be disabled + player?.currentItem?.select(mediaOption, in:group) + } + + } +} diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift new file mode 100644 index 00000000..5ead7fef --- /dev/null +++ b/ios/Video/Features/RCTResourceLoaderDelegate.swift @@ -0,0 +1,258 @@ +import AVFoundation + +class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { + + private var _loadingRequest:AVAssetResourceLoadingRequest? + private var _requestingCertificate:Bool = false + private var _requestingCertificateErrored:Bool = false + private var _drm: DRMParams? + private var _reactTag: NSNumber? + private var _onVideoError: RCTDirectEventBlock? + private var _onGetLicense: RCTDirectEventBlock? + + + init( + asset: AVURLAsset, + drm: DRMParams?, + onVideoError: RCTDirectEventBlock?, + onGetLicense: RCTDirectEventBlock?, + reactTag: NSNumber + ) { + super.init() + let queue = DispatchQueue(label: "assetQueue") + asset.resourceLoader.setDelegate(self, queue: queue) + _reactTag = reactTag + _onVideoError = onVideoError + _onGetLicense = onGetLicense + _drm = drm + } + + deinit { + _loadingRequest?.finishLoading() + } + + func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool { + return loadingRequestHandling(renewalRequest) + } + + func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest:AVAssetResourceLoadingRequest) -> Bool { + return loadingRequestHandling(loadingRequest) + } + + func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) { + NSLog("didCancelLoadingRequest") + } + + func base64DataFromBase64String(base64String:String?) -> Data? { + if let base64String = base64String { + return Data(base64Encoded:base64String) + } + return nil + } + + func setLicenseResult(_ license:String!) { + guard let respondData = self.base64DataFromBase64String(base64String: license), + let _loadingRequest = _loadingRequest else { + setLicenseResultError("No data from JS license response") + return + } + let dataRequest:AVAssetResourceLoadingDataRequest! = _loadingRequest.dataRequest + dataRequest.respond(with: respondData) + _loadingRequest.finishLoading() + } + + func setLicenseResultError(_ error:String!) { + if _loadingRequest != nil { + self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error)) + } + } + + func finishLoadingWithError(error:NSError!) -> Bool { + if let _loadingRequest = _loadingRequest, let error = error { + let licenseError:NSError! = error + _loadingRequest.finishLoading(with: licenseError) + + _onVideoError?([ + "error": [ + "code": NSNumber(value: error.code), + "localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription, + "localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "", + "localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "", + "domain": (error as NSError).domain + ], + "target": _reactTag + ]) + + } + return false + } + + func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { + if _requestingCertificate { + return true + } else if _requestingCertificateErrored { + return false + } + _loadingRequest = loadingRequest + + let url = loadingRequest.request.url + guard let _drm = _drm else { + return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) + } + + var contentId:String! + let contentIdOverride:String! = _drm.contentId + if contentIdOverride != nil { + contentId = contentIdOverride + } else if (_onGetLicense != nil) { + contentId = url?.host + } else { + contentId = url?.absoluteString.replacingOccurrences(of: "skd://", with:"") + } + + let drmType:String! = _drm.type + guard drmType == "fairplay" else { + return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) + } + + let certificateStringUrl:String! = _drm.certificateUrl + guard let certificateStringUrl = certificateStringUrl, let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { + return finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateURL) + } + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + var certificateData:Data? + if (_drm.base64Certificate != nil) { + certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters) + } else { + do { + certificateData = try Data(contentsOf: certificateURL) + } catch {} + } + + guard let certificateData = certificateData else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData) + self._requestingCertificateErrored = true + return + } + + var contentIdData:NSData! + if self._onGetLicense != nil { + contentIdData = contentId.data(using: .utf8) as NSData? + } else { + contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8)) + } + + let dataRequest:AVAssetResourceLoadingDataRequest! = loadingRequest.dataRequest + guard dataRequest != nil else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData) + self._requestingCertificateErrored = true + return + } + + var spcError:NSError! + var spcData: Data? + do { + spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil) + } catch let spcError { + print("SPC error") + } + // Request CKC to the server + var licenseServer:String! = _drm.licenseServer + if spcError != nil { + self.finishLoadingWithError(error: spcError) + self._requestingCertificateErrored = true + } + + guard spcData != nil else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noSPC) + self._requestingCertificateErrored = true + return + } + + // js client has a onGetLicense callback and will handle license fetching + if let _onGetLicense = self._onGetLicense { + let base64Encoded = spcData?.base64EncodedString(options: []) + self._requestingCertificate = true + if licenseServer == nil { + licenseServer = "" + } + _onGetLicense(["licenseUrl": licenseServer, + "contentId": contentId, + "spcBase64": base64Encoded, + "target": self._reactTag]) + + + } else if licenseServer != nil { + self.fetchLicense( + licenseServer: licenseServer, + spcData: spcData, + contentId: contentId, + dataRequest: dataRequest + ) + } + } + return true + } + + func fetchLicense( + licenseServer: String, + spcData: Data?, + contentId: String, + dataRequest: AVAssetResourceLoadingDataRequest! + ) { + var request = URLRequest(url: URL(string: licenseServer)!) + request.httpMethod = "POST" + + // HEADERS + if let headers = _drm?.headers { + for item in headers { + guard let key = item.key as? String, let value = item.value as? String else { + continue + } + request.setValue(value, forHTTPHeaderField: key) + } + } + + if (_onGetLicense != nil) { + request.httpBody = spcData + } else { + let spcEncoded = spcData?.base64EncodedString(options: []) + let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String + let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId) + let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true) + request.httpBody = postData + } + + let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ [weak self] (data:Data!,response:URLResponse!,error:Error!) in + guard let self = self else { return } + let httpResponse:HTTPURLResponse! = response as! HTTPURLResponse + guard error == nil else { + print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") + self.finishLoadingWithError(error: error as NSError?) + self._requestingCertificateErrored = true + return + } + guard httpResponse.statusCode == 200 else { + print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") + self.finishLoadingWithError(error: RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode)) + self._requestingCertificateErrored = true + return + } + + guard data != nil else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noDataFromLicenseRequest) + self._requestingCertificateErrored = true + return + } + + if (self._onGetLicense != nil) { + dataRequest.respond(with: data) + } else if let decodedData = Data(base64Encoded: data, options: []) { + dataRequest.respond(with: decodedData) + } + self._loadingRequest?.finishLoading() + }) + postDataTask.resume() + } +} diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift new file mode 100644 index 00000000..2b65a969 --- /dev/null +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -0,0 +1,83 @@ +enum RCTVideoError : Int { + case fromJSPart + case licenseRequestNotOk + case noDataFromLicenseRequest + case noSPC + case noDataRequest + case noCertificateData + case noCertificateURL + case noFairplayDRM + case noDRMData +} + +enum RCTVideoErrorHandler { + + static let noDRMData: NSError = NSError( + domain: "RCTVideo", + code: RCTVideoError.noDRMData.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No drm object found.", + NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?" + ]) + + static let noCertificateURL: NSError = NSError( + domain: "RCTVideo", + code: RCTVideoError.noCertificateURL.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM License.", + NSLocalizedFailureReasonErrorKey: "No certificate URL has been found.", + NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?" + ]) + + static let noCertificateData: NSError = NSError( + domain: "RCTVideo", + code: RCTVideoError.noCertificateData.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No certificate data obtained from the specificied url.", + NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?" + ]) + + static let noSPC:NSError! = NSError( + domain: "RCTVideo", + code: RCTVideoError.noSPC.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining license.", + NSLocalizedFailureReasonErrorKey: "No spc received.", + NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config." + ]) + + static let noDataFromLicenseRequest:NSError! = NSError( + domain: "RCTVideo", + code: RCTVideoError.noDataFromLicenseRequest.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No data received from the license server.", + NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?." + ]) + + static func licenseRequestNotOk(_ statusCode: Int) -> NSError { + return NSError( + domain: "RCTVideo", + code: RCTVideoError.licenseRequestNotOk.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining license.", + NSLocalizedFailureReasonErrorKey: String( + format:"License server responded with status code %li", + (statusCode) + ), + NSLocalizedRecoverySuggestionErrorKey: "Did you send the correct data to the license Server? Is the server ok?" + ]) + } + + static func fromJSPart(_ error: String) -> NSError { + return NSError(domain: "RCTVideo", + code: RCTVideoError.fromJSPart.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: error, + NSLocalizedFailureReasonErrorKey: error, + NSLocalizedRecoverySuggestionErrorKey: error + ]) + } +} diff --git a/ios/Video/Features/RCTVideoSave.swift b/ios/Video/Features/RCTVideoSave.swift new file mode 100644 index 00000000..ff8155ec --- /dev/null +++ b/ios/Video/Features/RCTVideoSave.swift @@ -0,0 +1,75 @@ +import AVFoundation + +enum RCTVideoSave { + + static func save( + options:NSDictionary!, + resolve: @escaping RCTPromiseResolveBlock, + reject:@escaping RCTPromiseRejectBlock, + + playerItem: AVPlayerItem? + ) { + let asset:AVAsset! = playerItem?.asset + + guard asset != nil else { + reject("ERROR_ASSET_NIL", "Asset is nil", nil) + return + } + + guard let exportSession = AVAssetExportSession(asset: asset, presetName:AVAssetExportPresetHighestQuality) else { + reject("ERROR_COULD_NOT_CREATE_EXPORT_SESSION", "Could not create export session", nil) + return + } + var path:String! = nil + path = RCTVideoSave.generatePathInDirectory( + directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path, + withExtension: ".mp4") + let url:NSURL! = NSURL.fileURL(withPath: path) as NSURL + exportSession.outputFileType = AVFileType.mp4 + exportSession.outputURL = url as URL? + exportSession.videoComposition = playerItem?.videoComposition + exportSession.shouldOptimizeForNetworkUse = true + exportSession.exportAsynchronously(completionHandler: { + + switch (exportSession.status) { + case .failed: + reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error) + break + case .cancelled: + reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error) + break + default: + resolve(["uri": url.absoluteString]) + break + } + + }) + } + + static func generatePathInDirectory(directory: String?, withExtension `extension`: String?) -> String? { + let fileName = UUID().uuidString + (`extension` ?? "") + RCTVideoSave.ensureDirExists(withPath: directory) + return URL(fileURLWithPath: directory ?? "").appendingPathComponent(fileName).path + } + + static func cacheDirectoryPath() -> String? { + let array = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path) + return array[0] + } + + static func ensureDirExists(withPath path: String?) -> Bool { + var isDir: ObjCBool = false + var error: Error? + let exists = FileManager.default.fileExists(atPath: path ?? "", isDirectory: &isDir) + if !(exists && isDir.boolValue) { + do { + try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil) + } catch { + } + if error != nil { + return false + } + } + return true + } +} diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift new file mode 100644 index 00000000..51a6aae7 --- /dev/null +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -0,0 +1,143 @@ +import AVFoundation + +/*! + * Collection of pure functions + */ +enum RCTVideoUtils { + + /*! + * Calculates and returns the playable duration of the current player item using its loaded time ranges. + * + * \returns The playable duration of the current player item in seconds. + */ + static func calculatePlayableDuration(_ player:AVPlayer?) -> NSNumber { + guard let player = player, + let video:AVPlayerItem = player.currentItem, + video.status == AVPlayerItem.Status.readyToPlay else { + return 0 + } + + var effectiveTimeRange:CMTimeRange? + for (_, value) in video.loadedTimeRanges.enumerated() { + let timeRange:CMTimeRange = value.timeRangeValue + if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) { + effectiveTimeRange = timeRange + break + } + } + + if let effectiveTimeRange = effectiveTimeRange { + let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) + if playableDuration > 0 { + return playableDuration as NSNumber + } + } + + return 0 + } + + static func urlFilePath(filepath:NSString!) -> NSURL! { + if filepath.contains("file://") { + return NSURL(string: filepath as String) + } + + // if no file found, check if the file exists in the Document directory + let paths:[String]! = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + var relativeFilePath:String! = filepath.lastPathComponent + // the file may be multiple levels below the documents directory + let fileComponents:[String]! = filepath.components(separatedBy: "Documents/") + if fileComponents.count > 1 { + relativeFilePath = fileComponents[1] + } + + let path:String! = (paths.first! as NSString).appendingPathComponent(relativeFilePath) + if FileManager.default.fileExists(atPath: path) { + return NSURL.fileURL(withPath: path) as NSURL + } + return nil + } + + static func playerItemSeekableTimeRange(_ player:AVPlayer?) -> CMTimeRange { + if let playerItem = player?.currentItem, + playerItem.status == .readyToPlay, + let firstItem = playerItem.seekableTimeRanges.first { + return firstItem.timeRangeValue + } + + return (CMTimeRange.zero) + } + + static func playerItemDuration(_ player:AVPlayer?) -> CMTime { + if let playerItem = player?.currentItem, + playerItem.status == .readyToPlay { + return(playerItem.duration) + } + + return(CMTime.invalid) + } + + static func calculateSeekableDuration(_ player:AVPlayer?) -> NSNumber { + let timeRange:CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player) + if CMTIME_IS_NUMERIC(timeRange.duration) + { + return NSNumber(value: CMTimeGetSeconds(timeRange.duration)) + } + return 0 + } + + static func getAudioTrackInfo(_ player:AVPlayer?) -> [AnyObject]! { + guard let player = player else { + return [] + } + + let audioTracks:NSMutableArray! = NSMutableArray() + let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) + for i in 0..<(group?.options.count ?? 0) { + let currentOption = group?.options[i] + var title = "" + let values = currentOption?.commonMetadata.map(\.value) + if (values?.count ?? 0) > 0, let value = values?[0] { + title = value as! String + } + let language:String! = currentOption?.extendedLanguageTag ?? "" + let audioTrack = [ + "index": NSNumber(value: i), + "title": title, + "language": language + ] as [String : Any] + audioTracks.add(audioTrack) + } + return audioTracks as [AnyObject]? + } + + static func getTextTrackInfo(_ player:AVPlayer?) -> [TextTrack]! { + guard let player = player else { + return [] + } + + // if streaming video, we extract the text tracks + var textTracks:[TextTrack] = [] + let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) + for i in 0..<(group?.options.count ?? 0) { + let currentOption = group?.options[i] + var title = "" + let values = currentOption?.commonMetadata.map(\.value) + if (values?.count ?? 0) > 0, let value = values?[0] { + title = value as! String + } + let language:String! = currentOption?.extendedLanguageTag ?? "" + let textTrack = TextTrack([ + "index": NSNumber(value: i), + "title": title, + "language": language + ]) + textTracks.append(textTrack) + } + return textTracks + } + + // UNUSED + static func getCurrentTime(playerItem:AVPlayerItem?) -> Float { + return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero)) + } +} diff --git a/ios/Video/RCTSwiftLog/RCTSwiftLog.h b/ios/Video/RCTSwiftLog/RCTSwiftLog.h new file mode 100644 index 00000000..8f9deab7 --- /dev/null +++ b/ios/Video/RCTSwiftLog/RCTSwiftLog.h @@ -0,0 +1,11 @@ +#import + +@interface RCTSwiftLog : NSObject + ++ (void)error:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)warn:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)info:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)log:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)trace:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + +@end diff --git a/ios/Video/RCTSwiftLog/RCTSwiftLog.m b/ios/Video/RCTSwiftLog/RCTSwiftLog.m new file mode 100644 index 00000000..4e153846 --- /dev/null +++ b/ios/Video/RCTSwiftLog/RCTSwiftLog.m @@ -0,0 +1,32 @@ +#import + +#import "RCTSwiftLog.h" + +@implementation RCTSwiftLog + ++ (void)info:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)warn:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelWarning, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)error:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelError, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)log:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)trace:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message); +} + +@end diff --git a/ios/Video/RCTSwiftLog/RCTSwiftLog.swift b/ios/Video/RCTSwiftLog/RCTSwiftLog.swift new file mode 100644 index 00000000..ec39d9b1 --- /dev/null +++ b/ios/Video/RCTSwiftLog/RCTSwiftLog.swift @@ -0,0 +1,53 @@ +// +// RCTLog.swift +// WebViewExample +// +// Created by Jimmy Dee on 4/5/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +/* + * Under at least some conditions, output from NSLog has been unavailable in the RNBranch module. + * Hence that module uses the RCTLog macros from . The React logger is nicer than + * NSLog anyway, since it provides log levels with runtime filtering, file and line context and + * an identifier for the thread that logged the message. + * + * This wrapper lets you use functions with the same name in Swift. For example: + * + * RCTLogInfo("application launched") + * + * generates + * + * 2017-04-06 12:31:09.611 [info][tid:main][AppDelegate.swift:18] application launched + * + * This is currently part of this sample app. There may be some issues integrating it into an + * Objective-C library, either react-native-branch or react-native itself, but it may find its + * way into one or the other eventually. Feel free to reuse it as desired. + */ + +func RCTLogError(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.error(message, file: file, line: line) +} + +func RCTLogWarn(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.warn(message, file: file, line: line) +} + +func RCTLogInfo(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.info(message, file: file, line: line) +} + +func RCTLog(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.log(message, file: file, line: line) +} + +func RCTLogTrace(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.trace(message, file: file, line: line) +} + +func DebugLog(_ message: String) { +#if DEBUG + print(message) +#endif +} + diff --git a/ios/Video/RCTVideo-Bridging-Header.h b/ios/Video/RCTVideo-Bridging-Header.h new file mode 100644 index 00000000..ad6089be --- /dev/null +++ b/ios/Video/RCTVideo-Bridging-Header.h @@ -0,0 +1,7 @@ +#import +#import "RCTSwiftLog.h" + +#if __has_include() +#import "RCTVideoCache.h" +#endif + diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h deleted file mode 100644 index 6fee2996..00000000 --- a/ios/Video/RCTVideo.h +++ /dev/null @@ -1,67 +0,0 @@ -#import -#import "AVKit/AVKit.h" -#import "UIView+FindUIViewController.h" -#import "RCTVideoPlayerViewController.h" -#import "RCTVideoPlayerViewControllerDelegate.h" -#import -#import - -#if __has_include() -#import -#import -#import -#endif - -@class RCTEventDispatcher; -#if __has_include() -@interface RCTVideo : UIView -#elif TARGET_OS_TV -@interface RCTVideo : UIView -#else -@interface RCTVideo : UIView -#endif - -@property (nonatomic, copy) RCTDirectEventBlock onVideoLoadStart; -@property (nonatomic, copy) RCTDirectEventBlock onVideoLoad; -@property (nonatomic, copy) RCTDirectEventBlock onVideoBuffer; -@property (nonatomic, copy) RCTDirectEventBlock onVideoError; -@property (nonatomic, copy) RCTDirectEventBlock onVideoProgress; -@property (nonatomic, copy) RCTDirectEventBlock onBandwidthUpdate; -@property (nonatomic, copy) RCTDirectEventBlock onVideoSeek; -@property (nonatomic, copy) RCTDirectEventBlock onVideoEnd; -@property (nonatomic, copy) RCTDirectEventBlock onTimedMetadata; -@property (nonatomic, copy) RCTDirectEventBlock onVideoAudioBecomingNoisy; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillPresent; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidPresent; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillDismiss; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidDismiss; -@property (nonatomic, copy) RCTDirectEventBlock onReadyForDisplay; -@property (nonatomic, copy) RCTDirectEventBlock onPlaybackStalled; -@property (nonatomic, copy) RCTDirectEventBlock onPlaybackResume; -@property (nonatomic, copy) RCTDirectEventBlock onPlaybackRateChange; -@property (nonatomic, copy) RCTDirectEventBlock onVideoExternalPlaybackChange; -@property (nonatomic, copy) RCTDirectEventBlock onPictureInPictureStatusChanged; -@property (nonatomic, copy) RCTDirectEventBlock onRestoreUserInterfaceForPictureInPictureStop; -@property (nonatomic, copy) RCTDirectEventBlock onGetLicense; - -typedef NS_ENUM(NSInteger, RCTVideoError) { - RCTVideoErrorFromJSPart, - RCTVideoErrorLicenseRequestNotOk, - RCTVideoErrorNoDataFromLicenseRequest, - RCTVideoErrorNoSPC, - RCTVideoErrorNoDataRequest, - RCTVideoErrorNoCertificateData, - RCTVideoErrorNoCertificateURL, - RCTVideoErrorNoFairplayDRM, - RCTVideoErrorNoDRMData -}; - -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - -- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem; - -- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; -- (void)setLicenseResult:(NSString * )license; -- (BOOL)setLicenseResultError:(NSString * )error; - -@end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m deleted file mode 100644 index 989167a6..00000000 --- a/ios/Video/RCTVideo.m +++ /dev/null @@ -1,2013 +0,0 @@ -#import -#import "RCTVideo.h" -#import -#import -#import -#include -#include - -static NSString *const statusKeyPath = @"status"; -static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"; -static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; -static NSString *const readyForDisplayKeyPath = @"readyForDisplay"; -static NSString *const playbackRate = @"rate"; -static NSString *const timedMetadata = @"timedMetadata"; -static NSString *const externalPlaybackActive = @"externalPlaybackActive"; - -static int const RCTVideoUnset = -1; - -#ifdef DEBUG - #define DebugLog(...) NSLog(__VA_ARGS__) -#else - #define DebugLog(...) (void)0 -#endif - -@implementation RCTVideo -{ - AVPlayer *_player; - AVPlayerItem *_playerItem; - NSDictionary *_source; - BOOL _playerItemObserversSet; - BOOL _playerBufferEmpty; - AVPlayerLayer *_playerLayer; - BOOL _playerLayerObserverSet; - RCTVideoPlayerViewController *_playerViewController; - NSURL *_videoURL; - BOOL _requestingCertificate; - BOOL _requestingCertificateErrored; - - /* DRM */ - NSDictionary *_drm; - AVAssetResourceLoadingRequest *_loadingRequest; - - /* Required to publish events */ - RCTEventDispatcher *_eventDispatcher; - BOOL _playbackRateObserverRegistered; - BOOL _isExternalPlaybackActiveObserverRegistered; - BOOL _videoLoadStarted; - - bool _pendingSeek; - float _pendingSeekTime; - float _lastSeekTime; - - /* For sending videoProgress events */ - Float64 _progressUpdateInterval; - BOOL _controls; - id _timeObserver; - - /* Keep track of any modifiers, need to be applied after each play */ - float _volume; - float _rate; - float _maxBitRate; - - BOOL _automaticallyWaitsToMinimizeStalling; - BOOL _muted; - BOOL _paused; - BOOL _repeat; - BOOL _allowsExternalPlayback; - NSArray * _textTracks; - NSDictionary * _selectedTextTrack; - NSDictionary * _selectedAudioTrack; - BOOL _playbackStalled; - BOOL _playInBackground; - BOOL _preventsDisplaySleepDuringVideoPlayback; - float _preferredForwardBufferDuration; - BOOL _playWhenInactive; - BOOL _pictureInPicture; - NSString * _ignoreSilentSwitch; - NSString * _mixWithOthers; - NSString * _resizeMode; - BOOL _fullscreen; - BOOL _fullscreenAutorotate; - NSString * _fullscreenOrientation; - BOOL _fullscreenPlayerPresented; - NSString *_filterName; - BOOL _filterEnabled; - UIViewController * _presentingViewController; -#if __has_include() - RCTVideoCache * _videoCache; -#endif -#if TARGET_OS_IOS - void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL); - AVPictureInPictureController *_pipController; -#endif -} - -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher -{ - if ((self = [super init])) { - _eventDispatcher = eventDispatcher; - _automaticallyWaitsToMinimizeStalling = YES; - _playbackRateObserverRegistered = NO; - _isExternalPlaybackActiveObserverRegistered = NO; - _playbackStalled = NO; - _rate = 1.0; - _volume = 1.0; - _resizeMode = @"AVLayerVideoGravityResizeAspectFill"; - _fullscreenAutorotate = YES; - _fullscreenOrientation = @"all"; - _pendingSeek = false; - _pendingSeekTime = 0.0f; - _lastSeekTime = 0.0f; - _progressUpdateInterval = 250; - _controls = NO; - _playerBufferEmpty = YES; - _playInBackground = false; - _preventsDisplaySleepDuringVideoPlayback = true; - _preferredForwardBufferDuration = 0.0f; - _allowsExternalPlayback = YES; - _playWhenInactive = false; - _pictureInPicture = false; - _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey - _mixWithOthers = @"inherit"; // inherit, mix, duck -#if TARGET_OS_IOS - _restoreUserInterfaceForPIPStopCompletionHandler = NULL; -#endif -#if __has_include() - _videoCache = [RCTVideoCache sharedInstance]; -#endif - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(audioRouteChanged:) - name:AVAudioSessionRouteChangeNotification - object:nil]; - } - - return self; -} - -- (RCTVideoPlayerViewController*)createPlayerViewController:(AVPlayer*)player - withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* viewController = [[RCTVideoPlayerViewController alloc] init]; - viewController.showsPlaybackControls = YES; - viewController.rctDelegate = self; - viewController.preferredOrientation = _fullscreenOrientation; - - viewController.view.frame = self.bounds; - viewController.player = player; - return viewController; -} - -/* --------------------------------------------------------- - ** Get the duration for a AVPlayerItem. - ** ------------------------------------------------------- */ - -- (CMTime)playerItemDuration -{ - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return([playerItem duration]); - } - - return(kCMTimeInvalid); -} - -- (CMTimeRange)playerItemSeekableTimeRange -{ - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; - } - - return (kCMTimeRangeZero); -} - --(void)addPlayerTimeObserver -{ - const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000; - // @see endScrubbing in AVPlayerDemoPlaybackViewController.m - // of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html - __weak RCTVideo *weakSelf = self; - _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(progressUpdateIntervalMS, NSEC_PER_SEC) - queue:NULL - usingBlock:^(CMTime time) { [weakSelf sendProgressUpdate]; } - ]; -} - -/* Cancels the previously registered time observer. */ --(void)removePlayerTimeObserver -{ - if (_timeObserver) - { - [_player removeTimeObserver:_timeObserver]; - _timeObserver = nil; - } -} - -#pragma mark - Progress - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self removePlayerLayer]; - [self removePlayerItemObservers]; - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - [_player removeObserver:self forKeyPath:externalPlaybackActive context: nil]; -} - -#pragma mark - App lifecycle handlers - -- (void)applicationWillResignActive:(NSNotification *)notification -{ - if (_playInBackground || _playWhenInactive || _paused) return; - - [_player pause]; - [_player setRate:0.0]; -} - -- (void)applicationDidEnterBackground:(NSNotification *)notification -{ - if (_playInBackground) { - // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html - [_playerLayer setPlayer:nil]; - [_playerViewController setPlayer:nil]; - } -} - -- (void)applicationWillEnterForeground:(NSNotification *)notification -{ - [self applyModifiers]; - if (_playInBackground) { - [_playerLayer setPlayer:_player]; - [_playerViewController setPlayer:_player]; - } -} - -#pragma mark - Audio events - -- (void)audioRouteChanged:(NSNotification *)notification -{ - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } -} - -#pragma mark - Progress - -- (void)sendProgressUpdate -{ - AVPlayerItem *video = [_player currentItem]; - if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) { - return; - } - - CMTime playerDuration = [self playerItemDuration]; - if (CMTIME_IS_INVALID(playerDuration)) { - return; - } - - CMTime currentTime = _player.currentTime; - NSDate *currentPlaybackTime = _player.currentItem.currentDate; - const Float64 duration = CMTimeGetSeconds(playerDuration); - const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime); - - [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}]; - - if( currentTimeSecs >= 0 && self.onVideoProgress) { - self.onVideoProgress(@{ - @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)], - @"playableDuration": [self calculatePlayableDuration], - @"atValue": [NSNumber numberWithLongLong:currentTime.value], - @"atTimescale": [NSNumber numberWithInt:currentTime.timescale], - @"currentPlaybackTime": [NSNumber numberWithLongLong:[@(floor([currentPlaybackTime timeIntervalSince1970] * 1000)) longLongValue]], - @"target": self.reactTag, - @"seekableDuration": [self calculateSeekableDuration], - }); - } -} - -/*! - * Calculates and returns the playable duration of the current player item using its loaded time ranges. - * - * \returns The playable duration of the current player item in seconds. - */ -- (NSNumber *)calculatePlayableDuration -{ - AVPlayerItem *video = _player.currentItem; - if (video.status == AVPlayerItemStatusReadyToPlay) { - __block CMTimeRange effectiveTimeRange; - [video.loadedTimeRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - CMTimeRange timeRange = [obj CMTimeRangeValue]; - if (CMTimeRangeContainsTime(timeRange, video.currentTime)) { - effectiveTimeRange = timeRange; - *stop = YES; - } - }]; - Float64 playableDuration = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)); - if (playableDuration > 0) { - return [NSNumber numberWithFloat:playableDuration]; - } - } - return [NSNumber numberWithInteger:0]; -} - -- (NSNumber *)calculateSeekableDuration -{ - CMTimeRange timeRange = [self playerItemSeekableTimeRange]; - if (CMTIME_IS_NUMERIC(timeRange.duration)) - { - return [NSNumber numberWithFloat:CMTimeGetSeconds(timeRange.duration)]; - } - return [NSNumber numberWithInteger:0]; -} - -- (void)addPlayerItemObservers -{ - [_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil]; - [_playerItem addObserver:self forKeyPath:playbackBufferEmptyKeyPath options:0 context:nil]; - [_playerItem addObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath options:0 context:nil]; - [_playerItem addObserver:self forKeyPath:timedMetadata options:NSKeyValueObservingOptionNew context:nil]; - _playerItemObserversSet = YES; -} - -/* Fixes https://github.com/brentvatne/react-native-video/issues/43 - * Crashes caused when trying to remove the observer when there is no - * observer set */ -- (void)removePlayerItemObservers -{ - if (_playerItemObserversSet) { - [_playerItem removeObserver:self forKeyPath:statusKeyPath]; - [_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath]; - [_playerItem removeObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath]; - [_playerItem removeObserver:self forKeyPath:timedMetadata]; - _playerItemObserversSet = NO; - } -} - -#pragma mark - Player and source - -- (void)setSrc:(NSDictionary *)source -{ - _source = source; - [self removePlayerLayer]; - [self removePlayerTimeObserver]; - [self removePlayerItemObservers]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ - - // perform on next run loop, otherwise other passed react-props may not be set - [self playerItemForSource:self->_source withCallback:^(AVPlayerItem * playerItem) { - self->_playerItem = playerItem; - _playerItem = playerItem; - [self setPreferredForwardBufferDuration:_preferredForwardBufferDuration]; - [self addPlayerItemObservers]; - [self setFilter:self->_filterName]; - [self setMaxBitRate:self->_maxBitRate]; - - [_player pause]; - - if (_playbackRateObserverRegistered) { - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - _playbackRateObserverRegistered = NO; - } - if (self->_isExternalPlaybackActiveObserverRegistered) { - [self->_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - self->_isExternalPlaybackActiveObserverRegistered = NO; - } - - self->_player = [AVPlayer playerWithPlayerItem:self->_playerItem]; - self->_player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - - [self->_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; - self->_playbackRateObserverRegistered = YES; - - [self->_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; - self->_isExternalPlaybackActiveObserverRegistered = YES; - - [self addPlayerTimeObserver]; - if (@available(iOS 10.0, *)) { - [self setAutomaticallyWaitsToMinimizeStalling:_automaticallyWaitsToMinimizeStalling]; - } - - //Perform on next run loop, otherwise onVideoLoadStart is nil - if (self.onVideoLoadStart) { - id uri = [self->_source objectForKey:@"uri"]; - id type = [self->_source objectForKey:@"type"]; - self.onVideoLoadStart(@{@"src": @{ - @"uri": uri ? uri : [NSNull null], - @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[self->_source objectForKey:@"isNetwork"]]}, - @"drm": self->_drm ? self->_drm : [NSNull null], - @"target": self.reactTag - }); - } - }]; - }); - _videoLoadStarted = YES; -} - -- (void)setDrm:(NSDictionary *)drm { - _drm = drm; -} - -- (NSURL*) urlFilePath:(NSString*) filepath { - if ([filepath containsString:@"file://"]) { - return [NSURL URLWithString:filepath]; - } - - // if no file found, check if the file exists in the Document directory - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString* relativeFilePath = [filepath lastPathComponent]; - // the file may be multiple levels below the documents directory - NSArray* fileComponents = [filepath componentsSeparatedByString:@"Documents/"]; - if (fileComponents.count > 1) { - relativeFilePath = [fileComponents objectAtIndex:1]; - } - - NSString *path = [paths.firstObject stringByAppendingPathComponent:relativeFilePath]; - if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { - return [NSURL fileURLWithPath:path]; - } - return nil; -} - -- (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nullable)assetOptions withCallback:(void(^)(AVPlayerItem *))handler -{ - if (!_textTracks || _textTracks.count==0) { - handler([AVPlayerItem playerItemWithAsset:asset]); - return; - } - - // AVPlayer can't airplay AVMutableCompositions - _allowsExternalPlayback = NO; - - // sideload text tracks - AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; - - AVAssetTrack *videoAsset = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject; - AVMutableCompositionTrack *videoCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; - [videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:videoAsset - atTime:kCMTimeZero - error:nil]; - - AVAssetTrack *audioAsset = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject; - AVMutableCompositionTrack *audioCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; - [audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:audioAsset - atTime:kCMTimeZero - error:nil]; - - NSMutableArray* validTextTracks = [NSMutableArray array]; - for (int i = 0; i < _textTracks.count; ++i) { - AVURLAsset *textURLAsset; - NSString *textUri = [_textTracks objectAtIndex:i][@"uri"]; - if ([[textUri lowercaseString] hasPrefix:@"http"]) { - textURLAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:textUri] options:assetOptions]; - } else { - textURLAsset = [AVURLAsset URLAssetWithURL:[self urlFilePath:textUri] options:nil]; - } - AVAssetTrack *textTrackAsset = [textURLAsset tracksWithMediaType:AVMediaTypeText].firstObject; - if (!textTrackAsset) continue; // fix when there's no textTrackAsset - [validTextTracks addObject:[_textTracks objectAtIndex:i]]; - AVMutableCompositionTrack *textCompTrack = [mixComposition - addMutableTrackWithMediaType:AVMediaTypeText - preferredTrackID:kCMPersistentTrackID_Invalid]; - [textCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:textTrackAsset - atTime:kCMTimeZero - error:nil]; - } - if (validTextTracks.count != _textTracks.count) { - [self setTextTracks:validTextTracks]; - } - - handler([AVPlayerItem playerItemWithAsset:mixComposition]); -} - -- (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlayerItem *))handler -{ - bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]]; - bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; - bool shouldCache = [RCTConvert BOOL:[source objectForKey:@"shouldCache"]]; - NSString *uri = [source objectForKey:@"uri"]; - NSString *type = [source objectForKey:@"type"]; - AVURLAsset *asset; - if (!uri || [uri isEqualToString:@""]) { - DebugLog(@"Could not find video URL in source '%@'", source); - return; - } - - NSURL *url = isNetwork || isAsset - ? [NSURL URLWithString:uri] - : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; - NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init]; - - if (isNetwork) { - NSDictionary *headers = [source objectForKey:@"requestHeaders"]; - if ([headers count] > 0) { - [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; - } - NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; - [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; - -#if __has_include() - if (shouldCache && (!_textTracks || !_textTracks.count)) { - /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying - * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. - * Until this is fixed, we need to bypass caching when text tracks are specified. - */ - DebugLog(@"Caching is not supported for uri '%@' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - [self playerItemForSourceUsingCache:uri assetOptions:assetOptions withCallback:handler]; - return; - } -#endif - - asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; - } else if (isAsset) { - asset = [AVURLAsset URLAssetWithURL:url options:nil]; - } else { - asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; - } - // Reset _loadingRequest - if (_loadingRequest != nil) { - [_loadingRequest finishLoading]; - } - _requestingCertificate = NO; - _requestingCertificateErrored = NO; - // End Reset _loadingRequest - if (self->_drm != nil) { - dispatch_queue_t queue = dispatch_queue_create("assetQueue", nil); - [asset.resourceLoader setDelegate:self queue:queue]; - } - - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; -} - -#if __has_include() - -- (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary *)options withCallback:(void(^)(AVPlayerItem *))handler { - NSURL *url = [NSURL URLWithString:uri]; - [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { - switch (videoCacheStatus) { - case RCTVideoCacheStatusMissingFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - case RCTVideoCacheStatusUnsupportedFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - default: - if (cachedAsset) { - DebugLog(@"Playing back uri '%@' from cache", uri); - // See note in playerItemForSource about not being able to support text tracks & caching - handler([AVPlayerItem playerItemWithAsset:cachedAsset]); - return; - } - } - - DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; - asset.loaderDelegate = self; - - /* More granular code to have control over the DVURLAsset - DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; - resourceLoaderDelegate.delegate = self; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; - components.scheme = [DVAssetLoaderDelegate scheme]; - AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; - [asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()]; - */ - - handler([AVPlayerItem playerItemWithAsset:asset]); - }]; -} - -#pragma mark - DVAssetLoaderDelegate - -- (void)dvAssetLoaderDelegate:(DVAssetLoaderDelegate *)loaderDelegate - didLoadData:(NSData *)data - forURL:(NSURL *)url { - [_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) { - DebugLog(@"Cache data stored successfully 🎉"); - }]; -} - -#endif - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - - if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) { - self.onReadyForDisplay(@{@"target": self.reactTag}); - return; - } - if (object == _playerItem) { - // When timeMetadata is read the event onTimedMetadata is triggered - if ([keyPath isEqualToString:timedMetadata]) { - NSArray *items = [change objectForKey:@"new"]; - if (items && ![items isEqual:[NSNull null]] && items.count > 0) { - NSMutableArray *array = [NSMutableArray new]; - for (AVMetadataItem *item in items) { - NSString *value = (NSString *)item.value; - NSString *identifier = item.identifier; - - if (![value isEqual: [NSNull null]]) { - NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]]; - - [array addObject:dictionary]; - } - } - - self.onTimedMetadata(@{ - @"target": self.reactTag, - @"metadata": array - }); - } - } - - if ([keyPath isEqualToString:statusKeyPath]) { - // Handle player item status change. - if (_playerItem.status == AVPlayerItemStatusReadyToPlay) { - float duration = CMTimeGetSeconds(_playerItem.asset.duration); - - if (isnan(duration)) { - duration = 0.0; - } - - NSObject *width = @"undefined"; - NSObject *height = @"undefined"; - NSString *orientation = @"undefined"; - - if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) { - AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; - width = [NSNumber numberWithFloat:videoTrack.naturalSize.width]; - height = [NSNumber numberWithFloat:videoTrack.naturalSize.height]; - CGAffineTransform preferredTransform = [videoTrack preferredTransform]; - - if ((videoTrack.naturalSize.width == preferredTransform.tx - && videoTrack.naturalSize.height == preferredTransform.ty) - || (preferredTransform.tx == 0 && preferredTransform.ty == 0)) - { - orientation = @"landscape"; - } else { - orientation = @"portrait"; - } - } else if (_playerItem.presentationSize.height) { - width = [NSNumber numberWithFloat:_playerItem.presentationSize.width]; - height = [NSNumber numberWithFloat:_playerItem.presentationSize.height]; - orientation = _playerItem.presentationSize.width > _playerItem.presentationSize.height ? @"landscape" : @"portrait"; - } - - if (_pendingSeek) { - [self setCurrentTime:_pendingSeekTime]; - _pendingSeek = false; - } - - if (self.onVideoLoad && _videoLoadStarted) { - self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration], - @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)], - @"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse], - @"canPlayFastForward": [NSNumber numberWithBool:_playerItem.canPlayFastForward], - @"canPlaySlowForward": [NSNumber numberWithBool:_playerItem.canPlaySlowForward], - @"canPlaySlowReverse": [NSNumber numberWithBool:_playerItem.canPlaySlowReverse], - @"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward], - @"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward], - @"naturalSize": @{ - @"width": width, - @"height": height, - @"orientation": orientation - }, - @"audioTracks": [self getAudioTrackInfo], - @"textTracks": [self getTextTrackInfo], - @"target": self.reactTag}); - } - _videoLoadStarted = NO; - - [self attachListeners]; - [self applyModifiers]; - } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { - self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code], - @"localizedDescription": [_playerItem.error localizedDescription] == nil ? @"" : [_playerItem.error localizedDescription], - @"localizedFailureReason": [_playerItem.error localizedFailureReason] == nil ? @"" : [_playerItem.error localizedFailureReason], - @"localizedRecoverySuggestion": [_playerItem.error localizedRecoverySuggestion] == nil ? @"" : [_playerItem.error localizedRecoverySuggestion], - @"domain": _playerItem != nil && _playerItem.error != nil ? _playerItem.error.domain : @"RTCVideo"}, - @"target": self.reactTag}); - } - } else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) { - _playerBufferEmpty = YES; - self.onVideoBuffer(@{@"isBuffering": @(YES), @"target": self.reactTag}); - } else if ([keyPath isEqualToString:playbackLikelyToKeepUpKeyPath]) { - // Continue playing (or not if paused) after being paused due to hitting an unbuffered zone. - if ((!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && _playerItem.playbackLikelyToKeepUp) { - [self setPaused:_paused]; - } - _playerBufferEmpty = NO; - self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); - } - } else if (object == _player) { - if([keyPath isEqualToString:playbackRate]) { - if(self.onPlaybackRateChange) { - self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], - @"target": self.reactTag}); - } - if(_playbackStalled && _player.rate > 0) { - if(self.onPlaybackResume) { - self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], - @"target": self.reactTag}); - } - _playbackStalled = NO; - } - } - else if([keyPath isEqualToString:externalPlaybackActive]) { - if(self.onVideoExternalPlaybackChange) { - self.onVideoExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], - @"target": self.reactTag}); - } - } - } else if (object == _playerViewController.contentOverlayView) { - // when controls==true, this is a hack to reset the rootview when rotation happens in fullscreen - if ([keyPath isEqualToString:@"frame"]) { - - CGRect oldRect = [change[NSKeyValueChangeOldKey] CGRectValue]; - CGRect newRect = [change[NSKeyValueChangeNewKey] CGRectValue]; - - if (!CGRectEqualToRect(oldRect, newRect)) { - if (CGRectEqualToRect(newRect, [UIScreen mainScreen].bounds)) { - NSLog(@"in fullscreen"); - - [self.reactViewController.view setFrame:[UIScreen mainScreen].bounds]; - [self.reactViewController.view setNeedsLayout]; - } else NSLog(@"not fullscreen"); - } - - return; - } - } -} - -- (void)attachListeners -{ - // listen for end of file - [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVPlayerItemDidPlayToEndTimeNotification - object:[_player currentItem]]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(playerItemDidReachEnd:) - name:AVPlayerItemDidPlayToEndTimeNotification - object:[_player currentItem]]; - - [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVPlayerItemPlaybackStalledNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(playbackStalled:) - name:AVPlayerItemPlaybackStalledNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVPlayerItemNewAccessLogEntryNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleAVPlayerAccess:) - name:AVPlayerItemNewAccessLogEntryNotification - object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self - name: AVPlayerItemFailedToPlayToEndTimeNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didFailToFinishPlaying:) - name: AVPlayerItemFailedToPlayToEndTimeNotification - object:nil]; - -} - -- (void)handleAVPlayerAccess:(NSNotification *)notification { - AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; - AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; - - /* TODO: get this working - if (self.onBandwidthUpdate) { - self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); - } - */ -} - -- (void)didFailToFinishPlaying:(NSNotification *)notification { - NSError *error = notification.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey]; - self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], - @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], - @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], - @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], - @"domain": error.domain}, - @"target": self.reactTag}); -} - -- (void)playbackStalled:(NSNotification *)notification -{ - if(self.onPlaybackStalled) { - self.onPlaybackStalled(@{@"target": self.reactTag}); - } - _playbackStalled = YES; -} - -- (void)playerItemDidReachEnd:(NSNotification *)notification -{ - if(self.onVideoEnd) { - self.onVideoEnd(@{@"target": self.reactTag}); - } - - if (_repeat) { - AVPlayerItem *item = [notification object]; - [item seekToTime:kCMTimeZero]; - [self applyModifiers]; - } else { - [self removePlayerTimeObserver]; - } -} - -#pragma mark - Prop setters - -- (void)setResizeMode:(NSString*)mode -{ - if( _controls ) - { - _playerViewController.videoGravity = mode; - } - else - { - _playerLayer.videoGravity = mode; - } - _resizeMode = mode; -} - -- (void)setPlayInBackground:(BOOL)playInBackground -{ - _playInBackground = playInBackground; -} - -- (void)setPreventsDisplaySleepDuringVideoPlayback:(BOOL)preventsDisplaySleepDuringVideoPlayback -{ - _preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback; - [self applyModifiers]; -} - -- (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback -{ - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; -} - -- (void)setPlayWhenInactive:(BOOL)playWhenInactive -{ - _playWhenInactive = playWhenInactive; -} - -- (void)setPictureInPicture:(BOOL)pictureInPicture -{ - #if TARGET_OS_IOS - if (_pictureInPicture == pictureInPicture) { - return; - } - - _pictureInPicture = pictureInPicture; - if (_pipController && _pictureInPicture && ![_pipController isPictureInPictureActive]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_pipController startPictureInPicture]; - }); - } else if (_pipController && !_pictureInPicture && [_pipController isPictureInPictureActive]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_pipController stopPictureInPicture]; - }); - } - #endif -} - -#if TARGET_OS_IOS -- (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore -{ - if (_restoreUserInterfaceForPIPStopCompletionHandler != NULL) { - _restoreUserInterfaceForPIPStopCompletionHandler(restore); - _restoreUserInterfaceForPIPStopCompletionHandler = NULL; - } -} - -- (void)setupPipController { - if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { - // Create new controller passing reference to the AVPlayerLayer - _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; - _pipController.delegate = self; - } -} -#endif - -- (void)setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch -{ - _ignoreSilentSwitch = ignoreSilentSwitch; - [self applyModifiers]; -} - -- (void)setMixWithOthers:(NSString *)mixWithOthers -{ - _mixWithOthers = mixWithOthers; - [self applyModifiers]; -} - -- (void)setPaused:(BOOL)paused -{ - if (paused) { - [_player pause]; - [_player setRate:0.0]; - } else { - AVAudioSession *session = [AVAudioSession sharedInstance]; - AVAudioSessionCategory category = nil; - AVAudioSessionCategoryOptions options = nil; - - if([_ignoreSilentSwitch isEqualToString:@"ignore"]) { - category = AVAudioSessionCategoryPlayback; - } else if([_ignoreSilentSwitch isEqualToString:@"obey"]) { - category = AVAudioSessionCategoryAmbient; - } - - if([_mixWithOthers isEqualToString:@"mix"]) { - options = AVAudioSessionCategoryOptionMixWithOthers; - } else if([_mixWithOthers isEqualToString:@"duck"]) { - options = AVAudioSessionCategoryOptionDuckOthers; - } - - if (category != nil && options != nil) { - [session setCategory:category withOptions:options error:nil]; - } else if (category != nil && options == nil) { - [session setCategory:category error:nil]; - } else if (category == nil && options != nil) { - [session setCategory:session.category withOptions:options error:nil]; - } - - if (@available(iOS 10.0, *) && !_automaticallyWaitsToMinimizeStalling) { - [_player playImmediatelyAtRate:_rate]; - } else { - [_player play]; - [_player setRate:_rate]; - } - [_player setRate:_rate]; - } - - _paused = paused; -} - -- (float)getCurrentTime -{ - return _playerItem != NULL ? CMTimeGetSeconds(_playerItem.currentTime) : 0; -} - -- (void)setCurrentTime:(float)currentTime -{ - NSDictionary *info = @{ - @"time": [NSNumber numberWithFloat:currentTime], - @"tolerance": [NSNumber numberWithInt:100] - }; - [self setSeek:info]; -} - -- (void)setSeek:(NSDictionary *)info -{ - NSNumber *seekTime = info[@"time"]; - NSNumber *seekTolerance = info[@"tolerance"]; - - int timeScale = 1000; - - AVPlayerItem *item = _player.currentItem; - if (item && item.status == AVPlayerItemStatusReadyToPlay) { - // TODO check loadedTimeRanges - - CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale); - CMTime current = item.currentTime; - // TODO figure out a good tolerance level - CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale); - BOOL wasPaused = _paused; - - if (CMTimeCompare(current, cmSeekTime) != 0) { - if (!wasPaused) [_player pause]; - [_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) { - if (!_timeObserver) { - [self addPlayerTimeObserver]; - } - if (!wasPaused) { - [self setPaused:false]; - } - if(self.onVideoSeek) { - self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)], - @"seekTime": seekTime, - @"target": self.reactTag}); - } - }]; - - _pendingSeek = false; - } - - } else { - _pendingSeek = true; - _pendingSeekTime = [seekTime floatValue]; - } -} - -- (void)setRate:(float)rate -{ - _rate = rate; - [self applyModifiers]; -} - -- (void)setMuted:(BOOL)muted -{ - _muted = muted; - [self applyModifiers]; -} - -- (void)setVolume:(float)volume -{ - _volume = volume; - [self applyModifiers]; -} - -- (void)setMaxBitRate:(float) maxBitRate { - _maxBitRate = maxBitRate; - _playerItem.preferredPeakBitRate = maxBitRate; -} - -- (void)setPreferredForwardBufferDuration:(float) preferredForwardBufferDuration -{ - _preferredForwardBufferDuration = preferredForwardBufferDuration; - _playerItem.preferredForwardBufferDuration = preferredForwardBufferDuration; -} - -- (void)setAutomaticallyWaitsToMinimizeStalling:(BOOL)waits -{ - _automaticallyWaitsToMinimizeStalling = waits; - _player.automaticallyWaitsToMinimizeStalling = waits; -} - - -- (void)applyModifiers -{ - if (_muted) { - if (!_controls) { - [_player setVolume:0]; - } - [_player setMuted:YES]; - } else { - [_player setVolume:_volume]; - [_player setMuted:NO]; - } - - if (@available(iOS 12.0, *)) { - self->_player.preventsDisplaySleepDuringVideoPlayback = _preventsDisplaySleepDuringVideoPlayback; - } else { - // Fallback on earlier versions - } - - [self setMaxBitRate:_maxBitRate]; - [self setSelectedAudioTrack:_selectedAudioTrack]; - [self setSelectedTextTrack:_selectedTextTrack]; - [self setResizeMode:_resizeMode]; - [self setRepeat:_repeat]; - [self setPaused:_paused]; - [self setControls:_controls]; - [self setAllowsExternalPlayback:_allowsExternalPlayback]; -} - -- (void)setRepeat:(BOOL)repeat { - _repeat = repeat; -} - -- (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic - withCriteria:(NSDictionary *)criteria -{ - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; - } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; -} - -- (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; -} - -- (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { - _selectedTextTrack = selectedTextTrack; - if (_textTracks) { // sideloaded text tracks - [self setSideloadedText]; - } else { // text tracks included in the HLS playlist - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicLegible - withCriteria:_selectedTextTrack]; - } -} - -- (void) setSideloadedText { - NSString *type = _selectedTextTrack[@"type"]; - NSArray *textTracks = [self getTextTrackInfo]; - - // The first few tracks will be audio & video track - int firstTextIndex = 0; - for (firstTextIndex = 0; firstTextIndex < _player.currentItem.tracks.count; ++firstTextIndex) { - if ([_player.currentItem.tracks[firstTextIndex].assetTrack hasMediaCharacteristic:AVMediaCharacteristicLegible]) { - break; - } - } - - int selectedTrackIndex = RCTVideoUnset; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"]) { - NSString *selectedValue = _selectedTextTrack[@"value"]; - for (int i = 0; i < textTracks.count; ++i) { - NSDictionary *currentTextTrack = [textTracks objectAtIndex:i]; - if ([selectedValue isEqualToString:currentTextTrack[@"language"]]) { - selectedTrackIndex = i; - break; - } - } - } else if ([type isEqualToString:@"title"]) { - NSString *selectedValue = _selectedTextTrack[@"value"]; - for (int i = 0; i < textTracks.count; ++i) { - NSDictionary *currentTextTrack = [textTracks objectAtIndex:i]; - if ([selectedValue isEqualToString:currentTextTrack[@"title"]]) { - selectedTrackIndex = i; - break; - } - } - } else if ([type isEqualToString:@"index"]) { - if ([_selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [_selectedTextTrack[@"value"] intValue]; - if (textTracks.count > index) { - selectedTrackIndex = index; - } - } - } - - // in the situation that a selected text track is not available (eg. specifies a textTrack not available) - if (![type isEqualToString:@"disabled"] && selectedTrackIndex == RCTVideoUnset) { - CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser); - NSArray *captionSettings = (__bridge NSArray*)captioningMediaCharacteristics; - if ([captionSettings containsObject:AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]) { - selectedTrackIndex = 0; // If we can't find a match, use the first available track - NSString *systemLanguage = [[NSLocale preferredLanguages] firstObject]; - for (int i = 0; i < textTracks.count; ++i) { - NSDictionary *currentTextTrack = [textTracks objectAtIndex:i]; - if ([systemLanguage isEqualToString:currentTextTrack[@"language"]]) { - selectedTrackIndex = i; - break; - } - } - } - } - - for (int i = firstTextIndex; i < _player.currentItem.tracks.count; ++i) { - BOOL isEnabled = NO; - if (selectedTrackIndex != RCTVideoUnset) { - isEnabled = i == selectedTrackIndex + firstTextIndex; - } - [_player.currentItem.tracks[i] setEnabled:isEnabled]; - } -} - --(void) setStreamingText { - NSString *type = _selectedTextTrack[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = _selectedTextTrack[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([_selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [_selectedTextTrack[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; - } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; -} - -- (void)setTextTracks:(NSArray*) textTracks; -{ - _textTracks = textTracks; - - // in case textTracks was set after selectedTextTrack - if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; -} - -- (NSArray *)getAudioTrackInfo -{ - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; - } - return audioTracks; -} - -- (NSArray *)getTextTrackInfo -{ - // if sideloaded, textTracks will already be set - if (_textTracks) return _textTracks; - - // if streaming video, we extract the text tracks - NSMutableArray *textTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *textTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [textTracks addObject:textTrack]; - } - return textTracks; -} - -- (BOOL)getFullscreen -{ - return _fullscreenPlayerPresented; -} - -- (void)setFullscreen:(BOOL) fullscreen { - if( fullscreen && !_fullscreenPlayerPresented && _player ) - { - // Ensure player view controller is not null - if( !_playerViewController ) - { - [self usePlayerViewController]; - } - // Set presentation style to fullscreen - [_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen]; - - // Find the nearest view controller - UIViewController *viewController = [self firstAvailableUIViewController]; - if( !viewController ) - { - UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; - viewController = keyWindow.rootViewController; - if( viewController.childViewControllers.count > 0 ) - { - viewController = viewController.childViewControllers.lastObject; - } - } - if( viewController ) - { - _presentingViewController = viewController; - if(self.onVideoFullscreenPlayerWillPresent) { - self.onVideoFullscreenPlayerWillPresent(@{@"target": self.reactTag}); - } - [viewController presentViewController:_playerViewController animated:true completion:^{ - _playerViewController.showsPlaybackControls = YES; - _fullscreenPlayerPresented = fullscreen; - _playerViewController.autorotate = _fullscreenAutorotate; - if(self.onVideoFullscreenPlayerDidPresent) { - self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag}); - } - }]; - } - } - else if ( !fullscreen && _fullscreenPlayerPresented ) - { - [self videoPlayerViewControllerWillDismiss:_playerViewController]; - [_presentingViewController dismissViewControllerAnimated:true completion:^{ - [self videoPlayerViewControllerDidDismiss:_playerViewController]; - }]; - } -} - -- (void)setFullscreenAutorotate:(BOOL)autorotate { - _fullscreenAutorotate = autorotate; - if (_fullscreenPlayerPresented) { - _playerViewController.autorotate = autorotate; - } -} - -- (void)setFullscreenOrientation:(NSString *)orientation { - _fullscreenOrientation = orientation; - if (_fullscreenPlayerPresented) { - _playerViewController.preferredOrientation = orientation; - } -} - -- (void)usePlayerViewController -{ - if( _player ) - { - if (!_playerViewController) { - _playerViewController = [self createPlayerViewController:_player withPlayerItem:_playerItem]; - } - // to prevent video from being animated when resizeMode is 'cover' - // resize mode must be set before subview is added - [self setResizeMode:_resizeMode]; - - if (_controls) { - UIViewController *viewController = [self reactViewController]; - [viewController addChildViewController:_playerViewController]; - [self addSubview:_playerViewController.view]; - } - - [_playerViewController addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; - - [_playerViewController.contentOverlayView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; - } -} - -- (void)usePlayerLayer -{ - if( _player ) - { - _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; - _playerLayer.frame = self.bounds; - _playerLayer.needsDisplayOnBoundsChange = YES; - - // to prevent video from being animated when resizeMode is 'cover' - // resize mode must be set before layer is added - [self setResizeMode:_resizeMode]; - [_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; - _playerLayerObserverSet = YES; - - [self.layer addSublayer:_playerLayer]; - self.layer.needsDisplayOnBoundsChange = YES; - #if TARGET_OS_IOS - [self setupPipController]; - #endif - } -} - -- (void)setControls:(BOOL)controls -{ - if( _controls != controls || (!_playerLayer && !_playerViewController) ) - { - _controls = controls; - if( _controls ) - { - [self removePlayerLayer]; - [self usePlayerViewController]; - } - else - { - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; - [self usePlayerLayer]; - } - } -} - -- (void)setProgressUpdateInterval:(float)progressUpdateInterval -{ - _progressUpdateInterval = progressUpdateInterval; - - if (_timeObserver) { - [self removePlayerTimeObserver]; - [self addPlayerTimeObserver]; - } -} - -- (void)removePlayerLayer -{ - if (_loadingRequest != nil) { - [_loadingRequest finishLoading]; - } - _requestingCertificate = NO; - _requestingCertificateErrored = NO; - [_playerLayer removeFromSuperlayer]; - if (_playerLayerObserverSet) { - [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; - _playerLayerObserverSet = NO; - } - _playerLayer = nil; -} - -#pragma mark - RCTVideoPlayerViewControllerDelegate - -- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController -{ - if (_playerViewController == playerViewController && _fullscreenPlayerPresented && self.onVideoFullscreenPlayerWillDismiss) - { - @try{ - [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; - [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; - }@catch(id anException){ - } - self.onVideoFullscreenPlayerWillDismiss(@{@"target": self.reactTag}); - } -} - -- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController -{ - if (_playerViewController == playerViewController && _fullscreenPlayerPresented) - { - _fullscreenPlayerPresented = false; - _presentingViewController = nil; - _playerViewController = nil; - [self applyModifiers]; - if(self.onVideoFullscreenPlayerDidDismiss) { - self.onVideoFullscreenPlayerDidDismiss(@{@"target": self.reactTag}); - } - } -} - -- (void)setFilter:(NSString *)filterName { - _filterName = filterName; - - if (!_filterEnabled) { - return; - } else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) { - return; // filters don't work for HLS... return - } else if (!_playerItem.asset) { - return; - } - - CIFilter *filter = [CIFilter filterWithName:filterName]; - _playerItem.videoComposition = [AVVideoComposition - videoCompositionWithAsset:_playerItem.asset - applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) { - if (filter == nil) { - [request finishWithImage:request.sourceImage context:nil]; - } else { - CIImage *image = request.sourceImage.imageByClampingToExtent; - [filter setValue:image forKey:kCIInputImageKey]; - CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent]; - [request finishWithImage:output context:nil]; - } - }]; -} - -- (void)setFilterEnabled:(BOOL)filterEnabled { - _filterEnabled = filterEnabled; -} - -#pragma mark - React View Management - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - // We are early in the game and somebody wants to set a subview. - // That can only be in the context of playerViewController. - if( !_controls && !_playerLayer && !_playerViewController ) - { - [self setControls:true]; - } - - if( _controls ) - { - view.frame = self.bounds; - [_playerViewController.contentOverlayView insertSubview:view atIndex:atIndex]; - } - else - { - RCTLogError(@"video cannot have any subviews"); - } - return; -} - -- (void)removeReactSubview:(UIView *)subview -{ - if( _controls ) - { - [subview removeFromSuperview]; - } - else - { - RCTLogError(@"video cannot have any subviews"); - } - return; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - if( _controls ) - { - _playerViewController.view.frame = self.bounds; - - // also adjust all subviews of contentOverlayView - for (UIView* subview in _playerViewController.contentOverlayView.subviews) { - subview.frame = self.bounds; - } - } - else - { - [CATransaction begin]; - [CATransaction setAnimationDuration:0]; - _playerLayer.frame = self.bounds; - [CATransaction commit]; - } -} - -#pragma mark - Lifecycle - -- (void)removeFromSuperview -{ - [_player pause]; - if (_playbackRateObserverRegistered) { - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - _playbackRateObserverRegistered = NO; - } - if (_isExternalPlaybackActiveObserverRegistered) { - [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - _isExternalPlaybackActiveObserverRegistered = NO; - } - _player = nil; - - [self removePlayerLayer]; - - [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; - [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; - [_playerViewController.view removeFromSuperview]; - _playerViewController.rctDelegate = nil; - _playerViewController.player = nil; - _playerViewController = nil; - - [self removePlayerTimeObserver]; - [self removePlayerItemObservers]; - - _eventDispatcher = nil; - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - [super removeFromSuperview]; -} - -#pragma mark - Export - -- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - - AVAsset *asset = _playerItem.asset; - - if (asset != nil) { - - AVAssetExportSession *exportSession = [AVAssetExportSession - exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality]; - - if (exportSession != nil) { - NSString *path = nil; - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"] - withExtension:@".mp4"]; - NSURL *url = [NSURL fileURLWithPath:path]; - exportSession.outputFileType = AVFileTypeMPEG4; - exportSession.outputURL = url; - exportSession.videoComposition = _playerItem.videoComposition; - exportSession.shouldOptimizeForNetworkUse = true; - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - - switch ([exportSession status]) { - case AVAssetExportSessionStatusFailed: - reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error); - break; - case AVAssetExportSessionStatusCancelled: - reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error); - break; - default: - resolve(@{@"uri": url.absoluteString}); - break; - } - - }]; - - } else { - - reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil); - - } - - } else { - - reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil); - - } -} - -- (void)setLicenseResult:(NSString *)license { - NSData *respondData = [self base64DataFromBase64String:license]; - if (_loadingRequest != nil && respondData != nil) { - AVAssetResourceLoadingDataRequest *dataRequest = [_loadingRequest dataRequest]; - [dataRequest respondWithData:respondData]; - [_loadingRequest finishLoading]; - } else { - [self setLicenseResultError:@"No data from JS license response"]; - } -} - -- (BOOL)setLicenseResultError:(NSString *)error { - if (_loadingRequest != nil) { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorFromJSPart - userInfo: @{ - NSLocalizedDescriptionKey: error, - NSLocalizedFailureReasonErrorKey: error, - NSLocalizedRecoverySuggestionErrorKey: error - } - ]; - [self finishLoadingWithError:licenseError]; - } - return NO; -} - -- (BOOL)finishLoadingWithError:(NSError *)error { - if (_loadingRequest && error != nil) { - NSError *licenseError = error; - [_loadingRequest finishLoadingWithError:licenseError]; - if (self.onVideoError) { - self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], - @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], - @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], - @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], - @"domain": _playerItem.error == nil ? @"RCTVideo" : _playerItem.error.domain}, - @"target": self.reactTag}); - } - } - return NO; -} - -- (BOOL)ensureDirExistsWithPath:(NSString *)path { - BOOL isDir = NO; - NSError *error; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; - if (!(exists && isDir)) { - [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) { - return NO; - } - } - return YES; -} - -- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension { - NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; - [self ensureDirExistsWithPath:directory]; - return [directory stringByAppendingPathComponent:fileName]; -} - -- (NSString *)cacheDirectoryPath { - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - return array[0]; -} - -#pragma mark - AVAssetResourceLoaderDelegate - -- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { - return [self loadingRequestHandling:renewalRequest]; -} - -- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { - return [self loadingRequestHandling:loadingRequest]; -} - -- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader -didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { - NSLog(@"didCancelLoadingRequest"); -} - -- (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { - if (self->_requestingCertificate) { - return YES; - } else if (self->_requestingCertificateErrored) { - return NO; - } - _loadingRequest = loadingRequest; - NSURL *url = loadingRequest.request.URL; - if (self->_drm != nil) { - NSString *contentId; - NSString *contentIdOverride = (NSString *)[self->_drm objectForKey:@"contentId"]; - if (contentIdOverride != nil) { - contentId = contentIdOverride; - } else if (self.onGetLicense) { - contentId = url.host; - } else { - contentId = [url.absoluteString stringByReplacingOccurrencesOfString:@"skd://" withString:@""]; - } - NSString *drmType = (NSString *)[self->_drm objectForKey:@"type"]; - if ([drmType isEqualToString:@"fairplay"]) { - NSString *certificateStringUrl = (NSString *)[self->_drm objectForKey:@"certificateUrl"]; - if (certificateStringUrl != nil) { - NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; - if ([self->_drm objectForKey:@"base64Certificate"]) { - certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; - } - - if (certificateData != nil) { - NSData *contentIdData; - if(self.onGetLicense) { - contentIdData = [contentId dataUsingEncoding:NSUTF8StringEncoding]; - } else { - contentIdData = [NSData dataWithBytes: [contentId cStringUsingEncoding:NSUTF8StringEncoding] length:[contentId lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; - } - AVAssetResourceLoadingDataRequest *dataRequest = [loadingRequest dataRequest]; - if (dataRequest != nil) { - NSError *spcError = nil; - NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError]; - // Request CKC to the server - NSString *licenseServer = (NSString *)[self->_drm objectForKey:@"licenseServer"]; - if (spcError != nil) { - [self finishLoadingWithError:spcError]; - self->_requestingCertificateErrored = YES; - } - if (spcData != nil) { - if(self.onGetLicense) { - NSString *base64Encoded = [spcData base64EncodedStringWithOptions:0]; - self->_requestingCertificate = YES; - if (licenseServer == nil) { - licenseServer = @""; - } - self.onGetLicense(@{@"licenseUrl": licenseServer, - @"contentId": contentId, - @"spcBase64": base64Encoded, - @"target": self.reactTag} - ); - } else if(licenseServer != nil) { - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"POST"]; - [request setURL:[NSURL URLWithString:licenseServer]]; - // HEADERS - NSDictionary *headers = (NSDictionary *)[self->_drm objectForKey:@"headers"]; - if (headers != nil) { - for (NSString *key in headers) { - NSString *value = headers[key]; - [request setValue:value forHTTPHeaderField:key]; - } - } - - if(self.onGetLicense) { - [request setHTTPBody: spcData]; - } else { - NSString *spcEncoded = [spcData base64EncodedStringWithOptions:0]; - NSString *spcUrlEncoded = (NSString *) CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)spcEncoded, NULL, CFSTR("?=&+"), kCFStringEncodingUTF8)); - NSString *post = [NSString stringWithFormat:@"spc=%@&%@", spcUrlEncoded, contentId]; - NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; - [request setHTTPBody: postData]; - } - - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; - NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; - if (error != nil) { - NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - [self finishLoadingWithError:error]; - self->_requestingCertificateErrored = YES; - } else { - if([httpResponse statusCode] != 200){ - NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorLicenseRequestNotOk - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining license.", - NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"License server responded with status code %li", (long)[httpResponse statusCode]], - NSLocalizedRecoverySuggestionErrorKey: @"Did you send the correct data to the license Server? Is the server ok?" - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } else if (data != nil) { - if(self.onGetLicense) { - [dataRequest respondWithData:data]; - } else { - NSData *decodedData = [[NSData alloc] initWithBase64EncodedData:data options:0]; - [dataRequest respondWithData:decodedData]; - } - [loadingRequest finishLoading]; - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataFromLicenseRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", - NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - - } - }]; - [postDataTask resume]; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoSPC - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining license.", - NSLocalizedFailureReasonErrorKey: @"No spc received.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateData - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - }); - return YES; - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateURL - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM License.", - NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", - NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoFairplayDRM - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDRMData - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No drm object found.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } - - - return NO; -} - -- (NSData *)base64DataFromBase64String: (NSString *)base64String { - if (base64String != nil) { - // NSData from the Base64 encoded str - NSData *base64Data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSASCIIStringEncoding]; - return base64Data; - } - return nil; -} -#pragma mark - Picture in Picture - -#if TARGET_OS_IOS -- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onPictureInPictureStatusChanged) { - self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:false] - }); - } -} - -- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onPictureInPictureStatusChanged) { - self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:true] - }); - } -} - -- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - -} - -- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - -} - -- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error { - -} - -- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler { - NSAssert(_restoreUserInterfaceForPIPStopCompletionHandler == NULL, @"restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited."); - if (self.onRestoreUserInterfaceForPictureInPictureStop) { - self.onRestoreUserInterfaceForPictureInPictureStop(@{}); - } - _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler; -} -#endif - -@end diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift new file mode 100644 index 00000000..fb5b66ba --- /dev/null +++ b/ios/Video/RCTVideo.swift @@ -0,0 +1,1147 @@ +import AVFoundation +import AVKit +import Foundation +import React + +class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { + + private var _player:AVPlayer? + private var _playerItem:AVPlayerItem? + private var _source:VideoSource? + private var _playerBufferEmpty:Bool = true + private var _playerLayer:AVPlayerLayer? + + private var _playerViewController:RCTVideoPlayerViewController? + private var _videoURL:NSURL? + + /* DRM */ + private var _drm:DRMParams? + + /* Required to publish events */ + private var _eventDispatcher:RCTEventDispatcher? + private var _videoLoadStarted:Bool = false + + private var _pendingSeek:Bool = false + private var _pendingSeekTime:Float = 0.0 + private var _lastSeekTime:Float = 0.0 + + /* For sending videoProgress events */ + private var _controls:Bool = false + + /* Keep track of any modifiers, need to be applied after each play */ + private var _volume:Float = 1.0 + private var _rate:Float = 1.0 + private var _maxBitRate:Float? + + private var _automaticallyWaitsToMinimizeStalling:Bool = true + private var _muted:Bool = false + private var _paused:Bool = false + private var _repeat:Bool = false + private var _allowsExternalPlayback:Bool = true + private var _textTracks:[TextTrack]? + private var _selectedTextTrackCriteria:SelectedTrackCriteria? + private var _selectedAudioTrackCriteria:SelectedTrackCriteria? + private var _playbackStalled:Bool = false + private var _playInBackground:Bool = false + private var _preventsDisplaySleepDuringVideoPlayback:Bool = true + private var _preferredForwardBufferDuration:Float = 0.0 + private var _playWhenInactive:Bool = false + private var _ignoreSilentSwitch:String! = "inherit" // inherit, ignore, obey + private var _mixWithOthers:String! = "inherit" // inherit, mix, duck + private var _resizeMode:String! = "AVLayerVideoGravityResizeAspectFill" + private var _fullscreen:Bool = false + private var _fullscreenAutorotate:Bool = true + private var _fullscreenOrientation:String! = "all" + private var _fullscreenPlayerPresented:Bool = false + private var _filterName:String! + private var _filterEnabled:Bool = false + private var _presentingViewController:UIViewController? + + private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? + private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver() + +#if canImport(RCTVideoCache) + private var _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler(self.playerItemPrepareText) +#endif + +#if TARGET_OS_IOS + private let _pip:RCTPictureInPicture = RCTPictureInPicture(self.onPictureInPictureStatusChanged, self.onRestoreUserInterfaceForPictureInPictureStop) +#endif + + // Events + @objc var onVideoLoadStart: RCTDirectEventBlock? + @objc var onVideoLoad: RCTDirectEventBlock? + @objc var onVideoBuffer: RCTDirectEventBlock? + @objc var onVideoError: RCTDirectEventBlock? + @objc var onVideoProgress: RCTDirectEventBlock? + @objc var onBandwidthUpdate: RCTDirectEventBlock? + @objc var onVideoSeek: RCTDirectEventBlock? + @objc var onVideoEnd: RCTDirectEventBlock? + @objc var onTimedMetadata: RCTDirectEventBlock? + @objc var onVideoAudioBecomingNoisy: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerWillPresent: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerDidPresent: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerWillDismiss: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerDidDismiss: RCTDirectEventBlock? + @objc var onReadyForDisplay: RCTDirectEventBlock? + @objc var onPlaybackStalled: RCTDirectEventBlock? + @objc var onPlaybackResume: RCTDirectEventBlock? + @objc var onPlaybackRateChange: RCTDirectEventBlock? + @objc var onVideoExternalPlaybackChange: RCTDirectEventBlock? + @objc var onPictureInPictureStatusChanged: RCTDirectEventBlock? + @objc var onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock? + @objc var onGetLicense: RCTDirectEventBlock? + + init(eventDispatcher:RCTEventDispatcher!) { + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + _eventDispatcher = eventDispatcher + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillResignActive(notification:)), + name: UIApplication.willResignActiveNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground(notification:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillEnterForeground(notification:)), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(audioRouteChanged(notification:)), + name: AVAudioSession.routeChangeNotification, + object: nil + ) + _playerObserver._handlers = self + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + deinit { + NotificationCenter.default.removeObserver(self) + self.removePlayerLayer() + _playerObserver.clearPlayer() + } + + // MARK: - App lifecycle handlers + + @objc func applicationWillResignActive(notification:NSNotification!) { + if _playInBackground || _playWhenInactive || _paused {return} + + _player?.pause() + _player?.rate = 0.0 + } + + @objc func applicationDidEnterBackground(notification:NSNotification!) { + if _playInBackground { + // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html + _playerLayer?.player = nil + _playerViewController?.player = nil + } + } + + @objc func applicationWillEnterForeground(notification:NSNotification!) { + self.applyModifiers() + if _playInBackground { + _playerLayer?.player = _player + _playerViewController?.player = _player + } + } + + // MARK: - Audio events + + @objc func audioRouteChanged(notification:NSNotification!) { + if let userInfo = notification.userInfo { + let reason:AVAudioSession.RouteChangeReason! = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason + // let previousRoute:NSNumber! = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? NSNumber + if reason == .oldDeviceUnavailable, let onVideoAudioBecomingNoisy = onVideoAudioBecomingNoisy { + onVideoAudioBecomingNoisy(["target": reactTag as Any]) + } + } + } + + // MARK: - Progress + + func sendProgressUpdate() { + if let video = _player?.currentItem, + video == nil || video.status != AVPlayerItem.Status.readyToPlay { + return + } + + let playerDuration:CMTime = RCTVideoUtils.playerItemDuration(_player) + if CMTIME_IS_INVALID(playerDuration) { + return + } + + let currentTime = _player?.currentTime() + let currentPlaybackTime = _player?.currentItem?.currentDate() + let duration = CMTimeGetSeconds(playerDuration) + let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) + + NotificationCenter.default.post(name: NSNotification.Name("RCTVideo_progress"), object: nil, userInfo: [ + "progress": NSNumber(value: currentTimeSecs / duration) + ]) + + if currentTimeSecs >= 0 { + onVideoProgress?([ + "currentTime": NSNumber(value: Float(currentTimeSecs)), + "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player), + "atValue": NSNumber(value: currentTime?.value ?? .zero), + "currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), + "target": reactTag, + "seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player) + ]) + } + } + + // MARK: - Player and source + + @objc + func setSrc(_ source:NSDictionary!) { + _source = VideoSource(source) + removePlayerLayer() + _playerObserver.player = nil + _playerObserver.playerItem = nil + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0)) / Double(NSEC_PER_SEC), execute: { [weak self] in + guard let self = self else {return} + // perform on next run loop, otherwise other passed react-props may not be set + self.playerItemForSource(withCallback:{ (playerItem:AVPlayerItem!) in + self._player?.pause() + self._playerItem = playerItem + self._playerObserver.playerItem = self._playerItem + self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration) + self.setFilter(self._filterName) + if let maxBitRate = self._maxBitRate { + self._playerItem?.preferredPeakBitRate = Double(maxBitRate) + } + + self._player = AVPlayer(playerItem: self._playerItem) + self._playerObserver.player = self._player + + self._player?.actionAtItemEnd = .none + + if #available(iOS 10.0, *) { + self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) + } + + //Perform on next run loop, otherwise onVideoLoadStart is nil + self.onVideoLoadStart?([ + "src": [ + "uri": self._source?.uri ?? NSNull(), + "type": self._source?.type ?? NSNull(), + "isNetwork": NSNumber(value: self._source?.isNetwork ?? false) + ], + "drm": self._drm?.json ?? NSNull(), + "target": self.reactTag + ]) + + }) + }) + _videoLoadStarted = true + } + + @objc + func setDrm(_ drm:NSDictionary!) { + _drm = DRMParams(drm) + } + + func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?, withCallback handler:(AVPlayerItem?)->Void) { + if (_textTracks == nil) || _textTracks?.count==0 { + handler(AVPlayerItem(asset: asset)) + return + } + + // AVPlayer can't airplay AVMutableCompositions + _allowsExternalPlayback = false + + // sideload text tracks + let mixComposition:AVMutableComposition! = AVMutableComposition() + + let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first + let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid) + do { + try videoCompTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), + of: videoAsset, + at: .zero) + } catch { + } + + let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first + let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid) + do { + try audioCompTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), + of: audioAsset, + at: .zero) + } catch { + } + + var validTextTracks:[TextTrack] = [] + if let textTracks = _textTracks, let textTrackCount = _textTracks?.count { + for i in 0..Void) { + var asset:AVURLAsset! + guard let source = _source, source.uri != nil && source.uri != "" else { + DebugLog("Could not find video URL in source '\(_source)'") + return + } + + let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" + let url = source.isNetwork || source.isAsset + ? URL(string: source.uri ?? "") + : URL(fileURLWithPath: bundlePath) + + let assetOptions:NSMutableDictionary! = NSMutableDictionary() + + if url != nil && source.isNetwork { + if let headers = source.requestHeaders, headers.count > 0 { + assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying) + } + let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies + assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying) +#if canImport(RCTVideoCache) + if _videoCache.playerItemForSourceUsingCache(shouldCache:shouldCache, textTracks:_textTracks, uri:uri, assetOptions:assetOptions, handler:handler) { + return + } +#endif + + asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any]) + } else { + asset = AVURLAsset(url: url!) + } + + if _drm != nil { + _resouceLoaderDelegate = RCTResourceLoaderDelegate( + asset: asset, + drm: _drm, + onVideoError: onVideoError, + onGetLicense: onGetLicense, + reactTag: reactTag + ) + } + + self.playerItemPrepareText(asset: asset, assetOptions:assetOptions, withCallback:handler) + } + + // MARK: - Prop setters + + @objc + func setResizeMode(_ mode: String?) { + if _controls { + _playerViewController?.videoGravity = AVLayerVideoGravity(rawValue: mode ?? "") + } else { + _playerLayer?.videoGravity = AVLayerVideoGravity(rawValue: mode ?? "") + } + _resizeMode = mode + } + + @objc + func setPlayInBackground(_ playInBackground:Bool) { + _playInBackground = playInBackground + } + + @objc + func setPreventsDisplaySleepDuringVideoPlayback(_ preventsDisplaySleepDuringVideoPlayback:Bool) { + _preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback + self.applyModifiers() + } + + @objc + func setAllowsExternalPlayback(_ allowsExternalPlayback:Bool) { + _allowsExternalPlayback = allowsExternalPlayback + _player?.allowsExternalPlayback = _allowsExternalPlayback + } + + @objc + func setPlayWhenInactive(_ playWhenInactive:Bool) { + _playWhenInactive = playWhenInactive + } + + @objc + func setPictureInPicture(_ pictureInPicture:Bool) { +#if TARGET_OS_IOS + _pip.setPictureInPicture(pictureInPicture) +#endif + } + + @objc + func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore:Bool) { +#if TARGET_OS_IOS + _pip.setRestoreUserInterfaceForPIPStopCompletionHandler(restore) +#endif + } + + @objc + func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String!) { + _ignoreSilentSwitch = ignoreSilentSwitch + self.applyModifiers() + } + + @objc + func setMixWithOthers(_ mixWithOthers:String!) { + _mixWithOthers = mixWithOthers + self.applyModifiers() + } + + @objc + func setPaused(_ paused:Bool) { + if paused { + _player?.pause() + _player?.rate = 0.0 + } else { + let session:AVAudioSession! = AVAudioSession.sharedInstance() + var category:AVAudioSession.Category? = nil + var options:AVAudioSession.CategoryOptions? = nil + + if (_ignoreSilentSwitch == "ignore") { + category = AVAudioSession.Category.playback + } else if (_ignoreSilentSwitch == "obey") { + category = AVAudioSession.Category.ambient + } + + if (_mixWithOthers == "mix") { + options = .mixWithOthers + } else if (_mixWithOthers == "duck") { + options = .duckOthers + } + + if let category = category, let options = options { + do { + try session.setCategory(category, options: options) + } catch { + } + } else if let category = category, options == nil { + do { + try session.setCategory(category) + } catch { + } + } else if category == nil, let options = options { + do { + try session.setCategory(session.category, options: options) + } catch { + } + } + + if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling { + _player?.playImmediately(atRate: _rate) + } else { + _player?.play() + _player?.rate = _rate + } + _player?.rate = _rate + } + + _paused = paused + } + + @objc + func setCurrentTime(_ currentTime:Float) { + let info:NSDictionary! = [ + "time": NSNumber(value: currentTime), + "tolerance": NSNumber(value: 100) + ] + setSeek(info) + } + + @objc + func setSeek(_ info:NSDictionary!) { + let seekTime:NSNumber! = info["time"] as! NSNumber + let seekTolerance:NSNumber! = info["tolerance"] as! NSNumber + + let timeScale:Int = 1000 + + let item:AVPlayerItem! = _player?.currentItem + guard item != nil && item.status == AVPlayerItem.Status.readyToPlay else { + _pendingSeek = true + _pendingSeekTime = seekTime.floatValue + return + } + + // TODO check loadedTimeRanges + let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime.floatValue), preferredTimescale: Int32(timeScale)) + let current:CMTime = item.currentTime() + // TODO figure out a good tolerance level + let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance.floatValue), timescale: Int32(timeScale)) + let wasPaused:Bool = _paused + + guard CMTimeCompare(current, cmSeekTime) != 0 else { return } + if !wasPaused { _player?.pause() } + + _player?.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ [weak self] (finished:Bool) in + guard let self = self else { return } + + self._playerObserver.addTimeObserverIfNotSet() + if !wasPaused { + self.setPaused(false) + } + self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), + "seekTime": seekTime, + "target": self.reactTag]) + }) + + _pendingSeek = false + } + + @objc + func setRate(_ rate:Float) { + _rate = rate + applyModifiers() + } + + @objc + func setMuted(_ muted:Bool) { + _muted = muted + applyModifiers() + } + + @objc + func setVolume(_ volume:Float) { + _volume = volume + applyModifiers() + } + + @objc + func setMaxBitRate(_ maxBitRate:Float) { + _maxBitRate = maxBitRate + _playerItem?.preferredPeakBitRate = Double(maxBitRate) + } + + @objc + func setPreferredForwardBufferDuration(_ preferredForwardBufferDuration:Float) { + _preferredForwardBufferDuration = preferredForwardBufferDuration + if #available(iOS 10.0, *) { + _playerItem?.preferredForwardBufferDuration = TimeInterval(preferredForwardBufferDuration) + } else { + // Fallback on earlier versions + } + } + + @objc + func setAutomaticallyWaitsToMinimizeStalling(_ waits:Bool) { + _automaticallyWaitsToMinimizeStalling = waits + if #available(iOS 10.0, *) { + _player?.automaticallyWaitsToMinimizeStalling = waits + } else { + // Fallback on earlier versions + } + } + + + func applyModifiers() { + if _muted { + if !_controls { + _player?.volume = 0 + } + _player?.isMuted = true + } else { + _player?.volume = _volume + _player?.isMuted = false + } + + if #available(iOS 12.0, *) { + _player?.preventsDisplaySleepDuringVideoPlayback = _preventsDisplaySleepDuringVideoPlayback + } else { + // Fallback on earlier versions + } + + if let _maxBitRate = _maxBitRate { + setMaxBitRate(_maxBitRate) + } + + setSelectedAudioTrack(_selectedAudioTrackCriteria) + setSelectedTextTrack(_selectedTextTrackCriteria) + setResizeMode(_resizeMode) + setRepeat(_repeat) + setPaused(_paused) + setControls(_controls) + setAllowsExternalPlayback(_allowsExternalPlayback) + } + + @objc + func setRepeat(_ `repeat`: Bool) { + _repeat = `repeat` + } + + + + @objc + func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary!) { + setSelectedAudioTrack(SelectedTrackCriteria(selectedAudioTrack)) + } + + func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria!) { + _selectedAudioTrackCriteria = selectedAudioTrack + RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.audible, + criteria:_selectedAudioTrackCriteria) + } + + @objc + func setSelectedTextTrack(_ selectedTextTrack:NSDictionary!) { + setSelectedTextTrack(SelectedTrackCriteria(selectedTextTrack)) + } + + func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria!) { + _selectedTextTrackCriteria = selectedTextTrack + if (_textTracks != nil) { // sideloaded text tracks + RCTPlayerOperations.setSideloadedText(player:_player, textTracks:_textTracks, criteria:_selectedTextTrackCriteria) + } else { // text tracks included in the HLS playlist + RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.legible, + criteria:_selectedTextTrackCriteria) + } + } + + @objc + func setTextTracks(_ textTracks:[NSDictionary]!) { + setTextTracks(textTracks.map { TextTrack($0) }) + } + + func setTextTracks(_ textTracks:[TextTrack]!) { + _textTracks = textTracks + + // in case textTracks was set after selectedTextTrack + if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)} + } + + @objc + func setFullscreen(_ fullscreen:Bool) { + if fullscreen && !_fullscreenPlayerPresented && _player != nil { + // Ensure player view controller is not null + if _playerViewController == nil { + self.usePlayerViewController() + } + + // Set presentation style to fullscreen + _playerViewController?.modalPresentationStyle = .fullScreen + + // Find the nearest view controller + var viewController:UIViewController! = self.firstAvailableUIViewController() + if (viewController == nil) { + let keyWindow:UIWindow! = UIApplication.shared.keyWindow + viewController = keyWindow.rootViewController + if viewController.children.count > 0 + { + viewController = viewController.children.last + } + } + if viewController != nil { + _presentingViewController = viewController + + self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any]) + + viewController.present(viewController, animated:true, completion:{ + self._playerViewController?.showsPlaybackControls = true + self._fullscreenPlayerPresented = fullscreen + self._playerViewController?.autorotate = self._fullscreenAutorotate + + self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag]) + + }) + } + } else if !fullscreen && _fullscreenPlayerPresented, let _playerViewController = _playerViewController { + self.videoPlayerViewControllerWillDismiss(playerViewController: _playerViewController) + _presentingViewController?.dismiss(animated: true, completion:{ + self.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController) + }) + } + } + + @objc + func setFullscreenAutorotate(_ autorotate:Bool) { + _fullscreenAutorotate = autorotate + if _fullscreenPlayerPresented { + _playerViewController?.autorotate = autorotate + } + } + + @objc + func setFullscreenOrientation(_ orientation:String!) { + _fullscreenOrientation = orientation + if _fullscreenPlayerPresented { + _playerViewController?.preferredOrientation = orientation + } + } + + func usePlayerViewController() { + guard _player != nil else { return } + + if _playerViewController == nil { + _playerViewController = createPlayerViewController(player: _player, withPlayerItem:_playerItem) + + } + // to prevent video from being animated when resizeMode is 'cover' + // resize mode must be set before subview is added + setResizeMode(_resizeMode) + + guard let _playerViewController = _playerViewController else { return } + + if _controls { + let viewController:UIViewController! = self.reactViewController() + viewController.addChild(_playerViewController) + self.addSubview(_playerViewController.view) + } + + _playerObserver.playerViewController = _playerViewController + } + + func createPlayerViewController(player:AVPlayer!, withPlayerItem playerItem:AVPlayerItem!) -> RCTVideoPlayerViewController! { + let viewController:RCTVideoPlayerViewController! = RCTVideoPlayerViewController() + viewController.showsPlaybackControls = true + viewController.rctDelegate = self + viewController.preferredOrientation = _fullscreenOrientation + + viewController.view.frame = self.bounds + viewController.player = player + return viewController + } + + func usePlayerLayer() { + if let _player = _player { + _playerLayer = AVPlayerLayer(player: _player) + _playerLayer?.frame = self.bounds + _playerLayer?.needsDisplayOnBoundsChange = true + + // to prevent video from being animated when resizeMode is 'cover' + // resize mode must be set before layer is added + setResizeMode(_resizeMode) + _playerObserver.playerLayer = _playerLayer + + if let _playerLayer = _playerLayer { + self.layer.addSublayer(_playerLayer) + } + self.layer.needsDisplayOnBoundsChange = true +#if TARGET_OS_IOS + _pip.setupPipController(_playerLayer) +#endif + } + } + + @objc + func setControls(_ controls:Bool) { + if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) + { + _controls = controls + if _controls + { + self.removePlayerLayer() + self.usePlayerViewController() + } + else + { + _playerViewController?.view.removeFromSuperview() + _playerViewController = nil + _playerObserver.playerViewController = nil + self.usePlayerLayer() + } + } + } + + @objc + func setProgressUpdateInterval(_ progressUpdateInterval:Float) { + _playerObserver.replaceTimeObserverIfSet(Float64(progressUpdateInterval)) + } + + func removePlayerLayer() { + _resouceLoaderDelegate = nil + _playerLayer?.removeFromSuperlayer() + _playerLayer = nil + _playerObserver.playerLayer = nil + } + + // MARK: - RCTVideoPlayerViewControllerDelegate + + func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) { + if _playerViewController == playerViewController && _fullscreenPlayerPresented, let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss { + _playerObserver.removePlayerViewControllerObservers() + onVideoFullscreenPlayerWillDismiss(["target": reactTag as Any]) + } + } + + + func videoPlayerViewControllerDidDismiss(playerViewController:AVPlayerViewController) { + if _playerViewController == playerViewController && _fullscreenPlayerPresented { + _fullscreenPlayerPresented = false + _presentingViewController = nil + _playerViewController = nil + _playerObserver.playerViewController = nil + self.applyModifiers() + + onVideoFullscreenPlayerDidDismiss?(["target": reactTag as Any]) + } + } + + @objc + func setFilter(_ filterName:String!) { + _filterName = filterName + + if !_filterEnabled { + return + } else if let uri = _source?.uri, uri.contains("m3u8") { + return // filters don't work for HLS... return + } else if _playerItem?.asset == nil { + return + } + + let filter:CIFilter! = CIFilter(name: filterName) + if #available(iOS 9.0, *), let _playerItem = _playerItem { + self._playerItem?.videoComposition = AVVideoComposition( + asset: _playerItem.asset, + applyingCIFiltersWithHandler: { (request:AVAsynchronousCIImageFilteringRequest) in + if filter == nil { + request.finish(with: request.sourceImage, context:nil) + } else { + let image:CIImage! = request.sourceImage.clampedToExtent() + filter.setValue(image, forKey:kCIInputImageKey) + let output:CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent) + request.finish(with: output, context:nil) + } + }) + } else { + // Fallback on earlier versions + } + } + + @objc + func setFilterEnabled(_ filterEnabled:Bool) { + _filterEnabled = filterEnabled + } + + // MARK: - React View Management + + func insertReactSubview(view:UIView!, atIndex:Int) { + // We are early in the game and somebody wants to set a subview. + // That can only be in the context of playerViewController. + if !_controls && (_playerLayer == nil) && (_playerViewController == nil) { + setControls(true) + } + + if _controls { + view.frame = self.bounds + _playerViewController?.contentOverlayView?.insertSubview(view, at:atIndex) + } else { + RCTLogError("video cannot have any subviews") + } + return + } + + func removeReactSubview(subview:UIView!) { + if _controls { + subview.removeFromSuperview() + } else { + RCTLog("video cannot have any subviews") + } + return + } + + override func layoutSubviews() { + super.layoutSubviews() + if _controls, let _playerViewController = _playerViewController { + _playerViewController.view.frame = bounds + + // also adjust all subviews of contentOverlayView + for subview in _playerViewController.contentOverlayView?.subviews ?? [] { + subview.frame = bounds + } + } else { + CATransaction.begin() + CATransaction.setAnimationDuration(0) + _playerLayer?.frame = bounds + CATransaction.commit() + } + } + + // MARK: - Lifecycle + + override func removeFromSuperview() { + _player?.pause() + _player = nil + _playerObserver.clearPlayer() + + self.removePlayerLayer() + + if let _playerViewController = _playerViewController { + _playerViewController.view.removeFromSuperview() + _playerViewController.rctDelegate = nil + _playerViewController.player = nil + self._playerViewController = nil + _playerObserver.playerViewController = nil + } + + _eventDispatcher = nil + NotificationCenter.default.removeObserver(self) + + super.removeFromSuperview() + } + + // MARK: - Export + + @objc + func save(options:NSDictionary!, resolve: @escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) { + RCTVideoSave.save( + options:options, + resolve:resolve, + reject:reject, + playerItem:_playerItem + ) + } + + func setLicenseResult(_ license:String!) { + _resouceLoaderDelegate?.setLicenseResult(license) + } + + func setLicenseResultError(_ error:String!) { + _resouceLoaderDelegate?.setLicenseResultError(error) + } + + // MARK: - RCTPlayerObserverHandler + + func handleTimeUpdate(time:CMTime) { + sendProgressUpdate() + } + + func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) { + onReadyForDisplay?([ + "target": reactTag + ]) + } + + // When timeMetadata is read the event onTimedMetadata is triggered + func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) { + guard let newValue = change.newValue, let _items = newValue, _items.count > 0 else { + return + } + + var metadata: [[String:String?]?] = [] + for item in _items { + let value = item.value as? String + let identifier = item.identifier?.rawValue + + if let value = value { + metadata.append(["value":value, "identifier":identifier]) + } + } + + onTimedMetadata?([ + "target": reactTag, + "metadata": metadata + ]) + } + + // Handle player item status change. + func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + guard let _playerItem = _playerItem else { + return + } + + if _playerItem.status == .readyToPlay { + handleReadyToPlay() + } else if _playerItem.status == .failed { + handlePlaybackFailed() + } + } + + func handleReadyToPlay() { + guard let _playerItem = _playerItem else { return } + var duration:Float = Float(CMTimeGetSeconds(_playerItem.asset.duration)) + + if duration.isNaN { + duration = 0.0 + } + + var width: Float? = nil + var height: Float? = nil + var orientation = "undefined" + + if _playerItem.asset.tracks(withMediaType: AVMediaType.video).count > 0 { + let videoTrack = _playerItem.asset.tracks(withMediaType: .video)[0] + width = Float(videoTrack.naturalSize.width) + height = Float(videoTrack.naturalSize.height) + let preferredTransform = videoTrack.preferredTransform + + if (videoTrack.naturalSize.width == preferredTransform.tx + && videoTrack.naturalSize.height == preferredTransform.ty) + || (preferredTransform.tx == 0 && preferredTransform.ty == 0) + { + orientation = "landscape" + } else { + orientation = "portrait" + } + } else if _playerItem.presentationSize.height != 0.0 { + width = Float(_playerItem.presentationSize.width) + height = Float(_playerItem.presentationSize.height) + orientation = _playerItem.presentationSize.width > _playerItem.presentationSize.height ? "landscape" : "portrait" + } + + if _pendingSeek { + setCurrentTime(_pendingSeekTime) + _pendingSeek = false + } + + if _videoLoadStarted { + onVideoLoad?(["duration": NSNumber(value: duration), + "currentTime": NSNumber(value: Float(CMTimeGetSeconds(_playerItem.currentTime()))), + "canPlayReverse": NSNumber(value: _playerItem.canPlayReverse), + "canPlayFastForward": NSNumber(value: _playerItem.canPlayFastForward), + "canPlaySlowForward": NSNumber(value: _playerItem.canPlaySlowForward), + "canPlaySlowReverse": NSNumber(value: _playerItem.canPlaySlowReverse), + "canStepBackward": NSNumber(value: _playerItem.canStepBackward), + "canStepForward": NSNumber(value: _playerItem.canStepForward), + "naturalSize": [ + "width": width != nil ? NSNumber(value: width!) : "undefinded", + "height": width != nil ? NSNumber(value: height!) : "undefinded", + "orientation": orientation + ], + "audioTracks": RCTVideoUtils.getAudioTrackInfo(_player), + "textTracks": _textTracks ?? RCTVideoUtils.getTextTrackInfo(_player), + "target": reactTag as Any]) + } + _videoLoadStarted = false + _playerObserver.attachPlayerEventListeners() + applyModifiers() + } + + func handlePlaybackFailed() { + guard let _playerItem = _playerItem else { return } + onVideoError?( + [ + "error": [ + "code": NSNumber(value: (_playerItem.error! as NSError).code), + "localizedDescription": _playerItem.error?.localizedDescription == nil ? "" : _playerItem.error?.localizedDescription, + "localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ? "" : (_playerItem.error! as NSError).localizedFailureReason) ?? "", + "localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ? "" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "", + "domain": (_playerItem.error as! NSError).domain + ], + "target": reactTag + ]) + } + + func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + _playerBufferEmpty = true + onVideoBuffer?(["isBuffering": true, "target": reactTag as Any]) + } + + // Continue playing (or not if paused) after being paused due to hitting an unbuffered zone. + func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + if (!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && ((_playerItem?.isPlaybackLikelyToKeepUp) != nil) { + setPaused(_paused) + } + _playerBufferEmpty = false + onVideoBuffer?(["isBuffering": false, "target": reactTag as Any]) + } + + func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange) { + guard let _player = _player else { return } + onPlaybackRateChange?(["playbackRate": NSNumber(value: _player.rate), + "target": reactTag as Any]) + if _playbackStalled && _player.rate > 0 { + onPlaybackResume?(["playbackRate": NSNumber(value: _player.rate), + "target": reactTag as Any]) + _playbackStalled = false + } + } + + func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange) { + guard let _player = _player else { return } + onVideoExternalPlaybackChange?(["isExternalPlaybackActive": NSNumber(value: _player.isExternalPlaybackActive), + "target": reactTag as Any]) + } + + func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange) { + let oldRect = change.oldValue + let newRect = change.newValue + if !oldRect!.equalTo(newRect!) { + if newRect!.equalTo(UIScreen.main.bounds) { + NSLog("in fullscreen") + + self.reactViewController().view.frame = UIScreen.main.bounds + self.reactViewController().view.setNeedsLayout() + } else {NSLog("not fullscreen")} + } + } + + @objc func handleDidFailToFinishPlaying(notification:NSNotification!) { + let error:NSError! = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError + onVideoError?( + [ + "error": [ + "code": NSNumber(value: (error as NSError).code), + "localizedDescription": error.localizedDescription ?? "", + "localizedFailureReason": (error as NSError).localizedFailureReason ?? "", + "localizedRecoverySuggestion": (error as NSError).localizedRecoverySuggestion ?? "", + "domain": (error as NSError).domain + ], + "target": reactTag + ]) + } + + @objc func handlePlaybackStalled(notification:NSNotification!) { + onPlaybackStalled?(["target": reactTag as Any]) + _playbackStalled = true + } + + @objc func handlePlayerItemDidReachEnd(notification:NSNotification!) { + onVideoEnd?(["target": reactTag as Any]) + + if _repeat { + let item:AVPlayerItem! = notification.object as? AVPlayerItem + item.seek(to: CMTime.zero) + self.applyModifiers() + } else { + _playerObserver.removePlayerTimeObserver() + } + } + + //unused +// @objc func handleAVPlayerAccess(notification:NSNotification!) { +// let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog() +// let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last +// +// /* TODO: get this working +// if (self.onBandwidthUpdate) { +// self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); +// } +// */ +// } +} diff --git a/ios/Video/RCTVideoManager.h b/ios/Video/RCTVideoManager.h deleted file mode 100644 index b3bfccb5..00000000 --- a/ios/Video/RCTVideoManager.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface RCTVideoManager : RCTViewManager - -@end diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 000a9e83..ece0a1e3 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -1,22 +1,7 @@ -#import "RCTVideoManager.h" -#import "RCTVideo.h" #import -#import -#import +#import "React/RCTViewManager.h" -@implementation RCTVideoManager - -RCT_EXPORT_MODULE(); - -- (UIView *)view -{ - return [[RCTVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; -} - -- (dispatch_queue_t)methodQueue -{ - return self.bridge.uiManager.methodQueue; -} +@interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary); @@ -49,6 +34,7 @@ RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); + /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock); @@ -70,64 +56,18 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGetLicense, RCTDirectEventBlock); -RCT_REMAP_METHOD(save, - options:(NSDictionary *)options - reactTag:(nonnull NSNumber *)reactTag - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTVideo *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RCTVideo class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); - } else { - [view save:options resolve:resolve reject:reject]; - } - }]; -}; -RCT_REMAP_METHOD(setLicenseResult, - license:(NSString *)license - reactTag:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTVideo *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RCTVideo class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); - } else { - [view setLicenseResult:license]; - } - }]; -}; - -RCT_REMAP_METHOD(setLicenseResultError, - error:(NSString *)error - reactTag:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTVideo *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RCTVideo class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); - } else { - [view setLicenseResultError:error]; - } - }]; -}; RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock); -- (NSDictionary *)constantsToExport -{ - return @{ - @"ScaleNone": AVLayerVideoGravityResizeAspect, - @"ScaleToFill": AVLayerVideoGravityResize, - @"ScaleAspectFit": AVLayerVideoGravityResizeAspect, - @"ScaleAspectFill": AVLayerVideoGravityResizeAspectFill - }; -} +RCT_EXTERN_METHOD(save:(NSDictionary *)options + reactTag:(nonnull NSNumber *)reactTag + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) -+ (BOOL)requiresMainQueueSetup -{ - return YES; -} +RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license + reactTag:(nonnull NSNumber *)reactTag) + +RCT_EXTERN_METHOD(setLicenseResultError(NSString *)error + reactTag:(nonnull NSNumber *)reactTag) @end diff --git a/ios/Video/RCTVideoManager.swift b/ios/Video/RCTVideoManager.swift new file mode 100644 index 00000000..bf42f707 --- /dev/null +++ b/ios/Video/RCTVideoManager.swift @@ -0,0 +1,63 @@ +import AVFoundation +import React + +@objc(RCTVideoManager) +class RCTVideoManager: RCTViewManager { + + override func view() -> UIView { + return RCTVideo(eventDispatcher: bridge.eventDispatcher()) + } + + func methodQueue() -> DispatchQueue { + return bridge.uiManager.methodQueue + } + + @objc(save:reactTag:resolver:rejecter:) + func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void { + bridge.uiManager.prependUIBlock({_ , viewRegistry in + let view = viewRegistry?[reactTag] + if !(view is RCTVideo) { + RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) + } else if let view = view as? RCTVideo { + view.save(options: options, resolve: resolve, reject: reject) + } + }) + } + + @objc(setLicenseResult:reactTag:) + func setLicenseResult(license: NSString, reactTag: NSNumber) -> Void { + bridge.uiManager.prependUIBlock({_ , viewRegistry in + let view = viewRegistry?[reactTag] + if !(view is RCTVideo) { + RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) + } else if let view = view as? RCTVideo { + view.setLicenseResult(license as String) + } + }) + } + + @objc(setLicenseResultError:reactTag:) + func setLicenseResultError(error: NSString, reactTag: NSNumber) -> Void { + bridge.uiManager.prependUIBlock({_ , viewRegistry in + let view = viewRegistry?[reactTag] + if !(view is RCTVideo) { + RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) + } else if let view = view as? RCTVideo { + view.setLicenseResultError(error as String) + } + }) + } + + override func constantsToExport() -> [AnyHashable : Any]? { + return [ + "ScaleNone": AVLayerVideoGravity.resizeAspect, + "ScaleToFill": AVLayerVideoGravity.resize, + "ScaleAspectFit": AVLayerVideoGravity.resizeAspect, + "ScaleAspectFill": AVLayerVideoGravity.resizeAspectFill + ] + } + + override class func requiresMainQueueSetup() -> Bool { + return true + } +} diff --git a/ios/Video/RCTVideoPlayerViewController.h b/ios/Video/RCTVideoPlayerViewController.h deleted file mode 100644 index ed9ebdde..00000000 --- a/ios/Video/RCTVideoPlayerViewController.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// RCTVideoPlayerViewController.h -// RCTVideo -// -// Created by Stanisław Chmiela on 31.03.2016. -// Copyright © 2016 Facebook. All rights reserved. -// - -#import -#import "RCTVideo.h" -#import "RCTVideoPlayerViewControllerDelegate.h" - -@interface RCTVideoPlayerViewController : AVPlayerViewController -@property (nonatomic, weak) id rctDelegate; - -// Optional paramters -@property (nonatomic, weak) NSString* preferredOrientation; -@property (nonatomic) BOOL autorotate; - -@end diff --git a/ios/Video/RCTVideoPlayerViewController.m b/ios/Video/RCTVideoPlayerViewController.m deleted file mode 100644 index 548a06ce..00000000 --- a/ios/Video/RCTVideoPlayerViewController.m +++ /dev/null @@ -1,43 +0,0 @@ -#import "RCTVideoPlayerViewController.h" - -@interface RCTVideoPlayerViewController () - -@end - -@implementation RCTVideoPlayerViewController - -- (BOOL)shouldAutorotate { - - if (self.autorotate || self.preferredOrientation.lowercaseString == nil || [self.preferredOrientation.lowercaseString isEqualToString:@"all"]) - return YES; - - return NO; -} - -- (void)viewDidDisappear:(BOOL)animated -{ - [super viewDidDisappear:animated]; - [_rctDelegate videoPlayerViewControllerWillDismiss:self]; - [_rctDelegate videoPlayerViewControllerDidDismiss:self]; -} - -#if !TARGET_OS_TV -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return UIInterfaceOrientationMaskAll; -} - -- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { - if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { - return UIInterfaceOrientationLandscapeRight; - } - else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { - return UIInterfaceOrientationPortrait; - } - else { // default case - UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; - return orientation; - } -} -#endif - -@end diff --git a/ios/Video/RCTVideoPlayerViewController.swift b/ios/Video/RCTVideoPlayerViewController.swift new file mode 100644 index 00000000..95c926af --- /dev/null +++ b/ios/Video/RCTVideoPlayerViewController.swift @@ -0,0 +1,44 @@ +import AVKit + +class RCTVideoPlayerViewController: AVPlayerViewController { + + var rctDelegate:RCTVideoPlayerViewControllerDelegate! + + // Optional paramters + var preferredOrientation:String? + var autorotate:Bool? + + func shouldAutorotate() -> Bool { + + if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") { + return true + } + + return false + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self) + rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self) + } + + #if !TARGET_OS_TV + + func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { + return .all + } + + func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation { + if preferredOrientation?.lowercased() == "landscape" { + return .landscapeRight + } else if preferredOrientation?.lowercased() == "portrait" { + return .portrait + } else { + // default case + let orientation = UIApplication.shared.statusBarOrientation + return orientation + } + } + #endif +} diff --git a/ios/Video/RCTVideoPlayerViewControllerDelegate.h b/ios/Video/RCTVideoPlayerViewControllerDelegate.h deleted file mode 100644 index e84b3f52..00000000 --- a/ios/Video/RCTVideoPlayerViewControllerDelegate.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import "AVKit/AVKit.h" - -@protocol RCTVideoPlayerViewControllerDelegate -- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController; -- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController; -@end diff --git a/ios/Video/RCTVideoPlayerViewControllerDelegate.swift b/ios/Video/RCTVideoPlayerViewControllerDelegate.swift new file mode 100644 index 00000000..6635975f --- /dev/null +++ b/ios/Video/RCTVideoPlayerViewControllerDelegate.swift @@ -0,0 +1,7 @@ +import Foundation +import AVKit + +protocol RCTVideoPlayerViewControllerDelegate : NSObject { + func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) + func videoPlayerViewControllerDidDismiss(playerViewController:AVPlayerViewController) +} diff --git a/ios/Video/UIView+FindUIViewController.h b/ios/Video/UIView+FindUIViewController.h deleted file mode 100644 index 09214261..00000000 --- a/ios/Video/UIView+FindUIViewController.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// UIView+FindUIViewController.h -// RCTVideo -// -// Created by Stanisław Chmiela on 31.03.2016. -// Copyright © 2016 Facebook. All rights reserved. -// -// Source: http://stackoverflow.com/a/3732812/1123156 - -#import - -@interface UIView (FindUIViewController) -- (UIViewController *) firstAvailableUIViewController; -- (id) traverseResponderChainForUIViewController; -@end diff --git a/ios/Video/UIView+FindUIViewController.m b/ios/Video/UIView+FindUIViewController.m deleted file mode 100644 index c29c742a..00000000 --- a/ios/Video/UIView+FindUIViewController.m +++ /dev/null @@ -1,21 +0,0 @@ -// Source: http://stackoverflow.com/a/3732812/1123156 - -#import "UIView+FindUIViewController.h" - -@implementation UIView (FindUIViewController) -- (UIViewController *) firstAvailableUIViewController { - // convenience function for casting and to "mask" the recursive function - return (UIViewController *)[self traverseResponderChainForUIViewController]; -} - -- (id) traverseResponderChainForUIViewController { - id nextResponder = [self nextResponder]; - if ([nextResponder isKindOfClass:[UIViewController class]]) { - return nextResponder; - } else if ([nextResponder isKindOfClass:[UIView class]]) { - return [nextResponder traverseResponderChainForUIViewController]; - } else { - return nil; - } -} -@end diff --git a/ios/Video/UIView+FindUIViewController.swift b/ios/Video/UIView+FindUIViewController.swift new file mode 100644 index 00000000..2947e1e3 --- /dev/null +++ b/ios/Video/UIView+FindUIViewController.swift @@ -0,0 +1,18 @@ +// Source: http://stackoverflow.com/a/3732812/1123156 + +extension UIView { + func firstAvailableUIViewController() -> UIViewController? { + // convenience function for casting and to "mask" the recursive function + return traverseResponderChainForUIViewController() + } + + func traverseResponderChainForUIViewController() -> UIViewController? { + if let nextUIViewController = next as? UIViewController { + return nextUIViewController + } else if let nextUIView = next as? UIView { + return nextUIView.traverseResponderChainForUIViewController() + } else { + return nil + } + } +} diff --git a/ios/VideoCaching/RCTVideoCachingHandler.swift b/ios/VideoCaching/RCTVideoCachingHandler.swift new file mode 100644 index 00000000..8e37f11a --- /dev/null +++ b/ios/VideoCaching/RCTVideoCachingHandler.swift @@ -0,0 +1,81 @@ +import Foundation +import AVFoundation +import DVAssetLoaderDelegate + +class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { + + private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance() + private var _playerItemPrepareText: (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void + + init(_ playerItemPrepareText: @escaping (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void) { + _playerItemPrepareText = playerItemPrepareText + } + + func playerItemForSourceUsingCache(shouldCache:Bool, textTracks:[AnyObject]?, uri:String, assetOptions:NSMutableDictionary, handler:@escaping (AVPlayerItem?)->Void) -> Bool { + if shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) { + /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying + * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. + * Until this is fixed, we need to bypass caching when text tracks are specified. + */ + DebugLog("Caching is not supported for uri '\(uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + playerItemForSourceUsingCache(uri: uri, assetOptions:assetOptions, withCallback:handler) + return true + } + return false + } + + func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!, withCallback handler: @escaping (AVPlayerItem?)->Void) { + let url = URL(string: uri) + _videoCache.getItemForUri(uri, withCallback:{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in + guard let self = self else { return } + switch (videoCacheStatus) { + case .missingFileExtension: + DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) + self._playerItemPrepareText(asset, options, handler) + return + + case .unsupportedFileExtension: + DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) + self._playerItemPrepareText(asset, options, handler) + return + + default: + if let cachedAsset = cachedAsset { + DebugLog("Playing back uri '\(uri)' from cache") + // See note in playerItemForSource about not being able to support text tracks & caching + handler(AVPlayerItem(asset: cachedAsset)) + return + } + } + + let asset:DVURLAsset! = DVURLAsset(url:url, options:options as! [String : Any], networkTimeout:10000) + asset.loaderDelegate = self + + /* More granular code to have control over the DVURLAsset + let resourceLoaderDelegate = DVAssetLoaderDelegate(url: url) + resourceLoaderDelegate.delegate = self + let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false) + components?.scheme = DVAssetLoaderDelegate.scheme() + var asset: AVURLAsset? = nil + if let url = components?.url { + asset = AVURLAsset(url: url, options: options) + } + asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main) + */ + + handler(AVPlayerItem(asset: asset)) + }) + } + + // MARK: - DVAssetLoaderDelegate + + func dvAssetLoaderDelegate(loaderDelegate:DVAssetLoaderDelegate!, didLoadData data:NSData!, forURL url:NSURL!) { + _videoCache.storeItem(data as Data?, forUri:url.absoluteString, withCallback:{ (success:Bool) in + DebugLog("Cache data stored successfully 🎉") + }) + } + +} + diff --git a/react-native-video.podspec b/react-native-video.podspec index 7013f957..5c3f5e5d 100644 --- a/react-native-video.podspec +++ b/react-native-video.podspec @@ -16,8 +16,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.subspec "Video" do |ss| - ss.source_files = "ios/Video/*.{h,m}" - s.static_framework = true + ss.source_files = "ios/Video/**/*.{h,m,swift}" end s.subspec "VideoCaching" do |ss| @@ -25,14 +24,15 @@ Pod::Spec.new do |s| ss.dependency "SPTPersistentCache", "~> 1.1.0" ss.dependency "DVAssetLoaderDelegate", "~> 0.3.1" - ss.source_files = "ios/VideoCaching/**/*.{h,m}" - s.static_framework = true + ss.source_files = "ios/VideoCaching/**/*.{h,m,swift}" end s.dependency "React-Core" s.default_subspec = "Video" + s.static_framework = true + s.xcconfig = { 'OTHER_LDFLAGS': '-ObjC', }