diff --git a/App.tsx b/App.tsx
index 6d9e55fd..102d672c 100644
--- a/App.tsx
+++ b/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}
/>
-
diff --git a/package.json b/package.json
index 1c1cf6f1..64512dcd 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/components/WebViewExtractor.tsx b/src/components/WebViewExtractor.tsx
deleted file mode 100644
index d5ee4528..00000000
--- a/src/components/WebViewExtractor.tsx
+++ /dev/null
@@ -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(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 (
-
-
-
- );
-};
-
-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
- },
-});
diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx
index d829b04a..a975b86e 100644
--- a/src/components/player/AndroidVideoPlayer.tsx
+++ b/src/components/player/AndroidVideoPlayer.tsx
@@ -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(uri);
const [currentVideoType, setCurrentVideoType] = useState((route.params as any).videoType);
-
- // Stream Resolution State
- const [isResolving, setIsResolving] = useState(false);
- const [resolutionError, setResolutionError] = useState(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(passedAvailableStreams || {});
const [currentQuality, setCurrentQuality] = useState(quality);
@@ -834,38 +783,6 @@ const AndroidVideoPlayer: React.FC = () => {
height={playerState.screenDimensions.height}
/>
- {/* Stream Resolution Overlay */}
- {isResolving && (
-
-
-
- Resolving Stream Source...
-
-
- Extracting video from embed link
-
-
- )}
-
{!isTransitioningStream && (
;
-}
-
-interface ExtractionResult {
- id: string;
- streamUrl?: string;
- headers?: Record;
- error?: string;
-}
-
-class StreamExtractorService {
- private static instance: StreamExtractorService;
- public events = new EventEmitter();
- private pendingRequests = new Map 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 } | 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;