expo-video-subtitle 0.4.0 → 0.4.1

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 CHANGED
@@ -7,6 +7,45 @@ 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.4.1
11
+
12
+ ### Fixed
13
+
14
+ - **iOS:** `SubtitleSource.bottomOffset` now anchors the **bottom** of the subtitle block, the same way
15
+ Android does, so one value places captions identically on both platforms.
16
+
17
+ 0.4.0 emitted a bare WebVTT `line:` percentage, which anchors the *top* of the cue box. A cue that
18
+ wrapped onto two lines therefore sat a full line lower on iOS than on Android — 5–13% of the video
19
+ height at typical caption sizes, comparable to the offset itself, and enough to drop the second line
20
+ back over the burned-in captions the offset exists to escape. The cue setting is now written as
21
+ `line:N%,end`, where the trailing `,end` is WebVTT's line alignment component and is what makes the
22
+ percentage refer to the bottom edge.
23
+
24
+ This also fixes `bottomOffset: 0`, which as a bare `line:100%` put the top of the cue box at the bottom
25
+ edge of the video and hid the subtitle completely. It now means "sit at the bottom", matching Android.
26
+
27
+ Worth knowing before upgrading: a WebVTT parser that predates the alignment component discards the
28
+ entire `line` setting rather than ignoring the component, so on such a parser captions would be left
29
+ unpositioned instead of top-anchored. Bottom anchoring cannot be expressed any other way — it is the
30
+ same setting — so confirm the offset still lands on your target OS versions.
31
+
32
+ - **iOS:** the cached rendition of a rewritten subtitle is keyed on the format version as well as on the
33
+ URL, language and offset. Without it, an install that had already rendered a given offset would be
34
+ handed the file written by the previous version and the fix above would appear not to work — on
35
+ exactly the devices being used to test it.
36
+
37
+ - **iOS:** the same cache key joins its parts on NUL instead of a hyphen. A hyphen occurs in both
38
+ variable parts — BCP-47 tags carry one (`pt-BR`) and so does almost every URL — so two different
39
+ subtitles could flatten to the same string and be served each other's rewritten file.
40
+
41
+ ### Documentation
42
+
43
+ - **Android:** `subtitleStyle.bottomOffset` was documented as a fraction of the *view* height. It is a
44
+ fraction of the content frame, which under the default `contentFit="contain"` is the displayed picture
45
+ — `SubtitleView` sits inside `AspectRatioFrameLayout`, which `RESIZE_MODE_FIT` shrinks to the video's
46
+ aspect ratio. The distinction matters for a letterboxed source, and the old wording invited callers to
47
+ write letterbox compensation that would have been wrong.
48
+
10
49
  ## 0.4.0
11
50
 
12
51
  ### Added
package/FORK.md CHANGED
@@ -22,7 +22,7 @@ Measured against the published `expo-video@56.1.4` tarball. `+`/`-` are added/re
22
22
  | --- | --- | --- |
23
23
  | `ios/Records/SubtitleSource.swift` | 41 | Record for one sideloaded subtitle; `isSrt` detection; `bottomOffset`, which lives here rather than on `SubtitleStyle` because the composition is built from a `VideoSource` alone, before any `VideoView` has supplied a style. |
24
24
  | `ios/Records/SubtitleStyle.swift` | 118 | Record for caption styling; hex→ARGB parsing; builds `[AVTextStyleRule]`. |
25
- | `ios/VideoPlayerSubtitleSideload.swift` | 257 | Builds the `AVMutableComposition`, downloads remote subs, SRT→WebVTT converter, and `applyLinePosition`, which writes a WebVTT `line:` cue setting per cue for `bottomOffset` — the only way to position captions AVFoundation renders itself. Cue timing lines are matched by **timestamp regex, not by searching for `-->`**: subtitle text may contain an arrow, and appending a cue setting to it prints the setting on screen. The temp-file hash includes `bottomOffset`, or a changed offset would reuse the rendition written for the previous one. |
25
+ | `ios/VideoPlayerSubtitleSideload.swift` | 281 | Builds the `AVMutableComposition`, downloads remote subs, SRT→WebVTT converter, and `applyLinePosition`, which writes a WebVTT `line:N%,end` cue setting per cue for `bottomOffset` — the only way to position captions AVFoundation renders itself. **The trailing `,end` is the line alignment component, not a `line-align` setting** (there is no such setting; the six are `vertical`/`line`/`position`/`size`/`align`/`region`), and it is what anchors the percentage to the bottom edge, matching Android. A parser predating it drops the whole `line` setting, so there is no graceful degradation — verify on device. Cue timing lines are matched by **timestamp regex, not by searching for `-->`**: subtitle text may contain an arrow, and appending a cue setting to it prints the setting on screen. The temp-file hash includes `bottomOffset` *and* a `formatVersion`, joined on **NUL** — the offset so a changed value does not reuse the previous rendition, the version so an upgrade that changes the written form does not reuse the previous release's, and NUL because a hyphen occurs inside both BCP-47 tags and URLs and let two subtitles collide. |
26
26
  | `android/…/records/SubtitleSource.kt` | 54 | Record → `MediaItem.SubtitleConfiguration` + MIME resolution. |
27
27
  | `android/…/records/SubtitleStyle.kt` | 144 | Record → `CaptionStyleCompat`; hex colour parsing; font-family resolution (`res/font` → `ReactFontManager`); the `applyEmbeddedStyles` flag, which `SubtitleUtils` reads directly rather than `toCaptionStyle`. |
28
28
  | `android/…/utils/StackingSubtitleParser.kt` | 300 | `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. Assigns each cue a vertical slot it keeps for its whole time on screen — a finished cue's line is held blank (`\u200B`) so the cues above it do not drop. Merges through a `SpannableStringBuilder` so cue spans survive. |
@@ -182,7 +182,8 @@ npm pack --dry-run # expect build/, plugin/build/, LICENSE, native sources
182
182
  # NO dev config (.prettierrc, eslint.config.js, *.tsbuildinfo) — see §1
183
183
  ```
184
184
 
185
- The file count is a blunt but effective regression check: **254 files** as of 0.2.2. If a change moves
185
+ The file count is a blunt but effective regression check: **255 files** as of 0.4.1 (254 at 0.2.2, plus
186
+ `RoundedSubtitleBackground.kt` in 0.3.0). If a change moves
186
187
  it, diff the list against the previous release and confirm every difference is intended —
187
188
  `npm pack --dry-run --json` gives a machine-readable list. Both directions matter: an unexpected
188
189
  *addition* means dev clutter leaked in, an unexpected *removal* means a consumer is missing something.
@@ -194,6 +195,36 @@ Neither is created by `expo-module configure`, so a sync that loses one breaks t
194
195
  that reads like a tooling bug. A `prettier/prettier` warning after a sync is far more likely to mean
195
196
  the config went missing than that the code is genuinely misformatted — check that first.
196
197
 
198
+ ### iOS — running the subtitle converter without a host app
199
+
200
+ `SubtitleConverter` at the bottom of `ios/VideoPlayerSubtitleSideload.swift` imports only `Foundation`,
201
+ so it compiles and runs on its own. It is worth exercising on every change to it: `srtToVtt` and
202
+ `applyLinePosition` are pure string transforms whose failure mode is a silently malformed cue rather
203
+ than a crash — a cue setting appended to a *text* line prints on screen, and a percentage outside
204
+ `0`–`100` can cost the whole cue.
205
+
206
+ Extract the shipped source rather than retyping it, so the test cannot drift from what ships:
207
+
208
+ ```bash
209
+ awk '/^internal enum SubtitleConverter \{/,0' ios/VideoPlayerSubtitleSideload.swift \
210
+ > /tmp/swiftcheck/Converter.swift
211
+ sed -i '' '1i\
212
+ import Foundation
213
+ ' /tmp/swiftcheck/Converter.swift
214
+ # Assertions go in main.swift — Swift allows top-level code only in a file with that name.
215
+ swiftc -o converttest Converter.swift main.swift && ./converttest
216
+ ```
217
+
218
+ Cases worth keeping covered: the emitted setting is `line:N%,end`; `0` and `1` map to `line:100%,end`
219
+ and `line:0%,end`; out-of-range offsets clamp; a text line containing `-->` is left alone; a cue already
220
+ carrying `line:` is passed through; an unrelated cue setting (`align:start`) is preserved while the cue
221
+ still moves; hourless timings are recognised; CRLF is normalised.
222
+
223
+ **A passing harness does not mean the cue renders where you asked.** It checks the bytes written, not
224
+ whether AVFoundation honours them. `line:` positioning is the part of this package with no automated
225
+ coverage at all — confirm it against a real player with a *two-line* cue, which is the case the
226
+ alignment component exists for.
227
+
197
228
  ### Android — compiling the parser without a host app
198
229
 
199
230
  `utils/StackingSubtitleParser.kt` imports only `android.text.*` and `androidx.media3.*`, so it
package/README.md CHANGED
@@ -138,13 +138,20 @@ takes effect the next time the source loads**.
138
138
  | Where | `subtitleStyle.bottomOffset` | `subtitleTracks[].bottomOffset` |
139
139
  | Applies to | every track, embedded included | sideloaded tracks only |
140
140
  | Updates | live | on next source load |
141
- | Anchors | the bottom of the subtitle block | the top of it, so a two-line cue grows downward |
141
+ | Anchors | the bottom of the subtitle block | the bottom of the subtitle block |
142
142
  | Cues that position themselves | left alone | left alone |
143
143
 
144
144
  That last row is worth knowing: subtitles carrying their own placement — SubRip `{\anN}` tags, WebVTT
145
145
  cues with a `line:` setting — were positioned deliberately by whoever wrote them, so neither platform
146
146
  moves them.
147
147
 
148
+ The iOS cue setting is written as `line:N%,end`. The trailing `,end` is WebVTT's line alignment
149
+ component, and it is what makes the percentage refer to the bottom edge of the cue box — so the two
150
+ platforms anchor the same way and one value is correct on both. Note that a WebVTT parser predating
151
+ that component discards the whole `line` setting rather than ignoring the component, which would leave
152
+ captions unpositioned; there is no form that degrades more gently, since both behaviours come from the
153
+ same setting.
154
+
148
155
  ## Styling subtitles
149
156
 
150
157
  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`.
@@ -256,7 +256,14 @@ export type SubtitleStyle = {
256
256
  */
257
257
  edgeColor?: string;
258
258
  /**
259
- * The distance of the subtitles from the bottom of the video, expressed as a fraction of the view height (`0`–`1`).
259
+ * The distance of the subtitles from the bottom of the video, expressed as a fraction (`0`–`1`).
260
+ *
261
+ * The fraction is of the content frame, which with the default `contentFit="contain"` is the displayed
262
+ * picture rather than the whole view — so one value keeps captions at the same height relative to the
263
+ * image across screen sizes, pixel densities, orientations and letterboxed sources, with no conversion
264
+ * needed. With `contentFit="cover"` or `"fill"` the content frame is the view.
265
+ *
266
+ * @default 0.08
260
267
  * @platform android
261
268
  */
262
269
  bottomOffset?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"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"}
@@ -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 * 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 * Corner radius, in dp, of the box drawn behind the caption text by [`backgroundColor`](#subtitlestylebackgroundcolor).\n *\n * A box is drawn per rendered line, so a caption that wraps onto two lines gets two rounded boxes, the\n * way VLC and most desktop players draw them. Has no effect unless `backgroundColor` is set and not\n * fully transparent.\n *\n * > **Note:** [`windowColor`](#subtitlestylewindowcolor) is not affected — the caption *window* stays\n * > rectangular. Rounding applies to the box that hugs the text.\n *\n * @default 0 (square corners)\n * @platform android\n */\n backgroundRadius?: number;\n\n /**\n * Whether the inline formatting carried by the subtitle track itself should be kept.\n *\n * Subtitle formats bring their own styling — `SubRip`'s `<b>`, `<i>`, `<u>` and `<font color>` tags, and the\n * richer per-cue styling of embedded `WebVTT`/`TTML` tracks. By default all of it is stripped, so the other\n * properties of this object are the only thing that decides how captions look. Set this to `true` to let a\n * track style its own text.\n *\n * The remaining `SubtitleStyle` properties still apply underneath, and `fontSize` keeps overriding any size\n * the track asks for. Only the properties a cue explicitly sets are affected — an unstyled cue looks the same\n * either way.\n *\n * > **Note:** a track that specifies its own colors will override [`textColor`](#subtitlestyletextcolor) and\n * > [`windowColor`](#subtitlestylewindowcolor) for the cues that specify them.\n *\n * @default false\n * @platform android\n */\n applyEmbeddedStyles?: boolean;\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 (`0`–`1`).\n *\n * The fraction is of the content frame, which with the default `contentFit=\"contain\"` is the displayed\n * picture rather than the whole view — so one value keeps captions at the same height relative to the\n * image across screen sizes, pixel densities, orientations and letterboxed sources, with no conversion\n * needed. With `contentFit=\"cover\"` or `\"fill\"` the content frame is the view.\n *\n * @default 0.08\n * @platform android\n */\n bottomOffset?: number;\n\n /**\n * Corner radius, in dp, of the box drawn behind the caption text by [`backgroundColor`](#subtitlestylebackgroundcolor).\n *\n * A box is drawn per rendered line, so a caption that wraps onto two lines gets two rounded boxes, the\n * way VLC and most desktop players draw them. Has no effect unless `backgroundColor` is set and not\n * fully transparent.\n *\n * > **Note:** [`windowColor`](#subtitlestylewindowcolor) is not affected — the caption *window* stays\n * > rectangular. Rounding applies to the box that hugs the text.\n *\n * @default 0 (square corners)\n * @platform android\n */\n backgroundRadius?: number;\n\n /**\n * Whether the inline formatting carried by the subtitle track itself should be kept.\n *\n * Subtitle formats bring their own styling — `SubRip`'s `<b>`, `<i>`, `<u>` and `<font color>` tags, and the\n * richer per-cue styling of embedded `WebVTT`/`TTML` tracks. By default all of it is stripped, so the other\n * properties of this object are the only thing that decides how captions look. Set this to `true` to let a\n * track style its own text.\n *\n * The remaining `SubtitleStyle` properties still apply underneath, and `fontSize` keeps overriding any size\n * the track asks for. Only the properties a cue explicitly sets are affected — an unstyled cue looks the same\n * either way.\n *\n * > **Note:** a track that specifies its own colors will override [`textColor`](#subtitlestyletextcolor) and\n * > [`windowColor`](#subtitlestylewindowcolor) for the cues that specify them.\n *\n * @default false\n * @platform android\n */\n applyEmbeddedStyles?: boolean;\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"]}
@@ -169,8 +169,23 @@ internal enum VideoPlayerSubtitleSideload {
169
169
  //
170
170
  // `bottomOffset` is part of the identity because it changes the bytes written: without it, the
171
171
  // reuse check below would hand back a rendition positioned for a previous offset.
172
+ //
173
+ // `formatVersion` covers the same hazard across an *upgrade*: the inputs can be identical while
174
+ // the bytes this version writes are not, and the reuse check would then serve the file the
175
+ // previous version wrote — on exactly the devices being used to test the change. Bump it whenever
176
+ // the written form changes. v2: `applyLinePosition` gained the `,end` line alignment.
177
+ //
178
+ // Joined on NUL rather than a hyphen because a hyphen occurs in both of the variable parts — BCP-47
179
+ // tags carry one (`pt-BR`) and so does almost every URL — which lets two different subtitles
180
+ // flatten to the same string and collide on the hash. NUL cannot appear in either.
181
+ let formatVersion = "v2"
172
182
  let offsetKey = source.bottomOffset.map { String(format: "%.4f", $0) } ?? ""
173
- let identifier = "\(source.uri?.absoluteString ?? "")-\(source.language ?? "")-\(offsetKey)"
183
+ let identifier = [
184
+ formatVersion,
185
+ source.uri?.absoluteString ?? "",
186
+ source.language ?? "",
187
+ offsetKey
188
+ ].joined(separator: "\u{0}")
174
189
  let digest = SHA256.hash(data: Data(identifier.utf8))
175
190
  let hashString = digest.compactMap { String(format: "%02x", $0) }.joined()
176
191
  let fileURL = directory.appendingPathComponent("\(hashString).vtt")
@@ -225,10 +240,19 @@ internal enum SubtitleConverter {
225
240
  /// - Parameter bottomOffset: fraction of the video height to sit above the bottom, matching the
226
241
  /// Android `subtitleStyle.bottomOffset`. WebVTT counts `line` down from the top, so it is inverted.
227
242
  ///
228
- /// Note the anchor differs from Android: a bare `line:` percentage positions the *top* of the cue
229
- /// box, so a cue that wraps onto two lines extends downward from there, where Android holds the
230
- /// bottom edge fixed. `line-align` would express it exactly, but it is a newer part of the syntax and
231
- /// a parser that rejects it would drop the whole setting, so the widely-understood form is used.
243
+ /// The emitted setting is `line:<percentage>,end`. The trailing `,end` is the WebVTT *line alignment*
244
+ /// component — not a separate `line-align` setting, which is not one of the six cue setting names —
245
+ /// and it is what makes the percentage refer to the *bottom* edge of the cue box, matching Android's
246
+ /// `setBottomPaddingFraction`. A bare percentage anchors the *top*, so a cue wrapping onto two lines
247
+ /// hangs a full line lower on iOS than on Android; at typical caption sizes that is 5–13% of the video
248
+ /// height, comparable to the offset itself, and enough to put the second line back over the burned-in
249
+ /// captions this exists to escape. It also fixes `bottomOffset: 0`, which as a bare `line:100%` put the
250
+ /// top of the box at the bottom edge of the video and hid the subtitle entirely.
251
+ ///
252
+ /// The alignment component is a known risk: a parser that predates it does not merely ignore it, it
253
+ /// discards the whole `line` setting, which would leave captions unpositioned rather than
254
+ /// top-anchored. There is no form that degrades gracefully — the two behaviours are expressed by the
255
+ /// same setting — so whether AVFoundation honours it has to be confirmed on a device. See FORK.md §5.
232
256
  static func applyLinePosition(_ vtt: String, bottomOffset: Double) -> String {
233
257
  let clamped = min(max(bottomOffset, 0), 1)
234
258
  let linePercentage = Int(((1 - clamped) * 100).rounded())
@@ -249,7 +273,7 @@ internal enum SubtitleConverter {
249
273
  guard !line.contains("line:") else {
250
274
  return line
251
275
  }
252
- return "\(line) line:\(linePercentage)%"
276
+ return "\(line) line:\(linePercentage)%,end"
253
277
  }
254
278
 
255
279
  return positioned.joined(separator: "\n")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-video-subtitle",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "A cross-platform, performant video component for React Native and Expo with external subtitle sideloading and custom subtitle styling.",
5
5
  "keywords": [
6
6
  "expo",
@@ -288,7 +288,14 @@ export type SubtitleStyle = {
288
288
  edgeColor?: string;
289
289
 
290
290
  /**
291
- * The distance of the subtitles from the bottom of the video, expressed as a fraction of the view height (`0`–`1`).
291
+ * The distance of the subtitles from the bottom of the video, expressed as a fraction (`0`–`1`).
292
+ *
293
+ * The fraction is of the content frame, which with the default `contentFit="contain"` is the displayed
294
+ * picture rather than the whole view — so one value keeps captions at the same height relative to the
295
+ * image across screen sizes, pixel densities, orientations and letterboxed sources, with no conversion
296
+ * needed. With `contentFit="cover"` or `"fill"` the content frame is the view.
297
+ *
298
+ * @default 0.08
292
299
  * @platform android
293
300
  */
294
301
  bottomOffset?: number;