mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
vlcplayer init
This commit is contained in:
parent
83c3bdbdde
commit
37ece44b9b
10 changed files with 261 additions and 30 deletions
|
|
@ -54,3 +54,4 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
|||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
android.minSdkVersion=26
|
||||
|
|
|
|||
7
app.json
7
app.json
|
|
@ -79,6 +79,13 @@
|
|||
{
|
||||
"username": "nayifleo"
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-libvlc-player",
|
||||
{
|
||||
"localNetworkPermission": "Allow $(PRODUCT_NAME) to access your local network",
|
||||
"supportsBackgroundPlayback": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"updates": {
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -462,7 +462,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
<string>_http._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>App uses the local network to discover and connect to devices.</string>
|
||||
<string>Allow $(PRODUCT_NAME) to access your local network</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require microphone access.</string>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
117
package-lock.json
generated
117
package-lock.json
generated
|
|
@ -46,6 +46,7 @@
|
|||
"expo-haptics": "~14.0.1",
|
||||
"expo-image": "~2.0.7",
|
||||
"expo-intent-launcher": "~12.0.2",
|
||||
"expo-libvlc-player": "^2.1.7",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-localization": "~16.0.1",
|
||||
"expo-notifications": "~0.29.14",
|
||||
|
|
@ -72,6 +73,7 @@
|
|||
"react-native-svg": "15.8.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-vlc-media-player": "^1.0.94",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-wheel-color-picker": "^1.3.1"
|
||||
},
|
||||
|
|
@ -7906,6 +7908,17 @@
|
|||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-libvlc-player": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/expo-libvlc-player/-/expo-libvlc-player-2.1.7.tgz",
|
||||
"integrity": "sha512-WJz5vZE3qcFi2yeSYFWimdUS6oG+/gZZzsuhTvjCJ0pDTWsVeYVvmZbF4fg6cKRZf2WkZTaLzfngfVEBa6+3hA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-linear-gradient": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
|
||||
|
|
@ -12737,6 +12750,15 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-slider": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
|
||||
"integrity": "sha512-jV9K87eu9uWr0uJIyrSpBLnCKvVlOySC2wynq9TFCdV9oGgjt7Niq8Q1A8R8v+5GHsuBw/s8vEj1AAkkUi+u+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-svg": {
|
||||
"version": "15.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.8.0.tgz",
|
||||
|
|
@ -12888,6 +12910,91 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",
|
||||
"integrity": "sha512-wKYLaFuQST/chH3AJRjmOLoLy3JEs1JR6zMNgTaemFpNoXs0ztRnTxcxFD9xhX7cJe1/zoN5BpQYe7kL0m5yyA==",
|
||||
"deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"yargs": "^16.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"fa5-upgrade": "bin/fa5-upgrade.sh",
|
||||
"generate-icon": "bin/generate-icon.js"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons/node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-video": {
|
||||
"version": "6.16.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.16.1.tgz",
|
||||
|
|
@ -12898,6 +13005,16 @@
|
|||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vlc-media-player": {
|
||||
"version": "1.0.94",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vlc-media-player/-/react-native-vlc-media-player-1.0.94.tgz",
|
||||
"integrity": "sha512-6Ee09NY3ir4UN7mSFv8N+4GBiUQzAyVWU54ilwBAOZO8ICU2aJJmOq9ptYxvswIiRcbSz8Z+aB7LnjIlMR6WoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-slider": "^0.11.0",
|
||||
"react-native-vector-icons": "^9.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.19.13",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"expo-haptics": "~14.0.1",
|
||||
"expo-image": "~2.0.7",
|
||||
"expo-intent-launcher": "~12.0.2",
|
||||
"expo-libvlc-player": "^2.1.7",
|
||||
"expo-linear-gradient": "~14.0.2",
|
||||
"expo-localization": "~16.0.1",
|
||||
"expo-notifications": "~0.29.14",
|
||||
|
|
@ -72,6 +73,7 @@
|
|||
"react-native-svg": "15.8.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-video": "^6.12.0",
|
||||
"react-native-vlc-media-player": "^1.0.94",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-wheel-color-picker": "^1.3.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
module.exports = {
|
||||
dependencies: {
|
||||
'react-native-vlc-media-player': {
|
||||
platforms: {
|
||||
android: {
|
||||
sourceDir: '../node_modules/react-native-vlc-media-player/android',
|
||||
packageImportPath: 'import io.github.react_native_vlc_media_player.ReactNativeVlcMediaPlayerPackage;',
|
||||
// Disable autolinking for Android
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -40,6 +40,7 @@ import { stremioService } from '../../services/stremioService';
|
|||
import { shouldUseKSPlayer } from '../../utils/playerSelection';
|
||||
import axios from 'axios';
|
||||
import * as Brightness from 'expo-brightness';
|
||||
import { LibVlcPlayerView } from 'expo-libvlc-player';
|
||||
|
||||
// Map VLC resize modes to react-native-video resize modes
|
||||
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
||||
|
|
@ -76,6 +77,16 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
backdrop
|
||||
} = route.params;
|
||||
|
||||
// Opt-in flag to use VLC backend
|
||||
const forceVlc = useMemo(() => {
|
||||
const rp: any = route.params || {};
|
||||
const v = rp.forceVlc !== undefined ? rp.forceVlc : rp.forceVLC;
|
||||
return typeof v === 'string' ? v.toLowerCase() === 'true' : Boolean(v);
|
||||
}, [route.params]);
|
||||
// TEMP toggle disabled; rely on route param forceVlc
|
||||
const TEMP_FORCE_VLC = false;
|
||||
const useVLC = Platform.OS === 'android' && (TEMP_FORCE_VLC || forceVlc);
|
||||
|
||||
|
||||
// Check if the stream is HLS (m3u8 playlist)
|
||||
const isHlsStream = (url: string) => {
|
||||
|
|
@ -250,6 +261,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [errorDetails, setErrorDetails] = useState<string>('');
|
||||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// VLC refs/state
|
||||
const vlcRef = useRef<any>(null);
|
||||
const [vlcActive, setVlcActive] = useState(true);
|
||||
|
||||
// Compute VLC aspect ratio mapping from current resize mode
|
||||
const vlcAspectRatio = useMemo(() => {
|
||||
if (!useVLC) return undefined;
|
||||
// Contain/original behavior -> let VLC choose best fit
|
||||
if (resizeMode === 'contain' || resizeMode === 'none') return undefined;
|
||||
// For cover/fill, force the view's aspect ratio to fill the container
|
||||
if ((resizeMode === 'cover' || resizeMode === 'fill') && screenDimensions.width > 0 && screenDimensions.height > 0) {
|
||||
return `${Math.round(screenDimensions.width)}:${Math.round(screenDimensions.height)}`;
|
||||
}
|
||||
return undefined;
|
||||
}, [useVLC, resizeMode, screenDimensions.width, screenDimensions.height]);
|
||||
|
||||
|
||||
// Volume and brightness controls
|
||||
const [volume, setVolume] = useState(1.0);
|
||||
|
|
@ -601,6 +628,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
enableImmersiveMode();
|
||||
// Workaround for VLC surface detach: briefly remount VLC view on focus
|
||||
if (useVLC) {
|
||||
setVlcActive(false);
|
||||
const t = setTimeout(() => setVlcActive(true), 60);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
return () => {};
|
||||
}, [])
|
||||
);
|
||||
|
|
@ -610,6 +643,11 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const onAppStateChange = (state: string) => {
|
||||
if (state === 'active') {
|
||||
enableImmersiveMode();
|
||||
if (useVLC) {
|
||||
// Briefly remount VLC view when app returns to foreground
|
||||
setVlcActive(false);
|
||||
setTimeout(() => setVlcActive(true), 60);
|
||||
}
|
||||
// On iOS, if we were playing before system interruption and the app becomes active again,
|
||||
// ensure playback resumes (handles status bar pull-down case)
|
||||
if (Platform.OS === 'ios' && wasPlayingBeforeIOSInterruptionRef.current && isPlayerReady) {
|
||||
|
|
@ -816,6 +854,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const seekToTime = (rawSeconds: number) => {
|
||||
// Clamp to just before the end of the media.
|
||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
||||
if (useVLC && duration > 0) {
|
||||
try {
|
||||
const fraction = Math.min(Math.max(timeInSeconds / duration, 0), 0.999);
|
||||
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
|
||||
vlcRef.current.seek(fraction);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (videoRef.current && duration > 0 && !isSeeking.current) {
|
||||
if (DEBUG_MODE) {
|
||||
if (__DEV__) logger.log(`[AndroidVideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||
|
|
@ -1844,14 +1891,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const togglePlayback = () => {
|
||||
if (videoRef.current) {
|
||||
const newPausedState = !paused;
|
||||
const newPausedState = !paused;
|
||||
if (useVLC && vlcRef.current) {
|
||||
try {
|
||||
if (newPausedState) {
|
||||
if (typeof vlcRef.current.pause === 'function') vlcRef.current.pause();
|
||||
} else {
|
||||
if (typeof vlcRef.current.play === 'function') vlcRef.current.play();
|
||||
}
|
||||
} catch {}
|
||||
setPaused(newPausedState);
|
||||
} else if (videoRef.current) {
|
||||
setPaused(newPausedState);
|
||||
}
|
||||
|
||||
// IMMEDIATE: Send immediate progress update to Trakt for both pause and unpause
|
||||
if (duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true); // force=true triggers immediate sync
|
||||
}
|
||||
if (duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2651,7 +2706,47 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
onLongPress={resetZoom}
|
||||
delayLongPress={300}
|
||||
>
|
||||
<Video
|
||||
{useVLC ? (
|
||||
<LibVlcPlayerView
|
||||
ref={vlcRef}
|
||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||
// Remount control
|
||||
key={vlcActive ? 'vlc-on' : 'vlc-off'}
|
||||
source={currentStreamUrl}
|
||||
aspectRatio={vlcAspectRatio}
|
||||
// When using contain/original, use scale 0 to let VLC manage
|
||||
scale={resizeMode === 'contain' || resizeMode === 'none' ? 0 : 0}
|
||||
volume={Math.round(Math.max(0, Math.min(1, volume)) * 100)}
|
||||
mute={false}
|
||||
repeat={false}
|
||||
rate={1}
|
||||
autoplay={!paused}
|
||||
// Restore approximate time after remount
|
||||
time={Math.max(0, Math.floor(currentTime * 1000))}
|
||||
onFirstPlay={(info: any) => {
|
||||
try {
|
||||
const lenSec = (info?.length ?? 0) / 1000;
|
||||
const width = info?.width || 0;
|
||||
const height = info?.height || 0;
|
||||
onLoad({ duration: lenSec, naturalSize: width && height ? { width, height } : undefined });
|
||||
} catch (e) {
|
||||
logger.warn('[AndroidVideoPlayer][VLC] onFirstPlay parse error', e);
|
||||
}
|
||||
}}
|
||||
onPositionChanged={(ev: any) => {
|
||||
const pos = typeof ev?.position === 'number' ? ev.position : 0;
|
||||
if (duration > 0) {
|
||||
const current = pos * duration;
|
||||
handleProgress({ currentTime: current, playableDuration: current });
|
||||
}
|
||||
}}
|
||||
onPlaying={() => setPaused(false)}
|
||||
onPaused={() => setPaused(true)}
|
||||
onEndReached={onEnd}
|
||||
onEncounteredError={(e: any) => handleError(e)}
|
||||
/>
|
||||
) : (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||
source={{
|
||||
|
|
@ -2721,6 +2816,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
// Use textureView on Android: allows 3D mapping but DRM not supported
|
||||
viewType={Platform.OS === 'android' ? ViewType.TEXTURE : undefined}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</PinchGestureHandler>
|
||||
|
|
|
|||
|
|
@ -874,9 +874,26 @@ export const StreamsScreen = () => {
|
|||
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
||||
const streamProvider = stream.addonId || stream.addonName || stream.name;
|
||||
|
||||
// iOS now always uses KSPlayer, no need for player selection logic
|
||||
// Keep forceVlc for backward compatibility but it's ignored by player selection
|
||||
const forceVlc = !!options?.forceVlc;
|
||||
// Decide if we should force VLC on Android based on scraper format or URL/content-type
|
||||
let forceVlc = !!options?.forceVlc;
|
||||
try {
|
||||
const providerId = stream.addonId || (stream as any).addon || '';
|
||||
// If provider declares MKV support in manifest, prefer VLC
|
||||
if (Platform.OS === 'android' && providerId) {
|
||||
const supportsMkv = await localScraperService.supportsFormat(providerId, 'mkv');
|
||||
if (supportsMkv) forceVlc = true;
|
||||
}
|
||||
// URL/content-type heuristic
|
||||
if (!forceVlc) {
|
||||
const lowerUrl = (stream.url || '').toLowerCase();
|
||||
const isMkvByPath = lowerUrl.includes('.mkv') || /[?&]ext=mkv\b/i.test(lowerUrl) || /format=mkv\b/i.test(lowerUrl) || /container=mkv\b/i.test(lowerUrl);
|
||||
const contentType = (stream.headers && ((stream.headers as any)['Content-Type'] || (stream.headers as any)['content-type'])) || '';
|
||||
const isMkvByHeader = typeof contentType === 'string' && /matroska|x-matroska/i.test(contentType);
|
||||
if (Platform.OS === 'android' && (isMkvByPath || isMkvByHeader)) {
|
||||
forceVlc = true;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
|
||||
// Show a quick full-screen black overlay to mask rotation flicker
|
||||
|
|
@ -915,7 +932,7 @@ export const StreamsScreen = () => {
|
|||
streamName: streamName,
|
||||
// Always prefer stream.headers; player will use these for requests
|
||||
headers: options?.headers || stream.headers || undefined,
|
||||
// iOS now always uses KSPlayer, forceVlc kept for backward compatibility
|
||||
// Android will use this to choose VLC path; iOS ignores
|
||||
forceVlc,
|
||||
id,
|
||||
type,
|
||||
|
|
|
|||
Loading…
Reference in a new issue