VEX-5938: Update resource loader to handle encrypted local files (#12)
Adds offline decryption key and uses it to decrypt content during offline playback Jira: VEX-5938 https://jira.tenkasu.net/browse/VEX-5938 - Update to accept scheme for key required to play offline playback - Uses provided scheme to intercept call from player and return the key - Fixes player item observer removal pattern ### Reviews - Major reviewer (domain expert): @armadilio3
This commit is contained in:
parent
8b75438148
commit
e27baeb065
13
README.md
13
README.md
@ -317,6 +317,7 @@ var styles = StyleSheet.create({
|
|||||||
* [trackId](#trackId)
|
* [trackId](#trackId)
|
||||||
* [useTextureView](#usetextureview)
|
* [useTextureView](#usetextureview)
|
||||||
* [volume](#volume)
|
* [volume](#volume)
|
||||||
|
* [localSourceEncryptionKeyScheme](#localSourceEncryptionKeyScheme)
|
||||||
|
|
||||||
### Event props
|
### Event props
|
||||||
* [onAudioBecomingNoisy](#onaudiobecomingnoisy)
|
* [onAudioBecomingNoisy](#onaudiobecomingnoisy)
|
||||||
@ -915,6 +916,18 @@ Adjust the volume.
|
|||||||
|
|
||||||
Platforms: all
|
Platforms: all
|
||||||
|
|
||||||
|
#### localSourceEncryptionKeyScheme
|
||||||
|
Set the url scheme for stream encryption key for local assets
|
||||||
|
|
||||||
|
Type: String
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
localSourceEncryptionKeyScheme="my-offline-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Platforms: iOS
|
||||||
|
|
||||||
|
|
||||||
### Event props
|
### Event props
|
||||||
|
|
||||||
|
1
Video.js
1
Video.js
@ -418,6 +418,7 @@ Video.propTypes = {
|
|||||||
certificateUrl: PropTypes.string,
|
certificateUrl: PropTypes.string,
|
||||||
getLicense: PropTypes.func,
|
getLicense: PropTypes.func,
|
||||||
}),
|
}),
|
||||||
|
localSourceEncryptionKeyScheme: PropTypes.string,
|
||||||
minLoadRetryCount: PropTypes.number,
|
minLoadRetryCount: PropTypes.number,
|
||||||
maxBitRate: PropTypes.number,
|
maxBitRate: PropTypes.number,
|
||||||
resizeMode: PropTypes.string,
|
resizeMode: PropTypes.string,
|
||||||
|
@ -28,39 +28,43 @@ class RCTPlayerObserver: NSObject {
|
|||||||
var _handlers: RCTPlayerObserverHandler!
|
var _handlers: RCTPlayerObserverHandler!
|
||||||
|
|
||||||
var player:AVPlayer? {
|
var player:AVPlayer? {
|
||||||
|
willSet {
|
||||||
|
removePlayerObservers()
|
||||||
|
removePlayerTimeObserver()
|
||||||
|
}
|
||||||
didSet {
|
didSet {
|
||||||
if player == nil {
|
if player != nil {
|
||||||
removePlayerObservers()
|
|
||||||
removePlayerTimeObserver()
|
|
||||||
} else {
|
|
||||||
addPlayerObservers()
|
addPlayerObservers()
|
||||||
addPlayerTimeObserver()
|
addPlayerTimeObserver()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var playerItem:AVPlayerItem? {
|
var playerItem:AVPlayerItem? {
|
||||||
|
willSet {
|
||||||
|
removePlayerItemObservers()
|
||||||
|
}
|
||||||
didSet {
|
didSet {
|
||||||
if playerItem == nil {
|
if playerItem != nil {
|
||||||
removePlayerItemObservers()
|
|
||||||
} else {
|
|
||||||
addPlayerItemObservers()
|
addPlayerItemObservers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var playerViewController:AVPlayerViewController? {
|
var playerViewController:AVPlayerViewController? {
|
||||||
|
willSet {
|
||||||
|
removePlayerViewControllerObservers()
|
||||||
|
}
|
||||||
didSet {
|
didSet {
|
||||||
if playerViewController == nil {
|
if playerViewController != nil {
|
||||||
removePlayerViewControllerObservers()
|
|
||||||
} else {
|
|
||||||
addPlayerViewControllerObservers()
|
addPlayerViewControllerObservers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var playerLayer:AVPlayerLayer? {
|
var playerLayer:AVPlayerLayer? {
|
||||||
|
willSet {
|
||||||
|
removePlayerLayerObserver()
|
||||||
|
}
|
||||||
didSet {
|
didSet {
|
||||||
if playerLayer == nil {
|
if playerLayer == nil {
|
||||||
removePlayerLayerObserver()
|
|
||||||
} else {
|
|
||||||
addPlayerLayerObserver()
|
addPlayerLayerObserver()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,8 +152,8 @@ class RCTPlayerObserver: NSObject {
|
|||||||
|
|
||||||
/* Cancels the previously registered time observer. */
|
/* Cancels the previously registered time observer. */
|
||||||
func removePlayerTimeObserver() {
|
func removePlayerTimeObserver() {
|
||||||
if let timeObserver = _timeObserver {
|
if _timeObserver != nil {
|
||||||
player?.removeTimeObserver(timeObserver)
|
player?.removeTimeObserver(_timeObserver)
|
||||||
_timeObserver = nil
|
_timeObserver = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import MediaAccessibility
|
import MediaAccessibility
|
||||||
|
import Promises
|
||||||
|
|
||||||
let RCTVideoUnset = -1
|
let RCTVideoUnset = -1
|
||||||
|
|
||||||
@ -157,4 +158,23 @@ enum RCTPlayerOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func seek(player: AVPlayer, playerItem:AVPlayerItem, paused:Bool, seekTime:Float, seekTolerance:Float) -> Promise<Bool> {
|
||||||
|
let timeScale:Int = 1000
|
||||||
|
let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
|
||||||
|
let current:CMTime = playerItem.currentTime()
|
||||||
|
let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
|
||||||
|
|
||||||
|
return Promise<Bool>(on: .global()) { fulfill, reject in
|
||||||
|
guard CMTimeCompare(current, cmSeekTime) != 0 else {
|
||||||
|
reject(NSError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !paused { player.pause() }
|
||||||
|
|
||||||
|
player.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ (finished:Bool) in
|
||||||
|
fulfill(finished)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Promises
|
||||||
|
|
||||||
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
|
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
private var _requestingCertificate:Bool = false
|
private var _requestingCertificate:Bool = false
|
||||||
private var _requestingCertificateErrored:Bool = false
|
private var _requestingCertificateErrored:Bool = false
|
||||||
private var _drm: DRMParams?
|
private var _drm: DRMParams?
|
||||||
|
private var _localSourceEncryptionKeyScheme: String?
|
||||||
private var _reactTag: NSNumber?
|
private var _reactTag: NSNumber?
|
||||||
private var _onVideoError: RCTDirectEventBlock?
|
private var _onVideoError: RCTDirectEventBlock?
|
||||||
private var _onGetLicense: RCTDirectEventBlock?
|
private var _onGetLicense: RCTDirectEventBlock?
|
||||||
@ -14,6 +16,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
init(
|
init(
|
||||||
asset: AVURLAsset,
|
asset: AVURLAsset,
|
||||||
drm: DRMParams?,
|
drm: DRMParams?,
|
||||||
|
localSourceEncryptionKeyScheme: String?,
|
||||||
onVideoError: RCTDirectEventBlock?,
|
onVideoError: RCTDirectEventBlock?,
|
||||||
onGetLicense: RCTDirectEventBlock?,
|
onGetLicense: RCTDirectEventBlock?,
|
||||||
reactTag: NSNumber
|
reactTag: NSNumber
|
||||||
@ -25,6 +28,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
_onVideoError = onVideoError
|
_onVideoError = onVideoError
|
||||||
_onGetLicense = onGetLicense
|
_onGetLicense = onGetLicense
|
||||||
_drm = drm
|
_drm = drm
|
||||||
|
_localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -42,16 +46,9 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) {
|
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) {
|
||||||
NSLog("didCancelLoadingRequest")
|
NSLog("didCancelLoadingRequest")
|
||||||
}
|
}
|
||||||
|
|
||||||
func base64DataFromBase64String(base64String:String?) -> Data? {
|
|
||||||
if let base64String = base64String {
|
|
||||||
return Data(base64Encoded:base64String)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setLicenseResult(_ license:String!) {
|
func setLicenseResult(_ license:String!) {
|
||||||
guard let respondData = self.base64DataFromBase64String(base64String: license),
|
guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license),
|
||||||
let _loadingRequest = _loadingRequest else {
|
let _loadingRequest = _loadingRequest else {
|
||||||
setLicenseResultError("No data from JS license response")
|
setLicenseResultError("No data from JS license response")
|
||||||
return
|
return
|
||||||
@ -67,14 +64,13 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func finishLoadingWithError(error:NSError!) -> Bool {
|
func finishLoadingWithError(error:Error!) -> Bool {
|
||||||
if let _loadingRequest = _loadingRequest, let error = error {
|
if let _loadingRequest = _loadingRequest, let error = error {
|
||||||
let licenseError:NSError! = error
|
_loadingRequest.finishLoading(with: error as! NSError)
|
||||||
_loadingRequest.finishLoading(with: licenseError)
|
|
||||||
|
|
||||||
_onVideoError?([
|
_onVideoError?([
|
||||||
"error": [
|
"error": [
|
||||||
"code": NSNumber(value: error.code),
|
"code": NSNumber(value: (error as NSError).code),
|
||||||
"localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription,
|
"localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription,
|
||||||
"localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "",
|
"localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "",
|
||||||
"localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "",
|
"localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "",
|
||||||
@ -88,6 +84,35 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool {
|
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 = _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 {
|
if _requestingCertificate {
|
||||||
return true
|
return true
|
||||||
} else if _requestingCertificateErrored {
|
} else if _requestingCertificateErrored {
|
||||||
@ -95,164 +120,48 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
}
|
}
|
||||||
_loadingRequest = loadingRequest
|
_loadingRequest = loadingRequest
|
||||||
|
|
||||||
let url = loadingRequest.request.url
|
guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else {
|
||||||
guard let _drm = _drm else {
|
|
||||||
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData)
|
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData)
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentId:String!
|
var promise: Promise<Data>
|
||||||
let contentIdOverride:String! = _drm.contentId
|
if _onGetLicense != nil {
|
||||||
if contentIdOverride != nil {
|
let contentId = _drm.contentId ?? loadingRequest.request.url?.host
|
||||||
contentId = contentIdOverride
|
promise = RCTVideoDRM.handleWithOnGetLicense(
|
||||||
} else if (_onGetLicense != nil) {
|
loadingRequest:loadingRequest,
|
||||||
contentId = url?.host
|
contentId:contentId,
|
||||||
} else {
|
certificateUrl:_drm.certificateUrl,
|
||||||
contentId = url?.absoluteString.replacingOccurrences(of: "skd://", with:"")
|
base64Certificate:_drm.base64Certificate
|
||||||
}
|
) .then{ spcData -> Void in
|
||||||
|
|
||||||
let drmType:String! = _drm.type
|
|
||||||
guard drmType == "fairplay" else {
|
|
||||||
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData)
|
|
||||||
}
|
|
||||||
|
|
||||||
let certificateStringUrl:String! = _drm.certificateUrl
|
|
||||||
guard let certificateStringUrl = certificateStringUrl, let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else {
|
|
||||||
return finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateURL)
|
|
||||||
}
|
|
||||||
DispatchQueue.global().async { [weak self] in
|
|
||||||
guard let self = self else { return }
|
|
||||||
var certificateData:Data?
|
|
||||||
if (_drm.base64Certificate != nil) {
|
|
||||||
certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters)
|
|
||||||
} else {
|
|
||||||
do {
|
|
||||||
certificateData = try Data(contentsOf: certificateURL)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let certificateData = certificateData else {
|
|
||||||
self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData)
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentIdData:NSData!
|
|
||||||
if self._onGetLicense != nil {
|
|
||||||
contentIdData = contentId.data(using: .utf8) as NSData?
|
|
||||||
} else {
|
|
||||||
contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8))
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataRequest:AVAssetResourceLoadingDataRequest! = loadingRequest.dataRequest
|
|
||||||
guard dataRequest != nil else {
|
|
||||||
self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData)
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var spcError:NSError!
|
|
||||||
var spcData: Data?
|
|
||||||
do {
|
|
||||||
spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil)
|
|
||||||
} catch let spcError {
|
|
||||||
print("SPC error")
|
|
||||||
}
|
|
||||||
// Request CKC to the server
|
|
||||||
var licenseServer:String! = _drm.licenseServer
|
|
||||||
if spcError != nil {
|
|
||||||
self.finishLoadingWithError(error: spcError)
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
}
|
|
||||||
|
|
||||||
guard spcData != nil else {
|
|
||||||
self.finishLoadingWithError(error: RCTVideoErrorHandler.noSPC)
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// js client has a onGetLicense callback and will handle license fetching
|
|
||||||
if let _onGetLicense = self._onGetLicense {
|
|
||||||
let base64Encoded = spcData?.base64EncodedString(options: [])
|
|
||||||
self._requestingCertificate = true
|
self._requestingCertificate = true
|
||||||
if licenseServer == nil {
|
self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "",
|
||||||
licenseServer = ""
|
"contentId": contentId,
|
||||||
}
|
"spcBase64": spcData.base64EncodedString(options: []),
|
||||||
_onGetLicense(["licenseUrl": licenseServer,
|
"target": self._reactTag])
|
||||||
"contentId": contentId,
|
|
||||||
"spcBase64": base64Encoded,
|
|
||||||
"target": self._reactTag])
|
|
||||||
|
|
||||||
|
|
||||||
} else if licenseServer != nil {
|
|
||||||
self.fetchLicense(
|
|
||||||
licenseServer: licenseServer,
|
|
||||||
spcData: spcData,
|
|
||||||
contentId: contentId,
|
|
||||||
dataRequest: dataRequest
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
promise = RCTVideoDRM.handleInternalGetLicense(
|
||||||
|
loadingRequest:loadingRequest,
|
||||||
|
contentId:_drm.contentId,
|
||||||
|
licenseServer:_drm.licenseServer,
|
||||||
|
certificateUrl:_drm.certificateUrl,
|
||||||
|
base64Certificate:_drm.base64Certificate,
|
||||||
|
headers:_drm.headers
|
||||||
|
) .then{ data -> Void in
|
||||||
|
guard let dataRequest = loadingRequest.dataRequest else {
|
||||||
|
throw RCTVideoErrorHandler.noCertificateData
|
||||||
|
}
|
||||||
|
dataRequest.respond(with:data)
|
||||||
|
loadingRequest.finishLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
promise.catch{ error in
|
||||||
|
self.finishLoadingWithError(error:error)
|
||||||
|
self._requestingCertificateErrored = true
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchLicense(
|
|
||||||
licenseServer: String,
|
|
||||||
spcData: Data?,
|
|
||||||
contentId: String,
|
|
||||||
dataRequest: AVAssetResourceLoadingDataRequest!
|
|
||||||
) {
|
|
||||||
var request = URLRequest(url: URL(string: licenseServer)!)
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
|
|
||||||
// HEADERS
|
|
||||||
if let headers = _drm?.headers {
|
|
||||||
for item in headers {
|
|
||||||
guard let key = item.key as? String, let value = item.value as? String else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
request.setValue(value, forHTTPHeaderField: key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_onGetLicense != nil) {
|
|
||||||
request.httpBody = spcData
|
|
||||||
} else {
|
|
||||||
let spcEncoded = spcData?.base64EncodedString(options: [])
|
|
||||||
let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String
|
|
||||||
let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId)
|
|
||||||
let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true)
|
|
||||||
request.httpBody = postData
|
|
||||||
}
|
|
||||||
|
|
||||||
let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ [weak self] (data:Data!,response:URLResponse!,error:Error!) in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let httpResponse:HTTPURLResponse! = response as! HTTPURLResponse
|
|
||||||
guard error == nil else {
|
|
||||||
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
|
||||||
self.finishLoadingWithError(error: error as NSError?)
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard httpResponse.statusCode == 200 else {
|
|
||||||
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
|
||||||
self.finishLoadingWithError(error: RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode))
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard data != nil else {
|
|
||||||
self.finishLoadingWithError(error: RCTVideoErrorHandler.noDataFromLicenseRequest)
|
|
||||||
self._requestingCertificateErrored = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self._onGetLicense != nil) {
|
|
||||||
dataRequest.respond(with: data)
|
|
||||||
} else if let decodedData = Data(base64Encoded: data, options: []) {
|
|
||||||
dataRequest.respond(with: decodedData)
|
|
||||||
}
|
|
||||||
self._loadingRequest?.finishLoading()
|
|
||||||
})
|
|
||||||
postDataTask.resume()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
168
ios/Video/Features/RCTVideoDRM.swift
Normal file
168
ios/Video/Features/RCTVideoDRM.swift
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Promises
|
||||||
|
|
||||||
|
struct RCTVideoDRM {
|
||||||
|
@available(*, unavailable) private init() {}
|
||||||
|
|
||||||
|
static func fetchLicense(
|
||||||
|
licenseServer: String,
|
||||||
|
spcData: Data?,
|
||||||
|
contentId: String,
|
||||||
|
headers: [String:Any]?
|
||||||
|
) -> Promise<Data> {
|
||||||
|
let request = createLicenseRequest(licenseServer:licenseServer, spcData:spcData, contentId:contentId, headers:headers)
|
||||||
|
|
||||||
|
return Promise<Data>(on: .global()) { fulfill, reject in
|
||||||
|
let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ (data:Data!,response:URLResponse!,error:Error!) in
|
||||||
|
|
||||||
|
let httpResponse:HTTPURLResponse! = (response as! HTTPURLResponse)
|
||||||
|
|
||||||
|
guard error == nil else {
|
||||||
|
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard httpResponse.statusCode == 200 else {
|
||||||
|
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
||||||
|
reject(RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard data != nil, let decodedData = Data(base64Encoded: data, options: []) else {
|
||||||
|
reject(RCTVideoErrorHandler.noDataFromLicenseRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfill(decodedData)
|
||||||
|
})
|
||||||
|
postDataTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
) -> Promise<Data> {
|
||||||
|
return Promise<Data>(on: .global()) { fulfill, reject in
|
||||||
|
var spcError:NSError!
|
||||||
|
var spcData: Data?
|
||||||
|
do {
|
||||||
|
spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil)
|
||||||
|
} catch _ {
|
||||||
|
print("SPC error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if spcError != nil {
|
||||||
|
reject(spcError)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let spcData = spcData else {
|
||||||
|
reject(RCTVideoErrorHandler.noSPC)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfill(spcData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func createCertificateData(certificateStringUrl:String?, base64Certificate:Bool?) -> Promise<Data> {
|
||||||
|
return Promise<Data>(on: .global()) { fulfill, reject in
|
||||||
|
|
||||||
|
guard let certificateStringUrl = certificateStringUrl,
|
||||||
|
let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else {
|
||||||
|
reject(RCTVideoErrorHandler.noCertificateURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var certificateData:Data?
|
||||||
|
do {
|
||||||
|
certificateData = try Data(contentsOf: certificateURL)
|
||||||
|
if (base64Certificate != nil) {
|
||||||
|
certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
guard let certificateData = certificateData else {
|
||||||
|
reject(RCTVideoErrorHandler.noCertificateData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fulfill(certificateData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, certificateUrl:String?, base64Certificate:Bool?) -> Promise<Data> {
|
||||||
|
let contentIdData = contentId?.data(using: .utf8)
|
||||||
|
|
||||||
|
return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate)
|
||||||
|
.then{ certificateData -> Promise<Data> in
|
||||||
|
guard let contentIdData = contentIdData else {
|
||||||
|
throw RCTVideoError.invalidContentId as! Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return RCTVideoDRM.fetchSpcData(
|
||||||
|
loadingRequest:loadingRequest,
|
||||||
|
certificateData:certificateData,
|
||||||
|
contentIdData:contentIdData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func handleInternalGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, licenseServer:String?, certificateUrl:String?, base64Certificate:Bool?, headers: [String:Any]?) -> Promise<Data> {
|
||||||
|
let url = loadingRequest.request.url
|
||||||
|
|
||||||
|
guard let contentId = contentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with:"") else {
|
||||||
|
return Promise(RCTVideoError.invalidContentId as! Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8)) as Data
|
||||||
|
|
||||||
|
return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate)
|
||||||
|
.then{ certificateData in
|
||||||
|
return RCTVideoDRM.fetchSpcData(
|
||||||
|
loadingRequest:loadingRequest,
|
||||||
|
certificateData:certificateData,
|
||||||
|
contentIdData:contentIdData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.then{ spcData -> Promise<Data> in
|
||||||
|
guard let licenseServer = licenseServer else {
|
||||||
|
throw RCTVideoError.noLicenseServerURL as! Error
|
||||||
|
}
|
||||||
|
return RCTVideoDRM.fetchLicense(
|
||||||
|
licenseServer: licenseServer,
|
||||||
|
spcData: spcData,
|
||||||
|
contentId: contentId,
|
||||||
|
headers: headers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
enum RCTVideoError : Int {
|
enum RCTVideoError : Int {
|
||||||
case fromJSPart
|
case fromJSPart
|
||||||
|
case noLicenseServerURL
|
||||||
case licenseRequestNotOk
|
case licenseRequestNotOk
|
||||||
case noDataFromLicenseRequest
|
case noDataFromLicenseRequest
|
||||||
case noSPC
|
case noSPC
|
||||||
@ -8,11 +9,12 @@ enum RCTVideoError : Int {
|
|||||||
case noCertificateURL
|
case noCertificateURL
|
||||||
case noFairplayDRM
|
case noFairplayDRM
|
||||||
case noDRMData
|
case noDRMData
|
||||||
|
case invalidContentId
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RCTVideoErrorHandler {
|
enum RCTVideoErrorHandler {
|
||||||
|
|
||||||
static let noDRMData: NSError = NSError(
|
static let noDRMData = NSError(
|
||||||
domain: "RCTVideo",
|
domain: "RCTVideo",
|
||||||
code: RCTVideoError.noDRMData.rawValue,
|
code: RCTVideoError.noDRMData.rawValue,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
@ -21,7 +23,7 @@ enum RCTVideoErrorHandler {
|
|||||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?"
|
NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?"
|
||||||
])
|
])
|
||||||
|
|
||||||
static let noCertificateURL: NSError = NSError(
|
static let noCertificateURL = NSError(
|
||||||
domain: "RCTVideo",
|
domain: "RCTVideo",
|
||||||
code: RCTVideoError.noCertificateURL.rawValue,
|
code: RCTVideoError.noCertificateURL.rawValue,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
@ -30,7 +32,7 @@ enum RCTVideoErrorHandler {
|
|||||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?"
|
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?"
|
||||||
])
|
])
|
||||||
|
|
||||||
static let noCertificateData: NSError = NSError(
|
static let noCertificateData = NSError(
|
||||||
domain: "RCTVideo",
|
domain: "RCTVideo",
|
||||||
code: RCTVideoError.noCertificateData.rawValue,
|
code: RCTVideoError.noCertificateData.rawValue,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
@ -39,7 +41,7 @@ enum RCTVideoErrorHandler {
|
|||||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?"
|
NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?"
|
||||||
])
|
])
|
||||||
|
|
||||||
static let noSPC:NSError! = NSError(
|
static let noSPC = NSError(
|
||||||
domain: "RCTVideo",
|
domain: "RCTVideo",
|
||||||
code: RCTVideoError.noSPC.rawValue,
|
code: RCTVideoError.noSPC.rawValue,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
@ -48,13 +50,22 @@ enum RCTVideoErrorHandler {
|
|||||||
NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config."
|
NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config."
|
||||||
])
|
])
|
||||||
|
|
||||||
static let noDataFromLicenseRequest:NSError! = NSError(
|
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",
|
domain: "RCTVideo",
|
||||||
code: RCTVideoError.noDataFromLicenseRequest.rawValue,
|
code: RCTVideoError.noDataFromLicenseRequest.rawValue,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||||
NSLocalizedFailureReasonErrorKey: "No data received from the license server.",
|
NSLocalizedFailureReasonErrorKey: "No data received from the license server.",
|
||||||
NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?."
|
NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?"
|
||||||
])
|
])
|
||||||
|
|
||||||
static func licenseRequestNotOk(_ statusCode: Int) -> NSError {
|
static func licenseRequestNotOk(_ statusCode: Int) -> NSError {
|
||||||
@ -80,4 +91,13 @@ enum RCTVideoErrorHandler {
|
|||||||
NSLocalizedRecoverySuggestionErrorKey: 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?"
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Promises
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Collection of pure functions
|
* Collection of pure functions
|
||||||
@ -140,4 +141,110 @@ enum RCTVideoUtils {
|
|||||||
static func getCurrentTime(playerItem:AVPlayerItem?) -> Float {
|
static func getCurrentTime(playerItem:AVPlayerItem?) -> Float {
|
||||||
return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero))
|
return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func base64DataFromBase64String(base64String:String?) -> Data? {
|
||||||
|
if let base64String = base64String {
|
||||||
|
return Data(base64Encoded:base64String)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func replaceURLScheme(url: URL, scheme: String?) -> URL? {
|
||||||
|
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||||
|
urlComponents?.scheme = scheme
|
||||||
|
|
||||||
|
return urlComponents?.url
|
||||||
|
}
|
||||||
|
|
||||||
|
static func extractDataFromCustomSchemeUrl(from url: URL, scheme: String) -> Data? {
|
||||||
|
guard url.scheme == scheme,
|
||||||
|
let adoptURL = RCTVideoUtils.replaceURLScheme(url:url, scheme: nil) else { return nil }
|
||||||
|
|
||||||
|
return Data(base64Encoded: adoptURL.absoluteString)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func generateMixComposition(_ asset:AVAsset) -> AVMutableComposition {
|
||||||
|
let mixComposition:AVMutableComposition = AVMutableComposition()
|
||||||
|
|
||||||
|
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
||||||
|
let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid)
|
||||||
|
do {
|
||||||
|
try videoCompTrack.insertTimeRange(
|
||||||
|
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
||||||
|
of: videoAsset,
|
||||||
|
at: .zero)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first
|
||||||
|
let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid)
|
||||||
|
do {
|
||||||
|
try audioCompTrack.insertTimeRange(
|
||||||
|
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
||||||
|
of: audioAsset,
|
||||||
|
at: .zero)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return mixComposition
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] {
|
||||||
|
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
||||||
|
var validTextTracks:[TextTrack] = []
|
||||||
|
if let textTracks = textTracks, textTracks.count > 0 {
|
||||||
|
for i in 0..<textTracks.count {
|
||||||
|
var textURLAsset:AVURLAsset!
|
||||||
|
let textUri:String = textTracks[i].uri
|
||||||
|
if textUri.lowercased().hasPrefix("http") {
|
||||||
|
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any]))
|
||||||
|
} else {
|
||||||
|
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?) as URL, options:nil)
|
||||||
|
}
|
||||||
|
let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
|
||||||
|
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset
|
||||||
|
validTextTracks.append(textTracks[i])
|
||||||
|
let textCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
|
||||||
|
preferredTrackID:kCMPersistentTrackID_Invalid)
|
||||||
|
do {
|
||||||
|
try textCompTrack.insertTimeRange(
|
||||||
|
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
||||||
|
of: textTrackAsset,
|
||||||
|
at: .zero)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validTextTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
static func delay(seconds: Int = 0) -> Promise<Void> {
|
||||||
|
return Promise<Void>(on: .global()) { fulfill, reject in
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: {
|
||||||
|
fulfill(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func prepareAsset(source:VideoSource) -> (asset:AVURLAsset?, assetOptions:NSMutableDictionary?)? {
|
||||||
|
guard source.uri != nil && source.uri != "" 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 assetOptions:NSMutableDictionary! = NSMutableDictionary()
|
||||||
|
|
||||||
|
if source.isNetwork {
|
||||||
|
if let headers = source.requestHeaders, headers.count > 0 {
|
||||||
|
assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying)
|
||||||
|
}
|
||||||
|
let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies
|
||||||
|
assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying)
|
||||||
|
asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any])
|
||||||
|
} else {
|
||||||
|
asset = AVURLAsset(url: url!)
|
||||||
|
}
|
||||||
|
return (asset, assetOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import AVFoundation
|
|||||||
import AVKit
|
import AVKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import React
|
import React
|
||||||
|
import Promises
|
||||||
|
|
||||||
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler {
|
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler {
|
||||||
|
|
||||||
@ -17,6 +18,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
/* DRM */
|
/* DRM */
|
||||||
private var _drm:DRMParams?
|
private var _drm:DRMParams?
|
||||||
|
|
||||||
|
private var _localSourceEncryptionKeyScheme:String?
|
||||||
|
|
||||||
/* Required to publish events */
|
/* Required to publish events */
|
||||||
private var _eventDispatcher:RCTEventDispatcher?
|
private var _eventDispatcher:RCTEventDispatcher?
|
||||||
private var _videoLoadStarted:Bool = false
|
private var _videoLoadStarted:Bool = false
|
||||||
@ -61,7 +64,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver()
|
private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver()
|
||||||
|
|
||||||
#if canImport(RCTVideoCache)
|
#if canImport(RCTVideoCache)
|
||||||
private var _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler(self.playerItemPrepareText)
|
private let _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if TARGET_OS_IOS
|
#if TARGET_OS_IOS
|
||||||
@ -125,6 +128,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
_playerObserver._handlers = self
|
_playerObserver._handlers = self
|
||||||
|
#if canImport(RCTVideoCache)
|
||||||
|
_videoCache.playerItemPrepareText = playerItemPrepareText
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
@ -216,10 +222,39 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
removePlayerLayer()
|
removePlayerLayer()
|
||||||
_playerObserver.player = nil
|
_playerObserver.player = nil
|
||||||
_playerObserver.playerItem = nil
|
_playerObserver.playerItem = nil
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0)) / Double(NSEC_PER_SEC), execute: { [weak self] in
|
|
||||||
guard let self = self else {return}
|
// perform on next run loop, otherwise other passed react-props may not be set
|
||||||
// perform on next run loop, otherwise other passed react-props may not be set
|
RCTVideoUtils.delay()
|
||||||
self.playerItemForSource(withCallback:{ (playerItem:AVPlayerItem!) in
|
.then{ [weak self] in
|
||||||
|
guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)}
|
||||||
|
guard let source = self._source,
|
||||||
|
let assetResult = RCTVideoUtils.prepareAsset(source: source),
|
||||||
|
let asset = assetResult.asset,
|
||||||
|
let assetOptions = assetResult.assetOptions else {
|
||||||
|
DebugLog("Could not find video URL in source '\(self._source)'")
|
||||||
|
throw NSError(domain: "", code: 0, userInfo: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(RCTVideoCache)
|
||||||
|
if self._videoCache.shouldCache(source:source, textTracks:self._textTracks) {
|
||||||
|
return self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions:assetOptions)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if self._drm != nil || self._localSourceEncryptionKeyScheme != nil {
|
||||||
|
self._resouceLoaderDelegate = RCTResourceLoaderDelegate(
|
||||||
|
asset: asset,
|
||||||
|
drm: self._drm,
|
||||||
|
localSourceEncryptionKeyScheme: self._localSourceEncryptionKeyScheme,
|
||||||
|
onVideoError: self.onVideoError,
|
||||||
|
onGetLicense: self.onGetLicense,
|
||||||
|
reactTag: self.reactTag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise{self.playerItemPrepareText(asset: asset, assetOptions:assetOptions)}
|
||||||
|
}.then{[weak self] (playerItem:AVPlayerItem!) in
|
||||||
|
guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)}
|
||||||
|
|
||||||
self._player?.pause()
|
self._player?.pause()
|
||||||
self._playerItem = playerItem
|
self._playerItem = playerItem
|
||||||
self._playerObserver.playerItem = self._playerItem
|
self._playerObserver.playerItem = self._playerItem
|
||||||
@ -233,7 +268,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
self._playerObserver.player = self._player
|
self._playerObserver.player = self._player
|
||||||
|
|
||||||
self._player?.actionAtItemEnd = .none
|
self._player?.actionAtItemEnd = .none
|
||||||
|
|
||||||
if #available(iOS 10.0, *) {
|
if #available(iOS 10.0, *) {
|
||||||
self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling)
|
self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling)
|
||||||
}
|
}
|
||||||
@ -248,122 +283,38 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
"drm": self._drm?.json ?? NSNull(),
|
"drm": self._drm?.json ?? NSNull(),
|
||||||
"target": self.reactTag
|
"target": self.reactTag
|
||||||
])
|
])
|
||||||
|
}.catch{_ in }
|
||||||
})
|
|
||||||
})
|
|
||||||
_videoLoadStarted = true
|
_videoLoadStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setDrm(_ drm:NSDictionary!) {
|
func setDrm(_ drm:NSDictionary) {
|
||||||
_drm = DRMParams(drm)
|
_drm = DRMParams(drm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?, withCallback handler:(AVPlayerItem?)->Void) {
|
@objc
|
||||||
|
func setLocalSourceEncryptionKeyScheme(_ keyScheme:String) {
|
||||||
|
_localSourceEncryptionKeyScheme = keyScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?) -> AVPlayerItem {
|
||||||
if (_textTracks == nil) || _textTracks?.count==0 {
|
if (_textTracks == nil) || _textTracks?.count==0 {
|
||||||
handler(AVPlayerItem(asset: asset))
|
return AVPlayerItem(asset: asset)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AVPlayer can't airplay AVMutableCompositions
|
// AVPlayer can't airplay AVMutableCompositions
|
||||||
_allowsExternalPlayback = false
|
_allowsExternalPlayback = false
|
||||||
|
let mixComposition = RCTVideoUtils.generateMixComposition(asset)
|
||||||
// sideload text tracks
|
let validTextTracks = RCTVideoUtils.getValidTextTracks(
|
||||||
let mixComposition:AVMutableComposition! = AVMutableComposition()
|
asset:asset,
|
||||||
|
assetOptions:assetOptions,
|
||||||
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
mixComposition:mixComposition,
|
||||||
let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid)
|
textTracks:_textTracks)
|
||||||
do {
|
|
||||||
try videoCompTrack.insertTimeRange(
|
|
||||||
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
|
||||||
of: videoAsset,
|
|
||||||
at: .zero)
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
|
|
||||||
let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first
|
|
||||||
let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid)
|
|
||||||
do {
|
|
||||||
try audioCompTrack.insertTimeRange(
|
|
||||||
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
|
||||||
of: audioAsset,
|
|
||||||
at: .zero)
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
|
|
||||||
var validTextTracks:[TextTrack] = []
|
|
||||||
if let textTracks = _textTracks, let textTrackCount = _textTracks?.count {
|
|
||||||
for i in 0..<textTracks.count {
|
|
||||||
var textURLAsset:AVURLAsset!
|
|
||||||
let textUri:String = textTracks[i].uri
|
|
||||||
if textUri.lowercased().hasPrefix("http") {
|
|
||||||
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any]))
|
|
||||||
} else {
|
|
||||||
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?) as URL, options:nil)
|
|
||||||
}
|
|
||||||
let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
|
|
||||||
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset
|
|
||||||
validTextTracks.append(textTracks[i])
|
|
||||||
let textCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
|
|
||||||
preferredTrackID:kCMPersistentTrackID_Invalid)
|
|
||||||
do {
|
|
||||||
try textCompTrack.insertTimeRange(
|
|
||||||
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
|
||||||
of: textTrackAsset,
|
|
||||||
at: .zero)
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if validTextTracks.count != _textTracks?.count {
|
if validTextTracks.count != _textTracks?.count {
|
||||||
setTextTracks(validTextTracks)
|
setTextTracks(validTextTracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler(AVPlayerItem(asset: mixComposition))
|
return AVPlayerItem(asset: mixComposition)
|
||||||
}
|
|
||||||
|
|
||||||
func playerItemForSource(withCallback handler:(AVPlayerItem?)->Void) {
|
|
||||||
var asset:AVURLAsset!
|
|
||||||
guard let source = _source, source.uri != nil && source.uri != "" else {
|
|
||||||
DebugLog("Could not find video URL in source '\(_source)'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? ""
|
|
||||||
let url = source.isNetwork || source.isAsset
|
|
||||||
? URL(string: source.uri ?? "")
|
|
||||||
: URL(fileURLWithPath: bundlePath)
|
|
||||||
|
|
||||||
let assetOptions:NSMutableDictionary! = NSMutableDictionary()
|
|
||||||
|
|
||||||
if url != nil && source.isNetwork {
|
|
||||||
if let headers = source.requestHeaders, headers.count > 0 {
|
|
||||||
assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying)
|
|
||||||
}
|
|
||||||
let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies
|
|
||||||
assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying)
|
|
||||||
#if canImport(RCTVideoCache)
|
|
||||||
if _videoCache.playerItemForSourceUsingCache(shouldCache:shouldCache, textTracks:_textTracks, uri:uri, assetOptions:assetOptions, handler:handler) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any])
|
|
||||||
} else {
|
|
||||||
asset = AVURLAsset(url: url!)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _drm != nil {
|
|
||||||
_resouceLoaderDelegate = RCTResourceLoaderDelegate(
|
|
||||||
asset: asset,
|
|
||||||
drm: _drm,
|
|
||||||
onVideoError: onVideoError,
|
|
||||||
onGetLicense: onGetLicense,
|
|
||||||
reactTag: reactTag
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.playerItemPrepareText(asset: asset, assetOptions:assetOptions, withCallback:handler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Prop setters
|
// MARK: - Prop setters
|
||||||
@ -415,13 +366,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String!) {
|
func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String?) {
|
||||||
_ignoreSilentSwitch = ignoreSilentSwitch
|
_ignoreSilentSwitch = ignoreSilentSwitch
|
||||||
self.applyModifiers()
|
self.applyModifiers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setMixWithOthers(_ mixWithOthers:String!) {
|
func setMixWithOthers(_ mixWithOthers:String?) {
|
||||||
_mixWithOthers = mixWithOthers
|
_mixWithOthers = mixWithOthers
|
||||||
self.applyModifiers()
|
self.applyModifiers()
|
||||||
}
|
}
|
||||||
@ -479,7 +430,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setCurrentTime(_ currentTime:Float) {
|
func setCurrentTime(_ currentTime:Float) {
|
||||||
let info:NSDictionary! = [
|
let info:NSDictionary = [
|
||||||
"time": NSNumber(value: currentTime),
|
"time": NSNumber(value: currentTime),
|
||||||
"tolerance": NSNumber(value: 100)
|
"tolerance": NSNumber(value: 100)
|
||||||
]
|
]
|
||||||
@ -490,37 +441,31 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
func setSeek(_ info:NSDictionary!) {
|
func setSeek(_ info:NSDictionary!) {
|
||||||
let seekTime:NSNumber! = info["time"] as! NSNumber
|
let seekTime:NSNumber! = info["time"] as! NSNumber
|
||||||
let seekTolerance:NSNumber! = info["tolerance"] as! NSNumber
|
let seekTolerance:NSNumber! = info["tolerance"] as! NSNumber
|
||||||
|
let item:AVPlayerItem? = _player?.currentItem
|
||||||
let timeScale:Int = 1000
|
guard item != nil, let player = _player, let item = item, item.status == AVPlayerItem.Status.readyToPlay else {
|
||||||
|
|
||||||
let item:AVPlayerItem! = _player?.currentItem
|
|
||||||
guard item != nil && item.status == AVPlayerItem.Status.readyToPlay else {
|
|
||||||
_pendingSeek = true
|
_pendingSeek = true
|
||||||
_pendingSeekTime = seekTime.floatValue
|
_pendingSeekTime = seekTime.floatValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let wasPaused = _paused
|
||||||
|
|
||||||
// TODO check loadedTimeRanges
|
RCTPlayerOperations.seek(
|
||||||
let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime.floatValue), preferredTimescale: Int32(timeScale))
|
player:player,
|
||||||
let current:CMTime = item.currentTime()
|
playerItem:item,
|
||||||
// TODO figure out a good tolerance level
|
paused:wasPaused,
|
||||||
let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance.floatValue), timescale: Int32(timeScale))
|
seekTime:seekTime.floatValue,
|
||||||
let wasPaused:Bool = _paused
|
seekTolerance:seekTolerance.floatValue)
|
||||||
|
.then{ [weak self] (finished:Bool) in
|
||||||
guard CMTimeCompare(current, cmSeekTime) != 0 else { return }
|
guard let self = self else { return }
|
||||||
if !wasPaused { _player?.pause() }
|
|
||||||
|
self._playerObserver.addTimeObserverIfNotSet()
|
||||||
_player?.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ [weak self] (finished:Bool) in
|
if !wasPaused {
|
||||||
guard let self = self else { return }
|
self.setPaused(false)
|
||||||
|
}
|
||||||
self._playerObserver.addTimeObserverIfNotSet()
|
self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))),
|
||||||
if !wasPaused {
|
"seekTime": seekTime,
|
||||||
self.setPaused(false)
|
"target": self.reactTag])
|
||||||
}
|
}.catch{_ in }
|
||||||
self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))),
|
|
||||||
"seekTime": seekTime,
|
|
||||||
"target": self.reactTag])
|
|
||||||
})
|
|
||||||
|
|
||||||
_pendingSeek = false
|
_pendingSeek = false
|
||||||
}
|
}
|
||||||
@ -605,46 +550,46 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
_repeat = `repeat`
|
_repeat = `repeat`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary!) {
|
func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary?) {
|
||||||
setSelectedAudioTrack(SelectedTrackCriteria(selectedAudioTrack))
|
setSelectedAudioTrack(SelectedTrackCriteria(selectedAudioTrack))
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria!) {
|
func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria?) {
|
||||||
_selectedAudioTrackCriteria = selectedAudioTrack
|
_selectedAudioTrackCriteria = selectedAudioTrack
|
||||||
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.audible,
|
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.audible,
|
||||||
criteria:_selectedAudioTrackCriteria)
|
criteria:_selectedAudioTrackCriteria)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setSelectedTextTrack(_ selectedTextTrack:NSDictionary!) {
|
func setSelectedTextTrack(_ selectedTextTrack:NSDictionary?) {
|
||||||
setSelectedTextTrack(SelectedTrackCriteria(selectedTextTrack))
|
setSelectedTextTrack(SelectedTrackCriteria(selectedTextTrack))
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria!) {
|
func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria?) {
|
||||||
_selectedTextTrackCriteria = selectedTextTrack
|
_selectedTextTrackCriteria = selectedTextTrack
|
||||||
if (_textTracks != nil) { // sideloaded text tracks
|
if (_textTracks != nil) { // sideloaded text tracks
|
||||||
RCTPlayerOperations.setSideloadedText(player:_player, textTracks:_textTracks, criteria:_selectedTextTrackCriteria)
|
RCTPlayerOperations.setSideloadedText(player:_player, textTracks:_textTracks, criteria:_selectedTextTrackCriteria)
|
||||||
} else { // text tracks included in the HLS playlist
|
} else { // text tracks included in the HLS playlist
|
||||||
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.legible,
|
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.legible,
|
||||||
criteria:_selectedTextTrackCriteria)
|
criteria:_selectedTextTrackCriteria)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setTextTracks(_ textTracks:[NSDictionary]!) {
|
func setTextTracks(_ textTracks:[NSDictionary]?) {
|
||||||
setTextTracks(textTracks.map { TextTrack($0) })
|
setTextTracks(textTracks?.map { TextTrack($0) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTextTracks(_ textTracks:[TextTrack]!) {
|
func setTextTracks(_ textTracks:[TextTrack]?) {
|
||||||
_textTracks = textTracks
|
_textTracks = textTracks
|
||||||
|
|
||||||
// in case textTracks was set after selectedTextTrack
|
// in case textTracks was set after selectedTextTrack
|
||||||
if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)}
|
if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setFullscreen(_ fullscreen:Bool) {
|
func setFullscreen(_ fullscreen:Bool) {
|
||||||
if fullscreen && !_fullscreenPlayerPresented && _player != nil {
|
if fullscreen && !_fullscreenPlayerPresented && _player != nil {
|
||||||
@ -697,7 +642,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setFullscreenOrientation(_ orientation:String!) {
|
func setFullscreenOrientation(_ orientation:String?) {
|
||||||
_fullscreenOrientation = orientation
|
_fullscreenOrientation = orientation
|
||||||
if _fullscreenPlayerPresented {
|
if _fullscreenPlayerPresented {
|
||||||
_playerViewController?.preferredOrientation = orientation
|
_playerViewController?.preferredOrientation = orientation
|
||||||
@ -705,18 +650,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usePlayerViewController() {
|
func usePlayerViewController() {
|
||||||
guard _player != nil else { return }
|
guard let _player = _player, let _playerItem = _playerItem else { return }
|
||||||
|
|
||||||
if _playerViewController == nil {
|
if _playerViewController == nil {
|
||||||
_playerViewController = createPlayerViewController(player: _player, withPlayerItem:_playerItem)
|
_playerViewController = createPlayerViewController(player:_player, withPlayerItem:_playerItem)
|
||||||
|
|
||||||
}
|
}
|
||||||
// to prevent video from being animated when resizeMode is 'cover'
|
// to prevent video from being animated when resizeMode is 'cover'
|
||||||
// resize mode must be set before subview is added
|
// resize mode must be set before subview is added
|
||||||
setResizeMode(_resizeMode)
|
setResizeMode(_resizeMode)
|
||||||
|
|
||||||
guard let _playerViewController = _playerViewController else { return }
|
guard let _playerViewController = _playerViewController else { return }
|
||||||
|
|
||||||
if _controls {
|
if _controls {
|
||||||
let viewController:UIViewController! = self.reactViewController()
|
let viewController:UIViewController! = self.reactViewController()
|
||||||
viewController.addChild(_playerViewController)
|
viewController.addChild(_playerViewController)
|
||||||
@ -726,8 +670,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
_playerObserver.playerViewController = _playerViewController
|
_playerObserver.playerViewController = _playerViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPlayerViewController(player:AVPlayer!, withPlayerItem playerItem:AVPlayerItem!) -> RCTVideoPlayerViewController! {
|
func createPlayerViewController(player:AVPlayer, withPlayerItem playerItem:AVPlayerItem) -> RCTVideoPlayerViewController {
|
||||||
let viewController:RCTVideoPlayerViewController! = RCTVideoPlayerViewController()
|
let viewController = RCTVideoPlayerViewController()
|
||||||
viewController.showsPlaybackControls = true
|
viewController.showsPlaybackControls = true
|
||||||
viewController.rctDelegate = self
|
viewController.rctDelegate = self
|
||||||
viewController.preferredOrientation = _fullscreenOrientation
|
viewController.preferredOrientation = _fullscreenOrientation
|
||||||
@ -953,20 +897,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata: [[String:String?]?] = []
|
var metadata: [[String:String?]?] = []
|
||||||
for item in _items {
|
for item in _items {
|
||||||
let value = item.value as? String
|
let value = item.value as? String
|
||||||
let identifier = item.identifier?.rawValue
|
let identifier = item.identifier?.rawValue
|
||||||
|
|
||||||
if let value = value {
|
if let value = value {
|
||||||
metadata.append(["value":value, "identifier":identifier])
|
metadata.append(["value":value, "identifier":identifier])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTimedMetadata?([
|
onTimedMetadata?([
|
||||||
"target": reactTag,
|
"target": reactTag,
|
||||||
"metadata": metadata
|
"metadata": metadata
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle player item status change.
|
// Handle player item status change.
|
||||||
@ -1134,14 +1078,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
//unused
|
//unused
|
||||||
// @objc func handleAVPlayerAccess(notification:NSNotification!) {
|
// @objc func handleAVPlayerAccess(notification:NSNotification!) {
|
||||||
// let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog()
|
// let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog()
|
||||||
// let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last
|
// let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last
|
||||||
//
|
//
|
||||||
// /* TODO: get this working
|
// /* TODO: get this working
|
||||||
// if (self.onBandwidthUpdate) {
|
// if (self.onBandwidthUpdate) {
|
||||||
// self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]});
|
// self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]});
|
||||||
// }
|
// }
|
||||||
// */
|
// */
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL);
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(localSourceEncryptionKeyScheme, NSString);
|
||||||
|
|
||||||
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
|
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);
|
||||||
|
@ -1,52 +1,50 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import DVAssetLoaderDelegate
|
import DVAssetLoaderDelegate
|
||||||
|
import Promises
|
||||||
|
|
||||||
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||||
|
|
||||||
private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance()
|
private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance()
|
||||||
private var _playerItemPrepareText: (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void
|
var playerItemPrepareText: ((AVAsset?, NSDictionary?) -> AVPlayerItem)?
|
||||||
|
|
||||||
init(_ playerItemPrepareText: @escaping (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void) {
|
override init() {
|
||||||
_playerItemPrepareText = playerItemPrepareText
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerItemForSourceUsingCache(shouldCache:Bool, textTracks:[AnyObject]?, uri:String, assetOptions:NSMutableDictionary, handler:@escaping (AVPlayerItem?)->Void) -> Bool {
|
func shouldCache(source: VideoSource, textTracks:[TextTrack]?) -> Bool {
|
||||||
if shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) {
|
if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) {
|
||||||
/* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying
|
/* 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.
|
* 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.
|
* Until this is fixed, we need to bypass caching when text tracks are specified.
|
||||||
*/
|
*/
|
||||||
DebugLog("Caching is not supported for uri '\(uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
DebugLog("Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
||||||
playerItemForSourceUsingCache(uri: uri, assetOptions:assetOptions, withCallback:handler)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!, withCallback handler: @escaping (AVPlayerItem?)->Void) {
|
func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!) -> Promise<AVPlayerItem?> {
|
||||||
let url = URL(string: uri)
|
let url = URL(string: uri)
|
||||||
_videoCache.getItemForUri(uri, withCallback:{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in
|
return getItemForUri(uri)
|
||||||
guard let self = self else { return }
|
.then{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) -> AVPlayerItem in
|
||||||
|
guard let self = self, let playerItemPrepareText = self.playerItemPrepareText else {throw NSError(domain: "", code: 0, userInfo: nil)}
|
||||||
switch (videoCacheStatus) {
|
switch (videoCacheStatus) {
|
||||||
case .missingFileExtension:
|
case .missingFileExtension:
|
||||||
DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
||||||
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any])
|
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any])
|
||||||
self._playerItemPrepareText(asset, options, handler)
|
return playerItemPrepareText(asset, options)
|
||||||
return
|
|
||||||
|
|
||||||
case .unsupportedFileExtension:
|
case .unsupportedFileExtension:
|
||||||
DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
||||||
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any])
|
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any])
|
||||||
self._playerItemPrepareText(asset, options, handler)
|
return playerItemPrepareText(asset, options)
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if let cachedAsset = cachedAsset {
|
if let cachedAsset = cachedAsset {
|
||||||
DebugLog("Playing back uri '\(uri)' from cache")
|
DebugLog("Playing back uri '\(uri)' from cache")
|
||||||
// See note in playerItemForSource about not being able to support text tracks & caching
|
// See note in playerItemForSource about not being able to support text tracks & caching
|
||||||
handler(AVPlayerItem(asset: cachedAsset))
|
return AVPlayerItem(asset: cachedAsset)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +63,16 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
|||||||
asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
|
asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
handler(AVPlayerItem(asset: asset))
|
return AVPlayerItem(asset: asset)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getItemForUri(_ uri:String) -> Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> {
|
||||||
|
return Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> { fulfill, reject in
|
||||||
|
self._videoCache.getItemForUri(uri, withCallback:{ (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in
|
||||||
|
fulfill((videoCacheStatus, cachedAsset))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DVAssetLoaderDelegate
|
// MARK: - DVAssetLoaderDelegate
|
||||||
|
@ -12,11 +12,12 @@ Pod::Spec.new do |s|
|
|||||||
s.homepage = 'https://github.com/react-native-community/react-native-video'
|
s.homepage = 'https://github.com/react-native-community/react-native-video'
|
||||||
s.source = { :git => "https://github.com/react-native-community/react-native-video.git", :tag => "#{s.version}" }
|
s.source = { :git => "https://github.com/react-native-community/react-native-video.git", :tag => "#{s.version}" }
|
||||||
|
|
||||||
s.ios.deployment_target = "8.0"
|
s.ios.deployment_target = "9.0"
|
||||||
s.tvos.deployment_target = "9.0"
|
s.tvos.deployment_target = "9.0"
|
||||||
|
|
||||||
s.subspec "Video" do |ss|
|
s.subspec "Video" do |ss|
|
||||||
ss.source_files = "ios/Video/**/*.{h,m,swift}"
|
ss.source_files = "ios/Video/**/*.{h,m,swift}"
|
||||||
|
ss.dependency "PromisesSwift"
|
||||||
end
|
end
|
||||||
|
|
||||||
s.subspec "VideoCaching" do |ss|
|
s.subspec "VideoCaching" do |ss|
|
||||||
|
Loading…
Reference in New Issue
Block a user