@stepincto/expo-video 1.0.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 +45 -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 +16 -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 +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -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 +44 -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 +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -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 +383 -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 +59 -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 +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -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 +46 -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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import androidx.media3.common.util.UnstableApi
|
|
7
|
+
import androidx.media3.database.DatabaseProvider
|
|
8
|
+
import androidx.media3.database.StandaloneDatabaseProvider
|
|
9
|
+
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
|
10
|
+
import androidx.media3.datasource.cache.SimpleCache
|
|
11
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
12
|
+
import java.io.File
|
|
13
|
+
import java.lang.ref.WeakReference
|
|
14
|
+
import java.util.UUID
|
|
15
|
+
|
|
16
|
+
private const val SHARED_PREFERENCES_NAME = "ExpoVideoCache"
|
|
17
|
+
private const val CACHE_SIZE_KEY = "cacheSize"
|
|
18
|
+
private const val VIDEO_CACHE_PARENT_DIR = "ExpoVideoCache"
|
|
19
|
+
private const val VIDEO_CACHE_DIR_KEY = "cacheDir"
|
|
20
|
+
private const val DEFAULT_CACHE_SIZE = 1024 * 1024 * 1024L // 1GB
|
|
21
|
+
|
|
22
|
+
@UnstableApi
|
|
23
|
+
class VideoCache(context: Context) {
|
|
24
|
+
// We don't want a strong reference to the context, as this class is used inside of a singleton (VideoManager)
|
|
25
|
+
private val weakContext = WeakReference(context)
|
|
26
|
+
private val context: Context
|
|
27
|
+
get() {
|
|
28
|
+
return weakContext.get() ?: throw Exceptions.ReactContextLost()
|
|
29
|
+
}
|
|
30
|
+
private val databaseProvider: DatabaseProvider = StandaloneDatabaseProvider(context)
|
|
31
|
+
private val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
32
|
+
private var cacheEvictor = LeastRecentlyUsedCacheEvictor(getMaxCacheSize())
|
|
33
|
+
var instance = SimpleCache(getCacheDir(), cacheEvictor, databaseProvider)
|
|
34
|
+
|
|
35
|
+
// Function that gets the cache size from shared preferences
|
|
36
|
+
private fun getMaxCacheSize(): Long {
|
|
37
|
+
return sharedPreferences.getLong(CACHE_SIZE_KEY, DEFAULT_CACHE_SIZE)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun setMaxCacheSize(size: Long) {
|
|
41
|
+
assertModificationReleaseConditions()
|
|
42
|
+
instance.release()
|
|
43
|
+
sharedPreferences.edit().putLong(CACHE_SIZE_KEY, size).apply()
|
|
44
|
+
cacheEvictor = LeastRecentlyUsedCacheEvictor(size)
|
|
45
|
+
instance = SimpleCache(getCacheDir(), cacheEvictor, databaseProvider)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun getCurrentCacheSize(): Long {
|
|
49
|
+
return getFileSize(getCacheDir())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// We have to keep the current directory name in the shared preferences, when the cache is released, a new name will be assigned to avoid conflicts.
|
|
53
|
+
private fun getCacheDir(): File {
|
|
54
|
+
// Weird structure, because kotlin marks the result of `getString` as nullable
|
|
55
|
+
val videoCacheDirName = sharedPreferences.getString(VIDEO_CACHE_DIR_KEY, null) ?: run {
|
|
56
|
+
val newCacheDirName = generateCacheDirName()
|
|
57
|
+
sharedPreferences.edit().putString(VIDEO_CACHE_DIR_KEY, newCacheDirName).apply()
|
|
58
|
+
newCacheDirName
|
|
59
|
+
}
|
|
60
|
+
val cacheParentDir = File(context.cacheDir, VIDEO_CACHE_PARENT_DIR)
|
|
61
|
+
val cacheDir = File(cacheParentDir, videoCacheDirName)
|
|
62
|
+
|
|
63
|
+
if (!cacheDir.exists()) {
|
|
64
|
+
cacheDir.mkdirs()
|
|
65
|
+
}
|
|
66
|
+
return cacheDir
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fun generateCacheDirName(): String {
|
|
70
|
+
return UUID.randomUUID().toString()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun clear() {
|
|
74
|
+
assertModificationReleaseConditions()
|
|
75
|
+
|
|
76
|
+
// Creates a new cache directory to avoid conflicts while removing the old cache
|
|
77
|
+
val oldCacheDirectory = getCacheDir()
|
|
78
|
+
val oldCache = instance
|
|
79
|
+
val newCacheName = generateCacheDirName()
|
|
80
|
+
|
|
81
|
+
sharedPreferences.edit().putString(VIDEO_CACHE_DIR_KEY, newCacheName).apply()
|
|
82
|
+
instance = SimpleCache(getCacheDir(), cacheEvictor, databaseProvider)
|
|
83
|
+
oldCache.release()
|
|
84
|
+
oldCacheDirectory.deleteRecursively()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun getFileSize(file: File): Long {
|
|
88
|
+
return file
|
|
89
|
+
.walkTopDown()
|
|
90
|
+
.filter { it.isFile }
|
|
91
|
+
.map { it.length() }
|
|
92
|
+
.sum()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fun assertModificationReleaseConditions() {
|
|
96
|
+
if (VideoManager.hasRegisteredPlayers()) {
|
|
97
|
+
throw VideoCacheException("Cannot clear cache while there are active players")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
101
|
+
Log.w("ExpoVideo", "Clearing cache on the main thread, this might cause performance issues")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.exception.CodedException
|
|
4
|
+
import expo.modules.video.enums.DRMType
|
|
5
|
+
|
|
6
|
+
internal class FullScreenVideoViewNotFoundException :
|
|
7
|
+
CodedException("VideoView id wasn't passed to the activity")
|
|
8
|
+
|
|
9
|
+
internal class VideoViewNotFoundException(id: String) :
|
|
10
|
+
CodedException("VideoView with id: $id not found")
|
|
11
|
+
|
|
12
|
+
internal class MethodUnsupportedException(methodName: String) :
|
|
13
|
+
CodedException("Method `$methodName` is not supported on Android")
|
|
14
|
+
|
|
15
|
+
internal class PictureInPictureEnterException(message: String?) :
|
|
16
|
+
CodedException("Failed to enter Picture in Picture mode${message?.let { ". $message" } ?: ""}")
|
|
17
|
+
|
|
18
|
+
internal class PictureInPictureConfigurationException :
|
|
19
|
+
CodedException("Current activity does not support picture-in-picture. Make sure you have configured the `expo-video` config plugin correctly.")
|
|
20
|
+
|
|
21
|
+
internal class PictureInPictureUnsupportedException :
|
|
22
|
+
CodedException("Picture in Picture mode is not supported on this device")
|
|
23
|
+
|
|
24
|
+
internal class UnsupportedDRMTypeException(type: DRMType) :
|
|
25
|
+
CodedException("DRM type `$type` is not supported on Android")
|
|
26
|
+
|
|
27
|
+
internal class PlaybackException(reason: String?, cause: Throwable? = null) :
|
|
28
|
+
CodedException("A playback exception has occurred: ${reason ?: "reason unknown"}", cause)
|
|
29
|
+
|
|
30
|
+
internal class FailedToGetAudioFocusManagerException :
|
|
31
|
+
CodedException("Failed to get AudioFocusManager service")
|
|
32
|
+
|
|
33
|
+
internal class VideoCacheException(message: String?, cause: Throwable? = null) :
|
|
34
|
+
CodedException(message ?: "Unexpected expo-video cache error", cause)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.OptIn
|
|
4
|
+
import androidx.media3.common.util.UnstableApi
|
|
5
|
+
import expo.modules.kotlin.AppContext
|
|
6
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
7
|
+
import expo.modules.video.player.VideoPlayer
|
|
8
|
+
import java.lang.ref.WeakReference
|
|
9
|
+
|
|
10
|
+
// Helper class used to keep track of all existing VideoViews and VideoPlayers
|
|
11
|
+
@OptIn(UnstableApi::class)
|
|
12
|
+
object VideoManager {
|
|
13
|
+
const val INTENT_PLAYER_KEY = "player_uuid"
|
|
14
|
+
|
|
15
|
+
// Used for sharing videoViews between VideoView and FullscreenPlayerActivity
|
|
16
|
+
private var videoViews = mutableMapOf<String, VideoView>()
|
|
17
|
+
private var fullscreenPlayerActivities = mutableMapOf<String, WeakReference<FullscreenPlayerActivity>>()
|
|
18
|
+
|
|
19
|
+
// Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
|
|
20
|
+
private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()
|
|
21
|
+
|
|
22
|
+
private lateinit var audioFocusManager: AudioFocusManager
|
|
23
|
+
lateinit var cache: VideoCache
|
|
24
|
+
|
|
25
|
+
fun onModuleCreated(appContext: AppContext) {
|
|
26
|
+
val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
27
|
+
|
|
28
|
+
if (!this::audioFocusManager.isInitialized) {
|
|
29
|
+
audioFocusManager = AudioFocusManager(appContext)
|
|
30
|
+
}
|
|
31
|
+
if (!this::cache.isInitialized) {
|
|
32
|
+
cache = VideoCache(context)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun registerVideoView(videoView: VideoView) {
|
|
37
|
+
videoViews[videoView.videoViewId] = videoView
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun getVideoView(id: String): VideoView {
|
|
41
|
+
return videoViews[id] ?: throw VideoViewNotFoundException(id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun unregisterVideoView(videoView: VideoView) {
|
|
45
|
+
videoViews.remove(videoView.videoViewId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun registerVideoPlayer(videoPlayer: VideoPlayer) {
|
|
49
|
+
videoPlayersToVideoViews[videoPlayer] = videoPlayersToVideoViews[videoPlayer] ?: mutableListOf()
|
|
50
|
+
audioFocusManager.registerPlayer(videoPlayer)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun unregisterVideoPlayer(videoPlayer: VideoPlayer) {
|
|
54
|
+
videoPlayersToVideoViews.remove(videoPlayer)
|
|
55
|
+
audioFocusManager.unregisterPlayer(videoPlayer)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun registerFullscreenPlayerActivity(id: String, fullscreenActivity: FullscreenPlayerActivity) {
|
|
59
|
+
fullscreenPlayerActivities[id] = WeakReference(fullscreenActivity)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fun unregisterFullscreenPlayerActivity(id: String) {
|
|
63
|
+
fullscreenPlayerActivities.remove(id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) {
|
|
67
|
+
if (videoPlayersToVideoViews[videoPlayer]?.contains(videoView) == true) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
videoPlayersToVideoViews[videoPlayer]?.add(videoView) ?: run {
|
|
71
|
+
videoPlayersToVideoViews[videoPlayer] = arrayListOf(videoView)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (videoPlayersToVideoViews[videoPlayer]?.size == 1) {
|
|
75
|
+
videoPlayer.serviceConnection.playbackServiceBinder?.service?.registerPlayer(videoPlayer)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fun onVideoPlayerDetachedFromView(videoPlayer: VideoPlayer, videoView: VideoView) {
|
|
80
|
+
videoPlayersToVideoViews[videoPlayer]?.remove(videoView)
|
|
81
|
+
|
|
82
|
+
// Unregister disconnected VideoPlayers from the playback service
|
|
83
|
+
if (videoPlayersToVideoViews[videoPlayer] == null || videoPlayersToVideoViews[videoPlayer]?.size == 0) {
|
|
84
|
+
videoPlayer.serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(videoPlayer.player)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun isVideoPlayerAttachedToView(videoPlayer: VideoPlayer): Boolean {
|
|
89
|
+
return videoPlayersToVideoViews[videoPlayer]?.isNotEmpty() ?: false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun hasRegisteredPlayers(): Boolean {
|
|
93
|
+
return videoPlayersToVideoViews.isNotEmpty()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fun onAppForegrounded() {
|
|
97
|
+
for (videoView in videoViews.values) {
|
|
98
|
+
videoView.playerView.useController = videoView.useNativeControls
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Pressing the app icon will bring up the mainActivity instead of the fullscreen activity (at least for BareExpo)
|
|
102
|
+
// In this case we have to manually finish the fullscreen activity
|
|
103
|
+
for (fullscreenActivity in fullscreenPlayerActivities.values) {
|
|
104
|
+
fullscreenActivity.get()?.finish()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fun onAppBackgrounded() {
|
|
109
|
+
for (videoView in videoViews.values) {
|
|
110
|
+
if (shouldPauseVideo(videoView)) {
|
|
111
|
+
handleVideoPause(videoView)
|
|
112
|
+
} else {
|
|
113
|
+
videoView.wasAutoPaused = false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private fun shouldPauseVideo(videoView: VideoView): Boolean {
|
|
119
|
+
return videoView.videoPlayer?.staysActiveInBackground == false &&
|
|
120
|
+
!videoView.willEnterPiP &&
|
|
121
|
+
!videoView.isInFullscreen
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private fun handleVideoPause(videoView: VideoView) {
|
|
125
|
+
videoView.playerView.useController = false
|
|
126
|
+
videoView.videoPlayer?.player?.let { player ->
|
|
127
|
+
if (player.isPlaying) {
|
|
128
|
+
player.pause()
|
|
129
|
+
videoView.wasAutoPaused = true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
@file:OptIn(EitherType::class)
|
|
2
|
+
|
|
3
|
+
package expo.modules.video
|
|
4
|
+
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import androidx.media3.common.PlaybackParameters
|
|
7
|
+
import androidx.media3.common.Player.REPEAT_MODE_OFF
|
|
8
|
+
import androidx.media3.common.Player.REPEAT_MODE_ONE
|
|
9
|
+
import androidx.media3.common.util.UnstableApi
|
|
10
|
+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
11
|
+
import expo.modules.kotlin.Promise
|
|
12
|
+
import expo.modules.kotlin.apifeatures.EitherType
|
|
13
|
+
import expo.modules.kotlin.functions.Coroutine
|
|
14
|
+
import expo.modules.kotlin.functions.Queues
|
|
15
|
+
import expo.modules.kotlin.modules.Module
|
|
16
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
17
|
+
import expo.modules.kotlin.types.Either
|
|
18
|
+
import expo.modules.kotlin.views.ViewDefinitionBuilder
|
|
19
|
+
import expo.modules.video.enums.AudioMixingMode
|
|
20
|
+
import expo.modules.video.enums.ContentFit
|
|
21
|
+
import expo.modules.video.player.VideoPlayer
|
|
22
|
+
import expo.modules.video.records.BufferOptions
|
|
23
|
+
import expo.modules.video.records.SubtitleTrack
|
|
24
|
+
import expo.modules.video.records.AudioTrack
|
|
25
|
+
import expo.modules.video.records.VideoSource
|
|
26
|
+
import expo.modules.video.records.VideoThumbnailOptions
|
|
27
|
+
import expo.modules.video.utils.runWithPiPMisconfigurationSoftHandling
|
|
28
|
+
import kotlinx.coroutines.async
|
|
29
|
+
import kotlinx.coroutines.awaitAll
|
|
30
|
+
import kotlinx.coroutines.launch
|
|
31
|
+
import kotlinx.coroutines.runBlocking
|
|
32
|
+
import kotlin.time.Duration
|
|
33
|
+
|
|
34
|
+
// https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
|
|
35
|
+
@UnstableReactNativeAPI
|
|
36
|
+
@androidx.annotation.OptIn(UnstableApi::class)
|
|
37
|
+
class VideoModule : Module() {
|
|
38
|
+
override fun definition() = ModuleDefinition {
|
|
39
|
+
Name("ExpoVideo")
|
|
40
|
+
|
|
41
|
+
OnCreate {
|
|
42
|
+
VideoManager.onModuleCreated(appContext)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Function("isPictureInPictureSupported") {
|
|
46
|
+
VideoView.isPictureInPictureSupported(appContext.throwingActivity)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Function("getCurrentVideoCacheSize") {
|
|
50
|
+
VideoManager.cache.getCurrentCacheSize()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
AsyncFunction("setVideoCacheSizeAsync") { size: Long ->
|
|
54
|
+
VideoManager.cache.setMaxCacheSize(size)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
AsyncFunction("clearVideoCacheAsync") {
|
|
58
|
+
VideoManager.cache.clear()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
View(SurfaceVideoView::class) {
|
|
62
|
+
VideoViewComponent<SurfaceVideoView>()
|
|
63
|
+
}
|
|
64
|
+
View(TextureVideoView::class) {
|
|
65
|
+
VideoViewComponent<TextureVideoView>()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Class(VideoPlayer::class) {
|
|
69
|
+
Constructor { source: VideoSource? ->
|
|
70
|
+
val player = VideoPlayer(appContext.throwingActivity.applicationContext, appContext, source)
|
|
71
|
+
appContext.mainQueue.launch {
|
|
72
|
+
player.prepare()
|
|
73
|
+
}
|
|
74
|
+
return@Constructor player
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Property("playing")
|
|
78
|
+
.get { ref: VideoPlayer ->
|
|
79
|
+
ref.playing
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Property("muted")
|
|
83
|
+
.get { ref: VideoPlayer ->
|
|
84
|
+
ref.muted
|
|
85
|
+
}
|
|
86
|
+
.set { ref: VideoPlayer, muted: Boolean ->
|
|
87
|
+
appContext.mainQueue.launch {
|
|
88
|
+
ref.muted = muted
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Property("volume")
|
|
93
|
+
.get { ref: VideoPlayer ->
|
|
94
|
+
ref.volume
|
|
95
|
+
}
|
|
96
|
+
.set { ref: VideoPlayer, volume: Float ->
|
|
97
|
+
appContext.mainQueue.launch {
|
|
98
|
+
ref.userVolume = volume
|
|
99
|
+
ref.volume = volume
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Property("currentTime")
|
|
104
|
+
.get { ref: VideoPlayer ->
|
|
105
|
+
// TODO: we shouldn't block the thread, but there are no events for the player position change,
|
|
106
|
+
// so we can't update the currentTime in a non-blocking way like the other properties.
|
|
107
|
+
// Until we think of something better we can temporarily do it this way
|
|
108
|
+
runBlocking(appContext.mainQueue.coroutineContext) {
|
|
109
|
+
ref.player.currentPosition / 1000f
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
.set { ref: VideoPlayer, currentTime: Double ->
|
|
113
|
+
appContext.mainQueue.launch {
|
|
114
|
+
ref.player.seekTo((currentTime * 1000).toLong())
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Property("currentLiveTimestamp")
|
|
119
|
+
.get { ref: VideoPlayer ->
|
|
120
|
+
runBlocking(appContext.mainQueue.coroutineContext) {
|
|
121
|
+
ref.currentLiveTimestamp
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Property("availableVideoTracks")
|
|
126
|
+
.get { ref: VideoPlayer ->
|
|
127
|
+
ref.availableVideoTracks
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Property("videoTrack")
|
|
131
|
+
.get { ref: VideoPlayer ->
|
|
132
|
+
ref.currentVideoTrack
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Property("availableSubtitleTracks")
|
|
136
|
+
.get { ref: VideoPlayer ->
|
|
137
|
+
ref.subtitles.availableSubtitleTracks
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
Property("subtitleTrack")
|
|
141
|
+
.get { ref: VideoPlayer ->
|
|
142
|
+
ref.subtitles.currentSubtitleTrack
|
|
143
|
+
}
|
|
144
|
+
.set { ref: VideoPlayer, subtitleTrack: SubtitleTrack? ->
|
|
145
|
+
appContext.mainQueue.launch {
|
|
146
|
+
ref.subtitles.currentSubtitleTrack = subtitleTrack
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Property("availableAudioTracks")
|
|
151
|
+
.get { ref: VideoPlayer ->
|
|
152
|
+
ref.audioTracks.availableAudioTracks
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Property("audioTrack")
|
|
156
|
+
.get { ref: VideoPlayer ->
|
|
157
|
+
ref.audioTracks.currentAudioTrack
|
|
158
|
+
}
|
|
159
|
+
.set { ref: VideoPlayer, audioTrack: AudioTrack? ->
|
|
160
|
+
appContext.mainQueue.launch {
|
|
161
|
+
ref.audioTracks.currentAudioTrack = audioTrack
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Property("currentOffsetFromLive")
|
|
166
|
+
.get { ref: VideoPlayer ->
|
|
167
|
+
runBlocking(appContext.mainQueue.coroutineContext) {
|
|
168
|
+
ref.currentOffsetFromLive
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Property("duration")
|
|
173
|
+
.get { ref: VideoPlayer ->
|
|
174
|
+
ref.duration
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Property("playbackRate")
|
|
178
|
+
.get { ref: VideoPlayer ->
|
|
179
|
+
ref.playbackParameters.speed
|
|
180
|
+
}
|
|
181
|
+
.set { ref: VideoPlayer, playbackRate: Float ->
|
|
182
|
+
appContext.mainQueue.launch {
|
|
183
|
+
val pitch = if (ref.preservesPitch) 1f else playbackRate
|
|
184
|
+
ref.playbackParameters = PlaybackParameters(playbackRate, pitch)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Property("isLive")
|
|
189
|
+
.get { ref: VideoPlayer ->
|
|
190
|
+
ref.isLive
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Property("preservesPitch")
|
|
194
|
+
.get { ref: VideoPlayer ->
|
|
195
|
+
ref.preservesPitch
|
|
196
|
+
}
|
|
197
|
+
.set { ref: VideoPlayer, preservesPitch: Boolean ->
|
|
198
|
+
appContext.mainQueue.launch {
|
|
199
|
+
ref.preservesPitch = preservesPitch
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Property("showNowPlayingNotification")
|
|
204
|
+
.get { ref: VideoPlayer ->
|
|
205
|
+
ref.showNowPlayingNotification
|
|
206
|
+
}
|
|
207
|
+
.set { ref: VideoPlayer, showNotification: Boolean ->
|
|
208
|
+
appContext.mainQueue.launch {
|
|
209
|
+
ref.showNowPlayingNotification = showNotification
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Property("status")
|
|
214
|
+
.get { ref: VideoPlayer ->
|
|
215
|
+
ref.status
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Property("staysActiveInBackground")
|
|
219
|
+
.get { ref: VideoPlayer ->
|
|
220
|
+
ref.staysActiveInBackground
|
|
221
|
+
}
|
|
222
|
+
.set { ref: VideoPlayer, staysActive: Boolean ->
|
|
223
|
+
ref.staysActiveInBackground = staysActive
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
Property("loop")
|
|
227
|
+
.get { ref: VideoPlayer ->
|
|
228
|
+
ref.player.repeatMode == REPEAT_MODE_ONE
|
|
229
|
+
}
|
|
230
|
+
.set { ref: VideoPlayer, loop: Boolean ->
|
|
231
|
+
appContext.mainQueue.launch {
|
|
232
|
+
ref.player.repeatMode = if (loop) {
|
|
233
|
+
REPEAT_MODE_ONE
|
|
234
|
+
} else {
|
|
235
|
+
REPEAT_MODE_OFF
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Property("bufferedPosition")
|
|
241
|
+
.get { ref: VideoPlayer ->
|
|
242
|
+
// Same as currentTime
|
|
243
|
+
runBlocking(appContext.mainQueue.coroutineContext) {
|
|
244
|
+
ref.bufferedPosition
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Property("bufferOptions")
|
|
249
|
+
.get { ref: VideoPlayer ->
|
|
250
|
+
ref.bufferOptions
|
|
251
|
+
}
|
|
252
|
+
.set { ref: VideoPlayer, bufferOptions: BufferOptions ->
|
|
253
|
+
ref.bufferOptions = bufferOptions
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Function("play") { ref: VideoPlayer ->
|
|
257
|
+
appContext.mainQueue.launch {
|
|
258
|
+
ref.player.play()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
Function("pause") { ref: VideoPlayer ->
|
|
263
|
+
appContext.mainQueue.launch {
|
|
264
|
+
ref.player.pause()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
Property("timeUpdateEventInterval")
|
|
269
|
+
.get { ref: VideoPlayer ->
|
|
270
|
+
ref.intervalUpdateClock.interval / 1000.0
|
|
271
|
+
}
|
|
272
|
+
.set { ref: VideoPlayer, intervalSeconds: Float ->
|
|
273
|
+
ref.intervalUpdateClock.interval = (intervalSeconds * 1000).toLong()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
Property("audioMixingMode")
|
|
277
|
+
.get { ref: VideoPlayer ->
|
|
278
|
+
ref.audioMixingMode
|
|
279
|
+
}
|
|
280
|
+
.set { ref: VideoPlayer, audioMixingMode: AudioMixingMode ->
|
|
281
|
+
appContext.mainQueue.launch {
|
|
282
|
+
ref.audioMixingMode = audioMixingMode
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Function("replace") { ref: VideoPlayer, source: Either<Uri, VideoSource>? ->
|
|
287
|
+
replaceImpl(ref, source)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ExoPlayer automatically offloads loading of the asset onto a different thread so we can keep the same
|
|
291
|
+
// implementation until `replace` is deprecated and removed.
|
|
292
|
+
// TODO: @behenate see if we can further reduce load on the main thread
|
|
293
|
+
AsyncFunction("replaceAsync") { ref: VideoPlayer, source: Either<Uri, VideoSource>?, promise: Promise ->
|
|
294
|
+
replaceImpl(ref, source, promise)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
Function("seekBy") { ref: VideoPlayer, seekTime: Double ->
|
|
298
|
+
appContext.mainQueue.launch {
|
|
299
|
+
val seekPos = ref.player.currentPosition + (seekTime * 1000).toLong()
|
|
300
|
+
ref.player.seekTo(seekPos)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Function("replay") { ref: VideoPlayer ->
|
|
305
|
+
appContext.mainQueue.launch {
|
|
306
|
+
ref.player.seekTo(0)
|
|
307
|
+
ref.player.play()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
AsyncFunction("generateThumbnailsAsync") Coroutine { ref: VideoPlayer, times: List<Duration>, options: VideoThumbnailOptions? ->
|
|
312
|
+
return@Coroutine ref.toMetadataRetriever().safeUse {
|
|
313
|
+
val bitmaps = times.map { time ->
|
|
314
|
+
appContext.backgroundCoroutineScope.async {
|
|
315
|
+
generateThumbnailAtTime(time, options)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
bitmaps.awaitAll()
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
Class<VideoThumbnail> {
|
|
324
|
+
Property("width") { ref -> ref.width }
|
|
325
|
+
Property("height") { ref -> ref.height }
|
|
326
|
+
Property("requestedTime") { ref -> ref.requestedTime }
|
|
327
|
+
Property("actualTime") { ref -> ref.actualTime }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
OnActivityEntersForeground {
|
|
332
|
+
VideoManager.onAppForegrounded()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
OnActivityEntersBackground {
|
|
336
|
+
VideoManager.onAppBackgrounded()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
private fun replaceImpl(
|
|
340
|
+
ref: VideoPlayer,
|
|
341
|
+
source: Either<Uri, VideoSource>?,
|
|
342
|
+
promise: Promise? = null
|
|
343
|
+
) {
|
|
344
|
+
val videoSource = source?.let {
|
|
345
|
+
if (it.`is`(VideoSource::class)) {
|
|
346
|
+
it.get(VideoSource::class)
|
|
347
|
+
} else {
|
|
348
|
+
VideoSource(it.get(Uri::class))
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
appContext.mainQueue.launch {
|
|
353
|
+
ref.uncommittedSource = videoSource
|
|
354
|
+
ref.prepare()
|
|
355
|
+
promise?.resolve()
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@androidx.annotation.OptIn(UnstableApi::class)
|
|
361
|
+
private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewComponent() {
|
|
362
|
+
Events(
|
|
363
|
+
"onPictureInPictureStart",
|
|
364
|
+
"onPictureInPictureStop",
|
|
365
|
+
"onFullscreenEnter",
|
|
366
|
+
"onFullscreenExit",
|
|
367
|
+
"onFirstFrameRender"
|
|
368
|
+
)
|
|
369
|
+
Prop("player") { view: T, player: VideoPlayer ->
|
|
370
|
+
view.videoPlayer = player
|
|
371
|
+
}
|
|
372
|
+
Prop("nativeControls") { view: T, useNativeControls: Boolean ->
|
|
373
|
+
view.useNativeControls = useNativeControls
|
|
374
|
+
}
|
|
375
|
+
Prop("contentFit") { view: T, contentFit: ContentFit ->
|
|
376
|
+
view.contentFit = contentFit
|
|
377
|
+
}
|
|
378
|
+
Prop("startsPictureInPictureAutomatically") { view: T, autoEnterPiP: Boolean ->
|
|
379
|
+
view.autoEnterPiP = autoEnterPiP
|
|
380
|
+
}
|
|
381
|
+
Prop("allowsFullscreen") { view: T, allowsFullscreen: Boolean? ->
|
|
382
|
+
view.allowsFullscreen = allowsFullscreen ?: true
|
|
383
|
+
}
|
|
384
|
+
Prop("requiresLinearPlayback") { view: T, requiresLinearPlayback: Boolean? ->
|
|
385
|
+
val linearPlayback = requiresLinearPlayback ?: false
|
|
386
|
+
view.playerView.applyRequiresLinearPlayback(linearPlayback)
|
|
387
|
+
view.videoPlayer?.requiresLinearPlayback = linearPlayback
|
|
388
|
+
}
|
|
389
|
+
Prop("useExoShutter") { view: T, useExoShutter: Boolean? ->
|
|
390
|
+
view.useExoShutter = useExoShutter
|
|
391
|
+
}
|
|
392
|
+
AsyncFunction("enterFullscreen") { view: T ->
|
|
393
|
+
view.enterFullscreen()
|
|
394
|
+
}.runOnQueue(Queues.MAIN)
|
|
395
|
+
AsyncFunction("exitFullscreen") {
|
|
396
|
+
throw MethodUnsupportedException("exitFullscreen")
|
|
397
|
+
}
|
|
398
|
+
AsyncFunction("startPictureInPicture") { view: T ->
|
|
399
|
+
runWithPiPMisconfigurationSoftHandling(true) {
|
|
400
|
+
view.enterPictureInPicture()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
AsyncFunction("stopPictureInPicture") {
|
|
404
|
+
throw MethodUnsupportedException("stopPictureInPicture")
|
|
405
|
+
}
|
|
406
|
+
OnViewDestroys {
|
|
407
|
+
VideoManager.unregisterVideoView(it)
|
|
408
|
+
}
|
|
409
|
+
OnViewDidUpdateProps { view ->
|
|
410
|
+
if (view.playerView.useController != view.useNativeControls) {
|
|
411
|
+
view.playerView.useController = view.useNativeControls
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import expo.modules.kotlin.sharedobjects.SharedRef
|
|
5
|
+
import kotlin.time.Duration
|
|
6
|
+
|
|
7
|
+
class VideoThumbnail(
|
|
8
|
+
ref: Bitmap,
|
|
9
|
+
val requestedTime: Duration,
|
|
10
|
+
val actualTime: Duration
|
|
11
|
+
) : SharedRef<Bitmap>(ref) {
|
|
12
|
+
val width = ref.width
|
|
13
|
+
val height = ref.height
|
|
14
|
+
|
|
15
|
+
override val nativeRefType: String = "image"
|
|
16
|
+
|
|
17
|
+
override fun getAdditionalMemoryPressure(): Int {
|
|
18
|
+
return ref.byteCount
|
|
19
|
+
}
|
|
20
|
+
}
|