video store optimization attempts
* Dispose WebViewController and remove JavaScript channels properly * Track and cleanup all MutationObservers to prevent accumulation * Throttle latency updates to once per 5 seconds (reduces CPU load) * Add timeout protection and null checks for robustness * Prevent duplicate event listeners on page reloads
This commit is contained in:
parent
9ff61890a4
commit
b1df7b0de4
2 changed files with 146 additions and 43 deletions
|
|
@ -347,6 +347,11 @@ abstract class VideoStoreBase with Store {
|
|||
try {
|
||||
await videoWebViewController.runJavaScript('''
|
||||
{
|
||||
// Initialize observer tracking array if it doesn't exist
|
||||
if (!window._observers) {
|
||||
window._observers = [];
|
||||
}
|
||||
|
||||
const hideElements = (...el) => {
|
||||
el.forEach((el) => {
|
||||
el?.style.setProperty("display", "none", "important");
|
||||
|
|
@ -365,9 +370,13 @@ abstract class VideoStoreBase with Store {
|
|||
hide();
|
||||
const videoOverlayObserver = new MutationObserver(hide);
|
||||
videoOverlayObserver.observe(videoOverlay, { childList: true, subtree: true });
|
||||
// Track the overlay observer for cleanup
|
||||
window._observers.push(videoOverlayObserver);
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
// Track the main observer for cleanup
|
||||
window._observers.push(observer);
|
||||
}
|
||||
''');
|
||||
} catch (e) {
|
||||
|
|
@ -402,10 +411,31 @@ abstract class VideoStoreBase with Store {
|
|||
(await _asyncQuerySelector('[data-a-target="player-settings-submenu-advanced-video-stats"] input')).click();
|
||||
(await _asyncQuerySelector('[data-a-target="player-overlay-video-stats"]')).style.display = "none";
|
||||
(await _asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
|
||||
|
||||
// Wait for and verify latency element exists
|
||||
const latencyElement = await _asyncQuerySelector('[aria-label="Latency To Broadcaster"]');
|
||||
if (!latencyElement) {
|
||||
console.warn("Latency element not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Throttle latency updates to once every 5 seconds minimum
|
||||
let lastLatencyUpdate = 0;
|
||||
const LATENCY_UPDATE_INTERVAL = 5000; // 5 seconds in milliseconds
|
||||
|
||||
const observer = new MutationObserver((changes) => {
|
||||
Latency.postMessage(changes[0].target.textContent);
|
||||
const now = Date.now();
|
||||
if (now - lastLatencyUpdate >= LATENCY_UPDATE_INTERVAL) {
|
||||
lastLatencyUpdate = now;
|
||||
Latency.postMessage(changes[0].target.textContent);
|
||||
}
|
||||
})
|
||||
observer.observe(document.querySelector('[aria-label="Latency To Broadcaster"]'), { characterData: true, attributes: false, childList: false, subtree: true });
|
||||
observer.observe(latencyElement, { characterData: true, attributes: false, childList: false, subtree: true });
|
||||
// Track the latency observer for cleanup
|
||||
if (!window._observers) {
|
||||
window._observers = [];
|
||||
}
|
||||
window._observers.push(observer);
|
||||
});
|
||||
''');
|
||||
} catch (e) {
|
||||
|
|
@ -426,50 +456,82 @@ abstract class VideoStoreBase with Store {
|
|||
window._PROMISE_QUEUE = window._PROMISE_QUEUE.then(method, method);
|
||||
return window._PROMISE_QUEUE;
|
||||
};
|
||||
window._asyncQuerySelector = (selector, timeout = undefined) => new Promise((resolve) => {
|
||||
window._asyncQuerySelector = (selector, timeout = 30000) => new Promise((resolve) => {
|
||||
let element = document.querySelector(selector);
|
||||
if (element) {
|
||||
return resolve(element);
|
||||
}
|
||||
|
||||
let timeoutId;
|
||||
const cleanup = () => {
|
||||
observer.disconnect();
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
// Remove from tracking array with null safety
|
||||
if (window._observers) {
|
||||
const index = window._observers.indexOf(observer);
|
||||
if (index > -1) {
|
||||
window._observers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
element = document.querySelector(selector);
|
||||
if (element) {
|
||||
observer.disconnect();
|
||||
cleanup();
|
||||
resolve(element);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve(undefined);
|
||||
}, timeout);
|
||||
|
||||
// Track observer for cleanup
|
||||
if (!window._observers) {
|
||||
window._observers = [];
|
||||
}
|
||||
window._observers.push(observer);
|
||||
|
||||
// Always set timeout with default of 30 seconds
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
resolve(undefined);
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
_queuePromise(async () => {
|
||||
const videoElement = await _asyncQuerySelector("video");
|
||||
videoElement.addEventListener("pause", () => {
|
||||
VideoPause.postMessage("video paused");
|
||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
||||
videoElement.textTracks[0].mode = "hidden";
|
||||
}
|
||||
});
|
||||
videoElement.addEventListener("playing", () => {
|
||||
VideoPlaying.postMessage("video playing");
|
||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
||||
videoElement.textTracks[0].mode = "hidden";
|
||||
}
|
||||
});
|
||||
|
||||
// Add PiP event listeners for iOS
|
||||
videoElement.addEventListener("enterpictureinpicture", () => {
|
||||
PipEntered.postMessage("pip entered");
|
||||
});
|
||||
videoElement.addEventListener("leavepictureinpicture", () => {
|
||||
PipExited.postMessage("pip exited");
|
||||
});
|
||||
|
||||
|
||||
// Null check in case element was not found within timeout
|
||||
if (!videoElement) {
|
||||
console.warn("Video element not found within timeout");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicate event listener registration
|
||||
if (!videoElement._listenersAdded) {
|
||||
videoElement._listenersAdded = true;
|
||||
|
||||
videoElement.addEventListener("pause", () => {
|
||||
VideoPause.postMessage("video paused");
|
||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
||||
videoElement.textTracks[0].mode = "hidden";
|
||||
}
|
||||
});
|
||||
videoElement.addEventListener("playing", () => {
|
||||
VideoPlaying.postMessage("video playing");
|
||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
||||
videoElement.textTracks[0].mode = "hidden";
|
||||
}
|
||||
});
|
||||
|
||||
// Add PiP event listeners for iOS
|
||||
videoElement.addEventListener("enterpictureinpicture", () => {
|
||||
PipEntered.postMessage("pip entered");
|
||||
});
|
||||
videoElement.addEventListener("leavepictureinpicture", () => {
|
||||
PipExited.postMessage("pip exited");
|
||||
});
|
||||
}
|
||||
|
||||
if (!videoElement.paused) {
|
||||
VideoPlaying.postMessage("video playing");
|
||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
||||
|
|
@ -673,8 +735,35 @@ abstract class VideoStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
/// Cleans up JavaScript state and observers to prevent memory leaks.
|
||||
Future<void> _cleanupJavaScript() async {
|
||||
try {
|
||||
await videoWebViewController.runJavaScript('''
|
||||
(function() {
|
||||
// Disconnect all MutationObservers created by our app
|
||||
if (window._observers) {
|
||||
window._observers.forEach(observer => observer.disconnect());
|
||||
delete window._observers;
|
||||
}
|
||||
|
||||
// Clear window-level state
|
||||
delete window._PROMISE_QUEUE;
|
||||
delete window._queuePromise;
|
||||
delete window._asyncQuerySelector;
|
||||
delete window._injected;
|
||||
|
||||
// Note: Video element listener cleanup is unnecessary since
|
||||
// about:blank will be loaded immediately after this (see video.dart:63-65)
|
||||
})();
|
||||
''');
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup as the WebView may already be disposed
|
||||
debugPrint('Error during JavaScript cleanup: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
// Disable auto PiP when leaving so that we don't enter PiP on other screens.
|
||||
if (Platform.isAndroid) {
|
||||
SimplePip.isAutoPipAvailable.then((isAutoPipAvailable) {
|
||||
|
|
@ -688,5 +777,21 @@ abstract class VideoStoreBase with Store {
|
|||
_disposeOverlayReaction();
|
||||
_disposeVideoModeReaction();
|
||||
_disposeAndroidAutoPipReaction?.call();
|
||||
|
||||
// Clean up JavaScript state and observers
|
||||
await _cleanupJavaScript();
|
||||
|
||||
// Remove all JavaScript channels to prevent memory leaks
|
||||
try {
|
||||
videoWebViewController.removeJavaScriptChannel('Latency');
|
||||
videoWebViewController.removeJavaScriptChannel('StreamQualities');
|
||||
videoWebViewController.removeJavaScriptChannel('VideoPause');
|
||||
videoWebViewController.removeJavaScriptChannel('VideoPlaying');
|
||||
videoWebViewController.removeJavaScriptChannel('PipEntered');
|
||||
videoWebViewController.removeJavaScriptChannel('PipExited');
|
||||
} catch (e) {
|
||||
// Ignore errors if channels were already removed
|
||||
debugPrint('Error removing JavaScript channels: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,6 +231,16 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
);
|
||||
}
|
||||
|
||||
late final _$disposeAsyncAction = AsyncAction(
|
||||
'VideoStoreBase.dispose',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> dispose() {
|
||||
return _$disposeAsyncAction.run(() => super.dispose());
|
||||
}
|
||||
|
||||
late final _$VideoStoreBaseActionController = ActionController(
|
||||
name: 'VideoStoreBase',
|
||||
context: context,
|
||||
|
|
@ -296,18 +306,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.dispose',
|
||||
);
|
||||
try {
|
||||
return super.dispose();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue