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:
75
ios/Video/Features/RCTPictureInPicture.swift
Normal file
75
ios/Video/Features/RCTPictureInPicture.swift
Normal 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
|
207
ios/Video/Features/RCTPlayerObserver.swift
Normal file
207
ios/Video/Features/RCTPlayerObserver.swift
Normal 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)
|
||||
}
|
||||
}
|
223
ios/Video/Features/RCTPlayerOperations.swift
Normal file
223
ios/Video/Features/RCTPlayerOperations.swift
Normal 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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
167
ios/Video/Features/RCTResourceLoaderDelegate.swift
Normal file
167
ios/Video/Features/RCTResourceLoaderDelegate.swift
Normal 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
|
||||
}
|
||||
}
|
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
103
ios/Video/Features/RCTVideoErrorHandling.swift
Normal file
103
ios/Video/Features/RCTVideoErrorHandling.swift
Normal 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?"
|
||||
])
|
||||
}
|
75
ios/Video/Features/RCTVideoSave.swift
Normal file
75
ios/Video/Features/RCTVideoSave.swift
Normal 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
|
||||
}
|
||||
}
|
250
ios/Video/Features/RCTVideoUtils.swift
Normal file
250
ios/Video/Features/RCTVideoUtils.swift
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user