import 'dart:async'; import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; abstract class ILinkExtractionService { Future extractVideoUrl(String pageUrl); } class LinkExtractionService implements ILinkExtractionService { @override Future extractVideoUrl(String pageUrl) async { // 1. Zaten direkt bir video linkiyse hemen döndür if (_isVideoUrl(pageUrl)) return pageUrl; final completer = Completer(); Timer? timeoutTimer; // HeadlessWebView'i yerel değişkene alıyoruz ki paralel çalışmalarda çakışmasın HeadlessInAppWebView? headlessWebView; // Temizlik ve Bellek Yönetimi void cleanup() { timeoutTimer?.cancel(); headlessWebView?.dispose(); headlessWebView = null; } // Bulunan URL'yi işleme void handleFoundUrl(String url) { if (!completer.isCompleted) { debugPrint("EXTRACTION: SUCCESS! Found: $url"); cleanup(); completer.complete(url); } } // Zaman aşımı (15 saniye) timeoutTimer = Timer(const Duration(seconds: 15), () { if (!completer.isCompleted) { debugPrint("EXTRACTION: Timeout - no video URL found"); cleanup(); completer.complete(null); } }); // 2. UserScript: JS Kodunu Iframe'ler dahil tüm sayfalara DOM yüklenmeden ENJEKTE EDİYORUZ final snifferScript = UserScript( source: ''' (function() { if (window.snifferInjected) return; window.snifferInjected = true; console.log("SNIFFER: Injected into frame: " + window.location.href); function reportUrl(url) { if (url && (url.includes('.m3u8') || url.includes('.mp4'))) { window.flutter_inappwebview.callHandler('foundVideoUrl', url); } } // 1. Override XMLHttpRequest var oldOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { reportUrl(url); return oldOpen.apply(this, arguments); }; // 2. Override Fetch var oldFetch = window.fetch; window.fetch = function() { var url = (arguments[0] instanceof Request) ? arguments[0].url : arguments[0]; if (url) reportUrl(url.toString()); return oldFetch.apply(this, arguments); }; // 3. Oynatma Butonlarına Tıklama (Tüm iframe'lerde çalışacak) window.clickPlayButtons = function() { var selectors = [ '.play-that-video', '.play-button', '.vjs-big-play-button', '[aria-label="Play"]', '.ytp-large-play-button', '.play-icon', '#play', '.player-play', '.jw-display-icon-container', 'div[class*="play"]', 'button[class*="play"]', 'svg[class*="play"]' ]; selectors.forEach(function(sel) { try { document.querySelectorAll(sel).forEach(function(el) { if (el.offsetWidth > 0 || el.offsetHeight > 0) { el.click(); } }); } catch(e) {} }); // Video etiketlerini direkt kontrol et document.querySelectorAll('video').forEach(function(v) { if (v.src && v.src.length > 5) reportUrl(v.src); v.querySelectorAll('source').forEach(function(s) { if (s.src) reportUrl(s.src); }); }); }; // HTML içindeki linkleri Regex ile tara var scanInterval = setInterval(function() { var html = document.documentElement.innerHTML; var match = html.match(/https?:\\/\\/[^"'\\s<>]+\\.(m3u8|mp4)[^"'\\s<>]*/i); if (match) reportUrl(match[0]); }, 1500); })(); ''', injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, // Sayfa açılmadan hemen önce forMainFrameOnly: false, // ÇOK ÖNEMLİ: Iframe'lerin içine girmesi için false olmalı! ); headlessWebView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(pageUrl)), initialUserScripts: UnmodifiableListView([snifferScript]), // Scripti buradan veriyoruz initialSettings: InAppWebViewSettings( javaScriptEnabled: true, isInspectable: true, mediaPlaybackRequiresUserGesture: false, // ÇOK ÖNEMLİ: Videoların tıklanmadan yüklenmesine izin ver mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, supportMultipleWindows: false, javaScriptCanOpenWindowsAutomatically: false, useShouldInterceptRequest: true, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", ), onWebViewCreated: (controller) { // JS'den gelen sinyali dinle controller.addJavaScriptHandler(handlerName: 'foundVideoUrl', callback: (args) { if (args.isNotEmpty) { handleFoundUrl(args[0].toString()); } }); }, onReceivedServerTrustAuthRequest: (controller, challenge) async { return ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED); }, shouldInterceptRequest: (controller, request) async { final url = request.url.toString(); if (_isVideoUrl(url)) { handleFoundUrl(url); } return null; }, onLoadStop: (controller, url) async { debugPrint("EXTRACTION: Page loaded: $url"); // JS yüklenmiş mi diye kontrol et ve butonlara tıkla for (int i = 0; i < 8; i++) { if (completer.isCompleted) break; await Future.delayed(const Duration(seconds: 1)); if (completer.isCompleted) break; try { // WebView kapanmışsa hata fırlatmasını engellemek için try-catch await controller.evaluateJavascript(source: "if(window.clickPlayButtons) window.clickPlayButtons();"); } catch (e) { debugPrint("WebView disposed before javascript execution."); } } }, ); await headlessWebView?.run(); return completer.future; } bool _isVideoUrl(String url) { final lower = url.toLowerCase(); if (lower.startsWith('data:') || lower.length < 10) return false; // Analitik veya reklam linklerini es geç if (lower.contains('google.com') || lower.contains('gstatic.com') || lower.contains('analytics')) return false; return lower.contains('.m3u8') || lower.contains('.mp4') || lower.contains('.mkv') || lower.contains('/hls/') || lower.contains('master.m3u8') || lower.contains('index.m3u8') || lower.contains('playlist.m3u8'); } }