Skip to content

Commit

Permalink
Update: Optimize webview video resource obtaining
Browse files Browse the repository at this point in the history
  • Loading branch information
songbirdzz committed May 24, 2024
1 parent 4e84bca commit 65fbeb0
Showing 1 changed file with 67 additions and 14 deletions.
81 changes: 67 additions & 14 deletions lib/utils/webview.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,66 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';

import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:logger/logger.dart';

class Webview {
static Future<String?> getVideoResourceUrl(String pageUrl) async {
/*
* So, in this method, we basically get video resources in 2 ways:
* 1. Use user script to check whether there's a video element in the html,
* and get its src attribute. However, this method cannot obtain video resource
* when the src is in the blob form.
* 2. Listen to network requests. However, due to iframe cross-domain restraints,
* we cannot intercept requests made in iframes, and that's how a lot of websites
* do to their players. Therefore we see the whole document as a tree and traverse
* it by setting href (directly fetch the urls may cause authentication issues, such
* as referers or cookies).
*/
String? videoResourceUrl;
final completer = Completer();
_Node currentNode = _Node(pageUrl);
_Node currentNode = _Node(WebUri(pageUrl));
List<String> history = [];
Future.delayed(
const Duration(seconds: 60), () => completer.complete(false));
final webview = HeadlessInAppWebView(
initialUrlRequest: URLRequest(url: WebUri(pageUrl)),
initialUserScripts: UnmodifiableListView<UserScript>([
UserScript(source: """
setInterval(() => {
if (document.querySelector('video') !== null && document.querySelector('video').attributes.src.textContent.startsWith('http')) {
console.log(`VIDEO:\${document.querySelector('video').attributes.src.textContent}`);
// window.flutter_inappwebview.callHandler('foundVideoSrc', document.querySelector('video').attributes.src.textContent);
}
}, 100);
""", injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START)
]),
onLoadStart: (controller, url) {
Modular.get<Logger>().i('Loading $url');
history.add(url.toString());
history.add(_removeQuery(url));
},
onConsoleMessage: (controller, consoleMessage) {
// Receive message from iframes
// We cannot use controller.addJavaScriptHandler here because
// window.flutter_inappwebview doesn't exist in cross-domain iframes
// Therefore we use onConsoleMessage to pass information,
// which is based on devtools API.
String message = jsonDecode(consoleMessage.message);
if (message.startsWith('VIDEO:')) {
Modular.get<Logger>().i('Received console message: $consoleMessage');
videoResourceUrl = message.substring('VIDEO:'.length);
completer.complete(true);
}
},
onWebViewCreated: (controller) {
controller.addDevToolsProtocolEventListener(
eventName: 'responseReceived',
callback: (resp) {
Modular.get<Logger>().i('Received resp: $resp');
return resp;
});
},
onLoadResource: (controller, resource) {
if (resource.url.toString().contains('.m3u8') ||
Expand All @@ -26,32 +72,35 @@ class Webview {
// For Windows webviews (and maybe macOS as well), onLoadResource
// can't capture requests sent within iframes. Therefore we store
// and them for later use.
currentNode.children
.add(_Node(resource.url.toString(), parent: currentNode));
currentNode.children.add(_Node(resource.url!, parent: currentNode));
}
},
onLoadStop: (controller, url) async {
if (completer.isCompleted) return;
// This timer seems redundant, however I do have seen some onLoadResource
// calls after onLoadStop. So we use a timer here to ensure maxium compatibility

// Sometimes resource loading requests are still being made after
// onLoadStop. Therefore we delay a little bit here to make sure
// we won't miss any (or at least won't miss a lot) requests
await Future.delayed(const Duration(seconds: 2));
while (!currentNode.isRoot &&
currentNode.children
.where((node) => !history.contains(node.url))
currentNode.children.reversed
.where((node) => !history.contains(_removeQuery(node.url)))
.isEmpty) {
await controller.evaluateJavascript(
source: "window.location.href='${currentNode.parent!.url}';");
currentNode = currentNode.parent!;
}
if (currentNode.children
.where((node) => !history.contains(node.url))
if (currentNode.children.reversed
.where((node) => !history.contains(_removeQuery(node.url)))
.isNotEmpty) {
final node = currentNode.children
.where((node) => !history.contains(node.url))
final node = currentNode.children.reversed
.where((node) => !history.contains(_removeQuery(node.url)))
.first;
currentNode = node;
await controller.evaluateJavascript(
source: "window.location.href='${node.url}';");
} else {
// Root & no other subframes to go
completer.complete(false);
}
},
shouldAllowDeprecatedTLS: (controller, challenge) async =>
Expand All @@ -67,10 +116,14 @@ class Webview {
Modular.get<Logger>().i('Cannot find any video in $pageUrl');
return null;
}

static String _removeQuery(WebUri? url) {
return url!.origin + url.path;
}
}

class _Node {
final String url;
final WebUri url;
final _Node? parent;
final List<_Node> children = [];
bool get isRoot => parent == null;
Expand Down

0 comments on commit 65fbeb0

Please sign in to comment.