Merge remote-tracking branch 'upstream/master'
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
deploy docs / deploy-docs (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
deploy docs / deploy-docs (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This commit is contained in:
18
ios/Video/DataStructures/AdParams.swift
Normal file
18
ios/Video/DataStructures/AdParams.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
struct AdParams {
|
||||
let adTagUrl: String?
|
||||
let adLanguage: String?
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
init(_ json: NSDictionary!) {
|
||||
guard json != nil else {
|
||||
self.json = nil
|
||||
adTagUrl = nil
|
||||
adLanguage = nil
|
||||
return
|
||||
}
|
||||
self.json = json
|
||||
adTagUrl = json["adTagUrl"] as? String
|
||||
adLanguage = json["adLanguage"] as? String
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ struct DRMParams {
|
||||
let contentId: String?
|
||||
let certificateUrl: String?
|
||||
let base64Certificate: Bool?
|
||||
let localSourceEncryptionKeyScheme: String?
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
@@ -17,6 +18,7 @@ struct DRMParams {
|
||||
self.certificateUrl = nil
|
||||
self.base64Certificate = nil
|
||||
self.headers = nil
|
||||
self.localSourceEncryptionKeyScheme = nil
|
||||
return
|
||||
}
|
||||
self.json = json
|
||||
@@ -36,5 +38,6 @@ struct DRMParams {
|
||||
} else {
|
||||
self.headers = nil
|
||||
}
|
||||
localSourceEncryptionKeyScheme = json["localSourceEncryptionKeyScheme"] as? String
|
||||
}
|
||||
}
|
||||
|
@@ -15,4 +15,8 @@ struct SelectedTrackCriteria {
|
||||
self.type = json["type"] as? String ?? ""
|
||||
self.value = json["value"] as? String
|
||||
}
|
||||
|
||||
static func none() -> SelectedTrackCriteria {
|
||||
return SelectedTrackCriteria(["type": "none", "value": ""])
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,9 @@ struct VideoSource {
|
||||
let cropEnd: Int64?
|
||||
let customMetadata: CustomMetadata?
|
||||
/* DRM */
|
||||
let drm: DRMParams?
|
||||
let drm: DRMParams
|
||||
var textTracks: [TextTrack] = []
|
||||
let adParams: AdParams
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
@@ -27,7 +29,8 @@ struct VideoSource {
|
||||
self.cropStart = nil
|
||||
self.cropEnd = nil
|
||||
self.customMetadata = nil
|
||||
self.drm = nil
|
||||
self.drm = DRMParams(nil)
|
||||
adParams = AdParams(nil)
|
||||
return
|
||||
}
|
||||
self.json = json
|
||||
@@ -52,5 +55,9 @@ struct VideoSource {
|
||||
self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) }
|
||||
self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary)
|
||||
self.drm = DRMParams(json["drm"] as? NSDictionary)
|
||||
self.textTracks = (json["textTracks"] as? NSArray)?.map { trackDict in
|
||||
return TextTrack(trackDict as? NSDictionary)
|
||||
} ?? []
|
||||
adParams = AdParams(json["ad"] as? NSDictionary)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// DRMManager+AVContentKeySessionDelegate.swift
|
||||
// react-native-video
|
||||
//
|
||||
// Created by Krzysztof Moch on 14/08/2024.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
|
||||
extension DRMManager: AVContentKeySessionDelegate {
|
||||
func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {
|
||||
handleContentKeyRequest(keyRequest: keyRequest)
|
||||
}
|
||||
|
||||
func contentKeySession(_: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) {
|
||||
handleContentKeyRequest(keyRequest: keyRequest)
|
||||
}
|
||||
|
||||
func contentKeySession(_: AVContentKeySession, shouldRetry _: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool {
|
||||
let retryReasons: [AVContentKeyRequest.RetryReason] = [
|
||||
.timedOut,
|
||||
.receivedResponseWithExpiredLease,
|
||||
.receivedObsoleteContentKey,
|
||||
]
|
||||
return retryReasons.contains(retryReason)
|
||||
}
|
||||
|
||||
func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) {
|
||||
Task {
|
||||
do {
|
||||
try await handlePersistableKeyRequest(keyRequest: keyRequest)
|
||||
} catch {
|
||||
handleError(error, for: keyRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError error: Error) {
|
||||
DebugLog(String(describing: error))
|
||||
}
|
||||
}
|
68
ios/Video/Features/DRMManager+OnGetLicense.swift
Normal file
68
ios/Video/Features/DRMManager+OnGetLicense.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// DRMManager+OnGetLicense.swift
|
||||
// react-native-video
|
||||
//
|
||||
// Created by Krzysztof Moch on 14/08/2024.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
|
||||
extension DRMManager {
|
||||
func requestLicenseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) async throws {
|
||||
guard let onGetLicense else {
|
||||
throw RCTVideoError.noDataFromLicenseRequest
|
||||
}
|
||||
|
||||
guard let licenseServerUrl = drmParams?.licenseServer, !licenseServerUrl.isEmpty else {
|
||||
throw RCTVideoError.noLicenseServerURL
|
||||
}
|
||||
|
||||
guard let loadedLicenseUrl = keyRequest.identifier as? String else {
|
||||
throw RCTVideoError.invalidContentId
|
||||
}
|
||||
|
||||
pendingLicenses[loadedLicenseUrl] = keyRequest
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
onGetLicense([
|
||||
"licenseUrl": licenseServerUrl,
|
||||
"loadedLicenseUrl": loadedLicenseUrl,
|
||||
"contentId": assetId,
|
||||
"spcBase64": spcData.base64EncodedString(),
|
||||
"target": self?.reactTag as Any,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func setJSLicenseResult(license: String, licenseUrl: String) {
|
||||
guard let keyContentRequest = pendingLicenses[licenseUrl] else {
|
||||
setJSLicenseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl)
|
||||
return
|
||||
}
|
||||
|
||||
guard let responseData = Data(base64Encoded: license) else {
|
||||
setJSLicenseError(error: "Invalid license data", licenseUrl: licenseUrl)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, license: responseData)
|
||||
pendingLicenses.removeValue(forKey: licenseUrl)
|
||||
} catch {
|
||||
handleError(error, for: keyContentRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func setJSLicenseError(error: String, licenseUrl: String) {
|
||||
let rctError = RCTVideoError.fromJSPart(error)
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onVideoError?([
|
||||
"error": RCTVideoErrorHandler.createError(from: rctError),
|
||||
"target": self?.reactTag as Any,
|
||||
])
|
||||
}
|
||||
|
||||
pendingLicenses.removeValue(forKey: licenseUrl)
|
||||
}
|
||||
}
|
34
ios/Video/Features/DRMManager+Persitable.swift
Normal file
34
ios/Video/Features/DRMManager+Persitable.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// DRMManager+Persitable.swift
|
||||
// react-native-video
|
||||
//
|
||||
// Created by Krzysztof Moch on 19/08/2024.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
|
||||
extension DRMManager {
|
||||
func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) async throws {
|
||||
if let localSourceEncryptionKeyScheme = drmParams?.localSourceEncryptionKeyScheme {
|
||||
try handleEmbeddedKey(keyRequest: keyRequest, scheme: localSourceEncryptionKeyScheme)
|
||||
} else {
|
||||
// Offline DRM is not supported yet - if you need it please check out the following issue:
|
||||
// https://github.com/TheWidlarzGroup/react-native-video/issues/3539
|
||||
throw RCTVideoError.offlineDRMNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEmbeddedKey(keyRequest: AVPersistableContentKeyRequest, scheme: String) throws {
|
||||
guard let uri = keyRequest.identifier as? String,
|
||||
let url = URL(string: uri) else {
|
||||
throw RCTVideoError.invalidContentId
|
||||
}
|
||||
|
||||
guard let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: scheme) else {
|
||||
throw RCTVideoError.embeddedKeyExtractionFailed
|
||||
}
|
||||
|
||||
let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData)
|
||||
try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: persistentKey)
|
||||
}
|
||||
}
|
213
ios/Video/Features/DRMManager.swift
Normal file
213
ios/Video/Features/DRMManager.swift
Normal file
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// DRMManager.swift
|
||||
// react-native-video
|
||||
//
|
||||
// Created by Krzysztof Moch on 13/08/2024.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
|
||||
class DRMManager: NSObject {
|
||||
static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue")
|
||||
let contentKeySession: AVContentKeySession?
|
||||
|
||||
var drmParams: DRMParams?
|
||||
var reactTag: NSNumber?
|
||||
var onVideoError: RCTDirectEventBlock?
|
||||
var onGetLicense: RCTDirectEventBlock?
|
||||
|
||||
// Licenses handled by onGetLicense (from JS side)
|
||||
var pendingLicenses: [String: AVContentKeyRequest] = [:]
|
||||
|
||||
override init() {
|
||||
#if targetEnvironment(simulator)
|
||||
contentKeySession = nil
|
||||
super.init()
|
||||
#else
|
||||
contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming)
|
||||
super.init()
|
||||
|
||||
contentKeySession?.setDelegate(self, queue: DRMManager.queue)
|
||||
#endif
|
||||
}
|
||||
|
||||
func createContentKeyRequest(
|
||||
asset: AVContentKeyRecipient,
|
||||
drmParams: DRMParams?,
|
||||
reactTag: NSNumber?,
|
||||
onVideoError: RCTDirectEventBlock?,
|
||||
onGetLicense: RCTDirectEventBlock?
|
||||
) {
|
||||
self.reactTag = reactTag
|
||||
self.onVideoError = onVideoError
|
||||
self.onGetLicense = onGetLicense
|
||||
self.drmParams = drmParams
|
||||
|
||||
if drmParams?.type != "fairplay" {
|
||||
self.onVideoError?([
|
||||
"error": RCTVideoErrorHandler.createError(from: RCTVideoError.unsupportedDRMType),
|
||||
"target": self.reactTag as Any,
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
DebugLog("Simulator is not supported for FairPlay DRM.")
|
||||
self.onVideoError?([
|
||||
"error": RCTVideoErrorHandler.createError(from: RCTVideoError.simulatorDRMNotSupported),
|
||||
"target": self.reactTag as Any,
|
||||
])
|
||||
#endif
|
||||
|
||||
contentKeySession?.addContentKeyRecipient(asset)
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func handleContentKeyRequest(keyRequest: AVContentKeyRequest) {
|
||||
Task {
|
||||
do {
|
||||
if drmParams?.localSourceEncryptionKeyScheme != nil {
|
||||
#if os(iOS)
|
||||
try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError()
|
||||
return
|
||||
#else
|
||||
throw RCTVideoError.offlineDRMNotSupported
|
||||
#endif
|
||||
}
|
||||
|
||||
try await processContentKeyRequest(keyRequest: keyRequest)
|
||||
} catch {
|
||||
handleError(error, for: keyRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws {
|
||||
let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: license)
|
||||
keyRequest.processContentKeyResponse(keyResponse)
|
||||
}
|
||||
|
||||
func handleError(_ error: Error, for keyRequest: AVContentKeyRequest) {
|
||||
let rctError: RCTVideoError
|
||||
if let videoError = error as? RCTVideoError {
|
||||
// handle RCTVideoError errors
|
||||
rctError = videoError
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onVideoError?([
|
||||
"error": RCTVideoErrorHandler.createError(from: rctError),
|
||||
"target": self?.reactTag as Any,
|
||||
])
|
||||
}
|
||||
} else {
|
||||
let err = error as NSError
|
||||
|
||||
// handle Other errors
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onVideoError?([
|
||||
"error": [
|
||||
"code": err.code,
|
||||
"localizedDescription": err.localizedDescription,
|
||||
"localizedFailureReason": err.localizedFailureReason ?? "",
|
||||
"localizedRecoverySuggestion": err.localizedRecoverySuggestion ?? "",
|
||||
"domain": err.domain,
|
||||
],
|
||||
"target": self?.reactTag as Any,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
keyRequest.processContentKeyResponseError(error)
|
||||
contentKeySession?.expire()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws {
|
||||
guard let assetId = getAssetId(keyRequest: keyRequest),
|
||||
let assetIdData = assetId.data(using: .utf8) else {
|
||||
throw RCTVideoError.invalidContentId
|
||||
}
|
||||
|
||||
let appCertificate = try await requestApplicationCertificate()
|
||||
let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificate, contentIdentifier: assetIdData)
|
||||
|
||||
if onGetLicense != nil {
|
||||
try await requestLicenseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest)
|
||||
} else {
|
||||
let license = try await requestLicense(spcData: spcData)
|
||||
try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: license)
|
||||
}
|
||||
}
|
||||
|
||||
private func requestApplicationCertificate() async throws -> Data {
|
||||
guard let urlString = drmParams?.certificateUrl,
|
||||
let url = URL(string: urlString) else {
|
||||
throw RCTVideoError.noCertificateURL
|
||||
}
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
httpResponse.statusCode == 200 else {
|
||||
throw RCTVideoError.noCertificateData
|
||||
}
|
||||
|
||||
if drmParams?.base64Certificate == true {
|
||||
guard let certData = Data(base64Encoded: data) else {
|
||||
throw RCTVideoError.noCertificateData
|
||||
}
|
||||
return certData
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private func requestLicense(spcData: Data) async throws -> Data {
|
||||
guard let licenseServerUrlString = drmParams?.licenseServer,
|
||||
let licenseServerUrl = URL(string: licenseServerUrlString) else {
|
||||
throw RCTVideoError.noLicenseServerURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: licenseServerUrl)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = spcData
|
||||
|
||||
if let headers = drmParams?.headers {
|
||||
for (key, value) in headers {
|
||||
if let stringValue = value as? String {
|
||||
request.setValue(stringValue, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw RCTVideoError.licenseRequestFailed(0)
|
||||
}
|
||||
|
||||
guard httpResponse.statusCode == 200 else {
|
||||
throw RCTVideoError.licenseRequestFailed(httpResponse.statusCode)
|
||||
}
|
||||
|
||||
guard !data.isEmpty else {
|
||||
throw RCTVideoError.noDataFromLicenseRequest
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
private func getAssetId(keyRequest: AVContentKeyRequest) -> String? {
|
||||
if let assetId = drmParams?.contentId {
|
||||
return assetId
|
||||
}
|
||||
|
||||
if let url = keyRequest.identifier as? String {
|
||||
return url.replacingOccurrences(of: "skd://", with: "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
@@ -4,22 +4,27 @@
|
||||
|
||||
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate {
|
||||
private weak var _video: RCTVideo?
|
||||
private var _pipEnabled: () -> Bool
|
||||
private var _isPictureInPictureActive: () -> Bool
|
||||
|
||||
/* Entry point for the SDK. Used to make ad requests. */
|
||||
private var adsLoader: IMAAdsLoader!
|
||||
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */
|
||||
private var adsManager: IMAAdsManager!
|
||||
|
||||
init(video: RCTVideo!, pipEnabled: @escaping () -> Bool) {
|
||||
init(video: RCTVideo!, isPictureInPictureActive: @escaping () -> Bool) {
|
||||
_video = video
|
||||
_pipEnabled = pipEnabled
|
||||
_isPictureInPictureActive = isPictureInPictureActive
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func setUpAdsLoader() {
|
||||
adsLoader = IMAAdsLoader(settings: nil)
|
||||
guard let _video else { return }
|
||||
let settings = IMASettings()
|
||||
if let adLanguage = _video.getAdLanguage() {
|
||||
settings.language = adLanguage
|
||||
}
|
||||
adsLoader = IMAAdsLoader(settings: settings)
|
||||
adsLoader.delegate = self
|
||||
}
|
||||
|
||||
@@ -98,7 +103,7 @@
|
||||
}
|
||||
// Play each ad once it has been loaded
|
||||
if event.type == IMAAdEventType.LOADED {
|
||||
if _pipEnabled() {
|
||||
if _isPictureInPictureActive() {
|
||||
return
|
||||
}
|
||||
adsManager.start()
|
||||
|
@@ -11,7 +11,9 @@ import React
|
||||
private var _onPictureInPictureExit: (() -> Void)?
|
||||
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
|
||||
private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
|
||||
private var _isActive = false
|
||||
private var _isPictureInPictureActive: Bool {
|
||||
return _pipController?.isPictureInPictureActive ?? false
|
||||
}
|
||||
|
||||
init(
|
||||
_ onPictureInPictureEnter: (() -> Void)? = nil,
|
||||
@@ -67,23 +69,35 @@ import React
|
||||
_pipController = nil
|
||||
}
|
||||
|
||||
func setPictureInPicture(_ isActive: Bool) {
|
||||
if _isActive == isActive {
|
||||
return
|
||||
}
|
||||
_isActive = isActive
|
||||
|
||||
func enterPictureInPicture() {
|
||||
guard let _pipController else { return }
|
||||
if !_isPictureInPictureActive {
|
||||
_pipController.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
if _isActive && !_pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async {
|
||||
_pipController.startPictureInPicture()
|
||||
}
|
||||
} else if !_isActive && _pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async {
|
||||
func exitPictureInPicture() {
|
||||
guard let _pipController else { return }
|
||||
if _isPictureInPictureActive {
|
||||
let state = UIApplication.shared.applicationState
|
||||
if state == .background || state == .inactive {
|
||||
deinitPipController()
|
||||
_onPictureInPictureExit?()
|
||||
_onRestoreUserInterfaceForPictureInPictureStop?()
|
||||
} else {
|
||||
_pipController.stopPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
class RCTPictureInPicture: NSObject {
|
||||
public let _pipController: NSObject? = nil
|
||||
|
||||
func setRestoreUserInterfaceForPIPStopCompletionHandler(_: Bool) {}
|
||||
func setupPipController(_: AVPlayerLayer?) {}
|
||||
func deinitPipController() {}
|
||||
func enterPictureInPicture() {}
|
||||
func exitPictureInPicture() {}
|
||||
}
|
||||
#endif
|
||||
|
@@ -234,10 +234,9 @@ class RCTPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate, AVPla
|
||||
|
||||
/* Cancels the previously registered time observer. */
|
||||
func removePlayerTimeObserver() {
|
||||
if _timeObserver != nil {
|
||||
player?.removeTimeObserver(_timeObserver)
|
||||
_timeObserver = nil
|
||||
}
|
||||
guard let timeObserver = _timeObserver else { return }
|
||||
player?.removeTimeObserver(timeObserver)
|
||||
_timeObserver = nil
|
||||
}
|
||||
|
||||
func addTimeObserverIfNotSet() {
|
||||
@@ -284,11 +283,11 @@ class RCTPlayerObserver: NSObject, AVPlayerItemMetadataOutputPushDelegate, AVPla
|
||||
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.removeObserver(_handlers, name: NSNotification.Name.AVPlayerItemNewAccessLogEntry, object: player?.currentItem)
|
||||
NotificationCenter.default.removeObserver(_handlers, name: AVPlayerItem.newAccessLogEntryNotification, object: player?.currentItem)
|
||||
|
||||
NotificationCenter.default.addObserver(_handlers,
|
||||
selector: #selector(RCTPlayerObserverHandlerObjc.handleAVPlayerAccess(notification:)),
|
||||
name: NSNotification.Name.AVPlayerItemNewAccessLogEntry,
|
||||
name: AVPlayerItem.newAccessLogEntryNotification,
|
||||
object: player?.currentItem)
|
||||
}
|
||||
|
||||
|
@@ -15,15 +15,19 @@ enum RCTPlayerOperations {
|
||||
let trackCount: Int! = player?.currentItem?.tracks.count ?? 0
|
||||
|
||||
// The first few tracks will be audio & video track
|
||||
var firstTextIndex = 0
|
||||
var firstTextIndex = -1
|
||||
for i in 0 ..< trackCount where player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
|
||||
firstTextIndex = i
|
||||
break
|
||||
}
|
||||
if firstTextIndex == -1 {
|
||||
// no sideLoaded text track available (can happen with invalid vtt url)
|
||||
return
|
||||
}
|
||||
|
||||
var selectedTrackIndex: Int = RCTVideoUnset
|
||||
|
||||
if type == "disabled" {
|
||||
if (type == "disabled") || (type == "none") || (type == "") {
|
||||
// Select the last text index which is the disabled text track
|
||||
selectedTrackIndex = trackCount - firstTextIndex
|
||||
} else if type == "language" {
|
||||
@@ -88,7 +92,7 @@ enum RCTPlayerOperations {
|
||||
return
|
||||
}
|
||||
|
||||
if type == "disabled" {
|
||||
if (type == "disabled") || (type == "none") || (type == "") {
|
||||
// Do nothing. We want to ensure option is nil
|
||||
} else if (type == "language") || (type == "title") {
|
||||
let value = criteria?.value as? String
|
||||
|
@@ -1,186 +0,0 @@
|
||||
import AVFoundation
|
||||
|
||||
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
|
||||
private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:]
|
||||
private var _requestingCertificate = false
|
||||
private var _requestingCertificateErrored = false
|
||||
private var _drm: DRMParams?
|
||||
private var _localSourceEncryptionKeyScheme: String?
|
||||
private var _reactTag: NSNumber?
|
||||
private var _onVideoError: RCTDirectEventBlock?
|
||||
private var _onGetLicense: RCTDirectEventBlock?
|
||||
|
||||
init(
|
||||
asset: AVURLAsset,
|
||||
drm: DRMParams?,
|
||||
localSourceEncryptionKeyScheme: String?,
|
||||
onVideoError: RCTDirectEventBlock?,
|
||||
onGetLicense: RCTDirectEventBlock?,
|
||||
reactTag: NSNumber
|
||||
) {
|
||||
super.init()
|
||||
let queue = DispatchQueue(label: "assetQueue")
|
||||
asset.resourceLoader.setDelegate(self, queue: queue)
|
||||
_reactTag = reactTag
|
||||
_onVideoError = onVideoError
|
||||
_onGetLicense = onGetLicense
|
||||
_drm = drm
|
||||
_localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme
|
||||
}
|
||||
|
||||
deinit {
|
||||
for request in _loadingRequests.values {
|
||||
request?.finishLoading()
|
||||
}
|
||||
}
|
||||
|
||||
func resourceLoader(_: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool {
|
||||
return loadingRequestHandling(renewalRequest)
|
||||
}
|
||||
|
||||
func resourceLoader(_: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
|
||||
return loadingRequestHandling(loadingRequest)
|
||||
}
|
||||
|
||||
func resourceLoader(_: AVAssetResourceLoader, didCancel _: AVAssetResourceLoadingRequest) {
|
||||
RCTLog("didCancelLoadingRequest")
|
||||
}
|
||||
|
||||
func setLicenseResult(_ license: String!, _ licenseUrl: String!) {
|
||||
// Check if the loading request exists in _loadingRequests based on licenseUrl
|
||||
guard let loadingRequest = _loadingRequests[licenseUrl] else {
|
||||
setLicenseResultError("Loading request for licenseUrl \(licenseUrl) not found", licenseUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the license data is valid
|
||||
guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license) else {
|
||||
setLicenseResultError("No data from JS license response", licenseUrl)
|
||||
return
|
||||
}
|
||||
|
||||
let dataRequest: AVAssetResourceLoadingDataRequest! = loadingRequest?.dataRequest
|
||||
dataRequest.respond(with: respondData)
|
||||
loadingRequest!.finishLoading()
|
||||
_loadingRequests.removeValue(forKey: licenseUrl)
|
||||
}
|
||||
|
||||
func setLicenseResultError(_ error: String!, _ licenseUrl: String!) {
|
||||
// Check if the loading request exists in _loadingRequests based on licenseUrl
|
||||
guard let loadingRequest = _loadingRequests[licenseUrl] else {
|
||||
print("Loading request for licenseUrl \(licenseUrl) not found. Error: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error), licenseUrl: licenseUrl)
|
||||
}
|
||||
|
||||
func finishLoadingWithError(error: Error!, licenseUrl: String!) -> Bool {
|
||||
// Check if the loading request exists in _loadingRequests based on licenseUrl
|
||||
guard let loadingRequest = _loadingRequests[licenseUrl], let error = error as NSError? else {
|
||||
// Handle the case where the loading request is not found or error is nil
|
||||
return false
|
||||
}
|
||||
|
||||
loadingRequest!.finishLoading(with: error)
|
||||
_loadingRequests.removeValue(forKey: licenseUrl)
|
||||
_onVideoError?([
|
||||
"error": [
|
||||
"code": NSNumber(value: error.code),
|
||||
"localizedDescription": error.localizedDescription ?? "",
|
||||
"localizedFailureReason": error.localizedFailureReason ?? "",
|
||||
"localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "",
|
||||
"domain": error.domain,
|
||||
],
|
||||
"target": _reactTag,
|
||||
])
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func loadingRequestHandling(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
|
||||
if handleEmbeddedKey(loadingRequest) {
|
||||
return true
|
||||
}
|
||||
|
||||
if _drm != nil {
|
||||
return handleDrm(loadingRequest)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func handleEmbeddedKey(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
|
||||
guard let url = loadingRequest.request.url,
|
||||
let _localSourceEncryptionKeyScheme,
|
||||
let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: _localSourceEncryptionKeyScheme)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType
|
||||
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
|
||||
loadingRequest.contentInformationRequest?.contentLength = Int64(persistentKeyData.count)
|
||||
loadingRequest.dataRequest?.respond(with: persistentKeyData)
|
||||
loadingRequest.finishLoading()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleDrm(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
|
||||
if _requestingCertificate {
|
||||
return true
|
||||
} else if _requestingCertificateErrored {
|
||||
return false
|
||||
}
|
||||
|
||||
let requestKey: String = loadingRequest.request.url?.absoluteString ?? ""
|
||||
|
||||
_loadingRequests[requestKey] = loadingRequest
|
||||
|
||||
guard let _drm, let drmType = _drm.type, drmType == "fairplay" else {
|
||||
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@@ -1,161 +0,0 @@
|
||||
import AVFoundation
|
||||
|
||||
enum RCTVideoDRM {
|
||||
static func fetchLicense(
|
||||
licenseServer: String,
|
||||
spcData: Data?,
|
||||
contentId: String,
|
||||
headers: [String: Any]?
|
||||
) async throws -> Data {
|
||||
let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers)
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(from: request)
|
||||
|
||||
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(
|
||||
licenseServer: String,
|
||||
spcData: Data?,
|
||||
contentId: String,
|
||||
headers: [String: Any]?
|
||||
) -> URLRequest {
|
||||
var request = URLRequest(url: URL(string: licenseServer)!)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
if let headers {
|
||||
for item in headers {
|
||||
guard let key = item.key as? String, let value = item.value as? String else {
|
||||
continue
|
||||
}
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
|
||||
let spcEncoded = spcData?.base64EncodedString(options: [])
|
||||
let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(
|
||||
kCFAllocatorDefault,
|
||||
spcEncoded as? CFString? as! CFString,
|
||||
nil,
|
||||
"?=&+" as CFString,
|
||||
CFStringBuiltInEncodings.UTF8.rawValue
|
||||
) as? String
|
||||
let post = String(format: "spc=%@&%@", spcUrlEncoded as! CVarArg, contentId)
|
||||
let postData = post.data(using: String.Encoding.utf8, allowLossyConversion: true)
|
||||
request.httpBody = postData
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
static func fetchSpcData(
|
||||
loadingRequest: AVAssetResourceLoadingRequest,
|
||||
certificateData: Data,
|
||||
contentIdData: Data
|
||||
) 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
|
||||
}
|
||||
|
||||
return spcData
|
||||
#endif
|
||||
}
|
||||
|
||||
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?) throws -> Data {
|
||||
let contentIdData = contentId?.data(using: .utf8)
|
||||
|
||||
let certificateData = try? RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate)
|
||||
|
||||
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(
|
||||
loadingRequest: AVAssetResourceLoadingRequest,
|
||||
contentId: String?,
|
||||
licenseServer: String?,
|
||||
certificateUrl: String?,
|
||||
base64Certificate: Bool?,
|
||||
headers: [String: Any]?
|
||||
) 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 {
|
||||
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
|
||||
)
|
||||
|
||||
guard let licenseServer else {
|
||||
throw RCTVideoError.noLicenseServerURL as! Error
|
||||
}
|
||||
|
||||
return try await RCTVideoDRM.fetchLicense(
|
||||
licenseServer: licenseServer,
|
||||
spcData: spcData,
|
||||
contentId: contentId,
|
||||
headers: headers
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,114 +1,188 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - RCTVideoError
|
||||
|
||||
enum RCTVideoError: Int {
|
||||
case fromJSPart
|
||||
enum RCTVideoError: Error, Hashable {
|
||||
case fromJSPart(String)
|
||||
case noLicenseServerURL
|
||||
case licenseRequestNotOk
|
||||
case licenseRequestFailed(Int)
|
||||
case noDataFromLicenseRequest
|
||||
case noSPC
|
||||
case noDataRequest
|
||||
case noCertificateData
|
||||
case noCertificateURL
|
||||
case noFairplayDRM
|
||||
case noDRMData
|
||||
case invalidContentId
|
||||
case invalidAppCert
|
||||
case keyRequestCreationFailed
|
||||
case persistableKeyRequestFailed
|
||||
case embeddedKeyExtractionFailed
|
||||
case offlineDRMNotSupported
|
||||
case unsupportedDRMType
|
||||
case simulatorDRMNotSupported
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .fromJSPart:
|
||||
return 1000
|
||||
case .noLicenseServerURL:
|
||||
return 1001
|
||||
case .licenseRequestFailed:
|
||||
return 1002
|
||||
case .noDataFromLicenseRequest:
|
||||
return 1003
|
||||
case .noSPC:
|
||||
return 1004
|
||||
case .noCertificateData:
|
||||
return 1005
|
||||
case .noCertificateURL:
|
||||
return 1006
|
||||
case .noDRMData:
|
||||
return 1007
|
||||
case .invalidContentId:
|
||||
return 1008
|
||||
case .invalidAppCert:
|
||||
return 1009
|
||||
case .keyRequestCreationFailed:
|
||||
return 1010
|
||||
case .persistableKeyRequestFailed:
|
||||
return 1011
|
||||
case .embeddedKeyExtractionFailed:
|
||||
return 1012
|
||||
case .offlineDRMNotSupported:
|
||||
return 1013
|
||||
case .unsupportedDRMType:
|
||||
return 1014
|
||||
case .simulatorDRMNotSupported:
|
||||
return 1015
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: LocalizedError
|
||||
|
||||
extension RCTVideoError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case let .fromJSPart(error):
|
||||
return NSLocalizedString("Error from JavaScript: \(error)", comment: "")
|
||||
case .noLicenseServerURL:
|
||||
return NSLocalizedString("No license server URL provided", comment: "")
|
||||
case let .licenseRequestFailed(statusCode):
|
||||
return NSLocalizedString("License request failed with status code: \(statusCode)", comment: "")
|
||||
case .noDataFromLicenseRequest:
|
||||
return NSLocalizedString("No data received from license server", comment: "")
|
||||
case .noSPC:
|
||||
return NSLocalizedString("Failed to create Server Playback Context (SPC)", comment: "")
|
||||
case .noCertificateData:
|
||||
return NSLocalizedString("No certificate data obtained", comment: "")
|
||||
case .noCertificateURL:
|
||||
return NSLocalizedString("No certificate URL provided", comment: "")
|
||||
case .noDRMData:
|
||||
return NSLocalizedString("No DRM data available", comment: "")
|
||||
case .invalidContentId:
|
||||
return NSLocalizedString("Invalid content ID", comment: "")
|
||||
case .invalidAppCert:
|
||||
return NSLocalizedString("Invalid application certificate", comment: "")
|
||||
case .keyRequestCreationFailed:
|
||||
return NSLocalizedString("Failed to create content key request", comment: "")
|
||||
case .persistableKeyRequestFailed:
|
||||
return NSLocalizedString("Failed to create persistable content key request", comment: "")
|
||||
case .embeddedKeyExtractionFailed:
|
||||
return NSLocalizedString("Failed to extract embedded key", comment: "")
|
||||
case .offlineDRMNotSupported:
|
||||
return NSLocalizedString("Offline DRM is not supported, see https://github.com/TheWidlarzGroup/react-native-video/issues/3539", comment: "")
|
||||
case .unsupportedDRMType:
|
||||
return NSLocalizedString("Unsupported DRM type", comment: "")
|
||||
case .simulatorDRMNotSupported:
|
||||
return NSLocalizedString("DRM on simulators is not supported", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
case .fromJSPart:
|
||||
return NSLocalizedString("An error occurred in the JavaScript part of the application.", comment: "")
|
||||
case .noLicenseServerURL:
|
||||
return NSLocalizedString("The license server URL is missing in the DRM configuration.", comment: "")
|
||||
case .licenseRequestFailed:
|
||||
return NSLocalizedString("The license server responded with an error status code.", comment: "")
|
||||
case .noDataFromLicenseRequest:
|
||||
return NSLocalizedString("The license server did not return any data.", comment: "")
|
||||
case .noSPC:
|
||||
return NSLocalizedString("Failed to generate the Server Playback Context (SPC) for the content.", comment: "")
|
||||
case .noCertificateData:
|
||||
return NSLocalizedString("Unable to retrieve certificate data from the specified URL.", comment: "")
|
||||
case .noCertificateURL:
|
||||
return NSLocalizedString("The certificate URL is missing in the DRM configuration.", comment: "")
|
||||
case .noDRMData:
|
||||
return NSLocalizedString("The required DRM data is not available or is invalid.", comment: "")
|
||||
case .invalidContentId:
|
||||
return NSLocalizedString("The content ID provided is not valid or recognized.", comment: "")
|
||||
case .invalidAppCert:
|
||||
return NSLocalizedString("The application certificate is invalid or not recognized.", comment: "")
|
||||
case .keyRequestCreationFailed:
|
||||
return NSLocalizedString("Unable to create a content key request for DRM.", comment: "")
|
||||
case .persistableKeyRequestFailed:
|
||||
return NSLocalizedString("Failed to create a persistable content key request for offline playback.", comment: "")
|
||||
case .embeddedKeyExtractionFailed:
|
||||
return NSLocalizedString("Unable to extract the embedded key from the custom scheme URL.", comment: "")
|
||||
case .offlineDRMNotSupported:
|
||||
return NSLocalizedString("You tried to use Offline DRM but it is not supported yet", comment: "")
|
||||
case .unsupportedDRMType:
|
||||
return NSLocalizedString("You tried to use unsupported DRM type", comment: "")
|
||||
case .simulatorDRMNotSupported:
|
||||
return NSLocalizedString("You tried to DRM on a simulator", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .fromJSPart:
|
||||
return NSLocalizedString("Check the JavaScript logs for more details and fix any issues in the JS code.", comment: "")
|
||||
case .noLicenseServerURL:
|
||||
return NSLocalizedString("Ensure that you have specified the 'licenseServer' property in the DRM configuration.", comment: "")
|
||||
case .licenseRequestFailed:
|
||||
return NSLocalizedString("Verify that the license server is functioning correctly and that you're sending the correct data.", comment: "")
|
||||
case .noDataFromLicenseRequest:
|
||||
return NSLocalizedString("Check if the license server is operational and responding with the expected data.", comment: "")
|
||||
case .noSPC:
|
||||
return NSLocalizedString("Verify that the content key request is properly configured and that the DRM setup is correct.", comment: "")
|
||||
case .noCertificateData:
|
||||
return NSLocalizedString("Check if the certificate URL is correct and accessible, and that it returns valid certificate data.", comment: "")
|
||||
case .noCertificateURL:
|
||||
return NSLocalizedString("Make sure you have specified the 'certificateUrl' property in the DRM configuration.", comment: "")
|
||||
case .noDRMData:
|
||||
return NSLocalizedString("Ensure that you have provided all necessary DRM-related data in the configuration.", comment: "")
|
||||
case .invalidContentId:
|
||||
return NSLocalizedString("Verify that the content ID is correct and matches the expected format for your DRM system.", comment: "")
|
||||
case .invalidAppCert:
|
||||
return NSLocalizedString("Check if the application certificate is valid and properly formatted for your DRM system.", comment: "")
|
||||
case .keyRequestCreationFailed:
|
||||
return NSLocalizedString("Review your DRM configuration and ensure all required parameters are correctly set.", comment: "")
|
||||
case .persistableKeyRequestFailed:
|
||||
return NSLocalizedString("Verify that offline playback is supported and properly configured for your content.", comment: "")
|
||||
case .embeddedKeyExtractionFailed:
|
||||
return NSLocalizedString("Check if the embedded key is present in the URL and the custom scheme is correctly implemented.", comment: "")
|
||||
case .offlineDRMNotSupported:
|
||||
return NSLocalizedString("Check if localSourceEncryptionKeyScheme is set", comment: "")
|
||||
case .unsupportedDRMType:
|
||||
return NSLocalizedString("Verify that you are using fairplay (on Apple devices)", comment: "")
|
||||
case .simulatorDRMNotSupported:
|
||||
return NSLocalizedString("You need to test DRM content on real device", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RCTVideoErrorHandler
|
||||
|
||||
enum RCTVideoErrorHandler {
|
||||
static let noDRMData = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noDRMData.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No drm object found.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?",
|
||||
static func createError(from error: RCTVideoError) -> [String: Any] {
|
||||
return [
|
||||
"code": error.errorCode,
|
||||
"localizedDescription": error.localizedDescription,
|
||||
"localizedFailureReason": error.failureReason ?? "",
|
||||
"localizedRecoverySuggestion": error.recoverySuggestion ?? "",
|
||||
"domain": "RCTVideo",
|
||||
]
|
||||
)
|
||||
|
||||
static let noCertificateURL = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noCertificateURL.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM License.",
|
||||
NSLocalizedFailureReasonErrorKey: "No certificate URL has been found.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noCertificateData = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noCertificateData.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No certificate data obtained from the specificied url.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noSPC = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noSPC.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No spc received.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config.",
|
||||
]
|
||||
)
|
||||
|
||||
static let noLicenseServerURL = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noLicenseServerURL.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM License.",
|
||||
NSLocalizedFailureReasonErrorKey: "No license server URL has been found.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop licenseServer?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noDataFromLicenseRequest = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noDataFromLicenseRequest.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No data received from the license server.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?",
|
||||
]
|
||||
)
|
||||
|
||||
static func licenseRequestNotOk(_ statusCode: Int) -> NSError {
|
||||
return NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.licenseRequestNotOk.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining license.",
|
||||
NSLocalizedFailureReasonErrorKey: String(
|
||||
format: "License server responded with status code %li",
|
||||
statusCode
|
||||
),
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you send the correct data to the license Server? Is the server ok?",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
static func fromJSPart(_ error: String) -> NSError {
|
||||
return NSError(domain: "RCTVideo",
|
||||
code: RCTVideoError.fromJSPart.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: error,
|
||||
NSLocalizedFailureReasonErrorKey: error,
|
||||
NSLocalizedRecoverySuggestionErrorKey: error,
|
||||
])
|
||||
}
|
||||
|
||||
static let invalidContentId = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.invalidContentId.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No valide content Id received",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
@@ -19,26 +19,32 @@ enum RCTVideoSave {
|
||||
reject("ERROR_COULD_NOT_CREATE_EXPORT_SESSION", "Could not create export session", nil)
|
||||
return
|
||||
}
|
||||
var path: String!
|
||||
path = RCTVideoSave.generatePathInDirectory(
|
||||
directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path,
|
||||
withExtension: ".mp4"
|
||||
)
|
||||
let url: NSURL! = NSURL.fileURL(withPath: path) as NSURL
|
||||
exportSession.outputFileType = AVFileType.mp4
|
||||
exportSession.outputURL = url as URL?
|
||||
exportSession.videoComposition = playerItem?.videoComposition
|
||||
exportSession.shouldOptimizeForNetworkUse = true
|
||||
exportSession.exportAsynchronously(completionHandler: {
|
||||
switch exportSession.status {
|
||||
case .failed:
|
||||
reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error)
|
||||
case .cancelled:
|
||||
reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error)
|
||||
default:
|
||||
resolve(["uri": url.absoluteString])
|
||||
}
|
||||
})
|
||||
|
||||
#if !os(visionOS)
|
||||
var path: String!
|
||||
path = RCTVideoSave.generatePathInDirectory(
|
||||
directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path,
|
||||
withExtension: ".mp4"
|
||||
)
|
||||
let url: NSURL! = NSURL.fileURL(withPath: path) as NSURL
|
||||
exportSession.outputFileType = .mp4
|
||||
exportSession.outputFileType = AVFileType.mp4
|
||||
exportSession.outputURL = url as URL?
|
||||
exportSession.videoComposition = playerItem?.videoComposition
|
||||
exportSession.shouldOptimizeForNetworkUse = true
|
||||
exportSession.exportAsynchronously(completionHandler: {
|
||||
switch exportSession.status {
|
||||
case .failed:
|
||||
reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error)
|
||||
case .cancelled:
|
||||
reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error)
|
||||
default:
|
||||
resolve(["uri": url.absoluteString])
|
||||
}
|
||||
})
|
||||
#else
|
||||
reject("ERROR_EXPORT_SESSION_CANCELLED", "this function is not supported on visionOS", nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func generatePathInDirectory(directory: String?, withExtension extension: String?) -> String? {
|
||||
@@ -54,13 +60,11 @@ enum RCTVideoSave {
|
||||
|
||||
static func ensureDirExists(withPath path: String?) -> Bool {
|
||||
var isDir: ObjCBool = false
|
||||
var error: Error?
|
||||
let exists = FileManager.default.fileExists(atPath: path ?? "", isDirectory: &isDir)
|
||||
if !(exists && isDir.boolValue) {
|
||||
do {
|
||||
try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {}
|
||||
if error != nil {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,15 @@ enum RCTVideoAssetsUtils {
|
||||
for mediaCharacteristic: AVMediaCharacteristic
|
||||
) async -> AVMediaSelectionGroup? {
|
||||
if #available(iOS 15, tvOS 15, visionOS 1.0, *) {
|
||||
return try? await asset?.loadMediaSelectionGroup(for: mediaCharacteristic)
|
||||
do {
|
||||
guard let asset else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return try await asset.loadMediaSelectionGroup(for: mediaCharacteristic)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
#if !os(visionOS)
|
||||
return asset?.mediaSelectionGroup(forMediaCharacteristic: mediaCharacteristic)
|
||||
@@ -73,22 +81,25 @@ enum RCTVideoUtils {
|
||||
return 0
|
||||
}
|
||||
|
||||
static func urlFilePath(filepath: NSString!, searchPath: FileManager.SearchPathDirectory) -> NSURL! {
|
||||
if filepath.contains("file://") {
|
||||
return NSURL(string: filepath as String)
|
||||
static func urlFilePath(filepath: NSString?, searchPath: FileManager.SearchPathDirectory) -> NSURL! {
|
||||
guard let _filepath = filepath else { return nil }
|
||||
|
||||
if _filepath.contains("file://") {
|
||||
return NSURL(string: _filepath as String)
|
||||
}
|
||||
|
||||
// if no file found, check if the file exists in the Document directory
|
||||
let paths: [String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true)
|
||||
var relativeFilePath: String! = filepath.lastPathComponent
|
||||
let paths: [String] = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true)
|
||||
var relativeFilePath: String = _filepath.lastPathComponent
|
||||
// the file may be multiple levels below the documents directory
|
||||
let directoryString: String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents"
|
||||
let fileComponents: [String]! = filepath.components(separatedBy: directoryString)
|
||||
let directoryString: String = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents"
|
||||
let fileComponents: [String] = _filepath.components(separatedBy: directoryString)
|
||||
if fileComponents.count > 1 {
|
||||
relativeFilePath = fileComponents[1]
|
||||
}
|
||||
|
||||
let path: String! = (paths.first! as NSString).appendingPathComponent(relativeFilePath)
|
||||
guard let _pathFirst = paths.first else { return nil }
|
||||
let path: String = (_pathFirst as NSString).appendingPathComponent(relativeFilePath)
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
return NSURL.fileURL(withPath: path) as NSURL
|
||||
}
|
||||
@@ -127,7 +138,7 @@ enum RCTVideoUtils {
|
||||
return []
|
||||
}
|
||||
|
||||
let audioTracks: NSMutableArray! = NSMutableArray()
|
||||
let audioTracks = NSMutableArray()
|
||||
|
||||
let group = await RCTVideoAssetsUtils.getMediaSelectionGroup(asset: asset, for: .audible)
|
||||
|
||||
@@ -138,14 +149,14 @@ enum RCTVideoUtils {
|
||||
if (values?.count ?? 0) > 0, let value = values?[0] {
|
||||
title = value as! String
|
||||
}
|
||||
let language: String! = currentOption?.extendedLanguageTag ?? ""
|
||||
let language: String = currentOption?.extendedLanguageTag ?? ""
|
||||
|
||||
let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
|
||||
|
||||
let audioTrack = [
|
||||
"index": NSNumber(value: i),
|
||||
"title": title,
|
||||
"language": language ?? "",
|
||||
"language": language,
|
||||
"selected": currentOption?.displayName == selectedOption?.displayName,
|
||||
] as [String: Any]
|
||||
audioTracks.add(audioTrack)
|
||||
@@ -170,13 +181,12 @@ enum RCTVideoUtils {
|
||||
if (values?.count ?? 0) > 0, let value = values?[0] {
|
||||
title = value as! String
|
||||
}
|
||||
let language: String! = currentOption?.extendedLanguageTag ?? ""
|
||||
let selectedOpt = player.currentItem?.currentMediaSelection
|
||||
let language: String = currentOption?.extendedLanguageTag ?? ""
|
||||
let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
|
||||
let textTrack = TextTrack([
|
||||
"index": NSNumber(value: i),
|
||||
"title": title,
|
||||
"language": language,
|
||||
"language": language as Any,
|
||||
"selected": currentOption?.displayName == selectedOption?.displayName,
|
||||
])
|
||||
textTracks.append(textTrack)
|
||||
@@ -356,10 +366,11 @@ enum RCTVideoUtils {
|
||||
static func prepareAsset(source: VideoSource) -> (asset: AVURLAsset?, assetOptions: NSMutableDictionary?)? {
|
||||
guard let sourceUri = source.uri, sourceUri != "" else { return nil }
|
||||
var asset: AVURLAsset!
|
||||
let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? ""
|
||||
let url = source.isNetwork || source.isAsset
|
||||
? URL(string: source.uri ?? "")
|
||||
: URL(fileURLWithPath: bundlePath)
|
||||
let bundlePath = Bundle.main.path(forResource: sourceUri, ofType: source.type) ?? ""
|
||||
guard let url = source.isNetwork || source.isAsset
|
||||
? URL(string: sourceUri)
|
||||
: URL(fileURLWithPath: bundlePath) else { return nil }
|
||||
|
||||
let assetOptions: NSMutableDictionary! = NSMutableDictionary()
|
||||
|
||||
if source.isNetwork {
|
||||
@@ -367,10 +378,10 @@ enum RCTVideoUtils {
|
||||
assetOptions.setObject(headers, forKey: "AVURLAssetHTTPHeaderFieldsKey" as NSCopying)
|
||||
}
|
||||
let cookies: [AnyObject]! = HTTPCookieStorage.shared.cookies
|
||||
assetOptions.setObject(cookies, forKey: AVURLAssetHTTPCookiesKey as NSCopying)
|
||||
asset = AVURLAsset(url: url!, options: assetOptions as! [String: Any])
|
||||
assetOptions.setObject(cookies as Any, forKey: AVURLAssetHTTPCookiesKey as NSCopying)
|
||||
asset = AVURLAsset(url: url, options: assetOptions as? [String: Any])
|
||||
} else {
|
||||
asset = AVURLAsset(url: url!)
|
||||
asset = AVURLAsset(url: url)
|
||||
}
|
||||
return (asset, assetOptions)
|
||||
}
|
||||
@@ -423,14 +434,10 @@ enum RCTVideoUtils {
|
||||
return try? await AVVideoComposition.videoComposition(
|
||||
with: 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)
|
||||
}
|
||||
let image: CIImage! = request.sourceImage.clampedToExtent()
|
||||
filter.setValue(image, forKey: kCIInputImageKey)
|
||||
let output: CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent)
|
||||
request.finish(with: output, context: nil)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -438,14 +445,10 @@ enum RCTVideoUtils {
|
||||
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)
|
||||
}
|
||||
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
|
||||
|
@@ -18,6 +18,7 @@ class NowPlayingInfoCenterManager {
|
||||
private var skipBackwardTarget: Any?
|
||||
private var playbackPositionTarget: Any?
|
||||
private var seekTarget: Any?
|
||||
private var togglePlayPauseTarget: Any?
|
||||
|
||||
private let remoteCommandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
@@ -167,13 +168,26 @@ class NowPlayingInfoCenterManager {
|
||||
return .commandFailed
|
||||
}
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent {
|
||||
player.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: .max)) { _ in
|
||||
player.play()
|
||||
}
|
||||
player.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: .max))
|
||||
return .success
|
||||
}
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
// Handler for togglePlayPauseCommand, sent by Apple's Earpods wired headphones
|
||||
togglePlayPauseTarget = remoteCommandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if player.rate == 0 {
|
||||
player.play()
|
||||
} else {
|
||||
player.pause()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidateCommandTargets() {
|
||||
@@ -182,6 +196,7 @@ class NowPlayingInfoCenterManager {
|
||||
remoteCommandCenter.skipForwardCommand.removeTarget(skipForwardTarget)
|
||||
remoteCommandCenter.skipBackwardCommand.removeTarget(skipBackwardTarget)
|
||||
remoteCommandCenter.changePlaybackPositionCommand.removeTarget(playbackPositionTarget)
|
||||
remoteCommandCenter.togglePlayPauseCommand.removeTarget(togglePlayPauseTarget)
|
||||
}
|
||||
|
||||
public func updateNowPlayingInfo() {
|
||||
|
@@ -17,7 +17,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
private var _playerViewController: RCTVideoPlayerViewController?
|
||||
private var _videoURL: NSURL?
|
||||
private var _localSourceEncryptionKeyScheme: String?
|
||||
|
||||
/* Required to publish events */
|
||||
private var _eventDispatcher: RCTEventDispatcher?
|
||||
@@ -42,20 +41,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _repeat = false
|
||||
private var _isPlaying = false
|
||||
private var _allowsExternalPlayback = true
|
||||
private var _textTracks: [TextTrack]?
|
||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria?
|
||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria?
|
||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria = .none()
|
||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria = .none()
|
||||
private var _playbackStalled = false
|
||||
private var _playInBackground = false
|
||||
private var _preventsDisplaySleepDuringVideoPlayback = true
|
||||
private var _preferredForwardBufferDuration: Float = 0.0
|
||||
private var _playWhenInactive = false
|
||||
private var _ignoreSilentSwitch: String! = "inherit" // inherit, ignore, obey
|
||||
private var _mixWithOthers: String! = "inherit" // inherit, mix, duck
|
||||
private var _resizeMode: String! = "cover"
|
||||
private var _ignoreSilentSwitch: String = "inherit" // inherit, ignore, obey
|
||||
private var _mixWithOthers: String = "inherit" // inherit, mix, duck
|
||||
private var _resizeMode: String = "cover"
|
||||
private var _fullscreen = false
|
||||
private var _fullscreenAutorotate = true
|
||||
private var _fullscreenOrientation: String! = "all"
|
||||
private var _fullscreenOrientation: String = "all"
|
||||
private var _fullscreenPlayerPresented = false
|
||||
private var _fullscreenUncontrolPlayerPresented = false // to call events switching full screen mode from player controls
|
||||
private var _filterName: String!
|
||||
@@ -63,10 +61,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _presentingViewController: UIViewController?
|
||||
private var _startPosition: Float64 = -1
|
||||
private var _showNotificationControls = false
|
||||
private var _pictureInPictureEnabled = false {
|
||||
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
||||
private var _lastBitrate = -2.0
|
||||
private var _enterPictureInPictureOnLeave = false {
|
||||
didSet {
|
||||
#if os(iOS)
|
||||
if _pictureInPictureEnabled {
|
||||
if isPictureInPictureActive() { return }
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
@@ -86,7 +87,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
/* IMA Ads */
|
||||
private var _adTagUrl: String?
|
||||
#if USE_GOOGLE_IMA
|
||||
private var _imaAdsManager: RCTIMAAdsManager!
|
||||
/* Playhead used by the SDK to track content video progress and insert mid-rolls. */
|
||||
@@ -95,16 +95,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _didRequestAds = false
|
||||
private var _adPlaying = false
|
||||
|
||||
private var _resouceLoaderDelegate: RCTResourceLoaderDelegate?
|
||||
private lazy var _drmManager: DRMManager? = DRMManager()
|
||||
private var _playerObserver: RCTPlayerObserver = .init()
|
||||
|
||||
#if USE_VIDEO_CACHING
|
||||
private let _videoCache: RCTVideoCachingHandler = .init()
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private var _pip: RCTPictureInPicture?
|
||||
#endif
|
||||
private var _pip: RCTPictureInPicture?
|
||||
|
||||
// Events
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@@ -168,11 +166,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
}
|
||||
|
||||
func isPipEnabled() -> Bool {
|
||||
return _pictureInPictureEnabled
|
||||
}
|
||||
|
||||
func isPipActive() -> Bool {
|
||||
func isPictureInPictureActive() -> Bool {
|
||||
#if os(iOS)
|
||||
return _pip?._pipController?.isPictureInPictureActive == true
|
||||
#else
|
||||
@@ -182,15 +176,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func initPictureinPicture() {
|
||||
#if os(iOS)
|
||||
_pip = RCTPictureInPicture({ [weak self] in
|
||||
self?._onPictureInPictureEnter()
|
||||
}, { [weak self] in
|
||||
self?._onPictureInPictureExit()
|
||||
}, { [weak self] in
|
||||
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
})
|
||||
if _pip == nil {
|
||||
_pip = RCTPictureInPicture({ [weak self] in
|
||||
self?._onPictureInPictureEnter()
|
||||
}, { [weak self] in
|
||||
self?._onPictureInPictureExit()
|
||||
}, { [weak self] in
|
||||
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
})
|
||||
}
|
||||
|
||||
if _playerLayer != nil && !_controls {
|
||||
if _playerLayer != nil && !_controls && _pip?._pipController == nil {
|
||||
_pip?.setupPipController(_playerLayer)
|
||||
}
|
||||
#else
|
||||
@@ -202,17 +198,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
ReactNativeVideoManager.shared.registerView(newInstance: self)
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||
#endif
|
||||
|
||||
_eventDispatcher = eventDispatcher
|
||||
|
||||
#if os(iOS)
|
||||
if _pictureInPictureEnabled {
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -244,6 +237,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenWillLock),
|
||||
name: UIApplication.protectedDataWillBecomeUnavailableNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenDidUnlock),
|
||||
name: UIApplication.protectedDataDidBecomeAvailableNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(audioRouteChanged(notification:)),
|
||||
@@ -259,7 +266,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -285,9 +292,18 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
// MARK: - App lifecycle handlers
|
||||
|
||||
func getIsExternalPlaybackActive() -> Bool {
|
||||
#if os(visionOS)
|
||||
let isExternalPlaybackActive = false
|
||||
#else
|
||||
let isExternalPlaybackActive = _player?.isExternalPlaybackActive ?? false
|
||||
#endif
|
||||
return isExternalPlaybackActive
|
||||
}
|
||||
|
||||
@objc
|
||||
func applicationWillResignActive(notification _: NSNotification!) {
|
||||
let isExternalPlaybackActive = _player?.isExternalPlaybackActive ?? false
|
||||
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
||||
if _playInBackground || _playWhenInactive || !_isPlaying || isExternalPlaybackActive { return }
|
||||
|
||||
_player?.pause()
|
||||
@@ -296,7 +312,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func applicationDidBecomeActive(notification _: NSNotification!) {
|
||||
let isExternalPlaybackActive = _player?.isExternalPlaybackActive ?? false
|
||||
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
||||
if _playInBackground || _playWhenInactive || !_isPlaying || isExternalPlaybackActive { return }
|
||||
|
||||
// Resume the player or any other tasks that should continue when the app becomes active.
|
||||
@@ -306,8 +322,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func applicationDidEnterBackground(notification _: NSNotification!) {
|
||||
let isExternalPlaybackActive = _player?.isExternalPlaybackActive ?? false
|
||||
if !_playInBackground || isExternalPlaybackActive || isPipActive() { return }
|
||||
if !_paused && isPictureInPictureActive() {
|
||||
_player?.play()
|
||||
_player?.rate = _rate
|
||||
}
|
||||
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
||||
if !_playInBackground || isExternalPlaybackActive || isPictureInPictureActive() { return }
|
||||
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
|
||||
_playerLayer?.player = nil
|
||||
_playerViewController?.player = nil
|
||||
@@ -320,6 +340,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_playerViewController?.player = _player
|
||||
}
|
||||
|
||||
@objc
|
||||
func screenWillLock() {
|
||||
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||
if _playInBackground || !_isPlaying || !isActiveBackgroundPip { return }
|
||||
|
||||
_player?.pause()
|
||||
_player?.rate = 0.0
|
||||
}
|
||||
|
||||
@objc
|
||||
func screenDidUnlock() {
|
||||
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||
if _paused || !isActiveBackgroundPip { return }
|
||||
|
||||
_player?.play()
|
||||
_player?.rate = _rate
|
||||
}
|
||||
|
||||
// MARK: - Audio events
|
||||
|
||||
@objc
|
||||
@@ -342,7 +380,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
#endif
|
||||
|
||||
if let video = _player?.currentItem,
|
||||
video == nil || video.status != AVPlayerItem.Status.readyToPlay {
|
||||
video.status != AVPlayerItem.Status.readyToPlay {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -365,7 +403,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
if currentTimeSecs >= 0 {
|
||||
#if USE_GOOGLE_IMA
|
||||
if !_didRequestAds && currentTimeSecs >= 0.0001 && _adTagUrl != nil {
|
||||
if !_didRequestAds && currentTimeSecs >= 0.0001 && _source?.adParams.adTagUrl != nil {
|
||||
_imaAdsManager.requestAds()
|
||||
_didRequestAds = true
|
||||
}
|
||||
@@ -375,7 +413,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"playableDuration": RCTVideoUtils.calculatePlayableDuration(_player, withSource: _source),
|
||||
"atValue": currentTime?.value ?? .zero,
|
||||
"currentPlaybackTime": NSNumber(value: Double(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value,
|
||||
"target": reactTag,
|
||||
"target": reactTag as Any,
|
||||
"seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player),
|
||||
])
|
||||
}
|
||||
@@ -407,17 +445,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
// Perform on next run loop, otherwise onVideoLoadStart is nil
|
||||
onVideoLoadStart?([
|
||||
"src": [
|
||||
"uri": _source?.uri ?? NSNull(),
|
||||
"uri": _source?.uri ?? NSNull() as Any,
|
||||
"type": _source?.type ?? NSNull(),
|
||||
"isNetwork": NSNumber(value: _source?.isNetwork ?? false),
|
||||
],
|
||||
"drm": source.drm?.json ?? NSNull(),
|
||||
"target": reactTag,
|
||||
"drm": source.drm.json ?? NSNull(),
|
||||
"target": reactTag as Any,
|
||||
])
|
||||
|
||||
if let uri = source.uri, uri.starts(with: "ph://") {
|
||||
let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri)
|
||||
return await playerItemPrepareText(asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
|
||||
return await playerItemPrepareText(source: source, asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
|
||||
}
|
||||
|
||||
guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
|
||||
@@ -443,23 +481,26 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
#if USE_VIDEO_CACHING
|
||||
if _videoCache.shouldCache(source: source, textTracks: _textTracks) {
|
||||
return try await _videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions)
|
||||
if _videoCache.shouldCache(source: source) {
|
||||
return try await _videoCache.playerItemForSourceUsingCache(source: source, assetOptions: assetOptions)
|
||||
}
|
||||
#endif
|
||||
|
||||
if source.drm != nil || _localSourceEncryptionKeyScheme != nil {
|
||||
_resouceLoaderDelegate = RCTResourceLoaderDelegate(
|
||||
if source.drm.json != nil {
|
||||
if _drmManager == nil {
|
||||
_drmManager = DRMManager()
|
||||
}
|
||||
|
||||
_drmManager?.createContentKeyRequest(
|
||||
asset: asset,
|
||||
drm: source.drm,
|
||||
localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme,
|
||||
drmParams: source.drm,
|
||||
reactTag: reactTag,
|
||||
onVideoError: onVideoError,
|
||||
onGetLicense: onGetLicense,
|
||||
reactTag: reactTag
|
||||
onGetLicense: onGetLicense
|
||||
)
|
||||
}
|
||||
|
||||
return await playerItemPrepareText(asset: asset, assetOptions: assetOptions, uri: source.uri ?? "")
|
||||
return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "")
|
||||
}
|
||||
|
||||
func setupPlayer(playerItem: AVPlayerItem) async throws {
|
||||
@@ -480,7 +521,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
if _player == nil {
|
||||
_player = AVPlayer()
|
||||
ReactNativeVideoManager.shared.onInstanceCreated(id: instanceId, player: _player)
|
||||
ReactNativeVideoManager.shared.onInstanceCreated(id: instanceId, player: _player as Any)
|
||||
|
||||
_player!.replaceCurrentItem(with: playerItem)
|
||||
|
||||
@@ -489,8 +530,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
NowPlayingInfoCenterManager.shared.registerPlayer(player: _player!)
|
||||
}
|
||||
} else {
|
||||
#if !os(tvOS) && !os(visionOS)
|
||||
if #available(iOS 16.0, *) {
|
||||
// This feature caused crashes, if the app was put in bg, before the source change
|
||||
// https://github.com/TheWidlarzGroup/react-native-video/issues/3900
|
||||
self._playerViewController?.allowsVideoFrameAnalysis = false
|
||||
}
|
||||
#endif
|
||||
_player?.replaceCurrentItem(with: playerItem)
|
||||
|
||||
#if !os(tvOS) && !os(visionOS)
|
||||
if #available(iOS 16.0, *) {
|
||||
self._playerViewController?.allowsVideoFrameAnalysis = true
|
||||
}
|
||||
#endif
|
||||
// later we can just call "updateNowPlayingInfo:
|
||||
NowPlayingInfoCenterManager.shared.updateNowPlayingInfo()
|
||||
}
|
||||
@@ -504,7 +556,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
#if USE_GOOGLE_IMA
|
||||
if _adTagUrl != nil {
|
||||
if _source?.adParams.adTagUrl != nil {
|
||||
// Set up your content playhead and contentComplete callback.
|
||||
_contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: _player!)
|
||||
|
||||
@@ -541,7 +593,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
self.removePlayerLayer()
|
||||
self._playerObserver.player = nil
|
||||
self._resouceLoaderDelegate = nil
|
||||
self._drmManager = nil
|
||||
self._playerObserver.playerItem = nil
|
||||
|
||||
// perform on next run loop, otherwise other passed react-props may not be set
|
||||
@@ -573,13 +625,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
DispatchQueue.global(qos: .default).async(execute: initializeSource)
|
||||
}
|
||||
|
||||
@objc
|
||||
func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) {
|
||||
_localSourceEncryptionKeyScheme = keyScheme
|
||||
}
|
||||
|
||||
func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem {
|
||||
if (self._textTracks == nil) || self._textTracks?.isEmpty == true || (uri.hasSuffix(".m3u8")) {
|
||||
func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem {
|
||||
if source.textTracks.isEmpty == true || uri.hasSuffix(".m3u8") {
|
||||
return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
|
||||
}
|
||||
|
||||
@@ -590,11 +637,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
asset: asset,
|
||||
assetOptions: assetOptions,
|
||||
mixComposition: mixComposition,
|
||||
textTracks: self._textTracks
|
||||
textTracks: source.textTracks
|
||||
)
|
||||
|
||||
if validTextTracks.count != self._textTracks?.count {
|
||||
self.setTextTracks(validTextTracks)
|
||||
if validTextTracks.isEmpty {
|
||||
DebugLog("Strange state, not valid textTrack")
|
||||
}
|
||||
|
||||
if validTextTracks.count != source.textTracks.count {
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
}
|
||||
|
||||
return await self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))
|
||||
@@ -690,19 +741,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
@objc
|
||||
func setPictureInPicture(_ pictureInPicture: Bool) {
|
||||
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
||||
#if os(iOS)
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(.playback)
|
||||
try audioSession.setActive(true, options: [])
|
||||
} catch {}
|
||||
if pictureInPicture {
|
||||
_pictureInPictureEnabled = true
|
||||
} else {
|
||||
_pictureInPictureEnabled = false
|
||||
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
||||
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
||||
}
|
||||
_pip?.setPictureInPicture(pictureInPicture)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -719,14 +767,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func setIgnoreSilentSwitch(_ ignoreSilentSwitch: String?) {
|
||||
_ignoreSilentSwitch = ignoreSilentSwitch
|
||||
_ignoreSilentSwitch = ignoreSilentSwitch ?? "inherit"
|
||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
||||
applyModifiers()
|
||||
}
|
||||
|
||||
@objc
|
||||
func setMixWithOthers(_ mixWithOthers: String?) {
|
||||
_mixWithOthers = mixWithOthers
|
||||
_mixWithOthers = mixWithOthers ?? "inherit"
|
||||
applyModifiers()
|
||||
}
|
||||
|
||||
@@ -804,8 +852,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
let newCurrentTime = NSNumber(value: Float(currentTimeAfterSeek))
|
||||
self.onVideoSeekComplete?(["currentTime": newCurrentTime,
|
||||
"seekTime": time,
|
||||
"target": self.reactTag])
|
||||
"seekTime": time,
|
||||
"target": self.reactTag as Any])
|
||||
}
|
||||
|
||||
player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime, completionHandler: seekCompletionHandler)
|
||||
@@ -903,7 +951,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func applyModifiers() {
|
||||
if let video = _player?.currentItem,
|
||||
video == nil || video.status != AVPlayerItem.Status.readyToPlay {
|
||||
video.status != AVPlayerItem.Status.readyToPlay {
|
||||
return
|
||||
}
|
||||
if _muted {
|
||||
@@ -928,9 +976,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
setMaxBitRate(_maxBitRate)
|
||||
}
|
||||
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
setAudioOutput(_audioOutput)
|
||||
setSelectedAudioTrack(_selectedAudioTrackCriteria)
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
setResizeMode(_resizeMode)
|
||||
setRepeat(_repeat)
|
||||
setControls(_controls)
|
||||
@@ -949,7 +997,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func setSelectedAudioTrack(_ selectedAudioTrack: SelectedTrackCriteria?) {
|
||||
_selectedAudioTrackCriteria = selectedAudioTrack
|
||||
_selectedAudioTrackCriteria = selectedAudioTrack ?? SelectedTrackCriteria.none()
|
||||
Task {
|
||||
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.audible,
|
||||
criteria: _selectedAudioTrackCriteria)
|
||||
@@ -962,29 +1010,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func setSelectedTextTrack(_ selectedTextTrack: SelectedTrackCriteria?) {
|
||||
_selectedTextTrackCriteria = selectedTextTrack
|
||||
if _textTracks != nil { // sideloaded text tracks
|
||||
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks!, criteria: _selectedTextTrackCriteria)
|
||||
} else { // text tracks included in the HLS playlist§
|
||||
Task {
|
||||
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible,
|
||||
criteria: _selectedTextTrackCriteria)
|
||||
_selectedTextTrackCriteria = selectedTextTrack ?? SelectedTrackCriteria.none()
|
||||
guard let source = _source else { return }
|
||||
if !source.textTracks.isEmpty { // sideloaded text tracks
|
||||
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: source.textTracks, criteria: _selectedTextTrackCriteria)
|
||||
} else { // text tracks included in the HLS playlist
|
||||
Task { [weak self] in
|
||||
guard let self,
|
||||
let player = self._player else { return }
|
||||
|
||||
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(
|
||||
player: player,
|
||||
characteristic: .legible,
|
||||
criteria: self._selectedTextTrackCriteria
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func setTextTracks(_ textTracks: [NSDictionary]?) {
|
||||
setTextTracks(textTracks?.map { TextTrack($0) })
|
||||
}
|
||||
|
||||
func setTextTracks(_ textTracks: [TextTrack]?) {
|
||||
_textTracks = textTracks
|
||||
|
||||
// in case textTracks was set after selectedTextTrack
|
||||
if _selectedTextTrackCriteria != nil { setSelectedTextTrack(_selectedTextTrackCriteria) }
|
||||
}
|
||||
|
||||
@objc
|
||||
func setChapters(_ chapters: [NSDictionary]?) {
|
||||
setChapters(chapters?.map { Chapter($0) })
|
||||
@@ -996,7 +1039,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func setFullscreen(_ fullscreen: Bool) {
|
||||
var alreadyFullscreenPresented = _presentingViewController?.presentedViewController != nil
|
||||
let alreadyFullscreenPresented = _presentingViewController?.presentedViewController != nil
|
||||
if fullscreen && !_fullscreenPlayerPresented && _player != nil && !alreadyFullscreenPresented {
|
||||
// Ensure player view controller is not null
|
||||
// Controls will be displayed even if it is disabled in configuration
|
||||
@@ -1035,7 +1078,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self._fullscreenPlayerPresented = fullscreen
|
||||
self._playerViewController?.autorotate = self._fullscreenAutorotate
|
||||
|
||||
self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag])
|
||||
self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag as Any])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1058,9 +1101,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func setFullscreenOrientation(_ orientation: String?) {
|
||||
_fullscreenOrientation = orientation
|
||||
_fullscreenOrientation = orientation ?? "all"
|
||||
if _fullscreenPlayerPresented {
|
||||
_playerViewController?.preferredOrientation = orientation
|
||||
_playerViewController?.preferredOrientation = _fullscreenOrientation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,8 +1140,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
viewController.view.frame = self.bounds
|
||||
viewController.player = player
|
||||
if #available(tvOS 14.0, *) {
|
||||
viewController.allowsPictureInPicturePlayback = _pictureInPictureEnabled
|
||||
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||
}
|
||||
#if os(iOS)
|
||||
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||
#endif
|
||||
return viewController
|
||||
}
|
||||
|
||||
@@ -1118,7 +1164,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
self.layer.needsDisplayOnBoundsChange = true
|
||||
#if os(iOS)
|
||||
if _pictureInPictureEnabled {
|
||||
if _enterPictureInPictureOnLeave {
|
||||
_pip?.setupPipController(_playerLayer)
|
||||
}
|
||||
#endif
|
||||
@@ -1223,13 +1269,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
// MARK: - RCTIMAAdsManager
|
||||
|
||||
func getAdTagUrl() -> String? {
|
||||
return _adTagUrl
|
||||
func getAdLanguage() -> String? {
|
||||
return _source?.adParams.adLanguage
|
||||
}
|
||||
|
||||
@objc
|
||||
func setAdTagUrl(_ adTagUrl: String!) {
|
||||
_adTagUrl = adTagUrl
|
||||
func getAdTagUrl() -> String? {
|
||||
return _source?.adParams.adTagUrl
|
||||
}
|
||||
|
||||
#if USE_GOOGLE_IMA
|
||||
@@ -1290,14 +1335,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_playerItem = nil
|
||||
_source = nil
|
||||
_chapters = nil
|
||||
_textTracks = nil
|
||||
_selectedTextTrackCriteria = nil
|
||||
_selectedAudioTrackCriteria = nil
|
||||
_selectedTextTrackCriteria = SelectedTrackCriteria.none()
|
||||
_selectedAudioTrackCriteria = SelectedTrackCriteria.none()
|
||||
_presentingViewController = nil
|
||||
|
||||
ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player)
|
||||
ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player as Any)
|
||||
_player = nil
|
||||
_resouceLoaderDelegate = nil
|
||||
_drmManager = nil
|
||||
_playerObserver.clearPlayer()
|
||||
|
||||
self.removePlayerLayer()
|
||||
@@ -1330,12 +1374,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
)
|
||||
}
|
||||
|
||||
func setLicenseResult(_ license: String!, _ licenseUrl: String!) {
|
||||
_resouceLoaderDelegate?.setLicenseResult(license, licenseUrl)
|
||||
func setLicenseResult(_ license: String, _ licenseUrl: String) {
|
||||
_drmManager?.setJSLicenseResult(license: license, licenseUrl: licenseUrl)
|
||||
}
|
||||
|
||||
func setLicenseResultError(_ error: String!, _ licenseUrl: String!) {
|
||||
_resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl)
|
||||
func setLicenseResultError(_ error: String, _ licenseUrl: String) {
|
||||
_drmManager?.setJSLicenseError(error: error, licenseUrl: licenseUrl)
|
||||
}
|
||||
|
||||
// MARK: - RCTPlayerObserverHandler
|
||||
@@ -1349,7 +1393,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_isBuffering = false
|
||||
}
|
||||
onReadyForDisplay?([
|
||||
"target": reactTag,
|
||||
"target": reactTag as Any,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1368,7 +1412,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
onTimedMetadata?([
|
||||
"target": reactTag,
|
||||
"target": reactTag as Any,
|
||||
"metadata": metadata,
|
||||
])
|
||||
}
|
||||
@@ -1386,9 +1430,23 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
func extractJsonWithIndex(from tracks: [TextTrack]) -> [NSDictionary]? {
|
||||
if tracks.isEmpty {
|
||||
// No tracks, need to return nil to handle
|
||||
return nil
|
||||
}
|
||||
// Map each enumerated pair to include the index in the json dictionary
|
||||
let mappedTracks = tracks.enumerated().compactMap { index, track -> NSDictionary? in
|
||||
guard let json = track.json?.mutableCopy() as? NSMutableDictionary else { return nil }
|
||||
json["index"] = index // Insert the index into the json dictionary
|
||||
return json
|
||||
}
|
||||
return mappedTracks
|
||||
}
|
||||
|
||||
func handleReadyToPlay() {
|
||||
guard let _playerItem else { return }
|
||||
|
||||
guard let source = _source else { return }
|
||||
Task {
|
||||
if self._pendingSeek {
|
||||
self.setSeek(NSNumber(value: self._pendingSeekTime), NSNumber(value: 100))
|
||||
@@ -1417,7 +1475,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
var orientation = "undefined"
|
||||
|
||||
let tracks = await RCTVideoAssetsUtils.getTracks(asset: _playerItem.asset, withMediaType: .video)
|
||||
var presentationSize = _playerItem.presentationSize
|
||||
let presentationSize = _playerItem.presentationSize
|
||||
if presentationSize.height != 0.0 {
|
||||
width = Float(presentationSize.width)
|
||||
height = Float(presentationSize.height)
|
||||
@@ -1444,7 +1502,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"orientation": orientation,
|
||||
],
|
||||
"audioTracks": audioTracks,
|
||||
"textTracks": self._textTracks?.compactMap { $0.json } ?? textTracks.map(\.json),
|
||||
"textTracks": extractJsonWithIndex(from: source.textTracks) ?? textTracks.map(\.json),
|
||||
"target": self.reactTag as Any])
|
||||
}
|
||||
|
||||
@@ -1464,14 +1522,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
[
|
||||
"error": [
|
||||
"code": NSNumber(value: (_playerItem.error! as NSError).code),
|
||||
"localizedDescription": _playerItem.error?.localizedDescription == nil ? "" : _playerItem.error?.localizedDescription,
|
||||
"localizedDescription": _playerItem.error?.localizedDescription == nil ? "" : _playerItem.error?.localizedDescription as Any,
|
||||
"localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ?
|
||||
"" : (_playerItem.error! as NSError).localizedFailureReason) ?? "",
|
||||
"localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ?
|
||||
"" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "",
|
||||
"domain": (_playerItem.error as! NSError).domain,
|
||||
],
|
||||
"target": reactTag,
|
||||
"target": reactTag as Any,
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1500,6 +1558,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
guard _isPlaying != isPlaying else { return }
|
||||
_isPlaying = isPlaying
|
||||
_paused = !isPlaying
|
||||
onVideoPlaybackStateChanged?(["isPlaying": isPlaying, "isSeeking": self._pendingSeek == true, "target": reactTag as Any])
|
||||
}
|
||||
|
||||
@@ -1584,12 +1643,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
[
|
||||
"error": [
|
||||
"code": NSNumber(value: (error as NSError).code),
|
||||
"localizedDescription": error.localizedDescription ?? "",
|
||||
"localizedDescription": error.localizedDescription,
|
||||
"localizedFailureReason": (error as NSError).localizedFailureReason ?? "",
|
||||
"localizedRecoverySuggestion": (error as NSError).localizedRecoverySuggestion ?? "",
|
||||
"domain": (error as NSError).domain,
|
||||
],
|
||||
"target": reactTag,
|
||||
"target": reactTag as Any,
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1622,7 +1681,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
)
|
||||
} else {
|
||||
_playerObserver.removePlayerTimeObserver()
|
||||
_player?.pause()
|
||||
_player?.rate = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1633,16 +1693,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
guard let accessLog = (notification.object as? AVPlayerItem)?.accessLog() else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let lastEvent = accessLog.events.last else { return }
|
||||
onVideoBandwidthUpdate?(["bitrate": lastEvent.observedBitrate, "target": reactTag])
|
||||
if lastEvent.indicatedBitrate != _lastBitrate {
|
||||
_lastBitrate = lastEvent.indicatedBitrate
|
||||
onVideoBandwidthUpdate?(["bitrate": _lastBitrate, "target": reactTag as Any])
|
||||
}
|
||||
}
|
||||
|
||||
func handleTracksChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<[AVPlayerItemTrack]>) {
|
||||
guard let source = _source else { return }
|
||||
if onTextTracks != nil {
|
||||
Task {
|
||||
let textTracks = await RCTVideoUtils.getTextTrackInfo(self._player)
|
||||
self.onTextTracks?(["textTracks": self._textTracks?.compactMap { $0.json } ?? textTracks.compactMap(\.json)])
|
||||
self.onTextTracks?(["textTracks": extractJsonWithIndex(from: source.textTracks) ?? textTracks.compactMap(\.json)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1672,6 +1735,31 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func enterPictureInPicture() {
|
||||
if _pip?._pipController == nil {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
}
|
||||
_pip?.enterPictureInPicture()
|
||||
}
|
||||
|
||||
@objc
|
||||
func exitPictureInPicture() {
|
||||
guard isPictureInPictureActive() else { return }
|
||||
|
||||
_pip?.exitPictureInPicture()
|
||||
#if os(iOS)
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
_pip?.deinitPipController()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862
|
||||
@objc
|
||||
func setOnClick(_: Any) {}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(adTagUrl, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxBitRate, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
|
||||
@@ -24,7 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(preventsDisplaySleepDuringVideoPlayback, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(preferredForwardBufferDuration, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(enterPictureInPictureOnLeave, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
||||
@@ -75,6 +74,9 @@ RCT_EXTERN_METHOD(setLicenseResultErrorCmd : (nonnull NSNumber*)reactTag error :
|
||||
RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
|
||||
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume)
|
||||
RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
|
||||
RCT_EXTERN_METHOD(enterPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||
RCT_EXTERN_METHOD(exitPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
|
||||
|
||||
RCT_EXTERN_METHOD(save
|
||||
: (nonnull NSNumber*)reactTag options
|
||||
|
@@ -72,6 +72,27 @@ class RCTVideoManager: RCTViewManager {
|
||||
})
|
||||
}
|
||||
|
||||
@objc(enterPictureInPictureCmd:)
|
||||
func enterPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.enterPictureInPicture()
|
||||
})
|
||||
}
|
||||
|
||||
@objc(exitPictureInPictureCmd:)
|
||||
func exitPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.exitPictureInPicture()
|
||||
})
|
||||
}
|
||||
|
||||
@objc(setSourceCmd:source:)
|
||||
func setSourceCmd(_ reactTag: NSNumber, source: NSDictionary) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.setSrc(source)
|
||||
})
|
||||
}
|
||||
|
||||
@objc(save:options:resolve:reject:)
|
||||
func save(_ reactTag: NSNumber, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
protocol RCTVideoPlayerViewControllerDelegate: class {
|
||||
protocol RCTVideoPlayerViewControllerDelegate: AnyObject {
|
||||
func videoPlayerViewControllerWillDismiss(playerViewController: AVPlayerViewController)
|
||||
func videoPlayerViewControllerDidDismiss(playerViewController: AVPlayerViewController)
|
||||
}
|
||||
|
@@ -6,33 +6,31 @@
|
||||
import Foundation
|
||||
|
||||
public class ReactNativeVideoManager: RNVPlugin {
|
||||
private let expectedMaxVideoCount = 10
|
||||
private let expectedMaxVideoCount = 2
|
||||
|
||||
// create a private initializer
|
||||
private init() {}
|
||||
|
||||
public static let shared: ReactNativeVideoManager = .init()
|
||||
|
||||
var instanceList: [RCTVideo] = Array()
|
||||
var pluginList: [RNVPlugin] = Array()
|
||||
private var instanceCount = 0
|
||||
private var pluginList: [RNVPlugin] = Array()
|
||||
|
||||
/**
|
||||
* register a new ReactExoplayerViewManager in the managed list
|
||||
* register a new view
|
||||
*/
|
||||
func registerView(newInstance: RCTVideo) {
|
||||
if instanceList.count > expectedMaxVideoCount {
|
||||
func registerView(newInstance _: RCTVideo) {
|
||||
if instanceCount > expectedMaxVideoCount {
|
||||
DebugLog("multiple Video displayed ?")
|
||||
}
|
||||
instanceList.append(newInstance)
|
||||
instanceCount += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* unregister existing ReactExoplayerViewManager in the managed list
|
||||
* unregister existing view
|
||||
*/
|
||||
func unregisterView(newInstance: RCTVideo) {
|
||||
if let i = instanceList.firstIndex(of: newInstance) {
|
||||
instanceList.remove(at: i)
|
||||
}
|
||||
func unregisterView(newInstance _: RCTVideo) {
|
||||
instanceCount -= 1
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,20 +4,20 @@ import Foundation
|
||||
|
||||
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance()
|
||||
var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) async -> AVPlayerItem)?
|
||||
var playerItemPrepareText: ((VideoSource, AVAsset?, NSDictionary?, String) async -> AVPlayerItem)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func shouldCache(source: VideoSource, textTracks: [TextTrack]?) -> Bool {
|
||||
if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.isEmpty)) {
|
||||
func shouldCache(source: VideoSource) -> Bool {
|
||||
if source.isNetwork && source.shouldCache && source.textTracks.isEmpty {
|
||||
/* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying
|
||||
* to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded.
|
||||
* Until this is fixed, we need to bypass caching when text tracks are specified.
|
||||
*/
|
||||
DebugLog("""
|
||||
Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache.
|
||||
Caching is not supported for uri '\(source.uri ?? "NO URI")' because text tracks are not compatible with the cache.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
return true
|
||||
@@ -25,7 +25,8 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) async throws -> AVPlayerItem {
|
||||
func playerItemForSourceUsingCache(source: VideoSource, assetOptions options: NSDictionary) async throws -> AVPlayerItem {
|
||||
let uri = source.uri!
|
||||
let url = URL(string: uri)
|
||||
let (videoCacheStatus, cachedAsset) = await getItemForUri(uri)
|
||||
|
||||
@@ -36,33 +37,33 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
switch videoCacheStatus {
|
||||
case .missingFileExtension:
|
||||
DebugLog("""
|
||||
Could not generate cache key for uri '\(uri ?? "NO_URI")'.
|
||||
Could not generate cache key for uri '\(uri)'.
|
||||
It is currently not supported to cache urls that do not include a file extension.
|
||||
The video file will not be cached.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||
return await playerItemPrepareText(asset, options, "")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as? [String: Any])
|
||||
return await playerItemPrepareText(source, asset, options, "")
|
||||
|
||||
case .unsupportedFileExtension:
|
||||
DebugLog("""
|
||||
Could not generate cache key for uri '\(uri ?? "NO_URI")'.
|
||||
Could not generate cache key for uri '\(uri)'.
|
||||
The file extension of that uri is currently not supported.
|
||||
The video file will not be cached.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||
return await playerItemPrepareText(asset, options, "")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as? [String: Any])
|
||||
return await playerItemPrepareText(source, asset, options, "")
|
||||
|
||||
default:
|
||||
if let cachedAsset {
|
||||
DebugLog("Playing back uri '\(uri ?? "NO_URI")' from cache")
|
||||
DebugLog("Playing back uri '\(uri)' from cache")
|
||||
// See note in playerItemForSource about not being able to support text tracks & caching
|
||||
return AVPlayerItem(asset: cachedAsset)
|
||||
}
|
||||
}
|
||||
|
||||
let asset: DVURLAsset! = DVURLAsset(url: url, options: options as! [String: Any], networkTimeout: 10000)
|
||||
let asset: DVURLAsset! = DVURLAsset(url: url, options: options as? [String: Any], networkTimeout: 10000)
|
||||
asset.loaderDelegate = self
|
||||
|
||||
/* More granular code to have control over the DVURLAsset
|
||||
|
Reference in New Issue
Block a user