Add quality change feature to custom overlay (#302)

Add menu for quality change
This commit is contained in:
Artiu 2023-11-26 05:23:21 +01:00 • committed by GitHub
parent 24dff8f795
commit e2f83aaa97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 155 additions and 14 deletions

View file

@ -46,6 +46,37 @@ class VideoOverlay extends StatelessWidget {
),
);
final videoSettingsButton = IconButton(
onPressed: () {
videoStore.updateStreamQualities();
showModalBottomSheet(
context: context,
builder: (context) => Observer(
builder: (context) => ListView.builder(
itemCount: videoStore.availableStreamQualities.length,
itemBuilder: (context, index) {
final streamQuality =
videoStore.availableStreamQualities[index];
return ListTile(
leading: videoStore.streamQuality == streamQuality
? const Icon(Icons.circle, size: 10)
: const SizedBox(
width: 10,
),
title: Text(streamQuality),
onTap: () {
videoStore.setStreamQuality(streamQuality);
Navigator.pop(context);
},
);
},
),
),
);
},
icon: const Icon(Icons.settings),
);
final refreshButton = Tooltip(
message: 'Refresh',
preferBelow: false,
@ -135,6 +166,7 @@ class VideoOverlay extends StatelessWidget {
return Stack(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
backButton,
@ -150,6 +182,7 @@ class VideoOverlay extends StatelessWidget {
if (videoStore.settingsStore.fullScreen &&
orientation == Orientation.landscape)
chatOverlayButton,
videoSettingsButton,
],
),
Center(

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
@ -39,6 +40,14 @@ abstract class VideoStoreBase with Store {
WebViewController.fromPlatformCreationParams(_videoWebViewParams)
..setBackgroundColor(Colors.black)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'StreamQualities',
onMessageReceived: (message) {
final data = jsonDecode(message.message) as List;
_availableStreamQualities =
data.map((item) => item as String).toList();
},
)
..addJavaScriptChannel(
'VideoPause',
onMessageReceived: (message) {
@ -89,11 +98,16 @@ abstract class VideoStoreBase with Store {
@readonly
StreamTwitch? _streamInfo;
/// The video URL to use for the webview. Controls will be disabled when custom overlay is enabled.
@computed
String get videoUrl => settingsStore.showOverlay
? 'https://player.twitch.tv/?channel=$userLogin&controls=false&muted=false&parent=frosty'
: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty';
@readonly
List<String> _availableStreamQualities = [];
// The current stream quality string
@readonly
String _streamQuality = 'Auto';
/// The video URL to use for the webview.
String get videoUrl =>
'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty';
VideoStoreBase({
required this.userLogin,
@ -137,6 +151,38 @@ abstract class VideoStoreBase with Store {
updateStreamInfo();
}
@action
Future<void> updateStreamQualities() 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();
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));
}
''');
} catch (e) {
debugPrint(e.toString());
}
}
@action
Future<void> setStreamQuality(String newStreamQuality) async {
final indexOfStreamQuality =
_availableStreamQualities.indexOf(newStreamQuality);
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();
''');
_streamQuality = newStreamQuality;
}
/// Initializes the video webview.
@action
Future<void> initVideo() async {
@ -155,6 +201,21 @@ abstract class VideoStoreBase with Store {
document.getElementsByTagName("video")[0].textTracks[0].mode = "hidden";
});''',
);
if (settingsStore.showOverlay) {
videoWebViewController.runJavaScript('''
{
const observer = new MutationObserver(() => {
const classificationGate = document.querySelector('[data-a-target="content-classification-gate-overlay"]');
if(classificationGate) return;
const overlay = document.querySelector('.video-player__overlay');
if(!overlay) return;
overlay.style.display = "none";
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
}
''');
}
} catch (e) {
debugPrint(e.toString());
}

View file

@ -9,14 +9,6 @@ part of 'video_store.dart';
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
mixin _$VideoStore on VideoStoreBase, Store {
Computed<String>? _$videoUrlComputed;
@override
String get videoUrl =>
(_$videoUrlComputed ??= Computed<String>(() => super.videoUrl,
name: 'VideoStoreBase.videoUrl'))
.value;
late final _$_pausedAtom =
Atom(name: 'VideoStoreBase._paused', context: context);
@ -89,6 +81,61 @@ mixin _$VideoStore on VideoStoreBase, Store {
});
}
late final _$_availableStreamQualitiesAtom =
Atom(name: 'VideoStoreBase._availableStreamQualities', context: context);
List<String> get availableStreamQualities {
_$_availableStreamQualitiesAtom.reportRead();
return super._availableStreamQualities;
}
@override
List<String> get _availableStreamQualities => availableStreamQualities;
@override
set _availableStreamQualities(List<String> value) {
_$_availableStreamQualitiesAtom
.reportWrite(value, super._availableStreamQualities, () {
super._availableStreamQualities = value;
});
}
late final _$_streamQualityAtom =
Atom(name: 'VideoStoreBase._streamQuality', context: context);
String get streamQuality {
_$_streamQualityAtom.reportRead();
return super._streamQuality;
}
@override
String get _streamQuality => streamQuality;
@override
set _streamQuality(String value) {
_$_streamQualityAtom.reportWrite(value, super._streamQuality, () {
super._streamQuality = value;
});
}
late final _$updateStreamQualitiesAsyncAction =
AsyncAction('VideoStoreBase.updateStreamQualities', context: context);
@override
Future<void> updateStreamQualities() {
return _$updateStreamQualitiesAsyncAction
.run(() => super.updateStreamQualities());
}
late final _$setStreamQualityAsyncAction =
AsyncAction('VideoStoreBase.setStreamQuality', context: context);
@override
Future<void> setStreamQuality(String newStreamQuality) {
return _$setStreamQualityAsyncAction
.run(() => super.setStreamQuality(newStreamQuality));
}
late final _$initVideoAsyncAction =
AsyncAction('VideoStoreBase.initVideo', context: context);
@ -155,7 +202,7 @@ mixin _$VideoStore on VideoStoreBase, Store {
@override
String toString() {
return '''
videoUrl: ${videoUrl}
''';
}
}