Skip to content

Commit

Permalink
Improve custom overlay player and add information about latency (#313)
Browse files Browse the repository at this point in the history
* Add latency

* Improve removing twitch overlay

* Update for player

* Remove hiding overlay for offline channels

* Improvements
  • Loading branch information
Artiu authored Dec 21, 2023
1 parent d61b938 commit c58eda9
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 19 deletions.
1 change: 1 addition & 0 deletions lib/screens/channel/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class _VideoChatState extends State<VideoChat> {
final videoOverlay = VideoOverlay(
videoStore: _videoStore,
chatStore: _chatStore,
settingsStore: settingsStore,
);

if (_videoStore.paused || _videoStore.streamInfo == null) {
Expand Down
35 changes: 34 additions & 1 deletion lib/screens/channel/video/video_overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:frosty/screens/channel/chat/details/chat_users_list.dart';
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
import 'package:frosty/screens/channel/video/video_bar.dart';
import 'package:frosty/screens/channel/video/video_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/widgets/section_header.dart';
import 'package:frosty/widgets/uptime.dart';
import 'package:intl/intl.dart';
Expand All @@ -15,11 +16,13 @@ import 'package:intl/intl.dart';
class VideoOverlay extends StatelessWidget {
final VideoStore videoStore;
final ChatStore chatStore;
final SettingsStore settingsStore;

const VideoOverlay({
super.key,
required this.videoStore,
required this.chatStore,
required this.settingsStore,
});

@override
Expand Down Expand Up @@ -92,6 +95,33 @@ class VideoOverlay extends StatelessWidget {
},
);

final latencyTooltip = Tooltip(
message: 'Latency to broadcaster',
preferBelow: false,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Observer(
builder: (context) => Text(
videoStore.latency ?? '',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(
width: 8,
),
const Icon(
Icons.speed,
),
],
),
),
);

final refreshButton = Tooltip(
message: 'Refresh',
preferBelow: false,
Expand Down Expand Up @@ -223,7 +253,7 @@ class VideoOverlay extends StatelessWidget {
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(14),
child: Row(
children: [
Tooltip(
Expand Down Expand Up @@ -294,6 +324,9 @@ class VideoOverlay extends StatelessWidget {
),
),
),
if (orientation == Orientation.landscape &&
settingsStore.showLatency)
latencyTooltip,
Tooltip(
message: 'Enter picture-in-picture',
preferBelow: false,
Expand Down
80 changes: 62 additions & 18 deletions lib/screens/channel/video/video_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ abstract class VideoStoreBase with Store {
WebViewController.fromPlatformCreationParams(_videoWebViewParams)
..setBackgroundColor(Colors.black)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'Latency',
onMessageReceived: (message) {
_latency = message.message;
},
)
..addJavaScriptChannel(
'StreamQualities',
onMessageReceived: (message) {
Expand Down Expand Up @@ -124,6 +130,9 @@ abstract class VideoStoreBase with Store {
@readonly
String _streamQuality = 'Auto';

@readonly
String? _latency;

/// The video URL to use for the webview.
String get videoUrl =>
'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty';
Expand Down Expand Up @@ -199,15 +208,61 @@ abstract class VideoStoreBase with Store {
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();
{
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;
}

void _hideDefaultOverlay() {
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 });
}
''');
}

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 });
}
''');
}

/// Initializes the video webview.
@action
Future<void> initVideo() async {
Expand All @@ -227,19 +282,8 @@ abstract class VideoStoreBase with Store {
});''',
);
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 });
}
''');
_hideDefaultOverlay();
_listenOnLatencyChanges();
}
} catch (e) {
debugPrint(e.toString());
Expand Down
18 changes: 18 additions & 0 deletions lib/screens/channel/video/video_store.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lib/screens/settings/stores/settings_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ abstract class _SettingsStoreBase with Store {
// Player defaults
static const defaultShowVideo = true;
static const defaultDefaultToHighestQuality = false;
static const defaultShowLatency = true;

// Overlay defaults
static const defaultShowOverlay = true;
Expand All @@ -72,6 +73,10 @@ abstract class _SettingsStoreBase with Store {
@observable
var defaultToHighestQuality = defaultDefaultToHighestQuality;

@JsonKey(defaultValue: defaultShowLatency)
@observable
var showLatency = defaultShowLatency;

// Overlay options
@JsonKey(defaultValue: defaultShowOverlay)
@observable
Expand All @@ -89,6 +94,7 @@ abstract class _SettingsStoreBase with Store {
void resetVideoSettings() {
showVideo = defaultShowVideo;
defaultToHighestQuality = defaultDefaultToHighestQuality;
showLatency = defaultShowLatency;

showOverlay = defaultShowOverlay;
toggleableOverlay = defaultToggleableOverlay;
Expand Down
19 changes: 19 additions & 0 deletions lib/screens/settings/stores/settings_store.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions lib/screens/settings/video_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class VideoSettings extends StatelessWidget {
onChanged: (newValue) =>
settingsStore.defaultToHighestQuality = newValue,
),
SettingsListSwitch(
title: 'Show latency',
value: settingsStore.showLatency,
onChanged: (newValue) => settingsStore.showLatency = newValue,
),
const SectionHeader('Overlay', showDivider: true),
SettingsListSwitch(
title: 'Use custom video overlay',
Expand Down

0 comments on commit c58eda9

Please sign in to comment.