38 Commits

Author SHA1 Message Date
bb9e08c43a Merge remote-tracking branch 'upstream/master'
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
deploy docs / deploy-docs (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
2025-01-21 17:45:59 -07:00
YangJH
8dc10fd4b7 feat(web): implement web pip method and event (#4370)
Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
2025-01-18 15:19:55 +01:00
Olivier Bouillet
449dfb62b5 chore: fix linter (#4377) 2025-01-18 15:05:58 +01:00
YangJH
6c3af99979 fix(tvOS): fix tvos compile error (#4369)
* fix(tvOS): fix tvos compile error
2025-01-18 11:49:12 +01:00
Kamil Moskała
6cc1bff167 docs: fix analytics (#4368) 2025-01-13 15:01:15 +01:00
Krzysztof Moch
89ee02bdab chore: release v6.9.1 2025-01-10 12:09:07 +01:00
Joy
3924b5e295 fix: NPE in setEnterPictureInPictureOnLeave for unsupported Android versions (#4362) 2025-01-10 12:07:25 +01:00
Krzysztof Moch
eff8ea24af infra: fix bot comments (#4367) 2025-01-10 12:01:31 +01:00
Olivier Bouillet
c47d165668 chore(android): fix type in generic folder (#4323) 2025-01-04 13:48:59 +01:00
Vladimir
424f4eedde fix: avoid memory leak on iOS (#4355)
* fix: avoid memory leak on iOS

---------

Co-authored-by: Vladimir Vlasov <crivlaldo@gmail.com>
2025-01-04 13:47:22 +01:00
Krzysztof Moch
d31c72fc04 chore: release v6.9.0 2025-01-04 12:51:27 +01:00
YangJH
69a7bc2d26 feat: implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) (#3385)
* docs: enable Android PIP

* chore: change comments

* feat(android): implement Android PictureInPicture

* refactor: minor refactor code and apply lint

* fix: rewrite pip action intent code for Android14

* fix: remove redundant codes

* feat: add isInPictureInPicture flag for lifecycle handling

- activity provide helper method for same purpose, but this flag makes code simple

* feat: add pipFullscreenPlayerView for makes PIP include video only

* fix: add manifest value checker for prevent crash

* docs: add pictureInPicture prop's Android guide

* fix: sync controller visibility

* refactor: refining variable name

* fix: check multi window mode when host pause

- some OS version call onPause on multi-window mode

* fix: handling when onStop is called while in multi-window mode

* refactor:  enhance PIP util codes

* fix: fix FullscreenPlayerView constructor

* refactor: add enterPictureInPictureOnLeave prop and pip methods

- remove pictureInPicture boolean prop
- add enterPictureInPictureOnLeave boolean prop
- add enterPictureInPicture method
- add exitPictureInPicture method

* fix: fix lint error

* fix: prevent audio play in background without playInBackground prop

* fix: fix onDetachedFromWindow

* docs: update docs for pip

* fix(android): sync pip controller with external controller state

- for media session

* fix(ios): fix pip active fn variable reference

* refactor(ios): refactor code

* refactor(android): refactor codes

* fix(android): fix lint error

* refactor(android): refactor android pip logics

* fix(android): fix flickering issue when stop picture in picture

* fix(android): fix import

* fix(android): fix picture in picture with fullscreen mode

* fix(ios): fix syntax error

* fix(android): fix Fragment managed code

* refactor(android): remove redundant override lifecycle

* fix(js): add PIP type definition for codegen

* fix(android): fix syntax

* chore(android): fix lint error

* fix(ios): fix enter background handler

* refactor(ios): remove redundant code

* fix(ios): fix applicationDidEnterBackground for PIP

* fix(android): fix onPictureInPictureStatusChanged

* fix(ios): fix RCTPictureInPicture

* refactor(android): Ignore exception for some device ignore pip checker

- some device ignore PIP availability check, so we need to handle exception to prevent crash

* fix(android): add hideWithoutPlayer fn into Kotlin ver

* refactor(android): remove redundant code

* fix(android): fix pip ratio to be calculated with correct ratio value

* fix(android): fix crash issue when unmounting in PIP mode

* fix(android): fix lint error

* Update android/src/main/java/com/brentvatne/react/VideoManagerModule.kt

* fix(android): fix lint error

* fix(ios): fix lint error

* fix(ios): fix lint error

* feat(expo): add android picture in picture config within expo plugin

* fix: Replace Fragment with androidx.activity

- remove code that uses Fragment, which is a tricky implementation

* fix: fix lint error

* fix(android): disable auto enter when player released

* fix(android): fix event handler to check based on Activity it's bound to

---------

Co-authored-by: jonghun <jonghun@toss.im>
Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
2025-01-04 12:37:33 +01:00
Kamil Moskała
a735a4a581 chore: update bot feature req message (#4341) 2024-12-23 16:28:25 +01:00
Kamil Moskała
78770d92f5 refactor: make twg badge visible on smaller screens (#4345) 2024-12-21 08:51:45 +01:00
Kamil Moskała
abc4d76099 docs: link tv example (#4342) 2024-12-18 09:24:21 +01:00
Kamil Moskała
16fa20411f docs: update useful projects list (#4337) 2024-12-14 16:15:18 +01:00
Giovanni P.
3da4f1ca97 fix(ios): _paused is updated when video playback pause (#4320) 2024-12-10 19:50:32 +01:00
Kamil Moskała
7b4bd9a016 fix(docs): bump next.js version & fix meta warnings (#4327)
* chore(docs): bump `next` version

* docs: fix meta warnings
2024-12-10 19:46:55 +01:00
proohit
1033c9d4f3 fix(ios): disables subtitles for none and empty track types (#4319) 2024-12-04 15:35:51 +01:00
Chris Wood
d757a44bb1 Docs(README): correct Expo heading link (#4316) 2024-12-03 16:32:13 +01:00
YangJH
64c222df44 chore(android): bump default androidx.activity version from v1.8.2 to v1.9.3 (#4314) 2024-12-01 13:46:56 +01:00
Kamil Moskała
621a80299c fix: hiding poster (#4308)
* fix: hiding poster

* fix: hiding poster

* remove zIndex: 1

* fix: remove showPoster from dependency array
2024-12-01 13:41:03 +01:00
Olivier Bouillet
63c592f7cd fix(android): disable caching on local asset files (#4304) 2024-12-01 13:29:24 +01:00
Olivier Bouillet
569a79c510 chore: release v6.8.2 2024-11-25 21:36:30 +01:00
Olivier Bouillet
f37dc9e33e fix: playback restart without bufferingConfig (#4305) 2024-11-25 21:33:56 +01:00
Olivier Bouillet
dd78241b0d chore: release v6.8.1 2024-11-24 21:25:46 +01:00
Olivier Bouillet
2b7c215e66 Fix(android): restart issue react76 (#4302)
* fix: upgrade to expo 54
* fix: more bufferConfig inside source
- restart issue on react 0.76
- fix constness
- deprecate bufferConfig in root props
- update documentation
2024-11-24 21:19:46 +01:00
Tarık
daaac9740a fix(ios): handle async player access in text track selection (#4293)
* fix(ios): add null check to setSelectedTextTrack for player instance

* Revert "fix(ios): add null check to setSelectedTextTrack for player instance"

This reverts commit 447c83423cdd77b0cfa9cc171b231327a2cf1586.

* fix(ios): ensure strong reference to player during async operation

* fix: linter

* fix: linter formatting

* fix: revert typo
2024-11-22 14:19:09 +01:00
Krzysztof Moch
d934f214f5 chore: release v6.8.0 2024-11-17 15:42:49 +01:00
f72b44d4df Merge pull request 'Implement onSeekComplete for Android' (#5) from kat/temp-4 into master
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App Without Ads (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Reviewed-on: #5
2024-08-26 17:23:32 -06:00
d2ab22b99f wip
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App Without Ads (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
2024-08-26 17:22:38 -06:00
2dcde42fd6 Add logs 2024-08-26 17:22:38 -06:00
c7a45d421b Only complete seek if seek was in progress 2024-08-26 17:22:38 -06:00
f0db0a6868 kat wip 2024-08-26 17:22:38 -06:00
01b3322e03 Log in seek 2024-08-26 17:22:38 -06:00
13beae1401 Merge pull request 'Implement ios onSeekComplete' (#3) from kat/seek-complete-ios into master
Some checks failed
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Reviewed-on: #3
2024-08-20 22:37:10 -06:00
f3deabd75e Implement ios onSeekComplete
Some checks failed
Build iOS / Build iOS Example App (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Caching (pull_request) Has been cancelled
Check CLang / CLang-Format (pull_request) Has been cancelled
Check iOS / Swift-Lint (pull_request) Has been cancelled
Check iOS / Swift-Format (pull_request) Has been cancelled
2024-08-20 22:24:00 -06:00
d69729dc04 expose-on-seek-complete (#1)
Some checks failed
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
Reviewed-on: #1
2024-08-06 00:11:17 -06:00
61 changed files with 3017 additions and 1583 deletions

View File

@@ -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>

View File

@@ -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)

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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)
}
}
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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");

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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) {

View File

@@ -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)
}
}

Binary file not shown.

View File

@@ -3,6 +3,6 @@
} }
.spanStyle { .spanStyle {
font-family: 'Orbitron'; font-family: var(--font-orbitron);
font-weight: 800; font-weight: 800;
} }

View 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;
}
}

View 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
View 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
View File

@@ -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.

View File

@@ -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
View 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} />
</>
);
}

View File

@@ -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"
}
}

View File

@@ -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.

View File

@@ -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']} />

View File

@@ -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']} />

View File

@@ -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

View File

@@ -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 |

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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}

View File

@@ -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()

View File

@@ -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"
} }

View File

@@ -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.

View File

@@ -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

View File

@@ -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") }

View File

@@ -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

View File

@@ -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"
} }

View File

@@ -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"
}, },

View File

@@ -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}

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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) {}
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
} }
/** /**

View File

@@ -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",

View File

@@ -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={

View File

@@ -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 (

View File

@@ -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

View 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;
});
};

View File

@@ -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);
} }

View File

@@ -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>;
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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