Compare commits
38 Commits
d277c5e946
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bb9e08c43a | |||
|
|
8dc10fd4b7 | ||
|
|
449dfb62b5 | ||
|
|
6c3af99979 | ||
|
|
6cc1bff167 | ||
|
|
89ee02bdab | ||
|
|
3924b5e295 | ||
|
|
eff8ea24af | ||
|
|
c47d165668 | ||
|
|
424f4eedde | ||
|
|
d31c72fc04 | ||
|
|
69a7bc2d26 | ||
|
|
a735a4a581 | ||
|
|
78770d92f5 | ||
|
|
abc4d76099 | ||
|
|
16fa20411f | ||
|
|
3da4f1ca97 | ||
|
|
7b4bd9a016 | ||
|
|
1033c9d4f3 | ||
|
|
d757a44bb1 | ||
|
|
64c222df44 | ||
|
|
621a80299c | ||
|
|
63c592f7cd | ||
|
|
569a79c510 | ||
|
|
f37dc9e33e | ||
|
|
dd78241b0d | ||
|
|
2b7c215e66 | ||
|
|
daaac9740a | ||
|
|
d934f214f5 | ||
| f72b44d4df | |||
| d2ab22b99f | |||
| 2dcde42fd6 | |||
| c7a45d421b | |||
| f0db0a6868 | |||
| 01b3322e03 | |||
| 13beae1401 | |||
| f3deabd75e | |||
| d69729dc04 |
12
.github/scripts/validate.js
vendored
12
.github/scripts/validate.js
vendored
@@ -31,7 +31,7 @@ const BOT_LABELS = [
|
|||||||
const SKIP_LABEL = 'No Validation';
|
const SKIP_LABEL = 'No Validation';
|
||||||
|
|
||||||
const MESSAGE = {
|
const MESSAGE = {
|
||||||
FEATURE_REQUEST: `Thank you for your feature request. We will review it and get back to you if we need more information.`,
|
FEATURE_REQUEST: `Thanks for the feature request! Check out our roadmap [here](https://github.com/TheWidlarzGroup/react-native-video/discussions/3351). If your request is already there – great! If not, give us some time, and we'll get back to you with information on when TheWidlarzGroup can address it as part of our free open-source support. Alternatively, [contact us](https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=feature-request#Contact) to discuss ways to speed up the process.`,
|
||||||
BUG_REPORT: `Thank you for your bug report. We will review it and get back to you if we need more information.`,
|
BUG_REPORT: `Thank you for your bug report. We will review it and get back to you if we need more information.`,
|
||||||
MISSING_INFO: (missingFields) => {
|
MISSING_INFO: (missingFields) => {
|
||||||
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
|
return `Thank you for your issue report. Please note that the following information is missing or incomplete:\n\n${missingFields
|
||||||
@@ -267,11 +267,15 @@ const hidePreviousComments = async ({github, context}) => {
|
|||||||
issue_number: context.payload.issue.number,
|
issue_number: context.payload.issue.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
const botComments = comments.data.filter(
|
// Filter for bot comments that aren't already hidden
|
||||||
(comment) => comment.user.type === 'Bot',
|
const unhiddenBotComments = comments.data.filter(
|
||||||
|
(comment) =>
|
||||||
|
comment.user.type === 'Bot' &&
|
||||||
|
!comment.body.includes('<details>') &&
|
||||||
|
!comment.body.includes('Previous bot comment')
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const comment of botComments) {
|
for (const comment of unhiddenBotComments) {
|
||||||
// Don't format string - it will broke the markdown
|
// Don't format string - it will broke the markdown
|
||||||
const hiddenBody = `
|
const hiddenBody = `
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,5 +1,50 @@
|
|||||||
|
|
||||||
|
|
||||||
|
## [6.9.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.9.0...v6.9.1) (2025-01-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid memory leak on iOS ([#4355](https://github.com/TheWidlarzGroup/react-native-video/issues/4355)) ([424f4ee](https://github.com/TheWidlarzGroup/react-native-video/commit/424f4eeddea989392e25c52f45a9a0281ead6fe1))
|
||||||
|
* NPE in setEnterPictureInPictureOnLeave for unsupported Android versions ([#4362](https://github.com/TheWidlarzGroup/react-native-video/issues/4362)) ([3924b5e](https://github.com/TheWidlarzGroup/react-native-video/commit/3924b5e295ed64c97284f4665bc294066a83574a))
|
||||||
|
|
||||||
|
# [6.9.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.2...v6.9.0) (2025-01-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** disable caching on local asset files ([#4304](https://github.com/TheWidlarzGroup/react-native-video/issues/4304)) ([63c592f](https://github.com/TheWidlarzGroup/react-native-video/commit/63c592f7cd897caf918fd3bd5f129c72432d2b55))
|
||||||
|
* **docs:** bump `next.js` version & fix meta warnings ([#4327](https://github.com/TheWidlarzGroup/react-native-video/issues/4327)) ([7b4bd9a](https://github.com/TheWidlarzGroup/react-native-video/commit/7b4bd9a0169fc2ea6f277dd7ed904bada98bc63a))
|
||||||
|
* hiding poster ([#4308](https://github.com/TheWidlarzGroup/react-native-video/issues/4308)) ([621a802](https://github.com/TheWidlarzGroup/react-native-video/commit/621a80299c690c07846f3fcd8a6c73b7ecde39bf))
|
||||||
|
* **ios:** `_paused` is updated when video playback pause ([#4320](https://github.com/TheWidlarzGroup/react-native-video/issues/4320)) ([3da4f1c](https://github.com/TheWidlarzGroup/react-native-video/commit/3da4f1ca979058b387b1be2c2141f6b93fd084a7))
|
||||||
|
* **ios:** disables subtitles for `none` and `empty` track types ([#4319](https://github.com/TheWidlarzGroup/react-native-video/issues/4319)) ([1033c9d](https://github.com/TheWidlarzGroup/react-native-video/commit/1033c9d4f3db7042a96e7108a7fe9f1567d69ded))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) ([#3385](https://github.com/TheWidlarzGroup/react-native-video/issues/3385)) ([69a7bc2](https://github.com/TheWidlarzGroup/react-native-video/commit/69a7bc2d265f2cf4985f8d81054c46f47ee3bae2))
|
||||||
|
|
||||||
|
## [6.8.2](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.1...v6.8.2) (2024-11-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* playback restart without bufferingConfig ([#4305](https://github.com/TheWidlarzGroup/react-native-video/issues/4305)) ([f37dc9e](https://github.com/TheWidlarzGroup/react-native-video/commit/f37dc9e33ebefd922605c5ae91360379fe91bed6))
|
||||||
|
|
||||||
|
## [6.8.1](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.8.0...v6.8.1) (2024-11-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios:** handle async player access in text track selection ([#4293](https://github.com/TheWidlarzGroup/react-native-video/issues/4293)) ([daaac97](https://github.com/TheWidlarzGroup/react-native-video/commit/daaac9740aed1858b7ababae0ec8b08274130a27))
|
||||||
|
|
||||||
|
# [6.8.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.7.0...v6.8.0) (2024-11-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** add helper to avoid type error ([#4257](https://github.com/TheWidlarzGroup/react-native-video/issues/4257)) ([3b4bfd3](https://github.com/TheWidlarzGroup/react-native-video/commit/3b4bfd3936a8cb846c0e61ffd396940987a7ba43))
|
||||||
|
|
||||||
# [6.7.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.4...v6.7.0) (2024-10-17)
|
# [6.7.0](https://github.com/TheWidlarzGroup/react-native-video/compare/v6.6.4...v6.7.0) (2024-10-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,9 +91,10 @@ def configStringPath = ExoplayerDependencies
|
|||||||
.concat("buildFromSource:$media3_buildFromSource")
|
.concat("buildFromSource:$media3_buildFromSource")
|
||||||
.md5()
|
.md5()
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
// commented as new architecture not yet fully supported
|
||||||
apply plugin: "com.facebook.react"
|
// if (isNewArchitectureEnabled()) {
|
||||||
}
|
// apply plugin: "com.facebook.react"
|
||||||
|
// }
|
||||||
|
|
||||||
android {
|
android {
|
||||||
if (supportsNamespace()) {
|
if (supportsNamespace()) {
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ RNVideo_useExoplayerSmoothStreaming=true
|
|||||||
RNVideo_useExoplayerDash=true
|
RNVideo_useExoplayerDash=true
|
||||||
RNVideo_useExoplayerHls=true
|
RNVideo_useExoplayerHls=true
|
||||||
RNVideo_androidxCoreVersion=1.13.1
|
RNVideo_androidxCoreVersion=1.13.1
|
||||||
RNVideo_androidxActivityVersion=1.8.2
|
RNVideo_androidxActivityVersion=1.9.3
|
||||||
RNVideo_buildFromMedia3Source=false
|
RNVideo_buildFromMedia3Source=false
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ class BufferConfig {
|
|||||||
|
|
||||||
var live: Live = Live()
|
var live: Live = Live()
|
||||||
|
|
||||||
|
/** return true if this and src are equals */
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || other !is BufferConfig) return false
|
||||||
|
return (
|
||||||
|
cacheSize == other.cacheSize &&
|
||||||
|
minBufferMs == other.minBufferMs &&
|
||||||
|
maxBufferMs == other.maxBufferMs &&
|
||||||
|
bufferForPlaybackMs == other.bufferForPlaybackMs &&
|
||||||
|
bufferForPlaybackAfterRebufferMs == other.bufferForPlaybackAfterRebufferMs &&
|
||||||
|
backBufferDurationMs == other.backBufferDurationMs &&
|
||||||
|
maxHeapAllocationPercent == other.maxHeapAllocationPercent &&
|
||||||
|
minBackBufferMemoryReservePercent == other.minBackBufferMemoryReservePercent &&
|
||||||
|
minBufferMemoryReservePercent == other.minBufferMemoryReservePercent &&
|
||||||
|
live == other.live
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Live {
|
class Live {
|
||||||
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
var maxPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
||||||
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
var minPlaybackSpeed: Float = BufferConfigPropUnsetDouble.toFloat()
|
||||||
@@ -30,12 +47,23 @@ class BufferConfig {
|
|||||||
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
var minOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
||||||
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
var targetOffsetMs: Long = BufferConfigPropUnsetInt.toLong()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null || other !is Live) return false
|
||||||
|
return (
|
||||||
|
maxPlaybackSpeed == other.maxPlaybackSpeed &&
|
||||||
|
minPlaybackSpeed == other.minPlaybackSpeed &&
|
||||||
|
maxOffsetMs == other.maxOffsetMs &&
|
||||||
|
minOffsetMs == other.minOffsetMs &&
|
||||||
|
targetOffsetMs == other.targetOffsetMs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
|
private const val PROP_BUFFER_CONFIG_LIVE_MAX_PLAYBACK_SPEED = "maxPlaybackSpeed"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
|
private const val PROP_BUFFER_CONFIG_LIVE_MIN_PLAYBACK_SPEED = "minPlaybackSpeed"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
|
private const val PROP_BUFFER_CONFIG_LIVE_MAX_OFFSET_MS = "maxOffsetMs"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
|
private const val PROP_BUFFER_CONFIG_LIVE_MIN_OFFSET_MS = "minOffsetMs"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
|
private const val PROP_BUFFER_CONFIG_LIVE_TARGET_OFFSET_MS = "targetOffsetMs"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(src: ReadableMap?): Live {
|
fun parse(src: ReadableMap?): Live {
|
||||||
@@ -54,16 +82,16 @@ class BufferConfig {
|
|||||||
val BufferConfigPropUnsetInt = -1
|
val BufferConfigPropUnsetInt = -1
|
||||||
val BufferConfigPropUnsetDouble = -1.0
|
val BufferConfigPropUnsetDouble = -1.0
|
||||||
|
|
||||||
private val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
|
private const val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB"
|
||||||
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
|
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs"
|
||||||
private val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
|
private const val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs"
|
||||||
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
|
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs"
|
||||||
private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
|
private const val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs"
|
||||||
private val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
|
private const val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent"
|
||||||
private val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
|
private const val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent"
|
||||||
private val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
|
private const val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent"
|
||||||
private val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
|
private const val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs"
|
||||||
private val PROP_BUFFER_CONFIG_LIVE = "live"
|
private const val PROP_BUFFER_CONFIG_LIVE = "live"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(src: ReadableMap?): BufferConfig {
|
fun parse(src: ReadableMap?): BufferConfig {
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ class Source {
|
|||||||
/** Parsed value of source to playback */
|
/** Parsed value of source to playback */
|
||||||
var uri: Uri? = null
|
var uri: Uri? = null
|
||||||
|
|
||||||
|
/** True if source is a local JS asset */
|
||||||
|
var isLocalAssetFile: Boolean = false
|
||||||
|
|
||||||
|
/** True if source is a local file asset://, ... */
|
||||||
|
var isAsset: Boolean = false
|
||||||
|
|
||||||
/** Start position of playback used to resume playback */
|
/** Start position of playback used to resume playback */
|
||||||
var startPositionMs: Int = -1
|
var startPositionMs: Int = -1
|
||||||
|
|
||||||
@@ -74,6 +80,11 @@ class Source {
|
|||||||
*/
|
*/
|
||||||
var adsProps: AdsProps? = null
|
var adsProps: AdsProps? = null
|
||||||
|
|
||||||
|
/*
|
||||||
|
* buffering configuration
|
||||||
|
*/
|
||||||
|
var bufferConfig = BufferConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of sideLoaded text tracks
|
* The list of sideLoaded text tracks
|
||||||
*/
|
*/
|
||||||
@@ -95,7 +106,10 @@ class Source {
|
|||||||
cmcdProps == other.cmcdProps &&
|
cmcdProps == other.cmcdProps &&
|
||||||
sideLoadedTextTracks == other.sideLoadedTextTracks &&
|
sideLoadedTextTracks == other.sideLoadedTextTracks &&
|
||||||
adsProps == other.adsProps &&
|
adsProps == other.adsProps &&
|
||||||
minLoadRetryCount == other.minLoadRetryCount
|
minLoadRetryCount == other.minLoadRetryCount &&
|
||||||
|
isLocalAssetFile == other.isLocalAssetFile &&
|
||||||
|
isAsset == other.isAsset &&
|
||||||
|
bufferConfig == other.bufferConfig
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +165,8 @@ class Source {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "Source"
|
private const val TAG = "Source"
|
||||||
private const val PROP_SRC_URI = "uri"
|
private const val PROP_SRC_URI = "uri"
|
||||||
|
private const val PROP_SRC_IS_LOCAL_ASSET_FILE = "isLocalAssetFile"
|
||||||
|
private const val PROP_SRC_IS_ASSET = "isAsset"
|
||||||
private const val PROP_SRC_START_POSITION = "startPosition"
|
private const val PROP_SRC_START_POSITION = "startPosition"
|
||||||
private const val PROP_SRC_CROP_START = "cropStart"
|
private const val PROP_SRC_CROP_START = "cropStart"
|
||||||
private const val PROP_SRC_CROP_END = "cropEnd"
|
private const val PROP_SRC_CROP_END = "cropEnd"
|
||||||
@@ -164,6 +180,7 @@ class Source {
|
|||||||
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
|
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
|
||||||
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
|
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
|
||||||
private const val PROP_SRC_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
|
private const val PROP_SRC_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount"
|
||||||
|
private const val PROP_SRC_BUFFER_CONFIG = "bufferConfig"
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
|
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
|
||||||
@@ -216,6 +233,8 @@ class Source {
|
|||||||
}
|
}
|
||||||
source.uriString = uriString
|
source.uriString = uriString
|
||||||
source.uri = uri
|
source.uri = uri
|
||||||
|
source.isLocalAssetFile = safeGetBool(src, PROP_SRC_IS_LOCAL_ASSET_FILE, false)
|
||||||
|
source.isAsset = safeGetBool(src, PROP_SRC_IS_ASSET, false)
|
||||||
source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
|
source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
|
||||||
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
|
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
|
||||||
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
|
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
|
||||||
@@ -229,6 +248,8 @@ class Source {
|
|||||||
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
|
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
|
||||||
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))
|
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))
|
||||||
source.minLoadRetryCount = safeGetInt(src, PROP_SRC_MIN_LOAD_RETRY_COUNT, 3)
|
source.minLoadRetryCount = safeGetInt(src, PROP_SRC_MIN_LOAD_RETRY_COUNT, 3)
|
||||||
|
source.bufferConfig = BufferConfig.parse(safeGetMap(src, PROP_SRC_BUFFER_CONFIG))
|
||||||
|
|
||||||
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
|
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
|
||||||
if (propSrcHeadersArray != null) {
|
if (propSrcHeadersArray != null) {
|
||||||
if (propSrcHeadersArray.size() > 0) {
|
if (propSrcHeadersArray.size() > 0) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ enum class EventTypes(val eventName: String) {
|
|||||||
EVENT_BANDWIDTH("onVideoBandwidthUpdate"),
|
EVENT_BANDWIDTH("onVideoBandwidthUpdate"),
|
||||||
EVENT_CONTROLS_VISIBILITY_CHANGE("onControlsVisibilityChange"),
|
EVENT_CONTROLS_VISIBILITY_CHANGE("onControlsVisibilityChange"),
|
||||||
EVENT_SEEK("onVideoSeek"),
|
EVENT_SEEK("onVideoSeek"),
|
||||||
|
EVENT_SEEK_COMPLETE("onVideoSeekComplete"),
|
||||||
EVENT_END("onVideoEnd"),
|
EVENT_END("onVideoEnd"),
|
||||||
EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"),
|
EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"),
|
||||||
EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"),
|
EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"),
|
||||||
@@ -42,7 +43,8 @@ enum class EventTypes(val eventName: String) {
|
|||||||
|
|
||||||
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
|
EVENT_TEXT_TRACK_DATA_CHANGED("onTextTrackDataChanged"),
|
||||||
EVENT_VIDEO_TRACKS("onVideoTracks"),
|
EVENT_VIDEO_TRACKS("onVideoTracks"),
|
||||||
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent");
|
EVENT_ON_RECEIVE_AD_EVENT("onReceiveAdEvent"),
|
||||||
|
EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED("onPictureInPictureStatusChanged");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun toMap() =
|
fun toMap() =
|
||||||
@@ -71,6 +73,7 @@ class VideoEventEmitter {
|
|||||||
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
|
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
|
||||||
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
|
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
|
||||||
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
|
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
|
||||||
|
lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit
|
||||||
lateinit var onVideoEnd: () -> Unit
|
lateinit var onVideoEnd: () -> Unit
|
||||||
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
|
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
|
||||||
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
|
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
|
||||||
@@ -90,6 +93,7 @@ class VideoEventEmitter {
|
|||||||
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
|
lateinit var onVideoTracks: (videoTracks: ArrayList<VideoTrack>?) -> Unit
|
||||||
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
|
lateinit var onTextTrackDataChanged: (textTrackData: String) -> Unit
|
||||||
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
|
lateinit var onReceiveAdEvent: (adEvent: String, adData: Map<String?, String?>?) -> Unit
|
||||||
|
lateinit var onPictureInPictureStatusChanged: (isActive: Boolean) -> Unit
|
||||||
|
|
||||||
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
|
fun addEventEmitters(reactContext: ThemedReactContext, view: ReactExoplayerView) {
|
||||||
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
||||||
@@ -174,6 +178,11 @@ class VideoEventEmitter {
|
|||||||
putDouble("seekTime", seekTime / 1000.0)
|
putDouble("seekTime", seekTime / 1000.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onVideoSeekComplete = { currentPosition ->
|
||||||
|
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
|
||||||
|
putDouble("currentTime", currentPosition / 1000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
onVideoEnd = {
|
onVideoEnd = {
|
||||||
event.dispatch(EventTypes.EVENT_END)
|
event.dispatch(EventTypes.EVENT_END)
|
||||||
}
|
}
|
||||||
@@ -278,6 +287,11 @@ class VideoEventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onPictureInPictureStatusChanged = { isActive ->
|
||||||
|
event.dispatch(EventTypes.EVENT_PICTURE_IN_PICTURE_STATUS_CHANGED) {
|
||||||
|
putBoolean("isActive", isActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ class ExoPlayerView(private val context: Context) :
|
|||||||
FrameLayout(context, null, 0),
|
FrameLayout(context, null, 0),
|
||||||
AdViewProvider {
|
AdViewProvider {
|
||||||
|
|
||||||
private var surfaceView: View? = null
|
var surfaceView: View? = null
|
||||||
|
private set
|
||||||
private var shutterView: View
|
private var shutterView: View
|
||||||
private var subtitleLayout: SubtitleView
|
private var subtitleLayout: SubtitleView
|
||||||
private var layout: AspectRatioFrameLayout
|
private var layout: AspectRatioFrameLayout
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.app.Dialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
@@ -125,6 +126,14 @@ class FullScreenPlayerView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideWithoutPlayer() {
|
||||||
|
for (i in 0 until containerView.childCount) {
|
||||||
|
if (containerView.getChildAt(i) !== exoPlayerView) {
|
||||||
|
containerView.getChildAt(i).visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
|
private fun getFullscreenIconResource(isFullscreen: Boolean): Int =
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
|
androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AppOpsManager
|
||||||
|
import android.app.PictureInPictureParams
|
||||||
|
import android.app.RemoteAction
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Rational
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.AppOpsManagerCompat
|
||||||
|
import androidx.core.app.PictureInPictureModeChangedInfo
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import com.brentvatne.common.toolbox.DebugLog
|
||||||
|
import com.brentvatne.receiver.PictureInPictureReceiver
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
|
|
||||||
|
internal fun Context.findActivity(): ComponentActivity {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper) {
|
||||||
|
if (context is ComponentActivity) return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Picture in picture should be called in the context of an Activity")
|
||||||
|
}
|
||||||
|
|
||||||
|
object PictureInPictureUtil {
|
||||||
|
private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000
|
||||||
|
private const val TAG = "PictureInPictureUtil"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun addLifecycleEventListener(context: ThemedReactContext, view: ReactExoplayerView): Runnable {
|
||||||
|
val activity = context.findActivity()
|
||||||
|
|
||||||
|
val onPictureInPictureModeChanged: (info: PictureInPictureModeChangedInfo) -> Unit = { info: PictureInPictureModeChangedInfo ->
|
||||||
|
view.setIsInPictureInPicture(info.isInPictureInPictureMode)
|
||||||
|
if (!info.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||||
|
// when user click close button of PIP
|
||||||
|
if (!view.playInBackground) view.setPausedModifier(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onUserLeaveHintCallback = {
|
||||||
|
if (view.enterPictureInPictureOnLeave) {
|
||||||
|
view.enterPictureInPictureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.addOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
activity.addOnUserLeaveHintListener(onUserLeaveHintCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO convert to lambda when ReactExoplayerView migrated
|
||||||
|
return object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
context.findActivity().removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChanged)
|
||||||
|
context.findActivity().removeOnUserLeaveHintListener(onUserLeaveHintCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) {
|
||||||
|
if (!isSupportPictureInPicture(context)) return
|
||||||
|
if (isSupportPictureInPictureAction() && pictureInPictureParams != null) {
|
||||||
|
try {
|
||||||
|
context.findActivity().enterPictureInPictureMode(pictureInPictureParams)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
DebugLog.e(TAG, e.toString())
|
||||||
|
}
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
try {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.findActivity().enterPictureInPictureMode()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
DebugLog.e(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun applyPlayingStatus(
|
||||||
|
context: ThemedReactContext,
|
||||||
|
pipParamsBuilder: PictureInPictureParams.Builder?,
|
||||||
|
receiver: PictureInPictureReceiver,
|
||||||
|
isPaused: Boolean
|
||||||
|
) {
|
||||||
|
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
val actions = getPictureInPictureActions(context, isPaused, receiver)
|
||||||
|
pipParamsBuilder.setActions(actions)
|
||||||
|
updatePictureInPictureActions(context, pipParamsBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun applyAutoEnterEnabled(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, autoEnterEnabled: Boolean) {
|
||||||
|
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
|
||||||
|
pipParamsBuilder.setAutoEnterEnabled(autoEnterEnabled)
|
||||||
|
updatePictureInPictureActions(context, pipParamsBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun applySourceRectHint(context: ThemedReactContext, pipParamsBuilder: PictureInPictureParams.Builder?, playerView: ExoPlayerView) {
|
||||||
|
if (pipParamsBuilder == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
pipParamsBuilder.setSourceRectHint(calcRectHint(playerView))
|
||||||
|
updatePictureInPictureActions(context, pipParamsBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePictureInPictureActions(context: ThemedReactContext, pipParams: PictureInPictureParams) {
|
||||||
|
if (!isSupportPictureInPictureAction()) return
|
||||||
|
if (!isSupportPictureInPicture(context)) return
|
||||||
|
try {
|
||||||
|
context.findActivity().setPictureInPictureParams(pipParams)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
DebugLog.e(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun getPictureInPictureActions(context: ThemedReactContext, isPaused: Boolean, receiver: PictureInPictureReceiver): ArrayList<RemoteAction> {
|
||||||
|
val intent = receiver.getPipActionIntent(isPaused)
|
||||||
|
val resource =
|
||||||
|
if (isPaused) androidx.media3.ui.R.drawable.exo_icon_play else androidx.media3.ui.R.drawable.exo_icon_pause
|
||||||
|
val icon = Icon.createWithResource(context, resource)
|
||||||
|
val title = if (isPaused) "play" else "pause"
|
||||||
|
return arrayListOf(RemoteAction(icon, title, title, intent))
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun calcRectHint(playerView: ExoPlayerView): Rect {
|
||||||
|
val hint = Rect()
|
||||||
|
playerView.surfaceView?.getGlobalVisibleRect(hint)
|
||||||
|
val location = IntArray(2)
|
||||||
|
playerView.surfaceView?.getLocationOnScreen(location)
|
||||||
|
|
||||||
|
val height = hint.bottom - hint.top
|
||||||
|
hint.top = location[1]
|
||||||
|
hint.bottom = hint.top + height
|
||||||
|
return hint
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun calcPictureInPictureAspectRatio(player: ExoPlayer): Rational {
|
||||||
|
var aspectRatio = Rational(player.videoSize.width, player.videoSize.height)
|
||||||
|
// AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
|
||||||
|
// https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
|
||||||
|
val maximumRatio = Rational(239, 100)
|
||||||
|
val minimumRatio = Rational(100, 239)
|
||||||
|
if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
|
||||||
|
aspectRatio = maximumRatio
|
||||||
|
} else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
|
||||||
|
aspectRatio = minimumRatio
|
||||||
|
}
|
||||||
|
return aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSupportPictureInPicture(context: ThemedReactContext): Boolean =
|
||||||
|
checkIsApiSupport() && checkIsSystemSupportPIP(context) && checkIsUserAllowPIP(context)
|
||||||
|
|
||||||
|
private fun isSupportPictureInPictureAction(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||||
|
|
||||||
|
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
|
||||||
|
private fun checkIsApiSupport(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
private fun checkIsSystemSupportPIP(context: ThemedReactContext): Boolean {
|
||||||
|
val activity = context.findActivity() ?: return false
|
||||||
|
|
||||||
|
val activityInfo = activity.packageManager.getActivityInfo(activity.componentName, PackageManager.GET_META_DATA)
|
||||||
|
// detect current activity's android:supportsPictureInPicture value defined within AndroidManifest.xml
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/ActivityInfo.java;l=1090-1093;drc=7651f0a4c059a98f32b0ba30cd64500bf135385f
|
||||||
|
val isActivitySupportPip = activityInfo.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE != 0
|
||||||
|
|
||||||
|
// PIP might be disabled on devices that have low RAM.
|
||||||
|
val isPipAvailable = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||||
|
|
||||||
|
return isActivitySupportPip && isPipAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkIsUserAllowPIP(context: ThemedReactContext): Boolean {
|
||||||
|
val activity = context.currentActivity ?: return false
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
val result = AppOpsManagerCompat.noteOpNoThrow(
|
||||||
|
activity,
|
||||||
|
AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
|
||||||
|
Process.myUid(),
|
||||||
|
activity.packageName
|
||||||
|
)
|
||||||
|
AppOpsManager.MODE_ALLOWED == result
|
||||||
|
} else {
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import static androidx.media3.common.C.TIME_END_OF_SOURCE;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
|
import android.app.PictureInPictureParams;
|
||||||
|
import android.app.RemoteAction;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -23,7 +25,9 @@ import android.os.IBinder;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.view.accessibility.CaptioningManager;
|
import android.view.accessibility.CaptioningManager;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -122,6 +126,7 @@ import com.brentvatne.react.R;
|
|||||||
import com.brentvatne.react.ReactNativeVideoManager;
|
import com.brentvatne.react.ReactNativeVideoManager;
|
||||||
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
|
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
|
||||||
import com.brentvatne.receiver.BecomingNoisyListener;
|
import com.brentvatne.receiver.BecomingNoisyListener;
|
||||||
|
import com.brentvatne.receiver.PictureInPictureReceiver;
|
||||||
import com.facebook.react.bridge.LifecycleEventListener;
|
import com.facebook.react.bridge.LifecycleEventListener;
|
||||||
import com.facebook.react.bridge.Promise;
|
import com.facebook.react.bridge.Promise;
|
||||||
import com.facebook.react.bridge.UiThreadUtil;
|
import com.facebook.react.bridge.UiThreadUtil;
|
||||||
@@ -202,19 +207,22 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
private boolean isPaused;
|
private boolean isPaused;
|
||||||
private boolean isBuffering;
|
private boolean isBuffering;
|
||||||
private boolean muted = false;
|
private boolean muted = false;
|
||||||
|
public boolean enterPictureInPictureOnLeave = false;
|
||||||
|
private PictureInPictureParams.Builder pictureInPictureParamsBuilder;
|
||||||
private boolean hasAudioFocus = false;
|
private boolean hasAudioFocus = false;
|
||||||
private float rate = 1f;
|
private float rate = 1f;
|
||||||
private AudioOutput audioOutput = AudioOutput.SPEAKER;
|
private AudioOutput audioOutput = AudioOutput.SPEAKER;
|
||||||
private float audioVolume = 1f;
|
private float audioVolume = 1f;
|
||||||
private BufferConfig bufferConfig = new BufferConfig();
|
|
||||||
private int maxBitRate = 0;
|
private int maxBitRate = 0;
|
||||||
private boolean hasDrmFailed = false;
|
private boolean hasDrmFailed = false;
|
||||||
private boolean isUsingContentResolution = false;
|
private boolean isUsingContentResolution = false;
|
||||||
private boolean selectTrackWhenReady = false;
|
private boolean selectTrackWhenReady = false;
|
||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private Runnable mainRunnable;
|
private Runnable mainRunnable;
|
||||||
|
private Runnable pipListenerUnsubscribe;
|
||||||
private boolean useCache = false;
|
private boolean useCache = false;
|
||||||
private ControlsConfig controlsConfig = new ControlsConfig();
|
private ControlsConfig controlsConfig = new ControlsConfig();
|
||||||
|
private ArrayList<Integer> rootViewChildrenOriginalVisibility = new ArrayList<Integer>();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
|
* When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
|
||||||
@@ -222,6 +230,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
*/
|
*/
|
||||||
private boolean isSeeking = false;
|
private boolean isSeeking = false;
|
||||||
private long seekPosition = -1;
|
private long seekPosition = -1;
|
||||||
|
private boolean isSeekInProgress = false;
|
||||||
|
|
||||||
// Props from React
|
// Props from React
|
||||||
private Source source = new Source();
|
private Source source = new Source();
|
||||||
@@ -238,7 +247,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
private boolean disableDisconnectError;
|
private boolean disableDisconnectError;
|
||||||
private boolean preventsDisplaySleepDuringVideoPlayback = true;
|
private boolean preventsDisplaySleepDuringVideoPlayback = true;
|
||||||
private float mProgressUpdateInterval = 250.0f;
|
private float mProgressUpdateInterval = 250.0f;
|
||||||
private boolean playInBackground = false;
|
protected boolean playInBackground = false;
|
||||||
private boolean mReportBandwidth = false;
|
private boolean mReportBandwidth = false;
|
||||||
private boolean controls;
|
private boolean controls;
|
||||||
|
|
||||||
@@ -249,6 +258,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
private final ThemedReactContext themedReactContext;
|
private final ThemedReactContext themedReactContext;
|
||||||
private final AudioManager audioManager;
|
private final AudioManager audioManager;
|
||||||
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
|
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
|
||||||
|
private final PictureInPictureReceiver pictureInPictureReceiver;
|
||||||
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||||
|
|
||||||
// store last progress event values to avoid sending unnecessary messages
|
// store last progress event values to avoid sending unnecessary messages
|
||||||
@@ -301,6 +311,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private void handleSeekCompletion() {
|
||||||
|
if (player != null && player.getPlaybackState() == Player.STATE_READY && isSeekInProgress) {
|
||||||
|
Log.d("ReactExoplayerView", "handleSeekCompletion: currentPosition=" + player.getCurrentPosition());
|
||||||
|
eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition());
|
||||||
|
isSeeking = false;
|
||||||
|
seekPosition = -1;
|
||||||
|
isSeekInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) {
|
public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) {
|
||||||
Timeline.Window window = new Timeline.Window();
|
Timeline.Window window = new Timeline.Window();
|
||||||
if(!player.getCurrentTimeline().isEmpty()) {
|
if(!player.getCurrentTimeline().isEmpty()) {
|
||||||
@@ -315,13 +335,19 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
this.eventEmitter = new VideoEventEmitter();
|
this.eventEmitter = new VideoEventEmitter();
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.bandwidthMeter = config.getBandwidthMeter();
|
this.bandwidthMeter = config.getBandwidthMeter();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pictureInPictureParamsBuilder == null) {
|
||||||
|
this.pictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
|
||||||
|
}
|
||||||
mainHandler = new Handler();
|
mainHandler = new Handler();
|
||||||
|
|
||||||
createViews();
|
createViews();
|
||||||
|
|
||||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
themedReactContext.addLifecycleEventListener(this);
|
themedReactContext.addLifecycleEventListener(this);
|
||||||
|
pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(context, this);
|
||||||
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
|
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
|
||||||
audioFocusChangeListener = new OnAudioFocusChangedListener(this, themedReactContext);
|
audioFocusChangeListener = new OnAudioFocusChangedListener(this, themedReactContext);
|
||||||
|
pictureInPictureReceiver = new PictureInPictureReceiver(this, themedReactContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPlayingAd() {
|
private boolean isPlayingAd() {
|
||||||
@@ -337,12 +363,21 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
LayoutParams.MATCH_PARENT,
|
LayoutParams.MATCH_PARENT,
|
||||||
LayoutParams.MATCH_PARENT);
|
LayoutParams.MATCH_PARENT);
|
||||||
exoPlayerView = new ExoPlayerView(getContext());
|
exoPlayerView = new ExoPlayerView(getContext());
|
||||||
|
exoPlayerView.addOnLayoutChangeListener( (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) ->
|
||||||
|
PictureInPictureUtil.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView)
|
||||||
|
);
|
||||||
exoPlayerView.setLayoutParams(layoutParams);
|
exoPlayerView.setLayoutParams(layoutParams);
|
||||||
addView(exoPlayerView, 0, layoutParams);
|
addView(exoPlayerView, 0, layoutParams);
|
||||||
|
|
||||||
exoPlayerView.setFocusable(this.focusable);
|
exoPlayerView.setFocusable(this.focusable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
cleanupPlaybackService();
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
}
|
||||||
|
|
||||||
// LifecycleEventListener implementation
|
// LifecycleEventListener implementation
|
||||||
@Override
|
@Override
|
||||||
public void onHostResume() {
|
public void onHostResume() {
|
||||||
@@ -355,7 +390,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
@Override
|
@Override
|
||||||
public void onHostPause() {
|
public void onHostPause() {
|
||||||
isInBackground = true;
|
isInBackground = true;
|
||||||
if (playInBackground) {
|
Activity activity = themedReactContext.getCurrentActivity();
|
||||||
|
boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode();
|
||||||
|
boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode();
|
||||||
|
if (playInBackground || isInPictureInPicture || isInMultiWindowMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPlayWhenReady(false);
|
setPlayWhenReady(false);
|
||||||
@@ -366,12 +404,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
cleanUpResources();
|
cleanUpResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDetachedFromWindow() {
|
|
||||||
cleanupPlaybackService();
|
|
||||||
super.onDetachedFromWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cleanUpResources() {
|
public void cleanUpResources() {
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
themedReactContext.removeLifecycleEventListener(this);
|
themedReactContext.removeLifecycleEventListener(this);
|
||||||
@@ -691,7 +723,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
runtime = Runtime.getRuntime();
|
runtime = Runtime.getRuntime();
|
||||||
ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
|
ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
|
||||||
double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
||||||
? bufferConfig.getMaxHeapAllocationPercent()
|
? config.getMaxHeapAllocationPercent()
|
||||||
: DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
: DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
||||||
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
|
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
|
||||||
}
|
}
|
||||||
@@ -710,8 +742,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||||
long freeMemory = runtime.maxMemory() - usedMemory;
|
long freeMemory = runtime.maxMemory() - usedMemory;
|
||||||
double minBufferMemoryReservePercent = bufferConfig.getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
double minBufferMemoryReservePercent = source.getBufferConfig().getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
||||||
? bufferConfig.getMinBufferMemoryReservePercent()
|
? source.getBufferConfig().getMinBufferMemoryReservePercent()
|
||||||
: ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
: ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
||||||
long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory();
|
long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory();
|
||||||
long bufferedMs = bufferedDurationUs / (long) 1000;
|
long bufferedMs = bufferedDurationUs / (long) 1000;
|
||||||
@@ -743,10 +775,20 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (runningSource.getUri() == null) {
|
if (runningSource.getUri() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
// Initialize core configuration and listeners
|
// Initialize core configuration and listeners
|
||||||
initializePlayerCore(self);
|
initializePlayerCore(self);
|
||||||
}
|
}
|
||||||
|
if (!source.isLocalAssetFile() && !source.isAsset() && source.getBufferConfig().getCacheSize() > 0) {
|
||||||
|
RNVSimpleCache.INSTANCE.setSimpleCache(
|
||||||
|
this.getContext(),
|
||||||
|
source.getBufferConfig().getCacheSize()
|
||||||
|
);
|
||||||
|
useCache = true;
|
||||||
|
} else {
|
||||||
|
useCache = false;
|
||||||
|
}
|
||||||
if (playerNeedsSource) {
|
if (playerNeedsSource) {
|
||||||
// Will force display of shutter view if needed
|
// Will force display of shutter view if needed
|
||||||
exoPlayerView.updateShutterViewVisibility();
|
exoPlayerView.updateShutterViewVisibility();
|
||||||
@@ -813,7 +855,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
RNVLoadControl loadControl = new RNVLoadControl(
|
RNVLoadControl loadControl = new RNVLoadControl(
|
||||||
allocator,
|
allocator,
|
||||||
bufferConfig
|
source.getBufferConfig()
|
||||||
);
|
);
|
||||||
DefaultRenderersFactory renderersFactory =
|
DefaultRenderersFactory renderersFactory =
|
||||||
new DefaultRenderersFactory(getContext())
|
new DefaultRenderersFactory(getContext())
|
||||||
@@ -833,7 +875,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
.setBandwidthMeter(bandwidthMeter)
|
.setBandwidthMeter(bandwidthMeter)
|
||||||
.setLoadControl(loadControl)
|
.setLoadControl(loadControl)
|
||||||
.setMediaSourceFactory(mediaSourceFactory)
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
.build();
|
.build();
|
||||||
|
player.addListener(self);
|
||||||
ReactNativeVideoManager.Companion.getInstance().onInstanceCreated(instanceId, player);
|
ReactNativeVideoManager.Companion.getInstance().onInstanceCreated(instanceId, player);
|
||||||
refreshDebugState();
|
refreshDebugState();
|
||||||
player.addListener(self);
|
player.addListener(self);
|
||||||
@@ -841,6 +884,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
exoPlayerView.setPlayer(player);
|
exoPlayerView.setPlayer(player);
|
||||||
|
|
||||||
audioBecomingNoisyReceiver.setListener(self);
|
audioBecomingNoisyReceiver.setListener(self);
|
||||||
|
pictureInPictureReceiver.setListener();
|
||||||
bandwidthMeter.addEventListener(new Handler(), self);
|
bandwidthMeter.addEventListener(new Handler(), self);
|
||||||
setPlayWhenReady(!isPaused);
|
setPlayWhenReady(!isPaused);
|
||||||
playerNeedsSource = true;
|
playerNeedsSource = true;
|
||||||
@@ -1119,7 +1163,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(bufferConfig);
|
MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(source.getBufferConfig());
|
||||||
mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());
|
mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());
|
||||||
|
|
||||||
MediaSource.Factory mediaSourceFactory;
|
MediaSource.Factory mediaSourceFactory;
|
||||||
@@ -1177,7 +1221,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri);
|
DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri);
|
||||||
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException("cannot open input file" + uri);
|
throw new IllegalStateException("cannot open input file:" + uri);
|
||||||
}
|
}
|
||||||
} else if ("file".equals(uri.getScheme()) ||
|
} else if ("file".equals(uri.getScheme()) ||
|
||||||
!useCache) {
|
!useCache) {
|
||||||
@@ -1265,6 +1309,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
updateResumePosition();
|
updateResumePosition();
|
||||||
player.release();
|
player.release();
|
||||||
player.removeListener(this);
|
player.removeListener(this);
|
||||||
|
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, false);
|
||||||
|
if (pipListenerUnsubscribe != null) {
|
||||||
|
new Handler().post(pipListenerUnsubscribe);
|
||||||
|
}
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
|
|
||||||
ReactNativeVideoManager.Companion.getInstance().onInstanceRemoved(instanceId, player);
|
ReactNativeVideoManager.Companion.getInstance().onInstanceRemoved(instanceId, player);
|
||||||
@@ -1277,6 +1325,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
progressHandler.removeMessages(SHOW_PROGRESS);
|
progressHandler.removeMessages(SHOW_PROGRESS);
|
||||||
audioBecomingNoisyReceiver.removeListener();
|
audioBecomingNoisyReceiver.removeListener();
|
||||||
|
pictureInPictureReceiver.removeListener();
|
||||||
bandwidthMeter.removeEventListener(this);
|
bandwidthMeter.removeEventListener(this);
|
||||||
|
|
||||||
if (mainHandler != null && mainRunnable != null) {
|
if (mainHandler != null && mainRunnable != null) {
|
||||||
@@ -1442,6 +1491,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
||||||
int playbackState = player.getPlaybackState();
|
int playbackState = player.getPlaybackState();
|
||||||
boolean playWhenReady = player.getPlayWhenReady();
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
|
Log.d("ReactExoplayerView", "onEvents: playbackState=" + playbackState + ", playWhenReady=" + playWhenReady);
|
||||||
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
||||||
eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
||||||
switch (playbackState) {
|
switch (playbackState) {
|
||||||
@@ -1475,6 +1525,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
playerControlView.show();
|
playerControlView.show();
|
||||||
}
|
}
|
||||||
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
|
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
|
||||||
|
Log.d("ReactExoplayerView", "Player STATE_READY: currentPosition=" + player.getCurrentPosition());
|
||||||
|
if (isSeekInProgress) {
|
||||||
|
handleSeekCompletion();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Player.STATE_ENDED:
|
case Player.STATE_ENDED:
|
||||||
text += "ended";
|
text += "ended";
|
||||||
@@ -1739,6 +1793,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
|
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
|
||||||
|
Log.d("ReactExoplayerView", "onPositionDiscontinuity: reason=" + reason + ", oldPosition=" + oldPosition.positionMs + ", newPosition=" + newPosition.positionMs);
|
||||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
isSeeking = true;
|
isSeeking = true;
|
||||||
seekPosition = newPosition.positionMs;
|
seekPosition = newPosition.positionMs;
|
||||||
@@ -1795,7 +1850,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (isPlaying && isSeeking) {
|
if (isPlaying && isSeeking) {
|
||||||
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
||||||
}
|
}
|
||||||
|
PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, pictureInPictureReceiver, !isPlaying);
|
||||||
eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);
|
eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@@ -1890,8 +1945,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReactExoplayerViewManager public api
|
|
||||||
|
|
||||||
public void setSrc(Source source) {
|
public void setSrc(Source source) {
|
||||||
if (source.getUri() != null) {
|
if (source.getUri() != null) {
|
||||||
clearResumePosition();
|
clearResumePosition();
|
||||||
@@ -2209,6 +2262,84 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) {
|
||||||
|
this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && enterPictureInPictureOnLeave;
|
||||||
|
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setIsInPictureInPicture(boolean isInPictureInPicture) {
|
||||||
|
eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture);
|
||||||
|
|
||||||
|
if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) {
|
||||||
|
if (isInPictureInPicture) fullScreenPlayerView.hideWithoutPlayer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity currentActivity = themedReactContext.getCurrentActivity();
|
||||||
|
if (currentActivity == null) return;
|
||||||
|
|
||||||
|
View decorView = currentActivity.getWindow().getDecorView();
|
||||||
|
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
||||||
|
|
||||||
|
LayoutParams layoutParams = new LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.MATCH_PARENT);
|
||||||
|
|
||||||
|
if (isInPictureInPicture) {
|
||||||
|
ViewGroup parent = (ViewGroup)exoPlayerView.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(exoPlayerView);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||||
|
if (rootView.getChildAt(i) != exoPlayerView) {
|
||||||
|
rootViewChildrenOriginalVisibility.add(rootView.getChildAt(i).getVisibility());
|
||||||
|
rootView.getChildAt(i).setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootView.addView(exoPlayerView, layoutParams);
|
||||||
|
} else {
|
||||||
|
rootView.removeView(exoPlayerView);
|
||||||
|
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
||||||
|
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||||
|
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
||||||
|
}
|
||||||
|
addView(exoPlayerView, 0, layoutParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enterPictureInPictureMode() {
|
||||||
|
PictureInPictureParams _pipParams = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
ArrayList<RemoteAction> actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver);
|
||||||
|
pictureInPictureParamsBuilder.setActions(actions);
|
||||||
|
_pipParams = pictureInPictureParamsBuilder
|
||||||
|
.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
PictureInPictureUtil.enterPictureInPictureMode(themedReactContext, _pipParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exitPictureInPictureMode() {
|
||||||
|
Activity currentActivity = themedReactContext.getCurrentActivity();
|
||||||
|
if (currentActivity == null) return;
|
||||||
|
|
||||||
|
View decorView = currentActivity.getWindow().getDecorView();
|
||||||
|
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
||||||
|
|
||||||
|
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
||||||
|
if (exoPlayerView.getParent().equals(rootView)) rootView.removeView(exoPlayerView);
|
||||||
|
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||||
|
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
||||||
|
}
|
||||||
|
rootViewChildrenOriginalVisibility.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && currentActivity.isInPictureInPictureMode()) {
|
||||||
|
currentActivity.moveTaskToBack(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setMutedModifier(boolean muted) {
|
public void setMutedModifier(boolean muted) {
|
||||||
this.muted = muted;
|
this.muted = muted;
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
@@ -2250,6 +2381,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
public void seekTo(long positionMs) {
|
public void seekTo(long positionMs) {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
|
Log.d("ReactExoplayerView", "seekTo: positionMs=" + positionMs);
|
||||||
|
isSeekInProgress = true;
|
||||||
|
isSeeking = true;
|
||||||
|
seekPosition = positionMs;
|
||||||
player.seekTo(positionMs);
|
player.seekTo(positionMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2368,21 +2503,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
exoPlayerView.setHideShutterView(hideShutterView);
|
exoPlayerView.setHideShutterView(hideShutterView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBufferConfig(BufferConfig config) {
|
|
||||||
bufferConfig = config;
|
|
||||||
if (bufferConfig.getCacheSize() > 0) {
|
|
||||||
RNVSimpleCache.INSTANCE.setSimpleCache(
|
|
||||||
this.getContext(),
|
|
||||||
bufferConfig.getCacheSize()
|
|
||||||
);
|
|
||||||
useCache = true;
|
|
||||||
} else {
|
|
||||||
useCache = false;
|
|
||||||
}
|
|
||||||
releasePlayer();
|
|
||||||
initializePlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||||
DebugLog.d("DRM Info", "onDrmKeysLoaded");
|
DebugLog.d("DRM Info", "onDrmKeysLoaded");
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.brentvatne.exoplayer
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.brentvatne.common.api.BufferConfig
|
|
||||||
import com.brentvatne.common.api.BufferingStrategy
|
import com.brentvatne.common.api.BufferingStrategy
|
||||||
import com.brentvatne.common.api.ControlsConfig
|
import com.brentvatne.common.api.ControlsConfig
|
||||||
import com.brentvatne.common.api.ResizeMode
|
import com.brentvatne.common.api.ResizeMode
|
||||||
@@ -33,10 +32,10 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
|
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
|
||||||
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
|
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
|
||||||
private const val PROP_PAUSED = "paused"
|
private const val PROP_PAUSED = "paused"
|
||||||
|
private const val PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE = "enterPictureInPictureOnLeave"
|
||||||
private const val PROP_MUTED = "muted"
|
private const val PROP_MUTED = "muted"
|
||||||
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
||||||
private const val PROP_VOLUME = "volume"
|
private const val PROP_VOLUME = "volume"
|
||||||
private const val PROP_BUFFER_CONFIG = "bufferConfig"
|
|
||||||
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
|
private const val PROP_PREVENTS_DISPLAY_SLEEP_DURING_VIDEO_PLAYBACK =
|
||||||
"preventsDisplaySleepDuringVideoPlayback"
|
"preventsDisplaySleepDuringVideoPlayback"
|
||||||
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
|
private const val PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval"
|
||||||
@@ -71,6 +70,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
|
|
||||||
override fun onDropViewInstance(view: ReactExoplayerView) {
|
override fun onDropViewInstance(view: ReactExoplayerView) {
|
||||||
view.cleanUpResources()
|
view.cleanUpResources()
|
||||||
|
view.exitPictureInPictureMode()
|
||||||
ReactNativeVideoManager.getInstance().unregisterView(this)
|
ReactNativeVideoManager.getInstance().unregisterView(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +156,11 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setMutedModifier(muted)
|
videoView.setMutedModifier(muted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = PROP_ENTER_PICTURE_IN_PICTURE_ON_LEAVE, defaultBoolean = false)
|
||||||
|
fun setEnterPictureInPictureOnLeave(videoView: ReactExoplayerView, enterPictureInPictureOnLeave: Boolean) {
|
||||||
|
videoView.setEnterPictureInPictureOnLeave(enterPictureInPictureOnLeave)
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = PROP_AUDIO_OUTPUT)
|
@ReactProp(name = PROP_AUDIO_OUTPUT)
|
||||||
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
|
fun setAudioOutput(videoView: ReactExoplayerView, audioOutput: String) {
|
||||||
videoView.setAudioOutput(AudioOutput.get(audioOutput))
|
videoView.setAudioOutput(AudioOutput.get(audioOutput))
|
||||||
@@ -242,12 +247,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setShutterColor(color)
|
videoView.setShutterColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactProp(name = PROP_BUFFER_CONFIG)
|
|
||||||
fun setBufferConfig(videoView: ReactExoplayerView, bufferConfig: ReadableMap?) {
|
|
||||||
val config = BufferConfig.parse(bufferConfig)
|
|
||||||
videoView.setBufferConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
|
@ReactProp(name = PROP_SHOW_NOTIFICATION_CONTROLS)
|
||||||
fun setShowNotificationControls(videoView: ReactExoplayerView, showNotificationControls: Boolean) {
|
fun setShowNotificationControls(videoView: ReactExoplayerView, showNotificationControls: Boolean) {
|
||||||
videoView.setShowNotificationControls(showNotificationControls)
|
videoView.setShowNotificationControls(showNotificationControls)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.brentvatne.react
|
package com.brentvatne.react
|
||||||
|
|
||||||
import com.brentvatne.common.toolbox.DebugLog
|
import com.brentvatne.common.toolbox.DebugLog
|
||||||
import com.brentvatne.exoplayer.ReactExoplayerViewManager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
|
* ReactNativeVideoManager is a singleton class which allows to manipulate / the global state of the app
|
||||||
@@ -23,13 +22,13 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var instanceList: ArrayList<ReactExoplayerViewManager> = ArrayList()
|
private var instanceList: ArrayList<Any> = ArrayList()
|
||||||
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
|
private var pluginList: ArrayList<RNVPlugin> = ArrayList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* register a new ReactExoplayerViewManager in the managed list
|
* register a new ReactExoplayerViewManager in the managed list
|
||||||
*/
|
*/
|
||||||
fun registerView(newInstance: ReactExoplayerViewManager) {
|
fun registerView(newInstance: Any) {
|
||||||
if (instanceList.size > 2) {
|
if (instanceList.size > 2) {
|
||||||
DebugLog.d(TAG, "multiple Video displayed ?")
|
DebugLog.d(TAG, "multiple Video displayed ?")
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ class ReactNativeVideoManager : RNVPlugin {
|
|||||||
/**
|
/**
|
||||||
* unregister existing ReactExoplayerViewManager in the managed list
|
* unregister existing ReactExoplayerViewManager in the managed list
|
||||||
*/
|
*/
|
||||||
fun unregisterView(newInstance: ReactExoplayerViewManager) {
|
fun unregisterView(newInstance: Any) {
|
||||||
instanceList.remove(newInstance)
|
instanceList.remove(newInstance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,20 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun enterPictureInPictureCmd(reactTag: Int) {
|
||||||
|
performOnPlayerView(reactTag) {
|
||||||
|
it?.enterPictureInPictureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun exitPictureInPictureCmd(reactTag: Int) {
|
||||||
|
performOnPlayerView(reactTag) {
|
||||||
|
it?.exitPictureInPictureMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
|
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
|
||||||
performOnPlayerView(reactTag) {
|
performOnPlayerView(reactTag) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.brentvatne.receiver
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.brentvatne.exoplayer.ReactExoplayerView
|
||||||
|
import com.facebook.react.uimanager.ThemedReactContext
|
||||||
|
|
||||||
|
class PictureInPictureReceiver(private val view: ReactExoplayerView, private val context: ThemedReactContext) : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_MEDIA_CONTROL = "rnv_media_control"
|
||||||
|
const val EXTRA_CONTROL_TYPE = "rnv_control_type"
|
||||||
|
|
||||||
|
// The request code for play action PendingIntent.
|
||||||
|
const val REQUEST_PLAY = 1
|
||||||
|
|
||||||
|
// The request code for pause action PendingIntent.
|
||||||
|
const val REQUEST_PAUSE = 2
|
||||||
|
|
||||||
|
// The intent extra value for play action.
|
||||||
|
const val CONTROL_TYPE_PLAY = 1
|
||||||
|
|
||||||
|
// The intent extra value for pause action.
|
||||||
|
const val CONTROL_TYPE_PAUSE = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
intent ?: return
|
||||||
|
if (intent.action == ACTION_MEDIA_CONTROL) {
|
||||||
|
when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
|
||||||
|
CONTROL_TYPE_PLAY -> view.setPausedModifier(false)
|
||||||
|
CONTROL_TYPE_PAUSE -> view.setPausedModifier(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setListener() {
|
||||||
|
ContextCompat.registerReceiver(context, this, IntentFilter(ACTION_MEDIA_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener() {
|
||||||
|
try {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignore if already unregistered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPipActionIntent(isPaused: Boolean): PendingIntent {
|
||||||
|
val requestCode = if (isPaused) REQUEST_PLAY else REQUEST_PAUSE
|
||||||
|
val controlType = if (isPaused) CONTROL_TYPE_PLAY else CONTROL_TYPE_PAUSE
|
||||||
|
val flag =
|
||||||
|
if (Build.VERSION.SDK_INT >=
|
||||||
|
Build.VERSION_CODES.M
|
||||||
|
) {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
}
|
||||||
|
val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(
|
||||||
|
EXTRA_CONTROL_TYPE,
|
||||||
|
controlType
|
||||||
|
)
|
||||||
|
intent.setPackage(context.packageName)
|
||||||
|
return PendingIntent.getBroadcast(context, requestCode, intent, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
@@ -3,6 +3,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spanStyle {
|
.spanStyle {
|
||||||
font-family: 'Orbitron';
|
font-family: var(--font-orbitron);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|||||||
62
docs/components/TWGBadge/TWGBadge.module.css
Normal file
62
docs/components/TWGBadge/TWGBadge.module.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
.extraContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #171717;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extraText {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extraButton {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #f9d85b;
|
||||||
|
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extraButton:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(html[class~=dark]) .extraContainer {
|
||||||
|
background-color: #87ccef;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(html[class~=dark]) .extraText {
|
||||||
|
color: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(html[class~=dark]) .extraButton {
|
||||||
|
background-color: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.visibleOnLarge {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
.visibleOnSmall {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
.visibleOnLarge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.visibleOnSmall {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
docs/components/TWGBadge/TWGBadge.tsx
Normal file
27
docs/components/TWGBadge/TWGBadge.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './TWGBadge.module.css';
|
||||||
|
|
||||||
|
interface TWGBadgeProps {
|
||||||
|
visibleOnLarge?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TWGBadge = ({visibleOnLarge}: TWGBadgeProps) => {
|
||||||
|
const visibilityClass = visibleOnLarge
|
||||||
|
? styles.visibleOnLarge
|
||||||
|
: styles.visibleOnSmall;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[styles.extraContainer, visibilityClass].join(' ')}>
|
||||||
|
<span className={styles.extraText}>We are TheWidlarzGroup</span>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||||
|
className={styles.extraButton}
|
||||||
|
rel="noreferrer">
|
||||||
|
Premium support →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TWGBadge;
|
||||||
7
docs/font.ts
Normal file
7
docs/font.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {Orbitron} from 'next/font/google';
|
||||||
|
|
||||||
|
export const orbitron = Orbitron({
|
||||||
|
display: 'swap',
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: ['400', '900'],
|
||||||
|
});
|
||||||
2
docs/next-env.d.ts
vendored
2
docs/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"build": "bun next build"
|
"build": "bun next build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^13.5.4",
|
"next": "14.2.20",
|
||||||
"nextra": "^2.13.2",
|
"nextra": "^2.13.2",
|
||||||
"nextra-theme-docs": "^2.13.2",
|
"nextra-theme-docs": "^2.13.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
docs/pages/_app.mdx
Normal file
14
docs/pages/_app.mdx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {orbitron} from '../font';
|
||||||
|
|
||||||
|
export default function Nextra({Component, pageProps}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style jsx global>{`
|
||||||
|
:root {
|
||||||
|
--font-orbitron: ${orbitron.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,5 +22,14 @@
|
|||||||
"newWindow": true,
|
"newWindow": true,
|
||||||
"href": "https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples"
|
"href": "https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples"
|
||||||
},
|
},
|
||||||
"projects": "Useful projects"
|
"projects": "Useful projects",
|
||||||
}
|
"separator_enterprise": {
|
||||||
|
"type": "separator",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"enterprise_support": {
|
||||||
|
"title": "Enterprise Support",
|
||||||
|
"newWindow": true,
|
||||||
|
"href": "https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ Example:
|
|||||||
|
|
||||||
### `onPictureInPictureStatusChanged`
|
### `onPictureInPictureStatusChanged`
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
<PlatformsList types={['iOS', 'Android', 'web']} />
|
||||||
|
|
||||||
Callback function that is called when picture in picture becomes active or inactive.
|
Callback function that is called when picture in picture becomes active or inactive.
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,56 @@ Future:
|
|||||||
- Will support more formats in the future through options
|
- Will support more formats in the future through options
|
||||||
- Will support custom directory and file name through options
|
- Will support custom directory and file name through options
|
||||||
|
|
||||||
|
### `enterPictureInPicture`
|
||||||
|
|
||||||
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
|
`enterPictureInPicture()`
|
||||||
|
|
||||||
|
To use this feature on Android with Expo, you must set 'enableAndroidPictureInPicture' true within expo plugin config (app.json)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"react-native-video",
|
||||||
|
{
|
||||||
|
"enableAndroidPictureInPicture": true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
To use this feature on Android with Bare React Native, you must:
|
||||||
|
|
||||||
|
- [Declare PiP support](https://developer.android.com/develop/ui/views/picture-in-picture#declaring) in your AndroidManifest.xml
|
||||||
|
- setting `android:supportsPictureInPicture` to `true`
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
...
|
||||||
|
android:supportsPictureInPicture="true">
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: Foreground picture in picture is not supported on Android due to limitations of react native (Single Activity App). So, If you call `enterPictureInPicture`, application will switch to background on Android.
|
||||||
|
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||||
|
|
||||||
|
### `exitPictureInPicture`
|
||||||
|
|
||||||
|
<PlatformsList types={['Android', 'iOS', 'web']} />
|
||||||
|
|
||||||
|
`exitPictureInPicture()`
|
||||||
|
|
||||||
|
Exits the active picture in picture; if it is not active, the function call is ignored.
|
||||||
|
|
||||||
|
### `restoreUserInterfaceForPictureInPictureStopCompleted`
|
||||||
|
|
||||||
|
<PlatformsList types={['iOS']} />
|
||||||
|
|
||||||
|
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
|
||||||
|
|
||||||
|
This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called.
|
||||||
|
|
||||||
### `seek`
|
### `seek`
|
||||||
|
|
||||||
<PlatformsList types={['All']} />
|
<PlatformsList types={['All']} />
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ A Boolean value that indicates whether the player should automatically delay pla
|
|||||||
|
|
||||||
### `bufferConfig`
|
### `bufferConfig`
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Deprecated, use source.bufferConfig instead
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
|
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
|
||||||
@@ -249,6 +252,42 @@ To setup DRM please follow [this guide](/component/drm)
|
|||||||
|
|
||||||
> ⚠️ DRM is not supported on visionOS yet
|
> ⚠️ DRM is not supported on visionOS yet
|
||||||
|
|
||||||
|
### `enterPictureInPictureOnLeave`
|
||||||
|
|
||||||
|
<PlatformsList types={['iOS', 'Android']} />
|
||||||
|
|
||||||
|
Determine whether to play media as a picture in picture when the user goes to the background.
|
||||||
|
|
||||||
|
- **false (default)** - Don't not play as picture in picture
|
||||||
|
- **true** - Play the media as picture in picture
|
||||||
|
|
||||||
|
To use this feature on Android with Expo, you must set 'enableAndroidPictureInPicture' true within expo plugin config (app.json)
|
||||||
|
|
||||||
|
```json
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"react-native-video",
|
||||||
|
{
|
||||||
|
"enableAndroidPictureInPicture": true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
To use this feature on Android with Bare React Native, you must:
|
||||||
|
|
||||||
|
- [Declare PiP support](https://developer.android.com/develop/ui/views/picture-in-picture#declaring) in your AndroidManifest.xml
|
||||||
|
- setting `android:supportsPictureInPicture` to `true`
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
...
|
||||||
|
android:supportsPictureInPicture="true">
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
||||||
|
|
||||||
### `filter`
|
### `filter`
|
||||||
|
|
||||||
<PlatformsList types={['iOS', 'visionOS']} />
|
<PlatformsList types={['iOS', 'visionOS']} />
|
||||||
@@ -422,17 +461,6 @@ Controls whether the media is paused
|
|||||||
- **false (default)** - Don't pause the media
|
- **false (default)** - Don't pause the media
|
||||||
- **true** - Pause the media
|
- **true** - Pause the media
|
||||||
|
|
||||||
### `pictureInPicture`
|
|
||||||
|
|
||||||
<PlatformsList types={['iOS']} />
|
|
||||||
|
|
||||||
Determine whether the media should played as picture in picture.
|
|
||||||
|
|
||||||
- **false (default)** - Don't not play as picture in picture
|
|
||||||
- **true** - Play the media as picture in picture
|
|
||||||
|
|
||||||
NOTE: Video ads cannot start when you are using the PIP on iOS (more info available at [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads)). If you are using custom controls, you must hide your PIP button when you receive the `STARTED` event from `onReceiveAdEvent` and show it again when you receive the `ALL_ADS_COMPLETED` event.
|
|
||||||
|
|
||||||
### `playInBackground`
|
### `playInBackground`
|
||||||
|
|
||||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||||
@@ -907,6 +935,56 @@ source={{
|
|||||||
}}
|
}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `bufferConfig`
|
||||||
|
|
||||||
|
<PlatformsList types={['Android']} />
|
||||||
|
|
||||||
|
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| minBufferMs | number | The default minimum duration of media that the player will attempt to ensure is buffered at all times, in milliseconds. |
|
||||||
|
| maxBufferMs | number | The default maximum duration of media that the player will attempt to buffer, in milliseconds. |
|
||||||
|
| bufferForPlaybackMs | number | The default duration of media that must be buffered for playback to start or resume following a user action such as a seek, in milliseconds. |
|
||||||
|
| bufferForPlaybackAfterRebufferMs | number | The default duration of media that must be buffered for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. |
|
||||||
|
| backBufferDurationMs | number | The number of milliseconds of buffer to keep before the current position. This allows rewinding without rebuffering within that duration. |
|
||||||
|
| maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1 |
|
||||||
|
| minBackBufferMemoryReservePercent | number | The percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1 |
|
||||||
|
| minBufferMemoryReservePercent | number | The percentage of available app memory to keep in reserve that prevents buffer from using it, between 0 and 1 |
|
||||||
|
| cacheSizeMB | number | Cache size in MB, enabling this to prevent new src requests and save bandwidth while repeating videos, or 0 to disable. Android only. |
|
||||||
|
| live | object | Object containing another config set for live playback configuration, see next table |
|
||||||
|
|
||||||
|
|
||||||
|
Description of live object:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| maxPlaybackSpeed | number | The maximum playback speed the player can use to catch up when trying to reach the target live offset. |
|
||||||
|
| minPlaybackSpeed | number | The minimum playback speed the player can use to fall back when trying to reach the target live offset. |
|
||||||
|
| maxOffsetMs | number | The maximum allowed live offset. Even when adjusting the offset to current network conditions, the player will not attempt to get above this offset during playback. |
|
||||||
|
| minOffsetMs | number | The minimum allowed live offset. Even when adjusting the offset to current network conditions, the player will not attempt to get below this offset during playback. |
|
||||||
|
| targetOffsetMs | number | The target live offset. The player will attempt to get close to this live offset during playback if possible. |
|
||||||
|
|
||||||
|
For android, more informations about live configuration can be find [here](https://developer.android.com/media/media3/exoplayer/live-streaming?hl=en)
|
||||||
|
|
||||||
|
Example with default values:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
bufferConfig={{
|
||||||
|
minBufferMs: 15000,
|
||||||
|
maxBufferMs: 50000,
|
||||||
|
bufferForPlaybackMs: 2500,
|
||||||
|
bufferForPlaybackAfterRebufferMs: 5000,
|
||||||
|
backBufferDurationMs: 120000,
|
||||||
|
cacheSizeMB: 0,
|
||||||
|
live: {
|
||||||
|
targetOffsetMs: 500,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that the Android cache is a global cache that is shared among all components; individual components can still opt out of caching behavior by setting cacheSizeMB to 0, but multiple components with a positive cacheSizeMB will be sharing the same one, and the cache size will always be the first value set; it will not change during the app's lifecycle.
|
||||||
|
|
||||||
#### `minLoadRetryCount`
|
#### `minLoadRetryCount`
|
||||||
|
|
||||||
<PlatformsList types={['Android']} />
|
<PlatformsList types={['Android']} />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ It allows to stream video files (m3u, mpd, mp4, ...) inside your react native ap
|
|||||||
- Subtitles (embeded or side loaded)
|
- Subtitles (embeded or side loaded)
|
||||||
- DRM support
|
- DRM support
|
||||||
- Client side Ads insertion (via google IMA)
|
- Client side Ads insertion (via google IMA)
|
||||||
- Pip (ios)
|
- Pip
|
||||||
- Embedded playback controls
|
- Embedded playback controls
|
||||||
- And much more
|
- And much more
|
||||||
|
|
||||||
|
|||||||
@@ -37,4 +37,5 @@ It's useful when you are using `expo` managed workflow (expo prebuild) as it wil
|
|||||||
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
|
| enableBackgroundAudio | boolean | false | Add required changes to play video in background on iOS |
|
||||||
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
|
| enableADSExtension | boolean | false | Add required changes to use ads extension for video player |
|
||||||
| enableCacheExtension | boolean | false | Add required changes to use cache extension for video player on iOS |
|
| enableCacheExtension | boolean | false | Add required changes to use cache extension for video player on iOS |
|
||||||
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |
|
| androidExtensions | object | {} | You can enable/disable extensions as per your requirement - this allow to reduce library size on android |
|
||||||
|
| enableAndroidPictureInPicture | boolean | false | Apply configs to be able to use Picture-in-picture on android |
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
This page links other open source projects which can be useful for your player implementation. <br>
|
This page links other open source projects which can be useful for your player implementation. <br>
|
||||||
If you have a project which can be useful for other users, feel free to open a PR to add it here.
|
If you have a project which can be useful for other users, feel free to open a PR to add it here.
|
||||||
|
|
||||||
## UI over react-native-video
|
## Our (TheWidlarzGroup) libraries
|
||||||
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): First reference player UI
|
- [react-native-video-player](https://github.com/TheWidlarzGroup/react-native-video-player): Our video player UI library
|
||||||
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
|
||||||
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
|
||||||
|
|
||||||
## Other tools
|
## Community libraries
|
||||||
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox to control player over media session
|
- [react-native-corner-video](https://github.com/Lg0gs/react-native-corner-video): A floating video player
|
||||||
|
- [react-native-track-player](https://github.com/doublesymmetry/react-native-track-player): A toolbox for audio playback
|
||||||
|
- [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls): Video player UI
|
||||||
|
- [react-native-media-console](https://github.com/criszz77/react-native-media-console): React-native-video-controls updated and rewritten in typescript
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import TWGBadge from './components/TWGBadge/TWGBadge';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
head: (
|
head: (
|
||||||
@@ -26,12 +27,6 @@ export default {
|
|||||||
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
content="https://docs.thewidlarzgroup.com/react-native-video/thumbnail.jpg"
|
||||||
/>
|
/>
|
||||||
<meta name="twitter:image:alt" content="React Native Video" />
|
<meta name="twitter:image:alt" content="React Native Video" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/png"
|
type="image/png"
|
||||||
@@ -39,13 +34,15 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
async
|
async
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=G-PM2TQQQMDN"
|
src="https://www.googletagmanager.com/gtag/js?id=G-4YEWQH5ZHS"
|
||||||
/>
|
/>
|
||||||
<script>
|
<script>
|
||||||
{`window.dataLayer = window.dataLayer || [];
|
{`
|
||||||
function gtag(){dataLayer.push(arguments);}
|
window.dataLayer = window.dataLayer || [];
|
||||||
gtag('js', new Date());
|
function gtag(){dataLayer.push(arguments);}
|
||||||
gtag('config', 'G-PM2TQQQMDN');`}
|
gtag('js', new Date());
|
||||||
|
gtag('config', 'G-4YEWQH5ZHS');
|
||||||
|
`}
|
||||||
</script>
|
</script>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -59,6 +56,15 @@ export default {
|
|||||||
},
|
},
|
||||||
docsRepositoryBase:
|
docsRepositoryBase:
|
||||||
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
|
'https://github.com/TheWidlarzGroup/react-native-video/tree/master/docs/',
|
||||||
|
main: ({children}) => (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<TWGBadge visibleOnLarge={false} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
toc: {
|
||||||
|
extraContent: <TWGBadge visibleOnLarge={true} />,
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
text: (
|
text: (
|
||||||
<span>
|
<span>
|
||||||
@@ -66,61 +72,6 @@ export default {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
toc: {
|
|
||||||
extraContent: (
|
|
||||||
<>
|
|
||||||
<style>{`
|
|
||||||
:is(html[class~=dark]) .extra-container {
|
|
||||||
background-color: #87ccef;
|
|
||||||
}
|
|
||||||
:is(html[class~=dark]) .extra-text {
|
|
||||||
color: #171717;
|
|
||||||
}
|
|
||||||
:is(html[class~=dark]) .extra-button {
|
|
||||||
background-color: #171717;
|
|
||||||
}
|
|
||||||
.extra-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #171717;
|
|
||||||
padding: 1rem;
|
|
||||||
gap: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
.extra-text {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.extra-button {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
background-color: #f9d85b;
|
|
||||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
.extra-button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<div className="extra-container">
|
|
||||||
<span className="extra-text">We are TheWidlarzGroup</span>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://www.thewidlarzgroup.com/?utm_source=rnv&utm_medium=docs#Contact"
|
|
||||||
className="extra-button"
|
|
||||||
rel="noreferrer">
|
|
||||||
Premium support →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
useNextSeoProps() {
|
useNextSeoProps() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ yarn start
|
|||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Make sure you've already downloaded the simulator on which you want to launch the app.
|
> Make sure you've already downloaded the simulator on which you want to launch the app.
|
||||||
|
|
||||||
## [Expo](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/bare)
|
## [Expo](https://github.com/TheWidlarzGroup/react-native-video/tree/master/examples/expo)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Additionally, there is a great example of a TV app available in the [AmazonAppDev/react-native-multi-tv-app-sample](https://github.com/AmazonAppDev/react-native-multi-tv-app-sample) repository.
|
||||||
|
It provides a sample application for Android TV, Fire TV, tvOS, and the web. The app includes customizable drawer navigation, a content grid, a hero header, and an integrated video player. Built with Expo, it serves as a great starting point for cross-platform TV app development.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ const BasicExample = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
videoRef.current?.setSource(currentSrc);
|
videoRef.current?.setSource({...currentSrc, bufferConfig: _bufferConfig });
|
||||||
}, [currentSrc]);
|
}, [currentSrc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -284,7 +284,6 @@ const BasicExample = () => {
|
|||||||
selectedAudioTrack={selectedAudioTrack}
|
selectedAudioTrack={selectedAudioTrack}
|
||||||
selectedVideoTrack={selectedVideoTrack}
|
selectedVideoTrack={selectedVideoTrack}
|
||||||
playInBackground={false}
|
playInBackground={false}
|
||||||
bufferConfig={_bufferConfig}
|
|
||||||
preventsDisplaySleepDuringVideoPlayback={true}
|
preventsDisplaySleepDuringVideoPlayback={true}
|
||||||
renderLoader={_renderLoader}
|
renderLoader={_renderLoader}
|
||||||
onPlaybackRateChange={onPlaybackRateChange}
|
onPlaybackRateChange={onPlaybackRateChange}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.facebook.react.ReactPackage
|
|||||||
import com.facebook.react.ReactHost
|
import com.facebook.react.ReactHost
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||||
|
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||||
import com.facebook.soloader.SoLoader
|
import com.facebook.soloader.SoLoader
|
||||||
|
|
||||||
import expo.modules.ApplicationLifecycleDispatcher
|
import expo.modules.ApplicationLifecycleDispatcher
|
||||||
@@ -42,7 +43,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
SoLoader.init(this, false)
|
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||||
load()
|
load()
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
|
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
||||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23')
|
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34')
|
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
|
||||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23'
|
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.24'
|
||||||
|
|
||||||
ndkVersion = "26.1.10909125"
|
ndkVersion = "26.1.10909125"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
|||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
|
||||||
android.enableJetifier=true
|
|
||||||
|
|
||||||
# Enable AAPT2 PNG crunching
|
# Enable AAPT2 PNG crunching
|
||||||
android.enablePngCrunchInReleaseBuilds=true
|
android.enablePngCrunchInReleaseBuilds=true
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
|||||||
# your application. You should enable this flag either if you want
|
# your application. You should enable this flag either if you want
|
||||||
# to write custom TurboModules/Fabric components OR use libraries that
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
# are providing them.
|
# are providing them.
|
||||||
newArchEnabled=false
|
newArchEnabled=true
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pluginManagement {
|
|||||||
if(reactNativeMinor == 74 && reactNativePatch <= 3){
|
if(reactNativeMinor == 74 && reactNativePatch <= 3){
|
||||||
includeBuild("react-settings-plugin")
|
includeBuild("react-settings-plugin")
|
||||||
}
|
}
|
||||||
|
includeBuild("../node_modules/@react-native/gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins { id("com.facebook.react.settings") }
|
plugins { id("com.facebook.react.settings") }
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
|
69D28F37B56772B7A9D5E544 /* libPods-ExpoExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C65020CA55EC90CEE9A23ED2 /* libPods-ExpoExample.a */; };
|
||||||
6E3E1907139F4681BB29BA3F /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD8985E3A84FEF8F310E21 /* noop-file.swift */; };
|
6E3E1907139F4681BB29BA3F /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD8985E3A84FEF8F310E21 /* noop-file.swift */; };
|
||||||
96905EF65AED1B983A6B3ABC /* libPods-ExpoExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-ExpoExample.a */; };
|
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
F6DD9B61853ED7F5FE2C32F9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3BCFB4767DA89C6653566DA4 /* PrivacyInfo.xcprivacy */; };
|
F6DD9B61853ED7F5FE2C32F9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3BCFB4767DA89C6653566DA4 /* PrivacyInfo.xcprivacy */; };
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ExpoExample/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ExpoExample/Info.plist; sourceTree = "<group>"; };
|
||||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ExpoExample/main.m; sourceTree = "<group>"; };
|
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ExpoExample/main.m; sourceTree = "<group>"; };
|
||||||
3BCFB4767DA89C6653566DA4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ExpoExample/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
3BCFB4767DA89C6653566DA4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ExpoExample/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-ExpoExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ExpoExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
5762883CF0477CB8DEB074D2 /* Pods-ExpoExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExpoExample.debug.xcconfig"; path = "Target Support Files/Pods-ExpoExample/Pods-ExpoExample.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
6C2E3173556A471DD304B334 /* Pods-ExpoExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExpoExample.debug.xcconfig"; path = "Target Support Files/Pods-ExpoExample/Pods-ExpoExample.debug.xcconfig"; sourceTree = "<group>"; };
|
9C0AF52D744330C1CAD2989E /* Pods-ExpoExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExpoExample.release.xcconfig"; path = "Target Support Files/Pods-ExpoExample/Pods-ExpoExample.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
7A4D352CD337FB3A3BF06240 /* Pods-ExpoExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExpoExample.release.xcconfig"; path = "Target Support Files/Pods-ExpoExample/Pods-ExpoExample.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = ExpoExample/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = ExpoExample/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
B7DF2D7F8A384D51B801CEE7 /* ExpoExample-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "ExpoExample-Bridging-Header.h"; path = "ExpoExample/ExpoExample-Bridging-Header.h"; sourceTree = "<group>"; };
|
B7DF2D7F8A384D51B801CEE7 /* ExpoExample-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "ExpoExample-Bridging-Header.h"; path = "ExpoExample/ExpoExample-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||||
|
C65020CA55EC90CEE9A23ED2 /* libPods-ExpoExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ExpoExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E1CD8985E3A84FEF8F310E21 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "ExpoExample/noop-file.swift"; sourceTree = "<group>"; };
|
E1CD8985E3A84FEF8F310E21 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "ExpoExample/noop-file.swift"; sourceTree = "<group>"; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-ExpoExample/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-ExpoExample/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
96905EF65AED1B983A6B3ABC /* libPods-ExpoExample.a in Frameworks */,
|
69D28F37B56772B7A9D5E544 /* libPods-ExpoExample.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-ExpoExample.a */,
|
C65020CA55EC90CEE9A23ED2 /* libPods-ExpoExample.a */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -125,8 +125,8 @@
|
|||||||
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6C2E3173556A471DD304B334 /* Pods-ExpoExample.debug.xcconfig */,
|
5762883CF0477CB8DEB074D2 /* Pods-ExpoExample.debug.xcconfig */,
|
||||||
7A4D352CD337FB3A3BF06240 /* Pods-ExpoExample.release.xcconfig */,
|
9C0AF52D744330C1CAD2989E /* Pods-ExpoExample.release.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -146,14 +146,14 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ExpoExample" */;
|
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ExpoExample" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
EBDF7B38F7D33D49CA2B39C6 /* [CP] Check Pods Manifest.lock */,
|
||||||
6E9FB0155D627BF633A3CB08 /* [Expo] Configure project */,
|
6E9FB0155D627BF633A3CB08 /* [Expo] Configure project */,
|
||||||
13B07F871A680F5B00A75B9A /* Sources */,
|
13B07F871A680F5B00A75B9A /* Sources */,
|
||||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
2ECC1AFFBE97E107FF88D15D /* [CP] Embed Pods Frameworks */,
|
||||||
CED90EB9939E56B1E5D48A24 /* [CP] Embed Pods Frameworks */,
|
DC89C28FF1E7E51E27603886 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -225,26 +225,22 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
|
||||||
};
|
};
|
||||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
2ECC1AFFBE97E107FF88D15D /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
"${PODS_ROOT}/Target Support Files/Pods-ExpoExample/Pods-ExpoExample-frameworks.sh",
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"$(DERIVED_FILE_DIR)/Pods-ExpoExample-checkManifestLockResult.txt",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoExample/Pods-ExpoExample-frameworks.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
6E9FB0155D627BF633A3CB08 /* [Expo] Configure project */ = {
|
6E9FB0155D627BF633A3CB08 /* [Expo] Configure project */ = {
|
||||||
@@ -266,7 +262,7 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-ExpoExample/expo-configure-project.sh\"\n";
|
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-ExpoExample/expo-configure-project.sh\"\n";
|
||||||
};
|
};
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
DC89C28FF1E7E51E27603886 /* [CP] Copy Pods Resources */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -276,36 +272,48 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoExample/Pods-ExpoExample-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoExample/Pods-ExpoExample-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
CED90EB9939E56B1E5D48A24 /* [CP] Embed Pods Frameworks */ = {
|
EBDF7B38F7D33D49CA2B39C6 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-ExpoExample/Pods-ExpoExample-frameworks.sh",
|
);
|
||||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
"$(DERIVED_FILE_DIR)/Pods-ExpoExample-checkManifestLockResult.txt",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoExample/Pods-ExpoExample-frameworks.sh\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
@@ -327,7 +335,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-ExpoExample.debug.xcconfig */;
|
baseConfigurationReference = 5762883CF0477CB8DEB074D2 /* Pods-ExpoExample.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
@@ -339,7 +347,7 @@
|
|||||||
"FB_SONARKIT_ENABLED=1",
|
"FB_SONARKIT_ENABLED=1",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = ExpoExample/Info.plist;
|
INFOPLIST_FILE = ExpoExample/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
@@ -360,14 +368,14 @@
|
|||||||
};
|
};
|
||||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-ExpoExample.release.xcconfig */;
|
baseConfigurationReference = 9C0AF52D744330C1CAD2989E /* Pods-ExpoExample.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = ExpoExample/ExpoExample.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ExpoExample/ExpoExample.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
INFOPLIST_FILE = ExpoExample/Info.plist;
|
INFOPLIST_FILE = ExpoExample/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
@@ -434,7 +442,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD = "";
|
LD = "";
|
||||||
LDPLUSPLUS = "";
|
LDPLUSPLUS = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||||
@@ -447,6 +455,7 @@
|
|||||||
);
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -493,7 +502,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD = "";
|
LD = "";
|
||||||
LDPLUSPLUS = "";
|
LDPLUSPLUS = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expo.jsEngine": "hermes",
|
"expo.jsEngine": "hermes",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true"
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
|
"ios.deploymentTarget": "15.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,30 +7,47 @@
|
|||||||
"start:tv": "EXPO_TV=1 expo start",
|
"start:tv": "EXPO_TV=1 expo start",
|
||||||
"android:tv": "EXPO_TV=1 expo run:android",
|
"android:tv": "EXPO_TV=1 expo run:android",
|
||||||
"android": "EXPO_TV=0 expo run:android",
|
"android": "EXPO_TV=0 expo run:android",
|
||||||
|
"release:android": "cd android && EXPO_TV=0 ./gradlew assembleRelease && cd -",
|
||||||
|
"release:android:tv": "cd android && EXPO_TV=1 ./gradlew assembleRelease && cd -",
|
||||||
"ios:tv": "EXPO_TV=1 expo run:ios",
|
"ios:tv": "EXPO_TV=1 expo run:ios",
|
||||||
"ios": "EXPO_TV=0 expo run:ios",
|
"ios": "EXPO_TV=0 expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"prebuild:tv": "EXPO_TV=1 expo prebuild",
|
"prebuild:tv": "EXPO_TV=1 expo prebuild",
|
||||||
"prebuild": "EXPO_TV=0 expo prebuild",
|
"prebuild": "EXPO_TV=0 expo prebuild",
|
||||||
"update-src": "echo 'Updating src from ../bare/src' && rm -r ./src && cp -r ../bare/src ./src && echo 'Updated src from ../bare/src'"
|
"update-src": "echo 'Updating src from ../bare/src' && rm -r ./src && cp -r ../bare/src ./src && echo 'Updated src from ../bare/src'",
|
||||||
|
"clean": "rm -rf node_modules && rm -rf android/build/ && rm -rf android/app/build && rm -rf ./lib @@ rm -rf ./android/.gradle && rm -rf ./android/.idea && rm -rf ./.expo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "^3.2.3",
|
"@expo/metro-runtime": "~4.0.0",
|
||||||
"@react-native-picker/picker": "2.8.1",
|
"@react-native-picker/picker": "2.9.0",
|
||||||
"expo": "~51.0.39",
|
"expo": "^52.0.7",
|
||||||
"expo-splash-screen": "~0.27.7",
|
"expo-splash-screen": "~0.29.10",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-status-bar": "~2.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "npm:react-native-tvos@~0.74.5-0",
|
"react-native": "npm:react-native-tvos@0.76.2-0",
|
||||||
"react-native-web": "^0.19.13"
|
"react-native-web": "^0.19.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.0",
|
"@babel/core": "^7.25.2",
|
||||||
"@expo/config-plugins": "^8.0.10",
|
"@babel/preset-env": "^7.25.3",
|
||||||
|
"@babel/runtime": "^7.25.0",
|
||||||
|
"@react-native-community/cli": "15.0.1",
|
||||||
|
"@react-native-community/cli-platform-android": "15.0.1",
|
||||||
|
"@react-native-community/cli-platform-ios": "15.0.1",
|
||||||
|
"@react-native/babel-preset": "0.76.3",
|
||||||
|
"@react-native/eslint-config": "0.76.3",
|
||||||
|
"@react-native/metro-config": "0.76.3",
|
||||||
|
"@react-native/typescript-config": "0.76.3",
|
||||||
|
"@types/react": "^18.2.6",
|
||||||
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
|
"babel-jest": "^29.6.3",
|
||||||
|
"eslint": "^8.19.0",
|
||||||
|
"jest": "^29.6.3",
|
||||||
|
"prettier": "2.8.8",
|
||||||
|
"react-test-renderer": "18.3.1",
|
||||||
|
"@expo/config-plugins": "^9.0.9",
|
||||||
"@react-native-tvos/config-tv": "^0.0.10",
|
"@react-native-tvos/config-tv": "^0.0.10",
|
||||||
"@react-native/metro-config": "^0.75.4",
|
|
||||||
"@types/react": "~18.2.45",
|
|
||||||
"babel-plugin-module-resolver": "^5.0.2",
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
"typescript": "~5.3.3"
|
"typescript": "~5.3.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ const BasicExample = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
videoRef.current?.setSource(currentSrc);
|
videoRef.current?.setSource({...currentSrc, bufferConfig: _bufferConfig });
|
||||||
}, [currentSrc]);
|
}, [currentSrc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -284,7 +284,6 @@ const BasicExample = () => {
|
|||||||
selectedAudioTrack={selectedAudioTrack}
|
selectedAudioTrack={selectedAudioTrack}
|
||||||
selectedVideoTrack={selectedVideoTrack}
|
selectedVideoTrack={selectedVideoTrack}
|
||||||
playInBackground={false}
|
playInBackground={false}
|
||||||
bufferConfig={_bufferConfig}
|
|
||||||
preventsDisplaySleepDuringVideoPlayback={true}
|
preventsDisplaySleepDuringVideoPlayback={true}
|
||||||
renderLoader={_renderLoader}
|
renderLoader={_renderLoader}
|
||||||
onPlaybackRateChange={onPlaybackRateChange}
|
onPlaybackRateChange={onPlaybackRateChange}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
|
|
||||||
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate {
|
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate {
|
||||||
private weak var _video: RCTVideo?
|
private weak var _video: RCTVideo?
|
||||||
private var _pipEnabled: () -> Bool
|
private var _isPictureInPictureActive: () -> Bool
|
||||||
|
|
||||||
/* Entry point for the SDK. Used to make ad requests. */
|
/* Entry point for the SDK. Used to make ad requests. */
|
||||||
private var adsLoader: IMAAdsLoader!
|
private var adsLoader: IMAAdsLoader!
|
||||||
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */
|
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */
|
||||||
private var adsManager: IMAAdsManager!
|
private var adsManager: IMAAdsManager!
|
||||||
|
|
||||||
init(video: RCTVideo!, pipEnabled: @escaping () -> Bool) {
|
init(video: RCTVideo!, isPictureInPictureActive: @escaping () -> Bool) {
|
||||||
_video = video
|
_video = video
|
||||||
_pipEnabled = pipEnabled
|
_isPictureInPictureActive = isPictureInPictureActive
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
}
|
}
|
||||||
// Play each ad once it has been loaded
|
// Play each ad once it has been loaded
|
||||||
if event.type == IMAAdEventType.LOADED {
|
if event.type == IMAAdEventType.LOADED {
|
||||||
if _pipEnabled() {
|
if _isPictureInPictureActive() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
adsManager.start()
|
adsManager.start()
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import React
|
|||||||
private var _onPictureInPictureExit: (() -> Void)?
|
private var _onPictureInPictureExit: (() -> Void)?
|
||||||
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
|
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
|
||||||
private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
|
private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
|
||||||
private var _isActive = false
|
private var _isPictureInPictureActive: Bool {
|
||||||
|
return _pipController?.isPictureInPictureActive ?? false
|
||||||
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ onPictureInPictureEnter: (() -> Void)? = nil,
|
_ onPictureInPictureEnter: (() -> Void)? = nil,
|
||||||
@@ -67,23 +69,35 @@ import React
|
|||||||
_pipController = nil
|
_pipController = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPictureInPicture(_ isActive: Bool) {
|
func enterPictureInPicture() {
|
||||||
if _isActive == isActive {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_isActive = isActive
|
|
||||||
|
|
||||||
guard let _pipController else { return }
|
guard let _pipController else { return }
|
||||||
|
if !_isPictureInPictureActive {
|
||||||
|
_pipController.startPictureInPicture()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _isActive && !_pipController.isPictureInPictureActive {
|
func exitPictureInPicture() {
|
||||||
DispatchQueue.main.async {
|
guard let _pipController else { return }
|
||||||
_pipController.startPictureInPicture()
|
if _isPictureInPictureActive {
|
||||||
}
|
let state = UIApplication.shared.applicationState
|
||||||
} else if !_isActive && _pipController.isPictureInPictureActive {
|
if state == .background || state == .inactive {
|
||||||
DispatchQueue.main.async {
|
deinitPipController()
|
||||||
|
_onPictureInPictureExit?()
|
||||||
|
_onRestoreUserInterfaceForPictureInPictureStop?()
|
||||||
|
} else {
|
||||||
_pipController.stopPictureInPicture()
|
_pipController.stopPictureInPicture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
class RCTPictureInPicture: NSObject {
|
||||||
|
public let _pipController: NSObject? = nil
|
||||||
|
|
||||||
|
func setRestoreUserInterfaceForPIPStopCompletionHandler(_: Bool) {}
|
||||||
|
func setupPipController(_: AVPlayerLayer?) {}
|
||||||
|
func deinitPipController() {}
|
||||||
|
func enterPictureInPicture() {}
|
||||||
|
func exitPictureInPicture() {}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ enum RCTPlayerOperations {
|
|||||||
|
|
||||||
var selectedTrackIndex: Int = RCTVideoUnset
|
var selectedTrackIndex: Int = RCTVideoUnset
|
||||||
|
|
||||||
if type == "disabled" {
|
if (type == "disabled") || (type == "none") || (type == "") {
|
||||||
// Select the last text index which is the disabled text track
|
// Select the last text index which is the disabled text track
|
||||||
selectedTrackIndex = trackCount - firstTextIndex
|
selectedTrackIndex = trackCount - firstTextIndex
|
||||||
} else if type == "language" {
|
} else if type == "language" {
|
||||||
@@ -92,7 +92,7 @@ enum RCTPlayerOperations {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if type == "disabled" {
|
if (type == "disabled") || (type == "none") || (type == "") {
|
||||||
// Do nothing. We want to ensure option is nil
|
// Do nothing. We want to ensure option is nil
|
||||||
} else if (type == "language") || (type == "title") {
|
} else if (type == "language") || (type == "title") {
|
||||||
let value = criteria?.value as? String
|
let value = criteria?.value as? String
|
||||||
|
|||||||
@@ -63,10 +63,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
private var _showNotificationControls = false
|
private var _showNotificationControls = false
|
||||||
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
||||||
private var _lastBitrate = -2.0
|
private var _lastBitrate = -2.0
|
||||||
private var _pictureInPictureEnabled = false {
|
private var _enterPictureInPictureOnLeave = false {
|
||||||
didSet {
|
didSet {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if _pictureInPictureEnabled {
|
if isPictureInPictureActive() { return }
|
||||||
|
if _enterPictureInPictureOnLeave {
|
||||||
initPictureinPicture()
|
initPictureinPicture()
|
||||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||||
} else {
|
} else {
|
||||||
@@ -101,9 +102,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
private let _videoCache: RCTVideoCachingHandler = .init()
|
private let _videoCache: RCTVideoCachingHandler = .init()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(iOS)
|
private var _pip: RCTPictureInPicture?
|
||||||
private var _pip: RCTPictureInPicture?
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||||
@@ -113,6 +112,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||||
@objc var onVideoBandwidthUpdate: RCTDirectEventBlock?
|
@objc var onVideoBandwidthUpdate: RCTDirectEventBlock?
|
||||||
@objc var onVideoSeek: RCTDirectEventBlock?
|
@objc var onVideoSeek: RCTDirectEventBlock?
|
||||||
|
@objc var onVideoSeekComplete: RCTDirectEventBlock?
|
||||||
@objc var onVideoEnd: RCTDirectEventBlock?
|
@objc var onVideoEnd: RCTDirectEventBlock?
|
||||||
@objc var onTimedMetadata: RCTDirectEventBlock?
|
@objc var onTimedMetadata: RCTDirectEventBlock?
|
||||||
@objc var onVideoAudioBecomingNoisy: RCTDirectEventBlock?
|
@objc var onVideoAudioBecomingNoisy: RCTDirectEventBlock?
|
||||||
@@ -166,11 +166,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
onRestoreUserInterfaceForPictureInPictureStop?([:])
|
onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPipEnabled() -> Bool {
|
func isPictureInPictureActive() -> Bool {
|
||||||
return _pictureInPictureEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPipActive() -> Bool {
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
return _pip?._pipController?.isPictureInPictureActive == true
|
return _pip?._pipController?.isPictureInPictureActive == true
|
||||||
#else
|
#else
|
||||||
@@ -180,15 +176,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
|
|
||||||
func initPictureinPicture() {
|
func initPictureinPicture() {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
_pip = RCTPictureInPicture({ [weak self] in
|
if _pip == nil {
|
||||||
self?._onPictureInPictureEnter()
|
_pip = RCTPictureInPicture({ [weak self] in
|
||||||
}, { [weak self] in
|
self?._onPictureInPictureEnter()
|
||||||
self?._onPictureInPictureExit()
|
}, { [weak self] in
|
||||||
}, { [weak self] in
|
self?._onPictureInPictureExit()
|
||||||
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
}, { [weak self] in
|
||||||
})
|
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if _playerLayer != nil && !_controls {
|
if _playerLayer != nil && !_controls && _pip?._pipController == nil {
|
||||||
_pip?.setupPipController(_playerLayer)
|
_pip?.setupPipController(_playerLayer)
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@@ -200,17 +198,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||||
ReactNativeVideoManager.shared.registerView(newInstance: self)
|
ReactNativeVideoManager.shared.registerView(newInstance: self)
|
||||||
#if USE_GOOGLE_IMA
|
#if USE_GOOGLE_IMA
|
||||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
_eventDispatcher = eventDispatcher
|
_eventDispatcher = eventDispatcher
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if _pictureInPictureEnabled {
|
if _enterPictureInPictureOnLeave {
|
||||||
initPictureinPicture()
|
initPictureinPicture()
|
||||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
|
||||||
} else {
|
|
||||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -242,6 +237,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(screenWillLock),
|
||||||
|
name: UIApplication.protectedDataWillBecomeUnavailableNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(screenDidUnlock),
|
||||||
|
name: UIApplication.protectedDataDidBecomeAvailableNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(audioRouteChanged(notification:)),
|
selector: #selector(audioRouteChanged(notification:)),
|
||||||
@@ -257,7 +266,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
#if USE_GOOGLE_IMA
|
#if USE_GOOGLE_IMA
|
||||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,8 +322,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
|
|
||||||
@objc
|
@objc
|
||||||
func applicationDidEnterBackground(notification _: NSNotification!) {
|
func applicationDidEnterBackground(notification _: NSNotification!) {
|
||||||
|
if !_paused && isPictureInPictureActive() {
|
||||||
|
_player?.play()
|
||||||
|
_player?.rate = _rate
|
||||||
|
}
|
||||||
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
||||||
if !_playInBackground || isExternalPlaybackActive || isPipActive() { return }
|
if !_playInBackground || isExternalPlaybackActive || isPictureInPictureActive() { return }
|
||||||
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
|
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
|
||||||
_playerLayer?.player = nil
|
_playerLayer?.player = nil
|
||||||
_playerViewController?.player = nil
|
_playerViewController?.player = nil
|
||||||
@@ -327,6 +340,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
_playerViewController?.player = _player
|
_playerViewController?.player = _player
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func screenWillLock() {
|
||||||
|
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||||
|
if _playInBackground || !_isPlaying || !isActiveBackgroundPip { return }
|
||||||
|
|
||||||
|
_player?.pause()
|
||||||
|
_player?.rate = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func screenDidUnlock() {
|
||||||
|
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||||
|
if _paused || !isActiveBackgroundPip { return }
|
||||||
|
|
||||||
|
_player?.play()
|
||||||
|
_player?.rate = _rate
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Audio events
|
// MARK: - Audio events
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -710,19 +741,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setPictureInPicture(_ pictureInPicture: Bool) {
|
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let audioSession = AVAudioSession.sharedInstance()
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
do {
|
do {
|
||||||
try audioSession.setCategory(.playback)
|
try audioSession.setCategory(.playback)
|
||||||
try audioSession.setActive(true, options: [])
|
try audioSession.setActive(true, options: [])
|
||||||
} catch {}
|
} catch {}
|
||||||
if pictureInPicture {
|
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
||||||
_pictureInPictureEnabled = true
|
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
||||||
} else {
|
|
||||||
_pictureInPictureEnabled = false
|
|
||||||
}
|
}
|
||||||
_pip?.setPictureInPicture(pictureInPicture)
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,34 +810,53 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
_paused = paused
|
_paused = paused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setSeek(_ time: NSNumber, _ tolerance: NSNumber) {
|
func setSeek(_ time: NSNumber, _ tolerance: NSNumber) {
|
||||||
let item: AVPlayerItem? = _player?.currentItem
|
let item: AVPlayerItem? = _player?.currentItem
|
||||||
|
|
||||||
_pendingSeek = true
|
|
||||||
|
|
||||||
guard item != nil, let player = _player, let item, item.status == AVPlayerItem.Status.readyToPlay else {
|
guard item != nil, let player = _player, let item, item.status == AVPlayerItem.Status.readyToPlay else {
|
||||||
|
_pendingSeek = true
|
||||||
_pendingSeekTime = time.floatValue
|
_pendingSeekTime = time.floatValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
RCTPlayerOperations.seek(
|
let wasPaused = _paused
|
||||||
player: player,
|
let seekTime = CMTimeMakeWithSeconds(Float64(time.floatValue), preferredTimescale: Int32(NSEC_PER_SEC))
|
||||||
playerItem: item,
|
let toleranceTime = CMTimeMakeWithSeconds(Float64(tolerance.floatValue), preferredTimescale: Int32(NSEC_PER_SEC))
|
||||||
paused: _paused,
|
|
||||||
seekTime: time.floatValue,
|
let currentTimeBeforeSeek = CMTimeGetSeconds(item.currentTime())
|
||||||
seekTolerance: tolerance.floatValue
|
|
||||||
) { [weak self] (_: Bool) in
|
// Call onVideoSeek before starting the seek operation
|
||||||
guard let self else { return }
|
let currentTime = NSNumber(value: Float(currentTimeBeforeSeek))
|
||||||
|
self.onVideoSeek?(["currentTime": currentTime,
|
||||||
|
"seekTime": time,
|
||||||
|
"target": self.reactTag])
|
||||||
|
|
||||||
|
_pendingSeek = true
|
||||||
|
|
||||||
|
let seekCompletionHandler: (Bool) -> Void = { [weak self] finished in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self._pendingSeek = false
|
||||||
|
|
||||||
|
guard finished else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self._playerObserver.addTimeObserverIfNotSet()
|
self._playerObserver.addTimeObserverIfNotSet()
|
||||||
self.setPaused(self._paused)
|
if !wasPaused {
|
||||||
self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))),
|
self.setPaused(false)
|
||||||
"seekTime": time,
|
}
|
||||||
"target": self.reactTag as Any])
|
|
||||||
|
let currentTimeAfterSeek = CMTimeGetSeconds(item.currentTime())
|
||||||
|
|
||||||
|
let newCurrentTime = NSNumber(value: Float(currentTimeAfterSeek))
|
||||||
|
self.onVideoSeekComplete?(["currentTime": newCurrentTime,
|
||||||
|
"seekTime": time,
|
||||||
|
"target": self.reactTag as Any])
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingSeek = false
|
player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime, completionHandler: seekCompletionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -967,10 +1014,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
guard let source = _source else { return }
|
guard let source = _source else { return }
|
||||||
if !source.textTracks.isEmpty { // sideloaded text tracks
|
if !source.textTracks.isEmpty { // sideloaded text tracks
|
||||||
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: source.textTracks, criteria: _selectedTextTrackCriteria)
|
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: source.textTracks, criteria: _selectedTextTrackCriteria)
|
||||||
} else { // text tracks included in the HLS playlist§
|
} else { // text tracks included in the HLS playlist
|
||||||
Task {
|
Task { [weak self] in
|
||||||
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible,
|
guard let self,
|
||||||
criteria: _selectedTextTrackCriteria)
|
let player = self._player else { return }
|
||||||
|
|
||||||
|
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(
|
||||||
|
player: player,
|
||||||
|
characteristic: .legible,
|
||||||
|
criteria: self._selectedTextTrackCriteria
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1087,8 +1140,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
viewController.view.frame = self.bounds
|
viewController.view.frame = self.bounds
|
||||||
viewController.player = player
|
viewController.player = player
|
||||||
if #available(tvOS 14.0, *) {
|
if #available(tvOS 14.0, *) {
|
||||||
viewController.allowsPictureInPicturePlayback = _pictureInPictureEnabled
|
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||||
|
#endif
|
||||||
return viewController
|
return viewController
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1108,7 +1164,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
self.layer.needsDisplayOnBoundsChange = true
|
self.layer.needsDisplayOnBoundsChange = true
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if _pictureInPictureEnabled {
|
if _enterPictureInPictureOnLeave {
|
||||||
_pip?.setupPipController(_playerLayer)
|
_pip?.setupPipController(_playerLayer)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1502,6 +1558,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
|
|
||||||
guard _isPlaying != isPlaying else { return }
|
guard _isPlaying != isPlaying else { return }
|
||||||
_isPlaying = isPlaying
|
_isPlaying = isPlaying
|
||||||
|
_paused = !isPlaying
|
||||||
onVideoPlaybackStateChanged?(["isPlaying": isPlaying, "isSeeking": self._pendingSeek == true, "target": reactTag as Any])
|
onVideoPlaybackStateChanged?(["isPlaying": isPlaying, "isSeeking": self._pendingSeek == true, "target": reactTag as Any])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1678,7 +1735,33 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func enterPictureInPicture() {
|
||||||
|
if _pip?._pipController == nil {
|
||||||
|
initPictureinPicture()
|
||||||
|
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||||
|
}
|
||||||
|
_pip?.enterPictureInPicture()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func exitPictureInPicture() {
|
||||||
|
guard isPictureInPictureActive() else { return }
|
||||||
|
|
||||||
|
_pip?.exitPictureInPicture()
|
||||||
|
#if os(iOS)
|
||||||
|
if _enterPictureInPictureOnLeave {
|
||||||
|
initPictureinPicture()
|
||||||
|
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||||
|
} else {
|
||||||
|
_pip?.deinitPipController()
|
||||||
|
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862
|
// Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862
|
||||||
@objc
|
@objc
|
||||||
func setOnClick(_: Any) {}
|
func setOnClick(_: Any) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(preventsDisplaySleepDuringVideoPlayback, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(preventsDisplaySleepDuringVideoPlayback, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(preferredForwardBufferDuration, float);
|
RCT_EXPORT_VIEW_PROPERTY(preferredForwardBufferDuration, float);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(enterPictureInPictureOnLeave, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString);
|
RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
||||||
@@ -45,6 +45,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTDirectEventBlock);
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoBandwidthUpdate, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onVideoBandwidthUpdate, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTDirectEventBlock);
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onVideoSeekComplete, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTDirectEventBlock);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onVideoAudioBecomingNoisy, RCTDirectEventBlock);
|
RCT_EXPORT_VIEW_PROPERTY(onVideoAudioBecomingNoisy, RCTDirectEventBlock);
|
||||||
@@ -73,6 +74,8 @@ RCT_EXTERN_METHOD(setLicenseResultErrorCmd : (nonnull NSNumber*)reactTag error :
|
|||||||
RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
|
RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
|
||||||
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume)
|
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume)
|
||||||
RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
|
RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
|
||||||
|
RCT_EXTERN_METHOD(enterPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||||
|
RCT_EXTERN_METHOD(exitPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||||
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
|
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
|
||||||
|
|
||||||
RCT_EXTERN_METHOD(save
|
RCT_EXTERN_METHOD(save
|
||||||
|
|||||||
@@ -72,6 +72,20 @@ class RCTVideoManager: RCTViewManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc(enterPictureInPictureCmd:)
|
||||||
|
func enterPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||||
|
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||||
|
videoView?.enterPictureInPicture()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(exitPictureInPictureCmd:)
|
||||||
|
func exitPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||||
|
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||||
|
videoView?.exitPictureInPicture()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@objc(setSourceCmd:source:)
|
@objc(setSourceCmd:source:)
|
||||||
func setSourceCmd(_ reactTag: NSNumber, source: NSDictionary) {
|
func setSourceCmd(_ reactTag: NSNumber, source: NSDictionary) {
|
||||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||||
|
|||||||
@@ -6,33 +6,31 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class ReactNativeVideoManager: RNVPlugin {
|
public class ReactNativeVideoManager: RNVPlugin {
|
||||||
private let expectedMaxVideoCount = 10
|
private let expectedMaxVideoCount = 2
|
||||||
|
|
||||||
// create a private initializer
|
// create a private initializer
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
public static let shared: ReactNativeVideoManager = .init()
|
public static let shared: ReactNativeVideoManager = .init()
|
||||||
|
|
||||||
var instanceList: [RCTVideo] = Array()
|
private var instanceCount = 0
|
||||||
var pluginList: [RNVPlugin] = Array()
|
private var pluginList: [RNVPlugin] = Array()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* register a new ReactExoplayerViewManager in the managed list
|
* register a new view
|
||||||
*/
|
*/
|
||||||
func registerView(newInstance: RCTVideo) {
|
func registerView(newInstance _: RCTVideo) {
|
||||||
if instanceList.count > expectedMaxVideoCount {
|
if instanceCount > expectedMaxVideoCount {
|
||||||
DebugLog("multiple Video displayed ?")
|
DebugLog("multiple Video displayed ?")
|
||||||
}
|
}
|
||||||
instanceList.append(newInstance)
|
instanceCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* unregister existing ReactExoplayerViewManager in the managed list
|
* unregister existing view
|
||||||
*/
|
*/
|
||||||
func unregisterView(newInstance: RCTVideo) {
|
func unregisterView(newInstance _: RCTVideo) {
|
||||||
if let i = instanceList.firstIndex(of: newInstance) {
|
instanceCount -= 1
|
||||||
instanceList.remove(at: i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-native-video",
|
"name": "react-native-video",
|
||||||
"version": "6.7.0",
|
"version": "6.9.1",
|
||||||
"description": "A <Video /> element for react-native",
|
"description": "A <Video /> element for react-native",
|
||||||
"main": "lib/index",
|
"main": "lib/index",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import type {
|
|||||||
OnPlaybackStateChangedData,
|
OnPlaybackStateChangedData,
|
||||||
OnProgressData,
|
OnProgressData,
|
||||||
OnSeekData,
|
OnSeekData,
|
||||||
|
OnSeekCompleteData,
|
||||||
OnTextTrackDataChangedData,
|
OnTextTrackDataChangedData,
|
||||||
OnTimedMetadataData,
|
OnTimedMetadataData,
|
||||||
OnVideoAspectRatioData,
|
OnVideoAspectRatioData,
|
||||||
@@ -81,6 +82,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
onError,
|
onError,
|
||||||
onProgress,
|
onProgress,
|
||||||
onSeek,
|
onSeek,
|
||||||
|
onSeekComplete,
|
||||||
onEnd,
|
onEnd,
|
||||||
onBuffer,
|
onBuffer,
|
||||||
onBandwidthUpdate,
|
onBandwidthUpdate,
|
||||||
@@ -108,6 +110,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
onAspectRatio,
|
onAspectRatio,
|
||||||
localSourceEncryptionKeyScheme,
|
localSourceEncryptionKeyScheme,
|
||||||
minLoadRetryCount,
|
minLoadRetryCount,
|
||||||
|
bufferConfig,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -150,6 +153,11 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
if (!_source) {
|
if (!_source) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLocalAssetFile =
|
||||||
|
typeof _source === 'number' ||
|
||||||
|
('uri' in _source && typeof _source.uri === 'number');
|
||||||
|
|
||||||
const resolvedSource = resolveAssetSourceForVideo(_source);
|
const resolvedSource = resolveAssetSourceForVideo(_source);
|
||||||
let uri = resolvedSource.uri || '';
|
let uri = resolvedSource.uri || '';
|
||||||
if (uri && uri.match(/^\//)) {
|
if (uri && uri.match(/^\//)) {
|
||||||
@@ -219,10 +227,13 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
|
|
||||||
const _minLoadRetryCount =
|
const _minLoadRetryCount =
|
||||||
_source.minLoadRetryCount || minLoadRetryCount;
|
_source.minLoadRetryCount || minLoadRetryCount;
|
||||||
|
|
||||||
|
const _bufferConfig = _source.bufferConfig || bufferConfig;
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
isNetwork,
|
isNetwork,
|
||||||
isAsset,
|
isAsset,
|
||||||
|
isLocalAssetFile,
|
||||||
shouldCache: resolvedSource.shouldCache || false,
|
shouldCache: resolvedSource.shouldCache || false,
|
||||||
type: resolvedSource.type || '',
|
type: resolvedSource.type || '',
|
||||||
mainVer: resolvedSource.mainVer || 0,
|
mainVer: resolvedSource.mainVer || 0,
|
||||||
@@ -240,6 +251,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
textTracksAllowChunklessPreparation:
|
textTracksAllowChunklessPreparation:
|
||||||
resolvedSource.textTracksAllowChunklessPreparation,
|
resolvedSource.textTracksAllowChunklessPreparation,
|
||||||
minLoadRetryCount: _minLoadRetryCount,
|
minLoadRetryCount: _minLoadRetryCount,
|
||||||
|
bufferConfig: _bufferConfig,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -251,6 +263,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
minLoadRetryCount,
|
minLoadRetryCount,
|
||||||
source?.cmcd,
|
source?.cmcd,
|
||||||
textTracks,
|
textTracks,
|
||||||
|
bufferConfig,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -398,6 +411,40 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
[setFullScreen],
|
[setFullScreen],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enterPictureInPicture = useCallback(async () => {
|
||||||
|
if (!nativeRef.current) {
|
||||||
|
console.warn('Video Component is not mounted');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _enterPictureInPicture = () => {
|
||||||
|
NativeVideoManager.enterPictureInPictureCmd(getReactTag(nativeRef));
|
||||||
|
};
|
||||||
|
|
||||||
|
Platform.select({
|
||||||
|
ios: _enterPictureInPicture,
|
||||||
|
android: _enterPictureInPicture,
|
||||||
|
default: () => {},
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exitPictureInPicture = useCallback(async () => {
|
||||||
|
if (!nativeRef.current) {
|
||||||
|
console.warn('Video Component is not mounted');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _exitPictureInPicture = () => {
|
||||||
|
NativeVideoManager.exitPictureInPictureCmd(getReactTag(nativeRef));
|
||||||
|
};
|
||||||
|
|
||||||
|
Platform.select({
|
||||||
|
ios: _exitPictureInPicture,
|
||||||
|
android: _exitPictureInPicture,
|
||||||
|
default: () => {},
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const save = useCallback((options: object) => {
|
const save = useCallback((options: object) => {
|
||||||
// VideoManager.save can be null on android & windows
|
// VideoManager.save can be null on android & windows
|
||||||
if (Platform.OS !== 'ios') {
|
if (Platform.OS !== 'ios') {
|
||||||
@@ -458,6 +505,13 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
[onSeek],
|
[onSeek],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onVideoSeekComplete = useCallback(
|
||||||
|
(e: NativeSyntheticEvent<OnSeekCompleteData>) => {
|
||||||
|
onSeekComplete?.(e.nativeEvent);
|
||||||
|
},
|
||||||
|
[onSeekComplete]
|
||||||
|
);
|
||||||
|
|
||||||
const onVideoPlaybackStateChanged = useCallback(
|
const onVideoPlaybackStateChanged = useCallback(
|
||||||
(e: NativeSyntheticEvent<OnPlaybackStateChangedData>) => {
|
(e: NativeSyntheticEvent<OnPlaybackStateChangedData>) => {
|
||||||
onPlaybackStateChanged?.(e.nativeEvent);
|
onPlaybackStateChanged?.(e.nativeEvent);
|
||||||
@@ -646,6 +700,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
setVolume,
|
setVolume,
|
||||||
getCurrentPosition,
|
getCurrentPosition,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
|
enterPictureInPicture,
|
||||||
|
exitPictureInPicture,
|
||||||
setSource,
|
setSource,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
@@ -659,6 +715,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
setVolume,
|
setVolume,
|
||||||
getCurrentPosition,
|
getCurrentPosition,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
|
enterPictureInPicture,
|
||||||
|
exitPictureInPicture,
|
||||||
setSource,
|
setSource,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -768,9 +826,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
const _style: StyleProp<ViewStyle> = useMemo(
|
const _style: StyleProp<ViewStyle> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
...(showPoster ? {display: 'none'} : {}),
|
|
||||||
}),
|
}),
|
||||||
[showPoster],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -800,6 +857,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
onVideoError={onError ? onVideoError : undefined}
|
onVideoError={onError ? onVideoError : undefined}
|
||||||
onVideoProgress={onProgress ? onVideoProgress : undefined}
|
onVideoProgress={onProgress ? onVideoProgress : undefined}
|
||||||
onVideoSeek={onSeek ? onVideoSeek : undefined}
|
onVideoSeek={onSeek ? onVideoSeek : undefined}
|
||||||
|
onVideoSeekComplete={onSeekComplete ? onVideoSeekComplete : undefined}
|
||||||
onVideoEnd={onEnd}
|
onVideoEnd={onEnd}
|
||||||
onVideoBuffer={onBuffer ? onVideoBuffer : undefined}
|
onVideoBuffer={onBuffer ? onVideoBuffer : undefined}
|
||||||
onVideoPlaybackStateChanged={
|
onVideoPlaybackStateChanged={
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
onVolumeChange,
|
onVolumeChange,
|
||||||
onEnd,
|
onEnd,
|
||||||
onPlaybackStateChanged,
|
onPlaybackStateChanged,
|
||||||
|
onPictureInPictureStatusChanged,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@@ -180,6 +181,31 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
[setFullScreen],
|
[setFullScreen],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enterPictureInPicture = useCallback(() => {
|
||||||
|
try {
|
||||||
|
if (!nativeRef.current) {
|
||||||
|
console.error('Video Component is not mounted');
|
||||||
|
} else {
|
||||||
|
nativeRef.current.requestPictureInPicture();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exitPictureInPicture = useCallback(() => {
|
||||||
|
if (
|
||||||
|
nativeRef.current &&
|
||||||
|
nativeRef.current === document.pictureInPictureElement
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
document.exitPictureInPicture();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
() => ({
|
() => ({
|
||||||
@@ -193,6 +219,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
dismissFullscreenPlayer,
|
dismissFullscreenPlayer,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
save: unsupported,
|
save: unsupported,
|
||||||
|
enterPictureInPicture,
|
||||||
|
exitPictureInPicture,
|
||||||
restoreUserInterfaceForPictureInPictureStopCompleted: unsupported,
|
restoreUserInterfaceForPictureInPictureStopCompleted: unsupported,
|
||||||
nativeHtmlVideoRef: nativeRef,
|
nativeHtmlVideoRef: nativeRef,
|
||||||
}),
|
}),
|
||||||
@@ -208,6 +236,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
presentFullscreenPlayer,
|
presentFullscreenPlayer,
|
||||||
dismissFullscreenPlayer,
|
dismissFullscreenPlayer,
|
||||||
setFullScreen,
|
setFullScreen,
|
||||||
|
enterPictureInPicture,
|
||||||
|
exitPictureInPicture,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,6 +282,27 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|||||||
nativeRef.current.playbackRate = rate;
|
nativeRef.current.playbackRate = rate;
|
||||||
}, [rate]);
|
}, [rate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof onPictureInPictureStatusChanged !== 'function' ||
|
||||||
|
!nativeRef.current
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onEnterPip = () =>
|
||||||
|
onPictureInPictureStatusChanged({isActive: true});
|
||||||
|
const onLeavePip = () =>
|
||||||
|
onPictureInPictureStatusChanged({isActive: false});
|
||||||
|
|
||||||
|
const video = nativeRef.current;
|
||||||
|
video.addEventListener('enterpictureinpicture', onEnterPip);
|
||||||
|
video.addEventListener('leavepictureinpicture', onLeavePip);
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('enterpictureinpicture', onEnterPip);
|
||||||
|
video.removeEventListener('leavepictureinpicture', onLeavePip);
|
||||||
|
};
|
||||||
|
}, [onPictureInPictureStatusChanged]);
|
||||||
|
|
||||||
useMediaSession(src?.metadata, nativeRef, showNotificationControls);
|
useMediaSession(src?.metadata, nativeRef, showNotificationControls);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export type ConfigProps = {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
enableNotificationControls?: boolean;
|
enableNotificationControls?: boolean;
|
||||||
|
/**
|
||||||
|
* Apply configs to be able to use Picture-in-picture on Android.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
enableAndroidPictureInPicture?: boolean;
|
||||||
/**
|
/**
|
||||||
* Whether to enable background audio feature.
|
* Whether to enable background audio feature.
|
||||||
* @default false
|
* @default false
|
||||||
|
|||||||
31
src/expo-plugins/withAndroidPictureInPicture.ts
Normal file
31
src/expo-plugins/withAndroidPictureInPicture.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
AndroidConfig,
|
||||||
|
withAndroidManifest,
|
||||||
|
type ConfigPlugin,
|
||||||
|
} from '@expo/config-plugins';
|
||||||
|
|
||||||
|
export const withAndroidPictureInPicture: ConfigPlugin<boolean> = (
|
||||||
|
config,
|
||||||
|
enableAndroidPictureInPicture,
|
||||||
|
) => {
|
||||||
|
return withAndroidManifest(config, (_config) => {
|
||||||
|
if (!enableAndroidPictureInPicture) {
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainActivity = AndroidConfig.Manifest.getMainActivity(
|
||||||
|
_config.modResults,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mainActivity) {
|
||||||
|
console.warn(
|
||||||
|
'AndroidManifest.xml is missing an <activity android:name=".MainActivity" /> element - skipping adding Picture-In-Picture related config.',
|
||||||
|
);
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainActivity.$['android:supportsPictureInPicture'] = 'true';
|
||||||
|
|
||||||
|
return _config;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import {type ConfigPlugin, createRunOncePlugin} from '@expo/config-plugins';
|
|||||||
import type {ConfigProps} from './@types';
|
import type {ConfigProps} from './@types';
|
||||||
import {withNotificationControls} from './withNotificationControls';
|
import {withNotificationControls} from './withNotificationControls';
|
||||||
import {withAndroidExtensions} from './withAndroidExtensions';
|
import {withAndroidExtensions} from './withAndroidExtensions';
|
||||||
|
import {withAndroidPictureInPicture} from './withAndroidPictureInPicture';
|
||||||
import {withAds} from './withAds';
|
import {withAds} from './withAds';
|
||||||
import {withBackgroundAudio} from './withBackgroundAudio';
|
import {withBackgroundAudio} from './withBackgroundAudio';
|
||||||
import {withPermissions} from '@expo/config-plugins/build/android/Permissions';
|
import {withPermissions} from '@expo/config-plugins/build/android/Permissions';
|
||||||
@@ -21,6 +22,13 @@ const withRNVideo: ConfigPlugin<ConfigProps> = (config, props = {}) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.enableAndroidPictureInPicture) {
|
||||||
|
config = withAndroidPictureInPicture(
|
||||||
|
config,
|
||||||
|
props.enableAndroidPictureInPicture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (props.androidExtensions != null) {
|
if (props.androidExtensions != null) {
|
||||||
config = withAndroidExtensions(config, props.androidExtensions);
|
config = withAndroidExtensions(config, props.androidExtensions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface VideoManagerType {
|
|||||||
setFullScreenCmd: (reactTag: Int32, fullScreen: boolean) => Promise<void>;
|
setFullScreenCmd: (reactTag: Int32, fullScreen: boolean) => Promise<void>;
|
||||||
setSourceCmd: (reactTag: Int32, source?: UnsafeObject) => Promise<void>;
|
setSourceCmd: (reactTag: Int32, source?: UnsafeObject) => Promise<void>;
|
||||||
setVolumeCmd: (reactTag: Int32, volume: number) => Promise<void>;
|
setVolumeCmd: (reactTag: Int32, volume: number) => Promise<void>;
|
||||||
|
enterPictureInPictureCmd: (reactTag: number) => Promise<void>;
|
||||||
|
exitPictureInPictureCmd: (reactTag: number) => Promise<void>;
|
||||||
save: (reactTag: Int32, option: UnsafeObject) => Promise<VideoSaveData>;
|
save: (reactTag: Int32, option: UnsafeObject) => Promise<VideoSaveData>;
|
||||||
getCurrentPosition: (reactTag: Int32) => Promise<Int32>;
|
getCurrentPosition: (reactTag: Int32) => Promise<Int32>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type VideoSrc = Readonly<{
|
|||||||
uri?: string;
|
uri?: string;
|
||||||
isNetwork?: boolean;
|
isNetwork?: boolean;
|
||||||
isAsset?: boolean;
|
isAsset?: boolean;
|
||||||
|
isLocalAssetFile?: boolean;
|
||||||
shouldCache?: boolean;
|
shouldCache?: boolean;
|
||||||
type?: string;
|
type?: string;
|
||||||
mainVer?: Int32;
|
mainVer?: Int32;
|
||||||
@@ -51,6 +52,7 @@ export type VideoSrc = Readonly<{
|
|||||||
textTracks?: TextTracks;
|
textTracks?: TextTracks;
|
||||||
ad?: AdsConfig;
|
ad?: AdsConfig;
|
||||||
minLoadRetryCount?: Int32; // Android
|
minLoadRetryCount?: Int32; // Android
|
||||||
|
bufferConfig?: BufferConfig; // Android
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type DRMType = WithDefault<string, 'widevine'>;
|
type DRMType = WithDefault<string, 'widevine'>;
|
||||||
@@ -203,6 +205,12 @@ export type OnSeekData = Readonly<{
|
|||||||
seekTime: Float;
|
seekTime: Float;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type OnSeekCompleteData = Readonly<{
|
||||||
|
currentTime: number;
|
||||||
|
seekTime: number;
|
||||||
|
target: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type OnPlaybackStateChangedData = Readonly<{
|
export type OnPlaybackStateChangedData = Readonly<{
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
isSeeking: boolean;
|
isSeeking: boolean;
|
||||||
@@ -347,7 +355,7 @@ export interface VideoNativeProps extends ViewProps {
|
|||||||
preventsDisplaySleepDuringVideoPlayback?: boolean;
|
preventsDisplaySleepDuringVideoPlayback?: boolean;
|
||||||
preferredForwardBufferDuration?: Float; //ios, 0
|
preferredForwardBufferDuration?: Float; //ios, 0
|
||||||
playWhenInactive?: boolean; // ios, false
|
playWhenInactive?: boolean; // ios, false
|
||||||
pictureInPicture?: boolean; // ios, false
|
enterPictureInPictureOnLeave?: boolean; // default false
|
||||||
ignoreSilentSwitch?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
ignoreSilentSwitch?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
||||||
mixWithOthers?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
mixWithOthers?: WithDefault<string, 'inherit'>; // ios, 'inherit'
|
||||||
rate?: Float;
|
rate?: Float;
|
||||||
@@ -358,7 +366,6 @@ export interface VideoNativeProps extends ViewProps {
|
|||||||
restoreUserInterfaceForPIPStopCompletionHandler?: boolean;
|
restoreUserInterfaceForPIPStopCompletionHandler?: boolean;
|
||||||
debug?: DebugConfig;
|
debug?: DebugConfig;
|
||||||
showNotificationControls?: WithDefault<boolean, false>; // Android, iOS
|
showNotificationControls?: WithDefault<boolean, false>; // Android, iOS
|
||||||
bufferConfig?: BufferConfig; // Android
|
|
||||||
currentPlaybackTime?: Double; // Android
|
currentPlaybackTime?: Double; // Android
|
||||||
disableDisconnectError?: boolean; // Android
|
disableDisconnectError?: boolean; // Android
|
||||||
focusable?: boolean; // Android
|
focusable?: boolean; // Android
|
||||||
@@ -377,6 +384,7 @@ export interface VideoNativeProps extends ViewProps {
|
|||||||
onVideoProgress?: DirectEventHandler<OnProgressData>;
|
onVideoProgress?: DirectEventHandler<OnProgressData>;
|
||||||
onVideoBandwidthUpdate?: DirectEventHandler<OnBandwidthUpdateData>;
|
onVideoBandwidthUpdate?: DirectEventHandler<OnBandwidthUpdateData>;
|
||||||
onVideoSeek?: DirectEventHandler<OnSeekData>;
|
onVideoSeek?: DirectEventHandler<OnSeekData>;
|
||||||
|
onVideoSeekComplete?: DirectEventHandler<OnSeekCompleteData>;
|
||||||
onVideoEnd?: DirectEventHandler<{}>; // all
|
onVideoEnd?: DirectEventHandler<{}>; // all
|
||||||
onVideoAudioBecomingNoisy?: DirectEventHandler<{}>;
|
onVideoAudioBecomingNoisy?: DirectEventHandler<{}>;
|
||||||
onVideoFullscreenPlayerWillPresent?: DirectEventHandler<{}>; // ios, android
|
onVideoFullscreenPlayerWillPresent?: DirectEventHandler<{}>; // ios, android
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
OnPlaybackStateChangedData,
|
OnPlaybackStateChangedData,
|
||||||
OnProgressData,
|
OnProgressData,
|
||||||
OnSeekData,
|
OnSeekData,
|
||||||
|
OnSeekCompleteData,
|
||||||
OnTextTrackDataChangedData,
|
OnTextTrackDataChangedData,
|
||||||
OnTimedMetadataData,
|
OnTimedMetadataData,
|
||||||
OnVideoAspectRatioData,
|
OnVideoAspectRatioData,
|
||||||
@@ -252,7 +253,7 @@ export interface ReactVideoEvents {
|
|||||||
onLoadStart?: (e: OnLoadStartData) => void; //All
|
onLoadStart?: (e: OnLoadStartData) => void; //All
|
||||||
onPictureInPictureStatusChanged?: (
|
onPictureInPictureStatusChanged?: (
|
||||||
e: OnPictureInPictureStatusChangedData,
|
e: OnPictureInPictureStatusChangedData,
|
||||||
) => void; //iOS
|
) => void; //Android, iOS
|
||||||
onPlaybackRateChange?: (e: OnPlaybackRateChangeData) => void; //All
|
onPlaybackRateChange?: (e: OnPlaybackRateChangeData) => void; //All
|
||||||
onVolumeChange?: (e: OnVolumeChangeData) => void; //Android, iOS
|
onVolumeChange?: (e: OnVolumeChangeData) => void; //Android, iOS
|
||||||
onProgress?: (e: OnProgressData) => void; //All
|
onProgress?: (e: OnProgressData) => void; //All
|
||||||
@@ -260,6 +261,7 @@ export interface ReactVideoEvents {
|
|||||||
onReceiveAdEvent?: (e: OnReceiveAdEventData) => void; //Android, iOS
|
onReceiveAdEvent?: (e: OnReceiveAdEventData) => void; //Android, iOS
|
||||||
onRestoreUserInterfaceForPictureInPictureStop?: () => void; //iOS
|
onRestoreUserInterfaceForPictureInPictureStop?: () => void; //iOS
|
||||||
onSeek?: (e: OnSeekData) => void; //Android, iOS, Windows UWP
|
onSeek?: (e: OnSeekData) => void; //Android, iOS, Windows UWP
|
||||||
|
onSeekComplete?: (e: OnSeekCompleteData) => void; // iOS
|
||||||
onPlaybackStateChanged?: (e: OnPlaybackStateChangedData) => void; // Android, iOS
|
onPlaybackStateChanged?: (e: OnPlaybackStateChangedData) => void; // Android, iOS
|
||||||
onTimedMetadata?: (e: OnTimedMetadataData) => void; //Android, iOS
|
onTimedMetadata?: (e: OnTimedMetadataData) => void; //Android, iOS
|
||||||
onAudioTracks?: (e: OnAudioTracksData) => void; // Android
|
onAudioTracks?: (e: OnAudioTracksData) => void; // Android
|
||||||
|
|||||||
@@ -19,5 +19,7 @@ export interface VideoRef {
|
|||||||
getCurrentPosition: () => Promise<number>;
|
getCurrentPosition: () => Promise<number>;
|
||||||
setFullScreen: (fullScreen: boolean) => void;
|
setFullScreen: (fullScreen: boolean) => void;
|
||||||
setSource: (source?: ReactVideoSource) => void;
|
setSource: (source?: ReactVideoSource) => void;
|
||||||
|
enterPictureInPicture: () => void;
|
||||||
|
exitPictureInPicture: () => void;
|
||||||
nativeHtmlVideoRef?: RefObject<HTMLVideoElement>; // web only
|
nativeHtmlVideoRef?: RefObject<HTMLVideoElement>; // web only
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type ReactVideoSourceProperties = {
|
|||||||
uri?: string;
|
uri?: string;
|
||||||
isNetwork?: boolean;
|
isNetwork?: boolean;
|
||||||
isAsset?: boolean;
|
isAsset?: boolean;
|
||||||
|
isLocalAssetFile?: boolean;
|
||||||
shouldCache?: boolean;
|
shouldCache?: boolean;
|
||||||
type?: string;
|
type?: string;
|
||||||
mainVer?: number;
|
mainVer?: number;
|
||||||
@@ -40,6 +41,7 @@ export type ReactVideoSourceProperties = {
|
|||||||
textTracks?: TextTracks;
|
textTracks?: TextTracks;
|
||||||
ad?: AdConfig;
|
ad?: AdConfig;
|
||||||
minLoadRetryCount?: number; // Android
|
minLoadRetryCount?: number; // Android
|
||||||
|
bufferConfig?: BufferConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReactVideoSource = Readonly<
|
export type ReactVideoSource = Readonly<
|
||||||
@@ -289,6 +291,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
|
|||||||
adLanguage?: ISO639_1;
|
adLanguage?: ISO639_1;
|
||||||
audioOutput?: AudioOutput; // Mobile
|
audioOutput?: AudioOutput; // Mobile
|
||||||
automaticallyWaitsToMinimizeStalling?: boolean; // iOS
|
automaticallyWaitsToMinimizeStalling?: boolean; // iOS
|
||||||
|
/** @deprecated Use source.bufferConfig */
|
||||||
bufferConfig?: BufferConfig; // Android
|
bufferConfig?: BufferConfig; // Android
|
||||||
bufferingStrategy?: BufferingStrategyType;
|
bufferingStrategy?: BufferingStrategyType;
|
||||||
chapters?: Chapters[]; // iOS
|
chapters?: Chapters[]; // iOS
|
||||||
@@ -312,7 +315,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
|
|||||||
mixWithOthers?: EnumValues<MixWithOthersType>; // iOS
|
mixWithOthers?: EnumValues<MixWithOthersType>; // iOS
|
||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
pictureInPicture?: boolean; // iOS
|
enterPictureInPictureOnLeave?: boolean;
|
||||||
playInBackground?: boolean;
|
playInBackground?: boolean;
|
||||||
playWhenInactive?: boolean; // iOS
|
playWhenInactive?: boolean; // iOS
|
||||||
poster?: string | ReactVideoPoster; // string is deprecated
|
poster?: string | ReactVideoPoster; // string is deprecated
|
||||||
|
|||||||
Reference in New Issue
Block a user