mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-17 00:06:30 +00:00
chore: remove experimental WebViewExtractor feature and dependency
This commit is contained in:
parent
8f60a2a810
commit
b77d681b41
5 changed files with 0 additions and 325 deletions
2
App.tsx
2
App.tsx
|
|
@ -44,7 +44,6 @@ import { AccountProvider, useAccount } from './src/contexts/AccountContext';
|
|||
import { ToastProvider } from './src/contexts/ToastContext';
|
||||
import { mmkvStorage } from './src/services/mmkvStorage';
|
||||
import { CampaignManager } from './src/components/promotions/CampaignManager';
|
||||
import { WebViewExtractor } from './src/components/WebViewExtractor';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||
|
|
@ -203,7 +202,6 @@ const ThemedApp = () => {
|
|||
onDismiss={githubUpdate.onDismiss}
|
||||
onLater={githubUpdate.onLater}
|
||||
/>
|
||||
<WebViewExtractor />
|
||||
<CampaignManager />
|
||||
</View>
|
||||
</DownloadsProvider>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@
|
|||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-video": "6.18.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-webview": "^13.15.0",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"react-native-worklets": "^0.7.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { streamExtractorService, EXTRACTOR_EVENTS } from '../services/StreamExtractorService';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export const WebViewExtractor: React.FC = () => {
|
||||
const [currentRequest, setCurrentRequest] = useState<{ id: string; url: string; script?: string } | null>(null);
|
||||
const webViewRef = useRef<WebView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const startListener = (request: { id: string; url: string; script?: string }) => {
|
||||
logger.log(`[WebViewExtractor] Received request: ${request.url}`);
|
||||
setCurrentRequest(request);
|
||||
};
|
||||
|
||||
streamExtractorService.events.on(EXTRACTOR_EVENTS.START_EXTRACTION, startListener);
|
||||
|
||||
return () => {
|
||||
streamExtractorService.events.off(EXTRACTOR_EVENTS.START_EXTRACTION, startListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleMessage = (event: any) => {
|
||||
if (!currentRequest) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.nativeEvent.data);
|
||||
if (data.type === 'found_stream') {
|
||||
streamExtractorService.events.emit(EXTRACTOR_EVENTS.EXTRACTION_SUCCESS, {
|
||||
id: currentRequest.id,
|
||||
streamUrl: data.url,
|
||||
headers: data.headers
|
||||
});
|
||||
setCurrentRequest(null); // Reset after success
|
||||
} else if (data.type === 'error') {
|
||||
// Optional: Retry logic or just log
|
||||
logger.warn(`[WebViewExtractor] Error from page: ${data.message}`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[WebViewExtractor] Failed to parse message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (syntheticEvent: any) => {
|
||||
const { nativeEvent } = syntheticEvent;
|
||||
logger.warn('[WebViewExtractor] WebView error: ', nativeEvent);
|
||||
if (currentRequest) {
|
||||
streamExtractorService.events.emit(EXTRACTOR_EVENTS.EXTRACTION_FAILURE, {
|
||||
id: currentRequest.id,
|
||||
error: `WebView Error: ${nativeEvent.description}`
|
||||
});
|
||||
setCurrentRequest(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Default extraction script: looks for video tags and intercepts network traffic
|
||||
const DEFAULT_INJECTED_JS = `
|
||||
(function() {
|
||||
function sendStream(url, headers) {
|
||||
// Broad regex to catch HLS, DASH, and common video containers
|
||||
const videoRegex = /\.(m3u8|mp4|mpd|mkv|webm|mov|avi)(\?|$)/i;
|
||||
if (!videoRegex.test(url)) return;
|
||||
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
type: 'found_stream',
|
||||
url: url,
|
||||
headers: headers
|
||||
}));
|
||||
}
|
||||
|
||||
// 1. Intercept Video Elements
|
||||
function checkVideoElements() {
|
||||
var videos = document.getElementsByTagName('video');
|
||||
for (var i = 0; i < videos.length; i++) {
|
||||
if (videos[i].src && !videos[i].src.startsWith('blob:')) {
|
||||
sendStream(videos[i].src);
|
||||
return true;
|
||||
}
|
||||
// Check for source children
|
||||
var sources = videos[i].getElementsByTagName('source');
|
||||
for (var j = 0; j < sources.length; j++) {
|
||||
if (sources[j].src) {
|
||||
sendStream(sources[j].src);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check periodically
|
||||
setInterval(checkVideoElements, 1000);
|
||||
|
||||
// 2. Intercept XHR (optional, for m3u8/mp4 fetches)
|
||||
var originalOpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function(method, url) {
|
||||
sendStream(url);
|
||||
originalOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
// 3. Intercept Fetch
|
||||
var originalFetch = window.fetch;
|
||||
window.fetch = function(input, init) {
|
||||
var url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
|
||||
sendStream(url);
|
||||
return originalFetch.apply(this, arguments);
|
||||
};
|
||||
|
||||
// 4. Check for specific common player variables (optional)
|
||||
// e.g., jwplayer, etc.
|
||||
})();
|
||||
`;
|
||||
|
||||
if (!currentRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.hiddenContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: currentRequest.url }}
|
||||
onMessage={handleMessage}
|
||||
onError={handleError}
|
||||
injectedJavaScript={currentRequest.script || DEFAULT_INJECTED_JS}
|
||||
javaScriptEnabled={true}
|
||||
domStorageEnabled={true}
|
||||
allowsInlineMediaPlayback={true}
|
||||
mediaPlaybackRequiresUserAction={false}
|
||||
// Important: Use a desktop or generic user agent to avoid mobile redirect loops sometimes
|
||||
userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
style={{ width: 1, height: 1 }} // Minimal size to keep it active
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
hiddenContainer: {
|
||||
position: 'absolute',
|
||||
top: -1000, // Move off-screen
|
||||
left: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0.01, // Almost invisible but technically rendered
|
||||
},
|
||||
});
|
||||
|
|
@ -62,7 +62,6 @@ import { WyzieSubtitle, SubtitleCue } from './utils/playerTypes';
|
|||
import { findBestSubtitleTrack, findBestAudioTrack } from './utils/trackSelectionUtils';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import axios from 'axios';
|
||||
import { streamExtractorService } from '../../services/StreamExtractorService';
|
||||
|
||||
const DEBUG_MODE = false;
|
||||
|
||||
|
|
@ -93,56 +92,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
||||
const [currentVideoType, setCurrentVideoType] = useState<string | undefined>((route.params as any).videoType);
|
||||
|
||||
// Stream Resolution State
|
||||
const [isResolving, setIsResolving] = useState(false);
|
||||
const [resolutionError, setResolutionError] = useState<string | null>(null);
|
||||
|
||||
// Auto-resolve stream URL if it's an embed
|
||||
useEffect(() => {
|
||||
const resolveStream = async () => {
|
||||
// Simple heuristic: If it doesn't look like a direct video file, try to resolve it
|
||||
// Valid video extensions: .mp4, .mkv, .avi, .m3u8, .mpd, .mov, .flv, .webm
|
||||
// Also check for magnet links (which don't need resolution)
|
||||
const lowerUrl = uri.toLowerCase();
|
||||
const isDirectFile = /\.(mp4|mkv|avi|m3u8|mpd|mov|flv|webm)(\?|$)/.test(lowerUrl);
|
||||
const isMagnet = lowerUrl.startsWith('magnet:');
|
||||
|
||||
// If it looks like a direct link or magnet, skip resolution
|
||||
if (isDirectFile || isMagnet) {
|
||||
setCurrentStreamUrl(uri);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(`[AndroidVideoPlayer] URL ${uri} does not look like a direct file. Attempting resolution...`);
|
||||
setIsResolving(true);
|
||||
setResolutionError(null);
|
||||
|
||||
try {
|
||||
const result = await streamExtractorService.extractStream(uri);
|
||||
|
||||
if (result && result.streamUrl) {
|
||||
logger.log(`[AndroidVideoPlayer] Resolved stream: ${result.streamUrl}`);
|
||||
setCurrentStreamUrl(result.streamUrl);
|
||||
|
||||
// If headers were returned, we might want to use them (though current player prop structure is simple)
|
||||
// For now we just use the URL
|
||||
} else {
|
||||
logger.warn(`[AndroidVideoPlayer] Resolution returned no URL, using original.`);
|
||||
setCurrentStreamUrl(uri); // Fallback to original
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[AndroidVideoPlayer] Stream resolution failed:`, error);
|
||||
// Fallback to original URL on error, maybe it works anyway?
|
||||
setCurrentStreamUrl(uri);
|
||||
// Optional: setResolutionError(error.message);
|
||||
} finally {
|
||||
setIsResolving(false);
|
||||
}
|
||||
};
|
||||
|
||||
resolveStream();
|
||||
}, [uri]);
|
||||
|
||||
const [availableStreams, setAvailableStreams] = useState<any>(passedAvailableStreams || {});
|
||||
const [currentQuality, setCurrentQuality] = useState(quality);
|
||||
|
|
@ -834,38 +783,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
height={playerState.screenDimensions.height}
|
||||
/>
|
||||
|
||||
{/* Stream Resolution Overlay */}
|
||||
{isResolving && (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'black',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999
|
||||
}}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={{
|
||||
color: 'white',
|
||||
marginTop: 20,
|
||||
fontSize: 16,
|
||||
fontWeight: '600'
|
||||
}}>
|
||||
Resolving Stream Source...
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
marginTop: 8,
|
||||
fontSize: 14
|
||||
}}>
|
||||
Extracting video from embed link
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||
{!isTransitioningStream && (
|
||||
<VideoSurface
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
import EventEmitter from 'eventemitter3';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Events for communication between Service and Component
|
||||
export const EXTRACTOR_EVENTS = {
|
||||
START_EXTRACTION: 'start_extraction',
|
||||
EXTRACTION_SUCCESS: 'extraction_success',
|
||||
EXTRACTION_FAILURE: 'extraction_failed',
|
||||
};
|
||||
|
||||
interface ExtractionRequest {
|
||||
id: string;
|
||||
url: string;
|
||||
script?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ExtractionResult {
|
||||
id: string;
|
||||
streamUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class StreamExtractorService {
|
||||
private static instance: StreamExtractorService;
|
||||
public events = new EventEmitter();
|
||||
private pendingRequests = new Map<string, { resolve: (val: any) => void; reject: (err: any) => void; timeout: NodeJS.Timeout }>();
|
||||
|
||||
private constructor() {
|
||||
// Listen for results from the component
|
||||
this.events.on(EXTRACTOR_EVENTS.EXTRACTION_SUCCESS, this.handleSuccess.bind(this));
|
||||
this.events.on(EXTRACTOR_EVENTS.EXTRACTION_FAILURE, this.handleFailure.bind(this));
|
||||
}
|
||||
|
||||
static getInstance(): StreamExtractorService {
|
||||
if (!StreamExtractorService.instance) {
|
||||
StreamExtractorService.instance = new StreamExtractorService();
|
||||
}
|
||||
return StreamExtractorService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a direct stream URL from an embed URL using a hidden WebView.
|
||||
* @param url The embed URL to load.
|
||||
* @param script Optional custom JavaScript to run in the WebView.
|
||||
* @param timeoutMs Timeout in milliseconds (default 15000).
|
||||
* @returns Promise resolving to the stream URL or object with url and headers.
|
||||
*/
|
||||
public async extractStream(url: string, script?: string, timeoutMs = 15000): Promise<{ streamUrl: string; headers?: Record<string, string> } | null> {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
logger.log(`[StreamExtractor] Starting extraction for ${url} (ID: ${id})`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.finishRequest(id, undefined, 'Timeout waiting for stream extraction');
|
||||
}, timeoutMs);
|
||||
|
||||
this.pendingRequests.set(id, { resolve, reject, timeout });
|
||||
|
||||
// Emit event for the component to pick up
|
||||
this.events.emit(EXTRACTOR_EVENTS.START_EXTRACTION, { id, url, script });
|
||||
});
|
||||
}
|
||||
|
||||
private handleSuccess(result: ExtractionResult) {
|
||||
logger.log(`[StreamExtractor] Extraction success for ID: ${result.id}`);
|
||||
this.finishRequest(result.id, { streamUrl: result.streamUrl, headers: result.headers });
|
||||
}
|
||||
|
||||
private handleFailure(result: ExtractionResult) {
|
||||
logger.log(`[StreamExtractor] Extraction failed for ID: ${result.id}: ${result.error}`);
|
||||
this.finishRequest(result.id, undefined, result.error);
|
||||
}
|
||||
|
||||
private finishRequest(id: string, data?: any, error?: string) {
|
||||
const pending = this.pendingRequests.get(id);
|
||||
if (pending) {
|
||||
clearTimeout(pending.timeout);
|
||||
this.pendingRequests.delete(id);
|
||||
if (error) {
|
||||
pending.reject(new Error(error));
|
||||
} else {
|
||||
pending.resolve(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const streamExtractorService = StreamExtractorService.getInstance();
|
||||
export default streamExtractorService;
|
||||
Loading…
Reference in a new issue