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
15 changed files with 686 additions and 785 deletions

View File

@@ -1,6 +1,5 @@
import AVFoundation
import MediaAccessibility
import Promises
let RCTVideoUnset = -1
@@ -10,187 +9,184 @@ let RCTVideoUnset = -1
* Collection of mutating functions
*/
enum RCTPlayerOperations {
static func setSideloadedText(player: AVPlayer?, textTracks: [TextTrack], criteria: SelectedTrackCriteria?) -> Promise<Void> {
return Promise {
let type = criteria?.type
static func setSideloadedText(player: AVPlayer?, textTracks: [TextTrack], criteria: SelectedTrackCriteria?) {
let type = criteria?.type
let trackCount: Int! = player?.currentItem?.tracks.count ?? 0
let trackCount: Int! = player?.currentItem?.tracks.count ?? 0
// The first few tracks will be audio & video track
var firstTextIndex = 0
for i in 0 ..< trackCount where player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
firstTextIndex = i
break
// The first few tracks will be audio & video track
var firstTextIndex = 0
for i in 0 ..< trackCount where player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
firstTextIndex = i
break
}
var selectedTrackIndex: Int = RCTVideoUnset
if type == "disabled" {
// Select the last text index which is the disabled text track
selectedTrackIndex = trackCount - firstTextIndex
} else if type == "language" {
let selectedValue = criteria?.value as? String
for i in 0 ..< textTracks.count {
let currentTextTrack = textTracks[i]
if selectedValue == currentTextTrack.language {
selectedTrackIndex = i
break
}
}
} else if type == "title" {
let selectedValue = criteria?.value as? String
for i in 0 ..< textTracks.count {
let currentTextTrack = textTracks[i]
if selectedValue == currentTextTrack.title {
selectedTrackIndex = i
break
}
}
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
if textTracks.count > index {
selectedTrackIndex = index
}
}
}
var selectedTrackIndex: Int = RCTVideoUnset
if type == "disabled" {
// Select the last text index which is the disabled text track
selectedTrackIndex = trackCount - firstTextIndex
} else if type == "language" {
let selectedValue = criteria?.value as? String
// 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)
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 ..< textTracks.count {
let currentTextTrack = textTracks[i]
if selectedValue == currentTextTrack.language {
if systemLanguage == currentTextTrack.language {
selectedTrackIndex = i
break
}
}
} else if type == "title" {
let selectedValue = criteria?.value as? String
for i in 0 ..< textTracks.count {
let currentTextTrack = textTracks[i]
if selectedValue == currentTextTrack.title {
selectedTrackIndex = i
break
}
}
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
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 != "disabled") && selectedTrackIndex == RCTVideoUnset {
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user)
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 ..< textTracks.count {
let currentTextTrack = textTracks[i]
if systemLanguage == currentTextTrack.language {
selectedTrackIndex = i
break
}
}
}
}
for i in firstTextIndex ..< trackCount {
var isEnabled = false
if selectedTrackIndex != RCTVideoUnset {
isEnabled = i == selectedTrackIndex + firstTextIndex
}
player?.currentItem?.tracks[i].isEnabled = isEnabled
for i in firstTextIndex ..< trackCount {
var isEnabled = false
if selectedTrackIndex != RCTVideoUnset {
isEnabled = i == selectedTrackIndex + firstTextIndex
}
player?.currentItem?.tracks[i].isEnabled = isEnabled
}
}
// UNUSED
static func setStreamingText(player: AVPlayer?, criteria: SelectedTrackCriteria?) {
static func setStreamingText(player: AVPlayer?, criteria: SelectedTrackCriteria?) async {
let type = criteria?.type
var mediaOption: AVMediaSelectionOption!
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: .legible).then { group in
guard let group else { return }
guard let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: .legible) else {
return
}
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 ..< group.options.count {
let currentOption: AVMediaSelectionOption! = group.options[i]
var optionValue: String!
if type == "language" {
optionValue = currentOption.extendedLanguageTag
} else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as! String
}
if value == optionValue {
mediaOption = currentOption
break
}
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 ..< group.options.count {
let currentOption: AVMediaSelectionOption! = group.options[i]
var optionValue: String!
if type == "language" {
optionValue = currentOption.extendedLanguageTag
} else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as! String
}
// } else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
if group.options.count > index {
mediaOption = group.options[index]
}
if value == optionValue {
mediaOption = currentOption
break
}
} else { // default. invalid type or "system"
#if os(tvOS)
// Do noting. Fix for tvOS native audio menu language selector
#else
player?.currentItem?.selectMediaOptionAutomatically(in: group)
return
#endif
}
// } else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
if group.options.count > index {
mediaOption = group.options[index]
}
}
} else { // default. invalid type or "system"
#if os(tvOS)
// Do noting. Fix for tvOS native audio menu language selector
#else
// 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?.selectMediaOptionAutomatically(in: group)
return
#endif
}
#if os(tvOS)
// Do noting. Fix for tvOS native audio menu language selector
#else
// If a match isn't found, option will be nil and text tracks will be disabled
await player?.currentItem?.select(mediaOption, in: group)
#endif
}
static func setMediaSelectionTrackForCharacteristic(player: AVPlayer?, characteristic: AVMediaCharacteristic, criteria: SelectedTrackCriteria?) {
static func setMediaSelectionTrackForCharacteristic(player: AVPlayer?, characteristic: AVMediaCharacteristic, criteria: SelectedTrackCriteria?) async {
let type = criteria?.type
var mediaOption: AVMediaSelectionOption!
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: characteristic).then { group in
guard let group else { return }
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 ..< group.options.count {
let currentOption: AVMediaSelectionOption! = group.options[i]
var optionValue: String!
if type == "language" {
optionValue = currentOption.extendedLanguageTag
} else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as? String
}
if value == optionValue {
mediaOption = currentOption
break
}
}
// } else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
if group.options.count > 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)
guard let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: player?.currentItem?.asset, for: characteristic) else {
return
}
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 ..< group.options.count {
let currentOption: AVMediaSelectionOption! = group.options[i]
var optionValue: String!
if type == "language" {
optionValue = currentOption.extendedLanguageTag
} else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as? String
}
if value == optionValue {
mediaOption = currentOption
break
}
}
// } else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
if group.options.count > index {
mediaOption = group.options[index]
}
}
} else { // default. invalid type or "system"
await player?.currentItem?.selectMediaOptionAutomatically(in: group)
return
}
// If a match isn't found, option will be nil and text tracks will be disabled
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 cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
let current: CMTime = playerItem.currentTime()
let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
return Promise<Bool>(on: .global()) { fulfill, reject in
guard CMTimeCompare(current, cmSeekTime) != 0 else {
reject(NSError(domain: "", code: 0, userInfo: nil))
return
}
if !paused { player.pause() }
player.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { (finished: Bool) in
fulfill(finished)
})
guard CMTimeCompare(current, cmSeekTime) != 0 else {
// skip if there is no diff in current time and seek time
return
}
if !paused { player.pause() }
player.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { (finished: Bool) in
completion(finished)
})
}
static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) {

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import AVFoundation
import Photos
import Promises
// MARK: - RCTVideoAssetsUtils
@@ -8,30 +7,22 @@ enum RCTVideoAssetsUtils {
static func getMediaSelectionGroup(
asset: AVAsset?,
for mediaCharacteristic: AVMediaCharacteristic
) -> Promise<AVMediaSelectionGroup?> {
) async -> AVMediaSelectionGroup? {
if #available(iOS 15, tvOS 15, visionOS 1.0, *) {
return wrap { handler in
asset?.loadMediaSelectionGroup(for: mediaCharacteristic, completionHandler: handler)
}
return try? await asset?.loadMediaSelectionGroup(for: mediaCharacteristic)
} else {
#if !os(visionOS)
return Promise { fulfill, _ in
fulfill(asset?.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic))
}
return asset?.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic)
#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, *) {
return wrap { handler in
asset.loadTracks(withMediaType: withMediaType, completionHandler: handler)
}
return try? await asset.loadTracks(withMediaType: withMediaType)
} else {
#if !os(visionOS)
return Promise { fulfill, _ in
fulfill(asset.tracks(withMediaType: withMediaType))
}
return asset.tracks(withMediaType: withMediaType)
#endif
}
}
@@ -131,73 +122,67 @@ enum RCTVideoUtils {
return 0
}
static func getAudioTrackInfo(_ player: AVPlayer?) -> Promise<[AnyObject]> {
return Promise { fulfill, _ in
guard let player, let asset = player.currentItem?.asset else {
fulfill([])
return
}
let audioTracks: NSMutableArray! = NSMutableArray()
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .audible).then { group in
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 selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
let audioTrack = [
"index": NSNumber(value: i),
"title": title,
"language": language ?? "",
"selected": currentOption?.displayName == selectedOption?.displayName,
] as [String: Any]
audioTracks.add(audioTrack)
}
fulfill(audioTracks as [AnyObject])
}
static func getAudioTrackInfo(_ player: AVPlayer?) async -> [AnyObject] {
guard let player, let asset = player.currentItem?.asset else {
return []
}
let audioTracks: NSMutableArray! = NSMutableArray()
let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .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 selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
let audioTrack = [
"index": NSNumber(value: i),
"title": title,
"language": language ?? "",
"selected": currentOption?.displayName == selectedOption?.displayName,
] as [String: Any]
audioTracks.add(audioTrack)
}
return audioTracks as [AnyObject]
}
static func getTextTrackInfo(_ player: AVPlayer?) -> Promise<[TextTrack]> {
return Promise { fulfill, _ in
guard let player, let asset = player.currentItem?.asset else {
fulfill([])
return
}
// if streaming video, we extract the text tracks
var textTracks: [TextTrack] = []
RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .legible).then { group in
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 selectedOpt = player.currentItem?.currentMediaSelection
let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
let textTrack = TextTrack([
"index": NSNumber(value: i),
"title": title,
"language": language,
"selected": currentOption?.displayName == selectedOption?.displayName,
])
textTracks.append(textTrack)
}
fulfill(textTracks)
}
static func getTextTrackInfo(_ player: AVPlayer?) async -> [TextTrack] {
guard let player, let asset = player.currentItem?.asset else {
return []
}
// if streaming video, we extract the text tracks
var textTracks: [TextTrack] = []
let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .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 selectedOpt = player.currentItem?.currentMediaSelection
let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
let textTrack = TextTrack([
"index": NSNumber(value: i),
"title": title,
"language": language,
"selected": currentOption?.displayName == selectedOption?.displayName,
])
textTracks.append(textTrack)
}
return textTracks
}
// UNUSED
@@ -226,111 +211,96 @@ enum RCTVideoUtils {
return Data(base64Encoded: adoptURL.absoluteString)
}
static func generateMixComposition(_ asset: AVAsset) -> Promise<AVMutableComposition> {
return Promise { fulfill, _ in
all(
RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video),
RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .audio)
).then { tracks in
let mixComposition = AVMutableComposition()
static func generateMixComposition(_ asset: AVAsset) async -> AVMutableComposition {
let videoTracks = await RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video)
let audioTracks = await RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .audio)
if let videoAsset = tracks.0?.first, let audioAsset = tracks.1?.first {
let videoCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
withMediaType: AVMediaType.video,
preferredTrackID: kCMPersistentTrackID_Invalid
)
try? videoCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
of: videoAsset,
at: .zero
)
let mixComposition = AVMutableComposition()
let audioCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
withMediaType: AVMediaType.audio,
preferredTrackID: kCMPersistentTrackID_Invalid
)
if let videoAsset = videoTracks?.first, let audioAsset = audioTracks?.first {
let videoCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
withMediaType: AVMediaType.video,
preferredTrackID: kCMPersistentTrackID_Invalid
)
try? videoCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
of: videoAsset,
at: .zero
)
try? audioCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: audioAsset.timeRange.duration),
of: audioAsset,
at: .zero
)
let audioCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
withMediaType: AVMediaType.audio,
preferredTrackID: kCMPersistentTrackID_Invalid
)
fulfill(mixComposition)
} else {
fulfill(mixComposition)
}
}
try? audioCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: audioAsset.timeRange.duration),
of: audioAsset,
at: .zero
)
}
return mixComposition
}
static func getValidTextTracks(asset: AVAsset, assetOptions: NSDictionary?, mixComposition: AVMutableComposition,
textTracks: [TextTrack]?) -> Promise<[TextTrack]> {
textTracks: [TextTrack]?) async -> [TextTrack] {
var validTextTracks: [TextTrack] = []
var queue: [Promise<[AVAssetTrack]?>] = []
var tracks: [[AVAssetTrack]] = []
return Promise { fulfill, _ in
RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video).then { tracks in
guard let videoAsset = tracks?.first else {
return
let videoTracks = await RCTVideoAssetsUtils.getTracks(asset: asset, withMediaType: .video)
guard let videoAsset = videoTracks?.first else { return validTextTracks }
if let textTracks, !textTracks.isEmpty {
for textTrack in textTracks {
var textURLAsset: AVURLAsset!
let textUri: String = textTrack.uri
if textUri.lowercased().hasPrefix("http") {
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options: (assetOptions as! [String: Any]))
} else {
let isDisabledTrack: Bool! = textTrack.type == "disabled"
let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory
textURLAsset = AVURLAsset(
url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL,
options: nil
)
}
if let textTracks, !textTracks.isEmpty {
for track in textTracks {
var textURLAsset: AVURLAsset!
let textUri: String = track.uri
if textUri.lowercased().hasPrefix("http") {
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options: (assetOptions as! [String: Any]))
} else {
let isDisabledTrack: Bool! = track.type == "disabled"
let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory
textURLAsset = AVURLAsset(
url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL,
options: nil
)
}
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 {
guard let track = tracks[i]?.first else { continue } // fix when there's no textTrackAsset
for i in 0 ..< tracks.count {
guard let track = tracks[i].first else { continue } // fix when there's no textTrackAsset
let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
preferredTrackID: kCMPersistentTrackID_Invalid)
let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
preferredTrackID: kCMPersistentTrackID_Invalid)
do {
try textCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
of: track,
at: .zero
)
validTextTracks.append(textTracks[i])
} catch {
// TODO: upgrade error by call some props callback to better inform user
print("Error occurred on textTrack insert attempt: \(error.localizedDescription)")
continue
}
}
}
return
}.then {
if !validTextTracks.isEmpty {
let emptyVttFile: TextTrack? = self.createEmptyVttFile()
if emptyVttFile != nil {
validTextTracks.append(emptyVttFile!)
}
}
fulfill(validTextTracks)
do {
try textCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
of: track,
at: .zero
)
validTextTracks.append(textTracks[i])
} catch {
// TODO: upgrade error by call some props callback to better inform user
print("Error occurred on textTrack insert attempt: \(error.localizedDescription)")
continue
}
}
}
if !validTextTracks.isEmpty {
let emptyVttFile: TextTrack? = self.createEmptyVttFile()
if emptyVttFile != nil {
validTextTracks.append(emptyVttFile!)
}
}
return validTextTracks
}
/*
@@ -362,25 +332,26 @@ enum RCTVideoUtils {
])
}
static func delay(seconds: Int = 0) -> Promise<Void> {
return Promise<Void>(on: .global()) { fulfill, _ in
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds)) {
fulfill(())
static func delay(seconds: Int = 0, completion: @escaping () async throws -> Void) {
return DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds)) {
Task.detached(priority: .userInitiated) {
try await completion()
}
}
}
static func preparePHAsset(uri: String) -> Promise<AVAsset?> {
return Promise<AVAsset?>(on: .global()) { fulfill, reject in
let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...])
guard let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else {
reject(NSError(domain: "", code: 0, userInfo: nil))
return
}
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
static func preparePHAsset(uri: String) async -> AVAsset? {
let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...])
guard let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject else {
return nil
}
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
return await withCheckedContinuation { continuation 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, *) {
return wrap { handler in
AVVideoComposition.videoComposition(with: asset, applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in
return try? await AVVideoComposition.videoComposition(
with: asset,
applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in
if filter == nil {
request.finish(with: request.sourceImage, context: nil)
} else {
@@ -456,25 +428,23 @@ enum RCTVideoUtils {
let output: CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent)
request.finish(with: output, context: nil)
}
}, completionHandler: handler)
}
}
)
} else {
#if !os(visionOS)
return Promise { fulfill, _ in
fulfill(AVVideoComposition(
asset: 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)
}
return AVVideoComposition(
asset: 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)
}
))
}
}
)
#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()
}
}
}
}