diff --git a/.github/workflows/check-android.yml b/.github/workflows/check-android.yml index b5e4e165..1c434a17 100644 --- a/.github/workflows/check-android.yml +++ b/.github/workflows/check-android.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: | - curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ + curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ - name: run ktlint working-directory: ./android/ run: | diff --git a/android/.editorconfig b/android/.editorconfig index 30514ea4..3f7f912c 100644 --- a/android/.editorconfig +++ b/android/.editorconfig @@ -1,6 +1,6 @@ [*.{kt,kts}] indent_style=space -indent_size=2 +indent_size=4 continuation_indent_size=4 insert_final_newline=true max_line_length=160 diff --git a/android/src/main/java/com/brentvatne/common/API/ResizeMode.kt b/android/src/main/java/com/brentvatne/common/api/ResizeMode.kt similarity index 92% rename from android/src/main/java/com/brentvatne/common/API/ResizeMode.kt rename to android/src/main/java/com/brentvatne/common/api/ResizeMode.kt index bc0b9522..c873a04e 100644 --- a/android/src/main/java/com/brentvatne/common/API/ResizeMode.kt +++ b/android/src/main/java/com/brentvatne/common/api/ResizeMode.kt @@ -1,4 +1,4 @@ -package com.brentvatne.common.API +package com.brentvatne.common.api import androidx.annotation.IntDef import java.lang.annotation.Retention @@ -29,10 +29,11 @@ internal object ResizeMode { * Keeps the aspect ratio but takes up the view's size. */ const val RESIZE_MODE_CENTER_CROP = 4 + @JvmStatic @Mode - fun toResizeMode(ordinal: Int): Int { - return when (ordinal) { + fun toResizeMode(ordinal: Int): Int = + when (ordinal) { RESIZE_MODE_FIXED_WIDTH -> RESIZE_MODE_FIXED_WIDTH RESIZE_MODE_FIXED_HEIGHT -> RESIZE_MODE_FIXED_HEIGHT RESIZE_MODE_FILL -> RESIZE_MODE_FILL @@ -40,7 +41,6 @@ internal object ResizeMode { RESIZE_MODE_FIT -> RESIZE_MODE_FIT else -> RESIZE_MODE_FIT } - } @Retention(RetentionPolicy.SOURCE) @IntDef( @@ -51,4 +51,4 @@ internal object ResizeMode { RESIZE_MODE_CENTER_CROP ) annotation class Mode -} \ No newline at end of file +} diff --git a/android/src/main/java/com/brentvatne/common/API/SubtitleStyle.kt b/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt similarity index 97% rename from android/src/main/java/com/brentvatne/common/API/SubtitleStyle.kt rename to android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt index 6e9c16f2..1e32c77a 100644 --- a/android/src/main/java/com/brentvatne/common/API/SubtitleStyle.kt +++ b/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt @@ -1,4 +1,4 @@ -package com.brentvatne.common.API +package com.brentvatne.common.api import com.brentvatne.common.toolbox.ReactBridgeUtils import com.facebook.react.bridge.ReadableMap @@ -24,6 +24,7 @@ class SubtitleStyle private constructor() { private const val PROP_PADDING_TOP = "paddingTop" private const val PROP_PADDING_LEFT = "paddingLeft" private const val PROP_PADDING_RIGHT = "paddingRight" + @JvmStatic fun parse(src: ReadableMap?): SubtitleStyle { val subtitleStyle = SubtitleStyle() @@ -35,4 +36,4 @@ class SubtitleStyle private constructor() { return subtitleStyle } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/brentvatne/common/API/TimedMetadata.kt b/android/src/main/java/com/brentvatne/common/api/TimedMetadata.kt similarity index 85% rename from android/src/main/java/com/brentvatne/common/API/TimedMetadata.kt rename to android/src/main/java/com/brentvatne/common/api/TimedMetadata.kt index 66868f36..affc255b 100644 --- a/android/src/main/java/com/brentvatne/common/API/TimedMetadata.kt +++ b/android/src/main/java/com/brentvatne/common/api/TimedMetadata.kt @@ -1,4 +1,4 @@ -package com.brentvatne.common.API +package com.brentvatne.common.api /* * class to handle timedEvent retrieved from the stream @@ -7,4 +7,4 @@ package com.brentvatne.common.API class TimedMetadata(_identifier: String? = null, _value: String? = null) { var identifier: String? = _identifier var value: String? = _value -} \ No newline at end of file +} diff --git a/android/src/main/java/com/brentvatne/common/API/Track.kt b/android/src/main/java/com/brentvatne/common/api/Track.kt similarity index 88% rename from android/src/main/java/com/brentvatne/common/API/Track.kt rename to android/src/main/java/com/brentvatne/common/api/Track.kt index 5bcd0351..bf5f3020 100644 --- a/android/src/main/java/com/brentvatne/common/API/Track.kt +++ b/android/src/main/java/com/brentvatne/common/api/Track.kt @@ -1,4 +1,4 @@ -package com.brentvatne.common.API +package com.brentvatne.common.api /* * internal representation of audio & text tracks @@ -8,7 +8,8 @@ class Track { var mimeType: String? = null var language: String? = null var isSelected = false + // in bps available only on audio tracks var bitrate = 0 var index = 0 -} \ No newline at end of file +} diff --git a/android/src/main/java/com/brentvatne/common/API/VideoTrack.kt b/android/src/main/java/com/brentvatne/common/api/VideoTrack.kt similarity index 85% rename from android/src/main/java/com/brentvatne/common/API/VideoTrack.kt rename to android/src/main/java/com/brentvatne/common/api/VideoTrack.kt index 8251467e..60e5da26 100644 --- a/android/src/main/java/com/brentvatne/common/API/VideoTrack.kt +++ b/android/src/main/java/com/brentvatne/common/api/VideoTrack.kt @@ -1,4 +1,4 @@ -package com.brentvatne.common.API +package com.brentvatne.common.api /* * internal representation of audio & text tracks @@ -12,4 +12,4 @@ class VideoTrack { var id = -1 var trackId = "" var isSelected = false -} \ No newline at end of file +} diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java index f119cb2f..31cbc27f 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.java @@ -4,9 +4,9 @@ import androidx.annotation.StringDef; import android.view.View; -import com.brentvatne.common.API.TimedMetadata; -import com.brentvatne.common.API.Track; -import com.brentvatne.common.API.VideoTrack; +import com.brentvatne.common.api.TimedMetadata; +import com.brentvatne.common.api.Track; +import com.brentvatne.common.api.VideoTrack; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.WritableArray; diff --git a/android/src/main/java/com/brentvatne/common/toolbox/DebugLog.kt b/android/src/main/java/com/brentvatne/common/toolbox/DebugLog.kt index a1250f8d..4b6f252d 100644 --- a/android/src/main/java/com/brentvatne/common/toolbox/DebugLog.kt +++ b/android/src/main/java/com/brentvatne/common/toolbox/DebugLog.kt @@ -12,8 +12,10 @@ import java.lang.Exception object DebugLog { // log level to display private var level = Log.WARN + // enable thread display in logs private var displayThread = true + // add a common prefix for easy filtering private const val TAG_PREFIX = "RNV" @@ -24,16 +26,15 @@ object DebugLog { } @JvmStatic - private fun getTag(tag: String): String { - return TAG_PREFIX + tag - } + private fun getTag(tag: String): String = TAG_PREFIX + tag @JvmStatic - private fun getMsg(msg: String): String { - return if (displayThread) { + private fun getMsg(msg: String): String = + if (displayThread) { "[" + Thread.currentThread().name + "] " + msg - } else msg - } + } else { + msg + } @JvmStatic fun v(tag: String, msg: String) { @@ -92,4 +93,4 @@ object DebugLog { wtf(tag, "------------------------>" + getMsg(msg)) } } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/brentvatne/common/toolbox/ReactBridgeUtils.kt b/android/src/main/java/com/brentvatne/common/toolbox/ReactBridgeUtils.kt index 1424bbae..65dc57a5 100644 --- a/android/src/main/java/com/brentvatne/common/toolbox/ReactBridgeUtils.kt +++ b/android/src/main/java/com/brentvatne/common/toolbox/ReactBridgeUtils.kt @@ -1,8 +1,8 @@ package com.brentvatne.common.toolbox import com.facebook.react.bridge.Dynamic -import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap import java.util.HashMap /* @@ -53,17 +53,19 @@ object ReactBridgeUtils { @JvmStatic fun safeGetInt(map: ReadableMap?, key: String?): Int { - return safeGetInt(map, key, 0); + return safeGetInt(map, key, 0) } @JvmStatic fun safeGetDouble(map: ReadableMap?, key: String?, fallback: Double): Double { return if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDouble(key) else fallback } + @JvmStatic fun safeGetDouble(map: ReadableMap?, key: String?): Double { - return safeGetDouble(map, key, 0.0); + return safeGetDouble(map, key, 0.0) } + /** * toStringMap converts a [ReadableMap] into a HashMap. * @@ -116,17 +118,16 @@ object ReactBridgeUtils { if (str1 == null || str2 == null) return false // only 1 is null if (str1.size != str2.size) return false // only 1 is null for (i in str1.indices) { - if (str1[i] == str2[i]) // standard check + if (str1[i] == str2[i]) { + // standard check return false + } } return true } @JvmStatic - fun safeStringMapEquals( - first: Map?, - second: Map? - ): Boolean { + fun safeStringMapEquals(first: Map?, second: Map?): Boolean { if (first == null && second == null) return true // both are null if (first == null || second == null) return false // only 1 is null if (first.size != second.size) { diff --git a/android/src/main/java/com/brentvatne/exoplayer/AspectRatioFrameLayout.java b/android/src/main/java/com/brentvatne/exoplayer/AspectRatioFrameLayout.java index e4085319..261f185d 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/AspectRatioFrameLayout.java +++ b/android/src/main/java/com/brentvatne/exoplayer/AspectRatioFrameLayout.java @@ -19,7 +19,7 @@ import android.content.Context; import android.util.AttributeSet; import android.widget.FrameLayout; -import com.brentvatne.common.API.ResizeMode; +import com.brentvatne.common.api.ResizeMode; /** * A {@link FrameLayout} that resizes itself to match a specified aspect ratio. diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 895b472f..8d9a7071 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -25,8 +25,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import com.brentvatne.common.API.ResizeMode; -import com.brentvatne.common.API.SubtitleStyle; +import com.brentvatne.common.api.ResizeMode; +import com.brentvatne.common.api.SubtitleStyle; import java.util.List; diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index f8c2cc95..fef6d247 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -91,11 +91,11 @@ import androidx.media3.extractor.metadata.id3.Id3Frame; import androidx.media3.extractor.metadata.id3.TextInformationFrame; import androidx.media3.ui.LegacyPlayerControlView; -import com.brentvatne.common.API.ResizeMode; -import com.brentvatne.common.API.SubtitleStyle; -import com.brentvatne.common.API.TimedMetadata; -import com.brentvatne.common.API.Track; -import com.brentvatne.common.API.VideoTrack; +import com.brentvatne.common.api.ResizeMode; +import com.brentvatne.common.api.SubtitleStyle; +import com.brentvatne.common.api.TimedMetadata; +import com.brentvatne.common.api.Track; +import com.brentvatne.common.api.VideoTrack; import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.toolbox.DebugLog; import com.brentvatne.react.R; diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 7cf6cbb2..1bd2398c 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -10,8 +10,8 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.RawResourceDataSource; import androidx.media3.exoplayer.DefaultLoadControl; -import com.brentvatne.common.API.ResizeMode; -import com.brentvatne.common.API.SubtitleStyle; +import com.brentvatne.common.api.ResizeMode; +import com.brentvatne.common.api.SubtitleStyle; import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.toolbox.DebugLog; import com.brentvatne.common.toolbox.ReactBridgeUtils; diff --git a/ios/.swiftformat b/ios/.swiftformat index 0bdcb6b7..f18154f3 100644 --- a/ios/.swiftformat +++ b/ios/.swiftformat @@ -1,5 +1,5 @@ --allman false ---indent 2 +--indent 4 --exclude Pods,Generated --disable andOperator @@ -10,4 +10,7 @@ --enable markTypes ---enable isEmpty \ No newline at end of file +--enable isEmpty + +--funcattributes "prev-line" +--maxwidth 160 \ No newline at end of file diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index 7d51a163..bddcfb40 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -6,6 +6,11 @@ disabled_rules: - file_length - cyclomatic_complexity - function_body_length + - function_parameter_count + - empty_string + # TODO: Remove this once all force casts are removed + - force_cast + opt_in_rules: - contains_over_filter_count - contains_over_filter_is_empty @@ -13,7 +18,6 @@ opt_in_rules: - contains_over_range_nil_comparison - empty_collection_literal - empty_count - - empty_string - first_where - flatmap_over_map_reduce - last_where diff --git a/ios/Video/DataStructures/Chapter.swift b/ios/Video/DataStructures/Chapter.swift index 398122b5..7fc4e546 100644 --- a/ios/Video/DataStructures/Chapter.swift +++ b/ios/Video/DataStructures/Chapter.swift @@ -1,12 +1,11 @@ - struct Chapter { let title: String let uri: String? let startTime: Double let endTime: Double - + let json: NSDictionary? - + init(_ json: NSDictionary!) { guard json != nil else { self.json = nil diff --git a/ios/Video/DataStructures/DRMParams.swift b/ios/Video/DataStructures/DRMParams.swift index 64add7e0..b0baba49 100644 --- a/ios/Video/DataStructures/DRMParams.swift +++ b/ios/Video/DataStructures/DRMParams.swift @@ -1,13 +1,13 @@ struct DRMParams { let type: String? let licenseServer: String? - let headers: Dictionary? + let headers: [String: Any]? let contentId: String? let certificateUrl: String? let base64Certificate: Bool? - + let json: NSDictionary? - + init(_ json: NSDictionary!) { guard json != nil else { self.json = nil @@ -25,6 +25,6 @@ struct DRMParams { self.contentId = json["contentId"] as? String self.certificateUrl = json["certificateUrl"] as? String self.base64Certificate = json["base64Certificate"] as? Bool - self.headers = json["headers"] as? Dictionary + self.headers = json["headers"] as? [String: Any] } } diff --git a/ios/Video/DataStructures/SelectedTrackCriteria.swift b/ios/Video/DataStructures/SelectedTrackCriteria.swift index 7d97b8f2..41d68aff 100644 --- a/ios/Video/DataStructures/SelectedTrackCriteria.swift +++ b/ios/Video/DataStructures/SelectedTrackCriteria.swift @@ -1,9 +1,9 @@ struct SelectedTrackCriteria { let type: String let value: Any? - + let json: NSDictionary? - + init(_ json: NSDictionary!) { guard json != nil else { self.json = nil diff --git a/ios/Video/DataStructures/TextTrack.swift b/ios/Video/DataStructures/TextTrack.swift index b0bdad80..4c186b28 100644 --- a/ios/Video/DataStructures/TextTrack.swift +++ b/ios/Video/DataStructures/TextTrack.swift @@ -1,12 +1,11 @@ - 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 diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index 368310b5..e9495a33 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -1,11 +1,10 @@ - struct VideoSource { let type: String? let uri: String? let isNetwork: Bool let isAsset: Bool let shouldCache: Bool - let requestHeaders: Dictionary? + let requestHeaders: [String: Any]? let startPosition: Int64? let cropStart: Int64? let cropEnd: Int64? @@ -14,9 +13,9 @@ struct VideoSource { let subtitle: String? let description: String? let customImageUri: String? - + let json: NSDictionary? - + init(_ json: NSDictionary!) { guard json != nil else { self.json = nil @@ -41,7 +40,7 @@ struct VideoSource { 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 + self.requestHeaders = json["requestHeaders"] as? [String: Any] self.startPosition = json["startPosition"] as? Int64 self.cropStart = json["cropStart"] as? Int64 self.cropEnd = json["cropEnd"] as? Int64 diff --git a/ios/Video/Features/RCTIMAAdsManager.swift b/ios/Video/Features/RCTIMAAdsManager.swift index e8f9b256..4345b8d0 100644 --- a/ios/Video/Features/RCTIMAAdsManager.swift +++ b/ios/Video/Features/RCTIMAAdsManager.swift @@ -1,230 +1,209 @@ #if USE_GOOGLE_IMA -import Foundation -import GoogleInteractiveMediaAds + import Foundation + import GoogleInteractiveMediaAds -class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate { + class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate { + private weak var _video: RCTVideo? + private var _pipEnabled: () -> Bool - private weak var _video: RCTVideo? - private var _pipEnabled:() -> Bool + /* Entry point for the SDK. Used to make ad requests. */ + private var adsLoader: IMAAdsLoader! + /* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */ + private var adsManager: IMAAdsManager! - /* Entry point for the SDK. Used to make ad requests. */ - private var adsLoader: IMAAdsLoader! - /* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */ - private var adsManager: IMAAdsManager! + init(video: RCTVideo!, pipEnabled: @escaping () -> Bool) { + _video = video + _pipEnabled = pipEnabled - init(video:RCTVideo!, pipEnabled:@escaping () -> Bool) { - _video = video - _pipEnabled = pipEnabled - - super.init() - } - - func setUpAdsLoader() { - adsLoader = IMAAdsLoader(settings: nil) - adsLoader.delegate = self - } - - func requestAds() { - guard let _video = _video else {return} - // Create ad display container for ad rendering. - let adDisplayContainer = IMAAdDisplayContainer(adContainer: _video, viewController: _video.reactViewController()) - - let adTagUrl = _video.getAdTagUrl() - let contentPlayhead = _video.getContentPlayhead() - - if adTagUrl != nil && contentPlayhead != nil { - // Create an ad request with our ad tag, display container, and optional user context. - let request = IMAAdsRequest( - adTagUrl: adTagUrl!, - adDisplayContainer: adDisplayContainer, - contentPlayhead: contentPlayhead, - userContext: nil) - - adsLoader.requestAds(with: request) - } - } - - // MARK: - Getters - - func getAdsLoader() -> IMAAdsLoader? { - return adsLoader - } - - func getAdsManager() -> IMAAdsManager? { - return adsManager - } - - // MARK: - IMAAdsLoaderDelegate - - func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { - guard let _video = _video else {return} - // Grab the instance of the IMAAdsManager and set yourself as the delegate. - adsManager = adsLoadedData.adsManager - adsManager?.delegate = self - - - // Create ads rendering settings and tell the SDK to use the in-app browser. - let adsRenderingSettings: IMAAdsRenderingSettings = IMAAdsRenderingSettings(); - adsRenderingSettings.linkOpenerDelegate = self; - adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController(); - - adsManager.initialize(with: adsRenderingSettings) - } - - func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) { - if adErrorData.adError.message != nil { - print("Error loading ads: " + adErrorData.adError.message!) + super.init() } - _video?.setPaused(false) - } - - // MARK: - IMAAdsManagerDelegate - - func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) { - guard let _video = _video else {return} - // Mute ad if the main player is muted - if (_video.isMuted()) { - adsManager.volume = 0; - } - // Play each ad once it has been loaded - if event.type == IMAAdEventType.LOADED { - if (_pipEnabled()) { - return - } - adsManager.start() + func setUpAdsLoader() { + adsLoader = IMAAdsLoader(settings: nil) + adsLoader.delegate = self } - if _video.onReceiveAdEvent != nil { - let type = convertEventToString(event: event.type) + func requestAds() { + guard let _video = _video else { return } + // Create ad display container for ad rendering. + let adDisplayContainer = IMAAdDisplayContainer(adContainer: _video, viewController: _video.reactViewController()) - if (event.adData != nil) { - _video.onReceiveAdEvent?([ - "event": type, - "data": event.adData ?? [String](), - "target": _video.reactTag! - ]); - } else { - _video.onReceiveAdEvent?([ - "event": type, - "target": _video.reactTag! - ]); + let adTagUrl = _video.getAdTagUrl() + let contentPlayhead = _video.getContentPlayhead() + + if adTagUrl != nil && contentPlayhead != nil { + // Create an ad request with our ad tag, display container, and optional user context. + let request = IMAAdsRequest( + adTagUrl: adTagUrl!, + adDisplayContainer: adDisplayContainer, + contentPlayhead: contentPlayhead, + userContext: nil + ) + + adsLoader.requestAds(with: request) } } - } - func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) { - if error.message != nil { - print("AdsManager error: " + error.message!) + // MARK: - Getters + + func getAdsLoader() -> IMAAdsLoader? { + return adsLoader } - guard let _video = _video else {return} - - if _video.onReceiveAdEvent != nil { - _video.onReceiveAdEvent?([ - "event": "ERROR", - "data": [ - "message": error.message ?? "", - "code": error.code, - "type": error.type, - ], - "target": _video.reactTag! - ]) + func getAdsManager() -> IMAAdsManager? { + return adsManager } - // Fall back to playing content - _video.setPaused(false) - } + // MARK: - IMAAdsLoaderDelegate - func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) { - // Pause the content for the SDK to play ads. - _video?.setPaused(true) - _video?.setAdPlaying(true) - } + func adsLoader(_: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { + guard let _video = _video else { return } + // Grab the instance of the IMAAdsManager and set yourself as the delegate. + adsManager = adsLoadedData.adsManager + adsManager?.delegate = self - func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) { - // Resume the content since the SDK is done playing ads (at least for now). - _video?.setAdPlaying(false) - _video?.setPaused(false) - } + // Create ads rendering settings and tell the SDK to use the in-app browser. + let adsRenderingSettings = IMAAdsRenderingSettings() + adsRenderingSettings.linkOpenerDelegate = self + adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController() - // MARK: - IMALinkOpenerDelegate + adsManager.initialize(with: adsRenderingSettings) + } - func linkOpenerDidClose(inAppLink linkOpener: NSObject) { - adsManager?.resume() - } + func adsLoader(_: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) { + if adErrorData.adError.message != nil { + print("Error loading ads: " + adErrorData.adError.message!) + } - // MARK: - Helpers + _video?.setPaused(false) + } - func convertEventToString(event: IMAAdEventType!) -> String { - var result = "UNKNOWN"; + // MARK: - IMAAdsManagerDelegate - switch(event) { + func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) { + guard let _video = _video else { return } + // Mute ad if the main player is muted + if _video.isMuted() { + adsManager.volume = 0 + } + // Play each ad once it has been loaded + if event.type == IMAAdEventType.LOADED { + if _pipEnabled() { + return + } + adsManager.start() + } + + if _video.onReceiveAdEvent != nil { + let type = convertEventToString(event: event.type) + + if event.adData != nil { + _video.onReceiveAdEvent?([ + "event": type, + "data": event.adData ?? [String](), + "target": _video.reactTag!, + ]) + } else { + _video.onReceiveAdEvent?([ + "event": type, + "target": _video.reactTag!, + ]) + } + } + } + + func adsManager(_: IMAAdsManager, didReceive error: IMAAdError) { + if error.message != nil { + print("AdsManager error: " + error.message!) + } + + guard let _video = _video else { return } + + if _video.onReceiveAdEvent != nil { + _video.onReceiveAdEvent?([ + "event": "ERROR", + "data": [ + "message": error.message ?? "", + "code": error.code, + "type": error.type, + ], + "target": _video.reactTag!, + ]) + } + + // Fall back to playing content + _video.setPaused(false) + } + + func adsManagerDidRequestContentPause(_: IMAAdsManager) { + // Pause the content for the SDK to play ads. + _video?.setPaused(true) + _video?.setAdPlaying(true) + } + + func adsManagerDidRequestContentResume(_: IMAAdsManager) { + // Resume the content since the SDK is done playing ads (at least for now). + _video?.setAdPlaying(false) + _video?.setPaused(false) + } + + // MARK: - IMALinkOpenerDelegate + + func linkOpenerDidClose(inAppLink _: NSObject) { + adsManager?.resume() + } + + // MARK: - Helpers + + func convertEventToString(event: IMAAdEventType!) -> String { + var result = "UNKNOWN" + + switch event { case .AD_BREAK_READY: - result = "AD_BREAK_READY"; - break; + result = "AD_BREAK_READY" case .AD_BREAK_ENDED: - result = "AD_BREAK_ENDED"; - break; + result = "AD_BREAK_ENDED" case .AD_BREAK_STARTED: - result = "AD_BREAK_STARTED"; - break; + result = "AD_BREAK_STARTED" case .AD_PERIOD_ENDED: - result = "AD_PERIOD_ENDED"; - break; + result = "AD_PERIOD_ENDED" case .AD_PERIOD_STARTED: - result = "AD_PERIOD_STARTED"; - break; + result = "AD_PERIOD_STARTED" case .ALL_ADS_COMPLETED: - result = "ALL_ADS_COMPLETED"; - break; + result = "ALL_ADS_COMPLETED" case .CLICKED: - result = "CLICK"; - break; + result = "CLICK" case .COMPLETE: - result = "COMPLETED"; - break; + result = "COMPLETED" case .CUEPOINTS_CHANGED: - result = "CUEPOINTS_CHANGED"; - break; + result = "CUEPOINTS_CHANGED" case .FIRST_QUARTILE: - result = "FIRST_QUARTILE"; - break; + result = "FIRST_QUARTILE" case .LOADED: - result = "LOADED"; - break; + result = "LOADED" case .LOG: - result = "LOG"; - break; + result = "LOG" case .MIDPOINT: - result = "MIDPOINT"; - break; + result = "MIDPOINT" case .PAUSE: - result = "PAUSED"; - break; + result = "PAUSED" case .RESUME: - result = "RESUMED"; - break; + result = "RESUMED" case .SKIPPED: - result = "SKIPPED"; - break; + result = "SKIPPED" case .STARTED: - result = "STARTED"; - break; + result = "STARTED" case .STREAM_LOADED: - result = "STREAM_LOADED"; - break; + result = "STREAM_LOADED" case .TAPPED: - result = "TAPPED"; - break; + result = "TAPPED" case .THIRD_QUARTILE: - result = "THIRD_QUARTILE"; - break; + result = "THIRD_QUARTILE" default: - result = "UNKNOWN"; - } + result = "UNKNOWN" + } - return result; + return result + } } -} #endif diff --git a/ios/Video/Features/RCTPictureInPicture.swift b/ios/Video/Features/RCTPictureInPicture.swift index 62bdb813..23dae308 100644 --- a/ios/Video/Features/RCTPictureInPicture.swift +++ b/ios/Video/Features/RCTPictureInPicture.swift @@ -1,75 +1,77 @@ import AVFoundation import AVKit +import Foundation import MediaAccessibility import React -import Foundation #if os(iOS) -class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate { - private var _onPictureInPictureStatusChanged: (() -> Void)? = nil - private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil - private var _restoreUserInterfaceForPIPStopCompletionHandler:((Bool) -> Void)? = nil - private var _pipController:AVPictureInPictureController? - private var _isActive:Bool = false - - init(_ onPictureInPictureStatusChanged: (() -> Void)? = nil, _ onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil) { - _onPictureInPictureStatusChanged = onPictureInPictureStatusChanged - _onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop - } - - func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } - - _onPictureInPictureStatusChanged() - } - - func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } - - _onPictureInPictureStatusChanged() - } - - func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate { + private var _onPictureInPictureStatusChanged: (() -> Void)? + private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? + private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)? + private var _pipController: AVPictureInPictureController? + private var _isActive = false - 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?) { - // Create new controller passing reference to the AVPlayerLayer - _pipController = AVPictureInPictureController(playerLayer:playerLayer!) - if #available(iOS 14.2, *) { - _pipController?.canStartPictureInPictureAutomaticallyFromInline = true + init(_ onPictureInPictureStatusChanged: (() -> Void)? = nil, _ onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil) { + _onPictureInPictureStatusChanged = onPictureInPictureStatusChanged + _onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop } - _pipController?.delegate = self - } - - func setPictureInPicture(_ isActive:Bool) { - if _isActive == isActive { - return + + func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) { + guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } + + _onPictureInPictureStatusChanged() } - _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() - }) + + func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) { + guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } + + _onPictureInPictureStatusChanged() + } + + func pictureInPictureController( + _: AVPictureInPictureController, + restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void + ) { + 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?) { + // Create new controller passing reference to the AVPlayerLayer + _pipController = AVPictureInPictureController(playerLayer: playerLayer!) + if #available(iOS 14.2, *) { + _pipController?.canStartPictureInPictureAutomaticallyFromInline = true + } + _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 { + _pipController.startPictureInPicture() + } + } else if !_isActive && _pipController.isPictureInPictureActive { + DispatchQueue.main.async { + _pipController.stopPictureInPicture() + } + } } } -} #endif diff --git a/ios/Video/Features/RCTPlayerObserver.swift b/ios/Video/Features/RCTPlayerObserver.swift index d5441409..658440bf 100644 --- a/ios/Video/Features/RCTPlayerObserver.swift +++ b/ios/Video/Features/RCTPlayerObserver.swift @@ -2,31 +2,37 @@ import AVFoundation import AVKit import Foundation +// MARK: - RCTPlayerObserverHandlerObjc + @objc protocol RCTPlayerObserverHandlerObjc { - func handleDidFailToFinishPlaying(notification:NSNotification!) - func handlePlaybackStalled(notification:NSNotification!) - func handlePlayerItemDidReachEnd(notification:NSNotification!) - func handleAVPlayerAccess(notification:NSNotification!) + func handleDidFailToFinishPlaying(notification: NSNotification!) + func handlePlaybackStalled(notification: NSNotification!) + func handlePlayerItemDidReachEnd(notification: NSNotification!) + func handleAVPlayerAccess(notification: NSNotification!) } +// MARK: - RCTPlayerObserverHandler + protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc { - func handleTimeUpdate(time:CMTime) - func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) - func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) - func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) - func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) - func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handleTimeUpdate(time: CMTime) + func handleReadyForDisplay(changeObject: Any, change: NSKeyValueObservedChange) + func handleTimeMetadataChange(playerItem: AVPlayerItem, change: NSKeyValueObservedChange<[AVMetadataItem]?>) + func handlePlayerItemStatusChange(playerItem: AVPlayerItem, change: NSKeyValueObservedChange) + func handlePlaybackBufferKeyEmpty(playerItem: AVPlayerItem, change: NSKeyValueObservedChange) + func handlePlaybackLikelyToKeepUp(playerItem: AVPlayerItem, change: NSKeyValueObservedChange) func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange) func handleVolumeChange(player: AVPlayer, change: NSKeyValueObservedChange) func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange) - func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange) + func handleViewControllerOverlayViewFrameChange(overlayView: UIView, change: NSKeyValueObservedChange) } +// MARK: - RCTPlayerObserver + class RCTPlayerObserver: NSObject { weak var _handlers: RCTPlayerObserverHandler? - - var player:AVPlayer? { + + var player: AVPlayer? { willSet { removePlayerObservers() removePlayerTimeObserver() @@ -38,7 +44,8 @@ class RCTPlayerObserver: NSObject { } } } - var playerItem:AVPlayerItem? { + + var playerItem: AVPlayerItem? { willSet { removePlayerItemObservers() } @@ -48,7 +55,8 @@ class RCTPlayerObserver: NSObject { } } } - var playerViewController:AVPlayerViewController? { + + var playerViewController: AVPlayerViewController? { willSet { removePlayerViewControllerObservers() } @@ -58,7 +66,8 @@ class RCTPlayerObserver: NSObject { } } } - var playerLayer:AVPlayerLayer? { + + var playerLayer: AVPlayerLayer? { willSet { removePlayerLayerObserver() } @@ -68,91 +77,108 @@ class RCTPlayerObserver: NSObject { } } } - - private var _progressUpdateInterval:TimeInterval = 250 - private var _timeObserver:Any? - - private var _playerRateChangeObserver:NSKeyValueObservation? - private var _playerVolumeChangeObserver:NSKeyValueObservation? - private var _playerExternalPlaybackActiveObserver: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? - + + private var _progressUpdateInterval: TimeInterval = 250 + private var _timeObserver: Any? + + private var _playerRateChangeObserver: NSKeyValueObservation? + private var _playerVolumeChangeObserver: NSKeyValueObservation? + private var _playerExternalPlaybackActiveObserver: 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 { if let _handlers = _handlers { NotificationCenter.default.removeObserver(_handlers) } } - + func addPlayerObservers() { guard let player = player, let _handlers = _handlers else { return } - + _playerRateChangeObserver = player.observe(\.rate, options: [.old], changeHandler: _handlers.handlePlaybackRateChange) - _playerVolumeChangeObserver = player.observe(\.volume, options: [.old] ,changeHandler: _handlers.handleVolumeChange) + _playerVolumeChangeObserver = player.observe(\.volume, options: [.old], changeHandler: _handlers.handleVolumeChange) _playerExternalPlaybackActiveObserver = player.observe(\.isExternalPlaybackActive, changeHandler: _handlers.handleExternalPlaybackActiveChange) } - + func removePlayerObservers() { _playerRateChangeObserver?.invalidate() _playerExternalPlaybackActiveObserver?.invalidate() } - + func addPlayerItemObservers() { guard let playerItem = playerItem, let _handlers = _handlers 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) + _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, let _handlers = _handlers else { return } - - _playerViewControllerReadyForDisplayObserver = playerViewController.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) - - _playerViewControllerOverlayFrameObserver = playerViewController.contentOverlayView?.observe(\.frame, options: [.new, .old], changeHandler: _handlers.handleViewControllerOverlayViewFrameChange) + + _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() { - guard let _handlers = _handlers else {return} - _playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) + guard let _handlers = _handlers else { return } + _playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) } - + func removePlayerLayerObserver() { _playerLayerReadyForDisplayObserver?.invalidate() } - + func addPlayerTimeObserver() { - guard let _handlers = _handlers else {return} + guard let _handlers = _handlers else { return } removePlayerTimeObserver() - let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000 + 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 + queue: nil, + using: _handlers.handleTimeUpdate ) } - + /* Cancels the previously registered time observer. */ func removePlayerTimeObserver() { if _timeObserver != nil { @@ -160,59 +186,59 @@ class RCTPlayerObserver: NSObject { _timeObserver = nil } } - + func addTimeObserverIfNotSet() { - if (_timeObserver == nil) { + if _timeObserver == nil { addPlayerTimeObserver() } } - - func replaceTimeObserverIfSet(_ newUpdateInterval:Float64? = nil) { + + func replaceTimeObserverIfSet(_ newUpdateInterval: Float64? = nil) { if let newUpdateInterval = newUpdateInterval { _progressUpdateInterval = newUpdateInterval } - if (_timeObserver != nil) { + if _timeObserver != nil { addPlayerTimeObserver() } } - + func attachPlayerEventListeners() { - guard let _handlers = _handlers else {return} + guard let _handlers = _handlers else { return } NotificationCenter.default.removeObserver(_handlers, - name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, - object:player?.currentItem) - + name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object: player?.currentItem) + NotificationCenter.default.addObserver(_handlers, - selector:#selector(RCTPlayerObserverHandler.handlePlayerItemDidReachEnd(notification:)), - name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, - object:player?.currentItem) - + selector: #selector(RCTPlayerObserverHandler.handlePlayerItemDidReachEnd(notification:)), + name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object: player?.currentItem) + NotificationCenter.default.removeObserver(_handlers, - name:NSNotification.Name.AVPlayerItemPlaybackStalled, - object:nil) - + name: NSNotification.Name.AVPlayerItemPlaybackStalled, + object: nil) + NotificationCenter.default.addObserver(_handlers, - selector:#selector(RCTPlayerObserverHandler.handlePlaybackStalled(notification:)), - name:NSNotification.Name.AVPlayerItemPlaybackStalled, - object:nil) - + selector: #selector(RCTPlayerObserverHandler.handlePlaybackStalled(notification:)), + name: NSNotification.Name.AVPlayerItemPlaybackStalled, + object: nil) + NotificationCenter.default.removeObserver(_handlers, name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, - object:nil) - + object: nil) + NotificationCenter.default.addObserver(_handlers, - selector:#selector(RCTPlayerObserverHandler.handleDidFailToFinishPlaying(notification:)), + selector: #selector(RCTPlayerObserverHandler.handleDidFailToFinishPlaying(notification:)), name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, - object:nil) - + object: nil) + NotificationCenter.default.removeObserver(_handlers, name: NSNotification.Name.AVPlayerItemNewAccessLogEntry, object: player?.currentItem) - + NotificationCenter.default.addObserver(_handlers, - selector:#selector(RCTPlayerObserverHandlerObjc.handleAVPlayerAccess(notification:)), + selector: #selector(RCTPlayerObserverHandlerObjc.handleAVPlayerAccess(notification:)), name: NSNotification.Name.AVPlayerItemNewAccessLogEntry, object: player?.currentItem) } - + func clearPlayer() { player = nil playerItem = nil diff --git a/ios/Video/Features/RCTPlayerOperations.swift b/ios/Video/Features/RCTPlayerOperations.swift index 6e82e2e7..885a621f 100644 --- a/ios/Video/Features/RCTPlayerOperations.swift +++ b/ios/Video/Features/RCTPlayerOperations.swift @@ -4,49 +4,48 @@ import Promises let RCTVideoUnset = -1 +// MARK: - RCTPlayerOperations + /*! * Collection of mutating functions */ enum RCTPlayerOperations { - - static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) { + 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 + 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 firstTextIndex = 0 + for i in 0 ..< trackCount where (player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible)) != nil { + firstTextIndex = i + break } - var selectedTrackIndex:Int = RCTVideoUnset + var selectedTrackIndex: Int = RCTVideoUnset - if (type == "disabled") { + if type == "disabled" { // Select the last text index which is the disabled text track selectedTrackIndex = trackCount - firstTextIndex - } else if (type == "language") { + } else if type == "language" { let selectedValue = criteria?.value as? String - for i in 0.. index { selectedTrackIndex = index @@ -58,10 +57,10 @@ enum RCTPlayerOperations { if (type != "disabled") && selectedTrackIndex == RCTVideoUnset { let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) let captionSettings = captioningMediaCharacteristics as? [AnyHashable] - if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) { + 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] @@ -113,7 +112,7 @@ enum RCTPlayerOperations { } } else { // default. invalid type or "system" #if os(tvOS) - // Do noting. Fix for tvOS native audio menu language selector + // Do noting. Fix for tvOS native audio menu language selector #else player?.currentItem?.selectMediaOptionAutomatically(in: group) return @@ -121,38 +120,38 @@ enum RCTPlayerOperations { } #if os(tvOS) - // Do noting. Fix for tvOS native audio menu language selector + // 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) + player?.currentItem?.select(mediaOption, in: group) #endif } - static func setMediaSelectionTrackForCharacteristic(player:AVPlayer?, characteristic:AVMediaCharacteristic, criteria:SelectedTrackCriteria?) { + 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! + let group: AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) + var mediaOption: AVMediaSelectionOption! guard group != nil else { return } - if (type == "disabled") { + 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.. 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)) + static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float) -> Promise { + let timeScale = 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 { @@ -185,26 +183,26 @@ enum RCTPlayerOperations { } if !paused { player.pause() } - player.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ (finished:Bool) in + player.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { (finished: Bool) in fulfill(finished) }) } } - static func configureAudio(ignoreSilentSwitch:String, mixWithOthers:String, audioOutput:String) { - let audioSession:AVAudioSession! = AVAudioSession.sharedInstance() - var category:AVAudioSession.Category? = nil - var options:AVAudioSession.CategoryOptions? = nil + static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) { + let audioSession: AVAudioSession! = AVAudioSession.sharedInstance() + var category: AVAudioSession.Category? + var options: AVAudioSession.CategoryOptions? - if (ignoreSilentSwitch == "ignore") { + if ignoreSilentSwitch == "ignore" { category = audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback - } else if (ignoreSilentSwitch == "obey") { + } else if ignoreSilentSwitch == "obey" { category = AVAudioSession.Category.ambient } - if (mixWithOthers == "mix") { + if mixWithOthers == "mix" { options = .mixWithOthers - } else if (mixWithOthers == "duck") { + } else if mixWithOthers == "duck" { options = .duckOthers } @@ -214,18 +212,21 @@ enum RCTPlayerOperations { } 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(audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.defaultToSpeaker) - } catch { - debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).") + // 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( + audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback, + options: AVAudioSession.CategoryOptions.defaultToSpeaker + ) + } catch { + debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).") + } } - } #endif } } else if let category = category, options == nil { diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift index 07b8e62a..e0bad41b 100644 --- a/ios/Video/Features/RCTResourceLoaderDelegate.swift +++ b/ios/Video/Features/RCTResourceLoaderDelegate.swift @@ -2,17 +2,15 @@ import AVFoundation import Promises class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { - private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:] - private var _requestingCertificate:Bool = false - private var _requestingCertificateErrored:Bool = false + private var _requestingCertificate = false + private var _requestingCertificateErrored = false private var _drm: DRMParams? private var _localSourceEncryptionKeyScheme: String? private var _reactTag: NSNumber? private var _onVideoError: RCTDirectEventBlock? private var _onGetLicense: RCTDirectEventBlock? - - + init( asset: AVURLAsset, drm: DRMParams?, @@ -30,46 +28,45 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes _drm = drm _localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme } - + deinit { for request in _loadingRequests.values { request?.finishLoading() } } - - func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool { + + func resourceLoader(_: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool { return loadingRequestHandling(renewalRequest) } - - func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest:AVAssetResourceLoadingRequest) -> Bool { + + func resourceLoader(_: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { return loadingRequestHandling(loadingRequest) } - - func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) { + + func resourceLoader(_: AVAssetResourceLoader, didCancel _: AVAssetResourceLoadingRequest) { RCTLog("didCancelLoadingRequest") } - func setLicenseResult(_ license:String!,_ licenseUrl: String!) { - + func setLicenseResult(_ license: String!, _ licenseUrl: String!) { // Check if the loading request exists in _loadingRequests based on licenseUrl guard let loadingRequest = _loadingRequests[licenseUrl] else { setLicenseResultError("Loading request for licenseUrl \(licenseUrl) not found", licenseUrl) return } - + // Check if the license data is valid guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license) else { setLicenseResultError("No data from JS license response", licenseUrl) return } - + let dataRequest: AVAssetResourceLoadingDataRequest! = loadingRequest?.dataRequest dataRequest.respond(with: respondData) loadingRequest!.finishLoading() _loadingRequests.removeValue(forKey: licenseUrl) } - - func setLicenseResultError(_ error:String!,_ licenseUrl: String!) { + + func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { // Check if the loading request exists in _loadingRequests based on licenseUrl guard let loadingRequest = _loadingRequests[licenseUrl] else { print("Loading request for licenseUrl \(licenseUrl) not found. Error: \(error)") @@ -78,7 +75,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error), licenseUrl: licenseUrl) } - + func finishLoadingWithError(error: Error!, licenseUrl: String!) -> Bool { // Check if the loading request exists in _loadingRequests based on licenseUrl guard let loadingRequest = _loadingRequests[licenseUrl], let error = error as NSError? else { @@ -94,45 +91,44 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes "localizedDescription": error.localizedDescription ?? "", "localizedFailureReason": error.localizedFailureReason ?? "", "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "", - "domain": error.domain + "domain": error.domain, ], - "target": _reactTag + "target": _reactTag, ]) return false } - - func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { + func loadingRequestHandling(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool { if handleEmbeddedKey(loadingRequest) { return true } - + if _drm != nil { return handleDrm(loadingRequest) } - - return false + + return false } - - func handleEmbeddedKey(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { + + 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 { + + func handleDrm(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool { if _requestingCertificate { return true } else if _requestingCertificateErrored { @@ -142,20 +138,20 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes var requestKey: String = loadingRequest.request.url?.absoluteString ?? "" _loadingRequests[requestKey] = loadingRequest - + guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else { return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey) } - + var promise: Promise 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 + loadingRequest: loadingRequest, + contentId: contentId, + certificateUrl: _drm.certificateUrl, + base64Certificate: _drm.base64Certificate + ).then { spcData in self._requestingCertificate = true self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? loadingRequest.request.url?.absoluteString ?? "", "contentId": contentId ?? "", @@ -164,27 +160,26 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes } } 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() + loadingRequest: loadingRequest, + contentId: _drm.contentId, + licenseServer: _drm.licenseServer, + certificateUrl: _drm.certificateUrl, + base64Certificate: _drm.base64Certificate, + headers: _drm.headers + ).then { data in + guard let dataRequest = loadingRequest.dataRequest else { + throw RCTVideoErrorHandler.noCertificateData } + dataRequest.respond(with: data) + loadingRequest.finishLoading() + } } - - - promise.catch{ error in - self.finishLoadingWithError(error:error, licenseUrl: requestKey) + + promise.catch { error in + self.finishLoadingWithError(error: error, licenseUrl: requestKey) self._requestingCertificateErrored = true } - + return true } } diff --git a/ios/Video/Features/RCTVideoDRM.swift b/ios/Video/Features/RCTVideoDRM.swift index d059bbc4..c47530e5 100644 --- a/ios/Video/Features/RCTVideoDRM.swift +++ b/ios/Video/Features/RCTVideoDRM.swift @@ -1,53 +1,53 @@ import AVFoundation import Promises -struct RCTVideoDRM { - @available(*, unavailable) private init() {} - +enum RCTVideoDRM { static func fetchLicense( licenseServer: String, spcData: Data?, contentId: String, - headers: [String:Any]? + headers: [String: Any]? ) -> Promise { - let request = createLicenseRequest(licenseServer:licenseServer, spcData:spcData, contentId:contentId, headers:headers) - - return Promise(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) + let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers) - guard error == nil else { - print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") - reject(error) - return + return Promise(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) } - 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]? + 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 { @@ -56,104 +56,117 @@ struct RCTVideoDRM { 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) + 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 { return Promise(on: .global()) { fulfill, reject in - var spcError:NSError! + 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 { - return Promise(on: .global()) { fulfill, reject in + static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) -> Promise { + return Promise(on: .global()) { fulfill, reject in guard let certificateStringUrl = certificateStringUrl, let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { - reject(RCTVideoErrorHandler.noCertificateURL) + reject(RCTVideoErrorHandler.noCertificateURL) return } - var certificateData:Data? + var certificateData: Data? do { - certificateData = try Data(contentsOf: certificateURL) - if (base64Certificate != nil) { + 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 { + + static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId: String?, certificateUrl: String?, + base64Certificate: Bool?) -> Promise { let contentIdData = contentId?.data(using: .utf8) - - return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate) - .then{ certificateData -> Promise in + + return RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate) + .then { certificateData -> Promise in guard let contentIdData = contentIdData else { throw RCTVideoError.invalidContentId as! Error } - + return RCTVideoDRM.fetchSpcData( - loadingRequest:loadingRequest, - certificateData:certificateData, - contentIdData:contentIdData + loadingRequest: loadingRequest, + certificateData: certificateData, + contentIdData: contentIdData ) } } - - static func handleInternalGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, licenseServer:String?, certificateUrl:String?, base64Certificate:Bool?, headers: [String:Any]?) -> Promise { + + static func handleInternalGetLicense( + loadingRequest: AVAssetResourceLoadingRequest, + contentId: String?, + licenseServer: String?, + certificateUrl: String?, + base64Certificate: Bool?, + headers: [String: Any]? + ) -> Promise { let url = loadingRequest.request.url - - guard let contentId = contentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with:"") else { + + 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 + + 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 + loadingRequest: loadingRequest, + certificateData: certificateData, + contentIdData: contentIdData ) } - .then{ spcData -> Promise in + .then { spcData -> Promise in guard let licenseServer = licenseServer else { throw RCTVideoError.noLicenseServerURL as! Error } diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index e795aa28..7dc68783 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -1,4 +1,6 @@ -enum RCTVideoError : Int { +// MARK: - RCTVideoError + +enum RCTVideoError: Int { case fromJSPart case noLicenseServerURL case licenseRequestNotOk @@ -12,62 +14,69 @@ enum RCTVideoError : Int { case invalidContentId } +// MARK: - RCTVideoErrorHandler + enum RCTVideoErrorHandler { - static let noDRMData = NSError( domain: "RCTVideo", code: RCTVideoError.noDRMData.rawValue, userInfo: [ NSLocalizedDescriptionKey: "Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: "No drm object found.", - NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?" - ]) - + 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?" - ]) - + 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'?" - ]) - + 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." - ]) - + 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?" - ]) - + 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?" - ]) - + NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?", + ] + ) + static func licenseRequestNotOk(_ statusCode: Int) -> NSError { return NSError( domain: "RCTVideo", @@ -75,29 +84,31 @@ enum RCTVideoErrorHandler { userInfo: [ NSLocalizedDescriptionKey: "Error obtaining license.", NSLocalizedFailureReasonErrorKey: String( - format:"License server responded with status code %li", - (statusCode) + format: "License server responded with status code %li", + statusCode ), - NSLocalizedRecoverySuggestionErrorKey: "Did you send the correct data to the license Server? Is the server ok?" - ]) + 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 - ]) + 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?" - ]) + NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?", + ] + ) } diff --git a/ios/Video/Features/RCTVideoSave.swift b/ios/Video/Features/RCTVideoSave.swift index ff8155ec..76fc2901 100644 --- a/ios/Video/Features/RCTVideoSave.swift +++ b/ios/Video/Features/RCTVideoSave.swift @@ -1,62 +1,57 @@ import AVFoundation enum RCTVideoSave { - static func save( - options:NSDictionary!, + options _: NSDictionary!, resolve: @escaping RCTPromiseResolveBlock, - reject:@escaping RCTPromiseRejectBlock, - + reject: @escaping RCTPromiseRejectBlock, + playerItem: AVPlayerItem? ) { - let asset:AVAsset! = playerItem?.asset - + 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 { + + 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 + var path: String! path = RCTVideoSave.generatePathInDirectory( directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path, - withExtension: ".mp4") - let url:NSURL! = NSURL.fileURL(withPath: path) as NSURL + 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) { + 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? { + + 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? @@ -64,8 +59,7 @@ enum RCTVideoSave { if !(exists && isDir.boolValue) { do { try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil) - } catch { - } + } catch {} if error != nil { return false } diff --git a/ios/Video/Features/RCTVideoTVUtils.swift b/ios/Video/Features/RCTVideoTVUtils.swift index 71f2a4a4..2edcf9c7 100644 --- a/ios/Video/Features/RCTVideoTVUtils.swift +++ b/ios/Video/Features/RCTVideoTVUtils.swift @@ -1,49 +1,48 @@ -import Foundation import AVFoundation import AVKit +import Foundation /*! * Collection of helper functions for tvOS specific features */ #if os(tvOS) -enum RCTVideoTVUtils { - static func makeNavigationMarkerGroups(_ chapters: [Chapter]) -> [AVNavigationMarkersGroup] { - var metadataGroups = [AVTimedMetadataGroup]() + enum RCTVideoTVUtils { + static func makeNavigationMarkerGroups(_ chapters: [Chapter]) -> [AVNavigationMarkersGroup] { + var metadataGroups = [AVTimedMetadataGroup]() - // Iterate over the defined chapters and build a timed metadata group object for each. - chapters.forEach { chapter in - metadataGroups.append(makeTimedMetadataGroup(for: chapter)) + // Iterate over the defined chapters and build a timed metadata group object for each. + chapters.forEach { chapter in + metadataGroups.append(makeTimedMetadataGroup(for: chapter)) + } + + return [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metadataGroups)] } - return [AVNavigationMarkersGroup(title: nil, timedNavigationMarkers: metadataGroups)] - } + static func makeTimedMetadataGroup(for chapter: Chapter) -> AVTimedMetadataGroup { + var metadata = [AVMetadataItem]() - static func makeTimedMetadataGroup(for chapter: Chapter) -> AVTimedMetadataGroup { - var metadata = [AVMetadataItem]() + // Create a metadata item that contains the chapter title. + let titleItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierTitle, value: chapter.title) + metadata.append(titleItem) - // Create a metadata item that contains the chapter title. - let titleItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierTitle, value: chapter.title) - metadata.append(titleItem) + // Create a time range for the metadata group. + let timescale: Int32 = 600 + let startTime = CMTime(seconds: chapter.startTime, preferredTimescale: timescale) + let endTime = CMTime(seconds: chapter.endTime, preferredTimescale: timescale) + let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime) - // Create a time range for the metadata group. - let timescale: Int32 = 600 - let startTime = CMTime(seconds: chapter.startTime, preferredTimescale: timescale) - let endTime = CMTime(seconds: chapter.endTime, preferredTimescale: timescale) - let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime) + // Image + if let imgUri = chapter.uri, + let uri = URL(string: imgUri), + let imgData = try? Data(contentsOf: uri), + let image = UIImage(data: imgData), + let pngData = image.pngData() { + let imageItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierArtwork, value: pngData) + metadata.append(imageItem) + } - // Image - if let imgUri = chapter.uri, - let uri = URL(string: imgUri), - let imgData = try? Data(contentsOf: uri), - let image = UIImage(data: imgData), - let pngData = image.pngData() - { - let imageItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierArtwork, value: pngData) - metadata.append(imageItem) + return AVTimedMetadataGroup(items: metadata, timeRange: timeRange) } - - return AVTimedMetadataGroup(items: metadata, timeRange: timeRange) } -} #endif diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index 6df65d9a..d812c52d 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -1,116 +1,114 @@ import AVFoundation -import Promises import Photos +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?, withSource source:VideoSource?) -> NSNumber { + static func calculatePlayableDuration(_ player: AVPlayer?, withSource source: VideoSource?) -> NSNumber { guard let player = player, - let video:AVPlayerItem = player.currentItem, + let video: AVPlayerItem = player.currentItem, video.status == AVPlayerItem.Status.readyToPlay else { return 0 } - - if (source?.cropStart != nil && source?.cropEnd != nil) { + + if source?.cropStart != nil && source?.cropEnd != nil { return NSNumber(value: (Float64(source?.cropEnd ?? 0) - Float64(source?.cropStart ?? 0)) / 1000) } - - var effectiveTimeRange:CMTimeRange? - for (_, value) in video.loadedTimeRanges.enumerated() { - let timeRange:CMTimeRange = value.timeRangeValue + + var effectiveTimeRange: CMTimeRange? + for value in video.loadedTimeRanges { + let timeRange: CMTimeRange = value.timeRangeValue if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) { effectiveTimeRange = timeRange break } } - + if let effectiveTimeRange = effectiveTimeRange { - let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) + let playableDuration: Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) if playableDuration > 0 { - if (source?.cropStart != nil) { - return NSNumber(value: (playableDuration - Float64(source?.cropStart ?? 0) / 1000)) + if source?.cropStart != nil { + return NSNumber(value: playableDuration - Float64(source?.cropStart ?? 0) / 1000) } - + return playableDuration as NSNumber } } - + return 0 } - static func urlFilePath(filepath:NSString!, searchPath:FileManager.SearchPathDirectory) -> NSURL! { + static func urlFilePath(filepath: NSString!, searchPath: FileManager.SearchPathDirectory) -> 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(searchPath, .userDomainMask, true) - var relativeFilePath:String! = filepath.lastPathComponent + let paths: [String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true) + var relativeFilePath: String! = filepath.lastPathComponent // the file may be multiple levels below the documents directory - let directoryString:String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents"; - let fileComponents:[String]! = filepath.components(separatedBy: directoryString) + let directoryString: String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents" + let fileComponents: [String]! = filepath.components(separatedBy: directoryString) if fileComponents.count > 1 { relativeFilePath = fileComponents[1] } - - let path:String! = (paths.first! as NSString).appendingPathComponent(relativeFilePath) + + 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 { + + 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) + + return CMTimeRange.zero } - - static func playerItemDuration(_ player:AVPlayer?) -> CMTime { + + static func playerItemDuration(_ player: AVPlayer?) -> CMTime { if let playerItem = player?.currentItem, playerItem.status == .readyToPlay { - return(playerItem.duration) + return playerItem.duration } - - return(CMTime.invalid) + + return CMTime.invalid } - - static func calculateSeekableDuration(_ player:AVPlayer?) -> NSNumber { - let timeRange:CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player) - if CMTIME_IS_NUMERIC(timeRange.duration) - { + + 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]! { + + static func getAudioTrackInfo(_ player: AVPlayer?) -> [AnyObject]! { guard let player = player else { return [] } - let audioTracks:NSMutableArray! = NSMutableArray() + let audioTracks: NSMutableArray! = NSMutableArray() let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) - for i in 0..<(group?.options.count ?? 0) { + 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 language: String! = currentOption?.extendedLanguageTag ?? "" let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!) @@ -118,50 +116,50 @@ enum RCTVideoUtils { "index": NSNumber(value: i), "title": title, "language": language ?? "", - "selected": currentOption?.displayName == selectedOption?.displayName - ] as [String : Any] + "selected": currentOption?.displayName == selectedOption?.displayName, + ] as [String: Any] audioTracks.add(audioTrack) } return audioTracks as [AnyObject]? } - - static func getTextTrackInfo(_ player:AVPlayer?) -> [TextTrack]! { + + static func getTextTrackInfo(_ player: AVPlayer?) -> [TextTrack]! { guard let player = player else { return [] } // if streaming video, we extract the text tracks - var textTracks:[TextTrack] = [] + var textTracks: [TextTrack] = [] let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) - for i in 0..<(group?.options.count ?? 0) { + 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 language: String! = currentOption?.extendedLanguageTag ?? "" let selectedOpt = player.currentItem?.currentMediaSelection let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!) let textTrack = TextTrack([ "index": NSNumber(value: i), "title": title, "language": language, - "selected": currentOption?.displayName == selectedOption?.displayName + "selected": currentOption?.displayName == selectedOption?.displayName, ]) textTracks.append(textTrack) } return textTracks } - + // UNUSED - static func getCurrentTime(playerItem:AVPlayerItem?) -> Float { + static func getCurrentTime(playerItem: AVPlayerItem?) -> Float { return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero)) } - - static func base64DataFromBase64String(base64String:String?) -> Data? { + + static func base64DataFromBase64String(base64String: String?) -> Data? { if let base64String = base64String { - return Data(base64Encoded:base64String) + return Data(base64Encoded: base64String) } return nil } @@ -175,76 +173,86 @@ enum RCTVideoUtils { static func extractDataFromCustomSchemeUrl(from url: URL, scheme: String) -> Data? { guard url.scheme == scheme, - let adoptURL = RCTVideoUtils.replaceURLScheme(url:url, scheme: nil) else { return nil } + 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 - + + static func generateMixComposition(_ asset: AVAsset) -> AVMutableComposition { + let mixComposition = AVMutableComposition() + + let videoAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first + // we need videoAsset asset to be not null to get durration later if videoAsset == nil { return mixComposition } - - let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid) + + let videoCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack( + withMediaType: AVMediaType.video, + preferredTrackID: kCMPersistentTrackID_Invalid + ) try? videoCompTrack.insertTimeRange( CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), of: videoAsset, - at: .zero) - - let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first - let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid) + at: .zero + ) + + let audioAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first + let audioCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack( + withMediaType: AVMediaType.audio, + preferredTrackID: kCMPersistentTrackID_Invalid + ) try? audioCompTrack.insertTimeRange( CMTimeRangeMake(start: .zero, duration: audioAsset.timeRange.duration), of: audioAsset, - at: .zero) - + at: .zero + ) + 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.. [TextTrack] { + let videoAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first + var validTextTracks: [TextTrack] = [] + + if let textTracks = textTracks, !textTracks.isEmpty { + 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])) + textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options: (assetOptions as! [String: Any])) } else { - let isDisabledTrack:Bool! = textTracks[i].type == "disabled" - let searchPath:FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory; - textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options:nil) + let isDisabledTrack: Bool! = textTracks[i].type == "disabled" + let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory + textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options: nil) } - let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first - if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset + 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) + let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text, + preferredTrackID: kCMPersistentTrackID_Invalid) if videoAsset != nil { try? textCompTrack.insertTimeRange( CMTimeRangeMake(start: .zero, duration: videoAsset!.timeRange.duration), of: textTrackAsset, - at: .zero) + at: .zero + ) } } } - - let emptyVttFile:TextTrack? = self.createEmptyVttFile() - if (emptyVttFile != nil) { + + let emptyVttFile: TextTrack? = self.createEmptyVttFile() + if emptyVttFile != nil { validTextTracks.append(emptyVttFile!) } - + return validTextTracks } /* - * Create an useless / almost empty VTT file in the list with available tracks. This track gets selected when you give type: "disabled" as the selectedTextTrack + * Create an useless/almost empty VTT file in the list with available tracks. + * This track gets selected when you give type: "disabled" as the selectedTextTrack * This is needed because there is a bug where sideloaded texttracks cannot be disabled in the AVPlayer. Loading this VTT file instead solves that problem. * For more info see: https://github.com/react-native-community/react-native-video/issues/1144 */ @@ -252,7 +260,7 @@ enum RCTVideoUtils { let fileManager = FileManager.default let cachesDirectoryUrl = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] let filePath = cachesDirectoryUrl.appendingPathComponent("empty.vtt").path - + if !fileManager.fileExists(atPath: filePath) { let stringToWrite = "WEBVTT\n\n1\n99:59:59.000 --> 99:59:59.001\n." @@ -262,7 +270,7 @@ enum RCTVideoUtils { return nil } } - + return TextTrack([ "language": "disabled", "title": "EmptyVttFile", @@ -270,15 +278,15 @@ enum RCTVideoUtils { "uri": filePath, ]) } - + static func delay(seconds: Int = 0) -> Promise { - return Promise(on: .global()) { fulfill, reject in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: { + return Promise(on: .global()) { fulfill, _ in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC)) { fulfill(()) - }) + } } } - + static func preparePHAsset(uri: String) -> Promise { return Promise(on: .global()) { fulfill, reject in let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...]) @@ -293,31 +301,31 @@ enum RCTVideoUtils { } } } - - static func prepareAsset(source:VideoSource) -> (asset:AVURLAsset?, assetOptions:NSMutableDictionary?)? { + + static func prepareAsset(source: VideoSource) -> (asset: AVURLAsset?, assetOptions: NSMutableDictionary?)? { guard let sourceUri = source.uri, sourceUri != "" else { return nil } - var asset:AVURLAsset! + var asset: AVURLAsset! let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" let url = source.isNetwork || source.isAsset - ? URL(string: source.uri?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") - : URL(fileURLWithPath: bundlePath) - let assetOptions:NSMutableDictionary! = NSMutableDictionary() - + ? URL(string: source.uri?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") + : 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) + if let headers = source.requestHeaders, !headers.isEmpty { + 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]) + 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) } - + static func createMetadataItems(for mapping: [AVMetadataIdentifier: Any]) -> [AVMetadataItem] { - return mapping.compactMap { createMetadataItem(for:$0, value:$1) } + return mapping.compactMap { createMetadataItem(for: $0, value: $1) } } static func createMetadataItem(for identifier: AVMetadataIdentifier, @@ -329,15 +337,15 @@ enum RCTVideoUtils { item.extendedLanguageTag = "und" return item.copy() as! AVMetadataItem } - - static func createImageMetadataItem(imageUri: String) -> Data? { + + static func createImageMetadataItem(imageUri: String) -> Data? { if let uri = URL(string: imageUri), let imgData = try? Data(contentsOf: uri), let image = UIImage(data: imgData), let pngData = image.pngData() { return pngData } - + return nil } } diff --git a/ios/Video/RCTVideo-Bridging-Header.h b/ios/Video/RCTVideo-Bridging-Header.h index 586eec11..8afe7e3e 100644 --- a/ios/Video/RCTVideo-Bridging-Header.h +++ b/ios/Video/RCTVideo-Bridging-Header.h @@ -1,8 +1,7 @@ -#import -#import "RCTVideoSwiftLog.h" #import "RCTEventDispatcher.h" +#import "RCTVideoSwiftLog.h" +#import #if __has_include() #import "RCTVideoCache.h" #endif - diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 530fe1fd..9ebb9b61 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -2,92 +2,93 @@ import AVFoundation import AVKit import Foundation #if USE_GOOGLE_IMA -import GoogleInteractiveMediaAds + import GoogleInteractiveMediaAds #endif -import React import Promises +import React + +// MARK: - RCTVideo class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { + private var _player: AVPlayer? + private var _playerItem: AVPlayerItem? + private var _source: VideoSource? + private var _playerBufferEmpty = true + private var _playerLayer: AVPlayerLayer? + private var _chapters: [Chapter]? - private var _player:AVPlayer? - private var _playerItem:AVPlayerItem? - private var _source:VideoSource? - private var _playerBufferEmpty:Bool = true - private var _playerLayer:AVPlayerLayer? - private var _chapters:[Chapter]? - - private var _playerViewController:RCTVideoPlayerViewController? - private var _videoURL:NSURL? + private var _playerViewController: RCTVideoPlayerViewController? + private var _videoURL: NSURL? /* DRM */ - private var _drm:DRMParams? + private var _drm: DRMParams? - private var _localSourceEncryptionKeyScheme:String? + private var _localSourceEncryptionKeyScheme: String? /* Required to publish events */ - private var _eventDispatcher:RCTEventDispatcher? - private var _videoLoadStarted:Bool = false + private var _eventDispatcher: RCTEventDispatcher? + private var _videoLoadStarted = false - private var _pendingSeek:Bool = false - private var _pendingSeekTime:Float = 0.0 - private var _lastSeekTime:Float = 0.0 + private var _pendingSeek = false + private var _pendingSeekTime: Float = 0.0 + private var _lastSeekTime: Float = 0.0 /* For sending videoProgress events */ - private var _controls:Bool = false + private var _controls = false /* Keep track of any modifiers, need to be applied after each play */ private var _audioOutput: String = "speaker" - private var _volume:Float = 1.0 - private var _rate:Float = 1.0 - private var _maxBitRate:Float? + private var _volume: Float = 1.0 + private var _rate: Float = 1.0 + private var _maxBitRate: Float? - private var _automaticallyWaitsToMinimizeStalling:Bool = true - private var _muted:Bool = false - private var _paused:Bool = false - private var _repeat:Bool = false - private var _allowsExternalPlayback:Bool = true - private var _textTracks:[TextTrack]? - private var _selectedTextTrackCriteria:SelectedTrackCriteria? - private var _selectedAudioTrackCriteria:SelectedTrackCriteria? - private var _playbackStalled:Bool = false - private var _playInBackground:Bool = false - private var _preventsDisplaySleepDuringVideoPlayback:Bool = true - private var _preferredForwardBufferDuration:Float = 0.0 - private var _playWhenInactive:Bool = false - private var _ignoreSilentSwitch:String! = "inherit" // inherit, ignore, obey - private var _mixWithOthers:String! = "inherit" // inherit, mix, duck - private var _resizeMode:String! = "cover" - private var _fullscreen:Bool = false - private var _fullscreenAutorotate:Bool = true - private var _fullscreenOrientation:String! = "all" - private var _fullscreenPlayerPresented:Bool = false - private var _fullscreenUncontrolPlayerPresented:Bool = false // to call events switching full screen mode from player controls - private var _filterName:String! - private var _filterEnabled:Bool = false - private var _presentingViewController:UIViewController? + private var _automaticallyWaitsToMinimizeStalling = true + private var _muted = false + private var _paused = false + private var _repeat = false + private var _allowsExternalPlayback = true + private var _textTracks: [TextTrack]? + private var _selectedTextTrackCriteria: SelectedTrackCriteria? + private var _selectedAudioTrackCriteria: SelectedTrackCriteria? + private var _playbackStalled = false + private var _playInBackground = false + private var _preventsDisplaySleepDuringVideoPlayback = true + private var _preferredForwardBufferDuration: Float = 0.0 + private var _playWhenInactive = false + private var _ignoreSilentSwitch: String! = "inherit" // inherit, ignore, obey + private var _mixWithOthers: String! = "inherit" // inherit, mix, duck + private var _resizeMode: String! = "cover" + private var _fullscreen = false + private var _fullscreenAutorotate = true + private var _fullscreenOrientation: String! = "all" + private var _fullscreenPlayerPresented = false + private var _fullscreenUncontrolPlayerPresented = false // to call events switching full screen mode from player controls + private var _filterName: String! + private var _filterEnabled = false + private var _presentingViewController: UIViewController? private var _pictureInPictureEnabled = false - private var _startPosition:Float64 = -1 + private var _startPosition: Float64 = -1 /* IMA Ads */ - private var _adTagUrl:String? -#if USE_GOOGLE_IMA - private var _imaAdsManager: RCTIMAAdsManager! - /* Playhead used by the SDK to track content video progress and insert mid-rolls. */ - private var _contentPlayhead: IMAAVPlayerContentPlayhead? -#endif - private var _didRequestAds:Bool = false - private var _adPlaying:Bool = false + private var _adTagUrl: String? + #if USE_GOOGLE_IMA + private var _imaAdsManager: RCTIMAAdsManager! + /* Playhead used by the SDK to track content video progress and insert mid-rolls. */ + private var _contentPlayhead: IMAAVPlayerContentPlayhead? + #endif + private var _didRequestAds = false + private var _adPlaying = false private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? - private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver() + private var _playerObserver: RCTPlayerObserver = .init() -#if USE_VIDEO_CACHING - private let _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler() -#endif + #if USE_VIDEO_CACHING + private let _videoCache: RCTVideoCachingHandler = .init() + #endif -#if os(iOS) - private var _pip:RCTPictureInPicture? = nil -#endif + #if os(iOS) + private var _pip: RCTPictureInPicture? + #endif // Events @objc var onVideoLoadStart: RCTDirectEventBlock? @@ -116,29 +117,31 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc var onGetLicense: RCTDirectEventBlock? @objc var onReceiveAdEvent: RCTDirectEventBlock? - @objc func _onPictureInPictureStatusChanged() { - onPictureInPictureStatusChanged?([ "isActive": NSNumber(value: true)]) + @objc + func _onPictureInPictureStatusChanged() { + onPictureInPictureStatusChanged?(["isActive": NSNumber(value: true)]) } - @objc func _onRestoreUserInterfaceForPictureInPictureStop() { - onPictureInPictureStatusChanged?([ "isActive": NSNumber(value: false)]) + @objc + func _onRestoreUserInterfaceForPictureInPictureStop() { + onPictureInPictureStatusChanged?(["isActive": NSNumber(value: false)]) } - func isPipEnabled () -> Bool { + func isPipEnabled() -> Bool { return _pictureInPictureEnabled } - init(eventDispatcher:RCTEventDispatcher!) { + init(eventDispatcher: RCTEventDispatcher!) { super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) -#if USE_GOOGLE_IMA - _imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled) -#endif + #if USE_GOOGLE_IMA + _imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled) + #endif _eventDispatcher = eventDispatcher -#if os(iOS) - _pip = RCTPictureInPicture(self._onPictureInPictureStatusChanged, self._onRestoreUserInterfaceForPictureInPictureStop) -#endif + #if os(iOS) + _pip = RCTPictureInPicture(self._onPictureInPictureStatusChanged, self._onRestoreUserInterfaceForPictureInPictureStop) + #endif NotificationCenter.default.addObserver( self, @@ -175,16 +178,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH object: nil ) _playerObserver._handlers = self -#if USE_VIDEO_CACHING - _videoCache.playerItemPrepareText = playerItemPrepareText -#endif + #if USE_VIDEO_CACHING + _videoCache.playerItemPrepareText = playerItemPrepareText + #endif } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) -#if USE_GOOGLE_IMA - _imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled) -#endif + #if USE_GOOGLE_IMA + _imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled) + #endif } deinit { @@ -195,14 +198,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - App lifecycle handlers - @objc func applicationWillResignActive(notification:NSNotification!) { - if _playInBackground || _playWhenInactive || _paused {return} + @objc + func applicationWillResignActive(notification _: NSNotification!) { + if _playInBackground || _playWhenInactive || _paused { return } _player?.pause() _player?.rate = 0.0 } - @objc func applicationDidBecomeActive(notification: NSNotification!) { + @objc + func applicationDidBecomeActive(notification _: NSNotification!) { if _playInBackground || _playWhenInactive || _paused { return } // Resume the player or any other tasks that should continue when the app becomes active. @@ -210,7 +215,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _player?.rate = _rate } - @objc func applicationDidEnterBackground(notification:NSNotification!) { + @objc + func applicationDidEnterBackground(notification _: NSNotification!) { if !_playInBackground { // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html _playerLayer?.player = nil @@ -218,7 +224,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } } - @objc func applicationWillEnterForeground(notification:NSNotification!) { + @objc + func applicationWillEnterForeground(notification _: NSNotification!) { self.applyModifiers() if !_playInBackground { _playerLayer?.player = _player @@ -228,9 +235,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Audio events - @objc func audioRouteChanged(notification:NSNotification!) { + @objc + func audioRouteChanged(notification: NSNotification!) { if let userInfo = notification.userInfo { - let reason:AVAudioSession.RouteChangeReason! = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason + let reason: AVAudioSession.RouteChangeReason! = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason // let previousRoute:NSNumber! = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? NSNumber if reason == .oldDeviceUnavailable, let onVideoAudioBecomingNoisy = onVideoAudioBecomingNoisy { onVideoAudioBecomingNoisy(["target": reactTag as Any]) @@ -246,13 +254,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH return } - let playerDuration:CMTime = RCTVideoUtils.playerItemDuration(_player) + let playerDuration: CMTime = RCTVideoUtils.playerItemDuration(_player) if CMTIME_IS_INVALID(playerDuration) { return } var currentTime = _player?.currentTime() - if (currentTime != nil && _source?.cropStart != nil) { + if currentTime != nil && _source?.cropStart != nil { currentTime = CMTimeSubtract(currentTime!, CMTimeMake(value: _source?.cropStart ?? 0, timescale: 1000)) } let currentPlaybackTime = _player?.currentItem?.currentDate() @@ -260,35 +268,36 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) NotificationCenter.default.post(name: NSNotification.Name("RCTVideo_progress"), object: nil, userInfo: [ - "progress": NSNumber(value: currentTimeSecs / duration) + "progress": NSNumber(value: currentTimeSecs / duration), ]) if currentTimeSecs >= 0 { -#if USE_GOOGLE_IMA - if !_didRequestAds && currentTimeSecs >= 0.0001 && _adTagUrl != nil { - _imaAdsManager.requestAds() - _didRequestAds = true - } -#endif + #if USE_GOOGLE_IMA + if !_didRequestAds && currentTimeSecs >= 0.0001 && _adTagUrl != nil { + _imaAdsManager.requestAds() + _didRequestAds = true + } + #endif onVideoProgress?([ "currentTime": NSNumber(value: Float(currentTimeSecs)), "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player, withSource: _source), "atValue": NSNumber(value: currentTime?.value ?? .zero), "currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), "target": reactTag, - "seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player) + "seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player), ]) } } // MARK: - Player and source + @objc - func setSrc(_ source:NSDictionary!) { + func setSrc(_ source: NSDictionary!) { let dispatchClosure = { self._source = VideoSource(source) - if (self._source?.uri == nil || self._source?.uri == "") { + if self._source?.uri == nil || self._source?.uri == "" { self._player?.replaceCurrentItem(with: nil) - return; + return } self.removePlayerLayer() self._playerObserver.player = nil @@ -297,8 +306,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // perform on next run loop, otherwise other passed react-props may not be set RCTVideoUtils.delay() - .then{ [weak self] in - guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} + .then { [weak self] in + guard let self = self else { throw NSError(domain: "", code: 0, userInfo: nil) } guard let source = self._source else { DebugLog("The source not exist") throw NSError(domain: "", code: 0, userInfo: nil) @@ -306,7 +315,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH if let uri = source.uri, uri.starts(with: "ph://") { return Promise { RCTVideoUtils.preparePHAsset(uri: uri).then { asset in - return self.playerItemPrepareText(asset:asset, assetOptions:nil, uri: source.uri ?? "") + return self.playerItemPrepareText(asset: asset, assetOptions: nil, uri: source.uri ?? "") } } } @@ -321,11 +330,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self._startPosition = Float64(startPosition) / 1000 } -#if USE_VIDEO_CACHING - if self._videoCache.shouldCache(source:source, textTracks:self._textTracks) { - return self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions:assetOptions) - } -#endif + #if USE_VIDEO_CACHING + if self._videoCache.shouldCache(source: source, textTracks: self._textTracks) { + return self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions) + } + #endif if self._drm != nil || self._localSourceEncryptionKeyScheme != nil { self._resouceLoaderDelegate = RCTResourceLoaderDelegate( @@ -338,9 +347,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ) } - return Promise{self.playerItemPrepareText(asset: asset, assetOptions:assetOptions, uri: source.uri ?? "")} - }.then{[weak self] (playerItem:AVPlayerItem!) in - guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} + return Promise { self.playerItemPrepareText(asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") } + }.then { [weak self] (playerItem: AVPlayerItem!) in + guard let self = self else { throw NSError(domain: "", code: 0, userInfo: nil) } self._player?.pause() self._playerItem = playerItem @@ -362,42 +371,42 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) } -#if USE_GOOGLE_IMA - if self._adTagUrl != nil { - // Set up your content playhead and contentComplete callback. - self._contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: self._player!) + #if USE_GOOGLE_IMA + if self._adTagUrl != nil { + // Set up your content playhead and contentComplete callback. + self._contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: self._player!) - self._imaAdsManager.setUpAdsLoader() - } -#endif - //Perform on next run loop, otherwise onVideoLoadStart is nil + self._imaAdsManager.setUpAdsLoader() + } + #endif + // Perform on next run loop, otherwise onVideoLoadStart is nil self.onVideoLoadStart?([ "src": [ "uri": self._source?.uri ?? NSNull(), "type": self._source?.type ?? NSNull(), - "isNetwork": NSNumber(value: self._source?.isNetwork ?? false) + "isNetwork": NSNumber(value: self._source?.isNetwork ?? false), ], "drm": self._drm?.json ?? NSNull(), - "target": self.reactTag + "target": self.reactTag, ]) - }.catch{_ in } + }.catch { _ in } self._videoLoadStarted = true } DispatchQueue.global(qos: .default).async(execute: dispatchClosure) } @objc - func setDrm(_ drm:NSDictionary) { + func setDrm(_ drm: NSDictionary) { _drm = DRMParams(drm) } @objc - func setLocalSourceEncryptionKeyScheme(_ keyScheme:String) { + func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) { _localSourceEncryptionKeyScheme = keyScheme } - func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?, uri: String) -> AVPlayerItem { - if (_textTracks == nil) || _textTracks?.count==0 || (uri.hasSuffix(".m3u8")) { + func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) -> AVPlayerItem { + if (_textTracks == nil) || _textTracks?.isEmpty == true || (uri.hasSuffix(".m3u8")) { return self.playerItemPropegateMetadata(AVPlayerItem(asset: asset)) } @@ -405,10 +414,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _allowsExternalPlayback = false let mixComposition = RCTVideoUtils.generateMixComposition(asset) let validTextTracks = RCTVideoUtils.getValidTextTracks( - asset:asset, - assetOptions:assetOptions, - mixComposition:mixComposition, - textTracks:_textTracks) + asset: asset, + assetOptions: assetOptions, + mixComposition: mixComposition, + textTracks: _textTracks + ) if validTextTracks.count != _textTracks?.count { setTextTracks(validTextTracks) } @@ -440,11 +450,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH playerItem.externalMetadata = RCTVideoUtils.createMetadataItems(for: mapping) } -#if os(tvOS) - if let chapters = _chapters { - playerItem.navigationMarkerGroups = RCTVideoTVUtils.makeNavigationMarkerGroups(chapters) - } -#endif + #if os(tvOS) + if let chapters = _chapters { + playerItem.navigationMarkerGroups = RCTVideoTVUtils.makeNavigationMarkerGroups(chapters) + } + #endif return playerItem } @@ -458,16 +468,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH switch mode { case "contain": resizeMode = .resizeAspect - break case "none": resizeMode = .resizeAspect - break case "cover": resizeMode = .resizeAspectFill - break case "stretch": resizeMode = .resize - break default: resizeMode = .resizeAspect } @@ -482,83 +488,82 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setPlayInBackground(_ playInBackground:Bool) { + func setPlayInBackground(_ playInBackground: Bool) { _playInBackground = playInBackground } @objc - func setPreventsDisplaySleepDuringVideoPlayback(_ preventsDisplaySleepDuringVideoPlayback:Bool) { + func setPreventsDisplaySleepDuringVideoPlayback(_ preventsDisplaySleepDuringVideoPlayback: Bool) { _preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback self.applyModifiers() } @objc - func setAllowsExternalPlayback(_ allowsExternalPlayback:Bool) { + func setAllowsExternalPlayback(_ allowsExternalPlayback: Bool) { _allowsExternalPlayback = allowsExternalPlayback _player?.allowsExternalPlayback = _allowsExternalPlayback } @objc - func setPlayWhenInactive(_ playWhenInactive:Bool) { + func setPlayWhenInactive(_ playWhenInactive: Bool) { _playWhenInactive = playWhenInactive } @objc - func setPictureInPicture(_ pictureInPicture:Bool) { -#if os(iOS) - let audioSession = AVAudioSession.sharedInstance() - do { - try audioSession.setCategory(.playback) - try audioSession.setActive(true, options: []) - } catch { - } - if (pictureInPicture) { - _pictureInPictureEnabled = true - } else { - _pictureInPictureEnabled = false - } - _pip?.setPictureInPicture(pictureInPicture) -#endif + func setPictureInPicture(_ pictureInPicture: Bool) { + #if os(iOS) + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.playback) + try audioSession.setActive(true, options: []) + } catch {} + if pictureInPicture { + _pictureInPictureEnabled = true + } else { + _pictureInPictureEnabled = false + } + _pip?.setPictureInPicture(pictureInPicture) + #endif } @objc - func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore:Bool) { -#if os(iOS) - _pip?.setRestoreUserInterfaceForPIPStopCompletionHandler(restore) -#endif + func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore: Bool) { + #if os(iOS) + _pip?.setRestoreUserInterfaceForPIPStopCompletionHandler(restore) + #endif } @objc - func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String?) { + func setIgnoreSilentSwitch(_ ignoreSilentSwitch: String?) { _ignoreSilentSwitch = ignoreSilentSwitch - RCTPlayerOperations.configureAudio(ignoreSilentSwitch:_ignoreSilentSwitch, mixWithOthers:_mixWithOthers, audioOutput:_audioOutput) + RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput) applyModifiers() } @objc - func setMixWithOthers(_ mixWithOthers:String?) { + func setMixWithOthers(_ mixWithOthers: String?) { _mixWithOthers = mixWithOthers applyModifiers() } @objc - func setPaused(_ paused:Bool) { + func setPaused(_ paused: Bool) { if paused { if _adPlaying { -#if USE_GOOGLE_IMA - _imaAdsManager.getAdsManager()?.pause() -#endif + #if USE_GOOGLE_IMA + _imaAdsManager.getAdsManager()?.pause() + #endif } else { _player?.pause() _player?.rate = 0.0 } } else { - RCTPlayerOperations.configureAudio(ignoreSilentSwitch:_ignoreSilentSwitch, mixWithOthers:_mixWithOthers, audioOutput:_audioOutput) + RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput) if _adPlaying { -#if USE_GOOGLE_IMA - _imaAdsManager.getAdsManager()?.resume() -#endif + #if USE_GOOGLE_IMA + _imaAdsManager.getAdsManager()?.resume() + #endif } else { if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling { _player?.playImmediately(atRate: _rate) @@ -574,10 +579,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setSeek(_ info:NSDictionary!) { - let seekTime:NSNumber! = info["time"] as! NSNumber - let seekTolerance:NSNumber! = info["tolerance"] as! NSNumber - let item:AVPlayerItem? = _player?.currentItem + func setSeek(_ info: NSDictionary!) { + let seekTime: NSNumber! = info["time"] as! NSNumber + let seekTolerance: NSNumber! = info["tolerance"] as! NSNumber + let item: AVPlayerItem? = _player?.currentItem guard item != nil, let player = _player, let item = item, item.status == AVPlayerItem.Status.readyToPlay else { _pendingSeek = true _pendingSeekTime = seekTime.floatValue @@ -586,12 +591,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH let wasPaused = _paused RCTPlayerOperations.seek( - player:player, - playerItem:item, - paused:wasPaused, - seekTime:seekTime.floatValue, - seekTolerance:seekTolerance.floatValue) - .then{ [weak self] (finished:Bool) in + player: player, + playerItem: item, + paused: wasPaused, + seekTime: seekTime.floatValue, + seekTolerance: seekTolerance.floatValue + ) + .then { [weak self] (_: Bool) in guard let self = self else { return } self._playerObserver.addTimeObserverIfNotSet() @@ -601,14 +607,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), "seekTime": seekTime, "target": self.reactTag]) - }.catch{_ in } + }.catch { _ in } _pendingSeek = false } - @objc - func setRate(_ rate:Float) { + func setRate(_ rate: Float) { _rate = rate applyModifiers() } @@ -619,20 +624,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setMuted(_ muted:Bool) { + func setMuted(_ muted: Bool) { _muted = muted applyModifiers() } @objc - func setAudioOutput(_ audioOutput:String) { + func setAudioOutput(_ audioOutput: String) { _audioOutput = audioOutput - RCTPlayerOperations.configureAudio(ignoreSilentSwitch:_ignoreSilentSwitch, mixWithOthers:_mixWithOthers, audioOutput:_audioOutput) + RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput) do { if audioOutput == "speaker" { -#if os(iOS) - try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) -#endif + #if os(iOS) + try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) + #endif } else if audioOutput == "earpiece" { try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none) } @@ -642,19 +647,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setVolume(_ volume:Float) { + func setVolume(_ volume: Float) { _volume = volume applyModifiers() } @objc - func setMaxBitRate(_ maxBitRate:Float) { + func setMaxBitRate(_ maxBitRate: Float) { _maxBitRate = maxBitRate _playerItem?.preferredPeakBitRate = Double(maxBitRate) } @objc - func setPreferredForwardBufferDuration(_ preferredForwardBufferDuration:Float) { + func setPreferredForwardBufferDuration(_ preferredForwardBufferDuration: Float) { _preferredForwardBufferDuration = preferredForwardBufferDuration if #available(iOS 10.0, *) { _playerItem?.preferredForwardBufferDuration = TimeInterval(preferredForwardBufferDuration) @@ -664,7 +669,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setAutomaticallyWaitsToMinimizeStalling(_ waits:Bool) { + func setAutomaticallyWaitsToMinimizeStalling(_ waits: Bool) { _automaticallyWaitsToMinimizeStalling = waits if #available(iOS 10.0, *) { _player?.automaticallyWaitsToMinimizeStalling = waits @@ -673,14 +678,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } } - func setPlaybackRange(_ item:AVPlayerItem!, withVideoStart videoStart:Int64?, withVideoEnd videoEnd:Int64?) { - if (videoStart != nil) { + func setPlaybackRange(_ item: AVPlayerItem!, withVideoStart videoStart: Int64?, withVideoEnd videoEnd: Int64?) { + if videoStart != nil { let start = CMTimeMake(value: videoStart!, timescale: 1000) item.reversePlaybackEndTime = start _pendingSeekTime = Float(CMTimeGetSeconds(start)) _pendingSeek = true } - if (videoEnd != nil) { + if videoEnd != nil { item.forwardPlaybackEndTime = CMTimeMake(value: videoEnd!, timescale: 1000) } } @@ -721,59 +726,59 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setRepeat(_ `repeat`: Bool) { + func setRepeat(_ repeat: Bool) { _repeat = `repeat` } @objc - func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary?) { + func setSelectedAudioTrack(_ selectedAudioTrack: NSDictionary?) { setSelectedAudioTrack(SelectedTrackCriteria(selectedAudioTrack)) } - func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria?) { + func setSelectedAudioTrack(_ selectedAudioTrack: SelectedTrackCriteria?) { _selectedAudioTrackCriteria = selectedAudioTrack - RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.audible, - criteria:_selectedAudioTrackCriteria) + RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.audible, + criteria: _selectedAudioTrackCriteria) } @objc - func setSelectedTextTrack(_ selectedTextTrack:NSDictionary?) { + func setSelectedTextTrack(_ selectedTextTrack: NSDictionary?) { setSelectedTextTrack(SelectedTrackCriteria(selectedTextTrack)) } - func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria?) { + func setSelectedTextTrack(_ selectedTextTrack: SelectedTrackCriteria?) { _selectedTextTrackCriteria = selectedTextTrack - if (_textTracks != nil) { // sideloaded text tracks - RCTPlayerOperations.setSideloadedText(player:_player, textTracks:_textTracks, criteria:_selectedTextTrackCriteria) + if _textTracks != nil { // sideloaded text tracks + RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks, criteria: _selectedTextTrackCriteria) } else { // text tracks included in the HLS playlist - RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.legible, - criteria:_selectedTextTrackCriteria) + RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible, + criteria: _selectedTextTrackCriteria) } } @objc - func setTextTracks(_ textTracks:[NSDictionary]?) { + func setTextTracks(_ textTracks: [NSDictionary]?) { setTextTracks(textTracks?.map { TextTrack($0) }) } - func setTextTracks(_ textTracks:[TextTrack]?) { + func setTextTracks(_ textTracks: [TextTrack]?) { _textTracks = textTracks // in case textTracks was set after selectedTextTrack - if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)} + if _selectedTextTrackCriteria != nil { setSelectedTextTrack(_selectedTextTrackCriteria) } } @objc - func setChapters(_ chapters:[NSDictionary]?) { + func setChapters(_ chapters: [NSDictionary]?) { setChapters(chapters?.map { Chapter($0) }) } - func setChapters(_ chapters:[Chapter]?) { + func setChapters(_ chapters: [Chapter]?) { _chapters = chapters } @objc - func setFullscreen(_ fullscreen:Bool) { + func setFullscreen(_ fullscreen: Bool) { if fullscreen && !_fullscreenPlayerPresented && _player != nil { // Ensure player view controller is not null // Controls will be displayed even if it is disabled in configuration @@ -785,12 +790,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _playerViewController?.modalPresentationStyle = .fullScreen // Find the nearest view controller - var viewController:UIViewController! = self.firstAvailableUIViewController() - if (viewController == nil) { - let keyWindow:UIWindow! = UIApplication.shared.keyWindow + var viewController: UIViewController! = self.firstAvailableUIViewController() + if viewController == nil { + let keyWindow: UIWindow! = UIApplication.shared.keyWindow viewController = keyWindow.rootViewController - if viewController.children.count > 0 - { + if !viewController.children.isEmpty { viewController = viewController.children.last } } @@ -800,33 +804,32 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any]) if let playerViewController = _playerViewController { - if(_controls) { + if _controls { // prevents crash https://github.com/react-native-video/react-native-video/issues/3040 self._playerViewController?.removeFromParent() } - viewController.present(playerViewController, animated:true, completion:{ [weak self] in - guard let self = self else {return} + viewController.present(playerViewController, animated: true, completion: { [weak self] in + guard let self = self else { return } // In fullscreen we must display controls self._playerViewController?.showsPlaybackControls = true self._fullscreenPlayerPresented = fullscreen self._playerViewController?.autorotate = self._fullscreenAutorotate self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag]) - }) } } } else if !fullscreen && _fullscreenPlayerPresented, let _playerViewController = _playerViewController { self.videoPlayerViewControllerWillDismiss(playerViewController: _playerViewController) - _presentingViewController?.dismiss(animated: true, completion:{[weak self] in + _presentingViewController?.dismiss(animated: true, completion: { [weak self] in self?.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController) }) } } @objc - func setFullscreenAutorotate(_ autorotate:Bool) { + func setFullscreenAutorotate(_ autorotate: Bool) { _fullscreenAutorotate = autorotate if _fullscreenPlayerPresented { _playerViewController?.autorotate = autorotate @@ -834,7 +837,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setFullscreenOrientation(_ orientation:String?) { + func setFullscreenOrientation(_ orientation: String?) { _fullscreenOrientation = orientation if _fullscreenPlayerPresented { _playerViewController?.preferredOrientation = orientation @@ -845,7 +848,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH guard let _player = _player, let _playerItem = _playerItem else { return } if _playerViewController == nil { - _playerViewController = createPlayerViewController(player:_player, withPlayerItem:_playerItem) + _playerViewController = createPlayerViewController(player: _player, withPlayerItem: _playerItem) } // to prevent video from being animated when resizeMode is 'cover' // resize mode must be set before subview is added @@ -854,7 +857,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH guard let _playerViewController = _playerViewController else { return } if _controls { - let viewController:UIViewController! = self.reactViewController() + let viewController: UIViewController! = self.reactViewController() viewController?.addChild(_playerViewController) self.addSubview(_playerViewController.view) } @@ -862,7 +865,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _playerObserver.playerViewController = _playerViewController } - func createPlayerViewController(player:AVPlayer, withPlayerItem playerItem:AVPlayerItem) -> RCTVideoPlayerViewController { + func createPlayerViewController(player: AVPlayer, withPlayerItem _: AVPlayerItem) -> RCTVideoPlayerViewController { let viewController = RCTVideoPlayerViewController() viewController.showsPlaybackControls = self._controls viewController.rctDelegate = self @@ -891,24 +894,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self.layer.addSublayer(_playerLayer) } self.layer.needsDisplayOnBoundsChange = true -#if os(iOS) - _pip?.setupPipController(_playerLayer) -#endif + #if os(iOS) + _pip?.setupPipController(_playerLayer) + #endif } } @objc - func setControls(_ controls:Bool) { - if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) - { + func setControls(_ controls: Bool) { + if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) { _controls = controls - if _controls - { + if _controls { self.removePlayerLayer() self.usePlayerViewController() - } - else - { + } else { _playerViewController?.view.removeFromSuperview() _playerViewController?.removeFromParent() _playerViewController = nil @@ -919,7 +918,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setProgressUpdateInterval(_ progressUpdateInterval:Float) { + func setProgressUpdateInterval(_ progressUpdateInterval: Float) { _playerObserver.replaceTimeObserverIfSet(Float64(progressUpdateInterval)) } @@ -931,14 +930,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - RCTVideoPlayerViewControllerDelegate - func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) { - if _playerViewController == playerViewController && _fullscreenPlayerPresented, let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss { + func videoPlayerViewControllerWillDismiss(playerViewController: AVPlayerViewController) { + if _playerViewController == playerViewController + && _fullscreenPlayerPresented, + let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss { _playerObserver.removePlayerViewControllerObservers() onVideoFullscreenPlayerWillDismiss(["target": reactTag as Any]) } } - func videoPlayerViewControllerDidDismiss(playerViewController:AVPlayerViewController) { + func videoPlayerViewControllerDidDismiss(playerViewController: AVPlayerViewController) { if _playerViewController == playerViewController && _fullscreenPlayerPresented { _fullscreenPlayerPresented = false _presentingViewController = nil @@ -951,7 +952,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setFilter(_ filterName:String!) { + func setFilter(_ filterName: String!) { _filterName = filterName if !_filterEnabled { @@ -962,27 +963,28 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH return } - let filter:CIFilter! = CIFilter(name: filterName) + let filter: CIFilter! = CIFilter(name: filterName) if #available(iOS 9.0, *), let _playerItem = _playerItem { self._playerItem?.videoComposition = AVVideoComposition( asset: _playerItem.asset, - applyingCIFiltersWithHandler: { (request:AVAsynchronousCIImageFilteringRequest) in + applyingCIFiltersWithHandler: { (request: AVAsynchronousCIImageFilteringRequest) in if filter == nil { - request.finish(with: request.sourceImage, context:nil) + request.finish(with: request.sourceImage, context: nil) } else { - let image:CIImage! = request.sourceImage.clampedToExtent() - filter.setValue(image, forKey:kCIInputImageKey) - let output:CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent) - request.finish(with: output, context:nil) + let image: CIImage! = request.sourceImage.clampedToExtent() + filter.setValue(image, forKey: kCIInputImageKey) + let output: CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent) + request.finish(with: output, context: nil) } - }) + } + ) } else { // Fallback on earlier versions } } @objc - func setFilterEnabled(_ filterEnabled:Bool) { + func setFilterEnabled(_ filterEnabled: Bool) { _filterEnabled = filterEnabled } @@ -993,31 +995,32 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setAdTagUrl(_ adTagUrl:String!) { + func setAdTagUrl(_ adTagUrl: String!) { _adTagUrl = adTagUrl } -#if USE_GOOGLE_IMA - func getContentPlayhead() -> IMAAVPlayerContentPlayhead? { - return _contentPlayhead - } -#endif - func setAdPlaying(_ adPlaying:Bool) { + + #if USE_GOOGLE_IMA + func getContentPlayhead() -> IMAAVPlayerContentPlayhead? { + return _contentPlayhead + } + #endif + func setAdPlaying(_ adPlaying: Bool) { _adPlaying = adPlaying } // MARK: - React View Management - func insertReactSubview(view:UIView!, atIndex:Int) { + func insertReactSubview(view: UIView!, atIndex: Int) { if _controls { view.frame = self.bounds - _playerViewController?.contentOverlayView?.insertSubview(view, at:atIndex) + _playerViewController?.contentOverlayView?.insertSubview(view, at: atIndex) } else { RCTLogError("video cannot have any subviews") } return } - func removeReactSubview(subview:UIView!) { + func removeReactSubview(subview: UIView!) { if _controls { subview.removeFromSuperview() } else { @@ -1063,6 +1066,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } _eventDispatcher = nil + // swiftlint:disable:next notification_center_detachment NotificationCenter.default.removeObserver(self) super.removeFromSuperview() @@ -1071,20 +1075,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Export @objc - func save(options:NSDictionary!, resolve: @escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) { + func save(options: NSDictionary!, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { RCTVideoSave.save( - options:options, - resolve:resolve, - reject:reject, - playerItem:_playerItem + options: options, + resolve: resolve, + reject: reject, + playerItem: _playerItem ) } - func setLicenseResult(_ license:String!, _ licenseUrl: String!) { + func setLicenseResult(_ license: String!, _ licenseUrl: String!) { _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl) } - func setLicenseResultError(_ error:String!, _ licenseUrl: String!) { + func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) } @@ -1098,40 +1102,40 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - RCTPlayerObserverHandler - func handleTimeUpdate(time:CMTime) { + func handleTimeUpdate(time _: CMTime) { sendProgressUpdate() } - func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) { + func handleReadyForDisplay(changeObject _: Any, change _: NSKeyValueObservedChange) { onReadyForDisplay?([ - "target": reactTag + "target": reactTag, ]) } // When timeMetadata is read the event onTimedMetadata is triggered - func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) { - guard let newValue = change.newValue, let _items = newValue, _items.count > 0 else { + func handleTimeMetadataChange(playerItem _: AVPlayerItem, change: NSKeyValueObservedChange<[AVMetadataItem]?>) { + guard let newValue = change.newValue, let _items = newValue, !_items.isEmpty else { return } - var metadata: [[String:String?]?] = [] + var metadata: [[String: String?]?] = [] for item in _items { let value = item.value as? String let identifier = item.identifier?.rawValue if let value = value { - metadata.append(["value":value, "identifier":identifier]) + metadata.append(["value": value, "identifier": identifier]) } } onTimedMetadata?([ "target": reactTag, - "metadata": metadata + "metadata": metadata, ]) } // Handle player item status change. - func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + func handlePlayerItemStatusChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange) { guard let _playerItem = _playerItem else { return } @@ -1145,17 +1149,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func handleReadyToPlay() { guard let _playerItem = _playerItem else { return } - var duration:Float = Float(CMTimeGetSeconds(_playerItem.asset.duration)) + var duration = Float(CMTimeGetSeconds(_playerItem.asset.duration)) if duration.isNaN { duration = 0.0 } - var width: Float? = nil - var height: Float? = nil + var width: Float? + var height: Float? var orientation = "undefined" - if _playerItem.asset.tracks(withMediaType: AVMediaType.video).count > 0 { + if !_playerItem.asset.tracks(withMediaType: AVMediaType.video).isEmpty { let videoTrack = _playerItem.asset.tracks(withMediaType: .video)[0] width = Float(videoTrack.naturalSize.width) height = Float(videoTrack.naturalSize.height) @@ -1163,8 +1167,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH if (videoTrack.naturalSize.width == preferredTransform.tx && videoTrack.naturalSize.height == preferredTransform.ty) - || (preferredTransform.tx == 0 && preferredTransform.ty == 0) - { + || (preferredTransform.tx == 0 && preferredTransform.ty == 0) { orientation = "landscape" } else { orientation = "portrait" @@ -1178,7 +1181,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH if _pendingSeek { setSeek([ "time": NSNumber(value: _pendingSeekTime), - "tolerance": NSNumber(value: 100) + "tolerance": NSNumber(value: 100), ]) _pendingSeek = false } @@ -1186,7 +1189,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH if _startPosition >= 0 { setSeek([ "time": NSNumber(value: _startPosition), - "tolerance": NSNumber(value: 100) + "tolerance": NSNumber(value: 100), ]) _startPosition = -1 } @@ -1203,9 +1206,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "canStepBackward": NSNumber(value: _playerItem.canStepBackward), "canStepForward": NSNumber(value: _playerItem.canStepForward), "naturalSize": [ - "width": width != nil ? NSNumber(value: width!) : "undefinded", - "height": width != nil ? NSNumber(value: height!) : "undefinded", - "orientation": orientation + "width": width != nil ? NSNumber(value: width!) : "undefinded", + "height": width != nil ? NSNumber(value: height!) : "undefinded", + "orientation": orientation, ], "audioTracks": audioTracks, "textTracks": textTracks, @@ -1223,21 +1226,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "error": [ "code": NSNumber(value: (_playerItem.error! as NSError).code), "localizedDescription": _playerItem.error?.localizedDescription == nil ? "" : _playerItem.error?.localizedDescription, - "localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ? "" : (_playerItem.error! as NSError).localizedFailureReason) ?? "", - "localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ? "" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "", - "domain": (_playerItem.error as! NSError).domain + "localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ? + "" : (_playerItem.error! as NSError).localizedFailureReason) ?? "", + "localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ? + "" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "", + "domain": (_playerItem.error as! NSError).domain, ], - "target": reactTag - ]) + "target": reactTag, + ] + ) } - func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + func handlePlaybackBufferKeyEmpty(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange) { _playerBufferEmpty = true onVideoBuffer?(["isBuffering": true, "target": reactTag as Any]) } // Continue playing (or not if paused) after being paused due to hitting an unbuffered zone. - func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + func handlePlaybackLikelyToKeepUp(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange) { if (!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && ((_playerItem?.isPlaybackLikelyToKeepUp) == true) { setPaused(_paused) } @@ -1248,7 +1254,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange) { guard let _player = _player else { return } - if(player.rate == change.oldValue && change.oldValue != nil) { + if player.rate == change.oldValue && change.oldValue != nil { return } @@ -1268,37 +1274,37 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func handleVolumeChange(player: AVPlayer, change: NSKeyValueObservedChange) { guard let _player = _player else { return } - if(player.rate == change.oldValue && change.oldValue != nil) { - return + if player.rate == change.oldValue && change.oldValue != nil { + return } onVolumeChange?(["volume": NSNumber(value: _player.volume), "target": reactTag as Any]) } - func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange) { + func handleExternalPlaybackActiveChange(player _: AVPlayer, change _: NSKeyValueObservedChange) { guard let _player = _player else { return } onVideoExternalPlaybackChange?(["isExternalPlaybackActive": NSNumber(value: _player.isExternalPlaybackActive), "target": reactTag as Any]) } - func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange) { + func handleViewControllerOverlayViewFrameChange(overlayView _: UIView, change: NSKeyValueObservedChange) { let oldRect = change.oldValue let newRect = change.newValue if !oldRect!.equalTo(newRect!) { // https://github.com/react-native-video/react-native-video/issues/3085#issuecomment-1557293391 if newRect!.equalTo(UIScreen.main.bounds) { RCTLog("in fullscreen") - if (!_fullscreenUncontrolPlayerPresented) { - _fullscreenUncontrolPlayerPresented = true; + if !_fullscreenUncontrolPlayerPresented { + _fullscreenUncontrolPlayerPresented = true self.onVideoFullscreenPlayerWillPresent?(["target": self.reactTag as Any]) self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag as Any]) } } else { NSLog("not fullscreen") - if (_fullscreenUncontrolPlayerPresented) { - _fullscreenUncontrolPlayerPresented = false; + if _fullscreenUncontrolPlayerPresented { + _fullscreenUncontrolPlayerPresented = false self.onVideoFullscreenPlayerWillDismiss?(["target": self.reactTag as Any]) self.onVideoFullscreenPlayerDidDismiss?(["target": self.reactTag as Any]) @@ -1310,8 +1316,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } } - @objc func handleDidFailToFinishPlaying(notification:NSNotification!) { - let error:NSError! = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError + @objc + func handleDidFailToFinishPlaying(notification: NSNotification!) { + let error: NSError! = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError onVideoError?( [ "error": [ @@ -1319,37 +1326,41 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "localizedDescription": error.localizedDescription ?? "", "localizedFailureReason": (error as NSError).localizedFailureReason ?? "", "localizedRecoverySuggestion": (error as NSError).localizedRecoverySuggestion ?? "", - "domain": (error as NSError).domain + "domain": (error as NSError).domain, ], - "target": reactTag - ]) + "target": reactTag, + ] + ) } - @objc func handlePlaybackStalled(notification:NSNotification!) { + @objc + func handlePlaybackStalled(notification _: NSNotification!) { onPlaybackStalled?(["target": reactTag as Any]) _playbackStalled = true } - @objc func handlePlayerItemDidReachEnd(notification:NSNotification!) { + @objc + func handlePlayerItemDidReachEnd(notification: NSNotification!) { onVideoEnd?(["target": reactTag as Any]) -#if USE_GOOGLE_IMA - if notification.object as? AVPlayerItem == _player?.currentItem { - _imaAdsManager.getAdsLoader()?.contentComplete() - } -#endif + #if USE_GOOGLE_IMA + if notification.object as? AVPlayerItem == _player?.currentItem { + _imaAdsManager.getAdsLoader()?.contentComplete() + } + #endif if _repeat { - let item:AVPlayerItem! = notification.object as? AVPlayerItem + let item: AVPlayerItem! = notification.object as? AVPlayerItem item.seek(to: CMTime.zero, completionHandler: nil) self.applyModifiers() } else { - self.setPaused(true); + self.setPaused(true) _playerObserver.removePlayerTimeObserver() } } - @objc func handleAVPlayerAccess(notification:NSNotification!) { - let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog() - let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last + @objc + func handleAVPlayerAccess(notification: NSNotification!) { + let accessLog: AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog() + let lastEvent: AVPlayerItemAccessLogEvent! = accessLog.events.last onVideoBandwidthUpdate?(["bitrate": lastEvent.observedBitrate, "target": reactTag]) } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 291868eb..c64db775 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -1,7 +1,7 @@ -#import #import "React/RCTViewManager.h" +#import -@interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) +@interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary); @@ -65,27 +65,22 @@ RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock); -RCT_EXTERN_METHOD(save:(NSDictionary *)options - reactTag:(nonnull NSNumber *)reactTag - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(save + : (NSDictionary*)options reactTag + : (nonnull NSNumber*)reactTag resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license - licenseUrl:(NSString *)licenseUrl - reactTag:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(setLicenseResult : (NSString*)license licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag) -RCT_EXTERN_METHOD(setLicenseResultError:(NSString *)error - licenseUrl:(NSString *)licenseUrl - reactTag:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(setLicenseResultError : (NSString*)error licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag) -RCT_EXTERN_METHOD(setPlayerPauseState:(nonnull NSNumber *)paused - reactTag:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(setPlayerPauseState : (nonnull NSNumber*)paused reactTag : (nonnull NSNumber*)reactTag) -RCT_EXTERN_METHOD(presentFullscreenPlayer:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(presentFullscreenPlayer : (nonnull NSNumber*)reactTag) -RCT_EXTERN_METHOD(dismissFullscreenPlayer:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(dismissFullscreenPlayer : (nonnull NSNumber*)reactTag) -RCT_EXTERN_METHOD(dismissFullscreenPlayer - reactTag:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(dismissFullscreenPlayer reactTag : (nonnull NSNumber*)reactTag) @end diff --git a/ios/Video/RCTVideoManager.swift b/ios/Video/RCTVideoManager.swift index 82851a1c..bfc398d7 100644 --- a/ios/Video/RCTVideoManager.swift +++ b/ios/Video/RCTVideoManager.swift @@ -3,77 +3,77 @@ import React @objc(RCTVideoManager) class RCTVideoManager: RCTViewManager { - override func view() -> UIView { return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher) } - + 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 + func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + 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:licenseUrl:reactTag:) - func setLicenseResult(license: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void { - bridge.uiManager.prependUIBlock({_ , viewRegistry in + func setLicenseResult(license: NSString, licenseUrl: NSString, reactTag: NSNumber) { + 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, licenseUrl as String) } - }) + } } - + @objc(setLicenseResultError:licenseUrl:reactTag:) - func setLicenseResultError(error: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void { - bridge.uiManager.prependUIBlock({_ , viewRegistry in + func setLicenseResultError(error: NSString, licenseUrl: NSString, reactTag: NSNumber) { + 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, licenseUrl as String) } - }) + } } - + @objc(dismissFullscreenPlayer:) - func dismissFullscreenPlayer(_ reactTag: NSNumber) -> Void { - bridge.uiManager.prependUIBlock({_ , viewRegistry in + func dismissFullscreenPlayer(_ reactTag: NSNumber) { + 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.dismissFullscreenPlayer() } - }) + } } + @objc(presentFullscreenPlayer:) - func presentFullscreenPlayer(_ reactTag: NSNumber) -> Void { - bridge.uiManager.prependUIBlock({_ , viewRegistry in + func presentFullscreenPlayer(_ reactTag: NSNumber) { + 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.presentFullscreenPlayer() } - }) + } } @objc(setPlayerPauseState:reactTag:) - func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) -> Void { - bridge.uiManager.prependUIBlock({_ , viewRegistry in + func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) { + 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)) @@ -81,7 +81,7 @@ class RCTVideoManager: RCTViewManager { let paused = paused.boolValue view.setPaused(paused) } - }) + } } override class func requiresMainQueueSetup() -> Bool { diff --git a/ios/Video/RCTVideoPlayerViewController.swift b/ios/Video/RCTVideoPlayerViewController.swift index 8d4324b7..eef65a36 100644 --- a/ios/Video/RCTVideoPlayerViewController.swift +++ b/ios/Video/RCTVideoPlayerViewController.swift @@ -1,15 +1,13 @@ import AVKit class RCTVideoPlayerViewController: AVPlayerViewController { - weak var rctDelegate: RCTVideoPlayerViewControllerDelegate? // Optional paramters - var preferredOrientation:String? - var autorotate:Bool? + var preferredOrientation: String? + var autorotate: Bool? func shouldAutorotate() -> Bool { - if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") { return true } @@ -26,21 +24,21 @@ class RCTVideoPlayerViewController: AVPlayerViewController { #if !os(tvOS) - 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 + 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 } diff --git a/ios/Video/RCTVideoPlayerViewControllerDelegate.swift b/ios/Video/RCTVideoPlayerViewControllerDelegate.swift index 6635975f..2f9fec3f 100644 --- a/ios/Video/RCTVideoPlayerViewControllerDelegate.swift +++ b/ios/Video/RCTVideoPlayerViewControllerDelegate.swift @@ -1,7 +1,7 @@ -import Foundation import AVKit +import Foundation -protocol RCTVideoPlayerViewControllerDelegate : NSObject { - func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) - func videoPlayerViewControllerDidDismiss(playerViewController:AVPlayerViewController) +protocol RCTVideoPlayerViewControllerDelegate: class { + func videoPlayerViewControllerWillDismiss(playerViewController: AVPlayerViewController) + func videoPlayerViewControllerDidDismiss(playerViewController: AVPlayerViewController) } diff --git a/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.h b/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.h index f9978dc8..2664b727 100644 --- a/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.h +++ b/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.h @@ -2,10 +2,10 @@ @interface RCTVideoSwiftLog : 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; ++ (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 diff --git a/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.m b/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.m index 61a38b19..1d8a670f 100644 --- a/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.m +++ b/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.m @@ -4,29 +4,24 @@ @implementation RCTVideoSwiftLog -+ (void)info:(NSString *)message file:(NSString *)file line:(NSUInteger)line -{ - _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); ++ (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)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)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)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); ++ (void)trace:(NSString*)message file:(NSString*)file line:(NSUInteger)line { + _RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message); } @end diff --git a/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.swift b/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.swift index 0e0e1697..36111725 100644 --- a/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.swift +++ b/ios/Video/RCTVideoSwiftLog/RCTVideoSwiftLog.swift @@ -1,5 +1,5 @@ // -// RCTLog.swift +// RCTVideoSwiftLog.swift // WebViewExample // // Created by Jimmy Dee on 4/5/17. @@ -27,29 +27,28 @@ let logHeader: String = "RNV:" -func RCTLogError(_ message: String, _ file: String=#file, _ line: UInt=#line) { +func RCTLogError(_ message: String, _ file: String = #file, _ line: UInt = #line) { RCTVideoSwiftLog.error(logHeader + message, file: file, line: line) } -func RCTLogWarn(_ message: String, _ file: String=#file, _ line: UInt=#line) { +func RCTLogWarn(_ message: String, _ file: String = #file, _ line: UInt = #line) { RCTVideoSwiftLog.warn(logHeader + message, file: file, line: line) } -func RCTLogInfo(_ message: String, _ file: String=#file, _ line: UInt=#line) { +func RCTLogInfo(_ message: String, _ file: String = #file, _ line: UInt = #line) { RCTVideoSwiftLog.info(logHeader + message, file: file, line: line) } -func RCTLog(_ message: String, _ file: String=#file, _ line: UInt=#line) { +func RCTLog(_ message: String, _ file: String = #file, _ line: UInt = #line) { RCTVideoSwiftLog.log(logHeader + message, file: file, line: line) } -func RCTLogTrace(_ message: String, _ file: String=#file, _ line: UInt=#line) { +func RCTLogTrace(_ message: String, _ file: String = #file, _ line: UInt = #line) { RCTVideoSwiftLog.trace(logHeader + message, file: file, line: line) } func DebugLog(_ message: String) { -#if DEBUG - print(logHeader + message) -#endif + #if DEBUG + print(logHeader + message) + #endif } - diff --git a/ios/VideoCaching/RCTVideoCache.h b/ios/VideoCaching/RCTVideoCache.h index bad999ff..0681cc31 100644 --- a/ios/VideoCaching/RCTVideoCache.h +++ b/ios/VideoCaching/RCTVideoCache.h @@ -1,8 +1,8 @@ -#import #import +#import +#import #import #import -#import typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) { RCTVideoCacheStatusMissingFileExtension, @@ -14,25 +14,24 @@ typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) { @class SPTPersistentCache; @class SPTPersistentCacheOptions; -@interface RCTVideoCache : NSObject -{ - SPTPersistentCache *videoCache; - NSString * _Nullable cachePath; - NSString * temporaryCachePath; - NSString * _Nullable cacheIdentifier; +@interface RCTVideoCache : NSObject { + SPTPersistentCache* videoCache; + NSString* _Nullable cachePath; + NSString* temporaryCachePath; + NSString* _Nullable cacheIdentifier; } -@property(nonatomic, strong) SPTPersistentCache * _Nullable videoCache; -@property(nonatomic, strong) NSString * cachePath; -@property(nonatomic, strong) NSString * cacheIdentifier; -@property(nonatomic, strong) NSString * temporaryCachePath; +@property(nonatomic, strong) SPTPersistentCache* _Nullable videoCache; +@property(nonatomic, strong) NSString* cachePath; +@property(nonatomic, strong) NSString* cacheIdentifier; +@property(nonatomic, strong) NSString* temporaryCachePath; -+ (RCTVideoCache *)sharedInstance; -- (void)storeItem:(NSData *)data forUri:(NSString *)uri withCallback:(void(^)(BOOL))handler; -- (void)getItemForUri:(NSString *)url withCallback:(void(^)(RCTVideoCacheStatus, AVAsset * _Nullable)) handler; -- (NSURL *)createUniqueTemporaryFileUrl:(NSString * _Nonnull)url withExtension:(NSString * _Nonnull) extension; -- (AVURLAsset *)getItemFromTemporaryStorage:(NSString *)key; -- (BOOL)saveDataToTemporaryStorage:(NSData *)data key:(NSString *)key; -- (void) createTemporaryPath; ++ (RCTVideoCache*)sharedInstance; +- (void)storeItem:(NSData*)data forUri:(NSString*)uri withCallback:(void (^)(BOOL))handler; +- (void)getItemForUri:(NSString*)url withCallback:(void (^)(RCTVideoCacheStatus, AVAsset* _Nullable))handler; +- (NSURL*)createUniqueTemporaryFileUrl:(NSString* _Nonnull)url withExtension:(NSString* _Nonnull)extension; +- (AVURLAsset*)getItemFromTemporaryStorage:(NSString*)key; +- (BOOL)saveDataToTemporaryStorage:(NSData*)data key:(NSString*)key; +- (void)createTemporaryPath; @end diff --git a/ios/VideoCaching/RCTVideoCache.m b/ios/VideoCaching/RCTVideoCache.m index 985b54c8..b59589a0 100644 --- a/ios/VideoCaching/RCTVideoCache.m +++ b/ios/VideoCaching/RCTVideoCache.m @@ -7,8 +7,8 @@ @synthesize cacheIdentifier; @synthesize temporaryCachePath; -+ (RCTVideoCache *)sharedInstance { - static RCTVideoCache *sharedInstance = nil; ++ (RCTVideoCache*)sharedInstance { + static RCTVideoCache* sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; @@ -20,8 +20,9 @@ if (self = [super init]) { self.cacheIdentifier = @"rct.video.cache"; self.temporaryCachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:self.cacheIdentifier]; - self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:self.cacheIdentifier]; - SPTPersistentCacheOptions *options = [SPTPersistentCacheOptions new]; + self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject + stringByAppendingPathComponent:self.cacheIdentifier]; + SPTPersistentCacheOptions* options = [SPTPersistentCacheOptions new]; options.cachePath = self.cachePath; options.cacheIdentifier = self.cacheIdentifier; options.defaultExpirationPeriod = 60 * 60 * 24 * 30; @@ -29,7 +30,7 @@ options.sizeConstraintBytes = 1024 * 1024 * 100; options.useDirectorySeparation = NO; #ifdef DEBUG - options.debugOutput = ^(NSString *string) { + options.debugOutput = ^(NSString* string) { NSLog(@"VideoCache: debug %@", string); }; #endif @@ -40,8 +41,8 @@ return self; } -- (void) createTemporaryPath { - NSError *error = nil; +- (void)createTemporaryPath { + NSError* error = nil; BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryCachePath withIntermediateDirectories:YES attributes:nil @@ -53,97 +54,101 @@ #endif } -- (void)storeItem:(NSData *)data forUri:(NSString *)uri withCallback:(void(^)(BOOL))handler; +- (void)storeItem:(NSData*)data forUri:(NSString*)uri withCallback:(void (^)(BOOL))handler; { - NSString *key = [self generateCacheKeyForUri:uri]; + NSString* key = [self generateCacheKeyForUri:uri]; if (key == nil) { handler(NO); return; } [self saveDataToTemporaryStorage:data key:key]; - [self.videoCache storeData:data forKey:key locked:NO withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { - if (response.error) { + [self.videoCache storeData:data + forKey:key + locked:NO + withCallback:^(SPTPersistentCacheResponse* _Nonnull response) { + if (response.error) { #ifdef DEBUG - NSLog(@"VideoCache: An error occured while saving the video into the cache: %@", [response.error localizedDescription]); + NSLog(@"VideoCache: An error occured while saving the video into the cache: %@", [response.error localizedDescription]); #endif - handler(NO); - return; - } - handler(YES); - } onQueue:dispatch_get_main_queue()]; + handler(NO); + return; + } + handler(YES); + } + onQueue:dispatch_get_main_queue()]; return; } -- (AVURLAsset *)getItemFromTemporaryStorage:(NSString *)key { - NSString * temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; - +- (AVURLAsset*)getItemFromTemporaryStorage:(NSString*)key { + NSString* temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; + BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:temporaryFilePath]; if (!fileExists) { return nil; } - NSURL *assetUrl = [[NSURL alloc] initFileURLWithPath:temporaryFilePath]; - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:assetUrl options:nil]; + NSURL* assetUrl = [[NSURL alloc] initFileURLWithPath:temporaryFilePath]; + AVURLAsset* asset = [AVURLAsset URLAssetWithURL:assetUrl options:nil]; return asset; } -- (BOOL)saveDataToTemporaryStorage:(NSData *)data key:(NSString *)key { - NSString *temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; +- (BOOL)saveDataToTemporaryStorage:(NSData*)data key:(NSString*)key { + NSString* temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; [data writeToFile:temporaryFilePath atomically:YES]; return YES; } -- (NSString *)generateCacheKeyForUri:(NSString *)uri { - NSString *uriWithoutQueryParams = uri; +- (NSString*)generateCacheKeyForUri:(NSString*)uri { + NSString* uriWithoutQueryParams = uri; // parse file extension if ([uri rangeOfString:@"?"].location != NSNotFound) { - NSArray * components = [uri componentsSeparatedByString:@"?"]; + NSArray* components = [uri componentsSeparatedByString:@"?"]; uriWithoutQueryParams = [components objectAtIndex:0]; } - NSString * pathExtension = [uriWithoutQueryParams pathExtension]; - NSArray * supportedExtensions = @[@"m4v", @"mp4", @"mov"]; + NSString* pathExtension = [uriWithoutQueryParams pathExtension]; + NSArray* supportedExtensions = @[ @"m4v", @"mp4", @"mov" ]; if ([pathExtension isEqualToString:@""]) { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Missing file extension.", nil), - NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Missing file extension.", nil), - NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Missing file extension.", nil) - }; - NSError *error = [NSError errorWithDomain:@"RCTVideoCache" - code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo]; + NSDictionary* userInfo = @{ + NSLocalizedDescriptionKey : NSLocalizedString(@"Missing file extension.", nil), + NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Missing file extension.", nil), + NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString(@"Missing file extension.", nil) + }; + NSError* error = [NSError errorWithDomain:@"RCTVideoCache" code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo]; @throw error; } else if (![supportedExtensions containsObject:pathExtension]) { // Notably, we don't currently support m3u8 (HLS playlists) - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Unsupported file extension.", nil), - NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Unsupported file extension.", nil), - NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Unsupported file extension.", nil) - }; - NSError *error = [NSError errorWithDomain:@"RCTVideoCache" - code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo]; + NSDictionary* userInfo = @{ + NSLocalizedDescriptionKey : NSLocalizedString(@"Unsupported file extension.", nil), + NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Unsupported file extension.", nil), + NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString(@"Unsupported file extension.", nil) + }; + NSError* error = [NSError errorWithDomain:@"RCTVideoCache" code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo]; @throw error; } return [[self generateHashForUrl:uri] stringByAppendingPathExtension:pathExtension]; } -- (void)getItemForUri:(NSString *)uri withCallback:(void(^)(RCTVideoCacheStatus, AVAsset * _Nullable)) handler { +- (void)getItemForUri:(NSString*)uri withCallback:(void (^)(RCTVideoCacheStatus, AVAsset* _Nullable))handler { @try { - NSString *key = [self generateCacheKeyForUri:uri]; - AVURLAsset * temporaryAsset = [self getItemFromTemporaryStorage:key]; + NSString* key = [self generateCacheKeyForUri:uri]; + AVURLAsset* temporaryAsset = [self getItemFromTemporaryStorage:key]; if (temporaryAsset != nil) { handler(RCTVideoCacheStatusAvailable, temporaryAsset); return; } - - [self.videoCache loadDataForKey:key withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { - if (response.record == nil || response.record.data == nil) { - handler(RCTVideoCacheStatusNotAvailable, nil); - return; - } - [self saveDataToTemporaryStorage:response.record.data key:key]; - handler(RCTVideoCacheStatusAvailable, [self getItemFromTemporaryStorage:key]); - } onQueue:dispatch_get_main_queue()]; - } @catch (NSError * err) { + + [self.videoCache loadDataForKey:key + withCallback:^(SPTPersistentCacheResponse* _Nonnull response) { + if (response.record == nil || response.record.data == nil) { + handler(RCTVideoCacheStatusNotAvailable, nil); + return; + } + [self saveDataToTemporaryStorage:response.record.data key:key]; + handler(RCTVideoCacheStatusAvailable, [self getItemFromTemporaryStorage:key]); + } + onQueue:dispatch_get_main_queue()]; + } @catch (NSError* err) { switch (err.code) { case RCTVideoCacheStatusMissingFileExtension: handler(RCTVideoCacheStatusMissingFileExtension, nil); @@ -157,18 +162,14 @@ } } -- (NSString *)generateHashForUrl:(NSString *)string { - const char *cStr = [string UTF8String]; +- (NSString*)generateHashForUrl:(NSString*)string { + const char* cStr = [string UTF8String]; unsigned char result[CC_MD5_DIGEST_LENGTH]; - CC_MD5( cStr, (CC_LONG)strlen(cStr), result ); - - return [NSString stringWithFormat: - @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", - result[0], result[1], result[2], result[3], - result[4], result[5], result[6], result[7], - result[8], result[9], result[10], result[11], - result[12], result[13], result[14], result[15] - ]; + CC_MD5(cStr, (CC_LONG)strlen(cStr), result); + + return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", result[0], result[1], result[2], + result[3], result[4], result[5], result[6], result[7], result[8], result[9], result[10], result[11], + result[12], result[13], result[14], result[15]]; } @end diff --git a/ios/VideoCaching/RCTVideoCachingHandler.swift b/ios/VideoCaching/RCTVideoCachingHandler.swift index db3dc4cd..8ccc4aaa 100644 --- a/ios/VideoCaching/RCTVideoCachingHandler.swift +++ b/ios/VideoCaching/RCTVideoCachingHandler.swift @@ -1,87 +1,97 @@ -import Foundation import AVFoundation import DVAssetLoaderDelegate +import Foundation import Promises class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { - - private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance() + private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance() var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) -> AVPlayerItem)? - + override init() { super.init() } - - func shouldCache(source: VideoSource, textTracks:[TextTrack]?) -> Bool { - if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) { + + func shouldCache(source: VideoSource, textTracks: [TextTrack]?) -> Bool { + if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.isEmpty)) { /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. * Until this is fixed, we need to bypass caching when text tracks are specified. */ - DebugLog("Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + DebugLog(""" + Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache. + Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md + """) return true } return false } - - func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!) -> Promise { + + func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) -> Promise { let url = URL(string: uri) return getItemForUri(uri) - .then{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) -> AVPlayerItem in - guard let self = self, let playerItemPrepareText = self.playerItemPrepareText else {throw NSError(domain: "", code: 0, userInfo: nil)} - switch (videoCacheStatus) { - case .missingFileExtension: - DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") - let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) - return playerItemPrepareText(asset, options, "") - - case .unsupportedFileExtension: - DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") - let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) - return playerItemPrepareText(asset, options, "") - - default: - if let cachedAsset = cachedAsset { - DebugLog("Playing back uri '\(uri)' from cache") - // See note in playerItemForSource about not being able to support text tracks & caching - return AVPlayerItem(asset: cachedAsset) + .then { [weak self] (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) -> AVPlayerItem in + guard let self = self, let playerItemPrepareText = self.playerItemPrepareText else { throw NSError(domain: "", code: 0, userInfo: nil) } + switch videoCacheStatus { + case .missingFileExtension: + DebugLog(""" + Could not generate cache key for uri '\(uri)'. + It is currently not supported to cache urls that do not include a file extension. + The video file will not be cached. + Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md + """) + let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any]) + return playerItemPrepareText(asset, options, "") + + case .unsupportedFileExtension: + DebugLog(""" + Could not generate cache key for uri '\(uri)'. + The file extension of that uri is currently not supported. + The video file will not be cached. + Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md + """) + let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any]) + return playerItemPrepareText(asset, options, "") + + default: + if let cachedAsset = cachedAsset { + DebugLog("Playing back uri '\(uri)' from cache") + // See note in playerItemForSource about not being able to support text tracks & caching + return AVPlayerItem(asset: cachedAsset) + } } + + let asset: DVURLAsset! = DVURLAsset(url: url, options: options as! [String: Any], networkTimeout: 10000) + asset.loaderDelegate = self + + /* More granular code to have control over the DVURLAsset + let resourceLoaderDelegate = DVAssetLoaderDelegate(url: url) + resourceLoaderDelegate.delegate = self + let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false) + components?.scheme = DVAssetLoaderDelegate.scheme() + var asset: AVURLAsset? = nil + if let url = components?.url { + asset = AVURLAsset(url: url, options: options) + } + asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main) + */ + + return AVPlayerItem(asset: asset) } - - let asset:DVURLAsset! = DVURLAsset(url:url, options:options as! [String : Any], networkTimeout:10000) - asset.loaderDelegate = self - - /* More granular code to have control over the DVURLAsset - let resourceLoaderDelegate = DVAssetLoaderDelegate(url: url) - resourceLoaderDelegate.delegate = self - let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false) - components?.scheme = DVAssetLoaderDelegate.scheme() - var asset: AVURLAsset? = nil - if let url = components?.url { - asset = AVURLAsset(url: url, options: options) - } - asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main) - */ - - return AVPlayerItem(asset: asset) - } } - func getItemForUri(_ uri:String) -> Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> { - return Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> { fulfill, reject in - self._videoCache.getItemForUri(uri, withCallback:{ (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in + func getItemForUri(_ uri: String) -> Promise<(videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?)> { + return Promise<(videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?)> { fulfill, _ in + self._videoCache.getItemForUri(uri, withCallback: { (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) in fulfill((videoCacheStatus, cachedAsset)) }) } } - + // MARK: - DVAssetLoaderDelegate - - func dvAssetLoaderDelegate(_ loaderDelegate: DVAssetLoaderDelegate!, didLoad data: Data!, for url: URL!) { - _videoCache.storeItem(data as Data?, forUri:url.absoluteString, withCallback:{ (success:Bool) in + + func dvAssetLoaderDelegate(_: DVAssetLoaderDelegate!, didLoad data: Data!, for url: URL!) { + _videoCache.storeItem(data as Data?, forUri: url.absoluteString, withCallback: { (_: Bool) in DebugLog("Cache data stored successfully 🎉") }) } - } -