expo-video 1.0.2 → 1.1.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/.eslintrc.js +2 -0
- package/CHANGELOG.md +56 -0
- package/README.md +33 -0
- package/android/build.gradle +33 -0
- package/android/src/main/AndroidManifest.xml +16 -0
- package/android/src/main/java/expo/modules/video/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/ExpoVideoPlaybackService.kt +140 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +82 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +21 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +88 -0
- package/android/src/main/java/expo/modules/video/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +285 -0
- package/android/src/main/java/expo/modules/video/VideoPlayer.kt +266 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +397 -0
- package/android/src/main/java/expo/modules/video/YogaUtils.kt +20 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +212 -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/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/VideoSource.kt +35 -0
- package/android/src/main/java/expo/modules/video/records/VolumeEvent.kt +10 -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/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +3 -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 +3 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +3 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoView.d.ts +41 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +77 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +248 -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 +44 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +283 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/ContentKeyDelegate.swift +200 -0
- package/ios/ContentKeyManager.swift +21 -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 +26 -0
- package/ios/NowPlayingManager.swift +204 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/VideoSource.swift +11 -0
- package/ios/Records/VolumeEvent.swift +14 -0
- package/ios/VideoExceptions.swift +33 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +66 -0
- package/ios/VideoModule.swift +192 -0
- package/ios/VideoPlayer.swift +172 -0
- package/ios/VideoPlayerItem.swift +11 -0
- package/ios/VideoPlayerObserver.swift +211 -0
- package/ios/VideoView.swift +152 -0
- package/package.json +26 -25
- package/plugin/build/withExpoVideo.d.ts +3 -0
- package/plugin/build/withExpoVideo.js +19 -0
- package/plugin/jest.config.js +1 -0
- package/plugin/src/withExpoVideo.ts +25 -0
- package/plugin/tsconfig.json +9 -0
- package/src/NativeVideoModule.ts +3 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +3 -0
- package/src/VideoView.tsx +92 -0
- package/src/VideoView.types.ts +281 -0
- package/src/VideoView.web.tsx +344 -0
- package/src/index.ts +10 -0
- package/tsconfig.json +5 -9
- package/index.tsx +0 -128
- package/readme.md +0 -134
- package/useCombinedRefs.tsx +0 -25
package/.eslintrc.js
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unpublished
|
|
4
|
+
|
|
5
|
+
### 🛠 Breaking changes
|
|
6
|
+
|
|
7
|
+
### 🎉 New features
|
|
8
|
+
|
|
9
|
+
### 🐛 Bug fixes
|
|
10
|
+
|
|
11
|
+
### 💡 Others
|
|
12
|
+
|
|
13
|
+
## 1.1.0 — 2024-04-18
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- Create a docs page. ([#27854](https://github.com/expo/expo/pull/27854) by [@behenate](https://github.com/behenate))
|
|
18
|
+
- Add support for events on Android and iOS. ([#27632](https://github.com/expo/expo/pull/27632) by [@behenate](https://github.com/behenate))
|
|
19
|
+
- Add support for `loop`, `playbackRate`, `preservesPitch` and `currentTime` properties. ([#27367](https://github.com/expo/expo/pull/27367) by [@behenate](https://github.com/behenate))
|
|
20
|
+
- Add background playback support. ([#27110](https://github.com/expo/expo/pull/27110) by [@behenate](https://github.com/behenate))
|
|
21
|
+
- Add DRM support for Android and iOS. ([#26465](https://github.com/expo/expo/pull/26465) by [@behenate](https://github.com/behenate))
|
|
22
|
+
- [Android] Add Picture in Picture support. ([#26368](https://github.com/expo/expo/pull/26368) by [@behenate](https://github.com/behenate))
|
|
23
|
+
- [Android] Add fullscreen support. ([#26159](https://github.com/expo/expo/pull/26159) by [@behenate](https://github.com/behenate))
|
|
24
|
+
- [web] Add volume ([#26137](https://github.com/expo/expo/pull/26137) by [@behenate](https://github.com/behenate))
|
|
25
|
+
- Initial release for Android 🎉 ([#26033](https://github.com/expo/expo/pull/26033) by [@behenate](https://github.com/behenate))
|
|
26
|
+
- [Android] Adds support for boarders. ([#27003](https://github.com/expo/expo/pull/27003) by [@lukmccall](https://github.com/lukmccall))
|
|
27
|
+
|
|
28
|
+
### 🐛 Bug fixes
|
|
29
|
+
|
|
30
|
+
- Fix memory leaks on fast refresh. ([#27428](https://github.com/expo/expo/pull/27428) by [@behenate](https://github.com/behenate))
|
|
31
|
+
|
|
32
|
+
### 💡 Others
|
|
33
|
+
|
|
34
|
+
- Removed deprecated backward compatible Gradle settings. ([#28083](https://github.com/expo/expo/pull/28083) by [@kudo](https://github.com/kudo))
|
|
35
|
+
|
|
36
|
+
## 0.3.1 — 2023-12-12
|
|
37
|
+
|
|
38
|
+
_This version does not introduce any user-facing changes._
|
|
39
|
+
|
|
40
|
+
## 0.3.0 — 2023-12-12
|
|
41
|
+
|
|
42
|
+
### 🎉 New features
|
|
43
|
+
|
|
44
|
+
- [iOS] Add Picture in Picture support. ([#25522](https://github.com/expo/expo/pull/25522) by [@behenate](https://github.com/behenate))
|
|
45
|
+
|
|
46
|
+
## 0.2.0 — 2023-11-14
|
|
47
|
+
|
|
48
|
+
### 🛠 Breaking changes
|
|
49
|
+
|
|
50
|
+
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
|
|
51
|
+
|
|
52
|
+
## 0.1.0 — 2023-10-30
|
|
53
|
+
|
|
54
|
+
### 🎉 New features
|
|
55
|
+
|
|
56
|
+
- Initial release for iOS 🎉
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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 main branch](https://github.com/expo/expo/blob/main/docs/pages/versions/unversioned/sdk/video.md)
|
|
15
|
+
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/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).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'host.exp.exponent'
|
|
4
|
+
version = '1.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useDefaultAndroidSdkVersions()
|
|
11
|
+
useExpoPublishing()
|
|
12
|
+
|
|
13
|
+
android {
|
|
14
|
+
namespace "expo.modules.video"
|
|
15
|
+
defaultConfig {
|
|
16
|
+
versionCode 1
|
|
17
|
+
versionName '1.1.0'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dependencies {
|
|
22
|
+
implementation 'com.facebook.react:react-android'
|
|
23
|
+
|
|
24
|
+
def androidxMedia3Version = "1.2.1"
|
|
25
|
+
implementation "androidx.media3:media3-session:${androidxMedia3Version}"
|
|
26
|
+
implementation "androidx.media3:media3-exoplayer:${androidxMedia3Version}"
|
|
27
|
+
implementation "androidx.media3:media3-exoplayer-dash:${androidxMedia3Version}"
|
|
28
|
+
implementation "androidx.media3:media3-ui:${androidxMedia3Version}"
|
|
29
|
+
|
|
30
|
+
def fragment_version = "1.6.2"
|
|
31
|
+
implementation "androidx.fragment:fragment:$fragment_version"
|
|
32
|
+
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
3
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
4
|
+
|
|
5
|
+
<application>
|
|
6
|
+
<activity android:name=".FullscreenPlayerActivity" />
|
|
7
|
+
<service
|
|
8
|
+
android:name=".ExpoVideoPlaybackService"
|
|
9
|
+
android:exported="false"
|
|
10
|
+
android:foregroundServiceType="mediaPlayback">
|
|
11
|
+
<intent-filter>
|
|
12
|
+
<action android:name="androidx.media3.session.MediaSessionService" />
|
|
13
|
+
</intent-filter>
|
|
14
|
+
</service>
|
|
15
|
+
</application>
|
|
16
|
+
</manifest>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import androidx.media3.ui.AspectRatioFrameLayout
|
|
4
|
+
import expo.modules.kotlin.types.Enumerable
|
|
5
|
+
|
|
6
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
7
|
+
enum class ContentFit(val value: String) : Enumerable {
|
|
8
|
+
CONTAIN("contain"),
|
|
9
|
+
FILL("fill"),
|
|
10
|
+
COVER("cover");
|
|
11
|
+
|
|
12
|
+
fun toResizeMode(): Int {
|
|
13
|
+
return when (this) {
|
|
14
|
+
CONTAIN -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
15
|
+
FILL -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
|
16
|
+
COVER -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Binder
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import android.os.IBinder
|
|
11
|
+
import androidx.annotation.OptIn
|
|
12
|
+
import androidx.core.app.NotificationCompat
|
|
13
|
+
import androidx.media3.common.util.UnstableApi
|
|
14
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
15
|
+
import androidx.media3.session.CommandButton
|
|
16
|
+
import androidx.media3.session.MediaSession
|
|
17
|
+
import androidx.media3.session.MediaSessionService
|
|
18
|
+
import androidx.media3.session.MediaStyleNotificationHelper
|
|
19
|
+
import androidx.media3.session.SessionCommand
|
|
20
|
+
import com.google.common.collect.ImmutableList
|
|
21
|
+
|
|
22
|
+
class PlaybackServiceBinder(val service: ExpoVideoPlaybackService) : Binder()
|
|
23
|
+
|
|
24
|
+
@OptIn(UnstableApi::class)
|
|
25
|
+
class ExpoVideoPlaybackService : MediaSessionService() {
|
|
26
|
+
private val mediaSessions = mutableMapOf<ExoPlayer, MediaSession>()
|
|
27
|
+
private val binder = PlaybackServiceBinder(this)
|
|
28
|
+
|
|
29
|
+
private val commandSeekForward = SessionCommand(SEEK_FORWARD_COMMAND, Bundle.EMPTY)
|
|
30
|
+
private val commandSeekBackward = SessionCommand(SEEK_BACKWARD_COMMAND, Bundle.EMPTY)
|
|
31
|
+
private val seekForwardButton = CommandButton.Builder()
|
|
32
|
+
.setDisplayName("rewind")
|
|
33
|
+
.setSessionCommand(commandSeekForward)
|
|
34
|
+
.setIconResId(R.drawable.seek_forwards_10s)
|
|
35
|
+
.build()
|
|
36
|
+
|
|
37
|
+
private val seekBackwardButton = CommandButton.Builder()
|
|
38
|
+
.setDisplayName("forward")
|
|
39
|
+
.setSessionCommand(commandSeekBackward)
|
|
40
|
+
.setIconResId(R.drawable.seek_backwards_10s)
|
|
41
|
+
.build()
|
|
42
|
+
|
|
43
|
+
fun registerPlayer(player: ExoPlayer) {
|
|
44
|
+
if (mediaSessions[player] != null) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
val mediaSession = MediaSession.Builder(this, player)
|
|
49
|
+
.setId("ExpoVideoPlaybackService_${player.hashCode()}")
|
|
50
|
+
.setCallback(VideoMediaSessionCallback())
|
|
51
|
+
.setCustomLayout(ImmutableList.of(seekBackwardButton, seekForwardButton))
|
|
52
|
+
.build()
|
|
53
|
+
|
|
54
|
+
mediaSessions[player] = mediaSession
|
|
55
|
+
addSession(mediaSession)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun unregisterPlayer(player: ExoPlayer) {
|
|
59
|
+
hidePlayerNotification(player)
|
|
60
|
+
val session = mediaSessions.remove(player)
|
|
61
|
+
session?.release()
|
|
62
|
+
if (mediaSessions.isEmpty()) {
|
|
63
|
+
cleanup()
|
|
64
|
+
stopSelf()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun onBind(intent: Intent?): IBinder {
|
|
69
|
+
super.onBind(intent)
|
|
70
|
+
return binder
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
|
|
74
|
+
createNotification(session)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
78
|
+
cleanup()
|
|
79
|
+
stopSelf()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onDestroy() {
|
|
87
|
+
cleanup()
|
|
88
|
+
super.onDestroy()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private fun createNotification(session: MediaSession) {
|
|
92
|
+
if (session.player.currentMediaItem == null) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
97
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
98
|
+
notificationManager.createNotificationChannel(NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If the title is null android sets the notification to "<AppName> is running..." we want to keep the notification empty.
|
|
102
|
+
val contentTitle = session.player.currentMediaItem?.mediaMetadata?.title ?: "\u200E"
|
|
103
|
+
val notificationCompat = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
104
|
+
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
|
105
|
+
.setContentTitle(contentTitle)
|
|
106
|
+
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
|
|
107
|
+
.build()
|
|
108
|
+
|
|
109
|
+
// Each of the players has it's own notification when playing.
|
|
110
|
+
notificationManager.notify(session.player.hashCode(), notificationCompat)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun cleanup() {
|
|
114
|
+
hideAllNotifications()
|
|
115
|
+
mediaSessions.forEach { (_, session) ->
|
|
116
|
+
session.release()
|
|
117
|
+
}
|
|
118
|
+
mediaSessions.clear()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun hidePlayerNotification(player: ExoPlayer) {
|
|
122
|
+
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
123
|
+
notificationManager.cancel(player.hashCode())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun hideAllNotifications() {
|
|
127
|
+
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
128
|
+
notificationManager.cancelAll()
|
|
129
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
130
|
+
notificationManager.deleteNotificationChannel(CHANNEL_ID)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
companion object {
|
|
135
|
+
const val SEEK_FORWARD_COMMAND = "SEEK_FORWARD"
|
|
136
|
+
const val SEEK_BACKWARD_COMMAND = "SEEK_REWIND"
|
|
137
|
+
const val CHANNEL_ID = "PlaybackService"
|
|
138
|
+
const val SEEK_INTERVAL_MS = 10000L
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.WindowInsets
|
|
8
|
+
import android.view.WindowInsetsController
|
|
9
|
+
import android.widget.ImageButton
|
|
10
|
+
import androidx.media3.ui.PlayerView
|
|
11
|
+
|
|
12
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
13
|
+
class FullscreenPlayerActivity : Activity() {
|
|
14
|
+
private lateinit var mContentView: View
|
|
15
|
+
private lateinit var videoViewId: String
|
|
16
|
+
private lateinit var playerView: PlayerView
|
|
17
|
+
private lateinit var videoView: VideoView
|
|
18
|
+
|
|
19
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
20
|
+
super.onCreate(savedInstanceState)
|
|
21
|
+
setContentView(R.layout.fullscreen_player_activity)
|
|
22
|
+
mContentView = findViewById(R.id.enclosing_layout)
|
|
23
|
+
|
|
24
|
+
playerView = findViewById(R.id.player_view)
|
|
25
|
+
videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY)
|
|
26
|
+
?: throw FullScreenVideoViewNotFoundException()
|
|
27
|
+
|
|
28
|
+
videoView = VideoManager.getVideoView(videoViewId)
|
|
29
|
+
videoView.videoPlayer?.changePlayerView(playerView)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun onPostCreate(savedInstanceState: Bundle?) {
|
|
33
|
+
super.onPostCreate(savedInstanceState)
|
|
34
|
+
hideStatusBar()
|
|
35
|
+
setupFullscreenButton()
|
|
36
|
+
playerView.applyRequiresLinearPlayback(videoView.videoPlayer?.requiresLinearPlayback ?: false)
|
|
37
|
+
playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
38
|
+
// On every re-layout ExoPlayer makes the timeBar interactive.
|
|
39
|
+
// We need to disable it to keep scrubbing off.
|
|
40
|
+
playerView.setTimeBarInteractive(videoView.videoPlayer?.requiresLinearPlayback ?: true)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun finish() {
|
|
45
|
+
super.finish()
|
|
46
|
+
VideoManager.getVideoView(videoViewId).exitFullscreen()
|
|
47
|
+
|
|
48
|
+
// Disable the exit transition
|
|
49
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
50
|
+
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
|
|
51
|
+
} else {
|
|
52
|
+
overridePendingTransition(0, 0)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun setupFullscreenButton() {
|
|
57
|
+
playerView.setFullscreenButtonClickListener { finish() }
|
|
58
|
+
|
|
59
|
+
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
|
60
|
+
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun hideStatusBar() {
|
|
64
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
65
|
+
val controller = mContentView.windowInsetsController
|
|
66
|
+
controller?.apply {
|
|
67
|
+
systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
68
|
+
hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
@Suppress("DEPRECATION")
|
|
72
|
+
mContentView.systemUiVisibility = (
|
|
73
|
+
View.SYSTEM_UI_FLAG_LOW_PROFILE
|
|
74
|
+
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
75
|
+
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
76
|
+
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
77
|
+
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
78
|
+
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
videoView.layoutForPiPEnter()
|
|
14
|
+
videoView.onPictureInPictureStart(Unit)
|
|
15
|
+
} else {
|
|
16
|
+
videoView.willEnterPiP = false
|
|
17
|
+
videoView.layoutForPiPExit()
|
|
18
|
+
videoView.onPictureInPictureStop(Unit)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 PictureInPictureUnsupportedException :
|
|
19
|
+
CodedException("Picture in Picture mode is not supported on this device")
|
|
20
|
+
|
|
21
|
+
internal class UnsupportedDRMTypeException(type: DRMType) :
|
|
22
|
+
CodedException("DRM type `$type` is not supported on Android")
|
|
23
|
+
|
|
24
|
+
internal class PlaybackException(reason: String?, cause: Throwable? = null) :
|
|
25
|
+
CodedException("A playback exception has occurred: ${reason ?: "reason unknown"}", cause)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.OptIn
|
|
4
|
+
import androidx.media3.common.MediaItem
|
|
5
|
+
import androidx.media3.common.util.UnstableApi
|
|
6
|
+
import expo.modules.video.records.VideoSource
|
|
7
|
+
import java.lang.ref.WeakReference
|
|
8
|
+
|
|
9
|
+
// Helper class used to keep track of all existing VideoViews and VideoPlayers
|
|
10
|
+
@OptIn(UnstableApi::class)
|
|
11
|
+
object VideoManager {
|
|
12
|
+
const val INTENT_PLAYER_KEY = "player_uuid"
|
|
13
|
+
|
|
14
|
+
// Used for sharing videoViews between VideoView and FullscreenPlayerActivity
|
|
15
|
+
private var videoViews = mutableMapOf<String, VideoView>()
|
|
16
|
+
|
|
17
|
+
// Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
|
|
18
|
+
private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()
|
|
19
|
+
|
|
20
|
+
// Keeps track of all existing MediaItems and their corresponding VideoSources. Used for recognizing source of MediaItems.
|
|
21
|
+
private var mediaItemsToVideoSources = mutableMapOf<String, WeakReference<VideoSource>>()
|
|
22
|
+
|
|
23
|
+
fun registerVideoView(videoView: VideoView) {
|
|
24
|
+
videoViews[videoView.id] = videoView
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun getVideoView(id: String): VideoView {
|
|
28
|
+
return videoViews[id] ?: throw VideoViewNotFoundException(id)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun unregisterVideoView(videoView: VideoView) {
|
|
32
|
+
videoViews.remove(videoView.id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun registerVideoPlayer(videoPlayer: VideoPlayer) {
|
|
36
|
+
videoPlayersToVideoViews[videoPlayer] = videoPlayersToVideoViews[videoPlayer] ?: mutableListOf()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fun unregisterVideoPlayer(videoPlayer: VideoPlayer) {
|
|
40
|
+
videoPlayersToVideoViews.remove(videoPlayer)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun registerVideoSourceToMediaItem(mediaItem: MediaItem, videoSource: VideoSource) {
|
|
44
|
+
mediaItemsToVideoSources[mediaItem.mediaId] = WeakReference(videoSource)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun getVideoSourceFromMediaItem(mediaItem: MediaItem?): VideoSource? {
|
|
48
|
+
if (mediaItem == null) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
return mediaItemsToVideoSources[mediaItem.mediaId]?.get()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) {
|
|
55
|
+
if (videoPlayersToVideoViews[videoPlayer]?.contains(videoView) == true) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
videoPlayersToVideoViews[videoPlayer]?.add(videoView) ?: run {
|
|
59
|
+
videoPlayersToVideoViews[videoPlayer] = arrayListOf(videoView)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (videoPlayersToVideoViews[videoPlayer]?.size == 1) {
|
|
63
|
+
videoPlayer.playbackServiceBinder?.service?.registerPlayer(videoPlayer.player)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fun onVideoPlayerDetachedFromView(videoPlayer: VideoPlayer, videoView: VideoView) {
|
|
68
|
+
videoPlayersToVideoViews[videoPlayer]?.remove(videoView)
|
|
69
|
+
|
|
70
|
+
// Unregister disconnected VideoPlayers from the playback service
|
|
71
|
+
if (videoPlayersToVideoViews[videoPlayer] == null || videoPlayersToVideoViews[videoPlayer]?.size == 0) {
|
|
72
|
+
videoPlayer.playbackServiceBinder?.service?.unregisterPlayer(videoPlayer.player)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun onAppForegrounded() = Unit
|
|
77
|
+
|
|
78
|
+
fun onAppBackgrounded() {
|
|
79
|
+
for (videoView in videoViews.values) {
|
|
80
|
+
if (videoView.videoPlayer?.staysActiveInBackground == false &&
|
|
81
|
+
!videoView.willEnterPiP &&
|
|
82
|
+
!videoView.isInFullscreen
|
|
83
|
+
) {
|
|
84
|
+
videoView.videoPlayer?.player?.pause()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.media3.common.Player
|
|
5
|
+
import androidx.media3.session.MediaSession
|
|
6
|
+
import androidx.media3.session.SessionCommand
|
|
7
|
+
import androidx.media3.session.SessionResult
|
|
8
|
+
import com.google.common.util.concurrent.ListenableFuture
|
|
9
|
+
import androidx.annotation.OptIn
|
|
10
|
+
import androidx.media3.common.util.UnstableApi
|
|
11
|
+
|
|
12
|
+
@OptIn(UnstableApi::class)
|
|
13
|
+
class VideoMediaSessionCallback : MediaSession.Callback {
|
|
14
|
+
override fun onConnect(
|
|
15
|
+
session: MediaSession,
|
|
16
|
+
controller: MediaSession.ControllerInfo
|
|
17
|
+
): MediaSession.ConnectionResult {
|
|
18
|
+
try {
|
|
19
|
+
// TODO @behenate: For now we're only allowing seek forward and back by 10 seconds and going to the
|
|
20
|
+
// beginning of the video. In the future we should add more customization options for the users.
|
|
21
|
+
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
|
22
|
+
.setAvailablePlayerCommands(
|
|
23
|
+
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
|
|
24
|
+
.add(Player.COMMAND_SEEK_FORWARD)
|
|
25
|
+
.add(Player.COMMAND_SEEK_BACK)
|
|
26
|
+
.build()
|
|
27
|
+
)
|
|
28
|
+
.setAvailableSessionCommands(
|
|
29
|
+
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
|
30
|
+
.add(SessionCommand(ExpoVideoPlaybackService.SEEK_BACKWARD_COMMAND, Bundle.EMPTY))
|
|
31
|
+
.add(SessionCommand(ExpoVideoPlaybackService.SEEK_FORWARD_COMMAND, Bundle.EMPTY))
|
|
32
|
+
.build()
|
|
33
|
+
)
|
|
34
|
+
.build()
|
|
35
|
+
} catch (e: Exception) {
|
|
36
|
+
return MediaSession.ConnectionResult.reject()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<SessionResult> {
|
|
41
|
+
when (customCommand.customAction) {
|
|
42
|
+
ExpoVideoPlaybackService.SEEK_FORWARD_COMMAND -> session.player.seekTo(session.player.currentPosition + ExpoVideoPlaybackService.SEEK_INTERVAL_MS)
|
|
43
|
+
ExpoVideoPlaybackService.SEEK_BACKWARD_COMMAND -> session.player.seekTo(session.player.currentPosition - ExpoVideoPlaybackService.SEEK_INTERVAL_MS)
|
|
44
|
+
}
|
|
45
|
+
return super.onCustomCommand(session, controller, customCommand, args)
|
|
46
|
+
}
|
|
47
|
+
}
|