mega-stream-proxy 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -49
- package/dist/megaFile.js +22 -9
- package/dist/{Player/playerAndroid/MegaVideoPlayerAndroid.d.ts → player/MegaVideoPlayer.d.ts} +4 -13
- package/dist/player/MegaVideoPlayer.js +307 -0
- package/dist/{Player/playerweb → player}/PlayerControls.d.ts +2 -14
- package/dist/player/PlayerControls.js +289 -0
- package/dist/{Player/playerweb → player}/SubtitleDisplay.d.ts +3 -3
- package/dist/{Player/playerweb → player}/SubtitleDisplay.js +1 -1
- package/dist/player/index.d.ts +3 -0
- package/dist/player/index.js +3 -0
- package/dist/{Player/playerweb/MegaVideoPlayer.d.ts → playerweb/MegaVideoPlayerWeb.d.ts} +6 -18
- package/dist/{Player/playerweb/MegaVideoPlayer.js → playerweb/MegaVideoPlayerWeb.js} +18 -18
- package/dist/{Player/playerAndroid/PlayerControlsAndroid.d.ts → playerweb/PlayerControlsWeb.d.ts} +2 -16
- package/dist/playerweb/PlayerControlsWeb.js +289 -0
- package/dist/playerweb/SubtitleDisplayWeb.d.ts +15 -0
- package/dist/playerweb/SubtitleDisplayWeb.js +32 -0
- package/dist/playerweb/index.d.ts +3 -0
- package/dist/playerweb/index.js +3 -0
- package/dist/proxy/native.js +11 -3
- package/expo-module.config.json +3 -0
- package/package.json +15 -18
- package/src/megaFile.ts +28 -15
- package/src/player/MegaVideoPlayer.tsx +474 -0
- package/src/{Player/playerweb → player}/PlayerControls.tsx +200 -369
- package/src/{Player/playerweb → player}/SubtitleDisplay.tsx +3 -3
- package/src/player/index.ts +7 -0
- package/src/{Player/playerweb/MegaVideoPlayer.tsx → playerweb/MegaVideoPlayerWeb.tsx} +41 -58
- package/src/playerweb/PlayerControlsWeb.tsx +629 -0
- package/src/playerweb/SubtitleDisplayWeb.tsx +48 -0
- package/src/playerweb/index.ts +3 -0
- package/src/proxy/native.ts +10 -3
- package/dist/Player/native.d.ts +0 -3
- package/dist/Player/native.js +0 -3
- package/dist/Player/playerAndroid/MegaVideoPlayerAndroid.js +0 -624
- package/dist/Player/playerAndroid/PlayerControlsAndroid.js +0 -370
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.d.ts +0 -21
- package/dist/Player/playerAndroid/SubtitleDisplayAndroid.js +0 -55
- package/dist/Player/playerAndroid/index.d.ts +0 -3
- package/dist/Player/playerAndroid/index.js +0 -3
- package/dist/Player/playerweb/PlayerControls.js +0 -346
- package/dist/Player/playerweb/index.d.ts +0 -3
- package/dist/Player/playerweb/index.js +0 -3
- package/dist/Player/web.d.ts +0 -3
- package/dist/Player/web.js +0 -3
- package/src/Player/playerAndroid/MegaVideoPlayerAndroid.tsx +0 -825
- package/src/Player/playerAndroid/PlayerControlsAndroid.tsx +0 -824
- package/src/Player/playerAndroid/SubtitleDisplayAndroid.tsx +0 -80
- package/src/Player/playerAndroid/index.ts +0 -7
- package/src/Player/playerweb/index.ts +0 -3
package/README.md
CHANGED
|
@@ -34,11 +34,15 @@ mega-link ──> megajs File.fromURL ──┬─> Android/iOS: 127.0.0.1:PORT/
|
|
|
34
34
|
|
|
35
35
|
- **Web**: a modern browser at `https:` (or `localhost`) for Service Worker
|
|
36
36
|
streaming; Node 18+ for the `copySwAssets` deploy step.
|
|
37
|
-
- **Android / iOS**: React Native ≥ 0.71 (Expo SDK 52
|
|
37
|
+
- **Android / iOS**: React Native ≥ 0.71 (Expo SDK 52 recommended) — the native
|
|
38
38
|
streaming module is auto-linked, no manual Gradle/Xcode edits.
|
|
39
39
|
- All platforms need `react`, `react-native`, `expo-video`, and `@expo/vector-icons`
|
|
40
40
|
(Expo apps already ship the last two).
|
|
41
41
|
|
|
42
|
+
## for sdk 54 + support versions
|
|
43
|
+
visit url https://www.npmjs.com/package/mega-proxy?activeTab=readme
|
|
44
|
+
|
|
45
|
+
|
|
42
46
|
## Install
|
|
43
47
|
|
|
44
48
|
```sh
|
|
@@ -69,7 +73,7 @@ your Expo app already provides them.
|
|
|
69
73
|
import { useEffect, useMemo } from "react";
|
|
70
74
|
import { View } from "react-native";
|
|
71
75
|
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/native";
|
|
72
|
-
import {
|
|
76
|
+
import { MegaVideoPlayer } from "mega-stream-proxy/player";
|
|
73
77
|
|
|
74
78
|
export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
75
79
|
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
|
|
@@ -93,51 +97,12 @@ export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
|
93
97
|
|
|
94
98
|
return (
|
|
95
99
|
<View style={{ flex: 1 }}>
|
|
96
|
-
<
|
|
100
|
+
<MegaVideoPlayer uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
|
|
97
101
|
</View>
|
|
98
102
|
);
|
|
99
103
|
}
|
|
100
104
|
```
|
|
101
105
|
|
|
102
|
-
## all Platfroms (streaming)
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
import { useEffect, useMemo } from "react";
|
|
106
|
-
import { View, Platform } from "react-native";
|
|
107
|
-
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/native";
|
|
108
|
-
import { MegaVideoPlayerAndroid } from "mega-stream-proxy/playerAndroid";
|
|
109
|
-
import { MegaVideoPlayerWeb } from "mega-stream-proxy/player";
|
|
110
|
-
export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
111
|
-
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
let alive = true;
|
|
115
|
-
void ensureProxy(); // idempotent — starts the singleton server once per app lifetime
|
|
116
|
-
createStream(megaUrl).then(({ url, sessionId }) => {
|
|
117
|
-
if (!alive) {
|
|
118
|
-
releaseStream(sessionId);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
ref.url = url;
|
|
122
|
-
ref.sessionId = sessionId;
|
|
123
|
-
});
|
|
124
|
-
return () => {
|
|
125
|
-
alive = false;
|
|
126
|
-
if (ref.sessionId) releaseStream(ref.sessionId);
|
|
127
|
-
};
|
|
128
|
-
}, [megaUrl]);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<View>
|
|
132
|
-
{Platform.OS === "web" ? (
|
|
133
|
-
<MegaVideoPlayerWeb uri={ref.url} subtitleUrl={subtitleUrl} />
|
|
134
|
-
) : (
|
|
135
|
-
<MegaVideoPlayerAndroid uri={ref.url} subtitleUrl={subtitleUrl} />
|
|
136
|
-
)}
|
|
137
|
-
</View>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
106
|
- `ensureProxy(): Promise<void>` — idempotent; binds the singleton local server once
|
|
142
107
|
per app lifetime. Safe to call repeatedly; no-op until `stopProxy()`.
|
|
143
108
|
- `createStream(megaUrl): Promise<{ url, sessionId }>` — one keyed session per MEGA
|
|
@@ -175,7 +140,7 @@ Server from a host that allows a Service Worker (`https:` or `localhost`).
|
|
|
175
140
|
import { useEffect, useMemo } from "react";
|
|
176
141
|
import { View } from "react-native";
|
|
177
142
|
import { ensureProxy, createStream, releaseStream } from "mega-stream-proxy/web";
|
|
178
|
-
import {
|
|
143
|
+
import { MegaVideoPlayer } from "mega-stream-proxy/player";
|
|
179
144
|
|
|
180
145
|
export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
181
146
|
const ref = useMemo(() => ({ url: null as string | null, sessionId: null as string | null }), []);
|
|
@@ -199,7 +164,7 @@ export function WatchScreen({ megaUrl, subtitleUrl }) {
|
|
|
199
164
|
|
|
200
165
|
return (
|
|
201
166
|
<View style={{ flex: 1 }}>
|
|
202
|
-
<
|
|
167
|
+
<MegaVideoPlayer uri={ref.url} subtitleUrl={subtitleUrl} autoPlay />
|
|
203
168
|
</View>
|
|
204
169
|
);
|
|
205
170
|
}
|
|
@@ -309,9 +274,9 @@ The same pattern applies to multi-part episodes: `createStreams(megaUrls)` retur
|
|
|
309
274
|
|
|
310
275
|
## Player component
|
|
311
276
|
|
|
312
|
-
`import {
|
|
277
|
+
`import { MegaVideoPlayer, PlayerControls, SubtitleDisplay } from "mega-stream-proxy/player";`
|
|
313
278
|
|
|
314
|
-
This is the **same player MyMovies uses** (`components/
|
|
279
|
+
This is the **same player MyMovies uses** (`components/MegaVideoPlayer.web.tsx`),
|
|
315
280
|
byte-for-byte — same expo-video engine, same custom controls (scrub bar, volume,
|
|
316
281
|
skip ±10s, subtitle picker + text-size slider, intro/outro regions), same subtitle
|
|
317
282
|
overlay style, same auto-hiding chrome, and the same multi-part advance logic. It
|
|
@@ -320,7 +285,7 @@ runs identically on Android, iOS and web.
|
|
|
320
285
|
### Simple usage — pass the subtitle URL as a prop
|
|
321
286
|
|
|
322
287
|
```tsx
|
|
323
|
-
<
|
|
288
|
+
<MegaVideoPlayer
|
|
324
289
|
uri="https://…/part-0.mp4" // or the proxy URL from /native or /web
|
|
325
290
|
title="Episode 1"
|
|
326
291
|
subtitleUrl="https://cdn.example.com/subs.srt" // <-- the subtitle file
|
|
@@ -343,7 +308,7 @@ The player shows a subtitle selector in the controls (same UI as MyMovies):
|
|
|
343
308
|
tabs: **Language** (the subtitle list) and **Text Size**.
|
|
344
309
|
|
|
345
310
|
```tsx
|
|
346
|
-
<
|
|
311
|
+
<MegaVideoPlayer
|
|
347
312
|
uri={url}
|
|
348
313
|
subtitleTracks={[
|
|
349
314
|
{ lang: "English", url: "https://cdn.example.com/subs.en.srt" },
|
|
@@ -376,7 +341,7 @@ Format auto-detection order: `subtitleFormat` prop → URL extension → content
|
|
|
376
341
|
### Multi-language tracks
|
|
377
342
|
|
|
378
343
|
```tsx
|
|
379
|
-
<
|
|
344
|
+
<MegaVideoPlayer
|
|
380
345
|
uri={url}
|
|
381
346
|
subtitleTracks={[
|
|
382
347
|
{ lang: "English", url: "https://cdn.example.com/subs.srt" },
|
package/dist/megaFile.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
const ATTRS_TIMEOUT_MS = 30_000;
|
|
2
|
+
const ATTRS_RETRIES = 2;
|
|
2
3
|
/** Lazily resolves megajs `File.fromURL` + attributes (incl. folder-link child). */
|
|
3
4
|
export async function loadMegaFile(url) {
|
|
4
5
|
const { File } = (await import("megajs"));
|
|
5
6
|
const file = File.fromURL(url.trim());
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
// MEGA API calls can fail transiently (dns/rate-limit/network blip). Retry a
|
|
8
|
+
// couple of times before surfacing "MEGA did not respond."
|
|
9
|
+
let lastError;
|
|
10
|
+
for (let attempt = 1; attempt <= ATTRS_RETRIES; attempt++) {
|
|
11
|
+
try {
|
|
12
|
+
await withTimeout(file.loadAttributes(), ATTRS_TIMEOUT_MS, "MEGA did not respond. Check your connection and try again.");
|
|
13
|
+
// Folder links: loadAttributes resolves to the folder itself — pick the
|
|
14
|
+
// first non-directory child instead (mirrors the megajs docs sample).
|
|
15
|
+
const group = file;
|
|
16
|
+
if (group.children?.length) {
|
|
17
|
+
const child = group.find?.((n) => !n.children, true);
|
|
18
|
+
if (child)
|
|
19
|
+
return child;
|
|
20
|
+
}
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
lastError = e;
|
|
25
|
+
console.warn(`[megaStream] loadAttributes attempt ${attempt}/${ATTRS_RETRIES} failed`);
|
|
26
|
+
}
|
|
14
27
|
}
|
|
15
|
-
|
|
28
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
16
29
|
}
|
|
17
30
|
export function fileSizeOf(file) {
|
|
18
31
|
return Number(file.size ?? 0);
|
package/dist/{Player/playerAndroid/MegaVideoPlayerAndroid.d.ts → player/MegaVideoPlayer.d.ts}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { type SubtitleFormat, type SubtitleTrack } from "
|
|
3
|
-
export type
|
|
2
|
+
import { type SubtitleFormat, type SubtitleTrack } from "../subtitles/index.js";
|
|
3
|
+
export type MegaVideoPlayerProps = {
|
|
4
4
|
/** A playable URL — a direct stream or the first part URL. */
|
|
5
5
|
uri?: string | null;
|
|
6
6
|
/** Ordered additional part URLs for multi-part episodes, played in sequence. */
|
|
@@ -18,13 +18,6 @@ export type MegaVideoPlayerAndroidProps = {
|
|
|
18
18
|
initialSubtitleLang?: string | null;
|
|
19
19
|
/** shift the cue timestamps by an offset in seconds (can be negative) */
|
|
20
20
|
subtitleOffset?: number;
|
|
21
|
-
/** resolution labels in display order — the FIRST is the default source
|
|
22
|
-
* (usually "HD: <url>"), e.g. ["Default", "1080p", "720p"]. */
|
|
23
|
-
resolutions?: string[];
|
|
24
|
-
/** currently active resolution label */
|
|
25
|
-
resolution?: string;
|
|
26
|
-
/** user picked a resolution label — resolve it to a URL and update `uri`. */
|
|
27
|
-
onSelectResolution?: (label: string) => void;
|
|
28
21
|
autoPlay?: boolean;
|
|
29
22
|
onEnd?: () => void;
|
|
30
23
|
onError?: (message: string) => void;
|
|
@@ -33,10 +26,8 @@ export type MegaVideoPlayerAndroidProps = {
|
|
|
33
26
|
duration: number;
|
|
34
27
|
}) => void;
|
|
35
28
|
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
36
|
-
width?: number;
|
|
37
|
-
height?: number;
|
|
38
29
|
};
|
|
39
|
-
export type
|
|
30
|
+
export type MegaVideoPlayerHandle = {
|
|
40
31
|
/** Loads a new playable URL into the player. */
|
|
41
32
|
add: (source: string, opts?: {
|
|
42
33
|
title?: string;
|
|
@@ -44,4 +35,4 @@ export type MegaVideoPlayerAndroidHandle = {
|
|
|
44
35
|
/** Stops playback and clears the current source. */
|
|
45
36
|
stop: () => void;
|
|
46
37
|
};
|
|
47
|
-
export declare const
|
|
38
|
+
export declare const MegaVideoPlayer: React.ForwardRefExoticComponent<MegaVideoPlayerProps & React.RefAttributes<MegaVideoPlayerHandle>>;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The exact MyMovies episode player (components/MegaVideoPlayer.web.tsx),
|
|
3
|
+
// packaged as a cross-platform component (Android / iOS / web). Driven by the
|
|
4
|
+
// react-native-video `<Video>` component, with ordered part URLs played
|
|
5
|
+
// back-to-back and auto-advancing to the next part when the current one ends.
|
|
6
|
+
// Sources come in as proxy URLs — from `mega-stream-proxy/native`
|
|
7
|
+
// (http://127.0.0.1:<port>/…) on Android/iOS or from the Service Worker
|
|
8
|
+
// (`/__mmstream__/…`) on the web.
|
|
9
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useState, } from "react";
|
|
10
|
+
import { ActivityIndicator, StyleSheet, Text, View, } from "react-native";
|
|
11
|
+
import { Video } from "react-native-video";
|
|
12
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
13
|
+
import { PlayerControls } from "./PlayerControls.js";
|
|
14
|
+
import { SubtitleDisplay } from "./SubtitleDisplay.js";
|
|
15
|
+
import { loadSubtitles, } from "../subtitles/index.js";
|
|
16
|
+
export const MegaVideoPlayer = forwardRef(function MegaVideoPlayer({ uri, sources, title, subtitleUrl, subtitleFormat, subtitleLang, subtitleTracks: subtitleTracksProp, initialSubtitleLang, subtitleOffset, autoPlay = true, onEnd, onError, onProgress, onFullscreenChange, }, ref) {
|
|
17
|
+
const [playing, setPlaying] = useState(false);
|
|
18
|
+
const [buffering, setBuffering] = useState(true);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
21
|
+
const [duration, setDuration] = useState(0);
|
|
22
|
+
const [muted, setMuted] = useState(false);
|
|
23
|
+
const [volume, setVolume] = useState(1);
|
|
24
|
+
const [visible, setVisible] = useState(true);
|
|
25
|
+
const [subtitlesOn, setSubtitlesOn] = useState(true);
|
|
26
|
+
const [subFontSize, setSubFontSize] = useState(14);
|
|
27
|
+
const [cues, setCues] = useState([]);
|
|
28
|
+
const [subtitleError, setSubtitleError] = useState(null);
|
|
29
|
+
const [error, setError] = useState(null);
|
|
30
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
31
|
+
const [sourceUri, setSourceUri] = useState(null);
|
|
32
|
+
const [tracks, setTracks] = useState([]);
|
|
33
|
+
const videoRef = React.useRef(null);
|
|
34
|
+
const mountedRef = React.useRef(true);
|
|
35
|
+
const autoplayRef = React.useRef(autoPlay !== false);
|
|
36
|
+
const hideTimerRef = React.useRef(null);
|
|
37
|
+
const subTrackIdxRef = React.useRef(0);
|
|
38
|
+
const sourceIndexRef = React.useRef(Math.max(0, (sources ?? []).indexOf(uri ?? "")));
|
|
39
|
+
// ---- source management ----
|
|
40
|
+
async function add(source, opts) {
|
|
41
|
+
if (!source)
|
|
42
|
+
return;
|
|
43
|
+
setError(null);
|
|
44
|
+
setLoading(true);
|
|
45
|
+
setBuffering(true);
|
|
46
|
+
setSourceUri(source);
|
|
47
|
+
setCurrentTime(0);
|
|
48
|
+
setDuration(0);
|
|
49
|
+
if (!mountedRef.current)
|
|
50
|
+
return;
|
|
51
|
+
console.log(`[MegaVideoPlayer] add: ${source} (${opts?.title ?? title})`);
|
|
52
|
+
if (autoplayRef.current) {
|
|
53
|
+
setPlaying(true);
|
|
54
|
+
scheduleHide();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function stop() {
|
|
58
|
+
setSourceUri(null);
|
|
59
|
+
setPlaying(false);
|
|
60
|
+
}
|
|
61
|
+
useImperativeHandle(ref, () => ({ add, stop }), [add, stop]);
|
|
62
|
+
// ---- player controllers ----
|
|
63
|
+
const playPause = () => {
|
|
64
|
+
setPlaying((p) => !p);
|
|
65
|
+
setVisible(true);
|
|
66
|
+
};
|
|
67
|
+
const skip = (delta) => {
|
|
68
|
+
const next = Math.min(Math.max(currentTime + delta, 0), duration);
|
|
69
|
+
videoRef.current?.seek(next);
|
|
70
|
+
setCurrentTime(next);
|
|
71
|
+
setVisible(true);
|
|
72
|
+
scheduleHide();
|
|
73
|
+
};
|
|
74
|
+
const seek = (seconds) => {
|
|
75
|
+
const clamped = Math.min(Math.max(seconds, 0), duration || seconds);
|
|
76
|
+
videoRef.current?.seek(clamped);
|
|
77
|
+
setCurrentTime(clamped);
|
|
78
|
+
setVisible(true);
|
|
79
|
+
scheduleHide();
|
|
80
|
+
};
|
|
81
|
+
const toggleMute = () => {
|
|
82
|
+
setMuted((m) => !m);
|
|
83
|
+
setVisible(true);
|
|
84
|
+
};
|
|
85
|
+
const changeVolume = (v) => {
|
|
86
|
+
setVolume(v);
|
|
87
|
+
if (v > 0)
|
|
88
|
+
setMuted(false);
|
|
89
|
+
};
|
|
90
|
+
const toggleSubtitles = () => {
|
|
91
|
+
setSubtitlesOn((s) => !s);
|
|
92
|
+
};
|
|
93
|
+
const toggleControls = () => {
|
|
94
|
+
setVisible((v) => !v);
|
|
95
|
+
if (!visible && playing)
|
|
96
|
+
scheduleHide();
|
|
97
|
+
};
|
|
98
|
+
const enterFullscreen = (next) => {
|
|
99
|
+
// PlayerControls calls through onPress, which passes the press event as
|
|
100
|
+
// the first argument — ignore it (it is not the desired fullscreen value).
|
|
101
|
+
const target = typeof next === "boolean" ? next : !fullscreen;
|
|
102
|
+
setFullscreen(target);
|
|
103
|
+
setVisible(true);
|
|
104
|
+
onFullscreenChange?.(target);
|
|
105
|
+
};
|
|
106
|
+
const changeSubtitleFontSize = (px) => {
|
|
107
|
+
setSubFontSize(px);
|
|
108
|
+
};
|
|
109
|
+
const selectSubtitleTrack = (lang) => {
|
|
110
|
+
const idx = tracks.findIndex((t) => t.lang === lang);
|
|
111
|
+
if (idx >= 0) {
|
|
112
|
+
subTrackIdxRef.current = idx;
|
|
113
|
+
setSubtitlesOn(true);
|
|
114
|
+
loadCues(tracks);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
// ---- react-native-video handlers ----
|
|
118
|
+
const onVideoLoad = (data) => {
|
|
119
|
+
const d = data.duration ?? duration;
|
|
120
|
+
setDuration(d);
|
|
121
|
+
setBuffering(false);
|
|
122
|
+
setLoading(false);
|
|
123
|
+
console.log(`[MegaVideoPlayer] onLoad duration=${d}`);
|
|
124
|
+
};
|
|
125
|
+
const onVideoProgress = (data) => {
|
|
126
|
+
const t = data.currentTime ?? 0;
|
|
127
|
+
setCurrentTime(t);
|
|
128
|
+
onProgress?.({ currentTime: t, duration });
|
|
129
|
+
};
|
|
130
|
+
const onVideoError = (e) => {
|
|
131
|
+
if (!mountedRef.current)
|
|
132
|
+
return;
|
|
133
|
+
const message = e?.error?.message || e?.error?.errorString || String(e) || "Video error";
|
|
134
|
+
console.warn("[MegaVideoPlayer] onError:", message);
|
|
135
|
+
setError(message);
|
|
136
|
+
setBuffering(false);
|
|
137
|
+
setLoading(false);
|
|
138
|
+
onError?.(message);
|
|
139
|
+
};
|
|
140
|
+
const onVideoEnd = () => {
|
|
141
|
+
const list = sources ?? [];
|
|
142
|
+
const next = list[sourceIndexRef.current + 1];
|
|
143
|
+
if (next) {
|
|
144
|
+
sourceIndexRef.current += 1;
|
|
145
|
+
console.log(`[MegaVideoPlayer] advancing to part ${sourceIndexRef.current + 1}/${list.length}`);
|
|
146
|
+
void add(next, { title });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
setPlaying(false);
|
|
150
|
+
setVisible(true);
|
|
151
|
+
onEnd?.();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
// ---- helpers ----
|
|
155
|
+
function scheduleHide() {
|
|
156
|
+
if (hideTimerRef.current)
|
|
157
|
+
clearTimeout(hideTimerRef.current);
|
|
158
|
+
hideTimerRef.current = setTimeout(() => {
|
|
159
|
+
setPlaying((p) => {
|
|
160
|
+
if (p)
|
|
161
|
+
setVisible(false);
|
|
162
|
+
return p;
|
|
163
|
+
});
|
|
164
|
+
}, 3000);
|
|
165
|
+
}
|
|
166
|
+
function loadCues(trackList = tracks) {
|
|
167
|
+
const track = trackList[Math.min(subTrackIdxRef.current, trackList.length - 1)] ?? null;
|
|
168
|
+
setCues([]);
|
|
169
|
+
setSubtitleError(null);
|
|
170
|
+
if (!track)
|
|
171
|
+
return;
|
|
172
|
+
loadSubtitles(track.url, { format: track.format ?? undefined })
|
|
173
|
+
.then((c) => {
|
|
174
|
+
if (!mountedRef.current)
|
|
175
|
+
return;
|
|
176
|
+
setCues(c);
|
|
177
|
+
setSubtitleError(c.length === 0 ? "No cues parsed from this subtitle file" : null);
|
|
178
|
+
})
|
|
179
|
+
.catch((e) => {
|
|
180
|
+
console.warn("[subtitles] load failed:", track.url, e);
|
|
181
|
+
if (mountedRef.current)
|
|
182
|
+
setSubtitleError("Subtitles unavailable");
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function syncSubtitleTracks() {
|
|
186
|
+
const nextTracks = subtitleTracksProp?.length
|
|
187
|
+
? subtitleTracksProp
|
|
188
|
+
: subtitleUrl
|
|
189
|
+
? [{ lang: subtitleLang ?? "SRT", url: subtitleUrl, format: subtitleFormat }]
|
|
190
|
+
: [];
|
|
191
|
+
setTracks(nextTracks);
|
|
192
|
+
// Preselect the requested track (from the watch screen's subtitle picker)
|
|
193
|
+
if (initialSubtitleLang) {
|
|
194
|
+
const idx = nextTracks.findIndex((t) => t.lang === initialSubtitleLang);
|
|
195
|
+
if (idx >= 0)
|
|
196
|
+
subTrackIdxRef.current = idx;
|
|
197
|
+
}
|
|
198
|
+
loadCues(nextTracks);
|
|
199
|
+
}
|
|
200
|
+
// ---- effects ----
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
mountedRef.current = true;
|
|
203
|
+
autoplayRef.current = autoPlay !== false;
|
|
204
|
+
return () => {
|
|
205
|
+
mountedRef.current = false;
|
|
206
|
+
if (hideTimerRef.current)
|
|
207
|
+
clearTimeout(hideTimerRef.current);
|
|
208
|
+
};
|
|
209
|
+
}, [autoPlay]);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
syncSubtitleTracks();
|
|
212
|
+
if (uri)
|
|
213
|
+
void add(uri, { title });
|
|
214
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
215
|
+
}, []);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!uri)
|
|
218
|
+
return;
|
|
219
|
+
sourceIndexRef.current = Math.max(0, (sources ?? []).indexOf(uri));
|
|
220
|
+
void add(uri, { title });
|
|
221
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
222
|
+
}, [uri]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
syncSubtitleTracks();
|
|
225
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
226
|
+
}, [subtitleTracksProp, subtitleUrl]);
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!visible || !playing)
|
|
229
|
+
return;
|
|
230
|
+
const t = setTimeout(() => setVisible(false), 3000);
|
|
231
|
+
return () => clearTimeout(t);
|
|
232
|
+
}, [visible, playing]);
|
|
233
|
+
// ---- render ----
|
|
234
|
+
const activeCue = cues.find((c) => currentTime - (subtitleOffset ?? 0) >= c.start &&
|
|
235
|
+
currentTime - (subtitleOffset ?? 0) < c.end) ?? null;
|
|
236
|
+
const activeTrack = tracks[Math.min(subTrackIdxRef.current, tracks.length - 1)] ?? null;
|
|
237
|
+
if (error) {
|
|
238
|
+
return (_jsxs(View, { style: [styles.wrap, styles.center], children: [_jsx(Ionicons, { name: "alert-circle-outline", size: 40, color: "#ff6b6b" }), _jsx(Text, { style: styles.errorText, children: error }), _jsx(Text, { style: styles.errorHint, children: "Use \"Download & Play\" instead." })] }));
|
|
239
|
+
}
|
|
240
|
+
return (_jsxs(View, { style: [styles.wrap, fullscreen && styles.fullscreen], children: [sourceUri ? (_jsx(Video, { ref: videoRef, source: { uri: sourceUri }, style: StyleSheet.absoluteFill, resizeMode: "contain", paused: !playing, muted: muted, volume: volume, progressUpdateInterval: 1000, onLoadStart: () => {
|
|
241
|
+
if (mountedRef.current)
|
|
242
|
+
setBuffering(true);
|
|
243
|
+
}, onLoad: onVideoLoad, onBuffer: ({ isBuffering }) => {
|
|
244
|
+
if (mountedRef.current)
|
|
245
|
+
setBuffering(!!isBuffering);
|
|
246
|
+
}, onProgress: onVideoProgress, onError: onVideoError, onEnd: onVideoEnd })) : null, loading && (_jsxs(View, { style: styles.centerBadge, pointerEvents: "none", children: [_jsx(ActivityIndicator, { color: "#e50914", size: "large" }), _jsx(Text, { style: styles.loadingText, children: "Starting stream\u2026" })] })), _jsx(SubtitleDisplay, { cue: activeCue, enabled: subtitlesOn, fontSize: subFontSize }), subtitlesOn && subtitleError && !cues.length && (_jsx(View, { pointerEvents: "none", style: styles.subErrorWrap, children: _jsxs(Text, { style: styles.subErrorText, children: [subtitleError, " \u00B7 ", activeTrack?.lang ?? "no track"] }) })), _jsx(PlayerControls, { playing: playing, buffering: buffering, currentTime: currentTime, duration: duration, muted: muted, title: title, visible: visible, subtitlesOn: subtitlesOn, volume: volume, subtitleTracks: tracks.map((t) => t.lang), subtitleTrack: activeTrack?.lang, onSelectSubtitleTrack: selectSubtitleTrack, subtitleFontSize: subFontSize, onChangeSubtitleFontSize: changeSubtitleFontSize, onToggleVisible: toggleControls, onPlayPause: playPause, onSkip: skip, onSeek: seek, onToggleMute: toggleMute, onToggleSubtitles: toggleSubtitles, onVolume: changeVolume, onFullscreen: enterFullscreen })] }));
|
|
247
|
+
});
|
|
248
|
+
const styles = StyleSheet.create({
|
|
249
|
+
wrap: {
|
|
250
|
+
flex: 1,
|
|
251
|
+
width: "100%",
|
|
252
|
+
height: "100%",
|
|
253
|
+
backgroundColor: "#000",
|
|
254
|
+
},
|
|
255
|
+
fullscreen: {
|
|
256
|
+
position: "absolute",
|
|
257
|
+
left: 0,
|
|
258
|
+
right: 0,
|
|
259
|
+
top: 0,
|
|
260
|
+
bottom: 0,
|
|
261
|
+
zIndex: 10,
|
|
262
|
+
},
|
|
263
|
+
center: {
|
|
264
|
+
flex: 1,
|
|
265
|
+
justifyContent: "center",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
gap: 12,
|
|
268
|
+
padding: 20,
|
|
269
|
+
backgroundColor: "#000",
|
|
270
|
+
},
|
|
271
|
+
errorText: {
|
|
272
|
+
color: "#ff6b6b",
|
|
273
|
+
fontSize: 14,
|
|
274
|
+
textAlign: "center",
|
|
275
|
+
},
|
|
276
|
+
errorHint: {
|
|
277
|
+
color: "#9aa0aa",
|
|
278
|
+
fontSize: 12,
|
|
279
|
+
textAlign: "center",
|
|
280
|
+
},
|
|
281
|
+
centerBadge: {
|
|
282
|
+
...StyleSheet.absoluteFillObject,
|
|
283
|
+
justifyContent: "center",
|
|
284
|
+
alignItems: "center",
|
|
285
|
+
gap: 12,
|
|
286
|
+
},
|
|
287
|
+
loadingText: { color: "#fff", fontSize: 14 },
|
|
288
|
+
subErrorWrap: {
|
|
289
|
+
position: "absolute",
|
|
290
|
+
left: 0,
|
|
291
|
+
right: 0,
|
|
292
|
+
bottom: 56,
|
|
293
|
+
alignItems: "center",
|
|
294
|
+
paddingHorizontal: 24,
|
|
295
|
+
zIndex: 5,
|
|
296
|
+
},
|
|
297
|
+
subErrorText: {
|
|
298
|
+
backgroundColor: "rgba(229,9,20,0.8)",
|
|
299
|
+
color: "#fff",
|
|
300
|
+
fontSize: 12,
|
|
301
|
+
textAlign: "center",
|
|
302
|
+
paddingHorizontal: 10,
|
|
303
|
+
paddingVertical: 4,
|
|
304
|
+
borderRadius: 4,
|
|
305
|
+
overflow: "hidden",
|
|
306
|
+
},
|
|
307
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type PlayerControlsProps = {
|
|
2
2
|
playing: boolean;
|
|
3
3
|
buffering: boolean;
|
|
4
4
|
currentTime: number;
|
|
@@ -28,18 +28,6 @@ export type PlayerControlsWebProps = {
|
|
|
28
28
|
outroRegion?: [number, number];
|
|
29
29
|
onSkipIntro?: () => void;
|
|
30
30
|
onSkipOutro?: () => void;
|
|
31
|
-
/** resolution labels in display order — the FIRST is the default source
|
|
32
|
-
* (the episode URL), e.g. ["Default", "1080p", "720p", "480p"]. */
|
|
33
|
-
resolutions?: string[];
|
|
34
|
-
/** currently active resolution label */
|
|
35
|
-
resolution?: string;
|
|
36
|
-
width?: number;
|
|
37
|
-
height?: number;
|
|
38
|
-
cue?: any;
|
|
39
|
-
enabled?: any;
|
|
40
|
-
fontSize?: any;
|
|
41
|
-
/** select a resolution — the parent resolves the label to its URL */
|
|
42
|
-
onSelectResolution?: (label: string) => void;
|
|
43
31
|
onToggleVisible: () => void;
|
|
44
32
|
onPlayPause: () => void;
|
|
45
33
|
onSkip: (deltaSeconds: number) => void;
|
|
@@ -49,4 +37,4 @@ export type PlayerControlsWebProps = {
|
|
|
49
37
|
onVolume: (v: number) => void;
|
|
50
38
|
onFullscreen: () => void;
|
|
51
39
|
};
|
|
52
|
-
export declare function
|
|
40
|
+
export declare function PlayerControls(props: PlayerControlsProps): import("react").JSX.Element;
|