@stepincto/expo-video 2.2.2-sicto.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +13 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +35 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +44 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +46 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +142 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +192 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +357 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +58 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +16 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +47 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +43 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<a href="https://docs.expo.dev/versions/unversioned/sdk/video/">
|
|
3
|
+
<img
|
|
4
|
+
src="../../.github/resources/expo-video.svg"
|
|
5
|
+
alt="expo-video"
|
|
6
|
+
height="64" />
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
A cross-platform, performant video component for React Native and Expo with Web support
|
|
11
|
+
|
|
12
|
+
# API documentation
|
|
13
|
+
|
|
14
|
+
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/video/)
|
|
15
|
+
- [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/video/)
|
|
16
|
+
|
|
17
|
+
# Installation in managed Expo projects
|
|
18
|
+
|
|
19
|
+
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
|
|
20
|
+
|
|
21
|
+
# Installation in bare React Native projects
|
|
22
|
+
|
|
23
|
+
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
|
|
24
|
+
|
|
25
|
+
### Add the package to your npm dependencies
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install expo-video
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
# Contributing
|
|
32
|
+
|
|
33
|
+
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# building with npm
|
|
37
|
+
```bash
|
|
38
|
+
npm run build:plugin # should succeed now
|
|
39
|
+
npm run build:once # or run build:lib then build:plugin
|
|
40
|
+
npm run pack:local
|
|
41
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'host.exp.exponent'
|
|
7
|
+
version = '2.2.2'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "expo.modules.video"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName '2.2.2'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
dependencies {
|
|
18
|
+
implementation 'com.facebook.react:react-android'
|
|
19
|
+
|
|
20
|
+
// Remember to keep this in sync with the version in `expo-audio`
|
|
21
|
+
def androidxMedia3Version = "1.4.0"
|
|
22
|
+
implementation "androidx.media3:media3-session:${androidxMedia3Version}"
|
|
23
|
+
implementation "androidx.media3:media3-exoplayer:${androidxMedia3Version}"
|
|
24
|
+
implementation "androidx.media3:media3-exoplayer-dash:${androidxMedia3Version}"
|
|
25
|
+
implementation "androidx.media3:media3-exoplayer-hls:${androidxMedia3Version}"
|
|
26
|
+
implementation "androidx.media3:media3-ui:${androidxMedia3Version}"
|
|
27
|
+
implementation "androidx.media3:media3-datasource-okhttp:${androidxMedia3Version}"
|
|
28
|
+
|
|
29
|
+
def fragment_version = "1.6.2"
|
|
30
|
+
implementation "androidx.fragment:fragment:$fragment_version"
|
|
31
|
+
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
3
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
4
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
5
|
+
|
|
6
|
+
<application>
|
|
7
|
+
<activity android:name=".FullscreenPlayerActivity"
|
|
8
|
+
android:supportsPictureInPicture="true"
|
|
9
|
+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
|
10
|
+
android:theme="@style/Fullscreen"/>
|
|
11
|
+
<service
|
|
12
|
+
android:name=".playbackService.ExpoVideoPlaybackService"
|
|
13
|
+
android:exported="false"
|
|
14
|
+
android:foregroundServiceType="mediaPlayback">
|
|
15
|
+
<intent-filter>
|
|
16
|
+
<action android:name="androidx.media3.session.MediaSessionService" />
|
|
17
|
+
</intent-filter>
|
|
18
|
+
</service>
|
|
19
|
+
</application>
|
|
20
|
+
</manifest>
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.AudioAttributes
|
|
5
|
+
import android.media.AudioFocusRequest
|
|
6
|
+
import android.media.AudioManager
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import androidx.media3.common.util.UnstableApi
|
|
9
|
+
import expo.modules.kotlin.AppContext
|
|
10
|
+
import expo.modules.video.enums.AudioMixingMode
|
|
11
|
+
import expo.modules.video.player.VideoPlayer
|
|
12
|
+
import expo.modules.video.player.VideoPlayerListener
|
|
13
|
+
import kotlinx.coroutines.launch
|
|
14
|
+
import java.lang.ref.WeakReference
|
|
15
|
+
|
|
16
|
+
@UnstableApi
|
|
17
|
+
class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAudioFocusChangeListener, VideoPlayerListener {
|
|
18
|
+
private val audioManager by lazy {
|
|
19
|
+
appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
|
|
20
|
+
throw FailedToGetAudioFocusManagerException()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private var players: MutableList<WeakReference<VideoPlayer>> = mutableListOf()
|
|
25
|
+
private var currentFocusRequest: AudioFocusRequest? = null
|
|
26
|
+
private var currentMixingMode: AudioMixingMode = AudioMixingMode.MIX_WITH_OTHERS
|
|
27
|
+
private val anyPlayerRequiresFocus: Boolean
|
|
28
|
+
get() = players.toList().any {
|
|
29
|
+
playerRequiresFocus(it)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private fun requestAudioFocus() {
|
|
33
|
+
val audioMixingMode = findAudioMixingMode()
|
|
34
|
+
|
|
35
|
+
// We don't request AudioFocus if we want to mix the audio with others
|
|
36
|
+
if (audioMixingMode == AudioMixingMode.MIX_WITH_OTHERS || !anyPlayerRequiresFocus) {
|
|
37
|
+
abandonAudioFocus()
|
|
38
|
+
currentMixingMode = audioMixingMode
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
val audioFocusType = when (audioMixingMode) {
|
|
42
|
+
AudioMixingMode.DUCK_OTHERS -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
|
43
|
+
AudioMixingMode.AUTO -> AudioManager.AUDIOFOCUS_GAIN
|
|
44
|
+
AudioMixingMode.DO_NOT_MIX -> AudioManager.AUDIOFOCUS_GAIN
|
|
45
|
+
else -> AudioManager.AUDIOFOCUS_GAIN
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
49
|
+
// We already have audio focus
|
|
50
|
+
currentFocusRequest?.let {
|
|
51
|
+
if (it.focusGain == audioFocusType) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
val newFocusRequest = AudioFocusRequest.Builder(audioFocusType).run {
|
|
57
|
+
setAudioAttributes(
|
|
58
|
+
AudioAttributes.Builder().run {
|
|
59
|
+
setUsage(AudioAttributes.USAGE_MEDIA)
|
|
60
|
+
setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
|
|
61
|
+
setOnAudioFocusChangeListener(this@AudioFocusManager)
|
|
62
|
+
build()
|
|
63
|
+
}
|
|
64
|
+
).build()
|
|
65
|
+
}
|
|
66
|
+
currentFocusRequest = newFocusRequest
|
|
67
|
+
audioManager.requestAudioFocus(newFocusRequest)
|
|
68
|
+
} else {
|
|
69
|
+
@Suppress("DEPRECATION")
|
|
70
|
+
audioManager.requestAudioFocus(
|
|
71
|
+
this,
|
|
72
|
+
AudioManager.STREAM_MUSIC,
|
|
73
|
+
audioFocusType
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
currentMixingMode = audioMixingMode
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun abandonAudioFocus() {
|
|
80
|
+
currentFocusRequest?.let {
|
|
81
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
82
|
+
audioManager.abandonAudioFocusRequest(it)
|
|
83
|
+
} else {
|
|
84
|
+
@Suppress("DEPRECATION")
|
|
85
|
+
audioManager.abandonAudioFocus(this)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
currentFocusRequest = null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun registerPlayer(player: VideoPlayer) {
|
|
92
|
+
players.find { it.get() == player } ?: run {
|
|
93
|
+
players.add(WeakReference(player))
|
|
94
|
+
}
|
|
95
|
+
player.addListener(this)
|
|
96
|
+
updateAudioFocus()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fun unregisterPlayer(player: VideoPlayer) {
|
|
100
|
+
player.removeListener(this)
|
|
101
|
+
players.removeAll { it.get() == player }
|
|
102
|
+
updateAudioFocus()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// VideoPlayerListener
|
|
106
|
+
|
|
107
|
+
override fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {
|
|
108
|
+
requestAudioFocus()
|
|
109
|
+
super.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {
|
|
113
|
+
// we can't use `updateAudioFocus`, because when losing focus the videos are paused sequentially,
|
|
114
|
+
// which can lead to unexpected results.
|
|
115
|
+
if (!isPlaying && !anyPlayerRequiresFocus) {
|
|
116
|
+
abandonAudioFocus()
|
|
117
|
+
} else if (isPlaying && anyPlayerRequiresFocus) {
|
|
118
|
+
requestAudioFocus()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override fun onVolumeChanged(player: VideoPlayer, volume: Float, oldVolume: Float?) {
|
|
123
|
+
updateAudioFocus()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun onMutedChanged(player: VideoPlayer, muted: Boolean, oldMuted: Boolean?) {
|
|
127
|
+
updateAudioFocus()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// AudioManager.OnAudioFocusChangeListener
|
|
131
|
+
|
|
132
|
+
override fun onAudioFocusChange(focusChange: Int) {
|
|
133
|
+
when (focusChange) {
|
|
134
|
+
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
135
|
+
appContext.mainQueue.launch {
|
|
136
|
+
players.forEach {
|
|
137
|
+
pausePlayerIfUnmuted(it)
|
|
138
|
+
}
|
|
139
|
+
currentFocusRequest = null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
144
|
+
// W could pause/mix the players here individually, but we will keep the behaviour in line with iOS,
|
|
145
|
+
// find the dominant audioMixingMode and apply it to all players.
|
|
146
|
+
val audioMixingMode = findAudioMixingMode()
|
|
147
|
+
if (audioMixingMode == AudioMixingMode.MIX_WITH_OTHERS) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
appContext.mainQueue.launch {
|
|
151
|
+
players.forEach {
|
|
152
|
+
pausePlayerIfUnmuted(it)
|
|
153
|
+
}
|
|
154
|
+
currentFocusRequest = null
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
159
|
+
val audioMixingMode = findAudioMixingMode()
|
|
160
|
+
|
|
161
|
+
appContext.mainQueue.launch {
|
|
162
|
+
players.forEach {
|
|
163
|
+
if (audioMixingMode == AudioMixingMode.DO_NOT_MIX) {
|
|
164
|
+
pausePlayerIfUnmuted(it)
|
|
165
|
+
} else {
|
|
166
|
+
duckPlayer(it)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
173
|
+
// TODO: For now this behaves like iOS and doesn't resume playback automatically
|
|
174
|
+
// In future versions we can add a prop to control this behavior.
|
|
175
|
+
appContext.mainQueue.launch {
|
|
176
|
+
players.forEach {
|
|
177
|
+
unduckPlayer(it)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Utils
|
|
185
|
+
|
|
186
|
+
private fun playerRequiresFocus(weakPlayer: WeakReference<VideoPlayer>): Boolean {
|
|
187
|
+
val player = weakPlayer?.get() ?: return false // Return false if player is null
|
|
188
|
+
return (!player.muted && player.playing && player.volume > 0) || player.audioMixingMode == AudioMixingMode.DO_NOT_MIX
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private fun pausePlayerIfUnmuted(weakPlayer: WeakReference<VideoPlayer>) {
|
|
192
|
+
weakPlayer.get()?.let { videoPlayer ->
|
|
193
|
+
if (!videoPlayer.muted) {
|
|
194
|
+
appContext.mainQueue.launch {
|
|
195
|
+
videoPlayer.player.pause()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private fun duckPlayer(weakPlayer: WeakReference<VideoPlayer>) {
|
|
202
|
+
weakPlayer.get()?.let { player ->
|
|
203
|
+
appContext.mainQueue.launch {
|
|
204
|
+
player.volume /= 2f
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private fun unduckPlayer(weakPlayer: WeakReference<VideoPlayer>) {
|
|
210
|
+
weakPlayer.get()?.let { player ->
|
|
211
|
+
if (!player.muted) {
|
|
212
|
+
appContext.mainQueue.launch {
|
|
213
|
+
player.volume = player.userVolume
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private fun updateAudioFocus() {
|
|
220
|
+
if (anyPlayerRequiresFocus || findAudioMixingMode() != currentMixingMode) {
|
|
221
|
+
requestAudioFocus()
|
|
222
|
+
} else {
|
|
223
|
+
abandonAudioFocus()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private fun findAudioMixingMode(): AudioMixingMode {
|
|
228
|
+
val playersSnapshot = players.toList()
|
|
229
|
+
|
|
230
|
+
val mixingModes = playersSnapshot.mapNotNull { player ->
|
|
231
|
+
player.get()?.takeIf { it.playing }?.audioMixingMode
|
|
232
|
+
}
|
|
233
|
+
if (mixingModes.isEmpty()) {
|
|
234
|
+
return AudioMixingMode.MIX_WITH_OTHERS
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return mixingModes.reduce { currentAudioMixingMode, next ->
|
|
238
|
+
next.takeIf { it.priority > currentAudioMixingMode.priority } ?: currentAudioMixingMode
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.view.WindowInsets
|
|
10
|
+
import android.view.WindowInsetsController
|
|
11
|
+
import android.widget.ImageButton
|
|
12
|
+
import androidx.media3.ui.PlayerView
|
|
13
|
+
import expo.modules.kotlin.exception.CodedException
|
|
14
|
+
import expo.modules.video.player.VideoPlayer
|
|
15
|
+
import expo.modules.video.utils.applyPiPParams
|
|
16
|
+
import expo.modules.video.utils.applyRectHint
|
|
17
|
+
import expo.modules.video.utils.calculatePiPAspectRatio
|
|
18
|
+
import expo.modules.video.utils.calculateRectHint
|
|
19
|
+
|
|
20
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
21
|
+
class FullscreenPlayerActivity : Activity() {
|
|
22
|
+
private lateinit var mContentView: View
|
|
23
|
+
private lateinit var videoViewId: String
|
|
24
|
+
private var videoPlayer: VideoPlayer? = null
|
|
25
|
+
private lateinit var playerView: PlayerView
|
|
26
|
+
private lateinit var videoView: VideoView
|
|
27
|
+
private var didFinish = false
|
|
28
|
+
private var wasAutoPaused = false
|
|
29
|
+
|
|
30
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
31
|
+
super.onCreate(savedInstanceState)
|
|
32
|
+
setContentView(R.layout.fullscreen_player_activity)
|
|
33
|
+
mContentView = findViewById(R.id.enclosing_layout)
|
|
34
|
+
playerView = findViewById(R.id.player_view)
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY)
|
|
38
|
+
?: throw FullScreenVideoViewNotFoundException()
|
|
39
|
+
videoView = VideoManager.getVideoView(videoViewId)
|
|
40
|
+
} catch (e: CodedException) {
|
|
41
|
+
Log.e("ExpoVideo", "${e.message}", e)
|
|
42
|
+
finish()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
videoPlayer = videoView.videoPlayer
|
|
46
|
+
videoPlayer?.changePlayerView(playerView)
|
|
47
|
+
VideoManager.registerFullscreenPlayerActivity(hashCode().toString(), this)
|
|
48
|
+
playerView.player?.let {
|
|
49
|
+
val aspectRatio = calculatePiPAspectRatio(it.videoSize, playerView.width, playerView.height, videoView.contentFit)
|
|
50
|
+
applyPiPParams(this, videoView.autoEnterPiP, aspectRatio)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun onPostCreate(savedInstanceState: Bundle?) {
|
|
55
|
+
super.onPostCreate(savedInstanceState)
|
|
56
|
+
hideStatusBar()
|
|
57
|
+
setupFullscreenButton()
|
|
58
|
+
playerView.applyRequiresLinearPlayback(videoPlayer?.requiresLinearPlayback ?: false)
|
|
59
|
+
playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
60
|
+
// On every re-layout ExoPlayer makes the timeBar interactive.
|
|
61
|
+
// We need to disable it to keep scrubbing off.
|
|
62
|
+
playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true)
|
|
63
|
+
}
|
|
64
|
+
playerView.setShowSubtitleButton(videoView.showsSubtitlesButton)
|
|
65
|
+
|
|
66
|
+
playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
67
|
+
applyRectHint(this, calculateRectHint(playerView))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun finish() {
|
|
72
|
+
super.finish()
|
|
73
|
+
didFinish = true
|
|
74
|
+
VideoManager.getVideoView(videoViewId).attachPlayer()
|
|
75
|
+
|
|
76
|
+
// Disable the exit transition
|
|
77
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
78
|
+
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
|
|
79
|
+
} else {
|
|
80
|
+
@Suppress("DEPRECATION")
|
|
81
|
+
overridePendingTransition(0, 0)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override fun onResume() {
|
|
86
|
+
playerView.useController = videoView.useNativeControls
|
|
87
|
+
super.onResume()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun onPause() {
|
|
91
|
+
if (videoPlayer?.staysActiveInBackground != true && !didFinish) {
|
|
92
|
+
wasAutoPaused = videoPlayer?.player?.isPlaying == true
|
|
93
|
+
if (wasAutoPaused) {
|
|
94
|
+
playerView.useController = false
|
|
95
|
+
videoPlayer?.player?.pause()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
super.onPause()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun onDestroy() {
|
|
102
|
+
super.onDestroy()
|
|
103
|
+
videoView.exitFullscreen()
|
|
104
|
+
VideoManager.unregisterFullscreenPlayerActivity(hashCode().toString())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun setupFullscreenButton() {
|
|
108
|
+
playerView.setFullscreenButtonClickListener { finish() }
|
|
109
|
+
|
|
110
|
+
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
|
111
|
+
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
|
115
|
+
if (!isInPictureInPictureMode) {
|
|
116
|
+
playerView.useController = videoView.useNativeControls
|
|
117
|
+
} else {
|
|
118
|
+
playerView.useController = false
|
|
119
|
+
}
|
|
120
|
+
if (wasAutoPaused && isInPictureInPictureMode) {
|
|
121
|
+
videoPlayer?.player?.play()
|
|
122
|
+
}
|
|
123
|
+
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun hideStatusBar() {
|
|
127
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
128
|
+
val controller = mContentView.windowInsetsController
|
|
129
|
+
controller?.apply {
|
|
130
|
+
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
131
|
+
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
@Suppress("DEPRECATION")
|
|
135
|
+
mContentView.systemUiVisibility = (
|
|
136
|
+
View.SYSTEM_UI_FLAG_LOW_PROFILE
|
|
137
|
+
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
138
|
+
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
139
|
+
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
140
|
+
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
141
|
+
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import expo.modules.video.delegates.IgnoreSameSet
|
|
6
|
+
import java.lang.ref.WeakReference
|
|
7
|
+
|
|
8
|
+
fun interface IntervalUpdateEmitter {
|
|
9
|
+
fun emitTimeUpdate()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class IntervalUpdateClock(emitter: IntervalUpdateEmitter) {
|
|
13
|
+
private val emitter: WeakReference<IntervalUpdateEmitter> = WeakReference(emitter)
|
|
14
|
+
private var handler: Handler = Handler(Looper.getMainLooper())
|
|
15
|
+
|
|
16
|
+
// TODO: @behenate - Once the Player instance is available in OnStartObserving we can automatically start/stop the clock.
|
|
17
|
+
var interval by IgnoreSameSet(0L) { new: Long, _: Long? ->
|
|
18
|
+
if (new <= 0) {
|
|
19
|
+
stop()
|
|
20
|
+
} else {
|
|
21
|
+
startOrUpdate()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private var isRunning: Boolean = false
|
|
26
|
+
|
|
27
|
+
private fun stop() {
|
|
28
|
+
handler.removeCallbacksAndMessages(null)
|
|
29
|
+
isRunning = false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private fun startOrUpdate() {
|
|
33
|
+
if (!isRunning) {
|
|
34
|
+
emitter.get()?.emitTimeUpdate()
|
|
35
|
+
} else {
|
|
36
|
+
handler.removeCallbacksAndMessages(null)
|
|
37
|
+
}
|
|
38
|
+
isRunning = true
|
|
39
|
+
scheduleNextUpdate()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private fun scheduleNextUpdate() {
|
|
43
|
+
if (interval <= 0L) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
val update = {
|
|
48
|
+
emitter.get()?.emitTimeUpdate()
|
|
49
|
+
scheduleNextUpdate()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
handler.postDelayed(update, interval)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.media.MediaMetadataRetriever
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import expo.modules.video.records.VideoThumbnailOptions
|
|
7
|
+
import kotlin.math.max
|
|
8
|
+
import kotlin.math.roundToLong
|
|
9
|
+
import kotlin.time.Duration
|
|
10
|
+
import kotlin.time.DurationUnit
|
|
11
|
+
import kotlin.time.toDuration
|
|
12
|
+
|
|
13
|
+
suspend fun <T> MediaMetadataRetriever.safeUse(block: suspend MediaMetadataRetriever.() -> T): T {
|
|
14
|
+
try {
|
|
15
|
+
return block()
|
|
16
|
+
} finally {
|
|
17
|
+
try {
|
|
18
|
+
this.close()
|
|
19
|
+
} finally {
|
|
20
|
+
// ignore
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun MediaMetadataRetriever.generateThumbnailAtTime(
|
|
26
|
+
requestedTime: Duration,
|
|
27
|
+
options: VideoThumbnailOptions? = null
|
|
28
|
+
): VideoThumbnail {
|
|
29
|
+
val sizeLimit = options?.toNativeSizeLimit()
|
|
30
|
+
|
|
31
|
+
val bitmap = if (sizeLimit != null) {
|
|
32
|
+
val (maxWidth, maxHeight) = sizeLimit
|
|
33
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
34
|
+
getScaledFrameAtTime(
|
|
35
|
+
requestedTime.inWholeMicroseconds,
|
|
36
|
+
MediaMetadataRetriever.OPTION_CLOSEST,
|
|
37
|
+
maxWidth,
|
|
38
|
+
maxHeight
|
|
39
|
+
)
|
|
40
|
+
} else {
|
|
41
|
+
getFrameAtTime(requestedTime.inWholeMicroseconds, MediaMetadataRetriever.OPTION_CLOSEST)
|
|
42
|
+
?.constrainToDimensions(maxWidth, maxHeight)
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
getFrameAtTime(requestedTime.inWholeMicroseconds, MediaMetadataRetriever.OPTION_CLOSEST)
|
|
46
|
+
} ?: throw IllegalStateException("Failed to generate thumbnail")
|
|
47
|
+
|
|
48
|
+
val actualTime = calculateActualFrameTime(this, requestedTime)
|
|
49
|
+
return VideoThumbnail(bitmap, requestedTime, actualTime)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun calculateActualFrameTime(mediaMetadataRetriever: MediaMetadataRetriever, time: Duration): Duration {
|
|
53
|
+
// if we can't get the frame rate, we are returning the requested time
|
|
54
|
+
val frameTime = mediaMetadataRetriever.frameTime() ?: return time
|
|
55
|
+
|
|
56
|
+
// calculate closest frame index
|
|
57
|
+
val frameIndex = (time.inWholeMicroseconds.toDouble() / frameTime).roundToLong()
|
|
58
|
+
|
|
59
|
+
return (frameIndex * frameTime).toDuration(DurationUnit.MICROSECONDS)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun MediaMetadataRetriever.frameTime(): Double? {
|
|
63
|
+
// frame count is not available on Android SDK < 28
|
|
64
|
+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
val duration = this.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toDouble()
|
|
69
|
+
?: return null
|
|
70
|
+
val frameCount = this.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT)?.toDouble()
|
|
71
|
+
?: return null
|
|
72
|
+
|
|
73
|
+
return (duration * 1000) / frameCount
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun Bitmap.constrainToDimensions(maxWidth: Int, maxHeight: Int): Bitmap {
|
|
77
|
+
val width = this.width
|
|
78
|
+
val height = this.height
|
|
79
|
+
|
|
80
|
+
val ratio = max(width / maxWidth.toFloat(), height / maxHeight.toFloat())
|
|
81
|
+
if (ratio <= 1) {
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
val newWidth = (width / ratio).toInt()
|
|
86
|
+
val newHeight = (height / ratio).toInt()
|
|
87
|
+
|
|
88
|
+
return Bitmap.createScaledBitmap(this, newWidth, newHeight, true)
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import androidx.fragment.app.Fragment
|
|
4
|
+
import java.util.UUID
|
|
5
|
+
|
|
6
|
+
class PictureInPictureHelperFragment(private val videoView: VideoView) : Fragment() {
|
|
7
|
+
val id = "${PictureInPictureHelperFragment::class.java.simpleName}_${UUID.randomUUID()}"
|
|
8
|
+
|
|
9
|
+
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
|
10
|
+
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
|
11
|
+
|
|
12
|
+
if (isInPictureInPictureMode) {
|
|
13
|
+
// We can't reliably detect when the PiP transition starts (while keeping the transition smooth 🙄), so we have to
|
|
14
|
+
// unpause the playback after the onPause event, is called right after onPause. So the pause is not noticeable
|
|
15
|
+
if (videoView.wasAutoPaused) {
|
|
16
|
+
videoView.playerView.player?.play()
|
|
17
|
+
}
|
|
18
|
+
videoView.layoutForPiPEnter()
|
|
19
|
+
videoView.onPictureInPictureStart(Unit)
|
|
20
|
+
} else {
|
|
21
|
+
videoView.willEnterPiP = false
|
|
22
|
+
videoView.layoutForPiPExit()
|
|
23
|
+
videoView.onPictureInPictureStop(Unit)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import androidx.media3.ui.DefaultTimeBar
|
|
5
|
+
import androidx.media3.ui.PlayerView
|
|
6
|
+
|
|
7
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
8
|
+
internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boolean) {
|
|
9
|
+
setShowFastForwardButton(!requireLinearPlayback)
|
|
10
|
+
setShowRewindButton(!requireLinearPlayback)
|
|
11
|
+
setShowPreviousButton(!requireLinearPlayback)
|
|
12
|
+
setShowNextButton(!requireLinearPlayback)
|
|
13
|
+
setTimeBarInteractive(requireLinearPlayback)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
17
|
+
internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
|
18
|
+
val timeBar = findViewById<DefaultTimeBar>(androidx.media3.ui.R.id.exo_progress)
|
|
19
|
+
if (interactive) {
|
|
20
|
+
timeBar?.setScrubberColor(Color.TRANSPARENT)
|
|
21
|
+
timeBar?.isEnabled = false
|
|
22
|
+
} else {
|
|
23
|
+
timeBar?.setScrubberColor(Color.WHITE)
|
|
24
|
+
timeBar?.isEnabled = true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
29
|
+
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
|
30
|
+
val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
|
31
|
+
fullscreenButton?.visibility = if (visible) {
|
|
32
|
+
android.view.View.VISIBLE
|
|
33
|
+
} else {
|
|
34
|
+
android.view.View.GONE
|
|
35
|
+
}
|
|
36
|
+
}
|