From 6695c13455183b89b0b1ca2a7e80bef703ee8163 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Mon, 9 Mar 2026 01:45:15 -0700 Subject: [PATCH] clean up native video player implementation Extract shared stream quality key constant, add null guard to playback token parsing, fix handleRefresh race condition and concurrency, and replace Container with SizedBox placeholder. Co-Authored-By: Claude Opus 4.6 --- lib/constants.dart | 3 +++ lib/models/playback_access_token.dart | 10 ++++++++-- lib/screens/channel/channel.dart | 2 +- lib/screens/channel/video/native_video_store.dart | 8 ++++---- lib/screens/channel/video/video_overlay.dart | 3 ++- lib/screens/channel/video/video_store.dart | 3 ++- 6 files changed, 20 insertions(+), 9 deletions(-) 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); }