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 +21 -0
- package/README.md +145 -14
- package/build/ExpoMpv.types.d.ts +30 -0
- package/build/ExpoMpv.types.d.ts.map +1 -1
- package/build/ExpoMpv.types.js.map +1 -1
- package/build/ExpoMpvView.d.ts.map +1 -1
- package/build/ExpoMpvView.js +6 -0
- package/build/ExpoMpvView.js.map +1 -1
- package/bun.lock +1 -0
- package/ios/ExpoMpv.podspec +4 -0
- package/ios/ExpoMpvModule.swift +24 -0
- package/ios/ExpoMpvView.swift +174 -2
- package/ios/Fonts/NotoSansCJKsc-Regular.otf +0 -0
- package/package.json +1 -1
- package/src/ExpoMpv.types.ts +34 -0
- package/src/ExpoMpvView.tsx +7 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
## Features
|
|
9
8
|
|
|
10
|
-
|
|
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
|
-
|
|
18
|
+
## Installation
|
|
13
19
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
## License
|
|
32
163
|
|
|
33
|
-
|
|
164
|
+
[MIT](./LICENSE)
|
package/build/ExpoMpv.types.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/build/ExpoMpvView.js
CHANGED
|
@@ -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
|
});
|
package/build/ExpoMpvView.js.map
CHANGED
|
@@ -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;
|
|
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
package/ios/ExpoMpv.podspec
CHANGED
package/ios/ExpoMpvModule.swift
CHANGED
|
@@ -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
|
}
|
package/ios/ExpoMpvView.swift
CHANGED
|
@@ -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
|
|
97
|
-
|
|
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
|
|
Binary file
|
package/package.json
CHANGED
package/src/ExpoMpv.types.ts
CHANGED
|
@@ -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
|
};
|
package/src/ExpoMpvView.tsx
CHANGED
|
@@ -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} />;
|