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.
Files changed (97) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +56 -0
  3. package/README.md +33 -0
  4. package/android/build.gradle +33 -0
  5. package/android/src/main/AndroidManifest.xml +16 -0
  6. package/android/src/main/java/expo/modules/video/ContentFit.kt +19 -0
  7. package/android/src/main/java/expo/modules/video/ExpoVideoPlaybackService.kt +140 -0
  8. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +82 -0
  9. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +21 -0
  10. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +25 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +88 -0
  13. package/android/src/main/java/expo/modules/video/VideoMediaSessionCallback.kt +47 -0
  14. package/android/src/main/java/expo/modules/video/VideoModule.kt +285 -0
  15. package/android/src/main/java/expo/modules/video/VideoPlayer.kt +266 -0
  16. package/android/src/main/java/expo/modules/video/VideoView.kt +397 -0
  17. package/android/src/main/java/expo/modules/video/YogaUtils.kt +20 -0
  18. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +212 -0
  19. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  20. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  21. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  22. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  23. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +35 -0
  24. package/android/src/main/java/expo/modules/video/records/VolumeEvent.kt +10 -0
  25. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  26. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  27. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  28. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  29. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  30. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  31. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  32. package/app.plugin.js +1 -0
  33. package/build/NativeVideoModule.d.ts +3 -0
  34. package/build/NativeVideoModule.d.ts.map +1 -0
  35. package/build/NativeVideoModule.js +3 -0
  36. package/build/NativeVideoModule.js.map +1 -0
  37. package/build/NativeVideoModule.web.d.ts +3 -0
  38. package/build/NativeVideoModule.web.d.ts.map +1 -0
  39. package/build/NativeVideoModule.web.js +2 -0
  40. package/build/NativeVideoModule.web.js.map +1 -0
  41. package/build/NativeVideoView.d.ts +3 -0
  42. package/build/NativeVideoView.d.ts.map +1 -0
  43. package/build/NativeVideoView.js +3 -0
  44. package/build/NativeVideoView.js.map +1 -0
  45. package/build/VideoView.d.ts +41 -0
  46. package/build/VideoView.d.ts.map +1 -0
  47. package/build/VideoView.js +77 -0
  48. package/build/VideoView.js.map +1 -0
  49. package/build/VideoView.types.d.ts +248 -0
  50. package/build/VideoView.types.d.ts.map +1 -0
  51. package/build/VideoView.types.js +2 -0
  52. package/build/VideoView.types.js.map +1 -0
  53. package/build/VideoView.web.d.ts +44 -0
  54. package/build/VideoView.web.d.ts.map +1 -0
  55. package/build/VideoView.web.js +283 -0
  56. package/build/VideoView.web.js.map +1 -0
  57. package/build/index.d.ts +5 -0
  58. package/build/index.d.ts.map +1 -0
  59. package/build/index.js +4 -0
  60. package/build/index.js.map +1 -0
  61. package/expo-module.config.json +9 -0
  62. package/ios/ContentKeyDelegate.swift +200 -0
  63. package/ios/ContentKeyManager.swift +21 -0
  64. package/ios/Enums/DRMType.swift +20 -0
  65. package/ios/Enums/PlayerStatus.swift +10 -0
  66. package/ios/Enums/VideoContentFit.swift +39 -0
  67. package/ios/ExpoVideo.podspec +26 -0
  68. package/ios/NowPlayingManager.swift +204 -0
  69. package/ios/Records/DRMOptions.swift +24 -0
  70. package/ios/Records/PlaybackError.swift +10 -0
  71. package/ios/Records/VideoSource.swift +11 -0
  72. package/ios/Records/VolumeEvent.swift +14 -0
  73. package/ios/VideoExceptions.swift +33 -0
  74. package/ios/VideoItem.swift +11 -0
  75. package/ios/VideoManager.swift +66 -0
  76. package/ios/VideoModule.swift +192 -0
  77. package/ios/VideoPlayer.swift +172 -0
  78. package/ios/VideoPlayerItem.swift +11 -0
  79. package/ios/VideoPlayerObserver.swift +211 -0
  80. package/ios/VideoView.swift +152 -0
  81. package/package.json +26 -25
  82. package/plugin/build/withExpoVideo.d.ts +3 -0
  83. package/plugin/build/withExpoVideo.js +19 -0
  84. package/plugin/jest.config.js +1 -0
  85. package/plugin/src/withExpoVideo.ts +25 -0
  86. package/plugin/tsconfig.json +9 -0
  87. package/src/NativeVideoModule.ts +3 -0
  88. package/src/NativeVideoModule.web.ts +1 -0
  89. package/src/NativeVideoView.ts +3 -0
  90. package/src/VideoView.tsx +92 -0
  91. package/src/VideoView.types.ts +281 -0
  92. package/src/VideoView.web.tsx +344 -0
  93. package/src/index.ts +10 -0
  94. package/tsconfig.json +5 -9
  95. package/index.tsx +0 -128
  96. package/readme.md +0 -134
  97. package/useCombinedRefs.tsx +0 -25
package/.eslintrc.js ADDED
@@ -0,0 +1,2 @@
1
+ // @generated by expo-module-scripts
2
+ module.exports = require('expo-module-scripts/eslintrc.base.js');
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 &mdash; 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
+ }