expo-mpv 0.1.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lonzzi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,33 +1,164 @@
1
1
  # expo-mpv
2
2
 
3
- # API documentation
3
+ An Expo module wrapping [libmpv](https://mpv.io/) for video playback on iOS, powered by [MPVKit](https://github.com/mpvkit/MPVKit).
4
4
 
5
- - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/mpv/)
6
- - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/mpv/)
5
+ > **Note:** Currently only **iOS** is supported. Android support is planned.
7
6
 
8
- # Installation in managed Expo projects
7
+ ## Features
9
8
 
10
- For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
9
+ - Hardware-accelerated video playback via libmpv + VideoToolbox
10
+ - Metal rendering via MoltenVK (Vulkan → Metal)
11
+ - Play/pause, seek, speed, volume, mute, loop
12
+ - Subtitle track selection (embedded + external)
13
+ - Audio track selection
14
+ - External subtitle loading (`sub-add`)
15
+ - Progress, buffering, error, and playback state events
16
+ - CJK subtitle support with bundled Noto Sans CJK SC font
11
17
 
12
- # Installation in bare React Native projects
18
+ ## Installation
13
19
 
14
- For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
20
+ ```bash
21
+ npx expo install expo-mpv
22
+ ```
23
+
24
+ This package requires `expo-build-properties` to set the iOS deployment target to 16.0:
25
+
26
+ ```bash
27
+ npx expo install expo-build-properties
28
+ ```
29
+
30
+ Add to your `app.json`:
31
+
32
+ ```json
33
+ {
34
+ "plugins": [
35
+ [
36
+ "expo-build-properties",
37
+ {
38
+ "ios": {
39
+ "deploymentTarget": "16.0"
40
+ }
41
+ }
42
+ ]
43
+ ]
44
+ }
45
+ ```
46
+
47
+ ### MPVKit XCFrameworks
15
48
 
16
- ### Add the package to your npm dependencies
49
+ Before building, download the pre-built MPVKit xcframeworks:
17
50
 
51
+ ```bash
52
+ cd node_modules/expo-mpv/ios
53
+ bash download-mpvkit.sh
18
54
  ```
19
- npm install expo-mpv
55
+
56
+ This downloads ~28 xcframeworks (libmpv, FFmpeg, MoltenVK, libass, etc.) from the [MPVKit releases](https://github.com/mpvkit/MPVKit/releases).
57
+
58
+ ## Usage
59
+
60
+ ```tsx
61
+ import { ExpoMpvView } from "expo-mpv";
62
+ import type { ExpoMpvViewRef } from "expo-mpv";
63
+ import { useRef } from "react";
64
+
65
+ export default function Player() {
66
+ const playerRef = useRef<ExpoMpvViewRef>(null);
67
+
68
+ return (
69
+ <ExpoMpvView
70
+ ref={playerRef}
71
+ source="https://example.com/video.mp4"
72
+ style={{ width: "100%", aspectRatio: 16 / 9 }}
73
+ onLoad={({ nativeEvent }) => {
74
+ console.log("Duration:", nativeEvent.duration);
75
+ }}
76
+ onProgress={({ nativeEvent }) => {
77
+ console.log("Position:", nativeEvent.position);
78
+ }}
79
+ onError={({ nativeEvent }) => {
80
+ console.error("Error:", nativeEvent.error);
81
+ }}
82
+ />
83
+ );
84
+ }
85
+ ```
86
+
87
+ ### Imperative API (via ref)
88
+
89
+ ```ts
90
+ playerRef.current?.play();
91
+ playerRef.current?.pause();
92
+ playerRef.current?.togglePlay();
93
+ playerRef.current?.seekTo(120); // seconds
94
+ playerRef.current?.seekBy(-10); // relative seconds
95
+ playerRef.current?.setSpeed(1.5);
96
+ playerRef.current?.setVolume(80); // 0-100
97
+ playerRef.current?.setMuted(true);
98
+ playerRef.current?.setSubtitleTrack(2);
99
+ playerRef.current?.setAudioTrack(1);
100
+ playerRef.current?.addSubtitle("https://example.com/subs.srt");
101
+ playerRef.current?.setSubtitleDelay(-0.5); // seconds
102
+
103
+ const info = await playerRef.current?.getPlaybackInfo();
104
+ const tracks = await playerRef.current?.getTrackList();
20
105
  ```
21
106
 
22
- ### Configure for Android
107
+ ### Props
23
108
 
109
+ | Prop | Type | Description |
110
+ |------|------|-------------|
111
+ | `source` | `string` | Media URL or local file path |
112
+ | `paused` | `boolean` | Pause/resume playback |
113
+ | `speed` | `number` | Playback speed (default: 1.0) |
114
+ | `volume` | `number` | Volume 0-100 (default: 100) |
115
+ | `muted` | `boolean` | Mute audio |
116
+ | `loop` | `boolean` | Loop current file |
24
117
 
118
+ ### Events
25
119
 
120
+ | Event | Payload | Description |
121
+ |-------|---------|-------------|
122
+ | `onPlaybackStateChange` | `{ state, isPlaying }` | Play/pause state changed |
123
+ | `onProgress` | `{ position, duration, bufferedDuration }` | Periodic progress update |
124
+ | `onLoad` | `{ duration, width, height }` | Media loaded and ready |
125
+ | `onError` | `{ error }` | Error occurred |
126
+ | `onEnd` | `{ reason }` | Playback ended |
127
+ | `onBuffer` | `{ isBuffering }` | Buffering state changed |
128
+ | `onSeek` | `{}` | Seek completed |
129
+ | `onVolumeChange` | `{ volume, muted }` | Volume/mute changed |
26
130
 
27
- ### Configure for iOS
131
+ ## CJK Subtitle Rendering
132
+
133
+ This module bundles [Noto Sans CJK SC](https://github.com/notofonts/noto-cjk) (SIL Open Font License) for Chinese/Japanese/Korean subtitle rendering.
134
+
135
+ **Why a bundled font is necessary:**
136
+
137
+ Starting with iOS 18, Apple changed system fonts (PingFang, Heiti, etc.) to a proprietary HVGL variable font format. FreeType — the font rasterizer used by libass (mpv's subtitle renderer) — cannot parse HVGL fonts. This means system CJK fonts are invisible to libass, causing Chinese characters to render as empty boxes (tofu).
138
+
139
+ This is a known issue across the ecosystem:
140
+ - [libass/libass#912](https://github.com/libass/libass/issues/912) — FreeType HVGL support tracking
141
+ - [mpv-player/mpv#14878](https://github.com/mpv-player/mpv/issues/14878) — PingFang broken on macOS 15
142
+ - [iina/iina#5176](https://github.com/iina/iina/issues/5176) — IINA Chinese subtitle garbling
143
+ - [arthenica/ffmpeg-kit#1001](https://github.com/arthenica/ffmpeg-kit/issues/1001) — ffmpeg-kit iOS 18 subtitle issue
144
+
145
+ The bundled Noto Sans CJK SC Regular (~16MB) covers Simplified Chinese, Traditional Chinese, Japanese, and Korean. It uses the [SIL Open Font License](https://openfontlicense.org/), which permits free use, embedding, and redistribution.
146
+
147
+ ## Architecture
148
+
149
+ ```
150
+ React Native (JS)
151
+ └─ ExpoMpvView (native view)
152
+ └─ mpv (libmpv C API)
153
+ ├─ FFmpeg (demuxing, decoding)
154
+ ├─ VideoToolbox (hardware decoding)
155
+ ├─ libplacebo → Vulkan → MoltenVK → Metal (rendering, device only)
156
+ ├─ gpu → Vulkan → MoltenVK → Metal (rendering, simulator)
157
+ └─ libass + FreeType + Noto Sans CJK (subtitle rendering)
158
+ ```
28
159
 
29
- Run `npx pod-install` after installing the npm package.
160
+ On simulator, `vo=gpu` is used instead of `vo=gpu-next` to avoid a crash in `MTLSimDriver` caused by XPC shared memory size limits when libplacebo uploads video frame textures.
30
161
 
31
- # Contributing
162
+ ## License
32
163
 
33
- Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
164
+ [MIT](./LICENSE)
@@ -35,6 +35,26 @@ export type PlaybackInfo = {
35
35
  volume: number;
36
36
  muted: boolean;
37
37
  };
38
+ export type TrackInfo = {
39
+ id: number;
40
+ type: 'video' | 'audio' | 'sub' | string;
41
+ title: string;
42
+ lang: string;
43
+ codec: string;
44
+ selected: boolean;
45
+ isDefault: boolean;
46
+ isExternal: boolean;
47
+ channelCount?: number;
48
+ sampleRate?: number;
49
+ width?: number;
50
+ height?: number;
51
+ fps?: number;
52
+ };
53
+ export type CurrentTrackIds = {
54
+ vid: number;
55
+ aid: number;
56
+ sid: number;
57
+ };
38
58
  export type ExpoMpvModuleEvents = {};
39
59
  export type ExpoMpvViewProps = {
40
60
  /**
@@ -123,6 +143,16 @@ export type ExpoMpvViewRef = {
123
143
  setMuted: (muted: boolean) => Promise<void>;
124
144
  setSubtitleTrack: (trackId: number) => Promise<void>;
125
145
  setAudioTrack: (trackId: number) => Promise<void>;
146
+ /** Load an external subtitle file (local path or URL). */
147
+ addSubtitle: (path: string, flag?: string, title?: string, lang?: string) => Promise<void>;
148
+ /** Remove a subtitle track by ID. */
149
+ removeSubtitle: (trackId: number) => Promise<void>;
150
+ /** Reload current subtitles. */
151
+ reloadSubtitles: () => Promise<void>;
152
+ /** Set subtitle delay in seconds (positive = later, negative = earlier). */
153
+ setSubtitleDelay: (seconds: number) => Promise<void>;
126
154
  getPlaybackInfo: () => Promise<PlaybackInfo>;
155
+ getTrackList: () => Promise<TrackInfo[]>;
156
+ getCurrentTrackIds: () => Promise<CurrentTrackIds>;
127
157
  };
128
158
  //# sourceMappingURL=ExpoMpv.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoMpv.types.d.ts","sourceRoot":"","sources":["../src/ExpoMpv.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoMpv.types.d.ts","sourceRoot":"","sources":["../src/ExpoMpv.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoMpv.types.js","sourceRoot":"","sources":["../src/ExpoMpv.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\n// MARK: - Event Payloads\n\nexport type PlaybackStateChangeEvent = {\n state: 'playing' | 'paused' | 'stopped';\n isPlaying: boolean;\n};\n\nexport type ProgressEvent = {\n position: number;\n duration: number;\n bufferedDuration: number;\n};\n\nexport type LoadEvent = {\n duration: number;\n width: number;\n height: number;\n};\n\nexport type ErrorEvent = {\n error: string;\n};\n\nexport type EndEvent = {\n reason: 'ended' | 'error' | 'stopped' | 'unknown';\n};\n\nexport type BufferEvent = {\n isBuffering: boolean;\n};\n\nexport type SeekEvent = {};\n\nexport type VolumeChangeEvent = {\n volume: number;\n muted: boolean;\n};\n\nexport type PlaybackInfo = {\n position: number;\n duration: number;\n isPlaying: boolean;\n speed: number;\n volume: number;\n muted: boolean;\n};\n\n// MARK: - Module Events (non-view)\n\nexport type ExpoMpvModuleEvents = {};\n\n// MARK: - View Props\n\nexport type ExpoMpvViewProps = {\n /**\n * Media source URL to play. Can be a remote URL or a local file path.\n */\n source?: string;\n\n /**\n * Whether playback is paused.\n */\n paused?: boolean;\n\n /**\n * Playback speed multiplier. Default is 1.0.\n */\n speed?: number;\n\n /**\n * Volume level (0-100). Default is 100.\n */\n volume?: number;\n\n /**\n * Whether audio is muted.\n */\n muted?: boolean;\n\n /**\n * Whether to loop the current file.\n */\n loop?: boolean;\n\n /**\n * Called when playback state changes (play/pause).\n */\n onPlaybackStateChange?: (event: { nativeEvent: PlaybackStateChangeEvent }) => void;\n\n /**\n * Called periodically with current playback progress.\n */\n onProgress?: (event: { nativeEvent: ProgressEvent }) => void;\n\n /**\n * Called when a media file has been loaded and is ready to play.\n */\n onLoad?: (event: { nativeEvent: LoadEvent }) => void;\n\n /**\n * Called when an error occurs.\n */\n onError?: (event: { nativeEvent: ErrorEvent }) => void;\n\n /**\n * Called when playback reaches the end.\n */\n onEnd?: (event: { nativeEvent: EndEvent }) => void;\n\n /**\n * Called when buffering state changes.\n */\n onBuffer?: (event: { nativeEvent: BufferEvent }) => void;\n\n /**\n * Called when a seek operation completes.\n */\n onSeek?: (event: { nativeEvent: SeekEvent }) => void;\n\n /**\n * Called when volume or mute state changes.\n */\n onVolumeChange?: (event: { nativeEvent: VolumeChangeEvent }) => void;\n\n style?: StyleProp<ViewStyle>;\n};\n\n// MARK: - View Ref Methods\n\nexport type ExpoMpvViewRef = {\n play: () => Promise<void>;\n pause: () => Promise<void>;\n togglePlay: () => Promise<void>;\n stop: () => Promise<void>;\n seekTo: (position: number) => Promise<void>;\n seekBy: (offset: number) => Promise<void>;\n setSpeed: (speed: number) => Promise<void>;\n setVolume: (volume: number) => Promise<void>;\n setMuted: (muted: boolean) => Promise<void>;\n setSubtitleTrack: (trackId: number) => Promise<void>;\n setAudioTrack: (trackId: number) => Promise<void>;\n getPlaybackInfo: () => Promise<PlaybackInfo>;\n};\n"]}
1
+ {"version":3,"file":"ExpoMpv.types.js","sourceRoot":"","sources":["../src/ExpoMpv.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\n// MARK: - Event Payloads\n\nexport type PlaybackStateChangeEvent = {\n state: 'playing' | 'paused' | 'stopped';\n isPlaying: boolean;\n};\n\nexport type ProgressEvent = {\n position: number;\n duration: number;\n bufferedDuration: number;\n};\n\nexport type LoadEvent = {\n duration: number;\n width: number;\n height: number;\n};\n\nexport type ErrorEvent = {\n error: string;\n};\n\nexport type EndEvent = {\n reason: 'ended' | 'error' | 'stopped' | 'unknown';\n};\n\nexport type BufferEvent = {\n isBuffering: boolean;\n};\n\nexport type SeekEvent = {};\n\nexport type VolumeChangeEvent = {\n volume: number;\n muted: boolean;\n};\n\nexport type PlaybackInfo = {\n position: number;\n duration: number;\n isPlaying: boolean;\n speed: number;\n volume: number;\n muted: boolean;\n};\n\nexport type TrackInfo = {\n id: number;\n type: 'video' | 'audio' | 'sub' | string;\n title: string;\n lang: string;\n codec: string;\n selected: boolean;\n isDefault: boolean;\n isExternal: boolean;\n // audio-specific\n channelCount?: number;\n sampleRate?: number;\n // video-specific\n width?: number;\n height?: number;\n fps?: number;\n};\n\nexport type CurrentTrackIds = {\n vid: number;\n aid: number;\n sid: number;\n};\n\n// MARK: - Module Events (non-view)\n\nexport type ExpoMpvModuleEvents = {};\n\n// MARK: - View Props\n\nexport type ExpoMpvViewProps = {\n /**\n * Media source URL to play. Can be a remote URL or a local file path.\n */\n source?: string;\n\n /**\n * Whether playback is paused.\n */\n paused?: boolean;\n\n /**\n * Playback speed multiplier. Default is 1.0.\n */\n speed?: number;\n\n /**\n * Volume level (0-100). Default is 100.\n */\n volume?: number;\n\n /**\n * Whether audio is muted.\n */\n muted?: boolean;\n\n /**\n * Whether to loop the current file.\n */\n loop?: boolean;\n\n /**\n * Called when playback state changes (play/pause).\n */\n onPlaybackStateChange?: (event: { nativeEvent: PlaybackStateChangeEvent }) => void;\n\n /**\n * Called periodically with current playback progress.\n */\n onProgress?: (event: { nativeEvent: ProgressEvent }) => void;\n\n /**\n * Called when a media file has been loaded and is ready to play.\n */\n onLoad?: (event: { nativeEvent: LoadEvent }) => void;\n\n /**\n * Called when an error occurs.\n */\n onError?: (event: { nativeEvent: ErrorEvent }) => void;\n\n /**\n * Called when playback reaches the end.\n */\n onEnd?: (event: { nativeEvent: EndEvent }) => void;\n\n /**\n * Called when buffering state changes.\n */\n onBuffer?: (event: { nativeEvent: BufferEvent }) => void;\n\n /**\n * Called when a seek operation completes.\n */\n onSeek?: (event: { nativeEvent: SeekEvent }) => void;\n\n /**\n * Called when volume or mute state changes.\n */\n onVolumeChange?: (event: { nativeEvent: VolumeChangeEvent }) => void;\n\n style?: StyleProp<ViewStyle>;\n};\n\n// MARK: - View Ref Methods\n\nexport type ExpoMpvViewRef = {\n play: () => Promise<void>;\n pause: () => Promise<void>;\n togglePlay: () => Promise<void>;\n stop: () => Promise<void>;\n seekTo: (position: number) => Promise<void>;\n seekBy: (offset: number) => Promise<void>;\n setSpeed: (speed: number) => Promise<void>;\n setVolume: (volume: number) => Promise<void>;\n setMuted: (muted: boolean) => Promise<void>;\n setSubtitleTrack: (trackId: number) => Promise<void>;\n setAudioTrack: (trackId: number) => Promise<void>;\n /** Load an external subtitle file (local path or URL). */\n addSubtitle: (path: string, flag?: string, title?: string, lang?: string) => Promise<void>;\n /** Remove a subtitle track by ID. */\n removeSubtitle: (trackId: number) => Promise<void>;\n /** Reload current subtitles. */\n reloadSubtitles: () => Promise<void>;\n /** Set subtitle delay in seconds (positive = later, negative = earlier). */\n setSubtitleDelay: (seconds: number) => Promise<void>;\n getPlaybackInfo: () => Promise<PlaybackInfo>;\n getTrackList: () => Promise<TrackInfo[]>;\n getCurrentTrackIds: () => Promise<CurrentTrackIds>;\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoMpvView.d.ts","sourceRoot":"","sources":["../src/ExpoMpvView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAKnE,QAAA,MAAM,WAAW,yFAmBf,CAAC;AAIH,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ExpoMpvView.d.ts","sourceRoot":"","sources":["../src/ExpoMpvView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAKnE,QAAA,MAAM,WAAW,yFA0Bf,CAAC;AAIH,eAAe,WAAW,CAAC"}
@@ -16,7 +16,13 @@ const ExpoMpvView = forwardRef((props, ref) => {
16
16
  setMuted: (muted) => nativeRef.current?.setMuted(muted),
17
17
  setSubtitleTrack: (trackId) => nativeRef.current?.setSubtitleTrack(trackId),
18
18
  setAudioTrack: (trackId) => nativeRef.current?.setAudioTrack(trackId),
19
+ addSubtitle: (path, flag, title, lang) => nativeRef.current?.addSubtitle(path, flag, title, lang),
20
+ removeSubtitle: (trackId) => nativeRef.current?.removeSubtitle(trackId),
21
+ reloadSubtitles: () => nativeRef.current?.reloadSubtitles(),
22
+ setSubtitleDelay: (seconds) => nativeRef.current?.setSubtitleDelay(seconds),
19
23
  getPlaybackInfo: () => nativeRef.current?.getPlaybackInfo(),
24
+ getTrackList: () => nativeRef.current?.getTrackList(),
25
+ getCurrentTrackIds: () => nativeRef.current?.getCurrentTrackIds(),
20
26
  }));
21
27
  return <NativeView ref={nativeRef} {...props}/>;
22
28
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoMpvView.js","sourceRoot":"","sources":["../src/ExpoMpvView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAIhE,MAAM,UAAU,GACd,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAE/B,MAAM,WAAW,GAAG,UAAU,CAAmC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC9E,MAAM,SAAS,GAAG,MAAM,CAAM,IAAI,CAAC,CAAC;IAEpC,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9B,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACrC,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE;QACvC,UAAU,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE;QACjD,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACrC,MAAM,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC;QACjE,MAAM,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QAC7D,QAAQ,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC;QAC/D,SAAS,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC;QACnE,QAAQ,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC;QAChE,gBAAgB,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC;QACnF,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC;QAC7E,eAAe,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE;KAC5D,CAAC,CAAC,CAAC;IAEJ,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACnD,CAAC,CAAC,CAAC;AAEH,WAAW,CAAC,WAAW,GAAG,aAAa,CAAC;AAExC,eAAe,WAAW,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport { forwardRef, useImperativeHandle, useRef } from 'react';\n\nimport { ExpoMpvViewProps, ExpoMpvViewRef } from './ExpoMpv.types';\n\nconst NativeView: React.ComponentType<ExpoMpvViewProps & { ref?: React.Ref<any> }> =\n requireNativeView('ExpoMpv');\n\nconst ExpoMpvView = forwardRef<ExpoMpvViewRef, ExpoMpvViewProps>((props, ref) => {\n const nativeRef = useRef<any>(null);\n\n useImperativeHandle(ref, () => ({\n play: () => nativeRef.current?.play(),\n pause: () => nativeRef.current?.pause(),\n togglePlay: () => nativeRef.current?.togglePlay(),\n stop: () => nativeRef.current?.stop(),\n seekTo: (position: number) => nativeRef.current?.seekTo(position),\n seekBy: (offset: number) => nativeRef.current?.seekBy(offset),\n setSpeed: (speed: number) => nativeRef.current?.setSpeed(speed),\n setVolume: (volume: number) => nativeRef.current?.setVolume(volume),\n setMuted: (muted: boolean) => nativeRef.current?.setMuted(muted),\n setSubtitleTrack: (trackId: number) => nativeRef.current?.setSubtitleTrack(trackId),\n setAudioTrack: (trackId: number) => nativeRef.current?.setAudioTrack(trackId),\n getPlaybackInfo: () => nativeRef.current?.getPlaybackInfo(),\n }));\n\n return <NativeView ref={nativeRef} {...props} />;\n});\n\nExpoMpvView.displayName = 'ExpoMpvView';\n\nexport default ExpoMpvView;\n"]}
1
+ {"version":3,"file":"ExpoMpvView.js","sourceRoot":"","sources":["../src/ExpoMpvView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAIhE,MAAM,UAAU,GACd,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAE/B,MAAM,WAAW,GAAG,UAAU,CAAmC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC9E,MAAM,SAAS,GAAG,MAAM,CAAM,IAAI,CAAC,CAAC;IAEpC,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9B,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACrC,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE;QACvC,UAAU,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE;QACjD,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE;QACrC,MAAM,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC;QACjE,MAAM,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QAC7D,QAAQ,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC;QAC/D,SAAS,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC;QACnE,QAAQ,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC;QAChE,gBAAgB,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC;QACnF,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC;QAC7E,WAAW,EAAE,CAAC,IAAY,EAAE,IAAa,EAAE,KAAc,EAAE,IAAa,EAAE,EAAE,CAC1E,SAAS,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC;QACzD,cAAc,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC;QAC/E,eAAe,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE;QAC3D,gBAAgB,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC;QACnF,eAAe,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE;QAC3D,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE;QACrD,kBAAkB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,kBAAkB,EAAE;KAClE,CAAC,CAAC,CAAC;IAEJ,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AACnD,CAAC,CAAC,CAAC;AAEH,WAAW,CAAC,WAAW,GAAG,aAAa,CAAC;AAExC,eAAe,WAAW,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport { forwardRef, useImperativeHandle, useRef } from 'react';\n\nimport { ExpoMpvViewProps, ExpoMpvViewRef } from './ExpoMpv.types';\n\nconst NativeView: React.ComponentType<ExpoMpvViewProps & { ref?: React.Ref<any> }> =\n requireNativeView('ExpoMpv');\n\nconst ExpoMpvView = forwardRef<ExpoMpvViewRef, ExpoMpvViewProps>((props, ref) => {\n const nativeRef = useRef<any>(null);\n\n useImperativeHandle(ref, () => ({\n play: () => nativeRef.current?.play(),\n pause: () => nativeRef.current?.pause(),\n togglePlay: () => nativeRef.current?.togglePlay(),\n stop: () => nativeRef.current?.stop(),\n seekTo: (position: number) => nativeRef.current?.seekTo(position),\n seekBy: (offset: number) => nativeRef.current?.seekBy(offset),\n setSpeed: (speed: number) => nativeRef.current?.setSpeed(speed),\n setVolume: (volume: number) => nativeRef.current?.setVolume(volume),\n setMuted: (muted: boolean) => nativeRef.current?.setMuted(muted),\n setSubtitleTrack: (trackId: number) => nativeRef.current?.setSubtitleTrack(trackId),\n setAudioTrack: (trackId: number) => nativeRef.current?.setAudioTrack(trackId),\n addSubtitle: (path: string, flag?: string, title?: string, lang?: string) =>\n nativeRef.current?.addSubtitle(path, flag, title, lang),\n removeSubtitle: (trackId: number) => nativeRef.current?.removeSubtitle(trackId),\n reloadSubtitles: () => nativeRef.current?.reloadSubtitles(),\n setSubtitleDelay: (seconds: number) => nativeRef.current?.setSubtitleDelay(seconds),\n getPlaybackInfo: () => nativeRef.current?.getPlaybackInfo(),\n getTrackList: () => nativeRef.current?.getTrackList(),\n getCurrentTrackIds: () => nativeRef.current?.getCurrentTrackIds(),\n }));\n\n return <NativeView ref={nativeRef} {...props} />;\n});\n\nExpoMpvView.displayName = 'ExpoMpvView';\n\nexport default ExpoMpvView;\n"]}
package/bun.lock CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "expo-mpv",
@@ -53,4 +53,8 @@ Pod::Spec.new do |s|
53
53
  }
54
54
 
55
55
  s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
56
+
57
+ # Bundle the CJK font for subtitle rendering
58
+ # iOS 18+ system fonts use HVGL format which FreeType/libass cannot parse
59
+ s.resources = ['Fonts/*.otf', 'Fonts/*.ttf']
56
60
  end
@@ -97,9 +97,33 @@ public class ExpoMpvModule: Module {
97
97
  view.setAudioTrack(trackId)
98
98
  }.runOnQueue(.main)
99
99
 
100
+ AsyncFunction("addSubtitle") { (view: ExpoMpvView, path: String, flag: String?, title: String?, lang: String?) in
101
+ view.addSubtitle(path, flag: flag ?? "auto", title: title, lang: lang)
102
+ }.runOnQueue(.main)
103
+
104
+ AsyncFunction("removeSubtitle") { (view: ExpoMpvView, trackId: Int) in
105
+ view.removeSubtitle(trackId)
106
+ }.runOnQueue(.main)
107
+
108
+ AsyncFunction("reloadSubtitles") { (view: ExpoMpvView) in
109
+ view.reloadSubtitles()
110
+ }.runOnQueue(.main)
111
+
112
+ AsyncFunction("setSubtitleDelay") { (view: ExpoMpvView, seconds: Double) in
113
+ view.setSubtitleDelay(seconds)
114
+ }.runOnQueue(.main)
115
+
100
116
  AsyncFunction("getPlaybackInfo") { (view: ExpoMpvView) -> [String: Any] in
101
117
  return view.getPlaybackInfo()
102
118
  }.runOnQueue(.main)
119
+
120
+ AsyncFunction("getTrackList") { (view: ExpoMpvView) -> [[String: Any]] in
121
+ return view.getTrackList()
122
+ }.runOnQueue(.main)
123
+
124
+ AsyncFunction("getCurrentTrackIds") { (view: ExpoMpvView) -> [String: Int] in
125
+ return view.getCurrentTrackIds()
126
+ }.runOnQueue(.main)
103
127
  }
104
128
  }
105
129
  }
@@ -1,5 +1,6 @@
1
1
  import ExpoModulesCore
2
2
  import Libmpv
3
+ import CoreText
3
4
 
4
5
  class ExpoMpvView: ExpoView {
5
6
  // MARK: - Metal Layer
@@ -93,11 +94,22 @@ class ExpoMpvView: ExpoView {
93
94
  var wid = unsafeBitCast(metalLayer, to: Int64.self)
94
95
  checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid), label: "set wid")
95
96
 
96
- // Rendering configuration: gpu-next + vulkan + moltenvk → Metal
97
- setOptionString("vo", "gpu-next")
97
+ // Rendering configuration
98
+ // On simulator: use vo=gpu (old pipeline, avoids libplacebo's pl_tex_upload_pbo
99
+ // which crashes on simulator due to MTLSimDriver XPC shared memory size limits)
100
+ // On device: use vo=gpu-next (libplacebo pipeline, better quality)
101
+ #if targetEnvironment(simulator)
102
+ log("Running on SIMULATOR — using vo=gpu to avoid MTLSimDriver crash")
103
+ setOptionString("vo", "gpu")
98
104
  setOptionString("gpu-api", "vulkan")
99
105
  setOptionString("gpu-context", "moltenvk")
100
106
  setOptionString("hwdec", "videotoolbox-copy")
107
+ #else
108
+ setOptionString("vo", "gpu-next")
109
+ setOptionString("gpu-api", "vulkan")
110
+ setOptionString("gpu-context", "moltenvk")
111
+ setOptionString("hwdec", "videotoolbox")
112
+ #endif
101
113
 
102
114
  // General options
103
115
  setOptionString("keep-open", "yes")
@@ -105,6 +117,11 @@ class ExpoMpvView: ExpoView {
105
117
  setOptionString("input-default-bindings", "no")
106
118
  setOptionString("input-vo-keyboard", "no")
107
119
 
120
+ // Subtitle font configuration
121
+ // iOS has no fontconfig, so libass can't discover system fonts.
122
+ // We use CoreText to find a CJK font file and point libass to it.
123
+ configureFonts(mpv)
124
+
108
125
  // Initialize mpv
109
126
  log("Initializing mpv...")
110
127
  let initResult = mpv_initialize(mpv)
@@ -415,6 +432,34 @@ class ExpoMpvView: ExpoView {
415
432
  setInt("aid", Int64(trackId))
416
433
  }
417
434
 
435
+ /// Load an external subtitle file (local path or URL).
436
+ func addSubtitle(_ path: String, flag: String = "auto", title: String? = nil, lang: String? = nil) {
437
+ guard mpv != nil else { return }
438
+ // sub-add <url> [<flags> [<title> [<lang>]]]
439
+ var args = [path, flag]
440
+ if let title = title { args.append(title) }
441
+ if let lang = lang {
442
+ if args.count == 2 { args.append("") } // placeholder for title
443
+ args.append(lang)
444
+ }
445
+ log("addSubtitle: \(path) flags=\(flag)")
446
+ commandAsync("sub-add", args: args)
447
+ }
448
+
449
+ /// Remove a subtitle track by id.
450
+ func removeSubtitle(_ trackId: Int) {
451
+ commandAsync("sub-remove", args: [String(trackId)])
452
+ }
453
+
454
+ /// Reload current subtitles (useful after font changes).
455
+ func reloadSubtitles() {
456
+ commandAsync("sub-reload")
457
+ }
458
+
459
+ func setSubtitleDelay(_ seconds: Double) {
460
+ setDouble("sub-delay", seconds)
461
+ }
462
+
418
463
  func getPlaybackInfo() -> [String: Any] {
419
464
  let position = getDouble("time-pos")
420
465
  let duration = getDouble("duration")
@@ -433,6 +478,52 @@ class ExpoMpvView: ExpoView {
433
478
  ]
434
479
  }
435
480
 
481
+ func getTrackList() -> [[String: Any]] {
482
+ guard mpv != nil else { return [] }
483
+
484
+ let count = getInt("track-list/count")
485
+ var tracks: [[String: Any]] = []
486
+
487
+ for i in 0..<count {
488
+ let prefix = "track-list/\(i)"
489
+ var track: [String: Any] = [:]
490
+
491
+ track["id"] = Int(getInt("\(prefix)/id"))
492
+ track["type"] = getString("\(prefix)/type") ?? "unknown"
493
+ track["title"] = getString("\(prefix)/title") ?? ""
494
+ track["lang"] = getString("\(prefix)/lang") ?? ""
495
+ track["codec"] = getString("\(prefix)/codec") ?? ""
496
+ track["selected"] = getFlag("\(prefix)/selected")
497
+ track["isDefault"] = getFlag("\(prefix)/default")
498
+ track["isExternal"] = getFlag("\(prefix)/external")
499
+
500
+ // Extra info based on track type
501
+ let trackType = track["type"] as? String ?? ""
502
+ if trackType == "audio" {
503
+ track["channelCount"] = Int(getInt("\(prefix)/demux-channel-count"))
504
+ track["sampleRate"] = Int(getInt("\(prefix)/demux-samplerate"))
505
+ } else if trackType == "video" {
506
+ track["width"] = Int(getInt("\(prefix)/demux-w"))
507
+ track["height"] = Int(getInt("\(prefix)/demux-h"))
508
+ track["fps"] = getDouble("\(prefix)/demux-fps")
509
+ }
510
+
511
+ tracks.append(track)
512
+ }
513
+
514
+ log("getTrackList: \(tracks.count) tracks found")
515
+ return tracks
516
+ }
517
+
518
+ func getCurrentTrackIds() -> [String: Int] {
519
+ guard mpv != nil else { return [:] }
520
+ return [
521
+ "vid": Int(getInt("vid")),
522
+ "aid": Int(getInt("aid")),
523
+ "sid": Int(getInt("sid")),
524
+ ]
525
+ }
526
+
436
527
  func destroy() {
437
528
  stopProgressTimer()
438
529
  if let mpv = mpv {
@@ -443,6 +534,79 @@ class ExpoMpvView: ExpoView {
443
534
  }
444
535
  }
445
536
 
537
+ // MARK: - Font Configuration
538
+
539
+ /// Configure fonts for subtitle rendering.
540
+ ///
541
+ /// iOS 18+ changed system fonts (PingFang etc.) to Apple's HVGL variable font format,
542
+ /// which FreeType (used by libass) cannot parse. This means system CJK fonts are
543
+ /// unusable by libass even if CoreText can find them.
544
+ ///
545
+ /// Solution: bundle a standard OTF/TTF CJK font (Noto Sans CJK SC) that FreeType
546
+ /// can read, and point libass to it via sub-fonts-dir.
547
+ /// See: https://github.com/libass/libass/issues/912
548
+ /// https://github.com/mpv-player/mpv/issues/14878
549
+ private func configureFonts(_ mpv: OpaquePointer) {
550
+ // Locate the bundled Noto Sans CJK SC font in the module's bundle
551
+ let fontFileName = "NotoSansCJKsc-Regular"
552
+ let fontFileExt = "otf"
553
+
554
+ // Search in all bundles (the font is in the ExpoMpv pod bundle)
555
+ var fontPath: String?
556
+ for bundle in Bundle.allBundles {
557
+ if let path = bundle.path(forResource: fontFileName, ofType: fontFileExt) {
558
+ fontPath = path
559
+ break
560
+ }
561
+ }
562
+
563
+ // Also check the main bundle's Frameworks
564
+ if fontPath == nil {
565
+ let frameworksPath = Bundle.main.bundlePath + "/Frameworks"
566
+ if let contents = try? FileManager.default.contentsOfDirectory(atPath: frameworksPath) {
567
+ for item in contents where item.hasSuffix(".framework") {
568
+ let bundlePath = frameworksPath + "/" + item
569
+ if let bundle = Bundle(path: bundlePath),
570
+ let path = bundle.path(forResource: fontFileName, ofType: fontFileExt) {
571
+ fontPath = path
572
+ break
573
+ }
574
+ }
575
+ }
576
+ }
577
+
578
+ guard let resolvedFontPath = fontPath else {
579
+ log("WARNING: Bundled font \(fontFileName).\(fontFileExt) not found in any bundle")
580
+ // Fallback: try auto font provider without bundled font
581
+ setOptionString("sub-font-provider", "auto")
582
+ setOptionString("sub-font", "sans-serif")
583
+ return
584
+ }
585
+
586
+ let fontsDir = (resolvedFontPath as NSString).deletingLastPathComponent
587
+ log("Font: \(resolvedFontPath)")
588
+
589
+ // Point libass to the directory containing our bundled font
590
+ setOptionString("sub-fonts-dir", fontsDir)
591
+
592
+ // auto = CoreText on Apple platforms (handles font name matching + fallback)
593
+ setOptionString("sub-font-provider", "auto")
594
+
595
+ // Default font for SRT / plain text subtitles
596
+ setOptionString("sub-font", "Noto Sans CJK SC")
597
+ setOptionString("sub-font-size", "40")
598
+ setOptionString("sub-codepage", "auto")
599
+
600
+ // ASS subtitles: don't force-override styles.
601
+ // When ASS references fonts like "Microsoft YaHei" that don't exist on iOS,
602
+ // CoreText + our bundled font provide fallback.
603
+ setOptionString("sub-ass-override", "no")
604
+ setOptionString("sub-ass-shaper", "simple")
605
+
606
+ // Auto-load external subtitles from same directory as video
607
+ setOptionString("sub-auto", "fuzzy")
608
+ }
609
+
446
610
  // MARK: - MPV Helpers
447
611
 
448
612
  private func commandAsync(_ command: String, args: [String] = []) {
@@ -501,6 +665,14 @@ class ExpoMpvView: ExpoView {
501
665
  return data
502
666
  }
503
667
 
668
+ private func getString(_ name: String) -> String? {
669
+ guard mpv != nil else { return nil }
670
+ let cstr = mpv_get_property_string(mpv, name)
671
+ defer { mpv_free(cstr) }
672
+ guard let cstr = cstr else { return nil }
673
+ return String(cString: cstr)
674
+ }
675
+
504
676
  private func getInt(_ name: String) -> Int64 {
505
677
  guard mpv != nil else { return 0 }
506
678
  var data: Int64 = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-mpv",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "My new module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -47,6 +47,30 @@ export type PlaybackInfo = {
47
47
  muted: boolean;
48
48
  };
49
49
 
50
+ export type TrackInfo = {
51
+ id: number;
52
+ type: 'video' | 'audio' | 'sub' | string;
53
+ title: string;
54
+ lang: string;
55
+ codec: string;
56
+ selected: boolean;
57
+ isDefault: boolean;
58
+ isExternal: boolean;
59
+ // audio-specific
60
+ channelCount?: number;
61
+ sampleRate?: number;
62
+ // video-specific
63
+ width?: number;
64
+ height?: number;
65
+ fps?: number;
66
+ };
67
+
68
+ export type CurrentTrackIds = {
69
+ vid: number;
70
+ aid: number;
71
+ sid: number;
72
+ };
73
+
50
74
  // MARK: - Module Events (non-view)
51
75
 
52
76
  export type ExpoMpvModuleEvents = {};
@@ -141,5 +165,15 @@ export type ExpoMpvViewRef = {
141
165
  setMuted: (muted: boolean) => Promise<void>;
142
166
  setSubtitleTrack: (trackId: number) => Promise<void>;
143
167
  setAudioTrack: (trackId: number) => Promise<void>;
168
+ /** Load an external subtitle file (local path or URL). */
169
+ addSubtitle: (path: string, flag?: string, title?: string, lang?: string) => Promise<void>;
170
+ /** Remove a subtitle track by ID. */
171
+ removeSubtitle: (trackId: number) => Promise<void>;
172
+ /** Reload current subtitles. */
173
+ reloadSubtitles: () => Promise<void>;
174
+ /** Set subtitle delay in seconds (positive = later, negative = earlier). */
175
+ setSubtitleDelay: (seconds: number) => Promise<void>;
144
176
  getPlaybackInfo: () => Promise<PlaybackInfo>;
177
+ getTrackList: () => Promise<TrackInfo[]>;
178
+ getCurrentTrackIds: () => Promise<CurrentTrackIds>;
145
179
  };
@@ -22,7 +22,14 @@ const ExpoMpvView = forwardRef<ExpoMpvViewRef, ExpoMpvViewProps>((props, ref) =>
22
22
  setMuted: (muted: boolean) => nativeRef.current?.setMuted(muted),
23
23
  setSubtitleTrack: (trackId: number) => nativeRef.current?.setSubtitleTrack(trackId),
24
24
  setAudioTrack: (trackId: number) => nativeRef.current?.setAudioTrack(trackId),
25
+ addSubtitle: (path: string, flag?: string, title?: string, lang?: string) =>
26
+ nativeRef.current?.addSubtitle(path, flag, title, lang),
27
+ removeSubtitle: (trackId: number) => nativeRef.current?.removeSubtitle(trackId),
28
+ reloadSubtitles: () => nativeRef.current?.reloadSubtitles(),
29
+ setSubtitleDelay: (seconds: number) => nativeRef.current?.setSubtitleDelay(seconds),
25
30
  getPlaybackInfo: () => nativeRef.current?.getPlaybackInfo(),
31
+ getTrackList: () => nativeRef.current?.getTrackList(),
32
+ getCurrentTrackIds: () => nativeRef.current?.getCurrentTrackIds(),
26
33
  }));
27
34
 
28
35
  return <NativeView ref={nativeRef} {...props} />;