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) let trackCount:Int! = player?.currentItem?.tracks.count ?? 0 // The first few tracks will be audio & video track var firstTextIndex:Int = 0 for i in 0..<(trackCount) { if player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false { firstTextIndex = i break } } var selectedTrackIndex:Int = RCTVideoUnset if (type == "disabled") { // Select the last text index which is the disabled text track selectedTrackIndex = trackCount - firstTextIndex } else if (type == "language") { let selectedValue = criteria?.value as? String for i in 0.. 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) 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.. index { mediaOption = group.options[index] } } } else { // default. invalid type or "system" #if os(iOS) player?.currentItem?.selectMediaOptionAutomatically(in: group) return #endif } #if os(iOS) // 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! guard group != nil else { return } 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.. 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 { 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(on: .global()) { fulfill, reject in guard CMTimeCompare(current, cmSeekTime) != 0 else { reject(NSError(domain: "", code: 0, userInfo: nil)) 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 audioSession: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 audioSession.setCategory(category, options: options) } catch { debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category and options. Error: \(error).") #if !os(tvOS) // Handle specific set category and option combination error // setCategory:AVAudioSessionCategoryPlayback withOptions:mixWithOthers || duckOthers // Failed to set category, error: 'what' Error Domain=NSOSStatusErrorDomain // https://developer.apple.com/forums/thread/714598 if #available(iOS 16.0, *) { do { debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category to playAndRecord with defaultToSpeaker options.") try audioSession.setCategory(AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.defaultToSpeaker) } catch { debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).") } } #endif } } else if let category = category, options == nil { do { try audioSession.setCategory(category) } catch { debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category. Error: \(error).") } } else if category == nil, let options = options { do { try audioSession.setCategory(audioSession.category, options: options) } catch { debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession options. Error: \(error).") } } } }