expo-video-subtitle 0.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 (255) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/FORK.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +108 -0
  5. package/android/build.gradle +32 -0
  6. package/android/src/main/AndroidManifest.xml +10 -0
  7. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +258 -0
  8. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  9. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  10. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +87 -0
  11. package/android/src/main/java/expo/modules/video/VideoCache.kt +109 -0
  12. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +45 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +446 -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 +457 -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/FullscreenOrientation.kt +25 -0
  23. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  24. package/android/src/main/java/expo/modules/video/enums/VideoRange.kt +27 -0
  25. package/android/src/main/java/expo/modules/video/listeners/VideoManagerListener.kt +10 -0
  26. package/android/src/main/java/expo/modules/video/listeners/VideoPlayerListener.kt +35 -0
  27. package/android/src/main/java/expo/modules/video/listeners/VideoViewListener.kt +13 -0
  28. package/android/src/main/java/expo/modules/video/managers/AudioFocusManager.kt +242 -0
  29. package/android/src/main/java/expo/modules/video/managers/PictureInPictureManager.kt +329 -0
  30. package/android/src/main/java/expo/modules/video/managers/VideoManager.kt +194 -0
  31. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +267 -0
  32. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +62 -0
  33. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  34. package/android/src/main/java/expo/modules/video/player/DefaultLoadControl.java +552 -0
  35. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +116 -0
  36. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +172 -0
  37. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +598 -0
  38. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +126 -0
  39. package/android/src/main/java/expo/modules/video/player/VideoPlayerKeepAwake.kt +91 -0
  40. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +57 -0
  41. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +126 -0
  42. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +18 -0
  43. package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +17 -0
  44. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +27 -0
  45. package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +14 -0
  46. package/android/src/main/java/expo/modules/video/records/PiPParams.kt +38 -0
  47. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +21 -0
  48. package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +15 -0
  49. package/android/src/main/java/expo/modules/video/records/ScrubbingModeOptions.kt +33 -0
  50. package/android/src/main/java/expo/modules/video/records/SeekTolerance.kt +30 -0
  51. package/android/src/main/java/expo/modules/video/records/SubtitleSource.kt +54 -0
  52. package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +98 -0
  53. package/android/src/main/java/expo/modules/video/records/Tracks.kt +115 -0
  54. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  55. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +14 -0
  56. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +16 -0
  57. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +113 -0
  58. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +26 -0
  59. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +76 -0
  60. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  61. package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
  62. package/android/src/main/java/expo/modules/video/utils/MediaSessionUtils.kt +14 -0
  63. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +26 -0
  64. package/android/src/main/java/expo/modules/video/utils/PictureInPictureHelperFragment.kt +49 -0
  65. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  66. package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +68 -0
  67. package/android/src/main/java/expo/modules/video/utils/ViewVisibilityUtils.kt +28 -0
  68. package/android/src/main/java/expo/modules/video/utils/WeakMutableSet.kt +120 -0
  69. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  70. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  71. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  72. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  73. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  74. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  75. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  76. package/android/src/main/res/layout/fullscreen_player_activity.xml +20 -0
  77. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  78. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  79. package/android/src/main/res/values/styles.xml +9 -0
  80. package/app.plugin.js +1 -0
  81. package/build/NativeVideoAirPlayButtonView.d.ts +3 -0
  82. package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
  83. package/build/NativeVideoAirPlayButtonView.js +3 -0
  84. package/build/NativeVideoAirPlayButtonView.js.map +1 -0
  85. package/build/NativeVideoModule.d.ts +13 -0
  86. package/build/NativeVideoModule.d.ts.map +1 -0
  87. package/build/NativeVideoModule.js +3 -0
  88. package/build/NativeVideoModule.js.map +1 -0
  89. package/build/NativeVideoModule.web.d.ts +5 -0
  90. package/build/NativeVideoModule.web.d.ts.map +1 -0
  91. package/build/NativeVideoModule.web.js +5 -0
  92. package/build/NativeVideoModule.web.js.map +1 -0
  93. package/build/NativeVideoView.d.ts +4 -0
  94. package/build/NativeVideoView.d.ts.map +1 -0
  95. package/build/NativeVideoView.js +6 -0
  96. package/build/NativeVideoView.js.map +1 -0
  97. package/build/VideoAirPlayButton.d.ts +10 -0
  98. package/build/VideoAirPlayButton.d.ts.map +1 -0
  99. package/build/VideoAirPlayButton.ios.d.ts +3 -0
  100. package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
  101. package/build/VideoAirPlayButton.ios.js +6 -0
  102. package/build/VideoAirPlayButton.ios.js.map +1 -0
  103. package/build/VideoAirPlayButton.js +13 -0
  104. package/build/VideoAirPlayButton.js.map +1 -0
  105. package/build/VideoAirPlayButton.types.d.ts +35 -0
  106. package/build/VideoAirPlayButton.types.d.ts.map +1 -0
  107. package/build/VideoAirPlayButton.types.js +2 -0
  108. package/build/VideoAirPlayButton.types.js.map +1 -0
  109. package/build/VideoModule.d.ts +35 -0
  110. package/build/VideoModule.d.ts.map +1 -0
  111. package/build/VideoModule.js +44 -0
  112. package/build/VideoModule.js.map +1 -0
  113. package/build/VideoPlayer.d.ts +17 -0
  114. package/build/VideoPlayer.d.ts.map +1 -0
  115. package/build/VideoPlayer.js +55 -0
  116. package/build/VideoPlayer.js.map +1 -0
  117. package/build/VideoPlayer.types.d.ts +782 -0
  118. package/build/VideoPlayer.types.d.ts.map +1 -0
  119. package/build/VideoPlayer.types.js +2 -0
  120. package/build/VideoPlayer.types.js.map +1 -0
  121. package/build/VideoPlayer.web.d.ts +79 -0
  122. package/build/VideoPlayer.web.d.ts.map +1 -0
  123. package/build/VideoPlayer.web.js +388 -0
  124. package/build/VideoPlayer.web.js.map +1 -0
  125. package/build/VideoPlayerEvents.types.d.ts +278 -0
  126. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  127. package/build/VideoPlayerEvents.types.js +2 -0
  128. package/build/VideoPlayerEvents.types.js.map +1 -0
  129. package/build/VideoThumbnail.d.ts +29 -0
  130. package/build/VideoThumbnail.d.ts.map +1 -0
  131. package/build/VideoThumbnail.js +4 -0
  132. package/build/VideoThumbnail.js.map +1 -0
  133. package/build/VideoView.d.ts +69 -0
  134. package/build/VideoView.d.ts.map +1 -0
  135. package/build/VideoView.js +100 -0
  136. package/build/VideoView.js.map +1 -0
  137. package/build/VideoView.types.d.ts +362 -0
  138. package/build/VideoView.types.d.ts.map +1 -0
  139. package/build/VideoView.types.js +2 -0
  140. package/build/VideoView.types.js.map +1 -0
  141. package/build/VideoView.web.d.ts +9 -0
  142. package/build/VideoView.web.d.ts.map +1 -0
  143. package/build/VideoView.web.js +239 -0
  144. package/build/VideoView.web.js.map +1 -0
  145. package/build/index.d.ts +11 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +6 -0
  148. package/build/index.js.map +1 -0
  149. package/build/resolveAssetSource.d.ts +3 -0
  150. package/build/resolveAssetSource.d.ts.map +1 -0
  151. package/build/resolveAssetSource.js +3 -0
  152. package/build/resolveAssetSource.js.map +1 -0
  153. package/build/resolveAssetSource.web.d.ts +4 -0
  154. package/build/resolveAssetSource.web.d.ts.map +1 -0
  155. package/build/resolveAssetSource.web.js +16 -0
  156. package/build/resolveAssetSource.web.js.map +1 -0
  157. package/expo-module.config.json +9 -0
  158. package/ios/Cache/CachableRequest.swift +46 -0
  159. package/ios/Cache/CacheVideoAssetTransportProvider.swift +64 -0
  160. package/ios/Cache/CachedResource.swift +164 -0
  161. package/ios/Cache/CachingHelpers.swift +107 -0
  162. package/ios/Cache/MediaFileHandle.swift +139 -0
  163. package/ios/Cache/MediaInfo.swift +185 -0
  164. package/ios/Cache/ResourceLoaderDelegate.swift +306 -0
  165. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  166. package/ios/Cache/VideoCacheManager.swift +192 -0
  167. package/ios/ContentKeyDelegate.swift +214 -0
  168. package/ios/ContentKeyManager.swift +21 -0
  169. package/ios/Enums/AudioMixingMode.swift +37 -0
  170. package/ios/Enums/ContentType.swift +11 -0
  171. package/ios/Enums/DRMType.swift +20 -0
  172. package/ios/Enums/FullscreenOrientation.swift +34 -0
  173. package/ios/Enums/KeepFullscreenOnPiPStopBehavior.swift +20 -0
  174. package/ios/Enums/PlayerStatus.swift +10 -0
  175. package/ios/Enums/VideoContentFit.swift +39 -0
  176. package/ios/Enums/VideoRange.swift +21 -0
  177. package/ios/ExpoVideo.podspec +27 -0
  178. package/ios/NowPlayingManager.swift +304 -0
  179. package/ios/OrientationAVPlayerViewController.swift +263 -0
  180. package/ios/Records/BufferOptions.swift +12 -0
  181. package/ios/Records/DRMOptions.swift +24 -0
  182. package/ios/Records/FullscreenOptions.swift +14 -0
  183. package/ios/Records/PlaybackError.swift +10 -0
  184. package/ios/Records/ScrubbingModeOptions.swift +7 -0
  185. package/ios/Records/SeekTolerance.swift +20 -0
  186. package/ios/Records/SubtitleSource.swift +32 -0
  187. package/ios/Records/SubtitleStyle.swift +118 -0
  188. package/ios/Records/Tracks.swift +229 -0
  189. package/ios/Records/VideoEventPayloads.swift +81 -0
  190. package/ios/Records/VideoMetadata.swift +16 -0
  191. package/ios/Records/VideoSize.swift +15 -0
  192. package/ios/Records/VideoSource.swift +28 -0
  193. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  194. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  195. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  196. package/ios/Utils/AVAssetVariant+VideoTracks.swift +20 -0
  197. package/ios/Utils/AvAssetTrack+VideoTracks.swift +79 -0
  198. package/ios/Utils/FourCharCode+toString.swift +25 -0
  199. package/ios/Utils/HlsUriUtils.swift +27 -0
  200. package/ios/Utils/URL+MediaLibraryAssets.swift +68 -0
  201. package/ios/VideoAirPlayButtonView.swift +70 -0
  202. package/ios/VideoAsset.swift +98 -0
  203. package/ios/VideoAssetTransport/VideoAssetLoadPlan.swift +69 -0
  204. package/ios/VideoAssetTransport/VideoAssetSourceDescriptor.swift +29 -0
  205. package/ios/VideoAssetTransport/VideoAssetTransportProvider.swift +24 -0
  206. package/ios/VideoAssetTransport/VideoAssetTransportRegistry.swift +103 -0
  207. package/ios/VideoExceptions.swift +53 -0
  208. package/ios/VideoItem.swift +11 -0
  209. package/ios/VideoManager.swift +182 -0
  210. package/ios/VideoModule.swift +413 -0
  211. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  212. package/ios/VideoPlayer.swift +485 -0
  213. package/ios/VideoPlayerAudioTracks.swift +109 -0
  214. package/ios/VideoPlayerItem.swift +169 -0
  215. package/ios/VideoPlayerObserver.swift +612 -0
  216. package/ios/VideoPlayerSeeker.swift +77 -0
  217. package/ios/VideoPlayerSubtitleSideload.swift +203 -0
  218. package/ios/VideoPlayerSubtitles.swift +88 -0
  219. package/ios/VideoSourceLoader.swift +90 -0
  220. package/ios/VideoSourceLoaderListener.swift +34 -0
  221. package/ios/VideoView.swift +201 -0
  222. package/package.json +51 -0
  223. package/plugin/build/index.d.ts +3 -0
  224. package/plugin/build/index.js +3 -0
  225. package/plugin/build/withExpoVideo.d.ts +9 -0
  226. package/plugin/build/withExpoVideo.js +73 -0
  227. package/plugin/index.d.ts +1 -0
  228. package/plugin/index.js +1 -0
  229. package/plugin/jest.config.js +1 -0
  230. package/plugin/src/index.ts +3 -0
  231. package/plugin/src/withExpoVideo.ts +103 -0
  232. package/plugin/tsconfig.json +10 -0
  233. package/src/NativeVideoAirPlayButtonView.ts +3 -0
  234. package/src/NativeVideoModule.ts +16 -0
  235. package/src/NativeVideoModule.web.ts +4 -0
  236. package/src/NativeVideoView.ts +8 -0
  237. package/src/VideoAirPlayButton.ios.tsx +8 -0
  238. package/src/VideoAirPlayButton.tsx +14 -0
  239. package/src/VideoAirPlayButton.types.ts +39 -0
  240. package/src/VideoModule.ts +47 -0
  241. package/src/VideoPlayer.tsx +74 -0
  242. package/src/VideoPlayer.types.ts +893 -0
  243. package/src/VideoPlayer.web.tsx +466 -0
  244. package/src/VideoPlayerEvents.types.ts +332 -0
  245. package/src/VideoThumbnail.ts +31 -0
  246. package/src/VideoView.tsx +111 -0
  247. package/src/VideoView.types.ts +405 -0
  248. package/src/VideoView.web.tsx +293 -0
  249. package/src/index.ts +26 -0
  250. package/src/resolveAssetSource.ts +2 -0
  251. package/src/resolveAssetSource.web.ts +17 -0
  252. package/src/ts-declarations/react-native-assets.d.ts +22 -0
  253. package/tsconfig.all.json +11 -0
  254. package/tsconfig.json +10 -0
  255. package/tsconfig.tsbuildinfo +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here.
4
+
5
+ This package is a fork of [`expo-video`](https://github.com/expo/expo/tree/main/packages/expo-video).
6
+ Changes inherited from the upstream base version are not repeated here — see the
7
+ [upstream changelog](https://github.com/expo/expo/blob/main/packages/expo-video/CHANGELOG.md)
8
+ for the history of everything that isn't subtitle related.
9
+
10
+ ## 0.1.0 — Unreleased
11
+
12
+ Initial release. Based on `expo-video` **56.1.4** (Expo SDK 56).
13
+
14
+ ### Added
15
+
16
+ - **External (sideloaded) subtitles** via `VideoSource.subtitleTracks`. Attach WebVTT or SubRip
17
+ subtitle files (local or remote) to any source — most importantly progressive **MP4**, which,
18
+ unlike HLS/DASH manifests, cannot advertise its own subtitle renditions. Sideloaded tracks are
19
+ merged into the media and surface through `availableSubtitleTracks`, so they are selected with
20
+ `subtitleTrack` exactly like embedded tracks.
21
+ - Android: merged with `MediaItem.SubtitleConfiguration` (Media3 wraps them in a `MergingMediaSource`).
22
+ - iOS: merged into an `AVMutableComposition` as a `.text` track. SRT is converted to WebVTT, remote
23
+ files are cached locally first, and per-track time ranges are clamped to the video duration.
24
+ Applies to progressive, non-DRM sources on the asynchronous loading path.
25
+ - **Custom subtitle styling** via the `VideoView` `subtitleStyle` prop: `textColor`, `backgroundColor`,
26
+ `windowColor`, `fontSize`, `fontFamily`, `bold`, `edgeType`, `edgeColor`, and `bottomOffset`.
27
+ Applies to embedded and sideloaded tracks alike.
28
+ - Android: `CaptionStyleCompat` on the `PlayerView` subtitle view.
29
+ - iOS: `AVPlayerItem.textStyleRules`.
30
+
31
+ ### Changed
32
+
33
+ - The Android `publication` block and the iOS prebuild artifacts are removed, so the modified
34
+ Kotlin and Swift are compiled from source instead of resolving a precompiled AAR/XCFramework.
package/FORK.md ADDED
@@ -0,0 +1,149 @@
1
+ # Fork maintenance guide
2
+
3
+ Everything you need to know to (a) understand exactly how this package differs from `expo-video`,
4
+ and (b) pull a newer `expo-video` release into it **without losing the subtitle work**.
5
+
6
+ - **Baseline:** `expo-video@56.1.4` (Expo SDK 56), as published on npm.
7
+ - **Native module name:** unchanged (`ExpoVideo`) — this is a drop-in replacement, not a coexisting package.
8
+ - **Design principle:** the fork is deliberately **surgical and additive**. 5 new files hold almost all
9
+ of the logic (505 lines); the 14 touched upstream files receive ~130 added lines and only ~29 removed
10
+ ones. Nothing upstream is rewritten, so a version bump is a small, mechanical re-apply.
11
+
12
+ ---
13
+
14
+ ## 1. Change manifest
15
+
16
+ Measured against the published `expo-video@56.1.4` tarball. `+`/`-` are added/removed lines.
17
+
18
+ ### Added — new files (never conflict on sync, just copy them over)
19
+
20
+ | File | Lines | Purpose |
21
+ | --- | --- | --- |
22
+ | `ios/Records/SubtitleSource.swift` | 32 | Record for one sideloaded subtitle; `isSrt` detection. |
23
+ | `ios/Records/SubtitleStyle.swift` | 118 | Record for caption styling; hex→ARGB parsing; builds `[AVTextStyleRule]`. |
24
+ | `ios/VideoPlayerSubtitleSideload.swift` | 203 | Builds the `AVMutableComposition`, downloads remote subs, SRT→WebVTT converter. |
25
+ | `android/…/records/SubtitleSource.kt` | 54 | Record → `MediaItem.SubtitleConfiguration` + MIME resolution. |
26
+ | `android/…/records/SubtitleStyle.kt` | 98 | Record → `CaptionStyleCompat`; hex colour parsing. |
27
+ | `FORK.md`, `LICENSE` | — | This guide; MIT licence (upstream copyright preserved). |
28
+
29
+ ### Modified — upstream files carrying fork edits (re-apply these after a sync)
30
+
31
+ | File | Δ | What to re-apply |
32
+ | --- | --- | --- |
33
+ | `src/VideoPlayer.types.ts` | +59 | `subtitleTracks?: SubtitleSource[]` on `VideoSourceObject`; the exported `SubtitleSource` type. |
34
+ | `src/VideoView.types.ts` | +94 | `subtitleStyle?: SubtitleStyle` on `VideoViewProps`; `SubtitleStyle` + `SubtitleEdgeType` types. |
35
+ | `src/index.ts` | +2 | Export `SubtitleStyle`, `SubtitleEdgeType` (`SubtitleSource` rides on `export type *`). |
36
+ | `src/ts-declarations/react-native-assets.d.ts` | +22 / −1 | Replace upstream's `../../../expo-asset/…` reference (monorepo-only) with standalone `declare module` shims. **Always needed outside the Expo monorepo.** |
37
+ | `ios/Records/VideoSource.swift` | +3 | `@Field var subtitleTracks: [SubtitleSource]?`. |
38
+ | `ios/VideoPlayerItem.swift` | +17 / −2 | In the **async** init: hoist `isHls` to a local, and build the sideload composition for non-HLS/non-DRM sources before `super.init(asset:)`. |
39
+ | `ios/VideoPlayer.swift` | +15 | `subtitleStyle` property + `applySubtitleStyle()`; re-apply `textStyleRules` in `onItemChanged`. |
40
+ | `ios/VideoView.swift` | +8 | `subtitleStyle` property; forward it when `player` is (re)assigned. |
41
+ | `ios/VideoModule.swift` | +4 | `Prop("subtitleStyle")`. |
42
+ | `android/…/records/VideoSource.kt` | +9 / −2 | `subtitleTracks` field; `setSubtitleConfigurations(…)` in `toMediaItem()`; subtitle key in `toMediaId()`. |
43
+ | `android/…/utils/SubtitleUtils.kt` | +27 / −16 | `configureSubtitleView(…, customStyle)` overload and a `styleProvider` on the captioning listener. Keeps the original behaviour when no custom style is set. |
44
+ | `android/…/VideoView.kt` | +12 / −5 | `subtitleStyle` property; pass it at all 4 `configureSubtitleView` call sites + the captioning listener. |
45
+ | `android/…/VideoModule.kt` | +4 | `Prop("subtitleStyle")`. |
46
+ | `android/…/FullscreenPlayerActivity.kt` | +3 / −3 | Pass `videoView.subtitleStyle` at its 2 call sites + the captioning listener. |
47
+ | `package.json` | — | Fork identity + build scripts — see §3. |
48
+ | `expo-module.config.json` | — | **Android `publication` block removed** — see §3. |
49
+ | `README.md`, `CHANGELOG.md` | — | Fork docs; do not take upstream's. |
50
+
51
+ ### Removed — upstream artifacts deliberately dropped
52
+
53
+ | Path | Why |
54
+ | --- | --- |
55
+ | `local-maven-repo/` (Android AAR) | Upstream ships a **precompiled** `expo.modules.video` AAR. If present, Gradle resolves it and **silently ignores the modified Kotlin**. |
56
+ | `prebuilds/` (iOS XCFramework) | Same trap on iOS: CocoaPods links the prebuilt `ExpoVideo.xcframework` instead of compiling the modified Swift. |
57
+ | `spm.config.json` | Swift Package Manager prebuild config; meaningless once we build from source. |
58
+
59
+ > **This is the single most important thing to preserve.** If a sync re-introduces the `publication`
60
+ > block, `local-maven-repo/` or `prebuilds/`, the app keeps building — it just runs **upstream's**
61
+ > video module, and every subtitle feature silently disappears. Verify with §4.
62
+
63
+ ---
64
+
65
+ ## 2. Syncing a newer `expo-video`
66
+
67
+ ```bash
68
+ # 0. Start clean, on a branch, with the current state tagged for easy diffing.
69
+ git checkout -b sync/expo-video-<new-version>
70
+ git tag pre-sync-<new-version>
71
+
72
+ # 1. Get the target version's source (must be the EXACT version your app uses).
73
+ npm pack expo-video@<new-version>
74
+ tar -xzf expo-video-<new-version>.tgz # -> ./package
75
+
76
+ # 2. Record the fork's edits BEFORE overwriting, so you can re-apply them.
77
+ git diff pre-sync-<new-version> -- src ios android > /tmp/fork.patch # or just use §1's table
78
+
79
+ # 3. Replace the upstream-owned trees.
80
+ rm -rf src ios android plugin
81
+ cp -R package/src package/ios package/android .
82
+ mkdir -p plugin && cp -R package/plugin/src plugin/src
83
+ cp package/plugin/tsconfig.json plugin/tsconfig.json
84
+ cp package/expo-module.config.json package/app.plugin.js package/tsconfig.json .
85
+
86
+ # 4. Drop the precompiled artifacts (see §1 "Removed"). NEVER skip this.
87
+ rm -rf local-maven-repo prebuilds spm.config.json package
88
+ ```
89
+
90
+ Then re-apply the fork:
91
+
92
+ 5. **Copy the 5 new files back** (they are self-contained — `git checkout pre-sync-<v> -- <path>` works).
93
+ 6. **Re-apply the 14 modified files** using the table in §1. Most are pure insertions; the only ones
94
+ with real deletions are `SubtitleUtils.kt` (function rewritten), `VideoView.kt`/`FullscreenPlayerActivity.kt`
95
+ (call-site signatures), `VideoPlayerItem.swift` (`isHls` hoist) and the ts-declarations shim.
96
+ 7. **Remove the Android `publication` block** from `expo-module.config.json`.
97
+ 8. **Restore the fork's `package.json`** (§3) and keep `README.md`, `CHANGELOG.md`, `LICENSE`, `FORK.md`.
98
+ 9. Update the baseline version in this file, the README, and add a `CHANGELOG.md` entry.
99
+ 10. Verify with §4, then commit.
100
+
101
+ ---
102
+
103
+ ## 3. `package.json` invariants
104
+
105
+ Upstream's `package.json` will overwrite these if copied blindly. Keep:
106
+
107
+ - `name: expo-video-subtitle`, the fork's `version`, `author`, `contributors`, `homepage`, `repository`, `bugs`.
108
+ - `"build": "expo-module build && npm run build:plugin"` and `"build:plugin": "tsc -p plugin"`.
109
+ **`expo-module build` only compiles `src/` → `build/`.** Without `build:plugin` there is no
110
+ `plugin/build/withExpoVideo.js`, and `app.plugin.js` (`require('./plugin/build/withExpoVideo')`)
111
+ throws for every consumer during config resolution.
112
+ - `"prepublishOnly": "npm run clean && npm run build"` so a publish can never ship a stale `build/`.
113
+ - `expo-module-scripts` must track the SDK (56.x for SDK 56). An older major provides a different CLI
114
+ (`expo-build` vs `expo-module build`) and the build breaks.
115
+ - `plugin/index.js` + `plugin/index.d.ts` shims (upstream layout parity).
116
+
117
+ ---
118
+
119
+ ## 4. Verification
120
+
121
+ Run all four. The native ones are what actually prove the fork survived the sync.
122
+
123
+ ```bash
124
+ npm run typecheck # must be clean
125
+ npm run lint
126
+ npm pack --dry-run # expect build/, plugin/build/, LICENSE, native sources;
127
+ # NO node_modules, prebuilds, local-maven-repo
128
+ ```
129
+
130
+ **Native compile** — because both platforms default to precompiled artifacts, you must build from
131
+ source inside a host app. Overlay this package into the app's `node_modules/expo-video`, then:
132
+
133
+ ```bash
134
+ # Android — expect BUILD SUCCESSFUL
135
+ cd <app>/android && ./gradlew :expo-video:compileDebugKotlin
136
+
137
+ # iOS — expect BUILD SUCCEEDED
138
+ cd <app>/ios && pod install
139
+ xcodebuild -project Pods/Pods.xcodeproj -target ExpoVideo -sdk iphoneos \
140
+ -configuration Debug build CODE_SIGNING_ALLOWED=NO
141
+ ```
142
+
143
+ Back up `node_modules/expo-video` first and restore it (plus `pod install`) afterwards.
144
+
145
+ **Smoke test in the app:** an MP4 source with `subtitleTracks` must list the tracks in
146
+ `player.availableSubtitleTracks`, render when selected via `player.subtitleTrack`, and visibly change
147
+ appearance when `subtitleStyle` is set. If tracks are missing on Android, `Format.id`/`Format.language`
148
+ are probably null (see `SubtitleTrack.fromFormat`); if they are missing on iOS, the composition likely
149
+ fell back to the plain asset — check the `[expo-video]` warnings.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # expo-video-subtitle
2
+
3
+ A cross-platform, performant video component for React Native and Expo with Web support.
4
+
5
+ This package is a fork of `expo-video` (SDK 56 / `56.1.4`) that adds two subtitle capabilities on top of the original module:
6
+
7
+ 1. **External (sideloaded) subtitles** — attach subtitle files to any source, including progressive **MP4** (which, unlike HLS/DASH, cannot advertise its own subtitle renditions).
8
+ 2. **Custom subtitle styling** — control the text color, background, font, edge, and position of the rendered captions.
9
+
10
+ Everything else in the API is identical to `expo-video`, so the [upstream documentation](https://docs.expo.dev/versions/latest/sdk/video/) applies to all other props and methods.
11
+
12
+ ## What's different from `expo-video`
13
+
14
+ Nothing in the existing API was renamed, removed, or given new behaviour — the changes are purely
15
+ additive, plus one packaging change.
16
+
17
+ **Added**
18
+
19
+ | API | Where | Description |
20
+ | --- | --- | --- |
21
+ | `subtitleTracks?: SubtitleSource[]` | `VideoSource` | Attach external subtitle files to a source. See [Sideloading subtitles onto MP4](#sideloading-subtitles-onto-mp4). |
22
+ | `subtitleStyle?: SubtitleStyle` | `VideoView` prop | Customise caption appearance. See [Styling subtitles](#styling-subtitles). |
23
+ | `SubtitleSource`, `SubtitleStyle`, `SubtitleEdgeType` | exported types | Supporting TypeScript types. |
24
+
25
+ **Changed**
26
+
27
+ - **Native code is compiled from source.** Upstream ships a precompiled Android AAR and iOS
28
+ XCFramework; those are removed here, because a prebuilt binary would ignore the modified native
29
+ code. Expect your first native build after installing to take longer than with `expo-video`.
30
+
31
+ **Removed**
32
+
33
+ - Nothing from the public API. Only the upstream prebuild artifacts (`local-maven-repo/`,
34
+ `prebuilds/`, `spm.config.json`) — see above.
35
+
36
+ Maintaining this fork or pulling in a newer `expo-video`? See [FORK.md](./FORK.md) for the full change
37
+ manifest and the upstream sync procedure.
38
+
39
+ ## Installation
40
+
41
+ ```
42
+ npm install expo-video-subtitle
43
+ ```
44
+
45
+ The native module registers under the same name as `expo-video` (`ExpoVideo`). It is a **drop-in replacement**: a project must use either `expo-video` or `expo-video-subtitle`, not both. Remove `expo-video` from your dependencies and import from `expo-video-subtitle` instead:
46
+
47
+ ```tsx
48
+ import { useVideoPlayer, VideoView } from 'expo-video-subtitle';
49
+ ```
50
+
51
+ ## Sideloading subtitles onto MP4
52
+
53
+ Pass a `subtitleTracks` array on the `VideoSource`. The tracks are merged into the media and surface through `player.availableSubtitleTracks`, so they are selected the same way as embedded tracks.
54
+
55
+ ```tsx
56
+ import { useVideoPlayer, VideoView } from 'expo-video-subtitle';
57
+
58
+ const player = useVideoPlayer({
59
+ uri: 'https://cdn.example.com/movie.mp4',
60
+ subtitleTracks: [
61
+ { uri: 'https://cdn.example.com/en.vtt', language: 'en', label: 'English', default: true },
62
+ { uri: 'https://cdn.example.com/id.srt', language: 'id', label: 'Indonesia', format: 'srt' },
63
+ ],
64
+ });
65
+
66
+ // After the source loads, pick a track:
67
+ player.subtitleTrack = player.availableSubtitleTracks[0];
68
+ ```
69
+
70
+ `SubtitleSource` fields: `uri` (local `file://` or remote `http(s)://`), `language` (BCP-47 / ISO 639), optional `label`, optional `format` (`'vtt' | 'srt'`, inferred from the extension when omitted), and optional `default`.
71
+
72
+ | | Android | iOS |
73
+ | --- | --- | --- |
74
+ | Mechanism | `MediaItem.SubtitleConfiguration` (Media3 merges into a `MergingMediaSource`) | `AVMutableComposition` (copies the video/audio tracks and inserts a `.text` track) |
75
+ | Formats | VTT, SRT, TTML natively | VTT natively; **SRT is auto-converted to VTT**; remote files are **downloaded to a local cache first** |
76
+
77
+ > **iOS limitations:** sideloading only runs on the asynchronous loading path (the default `VideoPlayer` constructor and `replaceAsync`, not the synchronous `replace`), and only for progressive (non-HLS, non-DRM) sources. HLS/DASH already carry their own subtitle renditions.
78
+
79
+ ## Styling subtitles
80
+
81
+ Pass a `subtitleStyle` prop to `VideoView`. It applies to whichever subtitle track is currently displayed, whether embedded or sideloaded. Colors accept a hex string (`#RGB`, `#RRGGBB`, `#RRGGBBAA`) or `transparent`.
82
+
83
+ ```tsx
84
+ <VideoView
85
+ player={player}
86
+ subtitleStyle={{
87
+ textColor: '#FFFFFF',
88
+ backgroundColor: '#000000A0', // box drawn behind the text
89
+ windowColor: 'transparent', // background of the whole cue region
90
+ fontSize: 20, // Android: absolute sp · iOS: percent relative to default (100 = default)
91
+ fontFamily: 'Helvetica',
92
+ bold: true,
93
+ edgeType: 'outline', // 'none' | 'outline' | 'dropShadow' | 'raised' | 'depressed'
94
+ edgeColor: '#000000',
95
+ bottomOffset: 0.1, // Android only — fraction of the view height (0–1)
96
+ }}
97
+ />
98
+ ```
99
+
100
+ | Mechanism | Android | iOS |
101
+ | --- | --- | --- |
102
+ | Implementation | `CaptionStyleCompat` on the `PlayerView` subtitle view | `AVPlayerItem.textStyleRules` |
103
+
104
+ > **iOS note:** styling relies on `textStyleRules`, which only affects WebVTT captions the system renders. The device's **Settings → Accessibility → Subtitles & Captioning** preferences take precedence when enabled. `edgeType`/`edgeColor` and `bottomOffset` are Android-only.
105
+
106
+ ## License
107
+
108
+ MIT. Based on [`expo-video`](https://github.com/expo/expo/tree/main/packages/expo-video) by 650 Industries, Inc.
@@ -0,0 +1,32 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'host.exp.exponent'
7
+ version = '56.1.4'
8
+
9
+ android {
10
+ namespace "expo.modules.video"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName '56.1.4'
14
+ }
15
+ }
16
+
17
+ dependencies {
18
+ implementation 'com.facebook.react:react-android'
19
+
20
+ // Remember to keep this in sync with the version in `expo-audio`
21
+ def androidxMedia3Version = "1.9.0"
22
+ implementation "androidx.media3:media3-session:${androidxMedia3Version}"
23
+ implementation "androidx.media3:media3-exoplayer:${androidxMedia3Version}"
24
+ implementation "androidx.media3:media3-exoplayer-dash:${androidxMedia3Version}"
25
+ implementation "androidx.media3:media3-exoplayer-hls:${androidxMedia3Version}"
26
+ implementation "androidx.media3:media3-ui:${androidxMedia3Version}"
27
+ implementation "androidx.media3:media3-datasource-okhttp:${androidxMedia3Version}"
28
+
29
+ def fragment_version = "1.8.9"
30
+ implementation "androidx.fragment:fragment:$fragment_version"
31
+ implementation "androidx.fragment:fragment-ktx:$fragment_version"
32
+ }
@@ -0,0 +1,10 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+
4
+ <application>
5
+ <activity android:name=".FullscreenPlayerActivity"
6
+ android:supportsPictureInPicture="true"
7
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|navigation"
8
+ android:theme="@style/Fullscreen" />
9
+ </application>
10
+ </manifest>
@@ -0,0 +1,258 @@
1
+ package expo.modules.video
2
+
3
+ import android.app.Activity
4
+ import android.content.pm.ActivityInfo
5
+ import android.content.res.Configuration
6
+ import android.os.Build
7
+ import android.os.Bundle
8
+ import android.content.Context
9
+ import android.util.Log
10
+ import android.view.View
11
+ import android.view.accessibility.CaptioningManager
12
+ import android.view.WindowInsets
13
+ import android.view.WindowInsetsController
14
+ import android.widget.ImageButton
15
+ import androidx.media3.ui.PlayerView
16
+ import expo.modules.kotlin.exception.CodedException
17
+ import expo.modules.video.listeners.VideoManagerListener
18
+ import expo.modules.video.player.VideoPlayer
19
+ import expo.modules.video.records.FullscreenOptions
20
+ import expo.modules.video.utils.FullscreenActivityOrientationHelper
21
+ import expo.modules.video.utils.SubtitleUtils
22
+ import expo.modules.video.utils.applyPiPParams
23
+ import expo.modules.video.utils.applyRectHint
24
+ import expo.modules.video.utils.calculatePiPAspectRatio
25
+ import expo.modules.video.utils.calculateRectHint
26
+ import expo.modules.video.managers.VideoManager
27
+
28
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
29
+ class FullscreenPlayerActivity : Activity(), VideoManagerListener {
30
+ private lateinit var mContentView: View
31
+ private var videoViewId: String? = null
32
+ private var videoPlayer: VideoPlayer? = null
33
+ private lateinit var playerView: PlayerView
34
+ private lateinit var videoView: VideoView
35
+ private var didFinish = false
36
+ private var wasAutoPaused = false
37
+ private var isStopped = false
38
+ private lateinit var options: FullscreenOptions
39
+ private var orientationHelper: FullscreenActivityOrientationHelper? = null
40
+ private var captioningChangeListener: CaptioningManager.CaptioningChangeListener? = null
41
+
42
+ override fun onCreate(savedInstanceState: Bundle?) {
43
+ super.onCreate(savedInstanceState)
44
+
45
+ try {
46
+ videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY)
47
+ ?: throw FullScreenVideoViewNotFoundException()
48
+
49
+ options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
50
+ intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY, FullscreenOptions::class.java)
51
+ ?: throw FullScreenOptionsNotFoundException()
52
+ } else {
53
+ @Suppress("DEPRECATION")
54
+ intent.getSerializableExtra(INTENT_FULLSCREEN_OPTIONS_KEY) as? FullscreenOptions
55
+ ?: throw FullScreenOptionsNotFoundException()
56
+ }
57
+
58
+ videoView = videoViewId?.let { VideoManager.getVideoView(it) }
59
+ ?: throw FullScreenVideoViewNotFoundException()
60
+
61
+ orientationHelper = FullscreenActivityOrientationHelper(
62
+ this,
63
+ options,
64
+ onShouldAutoExit = {
65
+ finish()
66
+ },
67
+ onShouldReleaseOrientation = {
68
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
69
+ }
70
+ )
71
+ orientationHelper?.startOrientationEventListener()
72
+ } catch (e: CodedException) {
73
+ Log.e("ExpoVideo", "${e.message}", e)
74
+ finish()
75
+ return
76
+ }
77
+
78
+ setContentView(R.layout.fullscreen_player_activity)
79
+ mContentView = findViewById(R.id.enclosing_layout)
80
+ playerView = findViewById(R.id.player_view)
81
+ requestedOrientation = options.orientation.toActivityOrientation()
82
+
83
+ videoPlayer = videoView.videoPlayer
84
+ videoPlayer?.player?.let {
85
+ PlayerView.switchTargetView(it, videoView.playerView, playerView)
86
+ videoPlayer?.hasBeenDisconnectedFromVideoView() // The video player is disconnected. We are only using the ExoPlayer it contained
87
+ }
88
+
89
+ videoViewId?.let { VideoManager.registerFullscreenPlayerActivity(it, this) }
90
+ VideoManager.registerListener(this)
91
+ playerView.player?.let {
92
+ val aspectRatio = calculatePiPAspectRatio(it.videoSize, playerView.width, playerView.height, videoView.contentFit)
93
+ applyPiPParams(this, videoView.autoEnterPiP, aspectRatio)
94
+ }
95
+ }
96
+
97
+ override fun onPostCreate(savedInstanceState: Bundle?) {
98
+ super.onPostCreate(savedInstanceState)
99
+ hideStatusBar()
100
+ setupFullscreenButton()
101
+ val requiresLinearPlayback = videoPlayer?.requiresLinearPlayback ?: false
102
+ val buttonConfig = videoView.buttonOptions.copy(showBottomBar = true) // Always show bottom bar in fullscreen mode so user can exit
103
+ playerView.applyButtonOptions(buttonConfig, requiresLinearPlayback)
104
+ playerView.setTimeBarInteractive(requiresLinearPlayback)
105
+ playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
106
+ // On every re-layout ExoPlayer makes the timeBar interactive.
107
+ // We need to disable it to keep scrubbing off.
108
+ playerView.setTimeBarInteractive(requiresLinearPlayback)
109
+ }
110
+ playerView.setShowSubtitleButton(videoView.buttonOptions.showSubtitles ?: videoView.currentTrackHasSubtitles)
111
+
112
+ // Configure subtitle view to fix sizing issues with embedded styles (same as VideoView)
113
+ SubtitleUtils.configureSubtitleView(playerView, this, videoView.subtitleStyle)
114
+
115
+ // Set up listener for accessibility caption changes
116
+ setupCaptioningChangeListener()
117
+
118
+ playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
119
+ applyRectHint(this, calculateRectHint(playerView))
120
+ }
121
+ }
122
+
123
+ override fun finish() {
124
+ super.finish()
125
+ didFinish = true
126
+ videoViewId?.let {
127
+ try {
128
+ VideoManager.getVideoView(it).attachPlayer()
129
+ } catch (e: VideoViewNotFoundException) {
130
+ Log.w("ExpoVideo", "VideoView $it already unmounted when finishing fullscreen — skipping attachPlayer")
131
+ }
132
+ }
133
+
134
+ // Disable the exit transition
135
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
136
+ overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
137
+ } else {
138
+ @Suppress("DEPRECATION")
139
+ overridePendingTransition(0, 0)
140
+ }
141
+ }
142
+
143
+ override fun onResume() {
144
+ orientationHelper?.startOrientationEventListener()
145
+ playerView.useController = true
146
+ // Reconfigure subtitles when resuming (handles returning from settings)
147
+ SubtitleUtils.configureSubtitleView(playerView, this, videoView.subtitleStyle)
148
+ super.onResume()
149
+ }
150
+
151
+ override fun onPause() {
152
+ if (videoPlayer?.staysActiveInBackground != true && !didFinish) {
153
+ wasAutoPaused = videoPlayer?.player?.isPlaying == true
154
+ if (wasAutoPaused) {
155
+ playerView.useController = false
156
+ videoPlayer?.player?.pause()
157
+ }
158
+ }
159
+ orientationHelper?.stopOrientationEventListener()
160
+ super.onPause()
161
+ }
162
+
163
+ override fun onStop() {
164
+ isStopped = true
165
+ super.onStop()
166
+ }
167
+
168
+ override fun onStart() {
169
+ isStopped = false
170
+ super.onStart()
171
+ }
172
+
173
+ override fun onDestroy() {
174
+ super.onDestroy()
175
+
176
+ // Clean up captioning change listener
177
+ captioningChangeListener?.let {
178
+ val captioningManager = getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager
179
+ captioningManager?.removeCaptioningChangeListener(it)
180
+ captioningChangeListener = null
181
+ }
182
+
183
+ if (::videoView.isInitialized) {
184
+ videoView.exitFullscreen()
185
+ }
186
+ VideoManager.unregisterListener(this)
187
+ videoViewId?.let { VideoManager.unregisterFullscreenPlayerActivity(it) }
188
+ orientationHelper?.stopOrientationEventListener()
189
+ }
190
+
191
+ // When the app enters the foreground, we need to decide whether to close this activity.
192
+ // - If stopped: the user went home from fullscreen and came back via app icon -> close
193
+ // - If in PiP: the user went home while in PiP and came back via app icon -> close
194
+ // - If only paused (not stopped, not yet in PiP): we're mid-transition into PiP -> keep alive
195
+ override fun onAppForegrounded() {
196
+ if (isStopped || isInPictureInPictureMode) {
197
+ finish()
198
+ }
199
+ }
200
+
201
+ private fun setupFullscreenButton() {
202
+ playerView.setFullscreenButtonClickListener { finish() }
203
+
204
+ val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
205
+ fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit)
206
+ }
207
+
208
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
209
+ if (!isInPictureInPictureMode) {
210
+ playerView.useController = videoView.useNativeControls
211
+ } else {
212
+ playerView.useController = false
213
+ }
214
+ if (wasAutoPaused && isInPictureInPictureMode) {
215
+ videoPlayer?.player?.play()
216
+ }
217
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
218
+ }
219
+
220
+ private fun hideStatusBar() {
221
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
222
+ val controller = mContentView.windowInsetsController
223
+ controller?.apply {
224
+ systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
225
+ hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
226
+ }
227
+ } else {
228
+ @Suppress("DEPRECATION")
229
+ mContentView.systemUiVisibility = (
230
+ View.SYSTEM_UI_FLAG_LOW_PROFILE
231
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
232
+ or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
233
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
234
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
235
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
236
+ )
237
+ }
238
+ }
239
+
240
+ private fun setupCaptioningChangeListener() {
241
+ val captioningManager = getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager
242
+
243
+ captioningChangeListener = SubtitleUtils.createCaptioningChangeListener(playerView, this) { videoView.subtitleStyle }
244
+
245
+ captioningChangeListener?.let { listener ->
246
+ captioningManager?.addCaptioningChangeListener(listener)
247
+ }
248
+ }
249
+
250
+ override fun onConfigurationChanged(newConfig: Configuration) {
251
+ super.onConfigurationChanged(newConfig)
252
+ orientationHelper?.onConfigurationChanged(newConfig)
253
+ }
254
+
255
+ companion object {
256
+ const val INTENT_FULLSCREEN_OPTIONS_KEY = "fullscreen_options"
257
+ }
258
+ }
@@ -0,0 +1,54 @@
1
+ package expo.modules.video
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import expo.modules.video.delegates.IgnoreSameSet
6
+ import java.lang.ref.WeakReference
7
+
8
+ fun interface IntervalUpdateEmitter {
9
+ fun emitTimeUpdate()
10
+ }
11
+
12
+ class IntervalUpdateClock(emitter: IntervalUpdateEmitter) {
13
+ private val emitter: WeakReference<IntervalUpdateEmitter> = WeakReference(emitter)
14
+ private var handler: Handler = Handler(Looper.getMainLooper())
15
+
16
+ // TODO: @behenate - Once the Player instance is available in OnStartObserving we can automatically start/stop the clock.
17
+ var interval by IgnoreSameSet(0L) { new: Long, _: Long? ->
18
+ if (new <= 0) {
19
+ stop()
20
+ } else {
21
+ startOrUpdate()
22
+ }
23
+ }
24
+
25
+ private var isRunning: Boolean = false
26
+
27
+ private fun stop() {
28
+ handler.removeCallbacksAndMessages(null)
29
+ isRunning = false
30
+ }
31
+
32
+ private fun startOrUpdate() {
33
+ if (!isRunning) {
34
+ emitter.get()?.emitTimeUpdate()
35
+ } else {
36
+ handler.removeCallbacksAndMessages(null)
37
+ }
38
+ isRunning = true
39
+ scheduleNextUpdate()
40
+ }
41
+
42
+ private fun scheduleNextUpdate() {
43
+ if (interval <= 0L) {
44
+ return
45
+ }
46
+
47
+ val update = {
48
+ emitter.get()?.emitTimeUpdate()
49
+ scheduleNextUpdate()
50
+ }
51
+
52
+ handler.postDelayed(update, interval)
53
+ }
54
+ }