Fix quality selection and latency stats using an asyncQuerySelector (#340)

* Fix quality select by waiting 50ms between clicks

* Use an asyncQuerySelector instead

* Use same fix for latency stats

* await
This commit is contained in:
Su-Shing Chen 2024-04-22 07:23:28 +10:00 • committed by GitHub
parent 796c4d8cc0
commit feb20302d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 139 additions and 72 deletions

View file

@ -68,14 +68,7 @@ abstract class VideoStoreBase with Store {
onMessageReceived: (message) async {
if (settingsStore.defaultToHighestQuality &&
_firstTimeSettingQuality) {
await updateStreamQualities();
await setStreamQuality(
_availableStreamQualities.firstWhere(
(quality) => quality != 'Auto',
orElse: () => 'Auto',
),
);
await _setStreamQualityIndex(1);
_firstTimeSettingQuality = false;
} else {
@ -126,9 +119,13 @@ abstract class VideoStoreBase with Store {
@readonly
List<String> _availableStreamQualities = [];
// The current stream quality string
// The current stream quality index
@readonly
String _streamQuality = 'Auto';
int _streamQualityIndex = 0;
// The current stream quality string
String get streamQuality =>
_availableStreamQualities.elementAtOrNull(_streamQualityIndex) ?? 'Auto';
@readonly
String? _latency;
@ -190,12 +187,27 @@ abstract class VideoStoreBase with Store {
try {
await videoWebViewController.runJavaScript('''
{
document.querySelector('[data-a-target="player-settings-button"]').click();
document.querySelector('[data-a-target="player-settings-menu-item-quality"]').click();
const qualities = [...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] label div')].map((el) => el.textContent);
document.querySelector('.tw-drop-down-menu-item-figure').click();
document.querySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button').click();
StreamQualities.postMessage(JSON.stringify(qualities));
const asyncQuerySelector = (selector) => new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
(async () => {
(await asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
(await asyncQuerySelector('[data-a-target="player-settings-menu-item-quality"]')).click();
await asyncQuerySelector('[data-a-target="player-settings-submenu-quality-option"] label div');
const qualities = [...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] label div')].map((el) => el.textContent);
StreamQualities.postMessage(JSON.stringify(qualities));
(await asyncQuerySelector('.tw-drop-down-menu-item-figure')).click();
(await asyncQuerySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button')).click();
})();
}
''');
} catch (e) {
@ -207,60 +219,106 @@ abstract class VideoStoreBase with Store {
Future<void> setStreamQuality(String newStreamQuality) async {
final indexOfStreamQuality =
_availableStreamQualities.indexOf(newStreamQuality);
await videoWebViewController.runJavaScript('''
await _setStreamQualityIndex(indexOfStreamQuality);
}
@action
Future<void> _setStreamQualityIndex(int newStreamQualityIndex) async {
try {
await videoWebViewController.runJavaScript('''
{
document.querySelector('[data-a-target="player-settings-button"]').click();
document.querySelector('[data-a-target="player-settings-menu-item-quality"]').click();
[...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] input')][$indexOfStreamQuality].click();
document.querySelector('.tw-drop-down-menu-item-figure').click();
document.querySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button').click();
const asyncQuerySelector = (selector) => new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
(async () => {
(await asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
(await asyncQuerySelector('[data-a-target="player-settings-menu-item-quality"]')).click();
await asyncQuerySelector('[data-a-target="player-settings-submenu-quality-option"] input');
[...document.querySelectorAll('[data-a-target="player-settings-submenu-quality-option"] input')][$newStreamQualityIndex].click();
(await asyncQuerySelector('.tw-drop-down-menu-item-figure')).click();
(await asyncQuerySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button')).click();
})();
}
''');
_streamQuality = newStreamQuality;
_streamQualityIndex = newStreamQualityIndex;
} catch (e) {
debugPrint(e.toString());
}
}
void _hideDefaultOverlay() {
videoWebViewController.runJavaScript('''
{
const hideElements = (...el) => {
el.forEach((el) => {
el?.style.setProperty("display", "none", "important");
})
Future<void> _hideDefaultOverlay() async {
try {
await videoWebViewController.runJavaScript('''
{
const hideElements = (...el) => {
el.forEach((el) => {
el?.style.setProperty("display", "none", "important");
})
}
const hide = () => {
const topBar = document.querySelector(".top-bar");
const playerControls = document.querySelector(".player-controls");
const channelDisclosures = document.querySelector("#channel-player-disclosures");
hideElements(topBar, playerControls, channelDisclosures);
}
const observer = new MutationObserver(() => {
const videoOverlay = document.querySelector('.video-player__overlay');
if(!videoOverlay) return;
hide();
const videoOverlayObserver = new MutationObserver(hide);
videoOverlayObserver.observe(videoOverlay, { childList: true, subtree: true });
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
}
const hide = () => {
const topBar = document.querySelector(".top-bar");
const playerControls = document.querySelector(".player-controls");
const channelDisclosures = document.querySelector("#channel-player-disclosures");
hideElements(topBar, playerControls, channelDisclosures);
}
const observer = new MutationObserver(() => {
const videoOverlay = document.querySelector('.video-player__overlay');
if(!videoOverlay) return;
hide();
const videoOverlayObserver = new MutationObserver(hide);
videoOverlayObserver.observe(videoOverlay, { childList: true, subtree: true });
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
}
''');
''');
} catch (e) {
debugPrint(e.toString());
}
}
void _listenOnLatencyChanges() {
videoWebViewController.runJavaScript('''
{
document.querySelector('[data-a-target="player-settings-button"]').click();
document.querySelector('[data-a-target="player-settings-menu-item-advanced"]').click();
document.querySelector('[data-a-target="player-settings-submenu-advanced-video-stats"] input').click();
document.querySelector('.tw-drop-down-menu-item-figure').click();
document.querySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button').click();
document.querySelector('[data-a-target="player-overlay-video-stats"]').style.display = "none";
const observer = new MutationObserver((changes) => {
Latency.postMessage(changes[0].target.textContent);
})
observer.observe(document.querySelector('[aria-label="Latency To Broadcaster"]'), { characterData: true, attributes: false, childList: false, subtree: true });
Future<void> _listenOnLatencyChanges() async {
try {
await videoWebViewController.runJavaScript('''
{
const asyncQuerySelector = (selector) => new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
''');
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
(async () => {
(await asyncQuerySelector('[data-a-target="player-settings-button"]')).click();
(await asyncQuerySelector('[data-a-target="player-settings-menu-item-advanced"]')).click();
(await asyncQuerySelector('[data-a-target="player-settings-submenu-advanced-video-stats"] input')).click();
(await asyncQuerySelector('.tw-drop-down-menu-item-figure')).click();
(await asyncQuerySelector('[data-a-target="player-settings-menu"] [role="menuitem"] button')).click();
(await asyncQuerySelector('[data-a-target="player-overlay-video-stats"]')).style.display = "none";
const observer = new MutationObserver((changes) => {
Latency.postMessage(changes[0].target.textContent);
})
observer.observe(document.querySelector('[aria-label="Latency To Broadcaster"]'), { characterData: true, attributes: false, childList: false, subtree: true });
})();
}
''');
} catch (e) {
debugPrint(e.toString());
}
}
/// Initializes the video webview.
@ -282,8 +340,8 @@ abstract class VideoStoreBase with Store {
});''',
);
if (settingsStore.showOverlay) {
_hideDefaultOverlay();
_listenOnLatencyChanges();
await _hideDefaultOverlay();
await _listenOnLatencyChanges();
}
} catch (e) {
debugPrint(e.toString());

View file

@ -100,21 +100,21 @@ mixin _$VideoStore on VideoStoreBase, Store {
});
}
late final _$_streamQualityAtom =
Atom(name: 'VideoStoreBase._streamQuality', context: context);
late final _$_streamQualityIndexAtom =
Atom(name: 'VideoStoreBase._streamQualityIndex', context: context);
String get streamQuality {
_$_streamQualityAtom.reportRead();
return super._streamQuality;
int get streamQualityIndex {
_$_streamQualityIndexAtom.reportRead();
return super._streamQualityIndex;
}
@override
String get _streamQuality => streamQuality;
int get _streamQualityIndex => streamQualityIndex;
@override
set _streamQuality(String value) {
_$_streamQualityAtom.reportWrite(value, super._streamQuality, () {
super._streamQuality = value;
set _streamQualityIndex(int value) {
_$_streamQualityIndexAtom.reportWrite(value, super._streamQualityIndex, () {
super._streamQualityIndex = value;
});
}
@ -154,6 +154,15 @@ mixin _$VideoStore on VideoStoreBase, Store {
.run(() => super.setStreamQuality(newStreamQuality));
}
late final _$_setStreamQualityIndexAsyncAction =
AsyncAction('VideoStoreBase._setStreamQualityIndex', context: context);
@override
Future<void> _setStreamQualityIndex(int newStreamQualityIndex) {
return _$_setStreamQualityIndexAsyncAction
.run(() => super._setStreamQualityIndex(newStreamQualityIndex));
}
late final _$initVideoAsyncAction =
AsyncAction('VideoStoreBase.initVideo', context: context);