Add quality change feature to custom overlay (#302)
Add menu for quality change
This commit is contained in:
parent
24dff8f795
commit
e2f83aaa97
3 changed files with 155 additions and 14 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue