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:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/
|
||||
curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/
|
||||
- name: run ktlint
|
||||
working-directory: ./android/
|
||||
run: |
|
||||
|
@ -1,6 +1,6 @@
|
||||
[*.{kt,kts}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
indent_size=4
|
||||
continuation_indent_size=4
|
||||
insert_final_newline=true
|
||||
max_line_length=160
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.brentvatne.common.API
|
||||
package com.brentvatne.common.api
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import java.lang.annotation.Retention
|
||||
@ -29,10 +29,11 @@ internal object ResizeMode {
|
||||
* Keeps the aspect ratio but takes up the view's size.
|
||||
*/
|
||||
const val RESIZE_MODE_CENTER_CROP = 4
|
||||
|
||||
@JvmStatic
|
||||
@Mode
|
||||
fun toResizeMode(ordinal: Int): Int {
|
||||
return when (ordinal) {
|
||||
fun toResizeMode(ordinal: Int): Int =
|
||||
when (ordinal) {
|
||||
RESIZE_MODE_FIXED_WIDTH -> RESIZE_MODE_FIXED_WIDTH
|
||||
RESIZE_MODE_FIXED_HEIGHT -> RESIZE_MODE_FIXED_HEIGHT
|
||||
RESIZE_MODE_FILL -> RESIZE_MODE_FILL
|
||||
@ -40,7 +41,6 @@ internal object ResizeMode {
|
||||
RESIZE_MODE_FIT -> RESIZE_MODE_FIT
|
||||
else -> RESIZE_MODE_FIT
|
||||
}
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(
|
@ -1,4 +1,4 @@
|
||||
package com.brentvatne.common.API
|
||||
package com.brentvatne.common.api
|
||||
|
||||
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
@ -24,6 +24,7 @@ class SubtitleStyle private constructor() {
|
||||
private const val PROP_PADDING_TOP = "paddingTop"
|
||||
private const val PROP_PADDING_LEFT = "paddingLeft"
|
||||
private const val PROP_PADDING_RIGHT = "paddingRight"
|
||||
|
||||
@JvmStatic
|
||||
fun parse(src: ReadableMap?): SubtitleStyle {
|
||||
val subtitleStyle = SubtitleStyle()
|
@ -1,4 +1,4 @@
|
||||
package com.brentvatne.common.API
|
||||
package com.brentvatne.common.api
|
||||
|
||||
/*
|
||||
* 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
|
||||
@ -8,6 +8,7 @@ class Track {
|
||||
var mimeType: String? = null
|
||||
var language: String? = null
|
||||
var isSelected = false
|
||||
|
||||
// in bps available only on audio tracks
|
||||
var bitrate = 0
|
||||
var index = 0
|
@ -1,4 +1,4 @@
|
||||
package com.brentvatne.common.API
|
||||
package com.brentvatne.common.api
|
||||
|
||||
/*
|
||||
* internal representation of audio & text tracks
|
@ -4,9 +4,9 @@ import androidx.annotation.StringDef;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.brentvatne.common.API.TimedMetadata;
|
||||
import com.brentvatne.common.API.Track;
|
||||
import com.brentvatne.common.API.VideoTrack;
|
||||
import com.brentvatne.common.api.TimedMetadata;
|
||||
import com.brentvatne.common.api.Track;
|
||||
import com.brentvatne.common.api.VideoTrack;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
|
@ -12,8 +12,10 @@ import java.lang.Exception
|
||||
object DebugLog {
|
||||
// log level to display
|
||||
private var level = Log.WARN
|
||||
|
||||
// enable thread display in logs
|
||||
private var displayThread = true
|
||||
|
||||
// add a common prefix for easy filtering
|
||||
private const val TAG_PREFIX = "RNV"
|
||||
|
||||
@ -24,15 +26,14 @@ object DebugLog {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun getTag(tag: String): String {
|
||||
return TAG_PREFIX + tag
|
||||
}
|
||||
private fun getTag(tag: String): String = TAG_PREFIX + tag
|
||||
|
||||
@JvmStatic
|
||||
private fun getMsg(msg: String): String {
|
||||
return if (displayThread) {
|
||||
private fun getMsg(msg: String): String =
|
||||
if (displayThread) {
|
||||
"[" + Thread.currentThread().name + "] " + msg
|
||||
} else msg
|
||||
} else {
|
||||
msg
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -1,8 +1,8 @@
|
||||
package com.brentvatne.common.toolbox
|
||||
|
||||
import com.facebook.react.bridge.Dynamic
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import java.util.HashMap
|
||||
|
||||
/*
|
||||
@ -53,17 +53,19 @@ object ReactBridgeUtils {
|
||||
|
||||
@JvmStatic
|
||||
fun safeGetInt(map: ReadableMap?, key: String?): Int {
|
||||
return safeGetInt(map, key, 0);
|
||||
return safeGetInt(map, key, 0)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun safeGetDouble(map: ReadableMap?, key: String?, fallback: Double): Double {
|
||||
return if (map != null && map.hasKey(key!!) && !map.isNull(key)) map.getDouble(key) else fallback
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun safeGetDouble(map: ReadableMap?, key: String?): Double {
|
||||
return safeGetDouble(map, key, 0.0);
|
||||
return safeGetDouble(map, key, 0.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* toStringMap converts a [ReadableMap] into a HashMap.
|
||||
*
|
||||
@ -116,17 +118,16 @@ object ReactBridgeUtils {
|
||||
if (str1 == null || str2 == null) return false // only 1 is null
|
||||
if (str1.size != str2.size) return false // only 1 is null
|
||||
for (i in str1.indices) {
|
||||
if (str1[i] == str2[i]) // standard check
|
||||
if (str1[i] == str2[i]) {
|
||||
// standard check
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun safeStringMapEquals(
|
||||
first: Map<String?, String?>?,
|
||||
second: Map<String?, String?>?
|
||||
): Boolean {
|
||||
fun safeStringMapEquals(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 false // only 1 is null
|
||||
if (first.size != second.size) {
|
||||
|
@ -19,7 +19,7 @@ import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.brentvatne.common.API.ResizeMode;
|
||||
import com.brentvatne.common.api.ResizeMode;
|
||||
|
||||
/**
|
||||
* A {@link FrameLayout} that resizes itself to match a specified aspect ratio.
|
||||
|
@ -25,8 +25,8 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.brentvatne.common.API.ResizeMode;
|
||||
import com.brentvatne.common.API.SubtitleStyle;
|
||||
import com.brentvatne.common.api.ResizeMode;
|
||||
import com.brentvatne.common.api.SubtitleStyle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -91,11 +91,11 @@ import androidx.media3.extractor.metadata.id3.Id3Frame;
|
||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
|
||||
import androidx.media3.ui.LegacyPlayerControlView;
|
||||
|
||||
import com.brentvatne.common.API.ResizeMode;
|
||||
import com.brentvatne.common.API.SubtitleStyle;
|
||||
import com.brentvatne.common.API.TimedMetadata;
|
||||
import com.brentvatne.common.API.Track;
|
||||
import com.brentvatne.common.API.VideoTrack;
|
||||
import com.brentvatne.common.api.ResizeMode;
|
||||
import com.brentvatne.common.api.SubtitleStyle;
|
||||
import com.brentvatne.common.api.TimedMetadata;
|
||||
import com.brentvatne.common.api.Track;
|
||||
import com.brentvatne.common.api.VideoTrack;
|
||||
import com.brentvatne.common.react.VideoEventEmitter;
|
||||
import com.brentvatne.common.toolbox.DebugLog;
|
||||
import com.brentvatne.react.R;
|
||||
|
@ -10,8 +10,8 @@ import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.RawResourceDataSource;
|
||||
import androidx.media3.exoplayer.DefaultLoadControl;
|
||||
|
||||
import com.brentvatne.common.API.ResizeMode;
|
||||
import com.brentvatne.common.API.SubtitleStyle;
|
||||
import com.brentvatne.common.api.ResizeMode;
|
||||
import com.brentvatne.common.api.SubtitleStyle;
|
||||
import com.brentvatne.common.react.VideoEventEmitter;
|
||||
import com.brentvatne.common.toolbox.DebugLog;
|
||||
import com.brentvatne.common.toolbox.ReactBridgeUtils;
|
||||
|
@ -1,5 +1,5 @@
|
||||
--allman false
|
||||
--indent 2
|
||||
--indent 4
|
||||
--exclude Pods,Generated
|
||||
|
||||
--disable andOperator
|
||||
@ -11,3 +11,6 @@
|
||||
--enable markTypes
|
||||
|
||||
--enable isEmpty
|
||||
|
||||
--funcattributes "prev-line"
|
||||
--maxwidth 160
|
@ -6,6 +6,11 @@ disabled_rules:
|
||||
- file_length
|
||||
- cyclomatic_complexity
|
||||
- function_body_length
|
||||
- function_parameter_count
|
||||
- empty_string
|
||||
# TODO: Remove this once all force casts are removed
|
||||
- force_cast
|
||||
|
||||
opt_in_rules:
|
||||
- contains_over_filter_count
|
||||
- contains_over_filter_is_empty
|
||||
@ -13,7 +18,6 @@ opt_in_rules:
|
||||
- contains_over_range_nil_comparison
|
||||
- empty_collection_literal
|
||||
- empty_count
|
||||
- empty_string
|
||||
- first_where
|
||||
- flatmap_over_map_reduce
|
||||
- last_where
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
struct Chapter {
|
||||
let title: String
|
||||
let uri: String?
|
||||
|
@ -1,7 +1,7 @@
|
||||
struct DRMParams {
|
||||
let type: String?
|
||||
let licenseServer: String?
|
||||
let headers: Dictionary<String,Any>?
|
||||
let headers: [String: Any]?
|
||||
let contentId: String?
|
||||
let certificateUrl: String?
|
||||
let base64Certificate: Bool?
|
||||
@ -25,6 +25,6 @@ struct DRMParams {
|
||||
self.contentId = json["contentId"] as? String
|
||||
self.certificateUrl = json["certificateUrl"] as? String
|
||||
self.base64Certificate = json["base64Certificate"] as? Bool
|
||||
self.headers = json["headers"] as? Dictionary<String,Any>
|
||||
self.headers = json["headers"] as? [String: Any]
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
struct TextTrack {
|
||||
let type: String
|
||||
let language: String
|
||||
|
@ -1,11 +1,10 @@
|
||||
|
||||
struct VideoSource {
|
||||
let type: String?
|
||||
let uri: String?
|
||||
let isNetwork: Bool
|
||||
let isAsset: Bool
|
||||
let shouldCache: Bool
|
||||
let requestHeaders: Dictionary<String,Any>?
|
||||
let requestHeaders: [String: Any]?
|
||||
let startPosition: Int64?
|
||||
let cropStart: Int64?
|
||||
let cropEnd: Int64?
|
||||
@ -41,7 +40,7 @@ struct VideoSource {
|
||||
self.isNetwork = json["isNetwork"] as? Bool ?? false
|
||||
self.isAsset = json["isAsset"] as? Bool ?? false
|
||||
self.shouldCache = json["shouldCache"] as? Bool ?? false
|
||||
self.requestHeaders = json["requestHeaders"] as? Dictionary<String,Any>
|
||||
self.requestHeaders = json["requestHeaders"] as? [String: Any]
|
||||
self.startPosition = json["startPosition"] as? Int64
|
||||
self.cropStart = json["cropStart"] as? Int64
|
||||
self.cropEnd = json["cropEnd"] as? Int64
|
||||
|
@ -3,7 +3,6 @@ import Foundation
|
||||
import GoogleInteractiveMediaAds
|
||||
|
||||
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate {
|
||||
|
||||
private weak var _video: RCTVideo?
|
||||
private var _pipEnabled: () -> Bool
|
||||
|
||||
@ -38,7 +37,8 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
adTagUrl: adTagUrl!,
|
||||
adDisplayContainer: adDisplayContainer,
|
||||
contentPlayhead: contentPlayhead,
|
||||
userContext: nil)
|
||||
userContext: nil
|
||||
)
|
||||
|
||||
adsLoader.requestAds(with: request)
|
||||
}
|
||||
@ -56,22 +56,21 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
|
||||
// MARK: - IMAAdsLoaderDelegate
|
||||
|
||||
func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
|
||||
func adsLoader(_: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
|
||||
guard let _video = _video else { return }
|
||||
// Grab the instance of the IMAAdsManager and set yourself as the delegate.
|
||||
adsManager = adsLoadedData.adsManager
|
||||
adsManager?.delegate = self
|
||||
|
||||
|
||||
// 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();
|
||||
let adsRenderingSettings = IMAAdsRenderingSettings()
|
||||
adsRenderingSettings.linkOpenerDelegate = self
|
||||
adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController()
|
||||
|
||||
adsManager.initialize(with: adsRenderingSettings)
|
||||
}
|
||||
|
||||
func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
|
||||
func adsLoader(_: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
|
||||
if adErrorData.adError.message != nil {
|
||||
print("Error loading ads: " + adErrorData.adError.message!)
|
||||
}
|
||||
@ -84,12 +83,12 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
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;
|
||||
if _video.isMuted() {
|
||||
adsManager.volume = 0
|
||||
}
|
||||
// Play each ad once it has been loaded
|
||||
if event.type == IMAAdEventType.LOADED {
|
||||
if (_pipEnabled()) {
|
||||
if _pipEnabled() {
|
||||
return
|
||||
}
|
||||
adsManager.start()
|
||||
@ -98,22 +97,22 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
if _video.onReceiveAdEvent != nil {
|
||||
let type = convertEventToString(event: event.type)
|
||||
|
||||
if (event.adData != nil) {
|
||||
if event.adData != nil {
|
||||
_video.onReceiveAdEvent?([
|
||||
"event": type,
|
||||
"data": event.adData ?? [String](),
|
||||
"target": _video.reactTag!
|
||||
]);
|
||||
"target": _video.reactTag!,
|
||||
])
|
||||
} else {
|
||||
_video.onReceiveAdEvent?([
|
||||
"event": type,
|
||||
"target": _video.reactTag!
|
||||
]);
|
||||
"target": _video.reactTag!,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) {
|
||||
func adsManager(_: IMAAdsManager, didReceive error: IMAAdError) {
|
||||
if error.message != nil {
|
||||
print("AdsManager error: " + error.message!)
|
||||
}
|
||||
@ -128,7 +127,7 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
"code": error.code,
|
||||
"type": error.type,
|
||||
],
|
||||
"target": _video.reactTag!
|
||||
"target": _video.reactTag!,
|
||||
])
|
||||
}
|
||||
|
||||
@ -136,13 +135,13 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
_video.setPaused(false)
|
||||
}
|
||||
|
||||
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) {
|
||||
func adsManagerDidRequestContentPause(_: IMAAdsManager) {
|
||||
// Pause the content for the SDK to play ads.
|
||||
_video?.setPaused(true)
|
||||
_video?.setAdPlaying(true)
|
||||
}
|
||||
|
||||
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) {
|
||||
func adsManagerDidRequestContentResume(_: IMAAdsManager) {
|
||||
// Resume the content since the SDK is done playing ads (at least for now).
|
||||
_video?.setAdPlaying(false)
|
||||
_video?.setPaused(false)
|
||||
@ -150,81 +149,61 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, I
|
||||
|
||||
// MARK: - IMALinkOpenerDelegate
|
||||
|
||||
func linkOpenerDidClose(inAppLink linkOpener: NSObject) {
|
||||
func linkOpenerDidClose(inAppLink _: NSObject) {
|
||||
adsManager?.resume()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
func convertEventToString(event: IMAAdEventType!) -> String {
|
||||
var result = "UNKNOWN";
|
||||
var result = "UNKNOWN"
|
||||
|
||||
switch(event) {
|
||||
switch event {
|
||||
case .AD_BREAK_READY:
|
||||
result = "AD_BREAK_READY";
|
||||
break;
|
||||
result = "AD_BREAK_READY"
|
||||
case .AD_BREAK_ENDED:
|
||||
result = "AD_BREAK_ENDED";
|
||||
break;
|
||||
result = "AD_BREAK_ENDED"
|
||||
case .AD_BREAK_STARTED:
|
||||
result = "AD_BREAK_STARTED";
|
||||
break;
|
||||
result = "AD_BREAK_STARTED"
|
||||
case .AD_PERIOD_ENDED:
|
||||
result = "AD_PERIOD_ENDED";
|
||||
break;
|
||||
result = "AD_PERIOD_ENDED"
|
||||
case .AD_PERIOD_STARTED:
|
||||
result = "AD_PERIOD_STARTED";
|
||||
break;
|
||||
result = "AD_PERIOD_STARTED"
|
||||
case .ALL_ADS_COMPLETED:
|
||||
result = "ALL_ADS_COMPLETED";
|
||||
break;
|
||||
result = "ALL_ADS_COMPLETED"
|
||||
case .CLICKED:
|
||||
result = "CLICK";
|
||||
break;
|
||||
result = "CLICK"
|
||||
case .COMPLETE:
|
||||
result = "COMPLETED";
|
||||
break;
|
||||
result = "COMPLETED"
|
||||
case .CUEPOINTS_CHANGED:
|
||||
result = "CUEPOINTS_CHANGED";
|
||||
break;
|
||||
result = "CUEPOINTS_CHANGED"
|
||||
case .FIRST_QUARTILE:
|
||||
result = "FIRST_QUARTILE";
|
||||
break;
|
||||
result = "FIRST_QUARTILE"
|
||||
case .LOADED:
|
||||
result = "LOADED";
|
||||
break;
|
||||
result = "LOADED"
|
||||
case .LOG:
|
||||
result = "LOG";
|
||||
break;
|
||||
result = "LOG"
|
||||
case .MIDPOINT:
|
||||
result = "MIDPOINT";
|
||||
break;
|
||||
result = "MIDPOINT"
|
||||
case .PAUSE:
|
||||
result = "PAUSED";
|
||||
break;
|
||||
result = "PAUSED"
|
||||
case .RESUME:
|
||||
result = "RESUMED";
|
||||
break;
|
||||
result = "RESUMED"
|
||||
case .SKIPPED:
|
||||
result = "SKIPPED";
|
||||
break;
|
||||
result = "SKIPPED"
|
||||
case .STARTED:
|
||||
result = "STARTED";
|
||||
break;
|
||||
result = "STARTED"
|
||||
case .STREAM_LOADED:
|
||||
result = "STREAM_LOADED";
|
||||
break;
|
||||
result = "STREAM_LOADED"
|
||||
case .TAPPED:
|
||||
result = "TAPPED";
|
||||
break;
|
||||
result = "TAPPED"
|
||||
case .THIRD_QUARTILE:
|
||||
result = "THIRD_QUARTILE";
|
||||
break;
|
||||
result = "THIRD_QUARTILE"
|
||||
default:
|
||||
result = "UNKNOWN";
|
||||
result = "UNKNOWN"
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -1,36 +1,38 @@
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import Foundation
|
||||
import MediaAccessibility
|
||||
import React
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate {
|
||||
private var _onPictureInPictureStatusChanged: (() -> Void)? = nil
|
||||
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil
|
||||
private var _restoreUserInterfaceForPIPStopCompletionHandler:((Bool) -> Void)? = nil
|
||||
private var _onPictureInPictureStatusChanged: (() -> Void)?
|
||||
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
|
||||
private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
|
||||
private var _pipController: AVPictureInPictureController?
|
||||
private var _isActive:Bool = false
|
||||
private var _isActive = false
|
||||
|
||||
init(_ onPictureInPictureStatusChanged: (() -> Void)? = nil, _ onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)? = nil) {
|
||||
_onPictureInPictureStatusChanged = onPictureInPictureStatusChanged
|
||||
_onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {
|
||||
guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
|
||||
|
||||
_onPictureInPictureStatusChanged()
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
|
||||
guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return }
|
||||
|
||||
_onPictureInPictureStatusChanged()
|
||||
}
|
||||
|
||||
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
|
||||
func pictureInPictureController(
|
||||
_: AVPictureInPictureController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||
) {
|
||||
guard let _onRestoreUserInterfaceForPictureInPictureStop = _onRestoreUserInterfaceForPictureInPictureStop else { return }
|
||||
|
||||
_onRestoreUserInterfaceForPictureInPictureStop()
|
||||
@ -62,13 +64,13 @@ class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate {
|
||||
guard let _pipController = _pipController else { return }
|
||||
|
||||
if _isActive && !_pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
_pipController.startPictureInPicture()
|
||||
})
|
||||
}
|
||||
} else if !_isActive && _pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
_pipController.stopPictureInPicture()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import AVFoundation
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - RCTPlayerObserverHandlerObjc
|
||||
|
||||
@objc
|
||||
protocol RCTPlayerObserverHandlerObjc {
|
||||
func handleDidFailToFinishPlaying(notification: NSNotification!)
|
||||
@ -10,6 +12,8 @@ protocol RCTPlayerObserverHandlerObjc {
|
||||
func handleAVPlayerAccess(notification: NSNotification!)
|
||||
}
|
||||
|
||||
// MARK: - RCTPlayerObserverHandler
|
||||
|
||||
protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc {
|
||||
func handleTimeUpdate(time: CMTime)
|
||||
func handleReadyForDisplay(changeObject: Any, change: NSKeyValueObservedChange<Bool>)
|
||||
@ -23,6 +27,8 @@ protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc {
|
||||
func handleViewControllerOverlayViewFrameChange(overlayView: UIView, change: NSKeyValueObservedChange<CGRect>)
|
||||
}
|
||||
|
||||
// MARK: - RCTPlayerObserver
|
||||
|
||||
class RCTPlayerObserver: NSObject {
|
||||
weak var _handlers: RCTPlayerObserverHandler?
|
||||
|
||||
@ -38,6 +44,7 @@ class RCTPlayerObserver: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var playerItem: AVPlayerItem? {
|
||||
willSet {
|
||||
removePlayerItemObservers()
|
||||
@ -48,6 +55,7 @@ class RCTPlayerObserver: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var playerViewController: AVPlayerViewController? {
|
||||
willSet {
|
||||
removePlayerViewControllerObservers()
|
||||
@ -58,6 +66,7 @@ class RCTPlayerObserver: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var playerLayer: AVPlayerLayer? {
|
||||
willSet {
|
||||
removePlayerLayerObserver()
|
||||
@ -107,8 +116,16 @@ class RCTPlayerObserver: NSObject {
|
||||
func addPlayerItemObservers() {
|
||||
guard let playerItem = playerItem, let _handlers = _handlers else { return }
|
||||
_playerItemStatusObserver = playerItem.observe(\.status, options: [.new, .old], changeHandler: _handlers.handlePlayerItemStatusChange)
|
||||
_playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty)
|
||||
_playerPlaybackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .old], changeHandler: _handlers.handlePlaybackLikelyToKeepUp)
|
||||
_playerPlaybackBufferEmptyObserver = playerItem.observe(
|
||||
\.isPlaybackBufferEmpty,
|
||||
options: [.new, .old],
|
||||
changeHandler: _handlers.handlePlaybackBufferKeyEmpty
|
||||
)
|
||||
_playerPlaybackLikelyToKeepUpObserver = playerItem.observe(
|
||||
\.isPlaybackLikelyToKeepUp,
|
||||
options: [.new, .old],
|
||||
changeHandler: _handlers.handlePlaybackLikelyToKeepUp
|
||||
)
|
||||
_playerTimedMetadataObserver = playerItem.observe(\.timedMetadata, options: [.new], changeHandler: _handlers.handleTimeMetadataChange)
|
||||
}
|
||||
|
||||
@ -118,12 +135,21 @@ class RCTPlayerObserver: NSObject {
|
||||
_playerPlaybackLikelyToKeepUpObserver?.invalidate()
|
||||
_playerTimedMetadataObserver?.invalidate()
|
||||
}
|
||||
|
||||
func addPlayerViewControllerObservers() {
|
||||
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() {
|
||||
@ -162,7 +188,7 @@ class RCTPlayerObserver: NSObject {
|
||||
}
|
||||
|
||||
func addTimeObserverIfNotSet() {
|
||||
if (_timeObserver == nil) {
|
||||
if _timeObserver == nil {
|
||||
addPlayerTimeObserver()
|
||||
}
|
||||
}
|
||||
@ -171,7 +197,7 @@ class RCTPlayerObserver: NSObject {
|
||||
if let newUpdateInterval = newUpdateInterval {
|
||||
_progressUpdateInterval = newUpdateInterval
|
||||
}
|
||||
if (_timeObserver != nil) {
|
||||
if _timeObserver != nil {
|
||||
addPlayerTimeObserver()
|
||||
}
|
||||
}
|
||||
|
@ -4,49 +4,48 @@ import Promises
|
||||
|
||||
let RCTVideoUnset = -1
|
||||
|
||||
// MARK: - RCTPlayerOperations
|
||||
|
||||
/*!
|
||||
* Collection of mutating functions
|
||||
*/
|
||||
enum RCTPlayerOperations {
|
||||
|
||||
static func setSideloadedText(player: AVPlayer?, textTracks: [TextTrack]?, criteria: SelectedTrackCriteria?) {
|
||||
let type = criteria?.type
|
||||
let textTracks: [TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
|
||||
let trackCount: Int! = player?.currentItem?.tracks.count ?? 0
|
||||
|
||||
// The first few tracks will be audio & video track
|
||||
var firstTextIndex:Int = 0
|
||||
for i in 0..<(trackCount) {
|
||||
if player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
|
||||
var firstTextIndex = 0
|
||||
for i in 0 ..< trackCount where (player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible)) != nil {
|
||||
firstTextIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var selectedTrackIndex: Int = RCTVideoUnset
|
||||
|
||||
if (type == "disabled") {
|
||||
if type == "disabled" {
|
||||
// Select the last text index which is the disabled text track
|
||||
selectedTrackIndex = trackCount - firstTextIndex
|
||||
} else if (type == "language") {
|
||||
} else if type == "language" {
|
||||
let selectedValue = criteria?.value as? String
|
||||
for i in 0 ..< textTracks.count {
|
||||
let currentTextTrack = textTracks[i]
|
||||
if (selectedValue == currentTextTrack.language) {
|
||||
if selectedValue == currentTextTrack.language {
|
||||
selectedTrackIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (type == "title") {
|
||||
} else if type == "title" {
|
||||
let selectedValue = criteria?.value as? String
|
||||
for i in 0 ..< textTracks.count {
|
||||
let currentTextTrack = textTracks[i]
|
||||
if (selectedValue == currentTextTrack.title) {
|
||||
if selectedValue == currentTextTrack.title {
|
||||
selectedTrackIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (type == "index") {
|
||||
} else if type == "index" {
|
||||
if let value = criteria?.value, let index = value as? Int {
|
||||
if textTracks.count > index {
|
||||
selectedTrackIndex = index
|
||||
@ -58,7 +57,7 @@ enum RCTPlayerOperations {
|
||||
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
|
||||
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user)
|
||||
let captionSettings = captioningMediaCharacteristics as? [AnyHashable]
|
||||
if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) {
|
||||
if (captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil {
|
||||
selectedTrackIndex = 0 // If we can't find a match, use the first available track
|
||||
let systemLanguage = NSLocale.preferredLanguages.first
|
||||
for i in 0 ..< textTracks.count {
|
||||
@ -71,7 +70,7 @@ enum RCTPlayerOperations {
|
||||
}
|
||||
}
|
||||
|
||||
for i in firstTextIndex..<(trackCount) {
|
||||
for i in firstTextIndex ..< trackCount {
|
||||
var isEnabled = false
|
||||
if selectedTrackIndex != RCTVideoUnset {
|
||||
isEnabled = i == selectedTrackIndex + firstTextIndex
|
||||
@ -86,26 +85,26 @@ enum RCTPlayerOperations {
|
||||
let group: AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible)
|
||||
var mediaOption: AVMediaSelectionOption!
|
||||
|
||||
if (type == "disabled") {
|
||||
if type == "disabled" {
|
||||
// Do nothing. We want to ensure option is nil
|
||||
} else if (type == "language") || (type == "title") {
|
||||
let value = criteria?.value as? String
|
||||
for i in 0 ..< group.options.count {
|
||||
let currentOption: AVMediaSelectionOption! = group.options[i]
|
||||
var optionValue: String!
|
||||
if (type == "language") {
|
||||
if type == "language" {
|
||||
optionValue = currentOption.extendedLanguageTag
|
||||
} else {
|
||||
optionValue = currentOption.commonMetadata.map(\.value)[0] as! String
|
||||
}
|
||||
if (value == optionValue) {
|
||||
if value == optionValue {
|
||||
mediaOption = currentOption
|
||||
break
|
||||
}
|
||||
}
|
||||
// } else if ([type isEqualToString:@"default"]) {
|
||||
// option = group.defaultOption; */
|
||||
} else if (type == "index") {
|
||||
} else if type == "index" {
|
||||
if let value = criteria?.value, let index = value as? Int {
|
||||
if group.options.count > index {
|
||||
mediaOption = group.options[index]
|
||||
@ -135,19 +134,19 @@ enum RCTPlayerOperations {
|
||||
|
||||
guard group != nil else { return }
|
||||
|
||||
if (type == "disabled") {
|
||||
if type == "disabled" {
|
||||
// Do nothing. We want to ensure option is nil
|
||||
} else if (type == "language") || (type == "title") {
|
||||
let value = criteria?.value as? String
|
||||
for i in 0 ..< group.options.count {
|
||||
let currentOption: AVMediaSelectionOption! = group.options[i]
|
||||
var optionValue: String!
|
||||
if (type == "language") {
|
||||
if type == "language" {
|
||||
optionValue = currentOption.extendedLanguageTag
|
||||
} else {
|
||||
optionValue = currentOption.commonMetadata.map(\.value)[0] as? String
|
||||
}
|
||||
if (value == optionValue) {
|
||||
if value == optionValue {
|
||||
mediaOption = currentOption
|
||||
break
|
||||
}
|
||||
@ -169,11 +168,10 @@ enum RCTPlayerOperations {
|
||||
// If a match isn't found, option will be nil and text tracks will be disabled
|
||||
player?.currentItem?.select(mediaOption, in: group)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float) -> Promise<Bool> {
|
||||
let timeScale:Int = 1000
|
||||
let timeScale = 1000
|
||||
let cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
|
||||
let current: CMTime = playerItem.currentTime()
|
||||
let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
|
||||
@ -193,18 +191,18 @@ enum RCTPlayerOperations {
|
||||
|
||||
static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) {
|
||||
let audioSession: AVAudioSession! = AVAudioSession.sharedInstance()
|
||||
var category:AVAudioSession.Category? = nil
|
||||
var options:AVAudioSession.CategoryOptions? = nil
|
||||
var category: AVAudioSession.Category?
|
||||
var options: AVAudioSession.CategoryOptions?
|
||||
|
||||
if (ignoreSilentSwitch == "ignore") {
|
||||
if ignoreSilentSwitch == "ignore" {
|
||||
category = audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback
|
||||
} else if (ignoreSilentSwitch == "obey") {
|
||||
} else if ignoreSilentSwitch == "obey" {
|
||||
category = AVAudioSession.Category.ambient
|
||||
}
|
||||
|
||||
if (mixWithOthers == "mix") {
|
||||
if mixWithOthers == "mix" {
|
||||
options = .mixWithOthers
|
||||
} else if (mixWithOthers == "duck") {
|
||||
} else if mixWithOthers == "duck" {
|
||||
options = .duckOthers
|
||||
}
|
||||
|
||||
@ -221,7 +219,10 @@ enum RCTPlayerOperations {
|
||||
if #available(iOS 16.0, *) {
|
||||
do {
|
||||
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category to playAndRecord with defaultToSpeaker options.")
|
||||
try audioSession.setCategory(audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.defaultToSpeaker)
|
||||
try audioSession.setCategory(
|
||||
audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback,
|
||||
options: AVAudioSession.CategoryOptions.defaultToSpeaker
|
||||
)
|
||||
} catch {
|
||||
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).")
|
||||
}
|
||||
|
@ -2,17 +2,15 @@ import AVFoundation
|
||||
import Promises
|
||||
|
||||
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
|
||||
|
||||
private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:]
|
||||
private var _requestingCertificate:Bool = false
|
||||
private var _requestingCertificateErrored:Bool = false
|
||||
private var _requestingCertificate = false
|
||||
private var _requestingCertificateErrored = false
|
||||
private var _drm: DRMParams?
|
||||
private var _localSourceEncryptionKeyScheme: String?
|
||||
private var _reactTag: NSNumber?
|
||||
private var _onVideoError: RCTDirectEventBlock?
|
||||
private var _onGetLicense: RCTDirectEventBlock?
|
||||
|
||||
|
||||
init(
|
||||
asset: AVURLAsset,
|
||||
drm: DRMParams?,
|
||||
@ -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)
|
||||
}
|
||||
|
||||
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest:AVAssetResourceLoadingRequest) -> Bool {
|
||||
func resourceLoader(_: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
|
||||
return loadingRequestHandling(loadingRequest)
|
||||
}
|
||||
|
||||
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) {
|
||||
func resourceLoader(_: AVAssetResourceLoader, didCancel _: AVAssetResourceLoadingRequest) {
|
||||
RCTLog("didCancelLoadingRequest")
|
||||
}
|
||||
|
||||
func setLicenseResult(_ license: String!, _ licenseUrl: String!) {
|
||||
|
||||
// Check if the loading request exists in _loadingRequests based on licenseUrl
|
||||
guard let loadingRequest = _loadingRequests[licenseUrl] else {
|
||||
setLicenseResultError("Loading request for licenseUrl \(licenseUrl) not found", licenseUrl)
|
||||
@ -94,15 +91,14 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
||||
"localizedDescription": error.localizedDescription ?? "",
|
||||
"localizedFailureReason": error.localizedFailureReason ?? "",
|
||||
"localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "",
|
||||
"domain": error.domain
|
||||
"domain": error.domain,
|
||||
],
|
||||
"target": _reactTag
|
||||
"target": _reactTag,
|
||||
])
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func loadingRequestHandling(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool {
|
||||
if handleEmbeddedKey(loadingRequest) {
|
||||
return true
|
||||
@ -155,7 +151,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
||||
contentId: contentId,
|
||||
certificateUrl: _drm.certificateUrl,
|
||||
base64Certificate: _drm.base64Certificate
|
||||
) .then{ spcData -> Void in
|
||||
).then { spcData in
|
||||
self._requestingCertificate = true
|
||||
self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? loadingRequest.request.url?.absoluteString ?? "",
|
||||
"contentId": contentId ?? "",
|
||||
@ -170,7 +166,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
||||
certificateUrl: _drm.certificateUrl,
|
||||
base64Certificate: _drm.base64Certificate,
|
||||
headers: _drm.headers
|
||||
) .then{ data -> Void in
|
||||
).then { data in
|
||||
guard let dataRequest = loadingRequest.dataRequest else {
|
||||
throw RCTVideoErrorHandler.noCertificateData
|
||||
}
|
||||
@ -179,7 +175,6 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
promise.catch { error in
|
||||
self.finishLoadingWithError(error: error, licenseUrl: requestKey)
|
||||
self._requestingCertificateErrored = true
|
||||
|
@ -1,9 +1,7 @@
|
||||
import AVFoundation
|
||||
import Promises
|
||||
|
||||
struct RCTVideoDRM {
|
||||
@available(*, unavailable) private init() {}
|
||||
|
||||
enum RCTVideoDRM {
|
||||
static func fetchLicense(
|
||||
licenseServer: String,
|
||||
spcData: Data?,
|
||||
@ -13,8 +11,9 @@ struct RCTVideoDRM {
|
||||
let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers)
|
||||
|
||||
return Promise<Data>(on: .global()) { fulfill, reject in
|
||||
let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ (data:Data!,response:URLResponse!,error:Error!) in
|
||||
|
||||
let postDataTask = URLSession.shared.dataTask(
|
||||
with: request as URLRequest,
|
||||
completionHandler: { (data: Data!, response: URLResponse!, error: Error!) in
|
||||
let httpResponse: HTTPURLResponse! = (response as! HTTPURLResponse)
|
||||
|
||||
guard error == nil else {
|
||||
@ -34,7 +33,8 @@ struct RCTVideoDRM {
|
||||
}
|
||||
|
||||
fulfill(decodedData)
|
||||
})
|
||||
}
|
||||
)
|
||||
postDataTask.resume()
|
||||
}
|
||||
}
|
||||
@ -58,7 +58,13 @@ struct RCTVideoDRM {
|
||||
}
|
||||
|
||||
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(
|
||||
kCFAllocatorDefault,
|
||||
spcEncoded as? CFString? as! CFString,
|
||||
nil,
|
||||
"?=&+" as CFString,
|
||||
CFStringBuiltInEncodings.UTF8.rawValue
|
||||
) as? String
|
||||
let post = String(format: "spc=%@&%@", spcUrlEncoded as! CVarArg, contentId)
|
||||
let postData = post.data(using: String.Encoding.utf8, allowLossyConversion: true)
|
||||
request.httpBody = postData
|
||||
@ -95,7 +101,6 @@ struct RCTVideoDRM {
|
||||
|
||||
static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) -> Promise<Data> {
|
||||
return Promise<Data>(on: .global()) { fulfill, reject in
|
||||
|
||||
guard let certificateStringUrl = certificateStringUrl,
|
||||
let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else {
|
||||
reject(RCTVideoErrorHandler.noCertificateURL)
|
||||
@ -105,7 +110,7 @@ struct RCTVideoDRM {
|
||||
var certificateData: Data?
|
||||
do {
|
||||
certificateData = try Data(contentsOf: certificateURL)
|
||||
if (base64Certificate != nil) {
|
||||
if base64Certificate != nil {
|
||||
certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters)
|
||||
}
|
||||
} catch {}
|
||||
@ -119,7 +124,8 @@ 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)
|
||||
|
||||
return RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate)
|
||||
@ -136,7 +142,14 @@ struct RCTVideoDRM {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
guard let contentId = contentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with: "") else {
|
||||
|
@ -1,3 +1,5 @@
|
||||
// MARK: - RCTVideoError
|
||||
|
||||
enum RCTVideoError: Int {
|
||||
case fromJSPart
|
||||
case noLicenseServerURL
|
||||
@ -12,16 +14,18 @@ enum RCTVideoError : Int {
|
||||
case invalidContentId
|
||||
}
|
||||
|
||||
enum RCTVideoErrorHandler {
|
||||
// MARK: - RCTVideoErrorHandler
|
||||
|
||||
enum RCTVideoErrorHandler {
|
||||
static let noDRMData = NSError(
|
||||
domain: "RCTVideo",
|
||||
code: RCTVideoError.noDRMData.rawValue,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No drm object found.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?"
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noCertificateURL = NSError(
|
||||
domain: "RCTVideo",
|
||||
@ -29,8 +33,9 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM License.",
|
||||
NSLocalizedFailureReasonErrorKey: "No certificate URL has been found.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?"
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noCertificateData = NSError(
|
||||
domain: "RCTVideo",
|
||||
@ -38,8 +43,9 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No certificate data obtained from the specificied url.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?"
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noSPC = NSError(
|
||||
domain: "RCTVideo",
|
||||
@ -47,8 +53,9 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No spc received.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config."
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config.",
|
||||
]
|
||||
)
|
||||
|
||||
static let noLicenseServerURL = NSError(
|
||||
domain: "RCTVideo",
|
||||
@ -56,8 +63,9 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM License.",
|
||||
NSLocalizedFailureReasonErrorKey: "No license server URL has been found.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop licenseServer?"
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop licenseServer?",
|
||||
]
|
||||
)
|
||||
|
||||
static let noDataFromLicenseRequest = NSError(
|
||||
domain: "RCTVideo",
|
||||
@ -65,8 +73,9 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No data received from the license server.",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?"
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?",
|
||||
]
|
||||
)
|
||||
|
||||
static func licenseRequestNotOk(_ statusCode: Int) -> NSError {
|
||||
return NSError(
|
||||
@ -76,10 +85,11 @@ enum RCTVideoErrorHandler {
|
||||
NSLocalizedDescriptionKey: "Error obtaining license.",
|
||||
NSLocalizedFailureReasonErrorKey: String(
|
||||
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 {
|
||||
@ -88,7 +98,7 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: error,
|
||||
NSLocalizedFailureReasonErrorKey: error,
|
||||
NSLocalizedRecoverySuggestionErrorKey: error
|
||||
NSLocalizedRecoverySuggestionErrorKey: error,
|
||||
])
|
||||
}
|
||||
|
||||
@ -98,6 +108,7 @@ enum RCTVideoErrorHandler {
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Error obtaining DRM license.",
|
||||
NSLocalizedFailureReasonErrorKey: "No valide content Id received",
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?"
|
||||
])
|
||||
NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import AVFoundation
|
||||
|
||||
enum RCTVideoSave {
|
||||
|
||||
static func save(
|
||||
options:NSDictionary!,
|
||||
options _: NSDictionary!,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock,
|
||||
|
||||
@ -20,33 +19,29 @@ enum RCTVideoSave {
|
||||
reject("ERROR_COULD_NOT_CREATE_EXPORT_SESSION", "Could not create export session", nil)
|
||||
return
|
||||
}
|
||||
var path:String! = nil
|
||||
var path: String!
|
||||
path = RCTVideoSave.generatePathInDirectory(
|
||||
directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path,
|
||||
withExtension: ".mp4")
|
||||
withExtension: ".mp4"
|
||||
)
|
||||
let url: NSURL! = NSURL.fileURL(withPath: path) as NSURL
|
||||
exportSession.outputFileType = AVFileType.mp4
|
||||
exportSession.outputURL = url as URL?
|
||||
exportSession.videoComposition = playerItem?.videoComposition
|
||||
exportSession.shouldOptimizeForNetworkUse = true
|
||||
exportSession.exportAsynchronously(completionHandler: {
|
||||
|
||||
switch (exportSession.status) {
|
||||
switch exportSession.status {
|
||||
case .failed:
|
||||
reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error)
|
||||
break
|
||||
case .cancelled:
|
||||
reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error)
|
||||
break
|
||||
default:
|
||||
resolve(["uri": url.absoluteString])
|
||||
break
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
static func generatePathInDirectory(directory: String?, withExtension `extension`: String?) -> String? {
|
||||
static func generatePathInDirectory(directory: String?, withExtension extension: String?) -> String? {
|
||||
let fileName = UUID().uuidString + (`extension` ?? "")
|
||||
RCTVideoSave.ensureDirExists(withPath: directory)
|
||||
return URL(fileURLWithPath: directory ?? "").appendingPathComponent(fileName).path
|
||||
@ -64,8 +59,7 @@ enum RCTVideoSave {
|
||||
if !(exists && isDir.boolValue) {
|
||||
do {
|
||||
try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
if error != nil {
|
||||
return false
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
/*!
|
||||
* Collection of helper functions for tvOS specific features
|
||||
@ -37,8 +37,7 @@ enum RCTVideoTVUtils {
|
||||
let uri = URL(string: imgUri),
|
||||
let imgData = try? Data(contentsOf: uri),
|
||||
let image = UIImage(data: imgData),
|
||||
let pngData = image.pngData()
|
||||
{
|
||||
let pngData = image.pngData() {
|
||||
let imageItem = RCTVideoUtils.createMetadataItem(for: .commonIdentifierArtwork, value: pngData)
|
||||
metadata.append(imageItem)
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import AVFoundation
|
||||
import Promises
|
||||
import Photos
|
||||
import Promises
|
||||
|
||||
/*!
|
||||
* Collection of pure functions
|
||||
*/
|
||||
enum RCTVideoUtils {
|
||||
|
||||
/*!
|
||||
* Calculates and returns the playable duration of the current player item using its loaded time ranges.
|
||||
*
|
||||
@ -19,12 +18,12 @@ enum RCTVideoUtils {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (source?.cropStart != nil && source?.cropEnd != nil) {
|
||||
if source?.cropStart != nil && source?.cropEnd != nil {
|
||||
return NSNumber(value: (Float64(source?.cropEnd ?? 0) - Float64(source?.cropStart ?? 0)) / 1000)
|
||||
}
|
||||
|
||||
var effectiveTimeRange: CMTimeRange?
|
||||
for (_, value) in video.loadedTimeRanges.enumerated() {
|
||||
for value in video.loadedTimeRanges {
|
||||
let timeRange: CMTimeRange = value.timeRangeValue
|
||||
if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) {
|
||||
effectiveTimeRange = timeRange
|
||||
@ -35,8 +34,8 @@ enum RCTVideoUtils {
|
||||
if let effectiveTimeRange = effectiveTimeRange {
|
||||
let playableDuration: Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange))
|
||||
if playableDuration > 0 {
|
||||
if (source?.cropStart != nil) {
|
||||
return NSNumber(value: (playableDuration - Float64(source?.cropStart ?? 0) / 1000))
|
||||
if source?.cropStart != nil {
|
||||
return NSNumber(value: playableDuration - Float64(source?.cropStart ?? 0) / 1000)
|
||||
}
|
||||
|
||||
return playableDuration as NSNumber
|
||||
@ -55,7 +54,7 @@ enum RCTVideoUtils {
|
||||
let paths: [String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true)
|
||||
var relativeFilePath: String! = filepath.lastPathComponent
|
||||
// the file may be multiple levels below the documents directory
|
||||
let directoryString:String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents";
|
||||
let directoryString: String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents"
|
||||
let fileComponents: [String]! = filepath.components(separatedBy: directoryString)
|
||||
if fileComponents.count > 1 {
|
||||
relativeFilePath = fileComponents[1]
|
||||
@ -75,22 +74,21 @@ enum RCTVideoUtils {
|
||||
return firstItem.timeRangeValue
|
||||
}
|
||||
|
||||
return (CMTimeRange.zero)
|
||||
return CMTimeRange.zero
|
||||
}
|
||||
|
||||
static func playerItemDuration(_ player: AVPlayer?) -> CMTime {
|
||||
if let playerItem = player?.currentItem,
|
||||
playerItem.status == .readyToPlay {
|
||||
return(playerItem.duration)
|
||||
return playerItem.duration
|
||||
}
|
||||
|
||||
return(CMTime.invalid)
|
||||
return CMTime.invalid
|
||||
}
|
||||
|
||||
static func calculateSeekableDuration(_ player: AVPlayer?) -> NSNumber {
|
||||
let timeRange: CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player)
|
||||
if CMTIME_IS_NUMERIC(timeRange.duration)
|
||||
{
|
||||
if CMTIME_IS_NUMERIC(timeRange.duration) {
|
||||
return NSNumber(value: CMTimeGetSeconds(timeRange.duration))
|
||||
}
|
||||
return 0
|
||||
@ -118,7 +116,7 @@ enum RCTVideoUtils {
|
||||
"index": NSNumber(value: i),
|
||||
"title": title,
|
||||
"language": language ?? "",
|
||||
"selected": currentOption?.displayName == selectedOption?.displayName
|
||||
"selected": currentOption?.displayName == selectedOption?.displayName,
|
||||
] as [String: Any]
|
||||
audioTracks.add(audioTrack)
|
||||
}
|
||||
@ -147,7 +145,7 @@ enum RCTVideoUtils {
|
||||
"index": NSNumber(value: i),
|
||||
"title": title,
|
||||
"language": language,
|
||||
"selected": currentOption?.displayName == selectedOption?.displayName
|
||||
"selected": currentOption?.displayName == selectedOption?.displayName,
|
||||
])
|
||||
textTracks.append(textTrack)
|
||||
}
|
||||
@ -181,7 +179,7 @@ enum RCTVideoUtils {
|
||||
}
|
||||
|
||||
static func generateMixComposition(_ asset: AVAsset) -> AVMutableComposition {
|
||||
let mixComposition:AVMutableComposition = AVMutableComposition()
|
||||
let mixComposition = AVMutableComposition()
|
||||
|
||||
let videoAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
||||
|
||||
@ -190,18 +188,26 @@ enum RCTVideoUtils {
|
||||
return mixComposition
|
||||
}
|
||||
|
||||
let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid)
|
||||
let videoCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(
|
||||
withMediaType: AVMediaType.video,
|
||||
preferredTrackID: kCMPersistentTrackID_Invalid
|
||||
)
|
||||
try? videoCompTrack.insertTimeRange(
|
||||
CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration),
|
||||
of: videoAsset,
|
||||
at: .zero)
|
||||
at: .zero
|
||||
)
|
||||
|
||||
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(
|
||||
CMTimeRangeMake(start: .zero, duration: audioAsset.timeRange.duration),
|
||||
of: audioAsset,
|
||||
at: .zero)
|
||||
at: .zero
|
||||
)
|
||||
|
||||
return mixComposition
|
||||
}
|
||||
@ -210,7 +216,7 @@ enum RCTVideoUtils {
|
||||
let videoAsset: AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
|
||||
var validTextTracks: [TextTrack] = []
|
||||
|
||||
if let textTracks = textTracks, textTracks.count > 0 {
|
||||
if let textTracks = textTracks, !textTracks.isEmpty {
|
||||
for i in 0 ..< textTracks.count {
|
||||
var textURLAsset: AVURLAsset!
|
||||
let textUri: String = textTracks[i].uri
|
||||
@ -218,11 +224,11 @@ enum RCTVideoUtils {
|
||||
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options: (assetOptions as! [String: Any]))
|
||||
} else {
|
||||
let isDisabledTrack: Bool! = textTracks[i].type == "disabled"
|
||||
let searchPath:FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory;
|
||||
let searchPath: FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory
|
||||
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options: nil)
|
||||
}
|
||||
let textTrackAsset: AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
|
||||
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset
|
||||
if textTrackAsset == nil { continue } // fix when there's no textTrackAsset
|
||||
validTextTracks.append(textTracks[i])
|
||||
let textCompTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.text,
|
||||
preferredTrackID: kCMPersistentTrackID_Invalid)
|
||||
@ -230,13 +236,14 @@ enum RCTVideoUtils {
|
||||
try? textCompTrack.insertTimeRange(
|
||||
CMTimeRangeMake(start: .zero, duration: videoAsset!.timeRange.duration),
|
||||
of: textTrackAsset,
|
||||
at: .zero)
|
||||
at: .zero
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let emptyVttFile: TextTrack? = self.createEmptyVttFile()
|
||||
if (emptyVttFile != nil) {
|
||||
if emptyVttFile != nil {
|
||||
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.
|
||||
* 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> {
|
||||
return Promise<Void>(on: .global()) { fulfill, reject in
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: {
|
||||
return Promise<Void>(on: .global()) { fulfill, _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC)) {
|
||||
fulfill(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,7 +312,7 @@ enum RCTVideoUtils {
|
||||
let assetOptions: NSMutableDictionary! = NSMutableDictionary()
|
||||
|
||||
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)
|
||||
}
|
||||
let cookies: [AnyObject]! = HTTPCookieStorage.shared.cookies
|
||||
|
@ -1,8 +1,7 @@
|
||||
#import <React/RCTViewManager.h>
|
||||
#import "RCTVideoSwiftLog.h"
|
||||
#import "RCTEventDispatcher.h"
|
||||
#import "RCTVideoSwiftLog.h"
|
||||
#import <React/RCTViewManager.h>
|
||||
|
||||
#if __has_include(<react-native-video/RCTVideoCache.h>)
|
||||
#import "RCTVideoCache.h"
|
||||
#endif
|
||||
|
||||
|
@ -4,15 +4,16 @@ import Foundation
|
||||
#if USE_GOOGLE_IMA
|
||||
import GoogleInteractiveMediaAds
|
||||
#endif
|
||||
import React
|
||||
import Promises
|
||||
import React
|
||||
|
||||
// MARK: - RCTVideo
|
||||
|
||||
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler {
|
||||
|
||||
private var _player: AVPlayer?
|
||||
private var _playerItem: AVPlayerItem?
|
||||
private var _source: VideoSource?
|
||||
private var _playerBufferEmpty:Bool = true
|
||||
private var _playerBufferEmpty = true
|
||||
private var _playerLayer: AVPlayerLayer?
|
||||
private var _chapters: [Chapter]?
|
||||
|
||||
@ -26,14 +27,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
/* Required to publish events */
|
||||
private var _eventDispatcher: RCTEventDispatcher?
|
||||
private var _videoLoadStarted:Bool = false
|
||||
private var _videoLoadStarted = false
|
||||
|
||||
private var _pendingSeek:Bool = false
|
||||
private var _pendingSeek = false
|
||||
private var _pendingSeekTime: Float = 0.0
|
||||
private var _lastSeekTime: Float = 0.0
|
||||
|
||||
/* For sending videoProgress events */
|
||||
private var _controls:Bool = false
|
||||
private var _controls = false
|
||||
|
||||
/* Keep track of any modifiers, need to be applied after each play */
|
||||
private var _audioOutput: String = "speaker"
|
||||
@ -41,29 +42,29 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _rate: Float = 1.0
|
||||
private var _maxBitRate: Float?
|
||||
|
||||
private var _automaticallyWaitsToMinimizeStalling:Bool = true
|
||||
private var _muted:Bool = false
|
||||
private var _paused:Bool = false
|
||||
private var _repeat:Bool = false
|
||||
private var _allowsExternalPlayback:Bool = true
|
||||
private var _automaticallyWaitsToMinimizeStalling = true
|
||||
private var _muted = false
|
||||
private var _paused = false
|
||||
private var _repeat = false
|
||||
private var _allowsExternalPlayback = true
|
||||
private var _textTracks: [TextTrack]?
|
||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria?
|
||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria?
|
||||
private var _playbackStalled:Bool = false
|
||||
private var _playInBackground:Bool = false
|
||||
private var _preventsDisplaySleepDuringVideoPlayback:Bool = true
|
||||
private var _playbackStalled = false
|
||||
private var _playInBackground = false
|
||||
private var _preventsDisplaySleepDuringVideoPlayback = true
|
||||
private var _preferredForwardBufferDuration: Float = 0.0
|
||||
private var _playWhenInactive:Bool = false
|
||||
private var _playWhenInactive = false
|
||||
private var _ignoreSilentSwitch: String! = "inherit" // inherit, ignore, obey
|
||||
private var _mixWithOthers: String! = "inherit" // inherit, mix, duck
|
||||
private var _resizeMode: String! = "cover"
|
||||
private var _fullscreen:Bool = false
|
||||
private var _fullscreenAutorotate:Bool = true
|
||||
private var _fullscreen = false
|
||||
private var _fullscreenAutorotate = true
|
||||
private var _fullscreenOrientation: String! = "all"
|
||||
private var _fullscreenPlayerPresented:Bool = false
|
||||
private var _fullscreenUncontrolPlayerPresented:Bool = false // to call events switching full screen mode from player controls
|
||||
private var _fullscreenPlayerPresented = false
|
||||
private var _fullscreenUncontrolPlayerPresented = false // to call events switching full screen mode from player controls
|
||||
private var _filterName: String!
|
||||
private var _filterEnabled:Bool = false
|
||||
private var _filterEnabled = false
|
||||
private var _presentingViewController: UIViewController?
|
||||
private var _pictureInPictureEnabled = false
|
||||
private var _startPosition: Float64 = -1
|
||||
@ -75,18 +76,18 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
/* Playhead used by the SDK to track content video progress and insert mid-rolls. */
|
||||
private var _contentPlayhead: IMAAVPlayerContentPlayhead?
|
||||
#endif
|
||||
private var _didRequestAds:Bool = false
|
||||
private var _adPlaying:Bool = false
|
||||
private var _didRequestAds = false
|
||||
private var _adPlaying = false
|
||||
|
||||
private var _resouceLoaderDelegate: RCTResourceLoaderDelegate?
|
||||
private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver()
|
||||
private var _playerObserver: RCTPlayerObserver = .init()
|
||||
|
||||
#if USE_VIDEO_CACHING
|
||||
private let _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler()
|
||||
private let _videoCache: RCTVideoCachingHandler = .init()
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private var _pip:RCTPictureInPicture? = nil
|
||||
private var _pip: RCTPictureInPicture?
|
||||
#endif
|
||||
|
||||
// Events
|
||||
@ -116,11 +117,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
@objc var onGetLicense: RCTDirectEventBlock?
|
||||
@objc var onReceiveAdEvent: RCTDirectEventBlock?
|
||||
|
||||
@objc func _onPictureInPictureStatusChanged() {
|
||||
@objc
|
||||
func _onPictureInPictureStatusChanged() {
|
||||
onPictureInPictureStatusChanged?(["isActive": NSNumber(value: true)])
|
||||
}
|
||||
|
||||
@objc func _onRestoreUserInterfaceForPictureInPictureStop() {
|
||||
@objc
|
||||
func _onRestoreUserInterfaceForPictureInPictureStop() {
|
||||
onPictureInPictureStatusChanged?(["isActive": NSNumber(value: false)])
|
||||
}
|
||||
|
||||
@ -195,14 +198,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
// MARK: - App lifecycle handlers
|
||||
|
||||
@objc func applicationWillResignActive(notification:NSNotification!) {
|
||||
@objc
|
||||
func applicationWillResignActive(notification _: NSNotification!) {
|
||||
if _playInBackground || _playWhenInactive || _paused { return }
|
||||
|
||||
_player?.pause()
|
||||
_player?.rate = 0.0
|
||||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(notification: NSNotification!) {
|
||||
@objc
|
||||
func applicationDidBecomeActive(notification _: NSNotification!) {
|
||||
if _playInBackground || _playWhenInactive || _paused { return }
|
||||
|
||||
// Resume the player or any other tasks that should continue when the app becomes active.
|
||||
@ -210,7 +215,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_player?.rate = _rate
|
||||
}
|
||||
|
||||
@objc func applicationDidEnterBackground(notification:NSNotification!) {
|
||||
@objc
|
||||
func applicationDidEnterBackground(notification _: NSNotification!) {
|
||||
if !_playInBackground {
|
||||
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
|
||||
_playerLayer?.player = nil
|
||||
@ -218,7 +224,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc func applicationWillEnterForeground(notification:NSNotification!) {
|
||||
@objc
|
||||
func applicationWillEnterForeground(notification _: NSNotification!) {
|
||||
self.applyModifiers()
|
||||
if !_playInBackground {
|
||||
_playerLayer?.player = _player
|
||||
@ -228,7 +235,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
// MARK: - Audio events
|
||||
|
||||
@objc func audioRouteChanged(notification:NSNotification!) {
|
||||
@objc
|
||||
func audioRouteChanged(notification: NSNotification!) {
|
||||
if let userInfo = notification.userInfo {
|
||||
let reason: AVAudioSession.RouteChangeReason! = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason
|
||||
// let previousRoute:NSNumber! = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? NSNumber
|
||||
@ -252,7 +260,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
var currentTime = _player?.currentTime()
|
||||
if (currentTime != nil && _source?.cropStart != nil) {
|
||||
if currentTime != nil && _source?.cropStart != nil {
|
||||
currentTime = CMTimeSubtract(currentTime!, CMTimeMake(value: _source?.cropStart ?? 0, timescale: 1000))
|
||||
}
|
||||
let currentPlaybackTime = _player?.currentItem?.currentDate()
|
||||
@ -260,7 +268,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero)
|
||||
|
||||
NotificationCenter.default.post(name: NSNotification.Name("RCTVideo_progress"), object: nil, userInfo: [
|
||||
"progress": NSNumber(value: currentTimeSecs / duration)
|
||||
"progress": NSNumber(value: currentTimeSecs / duration),
|
||||
])
|
||||
|
||||
if currentTimeSecs >= 0 {
|
||||
@ -276,19 +284,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"atValue": NSNumber(value: currentTime?.value ?? .zero),
|
||||
"currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value),
|
||||
"target": reactTag,
|
||||
"seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player)
|
||||
"seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Player and source
|
||||
|
||||
@objc
|
||||
func setSrc(_ source: NSDictionary!) {
|
||||
let dispatchClosure = {
|
||||
self._source = VideoSource(source)
|
||||
if (self._source?.uri == nil || self._source?.uri == "") {
|
||||
if self._source?.uri == nil || self._source?.uri == "" {
|
||||
self._player?.replaceCurrentItem(with: nil)
|
||||
return;
|
||||
return
|
||||
}
|
||||
self.removePlayerLayer()
|
||||
self._playerObserver.player = nil
|
||||
@ -375,10 +384,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"src": [
|
||||
"uri": self._source?.uri ?? NSNull(),
|
||||
"type": self._source?.type ?? NSNull(),
|
||||
"isNetwork": NSNumber(value: self._source?.isNetwork ?? false)
|
||||
"isNetwork": NSNumber(value: self._source?.isNetwork ?? false),
|
||||
],
|
||||
"drm": self._drm?.json ?? NSNull(),
|
||||
"target": self.reactTag
|
||||
"target": self.reactTag,
|
||||
])
|
||||
}.catch { _ in }
|
||||
self._videoLoadStarted = true
|
||||
@ -397,7 +406,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) -> AVPlayerItem {
|
||||
if (_textTracks == nil) || _textTracks?.count==0 || (uri.hasSuffix(".m3u8")) {
|
||||
if (_textTracks == nil) || _textTracks?.isEmpty == true || (uri.hasSuffix(".m3u8")) {
|
||||
return self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
|
||||
}
|
||||
|
||||
@ -408,7 +417,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
asset: asset,
|
||||
assetOptions: assetOptions,
|
||||
mixComposition: mixComposition,
|
||||
textTracks:_textTracks)
|
||||
textTracks: _textTracks
|
||||
)
|
||||
if validTextTracks.count != _textTracks?.count {
|
||||
setTextTracks(validTextTracks)
|
||||
}
|
||||
@ -458,16 +468,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
switch mode {
|
||||
case "contain":
|
||||
resizeMode = .resizeAspect
|
||||
break
|
||||
case "none":
|
||||
resizeMode = .resizeAspect
|
||||
break
|
||||
case "cover":
|
||||
resizeMode = .resizeAspectFill
|
||||
break
|
||||
case "stretch":
|
||||
resizeMode = .resize
|
||||
break
|
||||
default:
|
||||
resizeMode = .resizeAspect
|
||||
}
|
||||
@ -510,9 +516,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
do {
|
||||
try audioSession.setCategory(.playback)
|
||||
try audioSession.setActive(true, options: [])
|
||||
} catch {
|
||||
}
|
||||
if (pictureInPicture) {
|
||||
} catch {}
|
||||
if pictureInPicture {
|
||||
_pictureInPictureEnabled = true
|
||||
} else {
|
||||
_pictureInPictureEnabled = false
|
||||
@ -590,8 +595,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
playerItem: item,
|
||||
paused: wasPaused,
|
||||
seekTime: seekTime.floatValue,
|
||||
seekTolerance:seekTolerance.floatValue)
|
||||
.then{ [weak self] (finished:Bool) in
|
||||
seekTolerance: seekTolerance.floatValue
|
||||
)
|
||||
.then { [weak self] (_: Bool) in
|
||||
guard let self = self else { return }
|
||||
|
||||
self._playerObserver.addTimeObserverIfNotSet()
|
||||
@ -606,7 +612,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_pendingSeek = false
|
||||
}
|
||||
|
||||
|
||||
@objc
|
||||
func setRate(_ rate: Float) {
|
||||
_rate = rate
|
||||
@ -674,13 +679,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func setPlaybackRange(_ item: AVPlayerItem!, withVideoStart videoStart: Int64?, withVideoEnd videoEnd: Int64?) {
|
||||
if (videoStart != nil) {
|
||||
if videoStart != nil {
|
||||
let start = CMTimeMake(value: videoStart!, timescale: 1000)
|
||||
item.reversePlaybackEndTime = start
|
||||
_pendingSeekTime = Float(CMTimeGetSeconds(start))
|
||||
_pendingSeek = true
|
||||
}
|
||||
if (videoEnd != nil) {
|
||||
if videoEnd != nil {
|
||||
item.forwardPlaybackEndTime = CMTimeMake(value: videoEnd!, timescale: 1000)
|
||||
}
|
||||
}
|
||||
@ -721,7 +726,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
@objc
|
||||
func setRepeat(_ `repeat`: Bool) {
|
||||
func setRepeat(_ repeat: Bool) {
|
||||
_repeat = `repeat`
|
||||
}
|
||||
|
||||
@ -743,7 +748,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func setSelectedTextTrack(_ selectedTextTrack: SelectedTrackCriteria?) {
|
||||
_selectedTextTrackCriteria = selectedTextTrack
|
||||
if (_textTracks != nil) { // sideloaded text tracks
|
||||
if _textTracks != nil { // sideloaded text tracks
|
||||
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks, criteria: _selectedTextTrackCriteria)
|
||||
} else { // text tracks included in the HLS playlist
|
||||
RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible,
|
||||
@ -760,7 +765,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_textTracks = textTracks
|
||||
|
||||
// in case textTracks was set after selectedTextTrack
|
||||
if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)}
|
||||
if _selectedTextTrackCriteria != nil { setSelectedTextTrack(_selectedTextTrackCriteria) }
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -786,11 +791,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
// Find the nearest view controller
|
||||
var viewController: UIViewController! = self.firstAvailableUIViewController()
|
||||
if (viewController == nil) {
|
||||
if viewController == nil {
|
||||
let keyWindow: UIWindow! = UIApplication.shared.keyWindow
|
||||
viewController = keyWindow.rootViewController
|
||||
if viewController.children.count > 0
|
||||
{
|
||||
if !viewController.children.isEmpty {
|
||||
viewController = viewController.children.last
|
||||
}
|
||||
}
|
||||
@ -800,7 +804,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any])
|
||||
|
||||
if let playerViewController = _playerViewController {
|
||||
if(_controls) {
|
||||
if _controls {
|
||||
// prevents crash https://github.com/react-native-video/react-native-video/issues/3040
|
||||
self._playerViewController?.removeFromParent()
|
||||
}
|
||||
@ -813,7 +817,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self._playerViewController?.autorotate = self._fullscreenAutorotate
|
||||
|
||||
self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag])
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -862,7 +865,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_playerObserver.playerViewController = _playerViewController
|
||||
}
|
||||
|
||||
func createPlayerViewController(player:AVPlayer, withPlayerItem playerItem:AVPlayerItem) -> RCTVideoPlayerViewController {
|
||||
func createPlayerViewController(player: AVPlayer, withPlayerItem _: AVPlayerItem) -> RCTVideoPlayerViewController {
|
||||
let viewController = RCTVideoPlayerViewController()
|
||||
viewController.showsPlaybackControls = self._controls
|
||||
viewController.rctDelegate = self
|
||||
@ -899,16 +902,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func setControls(_ controls: Bool) {
|
||||
if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil))
|
||||
{
|
||||
if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) {
|
||||
_controls = controls
|
||||
if _controls
|
||||
{
|
||||
if _controls {
|
||||
self.removePlayerLayer()
|
||||
self.usePlayerViewController()
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
_playerViewController?.view.removeFromSuperview()
|
||||
_playerViewController?.removeFromParent()
|
||||
_playerViewController = nil
|
||||
@ -932,7 +931,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
// MARK: - RCTVideoPlayerViewControllerDelegate
|
||||
|
||||
func videoPlayerViewControllerWillDismiss(playerViewController: AVPlayerViewController) {
|
||||
if _playerViewController == playerViewController && _fullscreenPlayerPresented, let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss {
|
||||
if _playerViewController == playerViewController
|
||||
&& _fullscreenPlayerPresented,
|
||||
let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss {
|
||||
_playerObserver.removePlayerViewControllerObservers()
|
||||
onVideoFullscreenPlayerWillDismiss(["target": reactTag as Any])
|
||||
}
|
||||
@ -975,7 +976,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
let output: CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent)
|
||||
request.finish(with: output, context: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
@ -996,6 +998,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
func setAdTagUrl(_ adTagUrl: String!) {
|
||||
_adTagUrl = adTagUrl
|
||||
}
|
||||
|
||||
#if USE_GOOGLE_IMA
|
||||
func getContentPlayhead() -> IMAAVPlayerContentPlayhead? {
|
||||
return _contentPlayhead
|
||||
@ -1063,6 +1066,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
_eventDispatcher = nil
|
||||
// swiftlint:disable:next notification_center_detachment
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
super.removeFromSuperview()
|
||||
@ -1098,19 +1102,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
// MARK: - RCTPlayerObserverHandler
|
||||
|
||||
func handleTimeUpdate(time:CMTime) {
|
||||
func handleTimeUpdate(time _: CMTime) {
|
||||
sendProgressUpdate()
|
||||
}
|
||||
|
||||
func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange<Bool>) {
|
||||
func handleReadyForDisplay(changeObject _: Any, change _: NSKeyValueObservedChange<Bool>) {
|
||||
onReadyForDisplay?([
|
||||
"target": reactTag
|
||||
"target": reactTag,
|
||||
])
|
||||
}
|
||||
|
||||
// When timeMetadata is read the event onTimedMetadata is triggered
|
||||
func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) {
|
||||
guard let newValue = change.newValue, let _items = newValue, _items.count > 0 else {
|
||||
func handleTimeMetadataChange(playerItem _: AVPlayerItem, change: NSKeyValueObservedChange<[AVMetadataItem]?>) {
|
||||
guard let newValue = change.newValue, let _items = newValue, !_items.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1126,12 +1130,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
onTimedMetadata?([
|
||||
"target": reactTag,
|
||||
"metadata": metadata
|
||||
"metadata": metadata,
|
||||
])
|
||||
}
|
||||
|
||||
// Handle player item status change.
|
||||
func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<AVPlayerItem.Status>) {
|
||||
func handlePlayerItemStatusChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<AVPlayerItem.Status>) {
|
||||
guard let _playerItem = _playerItem else {
|
||||
return
|
||||
}
|
||||
@ -1145,17 +1149,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func handleReadyToPlay() {
|
||||
guard let _playerItem = _playerItem else { return }
|
||||
var duration:Float = Float(CMTimeGetSeconds(_playerItem.asset.duration))
|
||||
var duration = Float(CMTimeGetSeconds(_playerItem.asset.duration))
|
||||
|
||||
if duration.isNaN {
|
||||
duration = 0.0
|
||||
}
|
||||
|
||||
var width: Float? = nil
|
||||
var height: Float? = nil
|
||||
var width: Float?
|
||||
var height: Float?
|
||||
var orientation = "undefined"
|
||||
|
||||
if _playerItem.asset.tracks(withMediaType: AVMediaType.video).count > 0 {
|
||||
if !_playerItem.asset.tracks(withMediaType: AVMediaType.video).isEmpty {
|
||||
let videoTrack = _playerItem.asset.tracks(withMediaType: .video)[0]
|
||||
width = Float(videoTrack.naturalSize.width)
|
||||
height = Float(videoTrack.naturalSize.height)
|
||||
@ -1163,8 +1167,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
if (videoTrack.naturalSize.width == preferredTransform.tx
|
||||
&& videoTrack.naturalSize.height == preferredTransform.ty)
|
||||
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0)
|
||||
{
|
||||
|| (preferredTransform.tx == 0 && preferredTransform.ty == 0) {
|
||||
orientation = "landscape"
|
||||
} else {
|
||||
orientation = "portrait"
|
||||
@ -1178,7 +1181,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
if _pendingSeek {
|
||||
setSeek([
|
||||
"time": NSNumber(value: _pendingSeekTime),
|
||||
"tolerance": NSNumber(value: 100)
|
||||
"tolerance": NSNumber(value: 100),
|
||||
])
|
||||
_pendingSeek = false
|
||||
}
|
||||
@ -1186,7 +1189,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
if _startPosition >= 0 {
|
||||
setSeek([
|
||||
"time": NSNumber(value: _startPosition),
|
||||
"tolerance": NSNumber(value: 100)
|
||||
"tolerance": NSNumber(value: 100),
|
||||
])
|
||||
_startPosition = -1
|
||||
}
|
||||
@ -1205,7 +1208,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"naturalSize": [
|
||||
"width": width != nil ? NSNumber(value: width!) : "undefinded",
|
||||
"height": width != nil ? NSNumber(value: height!) : "undefinded",
|
||||
"orientation": orientation
|
||||
"orientation": orientation,
|
||||
],
|
||||
"audioTracks": audioTracks,
|
||||
"textTracks": textTracks,
|
||||
@ -1223,21 +1226,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"error": [
|
||||
"code": NSNumber(value: (_playerItem.error! as NSError).code),
|
||||
"localizedDescription": _playerItem.error?.localizedDescription == nil ? "" : _playerItem.error?.localizedDescription,
|
||||
"localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ? "" : (_playerItem.error! as NSError).localizedFailureReason) ?? "",
|
||||
"localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ? "" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "",
|
||||
"domain": (_playerItem.error as! NSError).domain
|
||||
"localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ?
|
||||
"" : (_playerItem.error! as NSError).localizedFailureReason) ?? "",
|
||||
"localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ?
|
||||
"" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "",
|
||||
"domain": (_playerItem.error as! NSError).domain,
|
||||
],
|
||||
"target": reactTag
|
||||
])
|
||||
"target": reactTag,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<Bool>) {
|
||||
func handlePlaybackBufferKeyEmpty(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<Bool>) {
|
||||
_playerBufferEmpty = true
|
||||
onVideoBuffer?(["isBuffering": true, "target": reactTag as Any])
|
||||
}
|
||||
|
||||
// Continue playing (or not if paused) after being paused due to hitting an unbuffered zone.
|
||||
func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<Bool>) {
|
||||
func handlePlaybackLikelyToKeepUp(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<Bool>) {
|
||||
if (!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && ((_playerItem?.isPlaybackLikelyToKeepUp) == true) {
|
||||
setPaused(_paused)
|
||||
}
|
||||
@ -1248,7 +1254,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange<Float>) {
|
||||
guard let _player = _player else { return }
|
||||
|
||||
if(player.rate == change.oldValue && change.oldValue != nil) {
|
||||
if player.rate == change.oldValue && change.oldValue != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1268,7 +1274,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
func handleVolumeChange(player: AVPlayer, change: NSKeyValueObservedChange<Float>) {
|
||||
guard let _player = _player else { return }
|
||||
|
||||
if(player.rate == change.oldValue && change.oldValue != nil) {
|
||||
if player.rate == change.oldValue && change.oldValue != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1276,29 +1282,29 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"target": reactTag as Any])
|
||||
}
|
||||
|
||||
func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange<Bool>) {
|
||||
func handleExternalPlaybackActiveChange(player _: AVPlayer, change _: NSKeyValueObservedChange<Bool>) {
|
||||
guard let _player = _player else { return }
|
||||
onVideoExternalPlaybackChange?(["isExternalPlaybackActive": NSNumber(value: _player.isExternalPlaybackActive),
|
||||
"target": reactTag as Any])
|
||||
}
|
||||
|
||||
func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange<CGRect>) {
|
||||
func handleViewControllerOverlayViewFrameChange(overlayView _: UIView, change: NSKeyValueObservedChange<CGRect>) {
|
||||
let oldRect = change.oldValue
|
||||
let newRect = change.newValue
|
||||
if !oldRect!.equalTo(newRect!) {
|
||||
// https://github.com/react-native-video/react-native-video/issues/3085#issuecomment-1557293391
|
||||
if newRect!.equalTo(UIScreen.main.bounds) {
|
||||
RCTLog("in fullscreen")
|
||||
if (!_fullscreenUncontrolPlayerPresented) {
|
||||
_fullscreenUncontrolPlayerPresented = true;
|
||||
if !_fullscreenUncontrolPlayerPresented {
|
||||
_fullscreenUncontrolPlayerPresented = true
|
||||
|
||||
self.onVideoFullscreenPlayerWillPresent?(["target": self.reactTag as Any])
|
||||
self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag as Any])
|
||||
}
|
||||
} else {
|
||||
NSLog("not fullscreen")
|
||||
if (_fullscreenUncontrolPlayerPresented) {
|
||||
_fullscreenUncontrolPlayerPresented = false;
|
||||
if _fullscreenUncontrolPlayerPresented {
|
||||
_fullscreenUncontrolPlayerPresented = false
|
||||
|
||||
self.onVideoFullscreenPlayerWillDismiss?(["target": self.reactTag as Any])
|
||||
self.onVideoFullscreenPlayerDidDismiss?(["target": self.reactTag as Any])
|
||||
@ -1310,7 +1316,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleDidFailToFinishPlaying(notification:NSNotification!) {
|
||||
@objc
|
||||
func handleDidFailToFinishPlaying(notification: NSNotification!) {
|
||||
let error: NSError! = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError
|
||||
onVideoError?(
|
||||
[
|
||||
@ -1319,18 +1326,21 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"localizedDescription": error.localizedDescription ?? "",
|
||||
"localizedFailureReason": (error as NSError).localizedFailureReason ?? "",
|
||||
"localizedRecoverySuggestion": (error as NSError).localizedRecoverySuggestion ?? "",
|
||||
"domain": (error as NSError).domain
|
||||
"domain": (error as NSError).domain,
|
||||
],
|
||||
"target": reactTag
|
||||
])
|
||||
"target": reactTag,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@objc func handlePlaybackStalled(notification:NSNotification!) {
|
||||
@objc
|
||||
func handlePlaybackStalled(notification _: NSNotification!) {
|
||||
onPlaybackStalled?(["target": reactTag as Any])
|
||||
_playbackStalled = true
|
||||
}
|
||||
|
||||
@objc func handlePlayerItemDidReachEnd(notification:NSNotification!) {
|
||||
@objc
|
||||
func handlePlayerItemDidReachEnd(notification: NSNotification!) {
|
||||
onVideoEnd?(["target": reactTag as Any])
|
||||
#if USE_GOOGLE_IMA
|
||||
if notification.object as? AVPlayerItem == _player?.currentItem {
|
||||
@ -1342,12 +1352,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
item.seek(to: CMTime.zero, completionHandler: nil)
|
||||
self.applyModifiers()
|
||||
} else {
|
||||
self.setPaused(true);
|
||||
self.setPaused(true)
|
||||
_playerObserver.removePlayerTimeObserver()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleAVPlayerAccess(notification:NSNotification!) {
|
||||
@objc
|
||||
func handleAVPlayerAccess(notification: NSNotification!) {
|
||||
let accessLog: AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog()
|
||||
let lastEvent: AVPlayerItemAccessLogEvent! = accessLog.events.last
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#import <React/RCTBridge.h>
|
||||
#import "React/RCTViewManager.h"
|
||||
#import <React/RCTBridge.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager)
|
||||
|
||||
@ -65,27 +65,22 @@ RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock);
|
||||
|
||||
RCT_EXTERN_METHOD(save:(NSDictionary *)options
|
||||
reactTag:(nonnull NSNumber *)reactTag
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
RCT_EXTERN_METHOD(save
|
||||
: (NSDictionary*)options reactTag
|
||||
: (nonnull NSNumber*)reactTag resolver
|
||||
: (RCTPromiseResolveBlock)resolve rejecter
|
||||
: (RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license
|
||||
licenseUrl:(NSString *)licenseUrl
|
||||
reactTag:(nonnull NSNumber *)reactTag)
|
||||
RCT_EXTERN_METHOD(setLicenseResult : (NSString*)license licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag)
|
||||
|
||||
RCT_EXTERN_METHOD(setLicenseResultError:(NSString *)error
|
||||
licenseUrl:(NSString *)licenseUrl
|
||||
reactTag:(nonnull NSNumber *)reactTag)
|
||||
RCT_EXTERN_METHOD(setLicenseResultError : (NSString*)error licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag)
|
||||
|
||||
RCT_EXTERN_METHOD(setPlayerPauseState:(nonnull NSNumber *)paused
|
||||
reactTag:(nonnull NSNumber *)reactTag)
|
||||
RCT_EXTERN_METHOD(setPlayerPauseState : (nonnull NSNumber*)paused reactTag : (nonnull NSNumber*)reactTag)
|
||||
|
||||
RCT_EXTERN_METHOD(presentFullscreenPlayer : (nonnull NSNumber*)reactTag)
|
||||
|
||||
RCT_EXTERN_METHOD(dismissFullscreenPlayer : (nonnull NSNumber*)reactTag)
|
||||
|
||||
RCT_EXTERN_METHOD(dismissFullscreenPlayer
|
||||
reactTag:(nonnull NSNumber *)reactTag)
|
||||
RCT_EXTERN_METHOD(dismissFullscreenPlayer reactTag : (nonnull NSNumber*)reactTag)
|
||||
|
||||
@end
|
||||
|
@ -3,7 +3,6 @@ import React
|
||||
|
||||
@objc(RCTVideoManager)
|
||||
class RCTVideoManager: RCTViewManager {
|
||||
|
||||
override func view() -> UIView {
|
||||
return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher)
|
||||
}
|
||||
@ -13,67 +12,68 @@ class RCTVideoManager: RCTViewManager {
|
||||
}
|
||||
|
||||
@objc(save:reactTag:resolver:rejecter:)
|
||||
func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
|
||||
bridge.uiManager.prependUIBlock({_ , viewRegistry in
|
||||
func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
bridge.uiManager.prependUIBlock { _, viewRegistry in
|
||||
let view = viewRegistry?[reactTag]
|
||||
if !(view is RCTVideo) {
|
||||
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
|
||||
} else if let view = view as? RCTVideo {
|
||||
view.save(options: options, resolve: resolve, reject: reject)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc(setLicenseResult:licenseUrl:reactTag:)
|
||||
func setLicenseResult(license: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void {
|
||||
bridge.uiManager.prependUIBlock({_ , viewRegistry in
|
||||
func setLicenseResult(license: NSString, licenseUrl: NSString, reactTag: NSNumber) {
|
||||
bridge.uiManager.prependUIBlock { _, viewRegistry in
|
||||
let view = viewRegistry?[reactTag]
|
||||
if !(view is RCTVideo) {
|
||||
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
|
||||
} else if let view = view as? RCTVideo {
|
||||
view.setLicenseResult(license as String, licenseUrl as String)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc(setLicenseResultError:licenseUrl:reactTag:)
|
||||
func setLicenseResultError(error: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void {
|
||||
bridge.uiManager.prependUIBlock({_ , viewRegistry in
|
||||
func setLicenseResultError(error: NSString, licenseUrl: NSString, reactTag: NSNumber) {
|
||||
bridge.uiManager.prependUIBlock { _, viewRegistry in
|
||||
let view = viewRegistry?[reactTag]
|
||||
if !(view is RCTVideo) {
|
||||
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
|
||||
} else if let view = view as? RCTVideo {
|
||||
view.setLicenseResultError(error as String, licenseUrl as String)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc(dismissFullscreenPlayer:)
|
||||
func dismissFullscreenPlayer(_ reactTag: NSNumber) -> Void {
|
||||
bridge.uiManager.prependUIBlock({_ , viewRegistry in
|
||||
func dismissFullscreenPlayer(_ reactTag: NSNumber) {
|
||||
bridge.uiManager.prependUIBlock { _, viewRegistry in
|
||||
let view = viewRegistry?[reactTag]
|
||||
if !(view is RCTVideo) {
|
||||
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
|
||||
} else if let view = view as? RCTVideo {
|
||||
view.dismissFullscreenPlayer()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc(presentFullscreenPlayer:)
|
||||
func presentFullscreenPlayer(_ reactTag: NSNumber) -> Void {
|
||||
bridge.uiManager.prependUIBlock({_ , viewRegistry in
|
||||
func presentFullscreenPlayer(_ reactTag: NSNumber) {
|
||||
bridge.uiManager.prependUIBlock { _, viewRegistry in
|
||||
let view = viewRegistry?[reactTag]
|
||||
if !(view is RCTVideo) {
|
||||
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
|
||||
} else if let view = view as? RCTVideo {
|
||||
view.presentFullscreenPlayer()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc(setPlayerPauseState:reactTag:)
|
||||
func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) -> Void {
|
||||
bridge.uiManager.prependUIBlock({_ , viewRegistry in
|
||||
func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) {
|
||||
bridge.uiManager.prependUIBlock { _, viewRegistry in
|
||||
let view = viewRegistry?[reactTag]
|
||||
if !(view is RCTVideo) {
|
||||
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
|
||||
@ -81,7 +81,7 @@ class RCTVideoManager: RCTViewManager {
|
||||
let paused = paused.boolValue
|
||||
view.setPaused(paused)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import AVKit
|
||||
|
||||
class RCTVideoPlayerViewController: AVPlayerViewController {
|
||||
|
||||
weak var rctDelegate: RCTVideoPlayerViewControllerDelegate?
|
||||
|
||||
// Optional paramters
|
||||
@ -9,7 +8,6 @@ class RCTVideoPlayerViewController: AVPlayerViewController {
|
||||
var autorotate: Bool?
|
||||
|
||||
func shouldAutorotate() -> Bool {
|
||||
|
||||
if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") {
|
||||
return true
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import AVKit
|
||||
import Foundation
|
||||
|
||||
protocol RCTVideoPlayerViewControllerDelegate : NSObject {
|
||||
protocol RCTVideoPlayerViewControllerDelegate: class {
|
||||
func videoPlayerViewControllerWillDismiss(playerViewController: AVPlayerViewController)
|
||||
func videoPlayerViewControllerDidDismiss(playerViewController: AVPlayerViewController)
|
||||
}
|
||||
|
@ -4,28 +4,23 @@
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
+ (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);
|
||||
}
|
||||
|
||||
+ (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);
|
||||
}
|
||||
|
||||
+ (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);
|
||||
}
|
||||
|
||||
+ (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);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// RCTLog.swift
|
||||
// RCTVideoSwiftLog.swift
|
||||
// WebViewExample
|
||||
//
|
||||
// Created by Jimmy Dee on 4/5/17.
|
||||
@ -52,4 +52,3 @@ func DebugLog(_ message: String) {
|
||||
print(logHeader + message)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SPTPersistentCache/SPTPersistentCache.h>
|
||||
#import <SPTPersistentCache/SPTPersistentCacheOptions.h>
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) {
|
||||
RCTVideoCacheStatusMissingFileExtension,
|
||||
@ -14,8 +14,7 @@ typedef NS_ENUM(NSUInteger, RCTVideoCacheStatus) {
|
||||
@class SPTPersistentCache;
|
||||
@class SPTPersistentCacheOptions;
|
||||
|
||||
@interface RCTVideoCache : NSObject
|
||||
{
|
||||
@interface RCTVideoCache : NSObject {
|
||||
SPTPersistentCache* videoCache;
|
||||
NSString* _Nullable cachePath;
|
||||
NSString* temporaryCachePath;
|
||||
|
@ -20,7 +20,8 @@
|
||||
if (self = [super init]) {
|
||||
self.cacheIdentifier = @"rct.video.cache";
|
||||
self.temporaryCachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:self.cacheIdentifier];
|
||||
self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:self.cacheIdentifier];
|
||||
self.cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject
|
||||
stringByAppendingPathComponent:self.cacheIdentifier];
|
||||
SPTPersistentCacheOptions* options = [SPTPersistentCacheOptions new];
|
||||
options.cachePath = self.cachePath;
|
||||
options.cacheIdentifier = self.cacheIdentifier;
|
||||
@ -61,7 +62,10 @@
|
||||
return;
|
||||
}
|
||||
[self saveDataToTemporaryStorage:data key:key];
|
||||
[self.videoCache storeData:data forKey:key locked:NO withCallback:^(SPTPersistentCacheResponse * _Nonnull response) {
|
||||
[self.videoCache storeData:data
|
||||
forKey:key
|
||||
locked:NO
|
||||
withCallback:^(SPTPersistentCacheResponse* _Nonnull response) {
|
||||
if (response.error) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"VideoCache: An error occured while saving the video into the cache: %@", [response.error localizedDescription]);
|
||||
@ -70,7 +74,8 @@
|
||||
return;
|
||||
}
|
||||
handler(YES);
|
||||
} onQueue:dispatch_get_main_queue()];
|
||||
}
|
||||
onQueue:dispatch_get_main_queue()];
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,8 +114,7 @@
|
||||
NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Missing file extension.", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString(@"Missing file extension.", nil)
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:@"RCTVideoCache"
|
||||
code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo];
|
||||
NSError* error = [NSError errorWithDomain:@"RCTVideoCache" code:RCTVideoCacheStatusMissingFileExtension userInfo:userInfo];
|
||||
@throw error;
|
||||
} else if (![supportedExtensions containsObject:pathExtension]) {
|
||||
// Notably, we don't currently support m3u8 (HLS playlists)
|
||||
@ -119,8 +123,7 @@
|
||||
NSLocalizedFailureReasonErrorKey : NSLocalizedString(@"Unsupported file extension.", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey : NSLocalizedString(@"Unsupported file extension.", nil)
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:@"RCTVideoCache"
|
||||
code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo];
|
||||
NSError* error = [NSError errorWithDomain:@"RCTVideoCache" code:RCTVideoCacheStatusUnsupportedFileExtension userInfo:userInfo];
|
||||
@throw error;
|
||||
}
|
||||
return [[self generateHashForUrl:uri] stringByAppendingPathExtension:pathExtension];
|
||||
@ -135,14 +138,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
[self.videoCache loadDataForKey:key withCallback:^(SPTPersistentCacheResponse * _Nonnull response) {
|
||||
[self.videoCache loadDataForKey:key
|
||||
withCallback:^(SPTPersistentCacheResponse* _Nonnull response) {
|
||||
if (response.record == nil || response.record.data == nil) {
|
||||
handler(RCTVideoCacheStatusNotAvailable, nil);
|
||||
return;
|
||||
}
|
||||
[self saveDataToTemporaryStorage:response.record.data key:key];
|
||||
handler(RCTVideoCacheStatusAvailable, [self getItemFromTemporaryStorage:key]);
|
||||
} onQueue:dispatch_get_main_queue()];
|
||||
}
|
||||
onQueue:dispatch_get_main_queue()];
|
||||
} @catch (NSError* err) {
|
||||
switch (err.code) {
|
||||
case RCTVideoCacheStatusMissingFileExtension:
|
||||
@ -162,13 +167,9 @@
|
||||
unsigned char result[CC_MD5_DIGEST_LENGTH];
|
||||
CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
|
||||
|
||||
return [NSString stringWithFormat:
|
||||
@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
|
||||
result[0], result[1], result[2], result[3],
|
||||
result[4], result[5], result[6], result[7],
|
||||
result[8], result[9], result[10], result[11],
|
||||
result[12], result[13], result[14], result[15]
|
||||
];
|
||||
return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", result[0], result[1], result[2],
|
||||
result[3], result[4], result[5], result[6], result[7], result[8], result[9], result[10], result[11],
|
||||
result[12], result[13], result[14], result[15]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -1,10 +1,9 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import DVAssetLoaderDelegate
|
||||
import Foundation
|
||||
import Promises
|
||||
|
||||
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
|
||||
private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance()
|
||||
var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) -> AVPlayerItem)?
|
||||
|
||||
@ -13,12 +12,15 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
}
|
||||
|
||||
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
|
||||
* to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded.
|
||||
* Until this is fixed, we need to bypass caching when text tracks are specified.
|
||||
*/
|
||||
DebugLog("Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
||||
DebugLog("""
|
||||
Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -29,14 +31,24 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
return getItemForUri(uri)
|
||||
.then { [weak self] (videoCacheStatus: RCTVideoCacheStatus, cachedAsset: AVAsset?) -> AVPlayerItem in
|
||||
guard let self = self, let playerItemPrepareText = self.playerItemPrepareText else { throw NSError(domain: "", code: 0, userInfo: nil) }
|
||||
switch (videoCacheStatus) {
|
||||
switch videoCacheStatus {
|
||||
case .missingFileExtension:
|
||||
DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
||||
DebugLog("""
|
||||
Could not generate cache key for uri '\(uri)'.
|
||||
It is currently not supported to cache urls that do not include a file extension.
|
||||
The video file will not be cached.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||
return playerItemPrepareText(asset, options, "")
|
||||
|
||||
case .unsupportedFileExtension:
|
||||
DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md")
|
||||
DebugLog("""
|
||||
Could not generate cache key for uri '\(uri)'.
|
||||
The file extension of that uri is currently not supported.
|
||||
The video file will not be cached.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||
return playerItemPrepareText(asset, options, "")
|
||||
|
||||
@ -68,7 +80,7 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
}
|
||||
|
||||
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
|
||||
fulfill((videoCacheStatus, cachedAsset))
|
||||
})
|
||||
@ -77,11 +89,9 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
|
||||
// MARK: - DVAssetLoaderDelegate
|
||||
|
||||
func dvAssetLoaderDelegate(_ loaderDelegate: DVAssetLoaderDelegate!, didLoad data: Data!, for url: URL!) {
|
||||
_videoCache.storeItem(data as Data?, forUri:url.absoluteString, withCallback:{ (success:Bool) in
|
||||
func dvAssetLoaderDelegate(_: DVAssetLoaderDelegate!, didLoad data: Data!, for url: URL!) {
|
||||
_videoCache.storeItem(data as Data?, forUri: url.absoluteString, withCallback: { (_: Bool) in
|
||||
DebugLog("Cache data stored successfully 🎉")
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user