@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.
Files changed (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,19 @@
1
+ package expo.modules.video.records
2
+
3
+ import androidx.media3.common.PlaybackException
4
+ import expo.modules.kotlin.records.Field
5
+ import expo.modules.kotlin.records.Record
6
+ import java.io.Serializable
7
+
8
+ class PlaybackError(
9
+ @Field var message: String? = null
10
+ ) : Record, Serializable {
11
+ constructor(exception: PlaybackException) : this(errorMessageFromException(exception))
12
+
13
+ companion object {
14
+ private fun errorMessageFromException(exception: PlaybackException): String {
15
+ val reason = "${exception.localizedMessage} ${exception.cause?.localizedMessage ?: ""}"
16
+ return "A playback exception has occurred: $reason"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,81 @@
1
+ package expo.modules.video.records
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.Format
5
+ import androidx.media3.common.util.UnstableApi
6
+ import expo.modules.kotlin.records.Field
7
+ import expo.modules.kotlin.records.Record
8
+ import java.io.Serializable
9
+ import java.util.Locale
10
+
11
+ class SubtitleTrack(
12
+ @Field val id: String,
13
+ @Field val language: String?,
14
+ @Field val label: String?
15
+ ) : Record, Serializable {
16
+ companion object {
17
+ fun fromFormat(format: Format?): SubtitleTrack? {
18
+ format ?: return null
19
+ val id = format.id ?: return null
20
+ val language = format.language ?: return null
21
+ val label = Locale(language).displayLanguage
22
+
23
+ return SubtitleTrack(
24
+ id = id,
25
+ language = language,
26
+ label = label
27
+ )
28
+ }
29
+ }
30
+ }
31
+
32
+ class AudioTrack(
33
+ @Field val id: String,
34
+ @Field val language: String?,
35
+ @Field val label: String?
36
+ ) : Record, Serializable {
37
+ companion object {
38
+ fun fromFormat(format: Format?): AudioTrack? {
39
+ format ?: return null
40
+ val id = format.id ?: return null
41
+ val language = format.language
42
+ val label = language?.let { Locale(it).displayLanguage } ?: "Unknown"
43
+
44
+ return AudioTrack(
45
+ id = id,
46
+ language = language,
47
+ label = label
48
+ )
49
+ }
50
+ }
51
+ }
52
+
53
+ @OptIn(UnstableApi::class)
54
+ class VideoTrack(
55
+ @Field val id: String,
56
+ @Field val size: VideoSize,
57
+ @Field val mimeType: String?,
58
+ @Field val isSupported: Boolean = true,
59
+ @Field val bitrate: Int? = null,
60
+ @Field val frameRate: Float? = null,
61
+ var format: Format? = null
62
+ ) : Record, Serializable {
63
+ companion object {
64
+ fun fromFormat(format: Format?, isSupported: Boolean): VideoTrack? {
65
+ val id = format?.id ?: return null
66
+ val size = VideoSize(format)
67
+ val mimeType = format.sampleMimeType
68
+ val bitrate = format.bitrate.takeIf { it != Format.NO_VALUE }
69
+ val frameRate = format.frameRate.takeIf { it != Format.NO_VALUE.toFloat() }
70
+ return VideoTrack(
71
+ id = id,
72
+ size = size,
73
+ mimeType = mimeType,
74
+ isSupported = isSupported,
75
+ bitrate = bitrate,
76
+ frameRate = frameRate,
77
+ format = format
78
+ )
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,79 @@
1
+ package expo.modules.video.records
2
+
3
+ import expo.modules.kotlin.records.Field
4
+ import expo.modules.kotlin.records.Record
5
+ import expo.modules.video.enums.PlayerStatus
6
+ import java.io.Serializable
7
+
8
+ interface VideoEventPayload : Record, Serializable
9
+
10
+ class StatusChangedEventPayload(
11
+ @Field val status: PlayerStatus,
12
+ @Field val oldStatus: PlayerStatus?,
13
+ @Field val error: PlaybackError?
14
+ ) : VideoEventPayload
15
+
16
+ class IsPlayingEventPayload(
17
+ @Field val isPlaying: Boolean,
18
+ @Field val oldIsPlaying: Boolean?
19
+ ) : VideoEventPayload
20
+
21
+ class VolumeChangedEventPayload(
22
+ @Field val volume: Float,
23
+ @Field val oldVolume: Float?
24
+ ) : VideoEventPayload
25
+
26
+ class MutedChangedEventPayload(
27
+ @Field val muted: Boolean,
28
+ @Field val oldMuted: Boolean?
29
+ ) : VideoEventPayload
30
+
31
+ class SourceChangedEventPayload(
32
+ @Field val source: VideoSource?,
33
+ @Field val oldSource: VideoSource?
34
+ ) : VideoEventPayload
35
+
36
+ class PlaybackRateChangedEventPayload(
37
+ @Field val playbackRate: Float,
38
+ @Field val oldPlaybackRate: Float?
39
+ ) : VideoEventPayload
40
+
41
+ class TimeUpdate(
42
+ @Field var currentTime: Double = .0,
43
+ @Field var currentOffsetFromLive: Float?,
44
+ @Field var currentLiveTimestamp: Long?,
45
+ @Field var bufferedPosition: Double = .0
46
+ ) : VideoEventPayload
47
+
48
+ class SubtitleTrackChangedEventPayload(
49
+ @Field val subtitleTrack: SubtitleTrack?,
50
+ @Field val oldSubtitleTrack: SubtitleTrack?
51
+ ) : VideoEventPayload
52
+
53
+ class AudioTrackChangedEventPayload(
54
+ @Field val audioTrack: AudioTrack?,
55
+ @Field val oldAudioTrack: AudioTrack?
56
+ ) : VideoEventPayload
57
+
58
+ class VideoTrackChangedEventPayload(
59
+ @Field val videoTrack: VideoTrack?,
60
+ @Field val oldVideoTrack: VideoTrack?
61
+ ) : VideoEventPayload
62
+
63
+ class AvailableSubtitleTracksChangedEventPayload(
64
+ @Field val availableSubtitleTracks: List<SubtitleTrack>,
65
+ @Field val oldAvailableSubtitleTracks: List<SubtitleTrack>
66
+ ) : VideoEventPayload
67
+
68
+ class AvailableAudioTracksChangedEventPayload(
69
+ @Field val availableAudioTracks: List<AudioTrack>,
70
+ @Field val oldAvailableAudioTracks: List<AudioTrack>
71
+ ) : VideoEventPayload
72
+
73
+ class VideoSourceLoadedEventPayload(
74
+ @Field val videoSource: VideoSource?,
75
+ @Field val duration: Double,
76
+ @Field val availableVideoTracks: List<VideoTrack>,
77
+ @Field val availableSubtitleTracks: List<SubtitleTrack>,
78
+ @Field val availableAudioTracks: List<AudioTrack>
79
+ ) : VideoEventPayload
@@ -0,0 +1,12 @@
1
+ package expo.modules.video.records
2
+
3
+ import android.net.Uri
4
+ import expo.modules.kotlin.records.Field
5
+ import expo.modules.kotlin.records.Record
6
+ import java.io.Serializable
7
+
8
+ class VideoMetadata(
9
+ @Field var title: String? = null,
10
+ @Field var artist: String? = null,
11
+ @Field var artwork: Uri? = null
12
+ ) : Record, Serializable
@@ -0,0 +1,14 @@
1
+ package expo.modules.video.records
2
+
3
+ import androidx.media3.common.Format
4
+ import expo.modules.kotlin.records.Field
5
+ import expo.modules.kotlin.records.Record
6
+ import java.io.Serializable
7
+
8
+ data class VideoSize(
9
+ @Field val width: Int = 0,
10
+ @Field val height: Int = 0
11
+ ) : Record, Serializable {
12
+ constructor(size: androidx.media3.common.VideoSize) : this(size.width, size.height)
13
+ constructor(format: Format) : this(format.width, format.height)
14
+ }
@@ -0,0 +1,104 @@
1
+ package expo.modules.video.records
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.ContentResolver
5
+ import android.content.Context
6
+ import android.net.Uri
7
+ import android.util.Log
8
+ import androidx.annotation.OptIn
9
+ import androidx.media3.common.MediaItem
10
+ import androidx.media3.common.MediaMetadata
11
+ import androidx.media3.common.util.UnstableApi
12
+ import androidx.media3.datasource.DataSpec
13
+ import androidx.media3.datasource.RawResourceDataSource
14
+ import androidx.media3.exoplayer.source.MediaSource
15
+ import expo.modules.kotlin.records.Field
16
+ import expo.modules.kotlin.records.Record
17
+ import expo.modules.video.UnsupportedDRMTypeException
18
+ import expo.modules.video.buildExpoVideoMediaSource
19
+ import expo.modules.video.enums.ContentType
20
+ import java.io.Serializable
21
+
22
+ @OptIn(UnstableApi::class)
23
+ class VideoSource(
24
+ @Field var uri: Uri? = null,
25
+ @Field var drm: DRMOptions? = null,
26
+ @Field var metadata: VideoMetadata? = null,
27
+ @Field var headers: Map<String, String>? = null,
28
+ @Field var useCaching: Boolean = false,
29
+ @Field val contentType: ContentType = ContentType.AUTO
30
+ ) : Record, Serializable {
31
+ private fun toMediaId(): String {
32
+ return "uri:${this.uri}" +
33
+ "Headers: ${this.headers}" +
34
+ "DrmType:${this.drm?.type}" +
35
+ "DrmLicenseServer:${this.drm?.licenseServer}" +
36
+ "DrmMultiKey:${this.drm?.multiKey}" +
37
+ "DRMHeadersKeys:${this.drm?.headers?.keys?.joinToString { it }}}" +
38
+ "DRMHeadersValues:${this.drm?.headers?.values?.joinToString { it }}}" +
39
+ "NotificationDataTitle:${this.metadata?.title}" +
40
+ "NotificationDataSecondaryText:${this.metadata?.artist}" +
41
+ "NotificationDataArtwork:${this.metadata?.artwork?.path}" +
42
+ "ContentType:${this.contentType.value}"
43
+ }
44
+
45
+ fun toMediaSource(context: Context): MediaSource? {
46
+ this.uri ?: return null
47
+ return buildExpoVideoMediaSource(context, this)
48
+ }
49
+
50
+ fun toMediaItem(context: Context) = MediaItem
51
+ .Builder()
52
+ .apply {
53
+ setUri(parseLocalAssetId(uri, context))
54
+ contentType.toMimeTypeString()?.let {
55
+ setMimeType(it)
56
+ }
57
+ drm?.let {
58
+ if (it.type.isSupported()) {
59
+ setDrmConfiguration(it.toDRMConfiguration())
60
+ } else {
61
+ throw UnsupportedDRMTypeException(it.type)
62
+ }
63
+ }
64
+ setMediaMetadata(
65
+ MediaMetadata.Builder().apply {
66
+ metadata?.let { data ->
67
+ setTitle(data.title)
68
+ setArtist(data.artist)
69
+ data.artwork?.let {
70
+ setArtworkUri(it)
71
+ }
72
+ }
73
+ }.build()
74
+ )
75
+ }
76
+ .build()
77
+
78
+ // Using `resolveAssetSource` to generate a local asset URI returns a resource name for android release builds
79
+ // we have to get the raw resource URI to play the video
80
+ @SuppressLint("DiscouragedApi") // AFAIK, in this case, there's no other way to get the resource URI
81
+ private fun parseLocalAssetId(uri: Uri?, context: Context): Uri? {
82
+ if (uri == null || uri.scheme != null) {
83
+ return uri
84
+ }
85
+ try {
86
+ val resourceId: Int = context.resources.getIdentifier(
87
+ uri.toString(),
88
+ "raw",
89
+ context.packageName
90
+ )
91
+ val parsedUri = Uri.Builder()
92
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
93
+ .appendPath(resourceId.toString())
94
+ .build()
95
+ val dataSpec = DataSpec(parsedUri)
96
+ val rawResourceDataSource = RawResourceDataSource(context)
97
+ rawResourceDataSource.open(dataSpec)
98
+ return rawResourceDataSource.uri
99
+ } catch (e: RawResourceDataSource.RawResourceDataSourceException) {
100
+ Log.e("ExpoVideo", "Error parsing local asset id, falling back to original uri", e)
101
+ return uri
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,24 @@
1
+ package expo.modules.video.records
2
+
3
+ import expo.modules.kotlin.records.Field
4
+ import expo.modules.kotlin.records.Record
5
+
6
+ class VideoThumbnailOptions(
7
+ @Field val maxWidth: Int? = null,
8
+ @Field val maxHeight: Int? = null
9
+ ) : Record {
10
+ // Returns a pair of Int values representing a valid size limitation for the thumbnail
11
+ // or null is no limit has been set
12
+ fun toNativeSizeLimit(): Pair<Int, Int>? {
13
+ if (this.maxWidth == null && this.maxHeight == null) {
14
+ return null
15
+ }
16
+ val width = this.maxWidth ?: Int.MAX_VALUE
17
+ val height = this.maxHeight ?: Int.MAX_VALUE
18
+
19
+ require(width >= 1 && height >= 1) {
20
+ "Failed to generate a thumbnail: The maxWidth and maxHeight parameters must be greater than zero"
21
+ }
22
+ return width to height
23
+ }
24
+ }
@@ -0,0 +1,75 @@
1
+ package expo.modules.video
2
+
3
+ import android.content.Context
4
+ import android.content.pm.ApplicationInfo
5
+ import androidx.annotation.OptIn
6
+ import androidx.media3.common.util.UnstableApi
7
+ import androidx.media3.common.util.Util
8
+ import androidx.media3.datasource.DataSource
9
+ import androidx.media3.datasource.DefaultDataSource
10
+ import androidx.media3.datasource.cache.CacheDataSource
11
+ import androidx.media3.datasource.okhttp.OkHttpDataSource
12
+ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
13
+ import androidx.media3.exoplayer.source.MediaSource
14
+ import expo.modules.video.records.VideoSource
15
+ import okhttp3.OkHttpClient
16
+
17
+ @OptIn(UnstableApi::class)
18
+ fun buildBaseDataSourceFactory(context: Context, videoSource: VideoSource): DataSource.Factory {
19
+ return if (videoSource.uri?.scheme?.startsWith("http") == true) {
20
+ buildOkHttpDataSourceFactory(context, videoSource)
21
+ } else {
22
+ DefaultDataSource.Factory(context)
23
+ }
24
+ }
25
+
26
+ @OptIn(UnstableApi::class)
27
+ fun buildOkHttpDataSourceFactory(context: Context, videoSource: VideoSource): OkHttpDataSource.Factory {
28
+ val client = OkHttpClient.Builder().build()
29
+
30
+ // If the application name has ANY non-ASCII characters, we need to strip them out. This is because using non-ASCII characters
31
+ // in the User-Agent header can cause issues with getting the media to play.
32
+ val applicationName = getApplicationName(context).filter { it.code in 0..127 }
33
+
34
+ val defaultUserAgent = Util.getUserAgent(context, applicationName)
35
+
36
+ return OkHttpDataSource.Factory(client).apply {
37
+ val headers = videoSource.headers
38
+ headers?.takeIf { it.isNotEmpty() }?.let {
39
+ setDefaultRequestProperties(it)
40
+ }
41
+ val userAgent = headers?.get("User-Agent") ?: defaultUserAgent
42
+ setUserAgent(userAgent)
43
+ }
44
+ }
45
+
46
+ @OptIn(UnstableApi::class)
47
+ fun buildCacheDataSourceFactory(context: Context, videoSource: VideoSource): DataSource.Factory {
48
+ return CacheDataSource.Factory().apply {
49
+ setCache(VideoManager.cache.instance)
50
+ setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
51
+ setUpstreamDataSourceFactory(buildBaseDataSourceFactory(context, videoSource))
52
+ }
53
+ }
54
+
55
+ fun buildMediaSourceFactory(context: Context, dataSourceFactory: DataSource.Factory): MediaSource.Factory {
56
+ return DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
57
+ }
58
+
59
+ @OptIn(UnstableApi::class)
60
+ fun buildExpoVideoMediaSource(context: Context, videoSource: VideoSource): MediaSource {
61
+ val dataSourceFactory = if (videoSource.useCaching) {
62
+ buildCacheDataSourceFactory(context, videoSource)
63
+ } else {
64
+ buildBaseDataSourceFactory(context, videoSource)
65
+ }
66
+ val mediaSourceFactory = buildMediaSourceFactory(context, dataSourceFactory)
67
+ val mediaItem = videoSource.toMediaItem(context)
68
+ return mediaSourceFactory.createMediaSource(mediaItem)
69
+ }
70
+
71
+ private fun getApplicationName(context: Context): String {
72
+ val applicationInfo: ApplicationInfo = context.applicationInfo
73
+ val stringId = applicationInfo.labelRes
74
+ return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString() else context.getString(stringId)
75
+ }
@@ -0,0 +1,43 @@
1
+ package expo.modules.video.utils
2
+
3
+ import android.util.Log
4
+ import android.view.MotionEvent
5
+ import android.view.View
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.facebook.react.uimanager.events.EventDispatcher
8
+ import com.facebook.react.uimanager.events.TouchEvent
9
+ import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper
10
+ import com.facebook.react.uimanager.events.TouchEventType
11
+
12
+ internal fun EventDispatcher.dispatchMotionEvent(view: View, event: MotionEvent?, touchEventCoalescingKeyHelper: TouchEventCoalescingKeyHelper) {
13
+ if (event == null) {
14
+ return
15
+ }
16
+
17
+ try {
18
+ val event = TouchEvent.obtain(
19
+ UIManagerHelper.getSurfaceId(view),
20
+ view.id,
21
+ event.toTouchEventType(),
22
+ event,
23
+ event.eventTime,
24
+ event.x,
25
+ event.y,
26
+ touchEventCoalescingKeyHelper
27
+ )
28
+ dispatchEvent(event)
29
+ } catch (e: RuntimeException) {
30
+ // We are not expecting any issues, but we want to prevent crashes in case the dispatch fails for any reason.
31
+ Log.e("EventDispatcherUtils", "Error dispatching touch event", e)
32
+ }
33
+ }
34
+
35
+ private fun MotionEvent.toTouchEventType(): TouchEventType {
36
+ return when (this.actionMasked) {
37
+ MotionEvent.ACTION_DOWN -> TouchEventType.START
38
+ MotionEvent.ACTION_UP -> TouchEventType.END
39
+ MotionEvent.ACTION_MOVE -> TouchEventType.MOVE
40
+ MotionEvent.ACTION_CANCEL -> TouchEventType.CANCEL
41
+ else -> TouchEventType.CANCEL
42
+ }
43
+ }
@@ -0,0 +1,15 @@
1
+ package expo.modules.video.utils
2
+
3
+ import java.lang.ref.WeakReference
4
+
5
+ internal class MutableWeakReference<T>(value: T? = null) {
6
+ private var ref: WeakReference<T> = WeakReference(value)
7
+
8
+ fun get(): T? {
9
+ return ref.get()
10
+ }
11
+
12
+ fun set(value: T) {
13
+ ref = WeakReference(value)
14
+ }
15
+ }
@@ -0,0 +1,96 @@
1
+ package expo.modules.video.utils
2
+
3
+ import android.app.Activity
4
+ import android.app.PictureInPictureParams
5
+ import android.graphics.Rect
6
+ import android.os.Build
7
+ import android.util.Log
8
+ import android.util.Rational
9
+ import androidx.annotation.OptIn
10
+ import androidx.media3.common.VideoSize
11
+ import androidx.media3.common.util.UnstableApi
12
+ import androidx.media3.ui.PlayerView
13
+ import expo.modules.video.PictureInPictureConfigurationException
14
+ import expo.modules.video.VideoView.Companion.isPictureInPictureSupported
15
+ import expo.modules.video.enums.ContentFit
16
+
17
+ @OptIn(UnstableApi::class)
18
+ internal fun calculateRectHint(playerView: PlayerView): Rect {
19
+ val hint = Rect()
20
+ playerView.videoSurfaceView?.getGlobalVisibleRect(hint)
21
+ val location = IntArray(2)
22
+ playerView.videoSurfaceView?.getLocationOnScreen(location)
23
+
24
+ // getGlobalVisibleRect doesn't take into account the offset for the notch, we use the screen location
25
+ // of the view to calculate the rectHint.
26
+ // We only apply this correction on the y axis due to something that looks like a bug in the Android SDK.
27
+ // If the video screen and home screen have the same orientation this works correctly,
28
+ // but if the home screen doesn't support landscape and the video screen does, we have to
29
+ // ignore the offset for the notch on the x axis even though it's present on the video screen
30
+ // because there will be no offset on the home screen
31
+ // there is no way to check the orientation support of the home screen, so we make the bet that
32
+ // it won't support landscape (as most android home screens do by default)
33
+ // This doesn't have any serious consequences if we are wrong with the guess, the transition will be a bit off though
34
+ val height = hint.bottom - hint.top
35
+ hint.top = location[1]
36
+ hint.bottom = hint.top + height
37
+ return hint
38
+ }
39
+
40
+ internal fun calculatePiPAspectRatio(videoSize: VideoSize, viewWidth: Int, viewHeight: Int, contentFit: ContentFit): Rational {
41
+ var aspectRatio = if (contentFit == ContentFit.CONTAIN) {
42
+ Rational(videoSize.width, videoSize.height)
43
+ } else {
44
+ Rational(viewWidth, viewHeight)
45
+ }
46
+ // AspectRatio for the activity in picture-in-picture, must be between 2.39:1 and 1:2.39 (inclusive).
47
+ // https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)
48
+ val maximumRatio = Rational(239, 100)
49
+ val minimumRatio = Rational(100, 239)
50
+
51
+ if (aspectRatio.toFloat() > maximumRatio.toFloat()) {
52
+ aspectRatio = maximumRatio
53
+ } else if (aspectRatio.toFloat() < minimumRatio.toFloat()) {
54
+ aspectRatio = minimumRatio
55
+ }
56
+ return aspectRatio
57
+ }
58
+
59
+ internal fun applyRectHint(activity: Activity, rectHint: Rect) {
60
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureSupported(activity)) {
61
+ runWithPiPMisconfigurationSoftHandling {
62
+ activity.setPictureInPictureParams(PictureInPictureParams.Builder().setSourceRectHint(rectHint).build())
63
+ }
64
+ }
65
+ }
66
+
67
+ // We can't check if AndroidManifest.xml is configured properly, so we have to handle the exceptions ourselves to prevent crashes
68
+ internal fun runWithPiPMisconfigurationSoftHandling(shouldThrow: Boolean = false, block: () -> Any?) {
69
+ try {
70
+ block()
71
+ } catch (e: IllegalStateException) {
72
+ Log.e("ExpoVideo", "Current activity does not support picture-in-picture. Make sure you have configured the `expo-video` config plugin correctly.")
73
+ if (shouldThrow) {
74
+ throw PictureInPictureConfigurationException()
75
+ }
76
+ }
77
+ }
78
+
79
+ internal fun applyPiPParams(activity: Activity, autoEnterPiP: Boolean, aspectRatio: Rational? = null) {
80
+ // If the aspect ratio exceeds the limits, the app will crash
81
+ val safeAspectRatio = aspectRatio?.takeIf { it.toFloat() in 0.41841..2.39 }
82
+
83
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureSupported(activity)) {
84
+ val paramsBuilder = PictureInPictureParams.Builder()
85
+
86
+ safeAspectRatio?.let {
87
+ paramsBuilder.setAspectRatio(it)
88
+ }
89
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
90
+ paramsBuilder.setAutoEnterEnabled(autoEnterPiP)
91
+ }
92
+ runWithPiPMisconfigurationSoftHandling {
93
+ activity.setPictureInPictureParams(paramsBuilder.build())
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,20 @@
1
+ package expo.modules.video.utils
2
+
3
+ import com.facebook.yoga.YogaConstants
4
+
5
+ fun Float.ifYogaUndefinedUse(value: Float) =
6
+ if (YogaConstants.isUndefined(this)) {
7
+ value
8
+ } else {
9
+ this
10
+ }
11
+
12
+ inline fun Float.ifYogaDefinedUse(transformFun: (current: Float) -> Float) =
13
+ if (YogaConstants.isUndefined(this)) {
14
+ this
15
+ } else {
16
+ transformFun(this)
17
+ }
18
+
19
+ fun makeYogaUndefinedIfNegative(value: Float) =
20
+ if (!YogaConstants.isUndefined(value) && value < 0) YogaConstants.UNDEFINED else value
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="24dp"
4
+ android:height="24dp"
5
+ android:viewportWidth="24"
6
+ android:viewportHeight="24">
7
+ <group
8
+ android:pivotX="12"
9
+ android:pivotY="4"
10
+ android:scaleX="1.15"
11
+ android:scaleY="1.15">
12
+ <path
13
+ android:fillColor="?android:attr/colorControlNormal"
14
+ android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
15
+ <group
16
+ android:pivotX="12"
17
+ android:pivotY="8"
18
+ android:scaleX="1"
19
+ android:scaleY="0.9">
20
+ <path
21
+ android:fillColor="?android:attr/colorControlNormal"
22
+ android:pathData="m10.412 17.244h-0.70313v-4.4805q-0.25391 0.24219-0.66797 0.48438-0.41016 0.24219-0.73828 0.36328v-0.67969q0.58984-0.27734 1.0313-0.67188 0.44141-0.39453 0.625-0.76563h0.45313zm1.2008-2.8242q0-1.0156 0.20703-1.6328 0.21094-0.6211 0.6211-0.95703 0.41406-0.33594 1.0391-0.33594 0.46094 0 0.80859 0.1875 0.34766 0.18359 0.57422 0.53516 0.22656 0.34766 0.35547 0.85156 0.12891 0.5 0.12891 1.3516 0 1.0078-0.20703 1.6289-0.20703 0.61719-0.6211 0.95703-0.41016 0.33594-1.0391 0.33594-0.82813 0-1.3008-0.59375-0.56641-0.71484-0.56641-2.3281zm0.72266 0q0 1.4102 0.32812 1.8789 0.33203 0.46484 0.81641 0.46484 0.48438 0 0.8125-0.46875 0.33203-0.46875 0.33203-1.875 0-1.4141-0.33203-1.8789-0.32813-0.46484-0.82031-0.46484-0.48438 0-0.77344 0.41016-0.36328 0.52344-0.36328 1.9336z"/>
23
+ </group>
24
+ </group>
25
+ </vector>
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="24dp"
4
+ android:height="24dp"
5
+ android:viewportWidth="24"
6
+ android:viewportHeight="24">
7
+ <group
8
+ android:pivotX="12"
9
+ android:pivotY="4"
10
+ android:scaleX="1.15"
11
+ android:scaleY="1.15">
12
+ <path
13
+ android:fillColor="?android:attr/colorControlNormal"
14
+ android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
15
+ <group
16
+ android:pivotX="12"
17
+ android:pivotY="8"
18
+ android:scaleX="0.9"
19
+ android:scaleY="0.9">
20
+ <path
21
+ android:fillColor="?android:attr/colorControlNormal"
22
+ android:pathData="m10.412 17.244h-0.70313v-4.4805q-0.25391 0.24219-0.66797 0.48438-0.41016 0.24219-0.73828 0.36328v-0.67969q0.58984-0.27734 1.0313-0.67188 0.44141-0.39453 0.625-0.76563h0.45313zm1.2008-1.5 0.73828-0.0625q0.08203 0.53906 0.37891 0.8125 0.30078 0.26953 0.72266 0.26953 0.50781 0 0.85938-0.38281t0.35156-1.0156q0-0.60156-0.33984-0.94922-0.33594-0.34766-0.88281-0.34766-0.33984 0-0.61328 0.15625-0.27344 0.15234-0.42969 0.39844l-0.66016-0.08594 0.55469-2.9414h2.8477v0.67188h-2.2852l-0.30859 1.5391q0.51562-0.35938 1.082-0.35938 0.75 0 1.2656 0.51953 0.51562 0.51953 0.51562 1.3359 0 0.77734-0.45312 1.3438-0.55078 0.69531-1.5039 0.69531-0.78125 0-1.2773-0.4375-0.49219-0.4375-0.5625-1.1602z" />
23
+ </group>
24
+ </group>
25
+ </vector>
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="24dp"
4
+ android:height="24dp"
5
+ android:viewportWidth="24"
6
+ android:viewportHeight="24">
7
+ <group
8
+ android:pivotX="12"
9
+ android:pivotY="4"
10
+ android:scaleX="1.15"
11
+ android:scaleY="1.15">
12
+ <path
13
+ android:fillColor="?android:attr/colorControlNormal"
14
+ android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
15
+ <group
16
+ android:pivotX="12"
17
+ android:pivotY="8"
18
+ android:scaleX="0.9"
19
+ android:scaleY="0.9">
20
+ <path
21
+ android:fillColor="?android:attr/colorControlNormal"
22
+ android:pathData="m 10.030088,16.051511 0.770367,-0.06521 q 0.08559,0.562491 0.395373,0.847811 0.313853,0.281245 0.754062,0.281245 0.529882,0 0.896723,-0.399449 0.366841,-0.39945 0.366841,-1.059764 0,-0.627706 -0.354612,-0.990471 -0.350538,-0.362765 -0.92118,-0.362765 -0.354613,0 -0.639933,0.163041 -0.285322,0.158964 -0.448362,0.415752 l -0.688846,-0.08967 0.578794,-3.069238 h 2.971413 v 0.701074 H 11.32626 l -0.322005,1.60595 q 0.538034,-0.374993 1.129056,-0.374993 0.782594,0 1.320628,0.54211 0.538034,0.542109 0.538034,1.393996 0,0.811127 -0.472817,1.402149 -0.574718,0.72553 -1.569266,0.72553 -0.815202,0 -1.332856,-0.456514 -0.513578,-0.456516 -0.586946,-1.210578 z" />
23
+ </group>
24
+ </group>
25
+ </vector>