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:
parent
72679a7d63
commit
800aee09de
2
.github/workflows/check-android.yml
vendored
2
.github/workflows/check-android.yml
vendored
@ -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: |
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
@ -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()
|
@ -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
|
@ -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,6 +8,7 @@ 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
|
@ -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
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
--allman false
|
--allman false
|
||||||
--indent 2
|
--indent 4
|
||||||
--exclude Pods,Generated
|
--exclude Pods,Generated
|
||||||
|
|
||||||
--disable andOperator
|
--disable andOperator
|
||||||
@ -11,3 +11,6 @@
|
|||||||
--enable markTypes
|
--enable markTypes
|
||||||
|
|
||||||
--enable isEmpty
|
--enable isEmpty
|
||||||
|
|
||||||
|
--funcattributes "prev-line"
|
||||||
|
--maxwidth 160
|
@ -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
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
struct Chapter {
|
struct Chapter {
|
||||||
let title: String
|
let title: String
|
||||||
let uri: String?
|
let uri: String?
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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?
|
||||||
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
struct TextTrack {
|
struct TextTrack {
|
||||||
let type: String
|
let type: String
|
||||||
let language: String
|
let language: String
|
||||||
|
@ -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?
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
init(_ onPictureInPictureStatusChanged: (() -> Void)? = nil, _ onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil) {
|
||||||
_onPictureInPictureStatusChanged = onPictureInPictureStatusChanged
|
_onPictureInPictureStatusChanged = onPictureInPictureStatusChanged
|
||||||
_onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop
|
_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 }
|
|
||||||
|
|
||||||
_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) {
|
func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {
|
||||||
if _isActive == isActive {
|
guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
|
||||||
return
|
|
||||||
|
_onPictureInPictureStatusChanged()
|
||||||
}
|
}
|
||||||
_isActive = isActive
|
|
||||||
|
|
||||||
guard let _pipController = _pipController else { return }
|
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
|
||||||
|
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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
@ -69,19 +78,19 @@ 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 {
|
||||||
@ -95,7 +104,7 @@ class RCTPlayerObserver: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,10 +115,18 @@ class RCTPlayerObserver: NSObject {
|
|||||||
|
|
||||||
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() {
|
||||||
@ -118,12 +135,21 @@ class RCTPlayerObserver: NSObject {
|
|||||||
_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,
|
||||||
|
options: [.new],
|
||||||
|
changeHandler: _handlers.handleReadyForDisplay
|
||||||
|
)
|
||||||
|
|
||||||
_playerViewControllerOverlayFrameObserver = playerViewController.contentOverlayView?.observe(\.frame, options: [.new, .old], changeHandler: _handlers.handleViewControllerOverlayViewFrameChange)
|
_playerViewControllerOverlayFrameObserver = playerViewController.contentOverlayView?.observe(
|
||||||
|
\.frame,
|
||||||
|
options: [.new, .old],
|
||||||
|
changeHandler: _handlers.handleViewControllerOverlayViewFrameChange
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removePlayerViewControllerObservers() {
|
func removePlayerViewControllerObservers() {
|
||||||
@ -132,8 +158,8 @@ class RCTPlayerObserver: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
@ -141,15 +167,15 @@ class RCTPlayerObserver: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,53 +188,53 @@ class RCTPlayerObserver: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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?,
|
||||||
@ -37,20 +35,19 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -69,7 +66,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
_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)")
|
||||||
@ -94,16 +91,15 @@ 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
|
||||||
}
|
}
|
||||||
@ -112,10 +108,10 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
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)
|
||||||
@ -132,7 +128,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
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 {
|
||||||
@ -151,11 +147,11 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
|||||||
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,24 +160,23 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,40 +1,40 @@
|
|||||||
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
|
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 postDataTask = URLSession.shared.dataTask(
|
||||||
|
with: request as URLRequest,
|
||||||
|
completionHandler: { (data: Data!, response: URLResponse!, error: Error!) in
|
||||||
|
let httpResponse: HTTPURLResponse! = (response as! HTTPURLResponse)
|
||||||
|
|
||||||
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 error == nil else {
|
guard data != nil, let decodedData = Data(base64Encoded: data, options: []) else {
|
||||||
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
reject(RCTVideoErrorHandler.noDataFromLicenseRequest)
|
||||||
reject(error)
|
return
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ struct RCTVideoDRM {
|
|||||||
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"
|
||||||
@ -58,9 +58,15 @@ struct RCTVideoDRM {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -72,7 +78,7 @@ struct RCTVideoDRM {
|
|||||||
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)
|
||||||
@ -93,19 +99,18 @@ struct RCTVideoDRM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func createCertificateData(certificateStringUrl:String?, base64Certificate:Bool?) -> Promise<Data> {
|
static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) -> Promise<Data> {
|
||||||
return Promise<Data>(on: .global()) { fulfill, reject in
|
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 {}
|
||||||
@ -119,41 +124,49 @@ struct RCTVideoDRM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -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,16 +14,18 @@ enum RCTVideoError : Int {
|
|||||||
case invalidContentId
|
case invalidContentId
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RCTVideoErrorHandler {
|
// MARK: - 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",
|
||||||
@ -29,8 +33,9 @@ enum RCTVideoErrorHandler {
|
|||||||
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",
|
||||||
@ -38,8 +43,9 @@ enum RCTVideoErrorHandler {
|
|||||||
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",
|
||||||
@ -47,8 +53,9 @@ enum RCTVideoErrorHandler {
|
|||||||
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",
|
||||||
@ -56,8 +63,9 @@ enum RCTVideoErrorHandler {
|
|||||||
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",
|
||||||
@ -65,8 +73,9 @@ enum RCTVideoErrorHandler {
|
|||||||
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(
|
||||||
@ -75,21 +84,22 @@ 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(
|
||||||
@ -98,6 +108,7 @@ enum RCTVideoErrorHandler {
|
|||||||
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?",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,47 @@
|
|||||||
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
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
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
|
||||||
@ -33,10 +32,10 @@ enum RCTVideoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -46,71 +45,70 @@ enum RCTVideoUtils {
|
|||||||
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,36 +116,36 @@ 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)
|
||||||
}
|
}
|
||||||
@ -155,13 +153,13 @@ enum RCTVideoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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,68 +173,77 @@ 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 audioAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first
|
||||||
let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid)
|
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!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +251,8 @@ enum RCTVideoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
@ -272,10 +280,10 @@ enum RCTVideoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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(())
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,22 +302,22 @@ 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!)
|
||||||
}
|
}
|
||||||
@ -317,7 +325,7 @@ enum RCTVideoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@ -330,7 +338,7 @@ enum RCTVideoUtils {
|
|||||||
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),
|
||||||
|
@ -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
@ -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
|
||||||
|
@ -3,7 +3,6 @@ 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)
|
||||||
}
|
}
|
||||||
@ -13,67 +12,68 @@ class RCTVideoManager: RCTViewManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
|
@ -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 {
|
func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
|
||||||
if preferredOrientation?.lowercased() == "landscape" {
|
if preferredOrientation?.lowercased() == "landscape" {
|
||||||
return .landscapeRight
|
return .landscapeRight
|
||||||
} else if preferredOrientation?.lowercased() == "portrait" {
|
} else if preferredOrientation?.lowercased() == "portrait" {
|
||||||
return .portrait
|
return .portrait
|
||||||
} else {
|
} else {
|
||||||
// default case
|
// default case
|
||||||
let orientation = UIApplication.shared.statusBarOrientation
|
let orientation = UIApplication.shared.statusBarOrientation
|
||||||
return orientation
|
return orientation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,75 +1,87 @@
|
|||||||
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.
|
||||||
|
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||||
|
""")
|
||||||
|
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||||
|
return playerItemPrepareText(asset, options, "")
|
||||||
|
|
||||||
case .unsupportedFileExtension:
|
case .unsupportedFileExtension:
|
||||||
DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
DebugLog("""
|
||||||
let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any])
|
Could not generate cache key for uri '\(uri)'.
|
||||||
return playerItemPrepareText(asset, options, "")
|
The file extension of that uri is currently not supported.
|
||||||
|
The video file will not be cached.
|
||||||
|
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||||
|
""")
|
||||||
|
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||||
|
return playerItemPrepareText(asset, options, "")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if let cachedAsset = cachedAsset {
|
if let cachedAsset = cachedAsset {
|
||||||
DebugLog("Playing back uri '\(uri)' from cache")
|
DebugLog("Playing back uri '\(uri)' from cache")
|
||||||
// See note in playerItemForSource about not being able to support text tracks & caching
|
// See note in playerItemForSource about not being able to support text tracks & caching
|
||||||
return AVPlayerItem(asset: cachedAsset)
|
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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -77,11 +89,9 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
|||||||
|
|
||||||
// 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 🎉")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user