diff --git a/lib/constants.dart b/lib/constants.dart index eb17910b..a09e6d9e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -6,6 +6,9 @@ const clientId = String.fromEnvironment('CLIENT_ID'); /// Twitch GQL client ID (web player, used for playback access tokens). const twitchGqlClientId = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; +/// SharedPreferences key for the last-selected stream quality. +const kLastStreamQualityKey = 'last_stream_quality'; + /// Twitch API client secret. const secret = String.fromEnvironment('SECRET'); diff --git a/lib/models/playback_access_token.dart b/lib/models/playback_access_token.dart index 244765d8..5c926a56 100644 --- a/lib/models/playback_access_token.dart +++ b/lib/models/playback_access_token.dart @@ -8,8 +8,14 @@ class PlaybackAccessToken { }); factory PlaybackAccessToken.fromGqlResponse(Map json) { - final data = json['data']['streamPlaybackAccessToken'] - as Map; + final data = + (json['data'] as Map?)?['streamPlaybackAccessToken'] + as Map?; + if (data == null) { + throw const FormatException( + 'streamPlaybackAccessToken missing from GQL response', + ); + } return PlaybackAccessToken( value: data['value'] as String, signature: data['signature'] as String, diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index f0a75617..8218c6ad 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -727,7 +727,7 @@ class _VideoChatState extends State if (settingsStore.showVideo) ...[ AspectRatio( aspectRatio: 16 / 9, - child: Container(), // Placeholder for video space + child: SizedBox(), ), ], Expanded(child: chat), diff --git a/lib/screens/channel/video/native_video_store.dart b/lib/screens/channel/video/native_video_store.dart index 704c9a99..837634f5 100644 --- a/lib/screens/channel/video/native_video_store.dart +++ b/lib/screens/channel/video/native_video_store.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:frosty/apis/twitch_api.dart'; import 'package:frosty/apis/twitch_gql_api.dart'; +import 'package:frosty/constants.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/models/playback_access_token.dart'; import 'package:frosty/models/stream.dart'; @@ -188,7 +189,7 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface { } else { SharedPreferences.getInstance().then((prefs) { if (_disposed) return; - final lastQuality = prefs.getString('last_stream_quality'); + final lastQuality = prefs.getString(kLastStreamQualityKey); if (lastQuality != null) { final index = _availableStreamQualities.indexOf(lastQuality); if (index != -1) _pendingQualityIndex = index; @@ -210,7 +211,7 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface { } catch (e) { if (_disposed) return; // Wait for stream info so we can distinguish "offline" from "broken". - await updateStreamInfo(); + await updateStreamInfo(forceUpdate: true); if (_disposed) return; runInAction(() { _loading = false; @@ -374,7 +375,6 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface { @action Future handleRefresh() async { HapticFeedback.lightImpact(); - _disposed = false; _loading = true; _paused = true; _userPaused = false; @@ -399,7 +399,7 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface { ); _controller!.addActivityListener(_handleActivityEvent); - await _initPlayer(); + _initPlayer(); updateStreamInfo(); } diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart index 3ba00545..fff3f885 100644 --- a/lib/screens/channel/video/video_overlay.dart +++ b/lib/screens/channel/video/video_overlay.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/constants.dart'; 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/native_video_store.dart'; @@ -109,7 +110,7 @@ class VideoOverlay extends StatelessWidget { videoStore.setStreamQuality(quality); SharedPreferences.getInstance().then( (prefs) => prefs.setString( - 'last_stream_quality', + kLastStreamQualityKey, quality, ), ); diff --git a/lib/screens/channel/video/video_store.dart b/lib/screens/channel/video/video_store.dart index 9634df86..f3db3b97 100644 --- a/lib/screens/channel/video/video_store.dart +++ b/lib/screens/channel/video/video_store.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:frosty/apis/twitch_api.dart'; +import 'package:frosty/constants.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/video/video_player_interface.dart'; @@ -88,7 +89,7 @@ abstract class VideoStoreBase with Store implements VideoPlayerInterface { return; } final prefs = await SharedPreferences.getInstance(); - final lastStreamQuality = prefs.getString('last_stream_quality'); + final lastStreamQuality = prefs.getString(kLastStreamQualityKey); if (lastStreamQuality == null) return; setStreamQuality(lastStreamQuality); }