chore: remove experimental WebViewExtractor feature and dependency

This commit is contained in:
paregi12 2026-01-17 21:16:08 +05:30
parent 8f60a2a810
commit b77d681b41
5 changed files with 0 additions and 325 deletions

View file

@ -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>

View file

@ -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"
},

View file

@ -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
},
});

View file

@ -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

View file

@ -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;