mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-29 04:13:03 +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 { ToastProvider } from './src/contexts/ToastContext';
|
||||||
import { mmkvStorage } from './src/services/mmkvStorage';
|
import { mmkvStorage } from './src/services/mmkvStorage';
|
||||||
import { CampaignManager } from './src/components/promotions/CampaignManager';
|
import { CampaignManager } from './src/components/promotions/CampaignManager';
|
||||||
import { WebViewExtractor } from './src/components/WebViewExtractor';
|
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||||
|
|
@ -203,7 +202,6 @@ const ThemedApp = () => {
|
||||||
onDismiss={githubUpdate.onDismiss}
|
onDismiss={githubUpdate.onDismiss}
|
||||||
onLater={githubUpdate.onLater}
|
onLater={githubUpdate.onLater}
|
||||||
/>
|
/>
|
||||||
<WebViewExtractor />
|
|
||||||
<CampaignManager />
|
<CampaignManager />
|
||||||
</View>
|
</View>
|
||||||
</DownloadsProvider>
|
</DownloadsProvider>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,6 @@
|
||||||
"react-native-vector-icons": "^10.3.0",
|
"react-native-vector-icons": "^10.3.0",
|
||||||
"react-native-video": "6.18.0",
|
"react-native-video": "6.18.0",
|
||||||
"react-native-web": "^0.21.0",
|
"react-native-web": "^0.21.0",
|
||||||
"react-native-webview": "^13.15.0",
|
|
||||||
"react-native-wheel-color-picker": "^1.3.1",
|
"react-native-wheel-color-picker": "^1.3.1",
|
||||||
"react-native-worklets": "^0.7.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 { findBestSubtitleTrack, findBestAudioTrack } from './utils/trackSelectionUtils';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { streamExtractorService } from '../../services/StreamExtractorService';
|
|
||||||
|
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
|
|
||||||
|
|
@ -94,56 +93,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
||||||
const [currentVideoType, setCurrentVideoType] = useState<string | undefined>((route.params as any).videoType);
|
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 [availableStreams, setAvailableStreams] = useState<any>(passedAvailableStreams || {});
|
||||||
const [currentQuality, setCurrentQuality] = useState(quality);
|
const [currentQuality, setCurrentQuality] = useState(quality);
|
||||||
const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider);
|
const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider);
|
||||||
|
|
@ -834,38 +783,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
height={playerState.screenDimensions.height}
|
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' }}>
|
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||||
{!isTransitioningStream && (
|
{!isTransitioningStream && (
|
||||||
<VideoSurface
|
<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