expo-video-subtitle 0.1.0 → 0.1.2
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 +37 -1
- package/FORK.md +13 -2
- package/README.md +26 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +18 -8
- package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +39 -6
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +6 -1
- package/android/src/main/java/expo/modules/video/utils/StackingSubtitleParser.kt +211 -0
- package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +7 -2
- package/build/VideoView.types.d.ts +14 -0
- package/build/VideoView.types.d.ts.map +1 -1
- package/build/VideoView.types.js.map +1 -1
- package/package.json +1 -1
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/src/VideoView.types.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,43 @@ Changes inherited from the upstream base version are not repeated here — see t
|
|
|
7
7
|
[upstream changelog](https://github.com/expo/expo/blob/main/packages/expo-video/CHANGELOG.md)
|
|
8
8
|
for the history of everything that isn't subtitle related.
|
|
9
9
|
|
|
10
|
-
## 0.1.
|
|
10
|
+
## 0.1.2
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Android:** subtitles that are on screen at the same time are now stacked as one block instead of
|
|
15
|
+
being drawn on top of each other. `.srt` files legitimately contain overlapping timestamps (two
|
|
16
|
+
speakers, dialogue over on-screen text), but SubRip carries no positioning, so Media3 pinned every
|
|
17
|
+
cue to the same spot and rendered them one over the other — Media3 does the stacking in its WebVTT
|
|
18
|
+
*parser* (`line = -1 - i`) and has no equivalent pass for SubRip. A `SubtitleParser` for SubRip now
|
|
19
|
+
splits the cue timeline at every boundary and merges the cues active in each slice into a single
|
|
20
|
+
multi-line cue, ordered like WebVTT: the cue that started earliest sits at the bottom. This also
|
|
21
|
+
makes Android match the iOS output, which already stacked because sideloaded SRT is converted to
|
|
22
|
+
WebVTT there. Cues with explicit `{\anN}` positioning are left untouched.
|
|
23
|
+
- **Android:** selecting a `subtitleTrack` whose id is not among the available tracks no longer
|
|
24
|
+
leaves the previously selected track on screen. The cleared track-selection parameters were only
|
|
25
|
+
applied when a matching track was found, so an unmatched assignment silently did nothing; the
|
|
26
|
+
selection is now cleared and a warning is logged.
|
|
27
|
+
- **Android:** `subtitleStyle.bottomOffset` is reset to the default when it is omitted, instead of
|
|
28
|
+
keeping the value from a previously applied style.
|
|
29
|
+
|
|
30
|
+
## 0.1.1
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Android:** `subtitleStyle.fontFamily` now resolves custom fonts bundled with the app, the same way
|
|
35
|
+
React Native resolves a `<Text>` component's `fontFamily`. It is looked up as an Android font
|
|
36
|
+
resource (`res/font`, where `expo-font`'s config plugin puts fonts) and then through React Native's
|
|
37
|
+
font manager, which covers the `assets/fonts/<name>.ttf` convention and fonts registered at runtime.
|
|
38
|
+
Previously only the built-in system families worked: `Typeface.create` silently fell back to the
|
|
39
|
+
default typeface for any bundled font, so `fontFamily` appeared to do nothing on Android while
|
|
40
|
+
working on iOS.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- Document font support and the silent-fallback behaviour for unresolved family names.
|
|
45
|
+
|
|
46
|
+
## 0.1.0
|
|
11
47
|
|
|
12
48
|
Initial release. Based on `expo-video` **56.1.4** (Expo SDK 56).
|
|
13
49
|
|
package/FORK.md
CHANGED
|
@@ -23,7 +23,8 @@ Measured against the published `expo-video@56.1.4` tarball. `+`/`-` are added/re
|
|
|
23
23
|
| `ios/Records/SubtitleStyle.swift` | 118 | Record for caption styling; hex→ARGB parsing; builds `[AVTextStyleRule]`. |
|
|
24
24
|
| `ios/VideoPlayerSubtitleSideload.swift` | 203 | Builds the `AVMutableComposition`, downloads remote subs, SRT→WebVTT converter. |
|
|
25
25
|
| `android/…/records/SubtitleSource.kt` | 54 | Record → `MediaItem.SubtitleConfiguration` + MIME resolution. |
|
|
26
|
-
| `android/…/records/SubtitleStyle.kt` |
|
|
26
|
+
| `android/…/records/SubtitleStyle.kt` | 129 | Record → `CaptionStyleCompat`; hex colour parsing; font-family resolution (`res/font` → `ReactFontManager`). |
|
|
27
|
+
| `android/…/utils/StackingSubtitleParser.kt` | 214 | `SubtitleParser` for SubRip that segments the cue timeline and merges simultaneous cues into one stacked block, plus the `SubtitleParser.Factory` that routes SubRip to it. |
|
|
27
28
|
| `FORK.md`, `LICENSE` | — | This guide; MIT licence (upstream copyright preserved). |
|
|
28
29
|
|
|
29
30
|
### Modified — upstream files carrying fork edits (re-apply these after a sync)
|
|
@@ -40,9 +41,11 @@ Measured against the published `expo-video@56.1.4` tarball. `+`/`-` are added/re
|
|
|
40
41
|
| `ios/VideoView.swift` | +8 | `subtitleStyle` property; forward it when `player` is (re)assigned. |
|
|
41
42
|
| `ios/VideoModule.swift` | +4 | `Prop("subtitleStyle")`. |
|
|
42
43
|
| `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/…/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. Passes `context` to `toCaptionStyle` for font lookup. |
|
|
44
45
|
| `android/…/VideoView.kt` | +12 / −5 | `subtitleStyle` property; pass it at all 4 `configureSubtitleView` call sites + the captioning listener. |
|
|
45
46
|
| `android/…/VideoModule.kt` | +4 | `Prop("subtitleStyle")`. |
|
|
47
|
+
| `android/…/utils/DataSourceUtils.kt` | +5 | `setSubtitleParserFactory(ExpoVideoSubtitleParserFactory())` on the `DefaultMediaSourceFactory`. Note this file declares `package expo.modules.video` despite living in `utils/`, so the factory needs an explicit import. |
|
|
48
|
+
| `android/…/player/VideoPlayerSubtitles.kt` | +14 / −8 | Apply the cleared track-selection parameters when the requested subtitle id matches nothing, instead of only inside the `format?.let` block. |
|
|
46
49
|
| `android/…/FullscreenPlayerActivity.kt` | +3 / −3 | Pass `videoView.subtitleStyle` at its 2 call sites + the captioning listener. |
|
|
47
50
|
| `package.json` | — | Fork identity + build scripts — see §3. |
|
|
48
51
|
| `expo-module.config.json` | — | **Android `publication` block removed** — see §3. |
|
|
@@ -60,6 +63,14 @@ Measured against the published `expo-video@56.1.4` tarball. `+`/`-` are added/re
|
|
|
60
63
|
> block, `local-maven-repo/` or `prebuilds/`, the app keeps building — it just runs **upstream's**
|
|
61
64
|
> video module, and every subtitle feature silently disappears. Verify with §4.
|
|
62
65
|
|
|
66
|
+
### External API coupling to watch
|
|
67
|
+
|
|
68
|
+
`SubtitleStyle.kt` imports `com.facebook.react.common.assets.ReactFontManager` to resolve
|
|
69
|
+
`fontFamily` the same way `<Text>` does. React Native moved this class from
|
|
70
|
+
`com.facebook.react.views.text` to `com.facebook.react.common.assets` (the old path is deprecated but
|
|
71
|
+
still present in RN 0.85). If a React Native upgrade breaks that import, update the package path — the
|
|
72
|
+
call itself, `getTypeface(family, style, assetManager)`, has been stable.
|
|
73
|
+
|
|
63
74
|
---
|
|
64
75
|
|
|
65
76
|
## 2. Syncing a newer `expo-video`
|
package/README.md
CHANGED
|
@@ -103,6 +103,32 @@ Pass a `subtitleStyle` prop to `VideoView`. It applies to whichever subtitle tra
|
|
|
103
103
|
|
|
104
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
105
|
|
|
106
|
+
### Fonts
|
|
107
|
+
|
|
108
|
+
`fontFamily` accepts the same name you would give a `<Text>` component, so a font bundled with your
|
|
109
|
+
app can be used for subtitles:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<VideoView player={player} subtitleStyle={{ fontFamily: 'Inter-Regular', bold: true }} />
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The simplest way to make a custom font available on both platforms is `expo-font`'s config plugin,
|
|
116
|
+
which bundles the file into `res/font` on Android and registers it via `UIAppFonts` on iOS:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
["expo-font", { "fonts": ["./assets/fonts/Inter-Regular.ttf"] }]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Font source | Android | iOS |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| Built-in system families (`sans-serif`, `serif`, `monospace`, `cursive`, …) | ✅ | ✅ (use iOS names, e.g. `Helvetica Neue`) |
|
|
125
|
+
| Bundled via `expo-font` config plugin | ✅ resolved from `res/font` | ✅ resolved from `UIAppFonts` |
|
|
126
|
+
| React Native convention (`assets/fonts/<name>.ttf`) | ✅ via RN's font manager | — |
|
|
127
|
+
|
|
128
|
+
> A font name that matches nothing falls back to the default typeface **silently** — neither platform
|
|
129
|
+
> reports an unresolved family. If the font doesn't change, check the spelling first: the name is the
|
|
130
|
+
> font's family name, not the filename.
|
|
131
|
+
|
|
106
132
|
## License
|
|
107
133
|
|
|
108
134
|
MIT. Based on [`expo-video`](https://github.com/expo/expo/tree/main/packages/expo-video) by 650 Industries, Inc.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package expo.modules.video.player
|
|
2
2
|
|
|
3
|
+
import android.util.Log
|
|
3
4
|
import androidx.annotation.OptIn
|
|
4
5
|
import androidx.media3.common.C
|
|
5
6
|
import androidx.media3.common.Format
|
|
@@ -86,15 +87,24 @@ class VideoPlayerSubtitles(owner: VideoPlayer) : VideoPlayerListener {
|
|
|
86
87
|
val format = formatsToGroups.keys.firstOrNull {
|
|
87
88
|
it.id == subtitleTrack.id
|
|
88
89
|
}
|
|
89
|
-
format?.let {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
val subtitlePair = format?.let { formatsToGroups[it] }
|
|
91
|
+
|
|
92
|
+
if (subtitlePair == null) {
|
|
93
|
+
// The requested track isn't among the available ones. The cleared parameters still have to be
|
|
94
|
+
// applied, otherwise the previously selected track keeps rendering and the assignment looks
|
|
95
|
+
// like it was silently ignored.
|
|
96
|
+
Log.w("ExpoVideo", "Subtitle track with id '${subtitleTrack.id}' is not available; clearing the current selection")
|
|
97
|
+
player.trackSelectionParameters = newParameters
|
|
98
|
+
setSubtitlesEnabled(false)
|
|
99
|
+
currentOverride = null
|
|
100
|
+
return
|
|
97
101
|
}
|
|
102
|
+
|
|
103
|
+
val override = TrackSelectionOverride(subtitlePair.first, subtitlePair.second)
|
|
104
|
+
newParameters = newParameters.buildUpon().addOverride(override).build()
|
|
105
|
+
player.trackSelectionParameters = newParameters
|
|
106
|
+
setSubtitlesEnabled(true)
|
|
107
|
+
currentOverride = override
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
private fun findSelectedSubtitleFormat(): Format? {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package expo.modules.video.records
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
3
4
|
import android.graphics.Color
|
|
4
5
|
import android.graphics.Typeface
|
|
5
6
|
import androidx.annotation.OptIn
|
|
7
|
+
import androidx.core.content.res.ResourcesCompat
|
|
6
8
|
import androidx.media3.common.util.UnstableApi
|
|
7
9
|
import androidx.media3.ui.CaptionStyleCompat
|
|
10
|
+
import com.facebook.react.common.assets.ReactFontManager
|
|
8
11
|
import expo.modules.kotlin.records.Field
|
|
9
12
|
import expo.modules.kotlin.records.Record
|
|
10
13
|
import java.io.Serializable
|
|
@@ -29,22 +32,52 @@ class SubtitleStyle(
|
|
|
29
32
|
* Builds a [CaptionStyleCompat], falling back to [base] for any unspecified property so that
|
|
30
33
|
* partial styles compose on top of the system/default caption style.
|
|
31
34
|
*/
|
|
32
|
-
fun toCaptionStyle(base: CaptionStyleCompat): CaptionStyleCompat {
|
|
35
|
+
fun toCaptionStyle(base: CaptionStyleCompat, context: Context): CaptionStyleCompat {
|
|
33
36
|
return CaptionStyleCompat(
|
|
34
37
|
parseColor(textColor) ?: base.foregroundColor,
|
|
35
38
|
parseColor(backgroundColor) ?: base.backgroundColor,
|
|
36
39
|
parseColor(windowColor) ?: base.windowColor,
|
|
37
40
|
parseEdgeType() ?: base.edgeType,
|
|
38
41
|
parseColor(edgeColor) ?: base.edgeColor,
|
|
39
|
-
resolveTypeface() ?: base.typeface
|
|
42
|
+
resolveTypeface(context) ?: base.typeface
|
|
40
43
|
)
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Resolves [fontFamily] the same way React Native resolves `<Text style={{ fontFamily }} />`, so a
|
|
48
|
+
* font bundled with the app can be used for subtitles under the same name.
|
|
49
|
+
*
|
|
50
|
+
* Lookup order:
|
|
51
|
+
* 1. An Android font resource (`res/font/<name>`), which is where `expo-font`'s config plugin puts fonts.
|
|
52
|
+
* 2. [ReactFontManager], which covers the React Native convention (`assets/fonts/<name>.ttf`) and any
|
|
53
|
+
* font registered at runtime, and itself falls back to the built-in system families.
|
|
54
|
+
*
|
|
55
|
+
* A name that matches nothing still falls back to the default typeface — Android provides no way to
|
|
56
|
+
* detect that, so an unknown family fails silently rather than throwing.
|
|
57
|
+
*/
|
|
58
|
+
private fun resolveTypeface(context: Context): Typeface? {
|
|
44
59
|
val style = if (bold) Typeface.BOLD else Typeface.NORMAL
|
|
45
|
-
fontFamily?.
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
val family = fontFamily?.takeIf { it.isNotBlank() }
|
|
61
|
+
?: return if (bold) Typeface.DEFAULT_BOLD else null
|
|
62
|
+
|
|
63
|
+
resolveFontResource(context, family)?.let { return Typeface.create(it, style) }
|
|
64
|
+
|
|
65
|
+
return try {
|
|
66
|
+
ReactFontManager.getInstance().getTypeface(family, style, context.assets)
|
|
67
|
+
} catch (e: Exception) {
|
|
68
|
+
// Never let font resolution break subtitle rendering.
|
|
69
|
+
Typeface.create(family, style)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun resolveFontResource(context: Context, family: String): Typeface? {
|
|
74
|
+
return try {
|
|
75
|
+
@Suppress("DiscouragedApi") // Font names arrive at runtime, so they can't be compile-time resource ids.
|
|
76
|
+
val resId = context.resources.getIdentifier(family.lowercase(), "font", context.packageName)
|
|
77
|
+
if (resId == 0) null else ResourcesCompat.getFont(context, resId)
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
null
|
|
80
|
+
}
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
private fun parseEdgeType(): Int? = when (edgeType?.lowercase()) {
|
|
@@ -13,6 +13,7 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
|
13
13
|
import androidx.media3.exoplayer.source.MediaSource
|
|
14
14
|
import expo.modules.video.records.VideoSource
|
|
15
15
|
import expo.modules.video.managers.VideoManager
|
|
16
|
+
import expo.modules.video.utils.ExpoVideoSubtitleParserFactory
|
|
16
17
|
import okhttp3.OkHttpClient
|
|
17
18
|
|
|
18
19
|
@OptIn(UnstableApi::class)
|
|
@@ -54,7 +55,11 @@ fun buildCacheDataSourceFactory(context: Context, videoSource: VideoSource): Dat
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
fun buildMediaSourceFactory(context: Context, dataSourceFactory: DataSource.Factory): MediaSource.Factory {
|
|
57
|
-
return DefaultMediaSourceFactory(context)
|
|
58
|
+
return DefaultMediaSourceFactory(context)
|
|
59
|
+
.setDataSourceFactory(dataSourceFactory)
|
|
60
|
+
// Lays out simultaneous SubRip cues as one stacked block instead of drawing them on top of
|
|
61
|
+
// each other. See `StackingSubripParser`.
|
|
62
|
+
.setSubtitleParserFactory(ExpoVideoSubtitleParserFactory())
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
@OptIn(UnstableApi::class)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package expo.modules.video.utils
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.OptIn
|
|
4
|
+
import androidx.media3.common.C
|
|
5
|
+
import androidx.media3.common.Format
|
|
6
|
+
import androidx.media3.common.MimeTypes
|
|
7
|
+
import androidx.media3.common.text.Cue
|
|
8
|
+
import androidx.media3.common.util.Consumer
|
|
9
|
+
import androidx.media3.common.util.UnstableApi
|
|
10
|
+
import androidx.media3.extractor.text.CuesWithTiming
|
|
11
|
+
import androidx.media3.extractor.text.DefaultSubtitleParserFactory
|
|
12
|
+
import androidx.media3.extractor.text.SubtitleParser
|
|
13
|
+
import androidx.media3.extractor.text.subrip.SubripParser
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A [SubtitleParser] for SubRip (`.srt`) that lays out simultaneous cues as a single stacked block
|
|
17
|
+
* instead of drawing them on top of each other.
|
|
18
|
+
*
|
|
19
|
+
* ### Why this is needed
|
|
20
|
+
*
|
|
21
|
+
* SubRip carries no positioning information, so every cue Media3's [SubripParser] produces has
|
|
22
|
+
* `line == Cue.DIMEN_UNSET`. `SubtitlePainter` pins every such cue to the same spot
|
|
23
|
+
* (`parentBottom - textHeight - parentHeight * bottomPaddingFraction`) and `CanvasSubtitleOutput`
|
|
24
|
+
* gives each painter the identical layout box, so two cues that are on screen at the same time are
|
|
25
|
+
* rendered in exactly the same place. Media3 performs no collision avoidance: for WebVTT the
|
|
26
|
+
* stacking is done by the *parser* (which assigns `line = -1 - i` per the WebVTT spec), and the
|
|
27
|
+
* SubRip path simply has no equivalent pass. `.srt` files with overlapping timestamps are perfectly
|
|
28
|
+
* legal and common (two speakers, dialogue plus on-screen signs), so the fix belongs here.
|
|
29
|
+
*
|
|
30
|
+
* ### Approach
|
|
31
|
+
*
|
|
32
|
+
* The cue timeline is split at every start/end boundary into non-overlapping segments, and the cues
|
|
33
|
+
* active in a segment are merged into one multi-line cue. Merging rather than assigning `line`
|
|
34
|
+
* values is deliberate: `SubtitlePainter` advances `LINE_TYPE_NUMBER` positions by the height of a
|
|
35
|
+
* cue's *first* line, so a two-line cue at line -1 would still collide with a cue at line -2, and
|
|
36
|
+
* a parser cannot predict where text will wrap. One merged cue is laid out as a single block, which
|
|
37
|
+
* cannot self-overlap, keeps line spacing uniform and lets the text layout handle wrapping.
|
|
38
|
+
*
|
|
39
|
+
* Ordering matches WebVTT (and therefore the iOS behaviour of this package): the cue that started
|
|
40
|
+
* earliest sits at the bottom and later cues stack above it.
|
|
41
|
+
*
|
|
42
|
+
* Cues that *do* carry explicit positioning (SubRip's `{\anN}` tags) are passed through untouched —
|
|
43
|
+
* they were placed deliberately and must not be merged.
|
|
44
|
+
*/
|
|
45
|
+
@OptIn(UnstableApi::class)
|
|
46
|
+
class StackingSubripParser : SubtitleParser {
|
|
47
|
+
private val delegate = SubripParser()
|
|
48
|
+
|
|
49
|
+
// Every emitted segment is a complete description of what should be on screen for its interval,
|
|
50
|
+
// so segments replace each other rather than being merged by the renderer.
|
|
51
|
+
override fun getCueReplacementBehavior(): Int = Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE
|
|
52
|
+
|
|
53
|
+
override fun reset() {
|
|
54
|
+
delegate.reset()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun parse(
|
|
58
|
+
data: ByteArray,
|
|
59
|
+
offset: Int,
|
|
60
|
+
length: Int,
|
|
61
|
+
outputOptions: SubtitleParser.OutputOptions,
|
|
62
|
+
output: Consumer<CuesWithTiming>
|
|
63
|
+
) {
|
|
64
|
+
val entries = mutableListOf<Entry>()
|
|
65
|
+
// Always ask the delegate for everything; `outputOptions` is applied to the merged result below,
|
|
66
|
+
// because a segment can span cues that fall on both sides of the requested start time.
|
|
67
|
+
delegate.parse(data, offset, length, SubtitleParser.OutputOptions.allCues()) { cuesWithTiming ->
|
|
68
|
+
val startUs = cuesWithTiming.startTimeUs
|
|
69
|
+
val endUs = resolveEndUs(cuesWithTiming)
|
|
70
|
+
for (cue in cuesWithTiming.cues) {
|
|
71
|
+
entries.add(Entry(cue, startUs, endUs, entries.size))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (entries.isEmpty()) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
val segments = buildSegments(entries)
|
|
80
|
+
emit(segments, outputOptions, output)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun resolveEndUs(cuesWithTiming: CuesWithTiming): Long {
|
|
84
|
+
val durationUs = cuesWithTiming.durationUs
|
|
85
|
+
return if (durationUs == C.TIME_UNSET || durationUs <= 0) {
|
|
86
|
+
// Without a duration there is nothing sensible to overlap against; treat it as instantaneous
|
|
87
|
+
// so it never swallows later cues.
|
|
88
|
+
cuesWithTiming.startTimeUs
|
|
89
|
+
} else {
|
|
90
|
+
cuesWithTiming.startTimeUs + durationUs
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Splits the timeline at every cue boundary and merges the cues active within each slice. */
|
|
95
|
+
private fun buildSegments(entries: List<Entry>): List<CuesWithTiming> {
|
|
96
|
+
val boundaries = sortedSetOf<Long>()
|
|
97
|
+
for (entry in entries) {
|
|
98
|
+
boundaries.add(entry.startUs)
|
|
99
|
+
boundaries.add(entry.endUs)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
val ordered = boundaries.toList()
|
|
103
|
+
val segments = mutableListOf<CuesWithTiming>()
|
|
104
|
+
|
|
105
|
+
for (i in 0 until ordered.size - 1) {
|
|
106
|
+
val segmentStart = ordered[i]
|
|
107
|
+
val segmentEnd = ordered[i + 1]
|
|
108
|
+
if (segmentEnd <= segmentStart) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Earliest first, falling back to file order for cues that start together.
|
|
113
|
+
val active = entries
|
|
114
|
+
.filter { it.startUs <= segmentStart && it.endUs > segmentStart }
|
|
115
|
+
.sortedWith(compareBy({ it.startUs }, { it.order }))
|
|
116
|
+
|
|
117
|
+
if (active.isEmpty()) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
val cues = mergeActiveCues(active)
|
|
122
|
+
if (cues.isNotEmpty()) {
|
|
123
|
+
segments.add(CuesWithTiming(cues, segmentStart, segmentEnd - segmentStart))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return segments
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun mergeActiveCues(active: List<Entry>): List<Cue> {
|
|
131
|
+
val positioned = active.filter { it.cue.hasExplicitPosition() }
|
|
132
|
+
val unpositioned = active.filter { !it.cue.hasExplicitPosition() }
|
|
133
|
+
|
|
134
|
+
if (unpositioned.isEmpty()) {
|
|
135
|
+
return positioned.map { it.cue }
|
|
136
|
+
}
|
|
137
|
+
if (unpositioned.size == 1) {
|
|
138
|
+
return positioned.map { it.cue } + unpositioned.first().cue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Reversed so the most recent cue is the first rendered line (the top of the block) and the
|
|
142
|
+
// earliest stays at the bottom, matching how WebVTT stacks with line = -1 - i.
|
|
143
|
+
val mergedText = unpositioned
|
|
144
|
+
.asReversed()
|
|
145
|
+
.mapNotNull { it.cue.text?.toString()?.takeIf(String::isNotBlank) }
|
|
146
|
+
.joinToString("\n")
|
|
147
|
+
|
|
148
|
+
if (mergedText.isEmpty()) {
|
|
149
|
+
return positioned.map { it.cue }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build from the bottom-most cue so any styling it carries survives the merge.
|
|
153
|
+
val template = unpositioned.first().cue
|
|
154
|
+
val merged = template.buildUpon().setText(mergedText).build()
|
|
155
|
+
return positioned.map { it.cue } + merged
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun emit(
|
|
159
|
+
segments: List<CuesWithTiming>,
|
|
160
|
+
outputOptions: SubtitleParser.OutputOptions,
|
|
161
|
+
output: Consumer<CuesWithTiming>
|
|
162
|
+
) {
|
|
163
|
+
val startTimeUs = outputOptions.startTimeUs
|
|
164
|
+
if (startTimeUs == C.TIME_UNSET) {
|
|
165
|
+
segments.forEach(output::accept)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Cues still visible at, or starting after, the requested time come first.
|
|
170
|
+
segments.filter { it.endTimeUs > startTimeUs }.forEach(output::accept)
|
|
171
|
+
if (outputOptions.outputAllCues) {
|
|
172
|
+
segments.filter { it.endTimeUs <= startTimeUs }.forEach(output::accept)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun Cue.hasExplicitPosition(): Boolean =
|
|
177
|
+
line != Cue.DIMEN_UNSET || position != Cue.DIMEN_UNSET
|
|
178
|
+
|
|
179
|
+
private data class Entry(
|
|
180
|
+
val cue: Cue,
|
|
181
|
+
val startUs: Long,
|
|
182
|
+
val endUs: Long,
|
|
183
|
+
/** Preserves file order so cues sharing a start time keep a stable, deterministic layout. */
|
|
184
|
+
val order: Int
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Routes SubRip through [StackingSubripParser] so simultaneous cues stack, and delegates every other
|
|
190
|
+
* subtitle format to Media3's default handling.
|
|
191
|
+
*/
|
|
192
|
+
@OptIn(UnstableApi::class)
|
|
193
|
+
class ExpoVideoSubtitleParserFactory : SubtitleParser.Factory {
|
|
194
|
+
private val defaultFactory = DefaultSubtitleParserFactory()
|
|
195
|
+
|
|
196
|
+
override fun supportsFormat(format: Format): Boolean =
|
|
197
|
+
format.isSubrip() || defaultFactory.supportsFormat(format)
|
|
198
|
+
|
|
199
|
+
override fun getCueReplacementBehavior(format: Format): Int =
|
|
200
|
+
if (format.isSubrip()) {
|
|
201
|
+
Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE
|
|
202
|
+
} else {
|
|
203
|
+
defaultFactory.getCueReplacementBehavior(format)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
override fun create(format: Format): SubtitleParser =
|
|
207
|
+
if (format.isSubrip()) StackingSubripParser() else defaultFactory.create(format)
|
|
208
|
+
|
|
209
|
+
private fun Format.isSubrip(): Boolean =
|
|
210
|
+
MimeTypes.APPLICATION_SUBRIP.equals(sampleMimeType, ignoreCase = true)
|
|
211
|
+
}
|
|
@@ -5,6 +5,7 @@ import android.util.TypedValue
|
|
|
5
5
|
import android.view.accessibility.CaptioningManager
|
|
6
6
|
import androidx.media3.ui.CaptionStyleCompat
|
|
7
7
|
import androidx.media3.ui.PlayerView
|
|
8
|
+
import androidx.media3.ui.SubtitleView
|
|
8
9
|
import expo.modules.video.records.SubtitleStyle
|
|
9
10
|
|
|
10
11
|
object SubtitleUtils {
|
|
@@ -29,10 +30,14 @@ object SubtitleUtils {
|
|
|
29
30
|
|
|
30
31
|
if (customStyle != null) {
|
|
31
32
|
val base = userStyle?.let { CaptionStyleCompat.createFromCaptionStyle(it) } ?: CaptionStyleCompat.DEFAULT
|
|
32
|
-
setStyle(customStyle.toCaptionStyle(base))
|
|
33
|
+
setStyle(customStyle.toCaptionStyle(base, context))
|
|
33
34
|
// On Android `fontSize` is an absolute sp value; when omitted, fall back to the accessibility-scaled base.
|
|
34
35
|
setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, customStyle.fontSize ?: (baseFontSize * fontScale))
|
|
35
|
-
|
|
36
|
+
// Always assign, so clearing `bottomOffset` restores the default instead of leaving the
|
|
37
|
+
// previously applied fraction in place.
|
|
38
|
+
setBottomPaddingFraction(
|
|
39
|
+
customStyle.bottomOffset?.coerceIn(0f, 1f) ?: SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION
|
|
40
|
+
)
|
|
36
41
|
} else if (userStyle != null) {
|
|
37
42
|
// Apply system accessibility caption style but with reasonable base font size
|
|
38
43
|
setStyle(CaptionStyleCompat.createFromCaptionStyle(userStyle))
|
|
@@ -225,6 +225,20 @@ export type SubtitleStyle = {
|
|
|
225
225
|
fontSize?: number;
|
|
226
226
|
/**
|
|
227
227
|
* The name of the font family used to render the subtitles.
|
|
228
|
+
*
|
|
229
|
+
* Custom fonts bundled with the app are supported, using the same name you would pass to a
|
|
230
|
+
* `<Text>` component's `fontFamily`:
|
|
231
|
+
* - On Android the font is looked up as an Android font resource (`res/font`, where `expo-font`'s
|
|
232
|
+
* config plugin places fonts) and then through React Native's font manager, which covers the
|
|
233
|
+
* `assets/fonts/<name>.ttf` convention and fonts registered at runtime.
|
|
234
|
+
* - On iOS the font must be registered with the system, which is what bundling it through
|
|
235
|
+
* `expo-font`'s config plugin (`UIAppFonts`) does.
|
|
236
|
+
*
|
|
237
|
+
* The built-in system families always work — for example `sans-serif`, `sans-serif-medium`,
|
|
238
|
+
* `serif`, `monospace` and `cursive` on Android.
|
|
239
|
+
*
|
|
240
|
+
* > A name that matches no font falls back to the default typeface silently: neither platform
|
|
241
|
+
* > reports an unresolved font family, so double-check the spelling if nothing changes.
|
|
228
242
|
*/
|
|
229
243
|
fontFamily?: string;
|
|
230
244
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,aAAa,CAAC;AAExD,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAE5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;OAEG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAEtC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;OAIG;IACH,eAAe,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE/C;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;OAUG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;IAE9C;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAEnC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE9B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEhC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,iBAAiB,CAAC;IAE9C;;;;;;;;;OASG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE1F;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB
|
|
1
|
+
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,aAAa,CAAC;AAExD,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAE5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;OAEG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAEtC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;OAIG;IACH,eAAe,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE/C;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;OAUG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;IAE9C;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAEnC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE9B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEhC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,iBAAiB,CAAC;IAE9C;;;;;;;;;OASG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE1F;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;OAGG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAE5B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,GAC7B,SAAS,GACT,UAAU,GACV,YAAY,GACZ,cAAc,GACd,WAAW,GACX,eAAe,GACf,gBAAgB,CAAC;AAErB;;;;;;GAMG;AACH,MAAM,MAAM,+BAA+B,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAE/E;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,+BAA+B,CAAC;CAC3D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ViewProps } from 'react-native';\n\nimport type { VideoPlayer } from './VideoPlayer.types';\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\nexport type VideoContentFit = 'contain' | 'cover' | 'fill';\n\n/**\n * Describes the type of the surface used to render the video.\n * - `surfaceView`: Uses the `SurfaceView` to render the video. This value should be used in the majority of cases. Provides significantly lower power consumption, better performance, and more features.\n * - `textureView`: Uses the `TextureView` to render the video. Should be used in cases where the SurfaceView is not supported or causes issues (for example, overlapping video views).\n *\n * You can learn more about surface types in the official [ExoPlayer documentation](https://developer.android.com/media/media3/ui/playerview#surfacetype).\n * @platform android\n */\nexport type SurfaceType = 'textureView' | 'surfaceView';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.\n */\n player?: VideoPlayer | null;\n\n /**\n * Determines whether native controls should be displayed or not.\n *\n * > **Note**: Due to platform limitations, the native controls are always enabled in fullscreen mode.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are `'contain'`, `'cover'`, and `'fill'`.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines the fullscreen mode options.\n */\n fullscreenOptions?: FullscreenOptions;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Configuration for controlling the visibility of player control buttons.\n * @platform android\n */\n buttonOptions?: ButtonOptions;\n\n /**\n * Customizes the appearance of the rendered subtitles/captions (text color, background, font, edge, and position).\n *\n * The style is applied to whatever subtitle track is currently displayed, regardless of whether it comes\n * from an embedded (`HLS`/`DASH`) track or an external one attached through\n * [`VideoSource.subtitleTracks`](#videosourcesubtitletracks).\n *\n * > **Note (iOS):** styling relies on `AVPlayerItem.textStyleRules`, which only affects `WebVTT` captions that\n * > the system renders itself. The OS-level *Settings → Accessibility → Subtitles & Captioning* preferences\n * > take precedence over these values when the user has enabled them.\n *\n * @platform android\n * @platform ios\n */\n subtitleStyle?: SubtitleStyle;\n\n /**\n * Determines the type of the surface used to render the video.\n * > This prop should not be changed at runtime.\n * @default 'surfaceView'\n * @platform android\n */\n surfaceType?: SurfaceType;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n * @platform android\n * @platform ios\n * @platform web\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether a video should be played \"inline\", that is, within the element's playback area.\n * @platform web\n */\n playsInline?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform android 12+\n * @platform ios\n */\n startsPictureInPictureAutomatically?: boolean;\n\n /**\n * Specifies whether to perform video frame analysis (Live Text in videos).\n * Check official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details.\n * @default true\n * @platform ios 16.0+\n */\n allowsVideoFrameAnalysis?: boolean;\n\n /**\n * A callback to call after the video player enters fullscreen mode.\n */\n onFullscreenEnter?: () => void;\n\n /**\n * A callback to call after the video player exits fullscreen mode.\n */\n onFullscreenExit?: () => void;\n\n /**\n * A callback to call after the mounted `VideoPlayer` has rendered the first frame into the `VideoView`.\n * This event can be used to hide any cover images that conceal the initial loading of the player.\n * > **Note:** This event may also be called during playback when the current video track changes (for example when the player switches video quality).\n */\n onFirstFrameRender?: () => void;\n\n /**\n * Determines whether the player should use the default ExoPlayer shutter that covers the `VideoView` before the first video frame is rendered.\n * Setting this property to `false` makes the Android behavior the same as iOS.\n *\n * @platform android\n * @default false\n */\n useExoShutter?: boolean;\n\n /**\n * Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\n * If `undefined` (default), does not use CORS at all. If set to `'anonymous'`, the video will be loaded with CORS enabled.\n * Note that some videos may not play if CORS is enabled, depending on the CDN settings.\n * If you encounter issues, consider adjusting the `crossOrigin` property.\n *\n *\n * @platform web\n * @default undefined\n */\n crossOrigin?: 'anonymous' | 'use-credentials';\n\n /**\n * Use Audio Nodes for sound playback. When the same player is playing in multiple video views the audio won't increase in volume\n * as the number of players increases.\n *\n * > **Note**: This property is experimental, when enabled it is known to break audio for some sources. Do not change this property at runtime.\n *\n * @experimental\n * @default false\n * @platform web\n */\n useAudioNodePlayback?: boolean;\n}\n\n/**\n * Describes the edge (outline) effect drawn around subtitle text.\n * - `none`: No edge effect.\n * - `outline`: A solid outline around each glyph.\n * - `dropShadow`: A drop shadow behind the text.\n * - `raised`: The text appears raised from the background.\n * - `depressed`: The text appears pressed into the background.\n */\nexport type SubtitleEdgeType = 'none' | 'outline' | 'dropShadow' | 'raised' | 'depressed';\n\n/**\n * Describes the visual style applied to the rendered subtitles/captions.\n *\n * Colors accept a hex string in the form `#RGB`, `#RRGGBB`, or `#RRGGBBAA` (an alpha suffix controls opacity).\n *\n * @platform android\n * @platform ios\n */\nexport type SubtitleStyle = {\n /**\n * The color of the subtitle text.\n * @default '#FFFFFF'\n */\n textColor?: string;\n\n /**\n * The background color drawn directly behind the text glyphs (the caption \"box\").\n * Use an alpha suffix (for example `#000000A0`) for a translucent box.\n * @default 'transparent'\n */\n backgroundColor?: string;\n\n /**\n * The background color of the whole cue region (the \"window\" behind the whole line).\n * @default 'transparent'\n */\n windowColor?: string;\n\n /**\n * The subtitle font size.\n *\n * > The unit differs per platform: on Android this is an absolute size in `sp`; on iOS it is a percentage\n * > relative to the default caption size (`100` = default, `150` = 1.5×).\n *\n * @platform android\n * @platform ios\n */\n fontSize?: number;\n\n /**\n * The name of the font family used to render the subtitles.\n */\n fontFamily?: string;\n\n /**\n * Whether the subtitle text should be rendered in bold.\n * @default false\n */\n bold?: boolean;\n\n /**\n * The edge (outline) effect drawn around the text.\n * @default 'none'\n */\n edgeType?: SubtitleEdgeType;\n\n /**\n * The color of the edge effect. Only has an effect when `edgeType` is not `'none'`.\n */\n edgeColor?: string;\n\n /**\n * The distance of the subtitles from the bottom of the video, expressed as a fraction of the view height (`0`–`1`).\n * @platform android\n */\n bottomOffset?: number;\n};\n\n/**\n * Configuration for controlling the visibility of player control buttons.\n *\n * > The fullscreen button should be controlled with [`fullscreenOptions.enable`](#fullscreenoptions).\n *\n * @platform android\n */\nexport type ButtonOptions = {\n /**\n * Whether to show the next button.\n * @default false\n */\n showNext?: boolean;\n /**\n * Whether to show the previous button.\n * @default false\n */\n showPrevious?: boolean;\n /**\n * Whether to show the seek forward button.\n * @default true\n */\n showSeekForward?: boolean;\n /**\n * Whether to show the seek backward button.\n * @default true\n */\n showSeekBackward?: boolean;\n /**\n * Whether to show the subtitles button.\n * - `true`: Button is always visible\n * - `false`: Button is never visible\n * - `undefined`: Button is visible only when subtitles are available (default behavior)\n * @default undefined\n */\n showSubtitles?: boolean | null;\n /**\n * Whether to show the settings button.\n * @default true\n */\n showSettings?: boolean;\n /**\n * Whether to show the play/pause button.\n * @default true\n */\n showPlayPause?: boolean;\n /**\n * Whether to show the bottom control bar (containing time, progress bar, and buttons).\n * When set to `false`, the entire bottom bar including the progress bar will be hidden.\n *\n * > **Note**: The bottom bar is always visible in fullscreen mode to allow users to exit fullscreen.\n *\n * @default true\n */\n showBottomBar?: boolean;\n};\n\n/**\n * Describes the orientation of the video in fullscreen mode. Available values are:\n * - `default`: The video is displayed in any of the available device rotations.\n * - `portrait`: The video is displayed in one of two available portrait orientations and rotates between them.\n * - `portraitUp`: The video is displayed in the portrait orientation - the notch of the phone points upwards.\n * - `portraitDown`: The video is displayed in the portrait orientation - the notch of the phone points downwards.\n * - `landscape`: The video is displayed in one of two available landscape orientations and rotates between them.\n * - `landscapeLeft`: The video is displayed in the left landscape orientation - the notch of the phone is in the left palm of the user.\n * - `landscapeRight`: The video is displayed in the right landscape orientation - the notch of the phone is in the right palm of the user.\n */\nexport type FullscreenOrientation =\n | 'default'\n | 'portrait'\n | 'portraitUp'\n | 'portraitDown'\n | 'landscape'\n | 'landscapeLeft'\n | 'landscapeRight';\n\n/**\n * Determines whether the player keeps fullscreen when Picture in Picture (PiP) stops.\n * Only has an effect if the player was in fullscreen when PiP started.\n * - `'always'`: Always re-enter fullscreen when PiP stops.\n * - `'autoEnter'`: Re-enter fullscreen only when PiP was started automatically by the app going to the background.\n * - `'never'`: Do not re-enter fullscreen when PiP stops.\n */\nexport type KeepFullscreenOnPiPStopBehavior = 'always' | 'autoEnter' | 'never';\n\n/**\n * Describes the options for fullscreen video mode.\n */\nexport type FullscreenOptions = {\n /**\n * Specifies whether the fullscreen mode should be available to the user. When `false`, the fullscreen button will be hidden in the player.\n * @default true\n */\n enable: boolean;\n /**\n * Specifies the orientation of the video in fullscreen mode.\n * @default 'default'\n * @platform android\n * @platform ios\n */\n orientation?: FullscreenOrientation;\n /**\n * Specifies whether the app should exit fullscreen mode when the device is rotated to a different orientation than the one specified in the `orientation` prop.\n * For example, if the `orientation` prop is set to `landscape` and the device is rotated to `portrait`, the app will exit fullscreen mode.\n *\n * > This prop will have no effect if the `orientation` prop is set to `default`.\n * > The `VideoView` will never auto-exit fullscreen when the device auto-rotate feature has been disabled in settings.\n *\n * @default false\n * @platform android\n * @platform ios\n */\n autoExitOnRotate?: boolean;\n\n /**\n * Determines whether the player keeps fullscreen when Picture in Picture (PiP) stops.\n * Only has an effect if the player was in fullscreen when PiP started.\n *\n * @default 'autoEnter'\n * @platform ios\n */\n keepFullscreenOnPiPStop?: KeepFullscreenOnPiPStopBehavior;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ViewProps } from 'react-native';\n\nimport type { VideoPlayer } from './VideoPlayer.types';\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\nexport type VideoContentFit = 'contain' | 'cover' | 'fill';\n\n/**\n * Describes the type of the surface used to render the video.\n * - `surfaceView`: Uses the `SurfaceView` to render the video. This value should be used in the majority of cases. Provides significantly lower power consumption, better performance, and more features.\n * - `textureView`: Uses the `TextureView` to render the video. Should be used in cases where the SurfaceView is not supported or causes issues (for example, overlapping video views).\n *\n * You can learn more about surface types in the official [ExoPlayer documentation](https://developer.android.com/media/media3/ui/playerview#surfacetype).\n * @platform android\n */\nexport type SurfaceType = 'textureView' | 'surfaceView';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.\n */\n player?: VideoPlayer | null;\n\n /**\n * Determines whether native controls should be displayed or not.\n *\n * > **Note**: Due to platform limitations, the native controls are always enabled in fullscreen mode.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are `'contain'`, `'cover'`, and `'fill'`.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines the fullscreen mode options.\n */\n fullscreenOptions?: FullscreenOptions;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Configuration for controlling the visibility of player control buttons.\n * @platform android\n */\n buttonOptions?: ButtonOptions;\n\n /**\n * Customizes the appearance of the rendered subtitles/captions (text color, background, font, edge, and position).\n *\n * The style is applied to whatever subtitle track is currently displayed, regardless of whether it comes\n * from an embedded (`HLS`/`DASH`) track or an external one attached through\n * [`VideoSource.subtitleTracks`](#videosourcesubtitletracks).\n *\n * > **Note (iOS):** styling relies on `AVPlayerItem.textStyleRules`, which only affects `WebVTT` captions that\n * > the system renders itself. The OS-level *Settings → Accessibility → Subtitles & Captioning* preferences\n * > take precedence over these values when the user has enabled them.\n *\n * @platform android\n * @platform ios\n */\n subtitleStyle?: SubtitleStyle;\n\n /**\n * Determines the type of the surface used to render the video.\n * > This prop should not be changed at runtime.\n * @default 'surfaceView'\n * @platform android\n */\n surfaceType?: SurfaceType;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios\n * @platform web\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n * @platform android\n * @platform ios\n * @platform web\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether a video should be played \"inline\", that is, within the element's playback area.\n * @platform web\n */\n playsInline?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-app-config)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform android 12+\n * @platform ios\n */\n startsPictureInPictureAutomatically?: boolean;\n\n /**\n * Specifies whether to perform video frame analysis (Live Text in videos).\n * Check official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details.\n * @default true\n * @platform ios 16.0+\n */\n allowsVideoFrameAnalysis?: boolean;\n\n /**\n * A callback to call after the video player enters fullscreen mode.\n */\n onFullscreenEnter?: () => void;\n\n /**\n * A callback to call after the video player exits fullscreen mode.\n */\n onFullscreenExit?: () => void;\n\n /**\n * A callback to call after the mounted `VideoPlayer` has rendered the first frame into the `VideoView`.\n * This event can be used to hide any cover images that conceal the initial loading of the player.\n * > **Note:** This event may also be called during playback when the current video track changes (for example when the player switches video quality).\n */\n onFirstFrameRender?: () => void;\n\n /**\n * Determines whether the player should use the default ExoPlayer shutter that covers the `VideoView` before the first video frame is rendered.\n * Setting this property to `false` makes the Android behavior the same as iOS.\n *\n * @platform android\n * @default false\n */\n useExoShutter?: boolean;\n\n /**\n * Determines the [cross origin policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin) used by the underlying native view on web.\n * If `undefined` (default), does not use CORS at all. If set to `'anonymous'`, the video will be loaded with CORS enabled.\n * Note that some videos may not play if CORS is enabled, depending on the CDN settings.\n * If you encounter issues, consider adjusting the `crossOrigin` property.\n *\n *\n * @platform web\n * @default undefined\n */\n crossOrigin?: 'anonymous' | 'use-credentials';\n\n /**\n * Use Audio Nodes for sound playback. When the same player is playing in multiple video views the audio won't increase in volume\n * as the number of players increases.\n *\n * > **Note**: This property is experimental, when enabled it is known to break audio for some sources. Do not change this property at runtime.\n *\n * @experimental\n * @default false\n * @platform web\n */\n useAudioNodePlayback?: boolean;\n}\n\n/**\n * Describes the edge (outline) effect drawn around subtitle text.\n * - `none`: No edge effect.\n * - `outline`: A solid outline around each glyph.\n * - `dropShadow`: A drop shadow behind the text.\n * - `raised`: The text appears raised from the background.\n * - `depressed`: The text appears pressed into the background.\n */\nexport type SubtitleEdgeType = 'none' | 'outline' | 'dropShadow' | 'raised' | 'depressed';\n\n/**\n * Describes the visual style applied to the rendered subtitles/captions.\n *\n * Colors accept a hex string in the form `#RGB`, `#RRGGBB`, or `#RRGGBBAA` (an alpha suffix controls opacity).\n *\n * @platform android\n * @platform ios\n */\nexport type SubtitleStyle = {\n /**\n * The color of the subtitle text.\n * @default '#FFFFFF'\n */\n textColor?: string;\n\n /**\n * The background color drawn directly behind the text glyphs (the caption \"box\").\n * Use an alpha suffix (for example `#000000A0`) for a translucent box.\n * @default 'transparent'\n */\n backgroundColor?: string;\n\n /**\n * The background color of the whole cue region (the \"window\" behind the whole line).\n * @default 'transparent'\n */\n windowColor?: string;\n\n /**\n * The subtitle font size.\n *\n * > The unit differs per platform: on Android this is an absolute size in `sp`; on iOS it is a percentage\n * > relative to the default caption size (`100` = default, `150` = 1.5×).\n *\n * @platform android\n * @platform ios\n */\n fontSize?: number;\n\n /**\n * The name of the font family used to render the subtitles.\n *\n * Custom fonts bundled with the app are supported, using the same name you would pass to a\n * `<Text>` component's `fontFamily`:\n * - On Android the font is looked up as an Android font resource (`res/font`, where `expo-font`'s\n * config plugin places fonts) and then through React Native's font manager, which covers the\n * `assets/fonts/<name>.ttf` convention and fonts registered at runtime.\n * - On iOS the font must be registered with the system, which is what bundling it through\n * `expo-font`'s config plugin (`UIAppFonts`) does.\n *\n * The built-in system families always work — for example `sans-serif`, `sans-serif-medium`,\n * `serif`, `monospace` and `cursive` on Android.\n *\n * > A name that matches no font falls back to the default typeface silently: neither platform\n * > reports an unresolved font family, so double-check the spelling if nothing changes.\n */\n fontFamily?: string;\n\n /**\n * Whether the subtitle text should be rendered in bold.\n * @default false\n */\n bold?: boolean;\n\n /**\n * The edge (outline) effect drawn around the text.\n * @default 'none'\n */\n edgeType?: SubtitleEdgeType;\n\n /**\n * The color of the edge effect. Only has an effect when `edgeType` is not `'none'`.\n */\n edgeColor?: string;\n\n /**\n * The distance of the subtitles from the bottom of the video, expressed as a fraction of the view height (`0`–`1`).\n * @platform android\n */\n bottomOffset?: number;\n};\n\n/**\n * Configuration for controlling the visibility of player control buttons.\n *\n * > The fullscreen button should be controlled with [`fullscreenOptions.enable`](#fullscreenoptions).\n *\n * @platform android\n */\nexport type ButtonOptions = {\n /**\n * Whether to show the next button.\n * @default false\n */\n showNext?: boolean;\n /**\n * Whether to show the previous button.\n * @default false\n */\n showPrevious?: boolean;\n /**\n * Whether to show the seek forward button.\n * @default true\n */\n showSeekForward?: boolean;\n /**\n * Whether to show the seek backward button.\n * @default true\n */\n showSeekBackward?: boolean;\n /**\n * Whether to show the subtitles button.\n * - `true`: Button is always visible\n * - `false`: Button is never visible\n * - `undefined`: Button is visible only when subtitles are available (default behavior)\n * @default undefined\n */\n showSubtitles?: boolean | null;\n /**\n * Whether to show the settings button.\n * @default true\n */\n showSettings?: boolean;\n /**\n * Whether to show the play/pause button.\n * @default true\n */\n showPlayPause?: boolean;\n /**\n * Whether to show the bottom control bar (containing time, progress bar, and buttons).\n * When set to `false`, the entire bottom bar including the progress bar will be hidden.\n *\n * > **Note**: The bottom bar is always visible in fullscreen mode to allow users to exit fullscreen.\n *\n * @default true\n */\n showBottomBar?: boolean;\n};\n\n/**\n * Describes the orientation of the video in fullscreen mode. Available values are:\n * - `default`: The video is displayed in any of the available device rotations.\n * - `portrait`: The video is displayed in one of two available portrait orientations and rotates between them.\n * - `portraitUp`: The video is displayed in the portrait orientation - the notch of the phone points upwards.\n * - `portraitDown`: The video is displayed in the portrait orientation - the notch of the phone points downwards.\n * - `landscape`: The video is displayed in one of two available landscape orientations and rotates between them.\n * - `landscapeLeft`: The video is displayed in the left landscape orientation - the notch of the phone is in the left palm of the user.\n * - `landscapeRight`: The video is displayed in the right landscape orientation - the notch of the phone is in the right palm of the user.\n */\nexport type FullscreenOrientation =\n | 'default'\n | 'portrait'\n | 'portraitUp'\n | 'portraitDown'\n | 'landscape'\n | 'landscapeLeft'\n | 'landscapeRight';\n\n/**\n * Determines whether the player keeps fullscreen when Picture in Picture (PiP) stops.\n * Only has an effect if the player was in fullscreen when PiP started.\n * - `'always'`: Always re-enter fullscreen when PiP stops.\n * - `'autoEnter'`: Re-enter fullscreen only when PiP was started automatically by the app going to the background.\n * - `'never'`: Do not re-enter fullscreen when PiP stops.\n */\nexport type KeepFullscreenOnPiPStopBehavior = 'always' | 'autoEnter' | 'never';\n\n/**\n * Describes the options for fullscreen video mode.\n */\nexport type FullscreenOptions = {\n /**\n * Specifies whether the fullscreen mode should be available to the user. When `false`, the fullscreen button will be hidden in the player.\n * @default true\n */\n enable: boolean;\n /**\n * Specifies the orientation of the video in fullscreen mode.\n * @default 'default'\n * @platform android\n * @platform ios\n */\n orientation?: FullscreenOrientation;\n /**\n * Specifies whether the app should exit fullscreen mode when the device is rotated to a different orientation than the one specified in the `orientation` prop.\n * For example, if the `orientation` prop is set to `landscape` and the device is rotated to `portrait`, the app will exit fullscreen mode.\n *\n * > This prop will have no effect if the `orientation` prop is set to `default`.\n * > The `VideoView` will never auto-exit fullscreen when the device auto-rotate feature has been disabled in settings.\n *\n * @default false\n * @platform android\n * @platform ios\n */\n autoExitOnRotate?: boolean;\n\n /**\n * Determines whether the player keeps fullscreen when Picture in Picture (PiP) stops.\n * Only has an effect if the player was in fullscreen when PiP started.\n *\n * @default 'autoEnter'\n * @platform ios\n */\n keepFullscreenOnPiPStop?: KeepFullscreenOnPiPStopBehavior;\n};\n"]}
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts","./src/withexpovideo.ts"],"version":"5.9.3"}
|
package/src/VideoView.types.ts
CHANGED
|
@@ -253,6 +253,20 @@ export type SubtitleStyle = {
|
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
255
|
* The name of the font family used to render the subtitles.
|
|
256
|
+
*
|
|
257
|
+
* Custom fonts bundled with the app are supported, using the same name you would pass to a
|
|
258
|
+
* `<Text>` component's `fontFamily`:
|
|
259
|
+
* - On Android the font is looked up as an Android font resource (`res/font`, where `expo-font`'s
|
|
260
|
+
* config plugin places fonts) and then through React Native's font manager, which covers the
|
|
261
|
+
* `assets/fonts/<name>.ttf` convention and fonts registered at runtime.
|
|
262
|
+
* - On iOS the font must be registered with the system, which is what bundling it through
|
|
263
|
+
* `expo-font`'s config plugin (`UIAppFonts`) does.
|
|
264
|
+
*
|
|
265
|
+
* The built-in system families always work — for example `sans-serif`, `sans-serif-medium`,
|
|
266
|
+
* `serif`, `monospace` and `cursive` on Android.
|
|
267
|
+
*
|
|
268
|
+
* > A name that matches no font falls back to the default typeface silently: neither platform
|
|
269
|
+
* > reports an unresolved font family, so double-check the spelling if nothing changes.
|
|
256
270
|
*/
|
|
257
271
|
fontFamily?: string;
|
|
258
272
|
|