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

This commit is contained in:
2025-01-21 17:45:59 -07:00
555 changed files with 12263 additions and 109934 deletions

View 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
}
}

View File

@@ -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
}
}

View File

@@ -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": ""])
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View 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)
}
}

View 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)
}
}

View 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
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
)
}
}

View File

@@ -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?",
]
)
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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) {}

View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +1,7 @@
import AVKit
import Foundation
protocol RCTVideoPlayerViewControllerDelegate: class {
protocol RCTVideoPlayerViewControllerDelegate: AnyObject {
func videoPlayerViewControllerWillDismiss(playerViewController: AVPlayerViewController)
func videoPlayerViewControllerDidDismiss(playerViewController: AVPlayerViewController)
}

View File

@@ -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
}
/**

View File

@@ -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