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.
- package/CHANGELOG.md +34 -0
- package/FORK.md +149 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +258 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +87 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +109 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +45 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +446 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +457 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +25 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/enums/VideoRange.kt +27 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoManagerListener.kt +10 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoPlayerListener.kt +35 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoViewListener.kt +13 -0
- package/android/src/main/java/expo/modules/video/managers/AudioFocusManager.kt +242 -0
- package/android/src/main/java/expo/modules/video/managers/PictureInPictureManager.kt +329 -0
- package/android/src/main/java/expo/modules/video/managers/VideoManager.kt +194 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +267 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +62 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/DefaultLoadControl.java +552 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +116 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +172 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +598 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +126 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerKeepAwake.kt +91 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +57 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +126 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +18 -0
- package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +17 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +27 -0
- package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/PiPParams.kt +38 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +21 -0
- package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/ScrubbingModeOptions.kt +33 -0
- package/android/src/main/java/expo/modules/video/records/SeekTolerance.kt +30 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleSource.kt +54 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +98 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +115 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +16 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +113 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +26 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +76 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
- package/android/src/main/java/expo/modules/video/utils/MediaSessionUtils.kt +14 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +26 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureHelperFragment.kt +49 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +68 -0
- package/android/src/main/java/expo/modules/video/utils/ViewVisibilityUtils.kt +28 -0
- package/android/src/main/java/expo/modules/video/utils/WeakMutableSet.kt +120 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +20 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoAirPlayButtonView.d.ts +3 -0
- package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
- package/build/NativeVideoAirPlayButtonView.js +3 -0
- package/build/NativeVideoAirPlayButtonView.js.map +1 -0
- package/build/NativeVideoModule.d.ts +13 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +5 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +5 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoAirPlayButton.d.ts +10 -0
- package/build/VideoAirPlayButton.d.ts.map +1 -0
- package/build/VideoAirPlayButton.ios.d.ts +3 -0
- package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
- package/build/VideoAirPlayButton.ios.js +6 -0
- package/build/VideoAirPlayButton.ios.js.map +1 -0
- package/build/VideoAirPlayButton.js +13 -0
- package/build/VideoAirPlayButton.js.map +1 -0
- package/build/VideoAirPlayButton.types.d.ts +35 -0
- package/build/VideoAirPlayButton.types.d.ts.map +1 -0
- package/build/VideoAirPlayButton.types.js +2 -0
- package/build/VideoAirPlayButton.types.js.map +1 -0
- package/build/VideoModule.d.ts +35 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +44 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +17 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +55 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +782 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +79 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +388 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +278 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +4 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +69 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +100 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +362 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +239 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +46 -0
- package/ios/Cache/CacheVideoAssetTransportProvider.swift +64 -0
- package/ios/Cache/CachedResource.swift +164 -0
- package/ios/Cache/CachingHelpers.swift +107 -0
- package/ios/Cache/MediaFileHandle.swift +139 -0
- package/ios/Cache/MediaInfo.swift +185 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +306 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +192 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +11 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/FullscreenOrientation.swift +34 -0
- package/ios/Enums/KeepFullscreenOnPiPStopBehavior.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/Enums/VideoRange.swift +21 -0
- package/ios/ExpoVideo.podspec +27 -0
- package/ios/NowPlayingManager.swift +304 -0
- package/ios/OrientationAVPlayerViewController.swift +263 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/FullscreenOptions.swift +14 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/ScrubbingModeOptions.swift +7 -0
- package/ios/Records/SeekTolerance.swift +20 -0
- package/ios/Records/SubtitleSource.swift +32 -0
- package/ios/Records/SubtitleStyle.swift +118 -0
- package/ios/Records/Tracks.swift +229 -0
- package/ios/Records/VideoEventPayloads.swift +81 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +28 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/Utils/AVAssetVariant+VideoTracks.swift +20 -0
- package/ios/Utils/AvAssetTrack+VideoTracks.swift +79 -0
- package/ios/Utils/FourCharCode+toString.swift +25 -0
- package/ios/Utils/HlsUriUtils.swift +27 -0
- package/ios/Utils/URL+MediaLibraryAssets.swift +68 -0
- package/ios/VideoAirPlayButtonView.swift +70 -0
- package/ios/VideoAsset.swift +98 -0
- package/ios/VideoAssetTransport/VideoAssetLoadPlan.swift +69 -0
- package/ios/VideoAssetTransport/VideoAssetSourceDescriptor.swift +29 -0
- package/ios/VideoAssetTransport/VideoAssetTransportProvider.swift +24 -0
- package/ios/VideoAssetTransport/VideoAssetTransportRegistry.swift +103 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +182 -0
- package/ios/VideoModule.swift +413 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +485 -0
- package/ios/VideoPlayerAudioTracks.swift +109 -0
- package/ios/VideoPlayerItem.swift +169 -0
- package/ios/VideoPlayerObserver.swift +612 -0
- package/ios/VideoPlayerSeeker.swift +77 -0
- package/ios/VideoPlayerSubtitleSideload.swift +203 -0
- package/ios/VideoPlayerSubtitles.swift +88 -0
- package/ios/VideoSourceLoader.swift +90 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +201 -0
- package/package.json +51 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +3 -0
- package/plugin/build/withExpoVideo.d.ts +9 -0
- package/plugin/build/withExpoVideo.js +73 -0
- package/plugin/index.d.ts +1 -0
- package/plugin/index.js +1 -0
- package/plugin/jest.config.js +1 -0
- package/plugin/src/index.ts +3 -0
- package/plugin/src/withExpoVideo.ts +103 -0
- package/plugin/tsconfig.json +10 -0
- package/src/NativeVideoAirPlayButtonView.ts +3 -0
- package/src/NativeVideoModule.ts +16 -0
- package/src/NativeVideoModule.web.ts +4 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoAirPlayButton.ios.tsx +8 -0
- package/src/VideoAirPlayButton.tsx +14 -0
- package/src/VideoAirPlayButton.types.ts +39 -0
- package/src/VideoModule.ts +47 -0
- package/src/VideoPlayer.tsx +74 -0
- package/src/VideoPlayer.types.ts +893 -0
- package/src/VideoPlayer.web.tsx +466 -0
- package/src/VideoPlayerEvents.types.ts +332 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +111 -0
- package/src/VideoView.types.ts +405 -0
- package/src/VideoView.web.tsx +293 -0
- package/src/index.ts +26 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +22 -0
- package/tsconfig.all.json +11 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|