iOS Swift Conversion (#2527)

Converts iOS implementation from Objective-c to Swift

# During the conversion process some updates to the code structure were also made
- Modularize codebase from single file to smaller focused files
- Untangled large nested IF statements
- Added more null checks, since Swift is more strict with null pointers
- Added property to allow for decoding of local video sources with self contained key for offline playback
- Updates example apps to test react-native 0.63.4 and uses auto native dependency imports for android and ios
This commit is contained in:
Nick Fujita
2022-05-19 22:29:25 +09:00
committed by GitHub
parent d681e5505f
commit 68b9db4d11
138 changed files with 8869 additions and 7589 deletions

View File

@@ -0,0 +1,30 @@
struct DRMParams {
let type: String?
let licenseServer: String?
let headers: Dictionary<String,Any>?
let contentId: String?
let certificateUrl: String?
let base64Certificate: Bool?
let json: NSDictionary?
init(_ json: NSDictionary!) {
guard json != nil else {
self.json = nil
self.type = nil
self.licenseServer = nil
self.contentId = nil
self.certificateUrl = nil
self.base64Certificate = nil
self.headers = nil
return
}
self.json = json
self.type = json["type"] as? String
self.licenseServer = json["licenseServer"] as? String
self.contentId = json["contentId"] as? String
self.certificateUrl = json["certificateUrl"] as? String
self.base64Certificate = json["base64Certificate"] as? Bool
self.headers = json["headers"] as? Dictionary<String,Any>
}
}

View File

@@ -0,0 +1,18 @@
struct SelectedTrackCriteria {
let type: String
let value: Any?
let json: NSDictionary?
init(_ json: NSDictionary!) {
guard json != nil else {
self.json = nil
self.type = ""
self.value = nil
return
}
self.json = json
self.type = json["type"] as? String ?? ""
self.value = json["value"]
}
}

View File

@@ -0,0 +1,25 @@
struct TextTrack {
let type: String
let language: String
let title: String
let uri: String
let json: NSDictionary?
init(_ json: NSDictionary!) {
guard json != nil else {
self.json = nil
self.type = ""
self.language = ""
self.title = ""
self.uri = ""
return
}
self.json = json
self.type = json["type"] as? String ?? ""
self.language = json["language"] as? String ?? ""
self.title = json["title"] as? String ?? ""
self.uri = json["uri"] as? String ?? ""
}
}

View File

@@ -0,0 +1,31 @@
struct VideoSource {
let type: String?
let uri: String?
let isNetwork: Bool
let isAsset: Bool
let shouldCache: Bool
let requestHeaders: Dictionary<String,Any>?
let json: NSDictionary?
init(_ json: NSDictionary!) {
guard json != nil else {
self.json = nil
self.type = nil
self.uri = nil
self.isNetwork = false
self.isAsset = false
self.shouldCache = false
self.requestHeaders = nil
return
}
self.json = json
self.type = json["type"] as? String
self.uri = json["uri"] as? String
self.isNetwork = json["isNetwork"] as? Bool ?? false
self.isAsset = json["isAsset"] as? Bool ?? false
self.shouldCache = json["shouldCache"] as? Bool ?? false
self.requestHeaders = json["requestHeaders"] as? Dictionary<String,Any>
}
}

View File

@@ -0,0 +1,75 @@
import AVFoundation
import AVKit
import MediaAccessibility
import React
import Foundation
#if TARGET_OS_IOS
class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate {
private var _onPictureInPictureStatusChanged: RCTDirectEventBlock?
private var _onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock?
private var _restoreUserInterfaceForPIPStopCompletionHandler:((Bool) -> Void)? = nil
private var _pipController:AVPictureInPictureController?
private var _isActive:Bool = false
init(_ onPictureInPictureStatusChanged: @escaping RCTDirectEventBlock, _ onRestoreUserInterfaceForPictureInPictureStop: @escaping RCTDirectEventBlock) {
_onPictureInPictureStatusChanged = onPictureInPictureStatusChanged
_onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop
}
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
_onPictureInPictureStatusChanged([ "isActive": NSNumber(value: true)])
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
_onPictureInPictureStatusChanged([ "isActive": NSNumber(value: false)])
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
assert(_restoreUserInterfaceForPIPStopCompletionHandler == nil, "restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited.")
guard let _onRestoreUserInterfaceForPictureInPictureStop = _onRestoreUserInterfaceForPictureInPictureStop else { return }
_onRestoreUserInterfaceForPictureInPictureStop([:])
_restoreUserInterfaceForPIPStopCompletionHandler = completionHandler
}
func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore:Bool) {
guard let _restoreUserInterfaceForPIPStopCompletionHandler = _restoreUserInterfaceForPIPStopCompletionHandler else { return }
_restoreUserInterfaceForPIPStopCompletionHandler(restore)
self._restoreUserInterfaceForPIPStopCompletionHandler = nil
}
func setupPipController(_ playerLayer: AVPlayerLayer?) {
guard playerLayer != nil && AVPictureInPictureController.isPictureInPictureSupported() && _isActive else { return }
// Create new controller passing reference to the AVPlayerLayer
_pipController = AVPictureInPictureController(playerLayer:playerLayer!)
_pipController?.delegate = self
}
func setPictureInPicture(_ isActive:Bool) {
if _isActive == isActive {
return
}
_isActive = isActive
guard let _pipController = _pipController else { return }
if _isActive && !_pipController.isPictureInPictureActive {
DispatchQueue.main.async(execute: {
_pipController.startPictureInPicture()
})
} else if !_isActive && _pipController.isPictureInPictureActive {
DispatchQueue.main.async(execute: {
_pipController.stopPictureInPicture()
})
}
}
}
#endif

View File

@@ -0,0 +1,207 @@
import AVFoundation
import AVKit
import Foundation
@objc
protocol RCTPlayerObserverHandlerObjc {
func handleDidFailToFinishPlaying(notification:NSNotification!)
func handlePlaybackStalled(notification:NSNotification!)
func handlePlayerItemDidReachEnd(notification:NSNotification!)
// unused
// func handleAVPlayerAccess(notification:NSNotification!)
}
protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc {
func handleTimeUpdate(time:CMTime)
func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange<Bool>)
func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>)
func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<AVPlayerItem.Status>)
func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<Bool>)
func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<Bool>)
func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange<Float>)
func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange<Bool>)
func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange<CGRect>)
}
class RCTPlayerObserver: NSObject {
var _handlers: RCTPlayerObserverHandler!
var player:AVPlayer? {
willSet {
removePlayerObservers()
removePlayerTimeObserver()
}
didSet {
if player != nil {
addPlayerObservers()
addPlayerTimeObserver()
}
}
}
var playerItem:AVPlayerItem? {
willSet {
removePlayerItemObservers()
}
didSet {
if playerItem != nil {
addPlayerItemObservers()
}
}
}
var playerViewController:AVPlayerViewController? {
willSet {
removePlayerViewControllerObservers()
}
didSet {
if playerViewController != nil {
addPlayerViewControllerObservers()
}
}
}
var playerLayer:AVPlayerLayer? {
willSet {
removePlayerLayerObserver()
}
didSet {
if playerLayer == nil {
addPlayerLayerObserver()
}
}
}
private var _progressUpdateInterval:TimeInterval = 250
private var _timeObserver:Any?
private var _playerRateChangeObserver:NSKeyValueObservation?
private var _playerExpernalPlaybackActiveObserver:NSKeyValueObservation?
private var _playerItemStatusObserver:NSKeyValueObservation?
private var _playerPlaybackBufferEmptyObserver:NSKeyValueObservation?
private var _playerPlaybackLikelyToKeepUpObserver:NSKeyValueObservation?
private var _playerTimedMetadataObserver:NSKeyValueObservation?
private var _playerViewControllerReadyForDisplayObserver:NSKeyValueObservation?
private var _playerLayerReadyForDisplayObserver:NSKeyValueObservation?
private var _playerViewControllerOverlayFrameObserver:NSKeyValueObservation?
deinit {
NotificationCenter.default.removeObserver(_handlers)
}
func addPlayerObservers() {
guard let player = player else {
return
}
_playerRateChangeObserver = player.observe(\.rate, changeHandler: _handlers.handlePlaybackRateChange)
_playerExpernalPlaybackActiveObserver = player.observe(\.isExternalPlaybackActive, changeHandler: _handlers.handleExternalPlaybackActiveChange)
}
func removePlayerObservers() {
_playerRateChangeObserver?.invalidate()
_playerExpernalPlaybackActiveObserver?.invalidate()
}
func addPlayerItemObservers() {
guard let playerItem = playerItem else { return }
_playerItemStatusObserver = playerItem.observe(\.status, options: [.new, .old], changeHandler: _handlers.handlePlayerItemStatusChange)
_playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty)
_playerPlaybackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .old], changeHandler: _handlers.handlePlaybackLikelyToKeepUp)
_playerTimedMetadataObserver = playerItem.observe(\.timedMetadata, options: [.new], changeHandler: _handlers.handleTimeMetadataChange)
}
func removePlayerItemObservers() {
_playerItemStatusObserver?.invalidate()
_playerPlaybackBufferEmptyObserver?.invalidate()
_playerPlaybackLikelyToKeepUpObserver?.invalidate()
_playerTimedMetadataObserver?.invalidate()
}
func addPlayerViewControllerObservers() {
guard let playerViewController = playerViewController else { return }
_playerViewControllerReadyForDisplayObserver = playerViewController.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay)
_playerViewControllerOverlayFrameObserver = playerViewController.contentOverlayView?.observe(\.frame, options: [.new, .old], changeHandler: _handlers.handleViewControllerOverlayViewFrameChange)
}
func removePlayerViewControllerObservers() {
_playerViewControllerReadyForDisplayObserver?.invalidate()
_playerViewControllerOverlayFrameObserver?.invalidate()
}
func addPlayerLayerObserver() {
_playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay)
}
func removePlayerLayerObserver() {
_playerLayerReadyForDisplayObserver?.invalidate()
}
func addPlayerTimeObserver() {
removePlayerTimeObserver()
let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m
// of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html
_timeObserver = player?.addPeriodicTimeObserver(
forInterval: CMTimeMakeWithSeconds(progressUpdateIntervalMS, preferredTimescale: Int32(NSEC_PER_SEC)),
queue:nil,
using:_handlers.handleTimeUpdate
)
}
/* Cancels the previously registered time observer. */
func removePlayerTimeObserver() {
if _timeObserver != nil {
player?.removeTimeObserver(_timeObserver)
_timeObserver = nil
}
}
func addTimeObserverIfNotSet() {
if (_timeObserver == nil) {
addPlayerTimeObserver()
}
}
func replaceTimeObserverIfSet(_ newUpdateInterval:Float64? = nil) {
if let newUpdateInterval = newUpdateInterval {
_progressUpdateInterval = newUpdateInterval
}
if (_timeObserver != nil) {
addPlayerTimeObserver()
}
}
func attachPlayerEventListeners() {
NotificationCenter.default.removeObserver(_handlers,
name:NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object:player?.currentItem)
NotificationCenter.default.addObserver(_handlers,
selector:#selector(RCTPlayerObserverHandler.handlePlayerItemDidReachEnd(notification:)),
name:NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object:player?.currentItem)
NotificationCenter.default.removeObserver(_handlers,
name:NSNotification.Name.AVPlayerItemPlaybackStalled,
object:nil)
NotificationCenter.default.addObserver(_handlers,
selector:#selector(RCTPlayerObserverHandler.handlePlaybackStalled(notification:)),
name:NSNotification.Name.AVPlayerItemPlaybackStalled,
object:nil)
NotificationCenter.default.removeObserver(_handlers,
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object:nil)
NotificationCenter.default.addObserver(_handlers,
selector:#selector(RCTPlayerObserverHandler.handleDidFailToFinishPlaying(notification:)),
name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime,
object:nil)
}
func clearPlayer() {
player = nil
playerItem = nil
NotificationCenter.default.removeObserver(_handlers)
}
}

View File

@@ -0,0 +1,223 @@
import AVFoundation
import MediaAccessibility
import Promises
let RCTVideoUnset = -1
/*!
* Collection of mutating functions
*/
enum RCTPlayerOperations {
static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) {
let type = criteria?.type
let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
// The first few tracks will be audio & video track
let firstTextIndex:Int = 0
for firstTextIndex in 0..<(player?.currentItem?.tracks.count ?? 0) {
if player?.currentItem?.tracks[firstTextIndex].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
break
}
}
var selectedTrackIndex:Int = RCTVideoUnset
if (type == "disabled") {
// Do nothing. We want to ensure option is nil
} else if (type == "language") {
let selectedValue = criteria?.value as? String
for i in 0..<textTracks.count {
let currentTextTrack = textTracks[i]
if (selectedValue == currentTextTrack.language) {
selectedTrackIndex = i
break
}
}
} else if (type == "title") {
let selectedValue = criteria?.value as? String
for i in 0..<textTracks.count {
let currentTextTrack = textTracks[i]
if (selectedValue == currentTextTrack.title) {
selectedTrackIndex = i
break
}
}
} else if (type == "index") {
if let value = criteria?.value, let index = value as? Int {
if textTracks.count > index {
selectedTrackIndex = index
}
}
}
// in the situation that a selected text track is not available (eg. specifies a textTrack not available)
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) as! CFArray
let captionSettings = captioningMediaCharacteristics as? [AnyHashable]
if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) {
selectedTrackIndex = 0 // If we can't find a match, use the first available track
let systemLanguage = NSLocale.preferredLanguages.first
for i in 0..<textTracks.count {
let currentTextTrack = textTracks[i]
if systemLanguage == currentTextTrack.language {
selectedTrackIndex = i
break
}
}
}
}
for i in firstTextIndex..<(player?.currentItem?.tracks.count ?? 0) {
var isEnabled = false
if selectedTrackIndex != RCTVideoUnset {
isEnabled = i == selectedTrackIndex + firstTextIndex
}
player?.currentItem?.tracks[i].isEnabled = isEnabled
}
}
// UNUSED
static func setStreamingText(player:AVPlayer?, criteria:SelectedTrackCriteria?) {
let type = criteria?.type
let group:AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible)
var mediaOption:AVMediaSelectionOption!
if (type == "disabled") {
// Do nothing. We want to ensure option is nil
} else if (type == "language") || (type == "title") {
let value = criteria?.value as? String
for i in 0..<group.options.count {
let currentOption:AVMediaSelectionOption! = group.options[i]
var optionValue:String!
if (type == "language") {
optionValue = currentOption.extendedLanguageTag
} else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as! String
}
if (value == optionValue) {
mediaOption = currentOption
break
}
}
//} else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if (type == "index") {
if let value = criteria?.value, let index = value as? Int {
if group.options.count > index {
mediaOption = group.options[index]
}
}
} else { // default. invalid type or "system"
#if TARGET_OS_TV
// Do noting. Fix for tvOS native audio menu language selector
#else
player?.currentItem?.selectMediaOptionAutomatically(in: group)
return
#endif
}
#if TARGET_OS_TV
// Do noting. Fix for tvOS native audio menu language selector
#else
// If a match isn't found, option will be nil and text tracks will be disabled
player?.currentItem?.select(mediaOption, in:group)
#endif
}
static func setMediaSelectionTrackForCharacteristic(player:AVPlayer?, characteristic:AVMediaCharacteristic, criteria:SelectedTrackCriteria?) {
let type = criteria?.type
let group:AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: characteristic)
var mediaOption:AVMediaSelectionOption!
if (type == "disabled") {
// Do nothing. We want to ensure option is nil
} else if (type == "language") || (type == "title") {
let value = criteria?.value as? String
for i in 0..<group.options.count {
let currentOption:AVMediaSelectionOption! = group.options[i]
var optionValue:String!
if (type == "language") {
optionValue = currentOption.extendedLanguageTag
} else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as? String
}
if (value == optionValue) {
mediaOption = currentOption
break
}
}
//} else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if type == "index" {
if let value = criteria?.value, let index = value as? Int {
if group.options.count > index {
mediaOption = group.options[index]
}
}
} else if let group = group { // default. invalid type or "system"
player?.currentItem?.selectMediaOptionAutomatically(in: group)
return
}
if let group = group {
// If a match isn't found, option will be nil and text tracks will be disabled
player?.currentItem?.select(mediaOption, in:group)
}
}
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)
})
}
}
static func configureAudio(ignoreSilentSwitch:String, mixWithOthers:String) {
let session:AVAudioSession! = AVAudioSession.sharedInstance()
var category:AVAudioSession.Category? = nil
var options:AVAudioSession.CategoryOptions? = nil
if (ignoreSilentSwitch == "ignore") {
category = AVAudioSession.Category.playback
} else if (ignoreSilentSwitch == "obey") {
category = AVAudioSession.Category.ambient
}
if (mixWithOthers == "mix") {
options = .mixWithOthers
} else if (mixWithOthers == "duck") {
options = .duckOthers
}
if let category = category, let options = options {
do {
try session.setCategory(category, options: options)
} catch {
}
} else if let category = category, options == nil {
do {
try session.setCategory(category)
} catch {
}
} else if category == nil, let options = options {
do {
try session.setCategory(session.category, options: options)
} catch {
}
}
}
}

View File

@@ -0,0 +1,167 @@
import AVFoundation
import Promises
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
private var _loadingRequest:AVAssetResourceLoadingRequest?
private var _requestingCertificate:Bool = false
private var _requestingCertificateErrored:Bool = 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 {
_loadingRequest?.finishLoading()
}
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool {
return loadingRequestHandling(renewalRequest)
}
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest:AVAssetResourceLoadingRequest) -> Bool {
return loadingRequestHandling(loadingRequest)
}
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) {
NSLog("didCancelLoadingRequest")
}
func setLicenseResult(_ license:String!) {
guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license),
let _loadingRequest = _loadingRequest else {
setLicenseResultError("No data from JS license response")
return
}
let dataRequest:AVAssetResourceLoadingDataRequest! = _loadingRequest.dataRequest
dataRequest.respond(with: respondData)
_loadingRequest.finishLoading()
}
func setLicenseResultError(_ error:String!) {
if _loadingRequest != nil {
self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error))
}
}
func finishLoadingWithError(error:Error!) -> Bool {
if let _loadingRequest = _loadingRequest, let error = error {
_loadingRequest.finishLoading(with: error as! NSError)
_onVideoError?([
"error": [
"code": NSNumber(value: (error as NSError).code),
"localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription,
"localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "",
"localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "",
"domain": (error as NSError).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 = _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
}
_loadingRequest = loadingRequest
guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else {
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData)
}
var promise: Promise<Data>
if _onGetLicense != nil {
let contentId = _drm.contentId ?? loadingRequest.request.url?.host
promise = RCTVideoDRM.handleWithOnGetLicense(
loadingRequest:loadingRequest,
contentId:contentId,
certificateUrl:_drm.certificateUrl,
base64Certificate:_drm.base64Certificate
) .then{ spcData -> Void in
self._requestingCertificate = true
self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "",
"contentId": contentId,
"spcBase64": spcData.base64EncodedString(options: []),
"target": self._reactTag])
}
} else {
promise = RCTVideoDRM.handleInternalGetLicense(
loadingRequest:loadingRequest,
contentId:_drm.contentId,
licenseServer:_drm.licenseServer,
certificateUrl:_drm.certificateUrl,
base64Certificate:_drm.base64Certificate,
headers:_drm.headers
) .then{ data -> 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
}
}

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

View File

@@ -0,0 +1,103 @@
enum RCTVideoError : Int {
case fromJSPart
case noLicenseServerURL
case licenseRequestNotOk
case noDataFromLicenseRequest
case noSPC
case noDataRequest
case noCertificateData
case noCertificateURL
case noFairplayDRM
case noDRMData
case invalidContentId
}
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 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

@@ -0,0 +1,75 @@
import AVFoundation
enum RCTVideoSave {
static func save(
options:NSDictionary!,
resolve: @escaping RCTPromiseResolveBlock,
reject:@escaping RCTPromiseRejectBlock,
playerItem: AVPlayerItem?
) {
let asset:AVAsset! = playerItem?.asset
guard asset != nil else {
reject("ERROR_ASSET_NIL", "Asset is nil", nil)
return
}
guard let exportSession = AVAssetExportSession(asset: asset, presetName:AVAssetExportPresetHighestQuality) else {
reject("ERROR_COULD_NOT_CREATE_EXPORT_SESSION", "Could not create export session", nil)
return
}
var path:String! = nil
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)
break
case .cancelled:
reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error)
break
default:
resolve(["uri": url.absoluteString])
break
}
})
}
static func generatePathInDirectory(directory: String?, withExtension `extension`: String?) -> String? {
let fileName = UUID().uuidString + (`extension` ?? "")
RCTVideoSave.ensureDirExists(withPath: directory)
return URL(fileURLWithPath: directory ?? "").appendingPathComponent(fileName).path
}
static func cacheDirectoryPath() -> String? {
let array = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path)
return array[0]
}
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 {
return false
}
}
return true
}
}

View File

@@ -0,0 +1,250 @@
import AVFoundation
import Promises
/*!
* Collection of pure functions
*/
enum RCTVideoUtils {
/*!
* Calculates and returns the playable duration of the current player item using its loaded time ranges.
*
* \returns The playable duration of the current player item in seconds.
*/
static func calculatePlayableDuration(_ player:AVPlayer?) -> NSNumber {
guard let player = player,
let video:AVPlayerItem = player.currentItem,
video.status == AVPlayerItem.Status.readyToPlay else {
return 0
}
var effectiveTimeRange:CMTimeRange?
for (_, value) in video.loadedTimeRanges.enumerated() {
let timeRange:CMTimeRange = value.timeRangeValue
if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) {
effectiveTimeRange = timeRange
break
}
}
if let effectiveTimeRange = effectiveTimeRange {
let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange))
if playableDuration > 0 {
return playableDuration as NSNumber
}
}
return 0
}
static func urlFilePath(filepath:NSString!) -> NSURL! {
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(.documentDirectory, .userDomainMask, true)
var relativeFilePath:String! = filepath.lastPathComponent
// the file may be multiple levels below the documents directory
let fileComponents:[String]! = filepath.components(separatedBy: "Documents/")
if fileComponents.count > 1 {
relativeFilePath = fileComponents[1]
}
let path:String! = (paths.first! as NSString).appendingPathComponent(relativeFilePath)
if FileManager.default.fileExists(atPath: path) {
return NSURL.fileURL(withPath: path) as NSURL
}
return nil
}
static func playerItemSeekableTimeRange(_ player:AVPlayer?) -> CMTimeRange {
if let playerItem = player?.currentItem,
playerItem.status == .readyToPlay,
let firstItem = playerItem.seekableTimeRanges.first {
return firstItem.timeRangeValue
}
return (CMTimeRange.zero)
}
static func playerItemDuration(_ player:AVPlayer?) -> CMTime {
if let playerItem = player?.currentItem,
playerItem.status == .readyToPlay {
return(playerItem.duration)
}
return(CMTime.invalid)
}
static func calculateSeekableDuration(_ player:AVPlayer?) -> NSNumber {
let timeRange:CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player)
if CMTIME_IS_NUMERIC(timeRange.duration)
{
return NSNumber(value: CMTimeGetSeconds(timeRange.duration))
}
return 0
}
static func getAudioTrackInfo(_ player:AVPlayer?) -> [AnyObject]! {
guard let player = player else {
return []
}
let audioTracks:NSMutableArray! = NSMutableArray()
let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible)
for i in 0..<(group?.options.count ?? 0) {
let currentOption = group?.options[i]
var title = ""
let values = currentOption?.commonMetadata.map(\.value)
if (values?.count ?? 0) > 0, let value = values?[0] {
title = value as! String
}
let language:String! = currentOption?.extendedLanguageTag ?? ""
let audioTrack = [
"index": NSNumber(value: i),
"title": title,
"language": language
] as [String : Any]
audioTracks.add(audioTrack)
}
return audioTracks as [AnyObject]?
}
static func getTextTrackInfo(_ player:AVPlayer?) -> [TextTrack]! {
guard let player = player else {
return []
}
// if streaming video, we extract the text tracks
var textTracks:[TextTrack] = []
let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible)
for i in 0..<(group?.options.count ?? 0) {
let currentOption = group?.options[i]
var title = ""
let values = currentOption?.commonMetadata.map(\.value)
if (values?.count ?? 0) > 0, let value = values?[0] {
title = value as! String
}
let language:String! = currentOption?.extendedLanguageTag ?? ""
let textTrack = TextTrack([
"index": NSNumber(value: i),
"title": title,
"language": language
])
textTracks.append(textTrack)
}
return textTracks
}
// UNUSED
static func getCurrentTime(playerItem:AVPlayerItem?) -> Float {
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)
}
}

View File

@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
@interface RCTSwiftLog : NSObject
+ (void)error:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line;
+ (void)warn:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line;
+ (void)info:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line;
+ (void)log:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line;
+ (void)trace:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line;
@end

View File

@@ -0,0 +1,32 @@
#import <React/RCTLog.h>
#import "RCTSwiftLog.h"
@implementation RCTSwiftLog
+ (void)info:(NSString *)message file:(NSString *)file line:(NSUInteger)line
{
_RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message);
}
+ (void)warn:(NSString *)message file:(NSString *)file line:(NSUInteger)line
{
_RCTLogNativeInternal(RCTLogLevelWarning, file.UTF8String, (int)line, @"%@", message);
}
+ (void)error:(NSString *)message file:(NSString *)file line:(NSUInteger)line
{
_RCTLogNativeInternal(RCTLogLevelError, file.UTF8String, (int)line, @"%@", message);
}
+ (void)log:(NSString *)message file:(NSString *)file line:(NSUInteger)line
{
_RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message);
}
+ (void)trace:(NSString *)message file:(NSString *)file line:(NSUInteger)line
{
_RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message);
}
@end

View File

@@ -0,0 +1,53 @@
//
// RCTLog.swift
// WebViewExample
//
// Created by Jimmy Dee on 4/5/17.
// Copyright © 2017 Branch Metrics. All rights reserved.
//
/*
* Under at least some conditions, output from NSLog has been unavailable in the RNBranch module.
* Hence that module uses the RCTLog macros from <React/RCTLog.h>. The React logger is nicer than
* NSLog anyway, since it provides log levels with runtime filtering, file and line context and
* an identifier for the thread that logged the message.
*
* This wrapper lets you use functions with the same name in Swift. For example:
*
* RCTLogInfo("application launched")
*
* generates
*
* 2017-04-06 12:31:09.611 [info][tid:main][AppDelegate.swift:18] application launched
*
* This is currently part of this sample app. There may be some issues integrating it into an
* Objective-C library, either react-native-branch or react-native itself, but it may find its
* way into one or the other eventually. Feel free to reuse it as desired.
*/
func RCTLogError(_ message: String, _ file: String=#file, _ line: UInt=#line) {
RCTSwiftLog.error(message, file: file, line: line)
}
func RCTLogWarn(_ message: String, _ file: String=#file, _ line: UInt=#line) {
RCTSwiftLog.warn(message, file: file, line: line)
}
func RCTLogInfo(_ message: String, _ file: String=#file, _ line: UInt=#line) {
RCTSwiftLog.info(message, file: file, line: line)
}
func RCTLog(_ message: String, _ file: String=#file, _ line: UInt=#line) {
RCTSwiftLog.log(message, file: file, line: line)
}
func RCTLogTrace(_ message: String, _ file: String=#file, _ line: UInt=#line) {
RCTSwiftLog.trace(message, file: file, line: line)
}
func DebugLog(_ message: String) {
#if DEBUG
print(message)
#endif
}

View File

@@ -0,0 +1,7 @@
#import <React/RCTViewManager.h>
#import "RCTSwiftLog.h"
#if __has_include(<react-native-video/RCTVideoCache.h>)
#import "RCTVideoCache.h"
#endif

View File

@@ -1,67 +0,0 @@
#import <AVFoundation/AVFoundation.h>
#import "AVKit/AVKit.h"
#import "UIView+FindUIViewController.h"
#import "RCTVideoPlayerViewController.h"
#import "RCTVideoPlayerViewControllerDelegate.h"
#import <React/RCTComponent.h>
#import <React/RCTBridgeModule.h>
#if __has_include(<react-native-video/RCTVideoCache.h>)
#import <react-native-video/RCTVideoCache.h>
#import <DVAssetLoaderDelegate/DVURLAsset.h>
#import <DVAssetLoaderDelegate/DVAssetLoaderDelegate.h>
#endif
@class RCTEventDispatcher;
#if __has_include(<react-native-video/RCTVideoCache.h>)
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, DVAssetLoaderDelegatesDelegate, AVAssetResourceLoaderDelegate>
#elif TARGET_OS_TV
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, AVAssetResourceLoaderDelegate>
#else
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate, AVAssetResourceLoaderDelegate>
#endif
@property (nonatomic, copy) RCTDirectEventBlock onVideoLoadStart;
@property (nonatomic, copy) RCTDirectEventBlock onVideoLoad;
@property (nonatomic, copy) RCTDirectEventBlock onVideoBuffer;
@property (nonatomic, copy) RCTDirectEventBlock onVideoError;
@property (nonatomic, copy) RCTDirectEventBlock onVideoProgress;
@property (nonatomic, copy) RCTDirectEventBlock onBandwidthUpdate;
@property (nonatomic, copy) RCTDirectEventBlock onVideoSeek;
@property (nonatomic, copy) RCTDirectEventBlock onVideoEnd;
@property (nonatomic, copy) RCTDirectEventBlock onTimedMetadata;
@property (nonatomic, copy) RCTDirectEventBlock onVideoAudioBecomingNoisy;
@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillPresent;
@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidPresent;
@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillDismiss;
@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidDismiss;
@property (nonatomic, copy) RCTDirectEventBlock onReadyForDisplay;
@property (nonatomic, copy) RCTDirectEventBlock onPlaybackStalled;
@property (nonatomic, copy) RCTDirectEventBlock onPlaybackResume;
@property (nonatomic, copy) RCTDirectEventBlock onPlaybackRateChange;
@property (nonatomic, copy) RCTDirectEventBlock onVideoExternalPlaybackChange;
@property (nonatomic, copy) RCTDirectEventBlock onPictureInPictureStatusChanged;
@property (nonatomic, copy) RCTDirectEventBlock onRestoreUserInterfaceForPictureInPictureStop;
@property (nonatomic, copy) RCTDirectEventBlock onGetLicense;
typedef NS_ENUM(NSInteger, RCTVideoError) {
RCTVideoErrorFromJSPart,
RCTVideoErrorLicenseRequestNotOk,
RCTVideoErrorNoDataFromLicenseRequest,
RCTVideoErrorNoSPC,
RCTVideoErrorNoDataRequest,
RCTVideoErrorNoCertificateData,
RCTVideoErrorNoCertificateURL,
RCTVideoErrorNoFairplayDRM,
RCTVideoErrorNoDRMData
};
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem;
- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
- (void)setLicenseResult:(NSString * )license;
- (BOOL)setLicenseResultError:(NSString * )error;
@end

File diff suppressed because it is too large Load Diff

1061
ios/Video/RCTVideo.swift Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
@interface RCTVideoManager : RCTViewManager <RCTBridgeModule>
@end

View File

@@ -1,22 +1,7 @@
#import "RCTVideoManager.h"
#import "RCTVideo.h"
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>
#import <AVFoundation/AVFoundation.h>
#import "React/RCTViewManager.h"
@implementation RCTVideoManager
RCT_EXPORT_MODULE();
- (UIView *)view
{
return [[RCTVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
}
- (dispatch_queue_t)methodQueue
{
return self.bridge.uiManager.methodQueue;
}
@interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary);
@@ -49,6 +34,8 @@ RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL);
RCT_EXPORT_VIEW_PROPERTY(localSourceEncryptionKeyScheme, NSString);
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock);
@@ -70,64 +57,18 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onGetLicense, RCTDirectEventBlock);
RCT_REMAP_METHOD(save,
options:(NSDictionary *)options
reactTag:(nonnull NSNumber *)reactTag
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTVideo *> *viewRegistry) {
RCTVideo *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTVideo class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view);
} else {
[view save:options resolve:resolve reject:reject];
}
}];
};
RCT_REMAP_METHOD(setLicenseResult,
license:(NSString *)license
reactTag:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTVideo *> *viewRegistry) {
RCTVideo *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTVideo class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view);
} else {
[view setLicenseResult:license];
}
}];
};
RCT_REMAP_METHOD(setLicenseResultError,
error:(NSString *)error
reactTag:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTVideo *> *viewRegistry) {
RCTVideo *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTVideo class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view);
} else {
[view setLicenseResultError:error];
}
}];
};
RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock);
- (NSDictionary *)constantsToExport
{
return @{
@"ScaleNone": AVLayerVideoGravityResizeAspect,
@"ScaleToFill": AVLayerVideoGravityResize,
@"ScaleAspectFit": AVLayerVideoGravityResizeAspect,
@"ScaleAspectFill": AVLayerVideoGravityResizeAspectFill
};
}
RCT_EXTERN_METHOD(save:(NSDictionary *)options
reactTag:(nonnull NSNumber *)reactTag
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license
reactTag:(nonnull NSNumber *)reactTag)
RCT_EXTERN_METHOD(setLicenseResultError(NSString *)error
reactTag:(nonnull NSNumber *)reactTag)
@end

View File

@@ -0,0 +1,63 @@
import AVFoundation
import React
@objc(RCTVideoManager)
class RCTVideoManager: RCTViewManager {
override func view() -> UIView {
return RCTVideo(eventDispatcher: bridge.eventDispatcher())
}
func methodQueue() -> DispatchQueue {
return bridge.uiManager.methodQueue
}
@objc(save:reactTag:resolver:rejecter:)
func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
bridge.uiManager.prependUIBlock({_ , viewRegistry in
let view = viewRegistry?[reactTag]
if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo {
view.save(options: options, resolve: resolve, reject: reject)
}
})
}
@objc(setLicenseResult:reactTag:)
func setLicenseResult(license: NSString, reactTag: NSNumber) -> Void {
bridge.uiManager.prependUIBlock({_ , viewRegistry in
let view = viewRegistry?[reactTag]
if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo {
view.setLicenseResult(license as String)
}
})
}
@objc(setLicenseResultError:reactTag:)
func setLicenseResultError(error: NSString, reactTag: NSNumber) -> Void {
bridge.uiManager.prependUIBlock({_ , viewRegistry in
let view = viewRegistry?[reactTag]
if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo {
view.setLicenseResultError(error as String)
}
})
}
override func constantsToExport() -> [AnyHashable : Any]? {
return [
"ScaleNone": AVLayerVideoGravity.resizeAspect,
"ScaleToFill": AVLayerVideoGravity.resize,
"ScaleAspectFit": AVLayerVideoGravity.resizeAspect,
"ScaleAspectFill": AVLayerVideoGravity.resizeAspectFill
]
}
override class func requiresMainQueueSetup() -> Bool {
return true
}
}

View File

@@ -1,20 +0,0 @@
//
// RCTVideoPlayerViewController.h
// RCTVideo
//
// Created by Stanisław Chmiela on 31.03.2016.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <AVKit/AVKit.h>
#import "RCTVideo.h"
#import "RCTVideoPlayerViewControllerDelegate.h"
@interface RCTVideoPlayerViewController : AVPlayerViewController
@property (nonatomic, weak) id<RCTVideoPlayerViewControllerDelegate> rctDelegate;
// Optional paramters
@property (nonatomic, weak) NSString* preferredOrientation;
@property (nonatomic) BOOL autorotate;
@end

View File

@@ -1,43 +0,0 @@
#import "RCTVideoPlayerViewController.h"
@interface RCTVideoPlayerViewController ()
@end
@implementation RCTVideoPlayerViewController
- (BOOL)shouldAutorotate {
if (self.autorotate || self.preferredOrientation.lowercaseString == nil || [self.preferredOrientation.lowercaseString isEqualToString:@"all"])
return YES;
return NO;
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[_rctDelegate videoPlayerViewControllerWillDismiss:self];
[_rctDelegate videoPlayerViewControllerDidDismiss:self];
}
#if !TARGET_OS_TV
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskAll;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) {
return UIInterfaceOrientationLandscapeRight;
}
else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) {
return UIInterfaceOrientationPortrait;
}
else { // default case
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
return orientation;
}
}
#endif
@end

View File

@@ -0,0 +1,44 @@
import AVKit
class RCTVideoPlayerViewController: AVPlayerViewController {
var rctDelegate:RCTVideoPlayerViewControllerDelegate!
// Optional paramters
var preferredOrientation:String?
var autorotate:Bool?
func shouldAutorotate() -> Bool {
if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") {
return true
}
return false
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self)
rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self)
}
#if !TARGET_OS_TV
func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return .all
}
func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
if preferredOrientation?.lowercased() == "landscape" {
return .landscapeRight
} else if preferredOrientation?.lowercased() == "portrait" {
return .portrait
} else {
// default case
let orientation = UIApplication.shared.statusBarOrientation
return orientation
}
}
#endif
}

View File

@@ -1,7 +0,0 @@
#import <Foundation/Foundation.h>
#import "AVKit/AVKit.h"
@protocol RCTVideoPlayerViewControllerDelegate <NSObject>
- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController;
- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController;
@end

View File

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

View File

@@ -1,15 +0,0 @@
//
// UIView+FindUIViewController.h
// RCTVideo
//
// Created by Stanisław Chmiela on 31.03.2016.
// Copyright © 2016 Facebook. All rights reserved.
//
// Source: http://stackoverflow.com/a/3732812/1123156
#import <UIKit/UIKit.h>
@interface UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController;
- (id) traverseResponderChainForUIViewController;
@end

View File

@@ -1,21 +0,0 @@
// Source: http://stackoverflow.com/a/3732812/1123156
#import "UIView+FindUIViewController.h"
@implementation UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController {
// convenience function for casting and to "mask" the recursive function
return (UIViewController *)[self traverseResponderChainForUIViewController];
}
- (id) traverseResponderChainForUIViewController {
id nextResponder = [self nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]]) {
return nextResponder;
} else if ([nextResponder isKindOfClass:[UIView class]]) {
return [nextResponder traverseResponderChainForUIViewController];
} else {
return nil;
}
}
@end

View File

@@ -0,0 +1,18 @@
// Source: http://stackoverflow.com/a/3732812/1123156
extension UIView {
func firstAvailableUIViewController() -> UIViewController? {
// convenience function for casting and to "mask" the recursive function
return traverseResponderChainForUIViewController()
}
func traverseResponderChainForUIViewController() -> UIViewController? {
if let nextUIViewController = next as? UIViewController {
return nextUIViewController
} else if let nextUIView = next as? UIView {
return nextUIView.traverseResponderChainForUIViewController()
} else {
return nil
}
}
}