feat!(ios): remove native dependency promises (#3631)

This commit is contained in:
Krzysztof Moch 2024-04-04 13:23:44 +02:00 committed by GitHub
parent 2633f087d2
commit 10b100de44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 686 additions and 785 deletions

View File

@ -6,7 +6,8 @@
## Beta Information ## Beta Information
> ⚠️ **Version 6 Beta**: The following documentation may refer to features only available through the v6.0.0 alpha releases, [please see version 5.2.x](https://github.com/react-native-video/react-native-video/blob/v5.2.0/README.md) for the current documentation! > ⚠️ **Version 6 Beta**: The following documentation may refer to features only available through the v6.0.0 alpha releases, [please see version 5.2.x](https://github.com/react-native-video/react-native-video/blob/v5.2.0/README.md) for the current documentation!
Version 6.x recommends react-native >= 0.68.2. Version 6.x requires **react-native >= 0.68.2**
> ⚠️ from **6.0.0-beta.8** requires also **iOS >= 13.0** (default in react-native 0.73)
For older versions of react-native, [please use version 5.x](https://github.com/react-native-video/react-native-video/tree/v5.2.0). For older versions of react-native, [please use version 5.x](https://github.com/react-native-video/react-native-video/tree/v5.2.0).

View File

@ -22,6 +22,7 @@ Then follow the instructions for your platform to link react-native-video into y
## iOS ## iOS
### Standard Method ### Standard Method
Run `pod install` in the `ios` directory of your project.
### Enable custom feature in podfile file ### Enable custom feature in podfile file
@ -155,16 +156,8 @@ Select RCTVideo-tvOS
<summary>visionOS</summary> <summary>visionOS</summary>
## visionOS ## visionOS
Add patch for `promises` pods to your pod files to make it work with `visionOS` target. Run `pod install` in the `visionos` directory of your project
> This patch is required only for `visionOS` target and will be removed in future.
```diff
+ pod 'PromisesSwift', :podspec => '../node_modules/react-native-video/ios/patches/PromisesSwift.podspec'
+ pod 'PromisesObjC', :podspec => '../node_modules/react-native-video/ios/patches/PromisesObjC.podspec'
```
**Remember** to run `pod install` after adding this patch.
After this you can follow the same steps as for `iOS` target.
</details> </details>
## Examples ## Examples

View File

@ -7,9 +7,6 @@ PODS:
- hermes-engine (0.74.0-rc.4): - hermes-engine (0.74.0-rc.4):
- hermes-engine/Pre-built (= 0.74.0-rc.4) - hermes-engine/Pre-built (= 0.74.0-rc.4)
- hermes-engine/Pre-built (0.74.0-rc.4) - hermes-engine/Pre-built (0.74.0-rc.4)
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- RCT-Folly (2024.01.01.00): - RCT-Folly (2024.01.01.00):
- boost - boost
- DoubleConversion - DoubleConversion
@ -942,7 +939,6 @@ PODS:
- React-Core - React-Core
- react-native-video/Video (= 6.0.0-beta.6) - react-native-video/Video (= 6.0.0-beta.6)
- react-native-video/Video (6.0.0-beta.6): - react-native-video/Video (6.0.0-beta.6):
- PromisesSwift
- React-Core - React-Core
- React-nativeconfig (0.74.0-rc.4) - React-nativeconfig (0.74.0-rc.4)
- React-NativeModulesApple (0.74.0-rc.4): - React-NativeModulesApple (0.74.0-rc.4):
@ -1239,8 +1235,6 @@ DEPENDENCIES:
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- PromisesObjC
- PromisesSwift
- SocketRocket - SocketRocket
EXTERNAL SOURCES: EXTERNAL SOURCES:
@ -1365,8 +1359,6 @@ SPEC CHECKSUMS:
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
hermes-engine: dfdcadd89a22aa872ef552b07e415d88df68af55 hermes-engine: dfdcadd89a22aa872ef552b07e415d88df68af55
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
RCT-Folly: 045d6ecaa59d826c5736dfba0b2f4083ff8d79df RCT-Folly: 045d6ecaa59d826c5736dfba0b2f4083ff8d79df
RCTDeprecation: 1c5ab5895f9fc7e8ae9fcde04859f0d246283209 RCTDeprecation: 1c5ab5895f9fc7e8ae9fcde04859f0d246283209
RCTRequired: 79e2e81174db06336f470c49aea7603ff29817a7 RCTRequired: 79e2e81174db06336f470c49aea7603ff29817a7
@ -1391,7 +1383,7 @@ SPEC CHECKSUMS:
React-jsitracing: 50e3ea936a199a2a7fcab922f156507c97f0b88c React-jsitracing: 50e3ea936a199a2a7fcab922f156507c97f0b88c
React-logger: 6004e0cf41b7e9714ca26b1648e5d76fcfd638b5 React-logger: 6004e0cf41b7e9714ca26b1648e5d76fcfd638b5
React-Mapbuffer: 9b163fa28e549d5f36f89a39a1145fcaf262d0d0 React-Mapbuffer: 9b163fa28e549d5f36f89a39a1145fcaf262d0d0
react-native-video: dc3118548cf8864a83f57df4345cf6c692402e8f react-native-video: d340c162bf7974c2935fbeec0c5dea362f9dd74a
React-nativeconfig: 3948d6fb6acfec364625cffbb1cf420346fb37c0 React-nativeconfig: 3948d6fb6acfec364625cffbb1cf420346fb37c0
React-NativeModulesApple: 46745aba687c1019983d56b6d5fa39265152f64f React-NativeModulesApple: 46745aba687c1019983d56b6d5fa39265152f64f
React-perflogger: 0d62c0261b6fd3920605850de91abc8135dd3ee9 React-perflogger: 0d62c0261b6fd3920605850de91abc8135dd3ee9

View File

@ -211,7 +211,7 @@
}; };
}; };
}; };
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "videoplayer" */; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "VideoPlayer" */;
compatibilityVersion = "Xcode 12.0"; compatibilityVersion = "Xcode 12.0";
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
@ -619,7 +619,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
); );
IPHONEOS_DEPLOYMENT_TARGET = 12.4; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift, /usr/lib/swift,
"$(inherited)", "$(inherited)",
@ -729,7 +729,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
); );
IPHONEOS_DEPLOYMENT_TARGET = 12.4; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift, /usr/lib/swift,
"$(inherited)", "$(inherited)",
@ -776,7 +776,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "videoplayer" */ = { 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "VideoPlayer" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */, 83CBBA201A601CBA00E9B192 /* Debug */,

View File

@ -78,9 +78,6 @@ PODS:
- hermes-engine/Pre-built (0.71.12-0) - hermes-engine/Pre-built (0.71.12-0)
- libevent (2.1.12.1) - libevent (2.1.12.1)
- OpenSSL-Universal (1.1.1100) - OpenSSL-Universal (1.1.1100)
- PromisesObjC (2.3.1)
- PromisesSwift (2.3.1):
- PromisesObjC (= 2.3.1)
- RCT-Folly (2021.07.22.00): - RCT-Folly (2021.07.22.00):
- boost - boost
- DoubleConversion - DoubleConversion
@ -319,12 +316,9 @@ PODS:
- React-jsinspector (0.71.12-0) - React-jsinspector (0.71.12-0)
- React-logger (0.71.12-0): - React-logger (0.71.12-0):
- glog - glog
- react-native-video (6.0.0-alpha.8): - react-native-video (6.0.0-beta.6):
- React-Core
- react-native-video/Video (= 6.0.0-alpha.8)
- react-native-video/Video (6.0.0-alpha.8):
- PromisesSwift
- React-Core - React-Core
- react-native-video/Video (= 6.0.0-beta.6)
- React-perflogger (0.71.12-0) - React-perflogger (0.71.12-0)
- React-RCTActionSheet (0.71.12-0): - React-RCTActionSheet (0.71.12-0):
- React-Core/RCTActionSheetHeaders (= 0.71.12-0) - React-Core/RCTActionSheetHeaders (= 0.71.12-0)
@ -486,8 +480,6 @@ SPEC REPOS:
- Flipper-RSocket - Flipper-RSocket
- FlipperKit - FlipperKit
- OpenSSL-Universal - OpenSSL-Universal
- PromisesObjC
- PromisesSwift
- SocketRocket - SocketRocket
EXTERNAL SOURCES: EXTERNAL SOURCES:
@ -582,8 +574,6 @@ SPEC CHECKSUMS:
hermes-engine: 3d04f537177e132da926803412639dacd59a0ee9 hermes-engine: 3d04f537177e132da926803412639dacd59a0ee9
libevent: a6d75fcd7be07cbc5070300ea8dbc8d55dfab88e libevent: a6d75fcd7be07cbc5070300ea8dbc8d55dfab88e
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
RCT-Folly: 136e9161a833a162fe3e8b647098759aae227036 RCT-Folly: 136e9161a833a162fe3e8b647098759aae227036
RCTRequired: 0c0d97ba9f1e2b2b70e0522d65992a2993a714cd RCTRequired: 0c0d97ba9f1e2b2b70e0522d65992a2993a714cd
RCTTypeSafety: 5a484bd8f18408b8918a668ac8bd8b9f9138142b RCTTypeSafety: 5a484bd8f18408b8918a668ac8bd8b9f9138142b
@ -598,7 +588,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: 0c8c5e8b2171be52295f59097923babf84d1cf66 React-jsiexecutor: 0c8c5e8b2171be52295f59097923babf84d1cf66
React-jsinspector: f8e6919523047a9bd1270ade75b4eca0108963b4 React-jsinspector: f8e6919523047a9bd1270ade75b4eca0108963b4
React-logger: 16c56636d4209cc204d06c5ba347cee21b960012 React-logger: 16c56636d4209cc204d06c5ba347cee21b960012
react-native-video: 86950ad481cec184d7c9420ec3bca0c27904bbcd react-native-video: 98040e05dace82fbbe8709cf42fd4496b0aed744
React-perflogger: 355109dc9d6f34e35bc35dabb32310f8ed2d29a2 React-perflogger: 355109dc9d6f34e35bc35dabb32310f8ed2d29a2
React-RCTActionSheet: 9d1be4d43972f2aae4b31d9e53ffb030115fa445 React-RCTActionSheet: 9d1be4d43972f2aae4b31d9e53ffb030115fa445
React-RCTAnimation: aab7e1ecd325db67e1f2a947d85a52adf86594b7 React-RCTAnimation: aab7e1ecd325db67e1f2a947d85a52adf86594b7
@ -617,4 +607,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 49dad183688257f9360c15d54e77f8de0f8048f7 PODFILE CHECKSUM: 49dad183688257f9360c15d54e77f8de0f8048f7
COCOAPODS: 1.12.1 COCOAPODS: 1.13.0

View File

@ -1,6 +1,5 @@
import AVFoundation import AVFoundation
import MediaAccessibility import MediaAccessibility
import Promises
let RCTVideoUnset = -1 let RCTVideoUnset = -1
@ -10,8 +9,7 @@ let RCTVideoUnset = -1
* Collection of mutating functions * Collection of mutating functions
*/ */
enum RCTPlayerOperations { enum RCTPlayerOperations {
static func setSideloadedText(player: AVPlayer?, textTracks: [TextTrack], criteria: SelectedTrackCriteria?) -> Promise<Void> { static func setSideloadedText(player: AVPlayer?, textTracks: [TextTrack], criteria: SelectedTrackCriteria?) {
return Promise {
let type = criteria?.type let type = criteria?.type
let trackCount: Int! = player?.currentItem?.tracks.count ?? 0 let trackCount: Int! = player?.currentItem?.tracks.count ?? 0
@ -79,15 +77,15 @@ enum RCTPlayerOperations {
player?.currentItem?.tracks[i].isEnabled = isEnabled player?.currentItem?.tracks[i].isEnabled = isEnabled
} }
} }
}
// UNUSED // UNUSED
static func setStreamingText(player: AVPlayer?, criteria: SelectedTrackCriteria?) { static func setStreamingText(player: AVPlayer?, criteria: SelectedTrackCriteria?) async {
let type = criteria?.type let type = criteria?.type
var mediaOption: AVMediaSelectionOption! var mediaOption: AVMediaSelectionOption!
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: .legible).then { group in guard let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: .legible) else {
guard let group else { return } return
}
if type == "disabled" { if type == "disabled" {
// Do nothing. We want to ensure option is nil // Do nothing. We want to ensure option is nil
@ -118,7 +116,7 @@ enum RCTPlayerOperations {
#if os(tvOS) #if os(tvOS)
// Do noting. Fix for tvOS native audio menu language selector // Do noting. Fix for tvOS native audio menu language selector
#else #else
player?.currentItem?.selectMediaOptionAutomatically(in: group) await player?.currentItem?.selectMediaOptionAutomatically(in: group)
return return
#endif #endif
} }
@ -127,17 +125,17 @@ enum RCTPlayerOperations {
// Do noting. Fix for tvOS native audio menu language selector // Do noting. Fix for tvOS native audio menu language selector
#else #else
// If a match isn't found, option will be nil and text tracks will be disabled // If a match isn't found, option will be nil and text tracks will be disabled
player?.currentItem?.select(mediaOption, in: group) await player?.currentItem?.select(mediaOption, in: group)
#endif #endif
} }
}
static func setMediaSelectionTrackForCharacteristic(player: AVPlayer?, characteristic: AVMediaCharacteristic, criteria: SelectedTrackCriteria?) { static func setMediaSelectionTrackForCharacteristic(player: AVPlayer?, characteristic: AVMediaCharacteristic, criteria: SelectedTrackCriteria?) async {
let type = criteria?.type let type = criteria?.type
var mediaOption: AVMediaSelectionOption! var mediaOption: AVMediaSelectionOption!
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: characteristic).then { group in guard let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: characteristic) else {
guard let group else { return } return
}
if type == "disabled" { if type == "disabled" {
// Do nothing. We want to ensure option is nil // Do nothing. We want to ensure option is nil
@ -165,33 +163,31 @@ enum RCTPlayerOperations {
} }
} }
} else { // default. invalid type or "system" } else { // default. invalid type or "system"
player?.currentItem?.selectMediaOptionAutomatically(in: group) await player?.currentItem?.selectMediaOptionAutomatically(in: group)
return return
} }
// If a match isn't found, option will be nil and text tracks will be disabled // If a match isn't found, option will be nil and text tracks will be disabled
player?.currentItem?.select(mediaOption, in: group) await player?.currentItem?.select(mediaOption, in: group)
}
} }
static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float) -> Promise<Bool> { static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float, completion: @escaping (Bool) -> Void) {
let timeScale = 1000 let timeScale = 1000
let cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale)) let cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
let current: CMTime = playerItem.currentTime() let current: CMTime = playerItem.currentTime()
let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale)) let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
return Promise<Bool>(on: .global()) { fulfill, reject in
guard CMTimeCompare(current, cmSeekTime) != 0 else { guard CMTimeCompare(current, cmSeekTime) != 0 else {
reject(NSError(domain: "", code: 0, userInfo: nil)) // skip if there is no diff in current time and seek time
return return
} }
if !paused { player.pause() } if !paused { player.pause() }
player.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { (finished: Bool) in player.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { (finished: Bool) in
fulfill(finished) completion(finished)
}) })
} }
}
static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) { static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) {
let audioSession: AVAudioSession! = AVAudioSession.sharedInstance() let audioSession: AVAudioSession! = AVAudioSession.sharedInstance()

View File

@ -1,5 +1,4 @@
import AVFoundation import AVFoundation
import Promises
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:] private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:]
@ -135,7 +134,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
return false return false
} }
var requestKey: String = loadingRequest.request.url?.absoluteString ?? "" let requestKey: String = loadingRequest.request.url?.absoluteString ?? ""
_loadingRequests[requestKey] = loadingRequest _loadingRequests[requestKey] = loadingRequest
@ -143,43 +142,44 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey) return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey)
} }
var promise: Promise<Data> Task {
do {
if _onGetLicense != nil { if _onGetLicense != nil {
let contentId = _drm.contentId ?? loadingRequest.request.url?.host let contentId = _drm.contentId ?? loadingRequest.request.url?.host
promise = RCTVideoDRM.handleWithOnGetLicense( let spcData = try await RCTVideoDRM.handleWithOnGetLicense(
loadingRequest: loadingRequest, loadingRequest: loadingRequest,
contentId: contentId, contentId: contentId,
certificateUrl: _drm.certificateUrl, certificateUrl: _drm.certificateUrl,
base64Certificate: _drm.base64Certificate base64Certificate: _drm.base64Certificate
).then { spcData in )
self._requestingCertificate = true self._requestingCertificate = true
self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "", self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "",
"loadedLicenseUrl": loadingRequest.request.url?.absoluteString ?? "", "loadedLicenseUrl": loadingRequest.request.url?.absoluteString ?? "",
"contentId": contentId ?? "", "contentId": contentId ?? "",
"spcBase64": spcData.base64EncodedString(options: []), "spcBase64": spcData.base64EncodedString(options: []),
"target": self._reactTag]) "target": self._reactTag])
}
} else { } else {
promise = RCTVideoDRM.handleInternalGetLicense( let data = try await RCTVideoDRM.handleInternalGetLicense(
loadingRequest: loadingRequest, loadingRequest: loadingRequest,
contentId: _drm.contentId, contentId: _drm.contentId,
licenseServer: _drm.licenseServer, licenseServer: _drm.licenseServer,
certificateUrl: _drm.certificateUrl, certificateUrl: _drm.certificateUrl,
base64Certificate: _drm.base64Certificate, base64Certificate: _drm.base64Certificate,
headers: _drm.headers headers: _drm.headers
).then { data in )
guard let dataRequest = loadingRequest.dataRequest else { guard let dataRequest = loadingRequest.dataRequest else {
throw RCTVideoErrorHandler.noCertificateData throw RCTVideoErrorHandler.noCertificateData
} }
dataRequest.respond(with: data) dataRequest.respond(with: data)
loadingRequest.finishLoading() loadingRequest.finishLoading()
} }
} } catch {
promise.catch { error in
self.finishLoadingWithError(error: error, licenseUrl: requestKey) self.finishLoadingWithError(error: error, licenseUrl: requestKey)
self._requestingCertificateErrored = true self._requestingCertificateErrored = true
} }
}
return true return true
} }

View File

@ -1,5 +1,4 @@
import AVFoundation import AVFoundation
import Promises
enum RCTVideoDRM { enum RCTVideoDRM {
static func fetchLicense( static func fetchLicense(
@ -7,36 +6,25 @@ enum RCTVideoDRM {
spcData: Data?, spcData: Data?,
contentId: String, contentId: String,
headers: [String: Any]? headers: [String: Any]?
) -> Promise<Data> { ) async throws -> Data {
let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers) let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers)
return Promise<Data>(on: .global()) { fulfill, reject in let (data, response) = try await URLSession.shared.data(from: request)
let postDataTask = URLSession.shared.dataTask(
with: request as URLRequest,
completionHandler: { (data: Data!, response: URLResponse!, error: Error!) in
let httpResponse: HTTPURLResponse! = (response as! HTTPURLResponse)
guard error == nil else { guard let httpResponse = response as? HTTPURLResponse else {
throw RCTVideoErrorHandler.noDataFromLicenseRequest
}
if httpResponse.statusCode != 200 {
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
reject(error) throw RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode)
return
}
guard httpResponse.statusCode == 200 else {
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
reject(RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode))
return
} }
guard data != nil, let decodedData = Data(base64Encoded: data, options: []) else { guard let decodedData = Data(base64Encoded: data, options: []) else {
reject(RCTVideoErrorHandler.noDataFromLicenseRequest) throw RCTVideoErrorHandler.noDataFromLicenseRequest
return
} }
fulfill(decodedData) return decodedData
}
)
postDataTask.resume()
}
} }
static func createLicenseRequest( static func createLicenseRequest(
@ -76,32 +64,27 @@ enum RCTVideoDRM {
loadingRequest: AVAssetResourceLoadingRequest, loadingRequest: AVAssetResourceLoadingRequest,
certificateData: Data, certificateData: Data,
contentIdData: Data contentIdData: Data
) -> Promise<Data> { ) throws -> Data {
return Promise<Data>(on: .global()) { fulfill, reject in
#if os(visionOS) #if os(visionOS)
// TODO: DRM is not supported yet on visionOS. See #3467 // TODO: DRM is not supported yet on visionOS. See #3467
reject(NSError(domain: "DRM is not supported yet on visionOS", code: 0, userInfo: nil)) throw NSError(domain: "DRM is not supported yet on visionOS", code: 0, userInfo: nil)
#else #else
guard let spcData = try? loadingRequest.streamingContentKeyRequestData( guard let spcData = try? loadingRequest.streamingContentKeyRequestData(
forApp: certificateData, forApp: certificateData,
contentIdentifier: contentIdData as Data, contentIdentifier: contentIdData as Data,
options: nil options: nil
) else { ) else {
reject(RCTVideoErrorHandler.noSPC) throw RCTVideoErrorHandler.noSPC
return
} }
fulfill(spcData) return spcData
#endif #endif
} }
}
static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) -> Promise<Data> { static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) throws -> Data {
return Promise<Data>(on: .global()) { fulfill, reject in
guard let certificateStringUrl, guard let certificateStringUrl,
let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else {
reject(RCTVideoErrorHandler.noCertificateURL) throw RCTVideoErrorHandler.noCertificateURL
return
} }
var certificateData: Data? var certificateData: Data?
@ -113,31 +96,32 @@ enum RCTVideoDRM {
} catch {} } catch {}
guard let certificateData else { guard let certificateData else {
reject(RCTVideoErrorHandler.noCertificateData) throw RCTVideoErrorHandler.noCertificateData
return
} }
fulfill(certificateData) return certificateData
}
} }
static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId: String?, certificateUrl: String?, static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId: String?, certificateUrl: String?,
base64Certificate: Bool?) -> Promise<Data> { base64Certificate: Bool?) throws -> Data {
let contentIdData = contentId?.data(using: .utf8) let contentIdData = contentId?.data(using: .utf8)
return RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate) let certificateData = try? RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate)
.then { certificateData -> Promise<Data> in
guard let contentIdData else { guard let contentIdData else {
throw RCTVideoError.invalidContentId as! Error throw RCTVideoError.invalidContentId as! Error
} }
return RCTVideoDRM.fetchSpcData( guard let certificateData else {
throw RCTVideoError.noCertificateData as! Error
}
return try RCTVideoDRM.fetchSpcData(
loadingRequest: loadingRequest, loadingRequest: loadingRequest,
certificateData: certificateData, certificateData: certificateData,
contentIdData: contentIdData contentIdData: contentIdData
) )
} }
}
static func handleInternalGetLicense( static func handleInternalGetLicense(
loadingRequest: AVAssetResourceLoadingRequest, loadingRequest: AVAssetResourceLoadingRequest,
@ -146,35 +130,32 @@ enum RCTVideoDRM {
certificateUrl: String?, certificateUrl: String?,
base64Certificate: Bool?, base64Certificate: Bool?,
headers: [String: Any]? headers: [String: Any]?
) -> Promise<Data> { ) async throws -> Data {
let url = loadingRequest.request.url let url = loadingRequest.request.url
let parsedContentId = contentId != nil && !contentId!.isEmpty ? contentId : nil let parsedContentId = contentId != nil && !contentId!.isEmpty ? contentId : nil
guard let contentId = parsedContentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with: "") else { guard let contentId = parsedContentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with: "") else {
return Promise(RCTVideoError.invalidContentId as! Error) throw RCTVideoError.invalidContentId as! Error
} }
let contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length: contentId.lengthOfBytes(using: String.Encoding.utf8)) as Data let contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length: contentId.lengthOfBytes(using: String.Encoding.utf8)) as Data
let certificateData = try RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate)
return RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate) let spcData = try RCTVideoDRM.fetchSpcData(
.then { certificateData in
return RCTVideoDRM.fetchSpcData(
loadingRequest: loadingRequest, loadingRequest: loadingRequest,
certificateData: certificateData, certificateData: certificateData,
contentIdData: contentIdData contentIdData: contentIdData
) )
}
.then { spcData -> Promise<Data> in
guard let licenseServer else { guard let licenseServer else {
throw RCTVideoError.noLicenseServerURL as! Error throw RCTVideoError.noLicenseServerURL as! Error
} }
return RCTVideoDRM.fetchLicense(
return try await RCTVideoDRM.fetchLicense(
licenseServer: licenseServer, licenseServer: licenseServer,
spcData: spcData, spcData: spcData,
contentId: contentId, contentId: contentId,
headers: headers headers: headers
) )
} }
}
} }

View File

@ -1,6 +1,5 @@
import AVFoundation import AVFoundation
import Photos import Photos
import Promises
// MARK: - RCTVideoAssetsUtils // MARK: - RCTVideoAssetsUtils
@ -8,30 +7,22 @@ enum RCTVideoAssetsUtils {
static func getMediaSelectionGroup( static func getMediaSelectionGroup(
asset: AVAsset?, asset: AVAsset?,
for mediaCharacteristic: AVMediaCharacteristic for mediaCharacteristic: AVMediaCharacteristic
) -> Promise<AVMediaSelectionGroup?> { ) async -> AVMediaSelectionGroup? {
if #available(iOS 15, tvOS 15, visionOS 1.0, *) { if #available(iOS 15, tvOS 15, visionOS 1.0, *) {
return wrap { handler in return try? await asset?.loadMediaSelectionGroup(for: mediaCharacteristic)
asset?.loadMediaSelectionGroup(for: mediaCharacteristic, completionHandler: handler)
}
} else { } else {
#if !os(visionOS) #if !os(visionOS)
return Promise { fulfill, _ in return asset?.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic)
fulfill(asset?.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic))
}
#endif #endif
} }
} }
static func getTracks(asset: AVAsset, withMediaType: AVMediaType) -> Promise<[AVAssetTrack]?> { static func getTracks(asset: AVAsset, withMediaType: AVMediaType) async -> [AVAssetTrack]? {
if #available(iOS 15, tvOS 15, visionOS 1.0, *) { if #available(iOS 15, tvOS 15, visionOS 1.0, *) {
return wrap { handler in return try? await asset.loadTracks(withMediaType: withMediaType)
asset.loadTracks(withMediaType: withMediaType, completionHandler: handler)
}
} else { } else {
#if !os(visionOS) #if !os(visionOS)
return Promise { fulfill, _ in return asset.tracks(withMediaType: withMediaType)
fulfill(asset.tracks(withMediaType: withMediaType))
}
#endif #endif
} }
} }
@ -131,16 +122,15 @@ enum RCTVideoUtils {
return 0 return 0
} }
static func getAudioTrackInfo(_ player: AVPlayer?) -> Promise<[AnyObject]> { static func getAudioTrackInfo(_ player: AVPlayer?) async -> [AnyObject] {
return Promise { fulfill, _ in
guard let player, let asset = player.currentItem?.asset else { guard let player, let asset = player.currentItem?.asset else {
fulfill([]) return []
return
} }
let audioTracks: NSMutableArray! = NSMutableArray() let audioTracks: NSMutableArray! = NSMutableArray()
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .audible).then { group in let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .audible)
for i in 0 ..< (group?.options.count ?? 0) { for i in 0 ..< (group?.options.count ?? 0) {
let currentOption = group?.options[i] let currentOption = group?.options[i]
var title = "" var title = ""
@ -161,21 +151,18 @@ enum RCTVideoUtils {
audioTracks.add(audioTrack) audioTracks.add(audioTrack)
} }
fulfill(audioTracks as [AnyObject]) return audioTracks as [AnyObject]
}
}
} }
static func getTextTrackInfo(_ player: AVPlayer?) -> Promise<[TextTrack]> { static func getTextTrackInfo(_ player: AVPlayer?) async -> [TextTrack] {
return Promise { fulfill, _ in
guard let player, let asset = player.currentItem?.asset else { guard let player, let asset = player.currentItem?.asset else {
fulfill([]) return []
return
} }
// if streaming video, we extract the text tracks // if streaming video, we extract the text tracks
var textTracks: [TextTrack] = [] var textTracks: [TextTrack] = []
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .legible).then { group in let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .legible)
for i in 0 ..< (group?.options.count ?? 0) { for i in 0 ..< (group?.options.count ?? 0) {
let currentOption = group?.options[i] let currentOption = group?.options[i]
var title = "" var title = ""
@ -195,9 +182,7 @@ enum RCTVideoUtils {
textTracks.append(textTrack) textTracks.append(textTrack)
} }
fulfill(textTracks) return textTracks
}
}
} }
// UNUSED // UNUSED
@ -226,15 +211,13 @@ enum RCTVideoUtils {
return Data(base64Encoded: adoptURL.absoluteString) return Data(base64Encoded: adoptURL.absoluteString)
} }
static func generateMixComposition(_ asset: AVAsset) -> Promise<AVMutableComposition> { static func generateMixComposition(_ asset: AVAsset) async -> AVMutableComposition {
return Promise { fulfill, _ in let videoTracks = await RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video)
all( let audioTracks = await RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .audio)
RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video),
RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .audio)
).then { tracks in
let mixComposition = AVMutableComposition() let mixComposition = AVMutableComposition()
if let videoAsset = tracks.0?.first, let audioAsset = tracks.1?.first { if let videoAsset = videoTracks?.first, let audioAsset = audioTracks?.first {
let videoCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack( let videoCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
withMediaType: AVMediaType.video, withMediaType: AVMediaType.video,
preferredTrackID: kCMPersistentTrackID_Invalid preferredTrackID: kCMPersistentTrackID_Invalid
@ -255,35 +238,28 @@ enum RCTVideoUtils {
of: audioAsset, of: audioAsset,
at: .zero at: .zero
) )
}
fulfill(mixComposition) return mixComposition
} else {
fulfill(mixComposition)
}
}
}
} }
static func getValidTextTracks(asset: AVAsset, assetOptions: NSDictionary?, mixComposition: AVMutableComposition, static func getValidTextTracks(asset: AVAsset, assetOptions: NSDictionary?, mixComposition: AVMutableComposition,
textTracks: [TextTrack]?) -> Promise<[TextTrack]> { textTracks: [TextTrack]?) async -> [TextTrack] {
var validTextTracks: [TextTrack] = [] var validTextTracks: [TextTrack] = []
var queue: [Promise<[AVAssetTrack]?>] = [] var tracks: [[AVAssetTrack]] = []
return Promise { fulfill, _ in let videoTracks = await RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video)
RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video).then { tracks in guard let videoAsset = videoTracks?.first else { return validTextTracks }
guard let videoAsset = tracks?.first else {
return
}
if let textTracks, !textTracks.isEmpty { if let textTracks, !textTracks.isEmpty {
for track in textTracks { for textTrack in textTracks {
var textURLAsset: AVURLAsset! var textURLAsset: AVURLAsset!
let textUri: String = track.uri let textUri: String = textTrack.uri
if textUri.lowercased().hasPrefix("http") { if textUri.lowercased().hasPrefix("http") {
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options: (assetOptions as! [String: Any])) textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options: (assetOptions as! [String: Any]))
} else { } else {
let isDisabledTrack: Bool! = track.type == "disabled" let isDisabledTrack: Bool! = textTrack.type == "disabled"
let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory
textURLAsset = AVURLAsset( textURLAsset = AVURLAsset(
url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL,
@ -291,14 +267,13 @@ enum RCTVideoUtils {
) )
} }
queue.append(RCTVideoAssetsUtils.getTracks(asset: textURLAsset, withMediaType: .text)) if let track = await RCTVideoAssetsUtils.getTracks(asset: textURLAsset, withMediaType: .text) {
tracks.append(track)
} }
} }
all(queue).then { tracks in
if let textTracks {
for i in 0 ..< tracks.count { for i in 0 ..< tracks.count {
guard let track = tracks[i]?.first else { continue } // fix when there's no textTrackAsset guard let track = tracks[i].first else { continue } // fix when there's no textTrackAsset
let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text, let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
preferredTrackID: kCMPersistentTrackID_Invalid) preferredTrackID: kCMPersistentTrackID_Invalid)
@ -318,8 +293,6 @@ enum RCTVideoUtils {
} }
} }
return
}.then {
if !validTextTracks.isEmpty { if !validTextTracks.isEmpty {
let emptyVttFile: TextTrack? = self.createEmptyVttFile() let emptyVttFile: TextTrack? = self.createEmptyVttFile()
if emptyVttFile != nil { if emptyVttFile != nil {
@ -327,10 +300,7 @@ enum RCTVideoUtils {
} }
} }
fulfill(validTextTracks) return validTextTracks
}
}
}
} }
/* /*
@ -362,25 +332,26 @@ enum RCTVideoUtils {
]) ])
} }
static func delay(seconds: Int = 0) -> Promise<Void> { static func delay(seconds: Int = 0, completion: @escaping () async throws -> Void) {
return Promise<Void>(on: .global()) { fulfill, _ in return DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds)) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds)) { Task.detached(priority: .userInitiated) {
fulfill(()) try await completion()
} }
} }
} }
static func preparePHAsset(uri: String) -> Promise<AVAsset?> { static func preparePHAsset(uri: String) async -> AVAsset? {
return Promise<AVAsset?>(on: .global()) { fulfill, reject in
let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...]) let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...])
guard let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else { guard let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else {
reject(NSError(domain: "", code: 0, userInfo: nil)) return nil
return
} }
let options = PHVideoRequestOptions() let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true options.isNetworkAccessAllowed = true
return await withCheckedContinuation { continuation in
PHCachingImageManager().requestAVAsset(forVideo: phAsset, options: options) { data, _, _ in PHCachingImageManager().requestAVAsset(forVideo: phAsset, options: options) { data, _, _ in
fulfill(data) continuation.resume(returning: data)
} }
} }
} }
@ -444,10 +415,11 @@ enum RCTVideoUtils {
} }
} }
static func generateVideoComposition(asset: AVAsset, filter: CIFilter) -> Promise<AVVideoComposition?> { static func generateVideoComposition(asset: AVAsset, filter: CIFilter) async -> AVVideoComposition? {
if #available(iOS 16, tvOS 16, visionOS 1.0, *) { if #available(iOS 16, tvOS 16, visionOS 1.0, *) {
return wrap { handler in return try? await AVVideoComposition.videoComposition(
AVVideoComposition.videoComposition(with: asset, applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in with: asset,
applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in
if filter == nil { if filter == nil {
request.finish(with: request.sourceImage, context: nil) request.finish(with: request.sourceImage, context: nil)
} else { } else {
@ -456,12 +428,11 @@ enum RCTVideoUtils {
let output: CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent) let output: CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent)
request.finish(with: output, context: nil) request.finish(with: output, context: nil)
} }
}, completionHandler: handler)
} }
)
} else { } else {
#if !os(visionOS) #if !os(visionOS)
return Promise { fulfill, _ in return AVVideoComposition(
fulfill(AVVideoComposition(
asset: asset, asset: asset,
applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in
if filter == nil { if filter == nil {
@ -473,8 +444,7 @@ enum RCTVideoUtils {
request.finish(with: output, context: nil) request.finish(with: output, context: nil)
} }
} }
)) )
}
#endif #endif
} }
} }

View File

@ -0,0 +1,24 @@
import Foundation
@available(iOS, deprecated: 15.0, message: "Use the built-in API instead")
@available(tvOS, deprecated: 15.0, message: "Use the built-in API instead")
extension URLSession {
func data(from request: URLRequest) async throws -> (Data, URLResponse) {
if #available(iOS 15, tvOS 15, *) {
return try await URLSession.shared.data(for: request)
} else {
return try await withCheckedThrowingContinuation { continuation in
let task = self.dataTask(with: request, completionHandler: { data, response, error in
guard let data, let response else {
let error = error ?? URLError(.badServerResponse)
return continuation.resume(throwing: error)
}
continuation.resume(returning: (data, response))
})
task.resume()
}
}
}
}

View File

@ -4,7 +4,6 @@ import Foundation
#if USE_GOOGLE_IMA #if USE_GOOGLE_IMA
import GoogleInteractiveMediaAds import GoogleInteractiveMediaAds
#endif #endif
import Promises
import React import React
// MARK: - RCTVideo // MARK: - RCTVideo
@ -316,47 +315,28 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
// MARK: - Player and source // MARK: - Player and source
@objc func preparePlayerItem() async throws -> AVPlayerItem {
func setSrc(_ source: NSDictionary!) {
if self.isSetSourceOngoing || self.nextSource != nil {
DebugLog("setSrc buffer request")
self._player?.replaceCurrentItem(with: nil)
nextSource = source
return
}
self.isSetSourceOngoing = true
let dispatchClosure = {
self._source = VideoSource(source)
if self._source?.uri == nil || self._source?.uri == "" {
self._player?.replaceCurrentItem(with: nil)
self.isSetSourceOngoing = false
self.applyNextSource()
DebugLog("setSrc Stopping playback")
return
}
self.removePlayerLayer()
self._playerObserver.player = nil
self._resouceLoaderDelegate = nil
self._playerObserver.playerItem = nil
// perform on next run loop, otherwise other passed react-props may not be set
RCTVideoUtils.delay()
.then { [weak self] in
guard let self else { throw NSError(domain: "", code: 0, userInfo: nil) }
guard let source = self._source else { guard let source = self._source else {
DebugLog("The source not exist") DebugLog("The source not exist")
self.isSetSourceOngoing = false self.isSetSourceOngoing = false
self.applyNextSource() self.applyNextSource()
throw NSError(domain: "", code: 0, userInfo: nil) throw NSError(domain: "", code: 0, userInfo: nil)
} }
if let uri = source.uri, uri.starts(with: "ph://") { if let uri = source.uri, uri.starts(with: "ph://") {
return Promise { let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri)
RCTVideoUtils.preparePHAsset(uri: uri).then { asset in return await self.playerItemPrepareText(asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
return self.playerItemPrepareText(asset: asset, assetOptions: nil, uri: source.uri ?? "")
}
} }
guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
let asset = assetResult.asset,
let assetOptions = assetResult.assetOptions else {
DebugLog("Could not find video URL in source '\(String(describing: self._source))'")
self.isSetSourceOngoing = false
self.applyNextSource()
throw NSError(domain: "", code: 0, userInfo: nil)
} }
guard let assetResult = RCTVideoUtils.prepareAsset(source: source), guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
let asset = assetResult.asset, let asset = assetResult.asset,
let assetOptions = assetResult.assetOptions else { let assetOptions = assetResult.assetOptions else {
@ -372,7 +352,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
#if USE_VIDEO_CACHING #if USE_VIDEO_CACHING
if self._videoCache.shouldCache(source: source, textTracks: self._textTracks) { if self._videoCache.shouldCache(source: source, textTracks: self._textTracks) {
return self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions) return try await self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions)
} }
#endif #endif
@ -387,13 +367,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
) )
} }
return self.playerItemPrepareText(asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") return await playerItemPrepareText(asset: asset, assetOptions: assetOptions, uri: source.uri ?? "")
}.then { [weak self] (playerItem: AVPlayerItem!) in }
guard let self else { throw NSError(domain: "", code: 0, userInfo: nil) }
func setupPlayer(playerItem: AVPlayerItem) async throws {
if !self.isSetSourceOngoing { if !self.isSetSourceOngoing {
DebugLog("setSrc has been canceled last step") DebugLog("setSrc has been canceled last step")
return return
} }
self._player?.pause() self._player?.pause()
self._playerItem = playerItem self._playerItem = playerItem
self._playerObserver.playerItem = self._playerItem self._playerObserver.playerItem = self._playerItem
@ -436,16 +418,55 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
]) ])
self.isSetSourceOngoing = false self.isSetSourceOngoing = false
self.applyNextSource() self.applyNextSource()
}.catch { error in }
@objc
func setSrc(_ source: NSDictionary!) {
if self.isSetSourceOngoing || self.nextSource != nil {
DebugLog("setSrc buffer request")
self._player?.replaceCurrentItem(with: nil)
nextSource = source
return
}
self.isSetSourceOngoing = true
let initializeSource = {
self._source = VideoSource(source)
if self._source?.uri == nil || self._source?.uri == "" {
self._player?.replaceCurrentItem(with: nil)
self.isSetSourceOngoing = false
self.applyNextSource()
DebugLog("setSrc Stopping playback")
return
}
self.removePlayerLayer()
self._playerObserver.player = nil
self._resouceLoaderDelegate = nil
self._playerObserver.playerItem = nil
// perform on next run loop, otherwise other passed react-props may not be set
RCTVideoUtils.delay { [weak self] in
do {
guard let self else { throw NSError(domain: "", code: 0, userInfo: nil) }
let playerItem = try await self.preparePlayerItem()
try await setupPlayer(playerItem: playerItem)
} catch {
DebugLog("An error occurred: \(error.localizedDescription)") DebugLog("An error occurred: \(error.localizedDescription)")
if let self {
self.onVideoError?(["error": error.localizedDescription]) self.onVideoError?(["error": error.localizedDescription])
self.isSetSourceOngoing = false self.isSetSourceOngoing = false
self.applyNextSource() self.applyNextSource()
} }
}
}
self._videoLoadStarted = true self._videoLoadStarted = true
self.applyNextSource() self.applyNextSource()
} }
DispatchQueue.global(qos: .default).async(execute: dispatchClosure)
DispatchQueue.global(qos: .default).async(execute: initializeSource)
} }
@objc @objc
@ -458,32 +479,26 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_localSourceEncryptionKeyScheme = keyScheme _localSourceEncryptionKeyScheme = keyScheme
} }
func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) -> Promise<AVPlayerItem> { func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem {
return Promise { [weak self] fulfill, _ in
guard let self else { return }
if (self._textTracks == nil) || self._textTracks?.isEmpty == true || (uri.hasSuffix(".m3u8")) { if (self._textTracks == nil) || self._textTracks?.isEmpty == true || (uri.hasSuffix(".m3u8")) {
fulfill(self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))) return self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
return
} }
// AVPlayer can't airplay AVMutableCompositions // AVPlayer can't airplay AVMutableCompositions
self._allowsExternalPlayback = false self._allowsExternalPlayback = false
RCTVideoUtils.generateMixComposition(asset).then { mixComposition in let mixComposition = await RCTVideoUtils.generateMixComposition(asset)
RCTVideoUtils.getValidTextTracks( let validTextTracks = await RCTVideoUtils.getValidTextTracks(
asset: asset, asset: asset,
assetOptions: assetOptions, assetOptions: assetOptions,
mixComposition: mixComposition, mixComposition: mixComposition,
textTracks: self._textTracks textTracks: self._textTracks
).then { [self] validTextTracks in )
if validTextTracks.count != self._textTracks?.count { if validTextTracks.count != self._textTracks?.count {
self.setTextTracks(validTextTracks) self.setTextTracks(validTextTracks)
} }
fulfill(self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))) return self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))
}
}
}
} }
func playerItemPropegateMetadata(_ playerItem: AVPlayerItem!) -> AVPlayerItem { func playerItemPropegateMetadata(_ playerItem: AVPlayerItem!) -> AVPlayerItem {
@ -658,8 +673,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
paused: wasPaused, paused: wasPaused,
seekTime: seekTime.floatValue, seekTime: seekTime.floatValue,
seekTolerance: seekTolerance.floatValue seekTolerance: seekTolerance.floatValue
) ) { [weak self] (_: Bool) in
.then { [weak self] (_: Bool) in
guard let self else { return } guard let self else { return }
self._playerObserver.addTimeObserverIfNotSet() self._playerObserver.addTimeObserverIfNotSet()
@ -669,7 +683,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))),
"seekTime": seekTime, "seekTime": seekTime,
"target": self.reactTag]) "target": self.reactTag])
}.catch { _ in } }
_pendingSeek = false _pendingSeek = false
} }
@ -801,9 +815,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
func setSelectedAudioTrack(_ selectedAudioTrack: SelectedTrackCriteria?) { func setSelectedAudioTrack(_ selectedAudioTrack: SelectedTrackCriteria?) {
_selectedAudioTrackCriteria = selectedAudioTrack _selectedAudioTrackCriteria = selectedAudioTrack
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.audible, Task {
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.audible,
criteria: _selectedAudioTrackCriteria) criteria: _selectedAudioTrackCriteria)
} }
}
@objc @objc
func setSelectedTextTrack(_ selectedTextTrack: NSDictionary?) { func setSelectedTextTrack(_ selectedTextTrack: NSDictionary?) {
@ -815,10 +831,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
if _textTracks != nil { // sideloaded text tracks if _textTracks != nil { // sideloaded text tracks
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks!, criteria: _selectedTextTrackCriteria) RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks!, criteria: _selectedTextTrackCriteria)
} else { // text tracks included in the HLS playlist§ } else { // text tracks included in the HLS playlist§
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible, Task {
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible,
criteria: _selectedTextTrackCriteria) criteria: _selectedTextTrackCriteria)
} }
} }
}
@objc @objc
func setTextTracks(_ textTracks: [NSDictionary]?) { func setTextTracks(_ textTracks: [NSDictionary]?) {
@ -1035,8 +1053,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
let filter: CIFilter! = CIFilter(name: filterName) let filter: CIFilter! = CIFilter(name: filterName)
RCTVideoUtils.generateVideoComposition(asset: _playerItem!.asset, filter: filter).then { [weak self] composition in Task {
self?._playerItem?.videoComposition = composition let composition = await RCTVideoUtils.generateVideoComposition(asset: _playerItem!.asset, filter: filter)
self._playerItem?.videoComposition = composition
} }
} }
@ -1213,9 +1232,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
var height: Float? var height: Float?
var orientation = "undefined" var orientation = "undefined"
RCTVideoAssetsUtils.getTracks(asset: _playerItem.asset, withMediaType: .video).then { [weak self] tracks in Task {
guard let self else { return } let tracks = await RCTVideoAssetsUtils.getTracks(asset: _playerItem.asset, withMediaType: .video)
if let videoTrack = tracks?.first { if let videoTrack = tracks?.first {
width = Float(videoTrack.naturalSize.width) width = Float(videoTrack.naturalSize.width)
height = Float(videoTrack.naturalSize.height) height = Float(videoTrack.naturalSize.height)
@ -1251,7 +1269,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
if self._videoLoadStarted { if self._videoLoadStarted {
all(RCTVideoUtils.getAudioTrackInfo(self._player), RCTVideoUtils.getTextTrackInfo(self._player)).then { audioTracks, textTracks in let audioTracks = await RCTVideoUtils.getAudioTrackInfo(self._player)
let textTracks = await RCTVideoUtils.getTextTrackInfo(self._player)
self.onVideoLoad?(["duration": NSNumber(value: duration), self.onVideoLoad?(["duration": NSNumber(value: duration),
"currentTime": NSNumber(value: Float(CMTimeGetSeconds(_playerItem.currentTime()))), "currentTime": NSNumber(value: Float(CMTimeGetSeconds(_playerItem.currentTime()))),
"canPlayReverse": NSNumber(value: _playerItem.canPlayReverse), "canPlayReverse": NSNumber(value: _playerItem.canPlayReverse),
@ -1269,7 +1288,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
"textTracks": self._textTracks?.compactMap { $0.json } ?? textTracks.map(\.json), "textTracks": self._textTracks?.compactMap { $0.json } ?? textTracks.map(\.json),
"target": self.reactTag as Any]) "target": self.reactTag as Any])
} }
}
self._videoLoadStarted = false self._videoLoadStarted = false
self._playerObserver.attachPlayerEventListeners() self._playerObserver.attachPlayerEventListeners()
self.applyModifiers() self.applyModifiers()
@ -1432,7 +1451,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
func handleTracksChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<[AVPlayerItemTrack]>) { func handleTracksChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<[AVPlayerItemTrack]>) {
all(RCTVideoUtils.getAudioTrackInfo(self._player), RCTVideoUtils.getTextTrackInfo(self._player)).then { audioTracks, textTracks in Task {
let audioTracks = await RCTVideoUtils.getAudioTrackInfo(self._player)
let textTracks = await RCTVideoUtils.getTextTrackInfo(self._player)
self.onTextTracks?(["textTracks": textTracks]) self.onTextTracks?(["textTracks": textTracks])
self.onAudioTracks?(["audioTracks": audioTracks]) self.onAudioTracks?(["audioTracks": audioTracks])
} }

View File

@ -1,11 +1,10 @@
import AVFoundation import AVFoundation
import DVAssetLoaderDelegate import DVAssetLoaderDelegate
import Foundation import Foundation
import Promises
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance() private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance()
var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) -> Promise<AVPlayerItem>)? var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) async -> AVPlayerItem)?
override init() { override init() {
super.init() super.init()
@ -26,39 +25,40 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
return false return false
} }
func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) -> Promise<AVPlayerItem> { func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) async throws -> AVPlayerItem {
let url = URL(string: uri) let url = URL(string: uri)
return getItemForUri(uri) let (videoCacheStatus, cachedAsset) = await getItemForUri(uri)
.then { [weak self] (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) -> Promise<AVPlayerItem> in
guard let self, let playerItemPrepareText = self.playerItemPrepareText else { throw NSError(domain: "", code: 0, userInfo: nil) } guard let playerItemPrepareText else {
throw NSError(domain: "", code: 0, userInfo: nil)
}
switch videoCacheStatus { switch videoCacheStatus {
case .missingFileExtension: case .missingFileExtension:
DebugLog(""" DebugLog("""
Could not generate cache key for uri '\(uri)'. Could not generate cache key for uri '\(uri ?? "NO_URI")'.
It is currently not supported to cache urls that do not include a file extension. It is currently not supported to cache urls that do not include a file extension.
The video file will not be cached. The video file will not be cached.
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md 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]) let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
return playerItemPrepareText(asset, options, "") return await playerItemPrepareText(asset, options, "")
case .unsupportedFileExtension: case .unsupportedFileExtension:
DebugLog(""" DebugLog("""
Could not generate cache key for uri '\(uri)'. Could not generate cache key for uri '\(uri ?? "NO_URI")'.
The file extension of that uri is currently not supported. The file extension of that uri is currently not supported.
The video file will not be cached. The video file will not be cached.
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md 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]) let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
return playerItemPrepareText(asset, options, "") return await playerItemPrepareText(asset, options, "")
default: default:
if let cachedAsset { if let cachedAsset {
DebugLog("Playing back uri '\(uri)' from cache") DebugLog("Playing back uri '\(uri ?? "NO_URI")' from cache")
// See note in playerItemForSource about not being able to support text tracks & caching // See note in playerItemForSource about not being able to support text tracks & caching
return Promise { return AVPlayerItem(asset: cachedAsset)
AVPlayerItem(asset: cachedAsset)
}
} }
} }
@ -77,18 +77,13 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main) asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
*/ */
return Promise { return AVPlayerItem(asset: asset)
AVPlayerItem(asset: asset)
}
}.then { playerItem -> AVPlayerItem in
return playerItem
}
} }
func getItemForUri(_ uri: String) -> Promise<(videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?)> { func getItemForUri(_ uri: String) async -> (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) {
return Promise<(videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?)> { fulfill, _ in await withCheckedContinuation { continuation in
self._videoCache.getItemForUri(uri, withCallback: { (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) in self._videoCache.getItemForUri(uri, withCallback: { (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) in
fulfill((videoCacheStatus, cachedAsset)) continuation.resume(returning: (videoCacheStatus, cachedAsset))
}) })
} }
} }

View File

@ -1,41 +0,0 @@
Pod::Spec.new do |s|
s.name = 'PromisesObjC'
s.version = '2.3.1.1'
s.authors = 'Google Inc.'
s.license = { :type => 'Apache-2.0', :file => 'LICENSE' }
s.homepage = 'https://github.com/google/promises'
s.source = { :git => 'https://github.com/google/promises.git', :tag => '2.3.1' }
s.summary = 'Synchronization construct for Objective-C'
s.description = <<-DESC
Promises is a modern framework that provides a synchronization construct for
Objective-C to facilitate writing asynchronous code.
DESC
s.platforms = { :ios => '9.0', :osx => '10.11', :tvos => '9.0', :watchos => '2.0', :visionos => '1.0' }
s.module_name = 'FBLPromises'
s.prefix_header_file = false
s.header_dir = "./"
s.public_header_files = "Sources/#{s.module_name}/include/**/*.h"
s.private_header_files = "Sources/#{s.module_name}/include/FBLPromisePrivate.h"
s.source_files = "Sources/#{s.module_name}/**/*.{h,m}"
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES'
}
s.test_spec 'Tests' do |ts|
# Note: Omits watchOS as a workaround since XCTest is not available to watchOS for now.
# Reference: https://github.com/CocoaPods/CocoaPods/issues/8283, https://github.com/CocoaPods/CocoaPods/issues/4185.
ts.platforms = {:ios => nil, :osx => nil, :tvos => nil}
ts.source_files = "Tests/#{s.module_name}Tests/*.m",
"Sources/#{s.module_name}TestHelpers/include/#{s.module_name}TestHelpers.h"
end
s.test_spec 'PerformanceTests' do |ts|
# Note: Omits watchOS as a workaround since XCTest is not available to watchOS for now.
# Reference: https://github.com/CocoaPods/CocoaPods/issues/8283, https://github.com/CocoaPods/CocoaPods/issues/4185.
ts.platforms = {:ios => nil, :osx => nil, :tvos => nil}
ts.source_files = "Tests/#{s.module_name}PerformanceTests/*.m",
"Sources/#{s.module_name}TestHelpers/include/#{s.module_name}TestHelpers.h"
end
end

View File

@ -1,21 +0,0 @@
Pod::Spec.new do |s|
s.name = 'PromisesSwift'
s.version = '2.3.1.1'
s.authors = 'Google Inc.'
s.license = { :type => 'Apache-2.0', :file => 'LICENSE' }
s.homepage = 'https://github.com/google/promises'
s.source = { :git => 'https://github.com/google/promises.git', :tag => '2.3.1' }
s.summary = 'Synchronization construct for Swift'
s.description = <<-DESC
Promises is a modern framework that provides a synchronization construct for
Swift to facilitate writing asynchronous code.
DESC
s.platforms = { :ios => '9.0', :osx => '10.11', :tvos => '9.0', :watchos => '2.0', :visionos => '1.0' }
s.swift_versions = ['5.0', '5.2']
s.module_name = 'Promises'
s.source_files = "Sources/#{s.module_name}/*.{swift}"
s.dependency 'PromisesObjC', "#{s.version}"
end

View File

@ -14,11 +14,10 @@ Pod::Spec.new do |s|
s.homepage = 'https://github.com/react-native-video/react-native-video' s.homepage = 'https://github.com/react-native-video/react-native-video'
s.source = { :git => "https://github.com/react-native-video/react-native-video.git", :tag => "v#{s.version}" } s.source = { :git => "https://github.com/react-native-video/react-native-video.git", :tag => "v#{s.version}" }
s.platforms = { :ios => "9.0", :tvos => "10.0", :visionos => "1.0" } s.platforms = { :ios => "13.0", :tvos => "13.0", :visionos => "1.0" }
s.subspec "Video" do |ss| s.subspec "Video" do |ss|
ss.source_files = "ios/Video/**/*.{h,m,swift}" ss.source_files = "ios/Video/**/*.{h,m,swift}"
ss.dependency "PromisesSwift"
if defined?($RNVideoUseGoogleIMA) if defined?($RNVideoUseGoogleIMA)
Pod::UI.puts "RNVideo: enable IMA SDK" Pod::UI.puts "RNVideo: enable IMA SDK"