chore: lint project (#3395)

* chore: format swift code
* chore: format clang code
* chore: format kotlin code
* refactor: rename folder "API" to "api"
This commit is contained in:
Krzysztof Moch 2023-12-07 08:47:40 +01:00 committed by GitHub
parent 72679a7d63
commit 800aee09de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1407 additions and 1364 deletions

View File

@ -18,7 +18,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: | - 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 - name: run ktlint
working-directory: ./android/ working-directory: ./android/
run: | run: |

View File

@ -1,6 +1,6 @@
[*.{kt,kts}] [*.{kt,kts}]
indent_style=space indent_style=space
indent_size=2 indent_size=4
continuation_indent_size=4 continuation_indent_size=4
insert_final_newline=true insert_final_newline=true
max_line_length=160 max_line_length=160

View File

@ -1,4 +1,4 @@
package com.brentvatne.common.API package com.brentvatne.common.api
import androidx.annotation.IntDef import androidx.annotation.IntDef
import java.lang.annotation.Retention import java.lang.annotation.Retention
@ -29,10 +29,11 @@ internal object ResizeMode {
* Keeps the aspect ratio but takes up the view's size. * Keeps the aspect ratio but takes up the view's size.
*/ */
const val RESIZE_MODE_CENTER_CROP = 4 const val RESIZE_MODE_CENTER_CROP = 4
@JvmStatic @JvmStatic
@Mode @Mode
fun toResizeMode(ordinal: Int): Int { fun toResizeMode(ordinal: Int): Int =
return when (ordinal) { when (ordinal) {
RESIZE_MODE_FIXED_WIDTH -> RESIZE_MODE_FIXED_WIDTH RESIZE_MODE_FIXED_WIDTH -> RESIZE_MODE_FIXED_WIDTH
RESIZE_MODE_FIXED_HEIGHT -> RESIZE_MODE_FIXED_HEIGHT RESIZE_MODE_FIXED_HEIGHT -> RESIZE_MODE_FIXED_HEIGHT
RESIZE_MODE_FILL -> RESIZE_MODE_FILL RESIZE_MODE_FILL -> RESIZE_MODE_FILL
@ -40,7 +41,6 @@ internal object ResizeMode {
RESIZE_MODE_FIT -> RESIZE_MODE_FIT RESIZE_MODE_FIT -> RESIZE_MODE_FIT
else -> RESIZE_MODE_FIT else -> RESIZE_MODE_FIT
} }
}
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef( @IntDef(
@ -51,4 +51,4 @@ internal object ResizeMode {
RESIZE_MODE_CENTER_CROP RESIZE_MODE_CENTER_CROP
) )
annotation class Mode annotation class Mode
} }

View File

@ -1,4 +1,4 @@
package com.brentvatne.common.API package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap 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_TOP = "paddingTop"
private const val PROP_PADDING_LEFT = "paddingLeft" private const val PROP_PADDING_LEFT = "paddingLeft"
private const val PROP_PADDING_RIGHT = "paddingRight" private const val PROP_PADDING_RIGHT = "paddingRight"
@JvmStatic @JvmStatic
fun parse(src: ReadableMap?): SubtitleStyle { fun parse(src: ReadableMap?): SubtitleStyle {
val subtitleStyle = SubtitleStyle() val subtitleStyle = SubtitleStyle()
@ -35,4 +36,4 @@ class SubtitleStyle private constructor() {
return subtitleStyle return subtitleStyle
} }
} }
} }

View File

@ -1,4 +1,4 @@
package com.brentvatne.common.API package com.brentvatne.common.api
/* /*
* class to handle timedEvent retrieved from the stream * 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) { class TimedMetadata(_identifier: String? = null, _value: String? = null) {
var identifier: String? = _identifier var identifier: String? = _identifier
var value: String? = _value var value: String? = _value
} }

View File

@ -1,4 +1,4 @@
package com.brentvatne.common.API package com.brentvatne.common.api
/* /*
* internal representation of audio & text tracks * internal representation of audio & text tracks
@ -8,7 +8,8 @@ class Track {
var mimeType: String? = null var mimeType: String? = null
var language: String? = null var language: String? = null
var isSelected = false var isSelected = false
// in bps available only on audio tracks // in bps available only on audio tracks
var bitrate = 0 var bitrate = 0
var index = 0 var index = 0
} }

View File

@ -1,4 +1,4 @@
package com.brentvatne.common.API package com.brentvatne.common.api
/* /*
* internal representation of audio & text tracks * internal representation of audio & text tracks
@ -12,4 +12,4 @@ class VideoTrack {
var id = -1 var id = -1
var trackId = "" var trackId = ""
var isSelected = false var isSelected = false
} }

View File

@ -4,9 +4,9 @@ import androidx.annotation.StringDef;
import android.view.View; import android.view.View;
import com.brentvatne.common.API.TimedMetadata; import com.brentvatne.common.api.TimedMetadata;
import com.brentvatne.common.API.Track; import com.brentvatne.common.api.Track;
import com.brentvatne.common.API.VideoTrack; import com.brentvatne.common.api.VideoTrack;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableArray;

View File

@ -12,8 +12,10 @@ import java.lang.Exception
object DebugLog { object DebugLog {
// log level to display // log level to display
private var level = Log.WARN private var level = Log.WARN
// enable thread display in logs // enable thread display in logs
private var displayThread = true private var displayThread = true
// add a common prefix for easy filtering // add a common prefix for easy filtering
private const val TAG_PREFIX = "RNV" private const val TAG_PREFIX = "RNV"
@ -24,16 +26,15 @@ object DebugLog {
} }
@JvmStatic @JvmStatic
private fun getTag(tag: String): String { private fun getTag(tag: String): String = TAG_PREFIX + tag
return TAG_PREFIX + tag
}
@JvmStatic @JvmStatic
private fun getMsg(msg: String): String { private fun getMsg(msg: String): String =
return if (displayThread) { if (displayThread) {
"[" + Thread.currentThread().name + "] " + msg "[" + Thread.currentThread().name + "] " + msg
} else msg } else {
} msg
}
@JvmStatic @JvmStatic
fun v(tag: String, msg: String) { fun v(tag: String, msg: String) {
@ -92,4 +93,4 @@ object DebugLog {
wtf(tag, "------------------------>" + getMsg(msg)) wtf(tag, "------------------------>" + getMsg(msg))
} }
} }
} }

View File

@ -1,8 +1,8 @@
package com.brentvatne.common.toolbox package com.brentvatne.common.toolbox
import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import java.util.HashMap import java.util.HashMap
/* /*
@ -53,17 +53,19 @@ object ReactBridgeUtils {
@JvmStatic @JvmStatic
fun safeGetInt(map: ReadableMap?, key: String?): Int { fun safeGetInt(map: ReadableMap?, key: String?): Int {
return safeGetInt(map, key, 0); return safeGetInt(map, key, 0)
} }
@JvmStatic @JvmStatic
fun safeGetDouble(map: ReadableMap?, key: String?, fallback: Double): Double { fun safeGetDouble(map: ReadableMap?, key: String?, fallback: Double): Double {
return if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDouble(key) else fallback return if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDouble(key) else fallback
} }
@JvmStatic @JvmStatic
fun safeGetDouble(map: ReadableMap?, key: String?): Double { 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. * 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 == null || str2 == null) return false // only 1 is null
if (str1.size != str2.size) return false // only 1 is null if (str1.size != str2.size) return false // only 1 is null
for (i in str1.indices) { for (i in str1.indices) {
if (str1[i] == str2[i]) // standard check if (str1[i] == str2[i]) {
// standard check
return false return false
}
} }
return true return true
} }
@JvmStatic @JvmStatic
fun safeStringMapEquals( fun safeStringMapEquals(first: Map<String?, String?>?, second: Map<String?, String?>?): Boolean {
first: Map<String?, String?>?,
second: Map<String?, String?>?
): Boolean {
if (first == null && second == null) return true // both are null if (first == null && second == null) return true // both are null
if (first == null || second == null) return false // only 1 is null if (first == null || second == null) return false // only 1 is null
if (first.size != second.size) { if (first.size != second.size) {

View File

@ -19,7 +19,7 @@ import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.FrameLayout; 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. * A {@link FrameLayout} that resizes itself to match a specified aspect ratio.

View File

@ -25,8 +25,8 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.brentvatne.common.API.ResizeMode; import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.API.SubtitleStyle; import com.brentvatne.common.api.SubtitleStyle;
import java.util.List; import java.util.List;

View File

@ -91,11 +91,11 @@ import androidx.media3.extractor.metadata.id3.Id3Frame;
import androidx.media3.extractor.metadata.id3.TextInformationFrame; import androidx.media3.extractor.metadata.id3.TextInformationFrame;
import androidx.media3.ui.LegacyPlayerControlView; import androidx.media3.ui.LegacyPlayerControlView;
import com.brentvatne.common.API.ResizeMode; import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.API.SubtitleStyle; import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.API.TimedMetadata; import com.brentvatne.common.api.TimedMetadata;
import com.brentvatne.common.API.Track; import com.brentvatne.common.api.Track;
import com.brentvatne.common.API.VideoTrack; import com.brentvatne.common.api.VideoTrack;
import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.react.VideoEventEmitter;
import com.brentvatne.common.toolbox.DebugLog; import com.brentvatne.common.toolbox.DebugLog;
import com.brentvatne.react.R; import com.brentvatne.react.R;

View File

@ -10,8 +10,8 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.RawResourceDataSource; import androidx.media3.datasource.RawResourceDataSource;
import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultLoadControl;
import com.brentvatne.common.API.ResizeMode; import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.API.SubtitleStyle; import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.react.VideoEventEmitter;
import com.brentvatne.common.toolbox.DebugLog; import com.brentvatne.common.toolbox.DebugLog;
import com.brentvatne.common.toolbox.ReactBridgeUtils; import com.brentvatne.common.toolbox.ReactBridgeUtils;

View File

@ -1,5 +1,5 @@
--allman false --allman false
--indent 2 --indent 4
--exclude Pods,Generated --exclude Pods,Generated
--disable andOperator --disable andOperator
@ -10,4 +10,7 @@
--enable markTypes --enable markTypes
--enable isEmpty --enable isEmpty
--funcattributes "prev-line"
--maxwidth 160

View File

@ -6,6 +6,11 @@ disabled_rules:
- file_length - file_length
- cyclomatic_complexity - cyclomatic_complexity
- function_body_length - function_body_length
- function_parameter_count
- empty_string
# TODO: Remove this once all force casts are removed
- force_cast
opt_in_rules: opt_in_rules:
- contains_over_filter_count - contains_over_filter_count
- contains_over_filter_is_empty - contains_over_filter_is_empty
@ -13,7 +18,6 @@ opt_in_rules:
- contains_over_range_nil_comparison - contains_over_range_nil_comparison
- empty_collection_literal - empty_collection_literal
- empty_count - empty_count
- empty_string
- first_where - first_where
- flatmap_over_map_reduce - flatmap_over_map_reduce
- last_where - last_where

View File

@ -1,12 +1,11 @@
struct Chapter { struct Chapter {
let title: String let title: String
let uri: String? let uri: String?
let startTime: Double let startTime: Double
let endTime: Double let endTime: Double
let json: NSDictionary? let json: NSDictionary?
init(_ json: NSDictionary!) { init(_ json: NSDictionary!) {
guard json != nil else { guard json != nil else {
self.json = nil self.json = nil

View File

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

View File

@ -1,9 +1,9 @@
struct SelectedTrackCriteria { struct SelectedTrackCriteria {
let type: String let type: String
let value: Any? let value: Any?
let json: NSDictionary? let json: NSDictionary?
init(_ json: NSDictionary!) { init(_ json: NSDictionary!) {
guard json != nil else { guard json != nil else {
self.json = nil self.json = nil

View File

@ -1,12 +1,11 @@
struct TextTrack { struct TextTrack {
let type: String let type: String
let language: String let language: String
let title: String let title: String
let uri: String let uri: String
let json: NSDictionary? let json: NSDictionary?
init(_ json: NSDictionary!) { init(_ json: NSDictionary!) {
guard json != nil else { guard json != nil else {
self.json = nil self.json = nil

View File

@ -1,11 +1,10 @@
struct VideoSource { struct VideoSource {
let type: String? let type: String?
let uri: String? let uri: String?
let isNetwork: Bool let isNetwork: Bool
let isAsset: Bool let isAsset: Bool
let shouldCache: Bool let shouldCache: Bool
let requestHeaders: Dictionary<String,Any>? let requestHeaders: [String: Any]?
let startPosition: Int64? let startPosition: Int64?
let cropStart: Int64? let cropStart: Int64?
let cropEnd: Int64? let cropEnd: Int64?
@ -14,9 +13,9 @@ struct VideoSource {
let subtitle: String? let subtitle: String?
let description: String? let description: String?
let customImageUri: String? let customImageUri: String?
let json: NSDictionary? let json: NSDictionary?
init(_ json: NSDictionary!) { init(_ json: NSDictionary!) {
guard json != nil else { guard json != nil else {
self.json = nil self.json = nil
@ -41,7 +40,7 @@ struct VideoSource {
self.isNetwork = json["isNetwork"] as? Bool ?? false self.isNetwork = json["isNetwork"] as? Bool ?? false
self.isAsset = json["isAsset"] as? Bool ?? false self.isAsset = json["isAsset"] as? Bool ?? false
self.shouldCache = json["shouldCache"] as? Bool ?? false self.shouldCache = json["shouldCache"] as? Bool ?? false
self.requestHeaders = json["requestHeaders"] as? Dictionary<String,Any> self.requestHeaders = json["requestHeaders"] as? [String: Any]
self.startPosition = json["startPosition"] as? Int64 self.startPosition = json["startPosition"] as? Int64
self.cropStart = json["cropStart"] as? Int64 self.cropStart = json["cropStart"] as? Int64
self.cropEnd = json["cropEnd"] as? Int64 self.cropEnd = json["cropEnd"] as? Int64

View File

@ -1,230 +1,209 @@
#if USE_GOOGLE_IMA #if USE_GOOGLE_IMA
import Foundation import Foundation
import GoogleInteractiveMediaAds 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? /* Entry point for the SDK. Used to make ad requests. */
private var _pipEnabled:() -> Bool 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. */ init(video: RCTVideo!, pipEnabled: @escaping () -> Bool) {
private var adsLoader: IMAAdsLoader! _video = video
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */ _pipEnabled = pipEnabled
private var adsManager: IMAAdsManager!
init(video:RCTVideo!, pipEnabled:@escaping () -> Bool) { super.init()
_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!)
} }
_video?.setPaused(false) func setUpAdsLoader() {
} adsLoader = IMAAdsLoader(settings: nil)
adsLoader.delegate = self
// 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()
} }
if _video.onReceiveAdEvent != nil { func requestAds() {
let type = convertEventToString(event: event.type) 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) { let adTagUrl = _video.getAdTagUrl()
_video.onReceiveAdEvent?([ let contentPlayhead = _video.getContentPlayhead()
"event": type,
"data": event.adData ?? [String](), if adTagUrl != nil && contentPlayhead != nil {
"target": _video.reactTag! // Create an ad request with our ad tag, display container, and optional user context.
]); let request = IMAAdsRequest(
} else { adTagUrl: adTagUrl!,
_video.onReceiveAdEvent?([ adDisplayContainer: adDisplayContainer,
"event": type, contentPlayhead: contentPlayhead,
"target": _video.reactTag! userContext: nil
]); )
adsLoader.requestAds(with: request)
} }
} }
}
func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) { // MARK: - Getters
if error.message != nil {
print("AdsManager error: " + error.message!) func getAdsLoader() -> IMAAdsLoader? {
return adsLoader
} }
guard let _video = _video else {return} func getAdsManager() -> IMAAdsManager? {
return adsManager
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 // MARK: - IMAAdsLoaderDelegate
_video.setPaused(false)
}
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) { func adsLoader(_: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
// Pause the content for the SDK to play ads. guard let _video = _video else { return }
_video?.setPaused(true) // Grab the instance of the IMAAdsManager and set yourself as the delegate.
_video?.setAdPlaying(true) adsManager = adsLoadedData.adsManager
} adsManager?.delegate = self
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) { // Create ads rendering settings and tell the SDK to use the in-app browser.
// Resume the content since the SDK is done playing ads (at least for now). let adsRenderingSettings = IMAAdsRenderingSettings()
_video?.setAdPlaying(false) adsRenderingSettings.linkOpenerDelegate = self
_video?.setPaused(false) adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController()
}
// MARK: - IMALinkOpenerDelegate adsManager.initialize(with: adsRenderingSettings)
}
func linkOpenerDidClose(inAppLink linkOpener: NSObject) { func adsLoader(_: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
adsManager?.resume() if adErrorData.adError.message != nil {
} print("Error loading ads: " + adErrorData.adError.message!)
}
// MARK: - Helpers _video?.setPaused(false)
}
func convertEventToString(event: IMAAdEventType!) -> String { // MARK: - IMAAdsManagerDelegate
var result = "UNKNOWN";
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: case .AD_BREAK_READY:
result = "AD_BREAK_READY"; result = "AD_BREAK_READY"
break;
case .AD_BREAK_ENDED: case .AD_BREAK_ENDED:
result = "AD_BREAK_ENDED"; result = "AD_BREAK_ENDED"
break;
case .AD_BREAK_STARTED: case .AD_BREAK_STARTED:
result = "AD_BREAK_STARTED"; result = "AD_BREAK_STARTED"
break;
case .AD_PERIOD_ENDED: case .AD_PERIOD_ENDED:
result = "AD_PERIOD_ENDED"; result = "AD_PERIOD_ENDED"
break;
case .AD_PERIOD_STARTED: case .AD_PERIOD_STARTED:
result = "AD_PERIOD_STARTED"; result = "AD_PERIOD_STARTED"
break;
case .ALL_ADS_COMPLETED: case .ALL_ADS_COMPLETED:
result = "ALL_ADS_COMPLETED"; result = "ALL_ADS_COMPLETED"
break;
case .CLICKED: case .CLICKED:
result = "CLICK"; result = "CLICK"
break;
case .COMPLETE: case .COMPLETE:
result = "COMPLETED"; result = "COMPLETED"
break;
case .CUEPOINTS_CHANGED: case .CUEPOINTS_CHANGED:
result = "CUEPOINTS_CHANGED"; result = "CUEPOINTS_CHANGED"
break;
case .FIRST_QUARTILE: case .FIRST_QUARTILE:
result = "FIRST_QUARTILE"; result = "FIRST_QUARTILE"
break;
case .LOADED: case .LOADED:
result = "LOADED"; result = "LOADED"
break;
case .LOG: case .LOG:
result = "LOG"; result = "LOG"
break;
case .MIDPOINT: case .MIDPOINT:
result = "MIDPOINT"; result = "MIDPOINT"
break;
case .PAUSE: case .PAUSE:
result = "PAUSED"; result = "PAUSED"
break;
case .RESUME: case .RESUME:
result = "RESUMED"; result = "RESUMED"
break;
case .SKIPPED: case .SKIPPED:
result = "SKIPPED"; result = "SKIPPED"
break;
case .STARTED: case .STARTED:
result = "STARTED"; result = "STARTED"
break;
case .STREAM_LOADED: case .STREAM_LOADED:
result = "STREAM_LOADED"; result = "STREAM_LOADED"
break;
case .TAPPED: case .TAPPED:
result = "TAPPED"; result = "TAPPED"
break;
case .THIRD_QUARTILE: case .THIRD_QUARTILE:
result = "THIRD_QUARTILE"; result = "THIRD_QUARTILE"
break;
default: default:
result = "UNKNOWN"; result = "UNKNOWN"
} }
return result; return result
}
} }
}
#endif #endif

View File

@ -1,75 +1,77 @@
import AVFoundation import AVFoundation
import AVKit import AVKit
import Foundation
import MediaAccessibility import MediaAccessibility
import React import React
import Foundation
#if os(iOS) #if os(iOS)
class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate { class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate {
private var _onPictureInPictureStatusChanged: (() -> Void)? = nil private var _onPictureInPictureStatusChanged: (() -> Void)?
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
private var _restoreUserInterfaceForPIPStopCompletionHandler:((Bool) -> Void)? = nil private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
private var _pipController:AVPictureInPictureController? private var _pipController: AVPictureInPictureController?
private var _isActive:Bool = false private var _isActive = 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) {
guard let _onRestoreUserInterfaceForPictureInPictureStop = _onRestoreUserInterfaceForPictureInPictureStop else { return } init(_ onPictureInPictureStatusChanged: (() -> Void)? = nil, _ onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil) {
_onPictureInPictureStatusChanged = onPictureInPictureStatusChanged
_onRestoreUserInterfaceForPictureInPictureStop() _onRestoreUserInterfaceForPictureInPictureStop = 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 pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {
guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
func setPictureInPicture(_ isActive:Bool) {
if _isActive == isActive { _onPictureInPictureStatusChanged()
return
} }
_isActive = isActive
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
guard let _pipController = _pipController else { return } guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
if _isActive && !_pipController.isPictureInPictureActive { _onPictureInPictureStatusChanged()
DispatchQueue.main.async(execute: { }
_pipController.startPictureInPicture()
}) func pictureInPictureController(
} else if !_isActive && _pipController.isPictureInPictureActive { _: AVPictureInPictureController,
DispatchQueue.main.async(execute: { restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
_pipController.stopPictureInPicture() ) {
}) 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 #endif

View File

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

View File

@ -4,49 +4,48 @@ import Promises
let RCTVideoUnset = -1 let RCTVideoUnset = -1
// MARK: - RCTPlayerOperations
/*! /*!
* Collection of mutating functions * Collection of mutating functions
*/ */
enum RCTPlayerOperations { 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 type = criteria?.type
let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player) let textTracks: [TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
let trackCount:Int! = player?.currentItem?.tracks.count ?? 0 let trackCount: Int! = player?.currentItem?.tracks.count ?? 0
// The first few tracks will be audio & video track // The first few tracks will be audio & video track
var firstTextIndex:Int = 0 var firstTextIndex = 0
for i in 0..<(trackCount) { for i in 0 ..< trackCount where (player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible)) != nil {
if player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false { firstTextIndex = i
firstTextIndex = i break
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 // Select the last text index which is the disabled text track
selectedTrackIndex = trackCount - firstTextIndex selectedTrackIndex = trackCount - firstTextIndex
} else if (type == "language") { } else if type == "language" {
let selectedValue = criteria?.value as? String let selectedValue = criteria?.value as? String
for i in 0..<textTracks.count { for i in 0 ..< textTracks.count {
let currentTextTrack = textTracks[i] let currentTextTrack = textTracks[i]
if (selectedValue == currentTextTrack.language) { if selectedValue == currentTextTrack.language {
selectedTrackIndex = i selectedTrackIndex = i
break break
} }
} }
} else if (type == "title") { } else if type == "title" {
let selectedValue = criteria?.value as? String let selectedValue = criteria?.value as? String
for i in 0..<textTracks.count { for i in 0 ..< textTracks.count {
let currentTextTrack = textTracks[i] let currentTextTrack = textTracks[i]
if (selectedValue == currentTextTrack.title) { if selectedValue == currentTextTrack.title {
selectedTrackIndex = i selectedTrackIndex = i
break break
} }
} }
} else if (type == "index") { } else if type == "index" {
if let value = criteria?.value, let index = value as? Int { if let value = criteria?.value, let index = value as? Int {
if textTracks.count > index { if textTracks.count > index {
selectedTrackIndex = index selectedTrackIndex = index
@ -58,10 +57,10 @@ enum RCTPlayerOperations {
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset { if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user)
let captionSettings = captioningMediaCharacteristics as? [AnyHashable] 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 selectedTrackIndex = 0 // If we can't find a match, use the first available track
let systemLanguage = NSLocale.preferredLanguages.first let systemLanguage = NSLocale.preferredLanguages.first
for i in 0..<textTracks.count { for i in 0 ..< textTracks.count {
let currentTextTrack = textTracks[i] let currentTextTrack = textTracks[i]
if systemLanguage == currentTextTrack.language { if systemLanguage == currentTextTrack.language {
selectedTrackIndex = i selectedTrackIndex = i
@ -71,7 +70,7 @@ enum RCTPlayerOperations {
} }
} }
for i in firstTextIndex..<(trackCount) { for i in firstTextIndex ..< trackCount {
var isEnabled = false var isEnabled = false
if selectedTrackIndex != RCTVideoUnset { if selectedTrackIndex != RCTVideoUnset {
isEnabled = i == selectedTrackIndex + firstTextIndex isEnabled = i == selectedTrackIndex + firstTextIndex
@ -81,31 +80,31 @@ enum RCTPlayerOperations {
} }
// UNUSED // UNUSED
static func setStreamingText(player:AVPlayer?, criteria:SelectedTrackCriteria?) { static func setStreamingText(player: AVPlayer?, criteria: SelectedTrackCriteria?) {
let type = criteria?.type let type = criteria?.type
let group:AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible) let group: AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible)
var mediaOption:AVMediaSelectionOption! var mediaOption: AVMediaSelectionOption!
if (type == "disabled") { if type == "disabled" {
// Do nothing. We want to ensure option is nil // Do nothing. We want to ensure option is nil
} else if (type == "language") || (type == "title") { } else if (type == "language") || (type == "title") {
let value = criteria?.value as? String let value = criteria?.value as? String
for i in 0..<group.options.count { for i in 0 ..< group.options.count {
let currentOption:AVMediaSelectionOption! = group.options[i] let currentOption: AVMediaSelectionOption! = group.options[i]
var optionValue:String! var optionValue: String!
if (type == "language") { if type == "language" {
optionValue = currentOption.extendedLanguageTag optionValue = currentOption.extendedLanguageTag
} else { } else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as! String optionValue = currentOption.commonMetadata.map(\.value)[0] as! String
} }
if (value == optionValue) { if value == optionValue {
mediaOption = currentOption mediaOption = currentOption
break break
} }
} }
//} else if ([type isEqualToString:@"default"]) { // } else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */ // option = group.defaultOption; */
} else if (type == "index") { } else if type == "index" {
if let value = criteria?.value, let index = value as? Int { if let value = criteria?.value, let index = value as? Int {
if group.options.count > index { if group.options.count > index {
mediaOption = group.options[index] mediaOption = group.options[index]
@ -113,7 +112,7 @@ enum RCTPlayerOperations {
} }
} else { // default. invalid type or "system" } else { // default. invalid type or "system"
#if os(tvOS) #if os(tvOS)
// Do noting. Fix for tvOS native audio menu language selector // Do noting. Fix for tvOS native audio menu language selector
#else #else
player?.currentItem?.selectMediaOptionAutomatically(in: group) player?.currentItem?.selectMediaOptionAutomatically(in: group)
return return
@ -121,38 +120,38 @@ enum RCTPlayerOperations {
} }
#if os(tvOS) #if os(tvOS)
// Do noting. Fix for tvOS native audio menu language selector // Do noting. Fix for tvOS native audio menu language selector
#else #else
// If a match isn't found, option will be nil and text tracks will be disabled // 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 #endif
} }
static func setMediaSelectionTrackForCharacteristic(player:AVPlayer?, characteristic:AVMediaCharacteristic, criteria:SelectedTrackCriteria?) { static func setMediaSelectionTrackForCharacteristic(player: AVPlayer?, characteristic: AVMediaCharacteristic, criteria: SelectedTrackCriteria?) {
let type = criteria?.type let type = criteria?.type
let group:AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) let group: AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: characteristic)
var mediaOption:AVMediaSelectionOption! var mediaOption: AVMediaSelectionOption!
guard group != nil else { return } guard group != nil else { return }
if (type == "disabled") { if type == "disabled" {
// Do nothing. We want to ensure option is nil // Do nothing. We want to ensure option is nil
} else if (type == "language") || (type == "title") { } else if (type == "language") || (type == "title") {
let value = criteria?.value as? String let value = criteria?.value as? String
for i in 0..<group.options.count { for i in 0 ..< group.options.count {
let currentOption:AVMediaSelectionOption! = group.options[i] let currentOption: AVMediaSelectionOption! = group.options[i]
var optionValue:String! var optionValue: String!
if (type == "language") { if type == "language" {
optionValue = currentOption.extendedLanguageTag optionValue = currentOption.extendedLanguageTag
} else { } else {
optionValue = currentOption.commonMetadata.map(\.value)[0] as? String optionValue = currentOption.commonMetadata.map(\.value)[0] as? String
} }
if (value == optionValue) { if value == optionValue {
mediaOption = currentOption mediaOption = currentOption
break break
} }
} }
//} else if ([type isEqualToString:@"default"]) { // } else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */ // option = group.defaultOption; */
} else if type == "index" { } else if type == "index" {
if let value = criteria?.value, let index = value as? Int { if let value = criteria?.value, let index = value as? Int {
@ -167,16 +166,15 @@ enum RCTPlayerOperations {
if let group = group { if let group = group {
// If a match isn't found, option will be nil and text tracks will be disabled // 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)
} }
} }
static func seek(player: AVPlayer, playerItem:AVPlayerItem, paused:Bool, seekTime:Float, seekTolerance:Float) -> Promise<Bool> { static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float) -> Promise<Bool> {
let timeScale:Int = 1000 let timeScale = 1000
let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale)) let cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
let current:CMTime = playerItem.currentTime() let current: CMTime = playerItem.currentTime()
let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale)) let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
return Promise<Bool>(on: .global()) { fulfill, reject in return Promise<Bool>(on: .global()) { fulfill, reject in
guard CMTimeCompare(current, cmSeekTime) != 0 else { guard CMTimeCompare(current, cmSeekTime) != 0 else {
@ -185,26 +183,26 @@ enum RCTPlayerOperations {
} }
if !paused { player.pause() } 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) fulfill(finished)
}) })
} }
} }
static func configureAudio(ignoreSilentSwitch:String, mixWithOthers:String, audioOutput:String) { static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) {
let audioSession:AVAudioSession! = AVAudioSession.sharedInstance() let audioSession: AVAudioSession! = AVAudioSession.sharedInstance()
var category:AVAudioSession.Category? = nil var category: AVAudioSession.Category?
var options:AVAudioSession.CategoryOptions? = nil var options: AVAudioSession.CategoryOptions?
if (ignoreSilentSwitch == "ignore") { if ignoreSilentSwitch == "ignore" {
category = audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback category = audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback
} else if (ignoreSilentSwitch == "obey") { } else if ignoreSilentSwitch == "obey" {
category = AVAudioSession.Category.ambient category = AVAudioSession.Category.ambient
} }
if (mixWithOthers == "mix") { if mixWithOthers == "mix" {
options = .mixWithOthers options = .mixWithOthers
} else if (mixWithOthers == "duck") { } else if mixWithOthers == "duck" {
options = .duckOthers options = .duckOthers
} }
@ -214,18 +212,21 @@ enum RCTPlayerOperations {
} catch { } catch {
debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category and options. Error: \(error).") debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category and options. Error: \(error).")
#if !os(tvOS) #if !os(tvOS)
// Handle specific set category and option combination error // Handle specific set category and option combination error
// setCategory:AVAudioSessionCategoryPlayback withOptions:mixWithOthers || duckOthers // setCategory:AVAudioSessionCategoryPlayback withOptions:mixWithOthers || duckOthers
// Failed to set category, error: 'what' Error Domain=NSOSStatusErrorDomain // Failed to set category, error: 'what' Error Domain=NSOSStatusErrorDomain
// https://developer.apple.com/forums/thread/714598 // https://developer.apple.com/forums/thread/714598
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
do { do {
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category to playAndRecord with defaultToSpeaker options.") 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) try audioSession.setCategory(
} catch { audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback,
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).") options: AVAudioSession.CategoryOptions.defaultToSpeaker
)
} catch {
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).")
}
} }
}
#endif #endif
} }
} else if let category = category, options == nil { } else if let category = category, options == nil {

View File

@ -2,17 +2,15 @@ import AVFoundation
import Promises import Promises
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:] private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:]
private var _requestingCertificate:Bool = false private var _requestingCertificate = false
private var _requestingCertificateErrored:Bool = false private var _requestingCertificateErrored = false
private var _drm: DRMParams? private var _drm: DRMParams?
private var _localSourceEncryptionKeyScheme: String? private var _localSourceEncryptionKeyScheme: String?
private var _reactTag: NSNumber? private var _reactTag: NSNumber?
private var _onVideoError: RCTDirectEventBlock? private var _onVideoError: RCTDirectEventBlock?
private var _onGetLicense: RCTDirectEventBlock? private var _onGetLicense: RCTDirectEventBlock?
init( init(
asset: AVURLAsset, asset: AVURLAsset,
drm: DRMParams?, drm: DRMParams?,
@ -30,46 +28,45 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
_drm = drm _drm = drm
_localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme _localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme
} }
deinit { deinit {
for request in _loadingRequests.values { for request in _loadingRequests.values {
request?.finishLoading() request?.finishLoading()
} }
} }
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool { func resourceLoader(_: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool {
return loadingRequestHandling(renewalRequest) return loadingRequestHandling(renewalRequest)
} }
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest:AVAssetResourceLoadingRequest) -> Bool { func resourceLoader(_: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
return loadingRequestHandling(loadingRequest) return loadingRequestHandling(loadingRequest)
} }
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) { func resourceLoader(_: AVAssetResourceLoader, didCancel _: AVAssetResourceLoadingRequest) {
RCTLog("didCancelLoadingRequest") 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 // Check if the loading request exists in _loadingRequests based on licenseUrl
guard let loadingRequest = _loadingRequests[licenseUrl] else { guard let loadingRequest = _loadingRequests[licenseUrl] else {
setLicenseResultError("Loading request for licenseUrl \(licenseUrl) not found", licenseUrl) setLicenseResultError("Loading request for licenseUrl \(licenseUrl) not found", licenseUrl)
return return
} }
// Check if the license data is valid // Check if the license data is valid
guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license) else { guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license) else {
setLicenseResultError("No data from JS license response", licenseUrl) setLicenseResultError("No data from JS license response", licenseUrl)
return return
} }
let dataRequest: AVAssetResourceLoadingDataRequest! = loadingRequest?.dataRequest let dataRequest: AVAssetResourceLoadingDataRequest! = loadingRequest?.dataRequest
dataRequest.respond(with: respondData) dataRequest.respond(with: respondData)
loadingRequest!.finishLoading() loadingRequest!.finishLoading()
_loadingRequests.removeValue(forKey: licenseUrl) _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 // Check if the loading request exists in _loadingRequests based on licenseUrl
guard let loadingRequest = _loadingRequests[licenseUrl] else { guard let loadingRequest = _loadingRequests[licenseUrl] else {
print("Loading request for licenseUrl \(licenseUrl) not found. Error: \(error)") 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) self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error), licenseUrl: licenseUrl)
} }
func finishLoadingWithError(error: Error!, licenseUrl: String!) -> Bool { func finishLoadingWithError(error: Error!, licenseUrl: String!) -> Bool {
// Check if the loading request exists in _loadingRequests based on licenseUrl // Check if the loading request exists in _loadingRequests based on licenseUrl
guard let loadingRequest = _loadingRequests[licenseUrl], let error = error as NSError? else { guard let loadingRequest = _loadingRequests[licenseUrl], let error = error as NSError? else {
@ -94,45 +91,44 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
"localizedDescription": error.localizedDescription ?? "", "localizedDescription": error.localizedDescription ?? "",
"localizedFailureReason": error.localizedFailureReason ?? "", "localizedFailureReason": error.localizedFailureReason ?? "",
"localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "", "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "",
"domain": error.domain "domain": error.domain,
], ],
"target": _reactTag "target": _reactTag,
]) ])
return false return false
} }
func loadingRequestHandling(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool {
if handleEmbeddedKey(loadingRequest) { if handleEmbeddedKey(loadingRequest) {
return true return true
} }
if _drm != nil { if _drm != nil {
return handleDrm(loadingRequest) return handleDrm(loadingRequest)
} }
return false return false
} }
func handleEmbeddedKey(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { func handleEmbeddedKey(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
guard let url = loadingRequest.request.url, guard let url = loadingRequest.request.url,
let _localSourceEncryptionKeyScheme = _localSourceEncryptionKeyScheme, let _localSourceEncryptionKeyScheme = _localSourceEncryptionKeyScheme,
let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: _localSourceEncryptionKeyScheme) let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: _localSourceEncryptionKeyScheme)
else { else {
return false return false
} }
loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentLength = Int64(persistentKeyData.count) loadingRequest.contentInformationRequest?.contentLength = Int64(persistentKeyData.count)
loadingRequest.dataRequest?.respond(with: persistentKeyData) loadingRequest.dataRequest?.respond(with: persistentKeyData)
loadingRequest.finishLoading() loadingRequest.finishLoading()
return true return true
} }
func handleDrm(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { func handleDrm(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
if _requestingCertificate { if _requestingCertificate {
return true return true
} else if _requestingCertificateErrored { } else if _requestingCertificateErrored {
@ -142,20 +138,20 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
var requestKey: String = loadingRequest.request.url?.absoluteString ?? "" var requestKey: String = loadingRequest.request.url?.absoluteString ?? ""
_loadingRequests[requestKey] = loadingRequest _loadingRequests[requestKey] = loadingRequest
guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else { guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else {
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey) return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey)
} }
var promise: Promise<Data> var promise: Promise<Data>
if _onGetLicense != nil { if _onGetLicense != nil {
let contentId = _drm.contentId ?? loadingRequest.request.url?.host let contentId = _drm.contentId ?? loadingRequest.request.url?.host
promise = RCTVideoDRM.handleWithOnGetLicense( promise = RCTVideoDRM.handleWithOnGetLicense(
loadingRequest:loadingRequest, loadingRequest: loadingRequest,
contentId:contentId, contentId: contentId,
certificateUrl:_drm.certificateUrl, certificateUrl: _drm.certificateUrl,
base64Certificate:_drm.base64Certificate base64Certificate: _drm.base64Certificate
) .then{ spcData -> Void in ).then { spcData in
self._requestingCertificate = true self._requestingCertificate = true
self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? loadingRequest.request.url?.absoluteString ?? "", self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? loadingRequest.request.url?.absoluteString ?? "",
"contentId": contentId ?? "", "contentId": contentId ?? "",
@ -164,27 +160,26 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
} }
} else { } else {
promise = RCTVideoDRM.handleInternalGetLicense( promise = RCTVideoDRM.handleInternalGetLicense(
loadingRequest:loadingRequest, loadingRequest: loadingRequest,
contentId:_drm.contentId, contentId: _drm.contentId,
licenseServer:_drm.licenseServer, licenseServer: _drm.licenseServer,
certificateUrl:_drm.certificateUrl, certificateUrl: _drm.certificateUrl,
base64Certificate:_drm.base64Certificate, base64Certificate: _drm.base64Certificate,
headers:_drm.headers headers: _drm.headers
) .then{ data -> Void in ).then { data in
guard let dataRequest = loadingRequest.dataRequest else { guard let dataRequest = loadingRequest.dataRequest else {
throw RCTVideoErrorHandler.noCertificateData throw RCTVideoErrorHandler.noCertificateData
}
dataRequest.respond(with:data)
loadingRequest.finishLoading()
} }
dataRequest.respond(with: data)
loadingRequest.finishLoading()
}
} }
promise.catch { error in
promise.catch{ error in self.finishLoadingWithError(error: error, licenseUrl: requestKey)
self.finishLoadingWithError(error:error, licenseUrl: requestKey)
self._requestingCertificateErrored = true self._requestingCertificateErrored = true
} }
return true return true
} }
} }

View File

@ -1,53 +1,53 @@
import AVFoundation import AVFoundation
import Promises import Promises
struct RCTVideoDRM { enum RCTVideoDRM {
@available(*, unavailable) private init() {}
static func fetchLicense( static func fetchLicense(
licenseServer: String, licenseServer: String,
spcData: Data?, spcData: Data?,
contentId: String, contentId: String,
headers: [String:Any]? headers: [String: Any]?
) -> Promise<Data> { ) -> Promise<Data> {
let request = createLicenseRequest(licenseServer:licenseServer, spcData:spcData, contentId:contentId, headers:headers) let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers)
return Promise<Data>(on: .global()) { fulfill, reject in
let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ (data:Data!,response:URLResponse!,error:Error!) in
let httpResponse:HTTPURLResponse! = (response as! HTTPURLResponse)
guard error == nil else { return Promise<Data>(on: .global()) { fulfill, reject in
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") let postDataTask = URLSession.shared.dataTask(
reject(error) with: request as URLRequest,
return 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() postDataTask.resume()
} }
} }
static func createLicenseRequest( static func createLicenseRequest(
licenseServer: String, licenseServer: String,
spcData: Data?, spcData: Data?,
contentId: String, contentId: String,
headers: [String:Any]? headers: [String: Any]?
) -> URLRequest { ) -> URLRequest {
var request = URLRequest(url: URL(string: licenseServer)!) var request = URLRequest(url: URL(string: licenseServer)!)
request.httpMethod = "POST" request.httpMethod = "POST"
if let headers = headers { if let headers = headers {
for item in headers { for item in headers {
guard let key = item.key as? String, let value = item.value as? String else { 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) request.setValue(value, forHTTPHeaderField: key)
} }
} }
let spcEncoded = spcData?.base64EncodedString(options: []) let spcEncoded = spcData?.base64EncodedString(options: [])
let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(
let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId) kCFAllocatorDefault,
let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true) 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 request.httpBody = postData
return request return request
} }
static func fetchSpcData( static func fetchSpcData(
loadingRequest: AVAssetResourceLoadingRequest, loadingRequest: AVAssetResourceLoadingRequest,
certificateData: Data, certificateData: Data,
contentIdData: Data contentIdData: Data
) -> Promise<Data> { ) -> Promise<Data> {
return Promise<Data>(on: .global()) { fulfill, reject in return Promise<Data>(on: .global()) { fulfill, reject in
var spcError:NSError! var spcError: NSError!
var spcData: Data? var spcData: Data?
do { do {
spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil) spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil)
} catch _ { } catch _ {
print("SPC error") print("SPC error")
} }
if spcError != nil { if spcError != nil {
reject(spcError) reject(spcError)
} }
guard let spcData = spcData else { guard let spcData = spcData else {
reject(RCTVideoErrorHandler.noSPC) reject(RCTVideoErrorHandler.noSPC)
return return
} }
fulfill(spcData) fulfill(spcData)
} }
} }
static func createCertificateData(certificateStringUrl:String?, base64Certificate:Bool?) -> Promise<Data> {
return Promise<Data>(on: .global()) { fulfill, reject in
static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) -> Promise<Data> {
return Promise<Data>(on: .global()) { fulfill, reject in
guard let certificateStringUrl = certificateStringUrl, guard let certificateStringUrl = certificateStringUrl,
let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else {
reject(RCTVideoErrorHandler.noCertificateURL) reject(RCTVideoErrorHandler.noCertificateURL)
return return
} }
var certificateData:Data? var certificateData: Data?
do { do {
certificateData = try Data(contentsOf: certificateURL) certificateData = try Data(contentsOf: certificateURL)
if (base64Certificate != nil) { if base64Certificate != nil {
certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters) certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters)
} }
} catch {} } catch {}
guard let certificateData = certificateData else { guard let certificateData = certificateData else {
reject(RCTVideoErrorHandler.noCertificateData) reject(RCTVideoErrorHandler.noCertificateData)
return return
} }
fulfill(certificateData) fulfill(certificateData)
} }
} }
static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, certificateUrl:String?, base64Certificate:Bool?) -> Promise<Data> { static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId: String?, certificateUrl: String?,
base64Certificate: Bool?) -> Promise<Data> {
let contentIdData = contentId?.data(using: .utf8) let contentIdData = contentId?.data(using: .utf8)
return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate) return RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate)
.then{ certificateData -> Promise<Data> in .then { certificateData -> Promise<Data> in
guard let contentIdData = contentIdData else { guard let contentIdData = contentIdData else {
throw RCTVideoError.invalidContentId as! Error throw RCTVideoError.invalidContentId as! Error
} }
return RCTVideoDRM.fetchSpcData( return RCTVideoDRM.fetchSpcData(
loadingRequest:loadingRequest, loadingRequest: loadingRequest,
certificateData:certificateData, certificateData: certificateData,
contentIdData:contentIdData contentIdData: contentIdData
) )
} }
} }
static func handleInternalGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, licenseServer:String?, certificateUrl:String?, base64Certificate:Bool?, headers: [String:Any]?) -> Promise<Data> { static func handleInternalGetLicense(
loadingRequest: AVAssetResourceLoadingRequest,
contentId: String?,
licenseServer: String?,
certificateUrl: String?,
base64Certificate: Bool?,
headers: [String: Any]?
) -> Promise<Data> {
let url = loadingRequest.request.url 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) 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 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) return RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate)
.then{ certificateData in .then { certificateData in
return RCTVideoDRM.fetchSpcData( return RCTVideoDRM.fetchSpcData(
loadingRequest:loadingRequest, loadingRequest: loadingRequest,
certificateData:certificateData, certificateData: certificateData,
contentIdData:contentIdData contentIdData: contentIdData
) )
} }
.then{ spcData -> Promise<Data> in .then { spcData -> Promise<Data> in
guard let licenseServer = licenseServer else { guard let licenseServer = licenseServer else {
throw RCTVideoError.noLicenseServerURL as! Error throw RCTVideoError.noLicenseServerURL as! Error
} }

View File

@ -1,4 +1,6 @@
enum RCTVideoError : Int { // MARK: - RCTVideoError
enum RCTVideoError: Int {
case fromJSPart case fromJSPart
case noLicenseServerURL case noLicenseServerURL
case licenseRequestNotOk case licenseRequestNotOk
@ -12,62 +14,69 @@ enum RCTVideoError : Int {
case invalidContentId case invalidContentId
} }
// MARK: - RCTVideoErrorHandler
enum RCTVideoErrorHandler { enum RCTVideoErrorHandler {
static let noDRMData = NSError( static let noDRMData = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.noDRMData.rawValue, code: RCTVideoError.noDRMData.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining DRM license.", NSLocalizedDescriptionKey: "Error obtaining DRM license.",
NSLocalizedFailureReasonErrorKey: "No drm object found.", NSLocalizedFailureReasonErrorKey: "No drm object found.",
NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?" NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?",
]) ]
)
static let noCertificateURL = NSError( static let noCertificateURL = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.noCertificateURL.rawValue, code: RCTVideoError.noCertificateURL.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining DRM License.", NSLocalizedDescriptionKey: "Error obtaining DRM License.",
NSLocalizedFailureReasonErrorKey: "No certificate URL has been found.", 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( static let noCertificateData = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.noCertificateData.rawValue, code: RCTVideoError.noCertificateData.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining DRM license.", NSLocalizedDescriptionKey: "Error obtaining DRM license.",
NSLocalizedFailureReasonErrorKey: "No certificate data obtained from the specificied url.", 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( static let noSPC = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.noSPC.rawValue, code: RCTVideoError.noSPC.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining license.", NSLocalizedDescriptionKey: "Error obtaining license.",
NSLocalizedFailureReasonErrorKey: "No spc received.", NSLocalizedFailureReasonErrorKey: "No spc received.",
NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config." NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config.",
]) ]
)
static let noLicenseServerURL = NSError( static let noLicenseServerURL = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.noLicenseServerURL.rawValue, code: RCTVideoError.noLicenseServerURL.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining DRM License.", NSLocalizedDescriptionKey: "Error obtaining DRM License.",
NSLocalizedFailureReasonErrorKey: "No license server URL has been found.", 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( static let noDataFromLicenseRequest = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.noDataFromLicenseRequest.rawValue, code: RCTVideoError.noDataFromLicenseRequest.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining DRM license.", NSLocalizedDescriptionKey: "Error obtaining DRM license.",
NSLocalizedFailureReasonErrorKey: "No data received from the license server.", NSLocalizedFailureReasonErrorKey: "No data received from the license server.",
NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?" NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?",
]) ]
)
static func licenseRequestNotOk(_ statusCode: Int) -> NSError { static func licenseRequestNotOk(_ statusCode: Int) -> NSError {
return NSError( return NSError(
domain: "RCTVideo", domain: "RCTVideo",
@ -75,29 +84,31 @@ enum RCTVideoErrorHandler {
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining license.", NSLocalizedDescriptionKey: "Error obtaining license.",
NSLocalizedFailureReasonErrorKey: String( NSLocalizedFailureReasonErrorKey: String(
format:"License server responded with status code %li", format: "License server responded with status code %li",
(statusCode) 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 { static func fromJSPart(_ error: String) -> NSError {
return NSError(domain: "RCTVideo", return NSError(domain: "RCTVideo",
code: RCTVideoError.fromJSPart.rawValue, code: RCTVideoError.fromJSPart.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: error, NSLocalizedDescriptionKey: error,
NSLocalizedFailureReasonErrorKey: error, NSLocalizedFailureReasonErrorKey: error,
NSLocalizedRecoverySuggestionErrorKey: error NSLocalizedRecoverySuggestionErrorKey: error,
]) ])
} }
static let invalidContentId = NSError( static let invalidContentId = NSError(
domain: "RCTVideo", domain: "RCTVideo",
code: RCTVideoError.invalidContentId.rawValue, code: RCTVideoError.invalidContentId.rawValue,
userInfo: [ userInfo: [
NSLocalizedDescriptionKey: "Error obtaining DRM license.", NSLocalizedDescriptionKey: "Error obtaining DRM license.",
NSLocalizedFailureReasonErrorKey: "No valide content Id received", NSLocalizedFailureReasonErrorKey: "No valide content Id received",
NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?" NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?",
]) ]
)
} }

View File

@ -1,62 +1,57 @@
import AVFoundation import AVFoundation
enum RCTVideoSave { enum RCTVideoSave {
static func save( static func save(
options:NSDictionary!, options _: NSDictionary!,
resolve: @escaping RCTPromiseResolveBlock, resolve: @escaping RCTPromiseResolveBlock,
reject:@escaping RCTPromiseRejectBlock, reject: @escaping RCTPromiseRejectBlock,
playerItem: AVPlayerItem? playerItem: AVPlayerItem?
) { ) {
let asset:AVAsset! = playerItem?.asset let asset: AVAsset! = playerItem?.asset
guard asset != nil else { guard asset != nil else {
reject("ERROR_ASSET_NIL", "Asset is nil", nil) reject("ERROR_ASSET_NIL", "Asset is nil", nil)
return 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) reject("ERROR_COULD_NOT_CREATE_EXPORT_SESSION", "Could not create export session", nil)
return return
} }
var path:String! = nil var path: String!
path = RCTVideoSave.generatePathInDirectory( path = RCTVideoSave.generatePathInDirectory(
directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path, directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path,
withExtension: ".mp4") withExtension: ".mp4"
let url:NSURL! = NSURL.fileURL(withPath: path) as NSURL )
let url: NSURL! = NSURL.fileURL(withPath: path) as NSURL
exportSession.outputFileType = AVFileType.mp4 exportSession.outputFileType = AVFileType.mp4
exportSession.outputURL = url as URL? exportSession.outputURL = url as URL?
exportSession.videoComposition = playerItem?.videoComposition exportSession.videoComposition = playerItem?.videoComposition
exportSession.shouldOptimizeForNetworkUse = true exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronously(completionHandler: { exportSession.exportAsynchronously(completionHandler: {
switch exportSession.status {
switch (exportSession.status) {
case .failed: case .failed:
reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error) reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error)
break
case .cancelled: case .cancelled:
reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error) reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error)
break
default: default:
resolve(["uri": url.absoluteString]) 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` ?? "") let fileName = UUID().uuidString + (`extension` ?? "")
RCTVideoSave.ensureDirExists(withPath: directory) RCTVideoSave.ensureDirExists(withPath: directory)
return URL(fileURLWithPath: directory ?? "").appendingPathComponent(fileName).path return URL(fileURLWithPath: directory ?? "").appendingPathComponent(fileName).path
} }
static func cacheDirectoryPath() -> String? { static func cacheDirectoryPath() -> String? {
let array = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path) let array = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path)
return array[0] return array[0]
} }
static func ensureDirExists(withPath path: String?) -> Bool { static func ensureDirExists(withPath path: String?) -> Bool {
var isDir: ObjCBool = false var isDir: ObjCBool = false
var error: Error? var error: Error?
@ -64,8 +59,7 @@ enum RCTVideoSave {
if !(exists && isDir.boolValue) { if !(exists && isDir.boolValue) {
do { do {
try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil)
} catch { } catch {}
}
if error != nil { if error != nil {
return false return false
} }

View File

@ -1,49 +1,48 @@
import Foundation
import AVFoundation import AVFoundation
import AVKit import AVKit
import Foundation
/*! /*!
* Collection of helper functions for tvOS specific features * Collection of helper functions for tvOS specific features
*/ */
#if os(tvOS) #if os(tvOS)
enum RCTVideoTVUtils { enum RCTVideoTVUtils {
static func makeNavigationMarkerGroups(_ chapters: [Chapter]) -> [AVNavigationMarkersGroup] { static func makeNavigationMarkerGroups(_ chapters: [Chapter]) -> [AVNavigationMarkersGroup] {
var metadataGroups = [AVTimedMetadataGroup]() var metadataGroups = [AVTimedMetadataGroup]()
// Iterate over the defined chapters and build a timed metadata group object for each. // Iterate over the defined chapters and build a timed metadata group object for each.
chapters.forEach { chapter in chapters.forEach { chapter in
metadataGroups.append(makeTimedMetadataGroup(for: chapter)) 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 { // Create a metadata item that contains the chapter title.
var metadata = [AVMetadataItem]() let titleItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierTitle, value: chapter.title)
metadata.append(titleItem)
// Create a metadata item that contains the chapter title. // Create a time range for the metadata group.
let titleItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierTitle, value: chapter.title) let timescale: Int32 = 600
metadata.append(titleItem) 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. // Image
let timescale: Int32 = 600 if let imgUri = chapter.uri,
let startTime = CMTime(seconds: chapter.startTime, preferredTimescale: timescale) let uri = URL(string: imgUri),
let endTime = CMTime(seconds: chapter.endTime, preferredTimescale: timescale) let imgData = try? Data(contentsOf: uri),
let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime) let image = UIImage(data: imgData),
let pngData = image.pngData() {
let imageItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierArtwork, value: pngData)
metadata.append(imageItem)
}
// Image return AVTimedMetadataGroup(items: metadata, timeRange: timeRange)
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)
} }
}
#endif #endif

View File

@ -1,116 +1,114 @@
import AVFoundation import AVFoundation
import Promises
import Photos import Photos
import Promises
/*! /*!
* Collection of pure functions * Collection of pure functions
*/ */
enum RCTVideoUtils { enum RCTVideoUtils {
/*! /*!
* Calculates and returns the playable duration of the current player item using its loaded time ranges. * 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. * \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, guard let player = player,
let video:AVPlayerItem = player.currentItem, let video: AVPlayerItem = player.currentItem,
video.status == AVPlayerItem.Status.readyToPlay else { video.status == AVPlayerItem.Status.readyToPlay else {
return 0 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) return NSNumber(value: (Float64(source?.cropEnd ?? 0) - Float64(source?.cropStart ?? 0)) / 1000)
} }
var effectiveTimeRange:CMTimeRange? var effectiveTimeRange: CMTimeRange?
for (_, value) in video.loadedTimeRanges.enumerated() { for value in video.loadedTimeRanges {
let timeRange:CMTimeRange = value.timeRangeValue let timeRange: CMTimeRange = value.timeRangeValue
if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) { if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) {
effectiveTimeRange = timeRange effectiveTimeRange = timeRange
break break
} }
} }
if let effectiveTimeRange = effectiveTimeRange { if let effectiveTimeRange = effectiveTimeRange {
let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) let playableDuration: Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange))
if playableDuration > 0 { if playableDuration > 0 {
if (source?.cropStart != nil) { if source?.cropStart != nil {
return NSNumber(value: (playableDuration - Float64(source?.cropStart ?? 0) / 1000)) return NSNumber(value: playableDuration - Float64(source?.cropStart ?? 0) / 1000)
} }
return playableDuration as NSNumber return playableDuration as NSNumber
} }
} }
return 0 return 0
} }
static func urlFilePath(filepath:NSString!, searchPath:FileManager.SearchPathDirectory) -> NSURL! { static func urlFilePath(filepath: NSString!, searchPath: FileManager.SearchPathDirectory) -> NSURL! {
if filepath.contains("file://") { if filepath.contains("file://") {
return NSURL(string: filepath as String) return NSURL(string: filepath as String)
} }
// if no file found, check if the file exists in the Document directory // if no file found, check if the file exists in the Document directory
let paths:[String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true) let paths: [String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true)
var relativeFilePath:String! = filepath.lastPathComponent var relativeFilePath: String! = filepath.lastPathComponent
// the file may be multiple levels below the documents directory // the file may be multiple levels below the documents directory
let directoryString:String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents"; let directoryString: String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents"
let fileComponents:[String]! = filepath.components(separatedBy: directoryString) let fileComponents: [String]! = filepath.components(separatedBy: directoryString)
if fileComponents.count > 1 { if fileComponents.count > 1 {
relativeFilePath = fileComponents[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) { if FileManager.default.fileExists(atPath: path) {
return NSURL.fileURL(withPath: path) as NSURL return NSURL.fileURL(withPath: path) as NSURL
} }
return nil return nil
} }
static func playerItemSeekableTimeRange(_ player:AVPlayer?) -> CMTimeRange { static func playerItemSeekableTimeRange(_ player: AVPlayer?) -> CMTimeRange {
if let playerItem = player?.currentItem, if let playerItem = player?.currentItem,
playerItem.status == .readyToPlay, playerItem.status == .readyToPlay,
let firstItem = playerItem.seekableTimeRanges.first { let firstItem = playerItem.seekableTimeRanges.first {
return firstItem.timeRangeValue 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, if let playerItem = player?.currentItem,
playerItem.status == .readyToPlay { playerItem.status == .readyToPlay {
return(playerItem.duration) return playerItem.duration
} }
return(CMTime.invalid) return CMTime.invalid
} }
static func calculateSeekableDuration(_ player:AVPlayer?) -> NSNumber { static func calculateSeekableDuration(_ player: AVPlayer?) -> NSNumber {
let timeRange:CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player) let timeRange: CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player)
if CMTIME_IS_NUMERIC(timeRange.duration) if CMTIME_IS_NUMERIC(timeRange.duration) {
{
return NSNumber(value: CMTimeGetSeconds(timeRange.duration)) return NSNumber(value: CMTimeGetSeconds(timeRange.duration))
} }
return 0 return 0
} }
static func getAudioTrackInfo(_ player:AVPlayer?) -> [AnyObject]! { static func getAudioTrackInfo(_ player: AVPlayer?) -> [AnyObject]! {
guard let player = player else { guard let player = player else {
return [] return []
} }
let audioTracks:NSMutableArray! = NSMutableArray() let audioTracks: NSMutableArray! = NSMutableArray()
let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) 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] let currentOption = group?.options[i]
var title = "" var title = ""
let values = currentOption?.commonMetadata.map(\.value) let values = currentOption?.commonMetadata.map(\.value)
if (values?.count ?? 0) > 0, let value = values?[0] { if (values?.count ?? 0) > 0, let value = values?[0] {
title = value as! String title = value as! String
} }
let language:String! = currentOption?.extendedLanguageTag ?? "" let language: String! = currentOption?.extendedLanguageTag ?? ""
let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!) let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
@ -118,50 +116,50 @@ enum RCTVideoUtils {
"index": NSNumber(value: i), "index": NSNumber(value: i),
"title": title, "title": title,
"language": language ?? "", "language": language ?? "",
"selected": currentOption?.displayName == selectedOption?.displayName "selected": currentOption?.displayName == selectedOption?.displayName,
] as [String : Any] ] as [String: Any]
audioTracks.add(audioTrack) audioTracks.add(audioTrack)
} }
return audioTracks as [AnyObject]? return audioTracks as [AnyObject]?
} }
static func getTextTrackInfo(_ player:AVPlayer?) -> [TextTrack]! { static func getTextTrackInfo(_ player: AVPlayer?) -> [TextTrack]! {
guard let player = player else { guard let player = player else {
return [] return []
} }
// if streaming video, we extract the text tracks // if streaming video, we extract the text tracks
var textTracks:[TextTrack] = [] var textTracks: [TextTrack] = []
let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) 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] let currentOption = group?.options[i]
var title = "" var title = ""
let values = currentOption?.commonMetadata.map(\.value) let values = currentOption?.commonMetadata.map(\.value)
if (values?.count ?? 0) > 0, let value = values?[0] { if (values?.count ?? 0) > 0, let value = values?[0] {
title = value as! String title = value as! String
} }
let language:String! = currentOption?.extendedLanguageTag ?? "" let language: String! = currentOption?.extendedLanguageTag ?? ""
let selectedOpt = player.currentItem?.currentMediaSelection let selectedOpt = player.currentItem?.currentMediaSelection
let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!) let selectedOption: AVMediaSelectionOption? = player.currentItem?.currentMediaSelection.selectedMediaOption(in: group!)
let textTrack = TextTrack([ let textTrack = TextTrack([
"index": NSNumber(value: i), "index": NSNumber(value: i),
"title": title, "title": title,
"language": language, "language": language,
"selected": currentOption?.displayName == selectedOption?.displayName "selected": currentOption?.displayName == selectedOption?.displayName,
]) ])
textTracks.append(textTrack) textTracks.append(textTrack)
} }
return textTracks return textTracks
} }
// UNUSED // UNUSED
static func getCurrentTime(playerItem:AVPlayerItem?) -> Float { static func getCurrentTime(playerItem: AVPlayerItem?) -> Float {
return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero)) return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero))
} }
static func base64DataFromBase64String(base64String:String?) -> Data? { static func base64DataFromBase64String(base64String: String?) -> Data? {
if let base64String = base64String { if let base64String = base64String {
return Data(base64Encoded:base64String) return Data(base64Encoded: base64String)
} }
return nil return nil
} }
@ -175,76 +173,86 @@ enum RCTVideoUtils {
static func extractDataFromCustomSchemeUrl(from url: URL, scheme: String) -> Data? { static func extractDataFromCustomSchemeUrl(from url: URL, scheme: String) -> Data? {
guard url.scheme == scheme, 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) return Data(base64Encoded: adoptURL.absoluteString)
} }
static func generateMixComposition(_ asset:AVAsset) -> AVMutableComposition { static func generateMixComposition(_ asset: AVAsset) -> AVMutableComposition {
let mixComposition:AVMutableComposition = AVMutableComposition() let mixComposition = AVMutableComposition()
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first let videoAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
// we need videoAsset asset to be not null to get durration later // we need videoAsset asset to be not null to get durration later
if videoAsset == nil { if videoAsset == nil {
return mixComposition 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( try? videoCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
of: videoAsset, of: videoAsset,
at: .zero) at: .zero
)
let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first
let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid) let audioAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first
let audioCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
withMediaType: AVMediaType.audio,
preferredTrackID: kCMPersistentTrackID_Invalid
)
try? audioCompTrack.insertTimeRange( try? audioCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: audioAsset.timeRange.duration), CMTimeRangeMake(start: .zero, duration: audioAsset.timeRange.duration),
of: audioAsset, of: audioAsset,
at: .zero) at: .zero
)
return mixComposition return mixComposition
} }
static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] { static func getValidTextTracks(asset: AVAsset, assetOptions: NSDictionary?, mixComposition: AVMutableComposition, textTracks: [TextTrack]?) -> [TextTrack] {
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first let videoAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
var validTextTracks:[TextTrack] = [] var validTextTracks: [TextTrack] = []
if let textTracks = textTracks, textTracks.count > 0 { if let textTracks = textTracks, !textTracks.isEmpty {
for i in 0..<textTracks.count { for i in 0 ..< textTracks.count {
var textURLAsset:AVURLAsset! var textURLAsset: AVURLAsset!
let textUri:String = textTracks[i].uri let textUri: String = textTracks[i].uri
if textUri.lowercased().hasPrefix("http") { 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 { } else {
let isDisabledTrack:Bool! = textTracks[i].type == "disabled" let isDisabledTrack: Bool! = textTracks[i].type == "disabled"
let searchPath:FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory; let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options:nil) textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options: nil)
} }
let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first let textTrackAsset: AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset if textTrackAsset == nil { continue } // fix when there's no textTrackAsset
validTextTracks.append(textTracks[i]) validTextTracks.append(textTracks[i])
let textCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text, let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
preferredTrackID:kCMPersistentTrackID_Invalid) preferredTrackID: kCMPersistentTrackID_Invalid)
if videoAsset != nil { if videoAsset != nil {
try? textCompTrack.insertTimeRange( try? textCompTrack.insertTimeRange(
CMTimeRangeMake(start: .zero, duration: videoAsset!.timeRange.duration), CMTimeRangeMake(start: .zero, duration: videoAsset!.timeRange.duration),
of: textTrackAsset, of: textTrackAsset,
at: .zero) at: .zero
)
} }
} }
} }
let emptyVttFile:TextTrack? = self.createEmptyVttFile() let emptyVttFile: TextTrack? = self.createEmptyVttFile()
if (emptyVttFile != nil) { if emptyVttFile != nil {
validTextTracks.append(emptyVttFile!) validTextTracks.append(emptyVttFile!)
} }
return validTextTracks 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. * 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 * 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 fileManager = FileManager.default
let cachesDirectoryUrl = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] let cachesDirectoryUrl = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let filePath = cachesDirectoryUrl.appendingPathComponent("empty.vtt").path let filePath = cachesDirectoryUrl.appendingPathComponent("empty.vtt").path
if !fileManager.fileExists(atPath: filePath) { if !fileManager.fileExists(atPath: filePath) {
let stringToWrite = "WEBVTT\n\n1\n99:59:59.000 --> 99:59:59.001\n." let stringToWrite = "WEBVTT\n\n1\n99:59:59.000 --> 99:59:59.001\n."
@ -262,7 +270,7 @@ enum RCTVideoUtils {
return nil return nil
} }
} }
return TextTrack([ return TextTrack([
"language": "disabled", "language": "disabled",
"title": "EmptyVttFile", "title": "EmptyVttFile",
@ -270,15 +278,15 @@ enum RCTVideoUtils {
"uri": filePath, "uri": filePath,
]) ])
} }
static func delay(seconds: Int = 0) -> Promise<Void> { static func delay(seconds: Int = 0) -> Promise<Void> {
return Promise<Void>(on: .global()) { fulfill, reject in return Promise<Void>(on: .global()) { fulfill, _ in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC)) {
fulfill(()) fulfill(())
}) }
} }
} }
static func preparePHAsset(uri: String) -> Promise<AVAsset?> { static func preparePHAsset(uri: String) -> Promise<AVAsset?> {
return Promise<AVAsset?>(on: .global()) { fulfill, reject in return Promise<AVAsset?>(on: .global()) { fulfill, reject in
let assetId = String(uri[uri.index(uri.startIndex, offsetBy: "ph://".count)...]) 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 } 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 bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? ""
let url = source.isNetwork || source.isAsset let url = source.isNetwork || source.isAsset
? URL(string: source.uri?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") ? URL(string: source.uri?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
: URL(fileURLWithPath: bundlePath) : URL(fileURLWithPath: bundlePath)
let assetOptions:NSMutableDictionary! = NSMutableDictionary() let assetOptions: NSMutableDictionary! = NSMutableDictionary()
if source.isNetwork { if source.isNetwork {
if let headers = source.requestHeaders, headers.count > 0 { if let headers = source.requestHeaders, !headers.isEmpty {
assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying) assetOptions.setObject(headers, forKey: "AVURLAssetHTTPHeaderFieldsKey" as NSCopying)
} }
let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies let cookies: [AnyObject]! = HTTPCookieStorage.shared.cookies
assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying) assetOptions.setObject(cookies, forKey: AVURLAssetHTTPCookiesKey as NSCopying)
asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any]) asset = AVURLAsset(url: url!, options: assetOptions as! [String: Any])
} else { } else {
asset = AVURLAsset(url: url!) asset = AVURLAsset(url: url!)
} }
return (asset, assetOptions) return (asset, assetOptions)
} }
static func createMetadataItems(for mapping: [AVMetadataIdentifier: Any]) -> [AVMetadataItem] { 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, static func createMetadataItem(for identifier: AVMetadataIdentifier,
@ -329,15 +337,15 @@ enum RCTVideoUtils {
item.extendedLanguageTag = "und" item.extendedLanguageTag = "und"
return item.copy() as! AVMetadataItem return item.copy() as! AVMetadataItem
} }
static func createImageMetadataItem(imageUri: String) -> Data? { static func createImageMetadataItem(imageUri: String) -> Data? {
if let uri = URL(string: imageUri), if let uri = URL(string: imageUri),
let imgData = try? Data(contentsOf: uri), let imgData = try? Data(contentsOf: uri),
let image = UIImage(data: imgData), let image = UIImage(data: imgData),
let pngData = image.pngData() { let pngData = image.pngData() {
return pngData return pngData
} }
return nil return nil
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#import <React/RCTBridge.h>
#import "React/RCTViewManager.h" #import "React/RCTViewManager.h"
#import <React/RCTBridge.h>
@interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(drm, 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(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock);
RCT_EXTERN_METHOD(save:(NSDictionary *)options RCT_EXTERN_METHOD(save
reactTag:(nonnull NSNumber *)reactTag : (NSDictionary*)options reactTag
resolver:(RCTPromiseResolveBlock)resolve : (nonnull NSNumber*)reactTag resolver
rejecter:(RCTPromiseRejectBlock)reject) : (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license RCT_EXTERN_METHOD(setLicenseResult : (NSString*)license licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag)
licenseUrl:(NSString *)licenseUrl
reactTag:(nonnull NSNumber *)reactTag)
RCT_EXTERN_METHOD(setLicenseResultError:(NSString *)error RCT_EXTERN_METHOD(setLicenseResultError : (NSString*)error licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag)
licenseUrl:(NSString *)licenseUrl
reactTag:(nonnull NSNumber *)reactTag)
RCT_EXTERN_METHOD(setPlayerPauseState:(nonnull NSNumber *)paused RCT_EXTERN_METHOD(setPlayerPauseState : (nonnull NSNumber*)paused reactTag : (nonnull NSNumber*)reactTag)
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 RCT_EXTERN_METHOD(dismissFullscreenPlayer reactTag : (nonnull NSNumber*)reactTag)
reactTag:(nonnull NSNumber *)reactTag)
@end @end

View File

@ -3,77 +3,77 @@ import React
@objc(RCTVideoManager) @objc(RCTVideoManager)
class RCTVideoManager: RCTViewManager { class RCTVideoManager: RCTViewManager {
override func view() -> UIView { override func view() -> UIView {
return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher) return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher)
} }
func methodQueue() -> DispatchQueue { func methodQueue() -> DispatchQueue {
return bridge.uiManager.methodQueue return bridge.uiManager.methodQueue
} }
@objc(save:reactTag:resolver:rejecter:) @objc(save:reactTag:resolver:rejecter:)
func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void { func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock { _, viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.save(options: options, resolve: resolve, reject: reject) view.save(options: options, resolve: resolve, reject: reject)
} }
}) }
} }
@objc(setLicenseResult:licenseUrl:reactTag:) @objc(setLicenseResult:licenseUrl:reactTag:)
func setLicenseResult(license: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void { func setLicenseResult(license: NSString, licenseUrl: NSString, reactTag: NSNumber) {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock { _, viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.setLicenseResult(license as String, licenseUrl as String) view.setLicenseResult(license as String, licenseUrl as String)
} }
}) }
} }
@objc(setLicenseResultError:licenseUrl:reactTag:) @objc(setLicenseResultError:licenseUrl:reactTag:)
func setLicenseResultError(error: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void { func setLicenseResultError(error: NSString, licenseUrl: NSString, reactTag: NSNumber) {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock { _, viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.setLicenseResultError(error as String, licenseUrl as String) view.setLicenseResultError(error as String, licenseUrl as String)
} }
}) }
} }
@objc(dismissFullscreenPlayer:) @objc(dismissFullscreenPlayer:)
func dismissFullscreenPlayer(_ reactTag: NSNumber) -> Void { func dismissFullscreenPlayer(_ reactTag: NSNumber) {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock { _, viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.dismissFullscreenPlayer() view.dismissFullscreenPlayer()
} }
}) }
} }
@objc(presentFullscreenPlayer:) @objc(presentFullscreenPlayer:)
func presentFullscreenPlayer(_ reactTag: NSNumber) -> Void { func presentFullscreenPlayer(_ reactTag: NSNumber) {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock { _, viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.presentFullscreenPlayer() view.presentFullscreenPlayer()
} }
}) }
} }
@objc(setPlayerPauseState:reactTag:) @objc(setPlayerPauseState:reactTag:)
func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) -> Void { func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock { _, viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
@ -81,7 +81,7 @@ class RCTVideoManager: RCTViewManager {
let paused = paused.boolValue let paused = paused.boolValue
view.setPaused(paused) view.setPaused(paused)
} }
}) }
} }
override class func requiresMainQueueSetup() -> Bool { override class func requiresMainQueueSetup() -> Bool {

View File

@ -1,15 +1,13 @@
import AVKit import AVKit
class RCTVideoPlayerViewController: AVPlayerViewController { class RCTVideoPlayerViewController: AVPlayerViewController {
weak var rctDelegate: RCTVideoPlayerViewControllerDelegate? weak var rctDelegate: RCTVideoPlayerViewControllerDelegate?
// Optional paramters // Optional paramters
var preferredOrientation:String? var preferredOrientation: String?
var autorotate:Bool? var autorotate: Bool?
func shouldAutorotate() -> Bool { func shouldAutorotate() -> Bool {
if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") { if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") {
return true return true
} }
@ -26,21 +24,21 @@ class RCTVideoPlayerViewController: AVPlayerViewController {
#if !os(tvOS) #if !os(tvOS)
func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return .all 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 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 #endif
} }

View File

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

View File

@ -2,10 +2,10 @@
@interface RCTVideoSwiftLog : NSObject @interface RCTVideoSwiftLog : NSObject
+ (void)error:(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)warn:(NSString* _Nonnull)message file:(NSString* _Nonnull)file line:(NSUInteger)line;
+ (void)info:(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)log:(NSString* _Nonnull)message file:(NSString* _Nonnull)file line:(NSUInteger)line;
+ (void)trace:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + (void)trace:(NSString* _Nonnull)message file:(NSString* _Nonnull)file line:(NSUInteger)line;
@end @end

View File

@ -4,29 +4,24 @@
@implementation RCTVideoSwiftLog @implementation RCTVideoSwiftLog
+ (void)info:(NSString *)message file:(NSString *)file line:(NSUInteger)line + (void)info:(NSString*)message file:(NSString*)file line:(NSUInteger)line {
{ _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message);
_RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message);
} }
+ (void)warn:(NSString *)message file:(NSString *)file line:(NSUInteger)line + (void)warn:(NSString*)message file:(NSString*)file line:(NSUInteger)line {
{ _RCTLogNativeInternal(RCTLogLevelWarning, file.UTF8String, (int)line, @"%@", message);
_RCTLogNativeInternal(RCTLogLevelWarning, file.UTF8String, (int)line, @"%@", message);
} }
+ (void)error:(NSString *)message file:(NSString *)file line:(NSUInteger)line + (void)error:(NSString*)message file:(NSString*)file line:(NSUInteger)line {
{ _RCTLogNativeInternal(RCTLogLevelError, file.UTF8String, (int)line, @"%@", message);
_RCTLogNativeInternal(RCTLogLevelError, file.UTF8String, (int)line, @"%@", message);
} }
+ (void)log:(NSString *)message file:(NSString *)file line:(NSUInteger)line + (void)log:(NSString*)message file:(NSString*)file line:(NSUInteger)line {
{ _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message);
_RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message);
} }
+ (void)trace:(NSString *)message file:(NSString *)file line:(NSUInteger)line + (void)trace:(NSString*)message file:(NSString*)file line:(NSUInteger)line {
{ _RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message);
_RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message);
} }
@end @end

View File

@ -1,5 +1,5 @@
// //
// RCTLog.swift // RCTVideoSwiftLog.swift
// WebViewExample // WebViewExample
// //
// Created by Jimmy Dee on 4/5/17. // Created by Jimmy Dee on 4/5/17.
@ -27,29 +27,28 @@
let logHeader: String = "RNV:" 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) 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) 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) 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) 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) RCTVideoSwiftLog.trace(logHeader + message, file: file, line: line)
} }
func DebugLog(_ message: String) { func DebugLog(_ message: String) {
#if DEBUG #if DEBUG
print(logHeader + message) print(logHeader + message)
#endif #endif
} }

View File

@ -1,8 +1,8 @@
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#import <SPTPersistentCache/SPTPersistentCache.h> #import <SPTPersistentCache/SPTPersistentCache.h>
#import <SPTPersistentCache/SPTPersistentCacheOptions.h> #import <SPTPersistentCache/SPTPersistentCacheOptions.h>
#import <CommonCrypto/CommonDigest.h>
typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) { typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) {
RCTVideoCacheStatusMissingFileExtension, RCTVideoCacheStatusMissingFileExtension,
@ -14,25 +14,24 @@ typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) {
@class SPTPersistentCache; @class SPTPersistentCache;
@class SPTPersistentCacheOptions; @class SPTPersistentCacheOptions;
@interface RCTVideoCache : NSObject @interface RCTVideoCache : NSObject {
{ SPTPersistentCache* videoCache;
SPTPersistentCache *videoCache; NSString* _Nullable cachePath;
NSString * _Nullable cachePath; NSString* temporaryCachePath;
NSString * temporaryCachePath; NSString* _Nullable cacheIdentifier;
NSString * _Nullable cacheIdentifier;
} }
@property(nonatomic, strong) SPTPersistentCache * _Nullable videoCache; @property(nonatomic, strong) SPTPersistentCache* _Nullable videoCache;
@property(nonatomic, strong) NSString * cachePath; @property(nonatomic, strong) NSString* cachePath;
@property(nonatomic, strong) NSString * cacheIdentifier; @property(nonatomic, strong) NSString* cacheIdentifier;
@property(nonatomic, strong) NSString * temporaryCachePath; @property(nonatomic, strong) NSString* temporaryCachePath;
+ (RCTVideoCache *)sharedInstance; + (RCTVideoCache*)sharedInstance;
- (void)storeItem:(NSData *)data forUri:(NSString *)uri withCallback:(void(^)(BOOL))handler; - (void)storeItem:(NSData*)data forUri:(NSString*)uri withCallback:(void (^)(BOOL))handler;
- (void)getItemForUri:(NSString *)url withCallback:(void(^)(RCTVideoCacheStatus, AVAsset * _Nullable)) handler; - (void)getItemForUri:(NSString*)url withCallback:(void (^)(RCTVideoCacheStatus, AVAsset* _Nullable))handler;
- (NSURL *)createUniqueTemporaryFileUrl:(NSString * _Nonnull)url withExtension:(NSString * _Nonnull) extension; - (NSURL*)createUniqueTemporaryFileUrl:(NSString* _Nonnull)url withExtension:(NSString* _Nonnull)extension;
- (AVURLAsset *)getItemFromTemporaryStorage:(NSString *)key; - (AVURLAsset*)getItemFromTemporaryStorage:(NSString*)key;
- (BOOL)saveDataToTemporaryStorage:(NSData *)data key:(NSString *)key; - (BOOL)saveDataToTemporaryStorage:(NSData*)data key:(NSString*)key;
- (void) createTemporaryPath; - (void)createTemporaryPath;
@end @end

View File

@ -7,8 +7,8 @@
@synthesize cacheIdentifier; @synthesize cacheIdentifier;
@synthesize temporaryCachePath; @synthesize temporaryCachePath;
+ (RCTVideoCache *)sharedInstance { + (RCTVideoCache*)sharedInstance {
static RCTVideoCache *sharedInstance = nil; static RCTVideoCache* sharedInstance = nil;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init]; sharedInstance = [[self alloc] init];
@ -20,8 +20,9 @@
if (self = [super init]) { if (self = [super init]) {
self.cacheIdentifier = @"rct.video.cache"; self.cacheIdentifier = @"rct.video.cache";
self.temporaryCachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:self.cacheIdentifier]; self.temporaryCachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:self.cacheIdentifier];
self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:self.cacheIdentifier]; self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject
SPTPersistentCacheOptions *options = [SPTPersistentCacheOptions new]; stringByAppendingPathComponent:self.cacheIdentifier];
SPTPersistentCacheOptions* options = [SPTPersistentCacheOptions new];
options.cachePath = self.cachePath; options.cachePath = self.cachePath;
options.cacheIdentifier = self.cacheIdentifier; options.cacheIdentifier = self.cacheIdentifier;
options.defaultExpirationPeriod = 60 * 60 * 24 * 30; options.defaultExpirationPeriod = 60 * 60 * 24 * 30;
@ -29,7 +30,7 @@
options.sizeConstraintBytes = 1024 * 1024 * 100; options.sizeConstraintBytes = 1024 * 1024 * 100;
options.useDirectorySeparation = NO; options.useDirectorySeparation = NO;
#ifdef DEBUG #ifdef DEBUG
options.debugOutput = ^(NSString *string) { options.debugOutput = ^(NSString* string) {
NSLog(@"VideoCache: debug %@", string); NSLog(@"VideoCache: debug %@", string);
}; };
#endif #endif
@ -40,8 +41,8 @@
return self; return self;
} }
- (void) createTemporaryPath { - (void)createTemporaryPath {
NSError *error = nil; NSError* error = nil;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryCachePath BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryCachePath
withIntermediateDirectories:YES withIntermediateDirectories:YES
attributes:nil attributes:nil
@ -53,97 +54,101 @@
#endif #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) { if (key == nil) {
handler(NO); handler(NO);
return; return;
} }
[self saveDataToTemporaryStorage:data key:key]; [self saveDataToTemporaryStorage:data key:key];
[self.videoCache storeData:data forKey:key locked:NO withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { [self.videoCache storeData:data
if (response.error) { forKey:key
locked:NO
withCallback:^(SPTPersistentCacheResponse* _Nonnull response) {
if (response.error) {
#ifdef DEBUG #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 #endif
handler(NO); handler(NO);
return; return;
} }
handler(YES); handler(YES);
} onQueue:dispatch_get_main_queue()]; }
onQueue:dispatch_get_main_queue()];
return; return;
} }
- (AVURLAsset *)getItemFromTemporaryStorage:(NSString *)key { - (AVURLAsset*)getItemFromTemporaryStorage:(NSString*)key {
NSString * temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; NSString* temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:temporaryFilePath]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:temporaryFilePath];
if (!fileExists) { if (!fileExists) {
return nil; return nil;
} }
NSURL *assetUrl = [[NSURL alloc] initFileURLWithPath:temporaryFilePath]; NSURL* assetUrl = [[NSURL alloc] initFileURLWithPath:temporaryFilePath];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:assetUrl options:nil]; AVURLAsset* asset = [AVURLAsset URLAssetWithURL:assetUrl options:nil];
return asset; return asset;
} }
- (BOOL)saveDataToTemporaryStorage:(NSData *)data key:(NSString *)key { - (BOOL)saveDataToTemporaryStorage:(NSData*)data key:(NSString*)key {
NSString *temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key]; NSString* temporaryFilePath = [self.temporaryCachePath stringByAppendingPathComponent:key];
[data writeToFile:temporaryFilePath atomically:YES]; [data writeToFile:temporaryFilePath atomically:YES];
return YES; return YES;
} }
- (NSString *)generateCacheKeyForUri:(NSString *)uri { - (NSString*)generateCacheKeyForUri:(NSString*)uri {
NSString *uriWithoutQueryParams = uri; NSString* uriWithoutQueryParams = uri;
// parse file extension // parse file extension
if ([uri rangeOfString:@"?"].location != NSNotFound) { if ([uri rangeOfString:@"?"].location != NSNotFound) {
NSArray<NSString*> * components = [uri componentsSeparatedByString:@"?"]; NSArray<NSString*>* components = [uri componentsSeparatedByString:@"?"];
uriWithoutQueryParams = [components objectAtIndex:0]; uriWithoutQueryParams = [components objectAtIndex:0];
} }
NSString * pathExtension = [uriWithoutQueryParams pathExtension]; NSString* pathExtension = [uriWithoutQueryParams pathExtension];
NSArray * supportedExtensions = @[@"m4v", @"mp4", @"mov"]; NSArray* supportedExtensions = @[ @"m4v", @"mp4", @"mov" ];
if ([pathExtension isEqualToString:@""]) { if ([pathExtension isEqualToString:@""]) {
NSDictionary *userInfo = @{ NSDictionary* userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing file extension.", nil), NSLocalizedDescriptionKey : NSLocalizedString(@"Missing file extension.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Missing file extension.", nil), NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Missing file extension.", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Missing file extension.", nil) NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString(@"Missing file extension.", nil)
}; };
NSError *error = [NSError errorWithDomain:@"RCTVideoCache" NSError* error = [NSError errorWithDomain:@"RCTVideoCache" code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo];
code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo];
@throw error; @throw error;
} else if (![supportedExtensions containsObject:pathExtension]) { } else if (![supportedExtensions containsObject:pathExtension]) {
// Notably, we don't currently support m3u8 (HLS playlists) // Notably, we don't currently support m3u8 (HLS playlists)
NSDictionary *userInfo = @{ NSDictionary* userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Unsupported file extension.", nil), NSLocalizedDescriptionKey : NSLocalizedString(@"Unsupported file extension.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"Unsupported file extension.", nil), NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Unsupported file extension.", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Unsupported file extension.", nil) NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString(@"Unsupported file extension.", nil)
}; };
NSError *error = [NSError errorWithDomain:@"RCTVideoCache" NSError* error = [NSError errorWithDomain:@"RCTVideoCache" code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo];
code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo];
@throw error; @throw error;
} }
return [[self generateHashForUrl:uri] stringByAppendingPathExtension:pathExtension]; 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 { @try {
NSString *key = [self generateCacheKeyForUri:uri]; NSString* key = [self generateCacheKeyForUri:uri];
AVURLAsset * temporaryAsset = [self getItemFromTemporaryStorage:key]; AVURLAsset* temporaryAsset = [self getItemFromTemporaryStorage:key];
if (temporaryAsset != nil) { if (temporaryAsset != nil) {
handler(RCTVideoCacheStatusAvailable, temporaryAsset); handler(RCTVideoCacheStatusAvailable, temporaryAsset);
return; return;
} }
[self.videoCache loadDataForKey:key withCallback:^(SPTPersistentCacheResponse * _Nonnull response) { [self.videoCache loadDataForKey:key
if (response.record == nil || response.record.data == nil) { withCallback:^(SPTPersistentCacheResponse* _Nonnull response) {
handler(RCTVideoCacheStatusNotAvailable, nil); if (response.record == nil || response.record.data == nil) {
return; handler(RCTVideoCacheStatusNotAvailable, nil);
} return;
[self saveDataToTemporaryStorage:response.record.data key:key]; }
handler(RCTVideoCacheStatusAvailable, [self getItemFromTemporaryStorage:key]); [self saveDataToTemporaryStorage:response.record.data key:key];
} onQueue:dispatch_get_main_queue()]; handler(RCTVideoCacheStatusAvailable, [self getItemFromTemporaryStorage:key]);
} @catch (NSError * err) { }
onQueue:dispatch_get_main_queue()];
} @catch (NSError* err) {
switch (err.code) { switch (err.code) {
case RCTVideoCacheStatusMissingFileExtension: case RCTVideoCacheStatusMissingFileExtension:
handler(RCTVideoCacheStatusMissingFileExtension, nil); handler(RCTVideoCacheStatusMissingFileExtension, nil);
@ -157,18 +162,14 @@
} }
} }
- (NSString *)generateHashForUrl:(NSString *)string { - (NSString*)generateHashForUrl:(NSString*)string {
const char *cStr = [string UTF8String]; const char* cStr = [string UTF8String];
unsigned char result[CC_MD5_DIGEST_LENGTH]; unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5( cStr, (CC_LONG)strlen(cStr), result ); CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
return [NSString stringWithFormat: 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],
@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", result[3], result[4], result[5], result[6], result[7], result[8], result[9], result[10], result[11],
result[0], result[1], result[2], result[3], result[12], result[13], result[14], result[15]];
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 @end

View File

@ -1,87 +1,97 @@
import Foundation
import AVFoundation import AVFoundation
import DVAssetLoaderDelegate import DVAssetLoaderDelegate
import Foundation
import Promises import Promises
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance()
private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance()
var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) -> AVPlayerItem)? var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) -> AVPlayerItem)?
override init() { override init() {
super.init() super.init()
} }
func shouldCache(source: VideoSource, textTracks:[TextTrack]?) -> Bool { func shouldCache(source: VideoSource, textTracks: [TextTrack]?) -> Bool {
if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) { if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.isEmpty)) {
/* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying
* to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded.
* Until this is fixed, we need to bypass caching when text tracks are specified. * Until this is fixed, we need to bypass caching when text tracks are specified.
*/ */
DebugLog("Caching is not supported for uri '\(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 true
} }
return false return false
} }
func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!) -> Promise<AVPlayerItem?> { func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) -> Promise<AVPlayerItem?> {
let url = URL(string: uri) let url = URL(string: uri)
return getItemForUri(uri) return getItemForUri(uri)
.then{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) -> AVPlayerItem in .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)} guard let self = self, let playerItemPrepareText = self.playerItemPrepareText else { throw NSError(domain: "", code: 0, userInfo: nil) }
switch (videoCacheStatus) { switch videoCacheStatus {
case .missingFileExtension: case .missingFileExtension:
DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") DebugLog("""
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) Could not generate cache key for uri '\(uri)'.
return playerItemPrepareText(asset, options, "") It is currently not supported to cache urls that do not include a file extension.
The video file will not be cached.
case .unsupportedFileExtension: Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") """)
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
return playerItemPrepareText(asset, options, "") return playerItemPrepareText(asset, options, "")
default: case .unsupportedFileExtension:
if let cachedAsset = cachedAsset { DebugLog("""
DebugLog("Playing back uri '\(uri)' from cache") Could not generate cache key for uri '\(uri)'.
// See note in playerItemForSource about not being able to support text tracks & caching The file extension of that uri is currently not supported.
return AVPlayerItem(asset: cachedAsset) 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?)> { func getItemForUri(_ uri: String) -> Promise<(videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?)> {
return Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> { fulfill, reject in return Promise<(videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?)> { fulfill, _ in
self._videoCache.getItemForUri(uri, withCallback:{ (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in self._videoCache.getItemForUri(uri, withCallback: { (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) in
fulfill((videoCacheStatus, cachedAsset)) fulfill((videoCacheStatus, cachedAsset))
}) })
} }
} }
// MARK: - DVAssetLoaderDelegate // MARK: - DVAssetLoaderDelegate
func dvAssetLoaderDelegate(_ loaderDelegate: DVAssetLoaderDelegate!, didLoad data: Data!, for url: URL!) { func dvAssetLoaderDelegate(_: DVAssetLoaderDelegate!, didLoad data: Data!, for url: URL!) {
_videoCache.storeItem(data as Data?, forUri:url.absoluteString, withCallback:{ (success:Bool) in _videoCache.storeItem(data as Data?, forUri: url.absoluteString, withCallback: { (_: Bool) in
DebugLog("Cache data stored successfully 🎉") DebugLog("Cache data stored successfully 🎉")
}) })
} }
} }