diff --git a/assets/release-notes.md b/assets/release-notes.md index 6d112234..fd17a0e7 100644 --- a/assets/release-notes.md +++ b/assets/release-notes.md @@ -1,13 +1,3 @@ -## v5.3.1 - -October 12, 2026 - -### Bug Fixes - -- Fixed the player getting stuck on a Source error after the screen turns off or the network switches — it now reloads automatically instead of waiting for a manual refresh. - ---- - ## v5.3.0 October 12, 2026 @@ -26,3 +16,13 @@ October 7, 2026 - Fixed tag stream lists reshuffling when loading more pages by paginating with Twitch’s sorted tag directory (GQL) instead of scanning Helix. - Fixed follow/unfollow failing with an integrity check error while logged in by confirming via a short Twitch WebView when needed. + +--- + +## v5.2.8 + +October 7, 2026 + +### New Features + +- Added follow and unfollow for live and offline channels from the long-press actions sheet (needs a linked Twitch web session). diff --git a/lib/screens/channel/video/native_video_store.dart b/lib/screens/channel/video/native_video_store.dart index 31077620..1af97005 100644 --- a/lib/screens/channel/video/native_video_store.dart +++ b/lib/screens/channel/video/native_video_store.dart @@ -15,7 +15,6 @@ import 'package:frosty/models/playback_access_token.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/video/chat_latency_sync.dart'; import 'package:frosty/screens/channel/video/native_video_player_interface.dart'; -import 'package:frosty/screens/channel/video/playback_error_recovery.dart'; import 'package:frosty/screens/channel/video/stream_info_poller.dart'; import 'package:frosty/screens/channel/video/video_timing_constants.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; @@ -684,44 +683,26 @@ abstract class NativeVideoStoreBase return; } - // Mid-stream failures (expired HLS session, ExoPlayer "Source error" - // after screen-off / network switch, CDN blips). A fresh token + reload - // usually fixes these — only pin the Reload overlay once retries are - // spent. 4xx refreshes immediately; network/source waits briefly so the - // interface can come back. - if (shouldAutoRecoverFromPlayerError( - refreshCapReached: _refreshCapReached(), - errorCode: errorCode, - )) { + // 4xx on the HLS fetch means the current playback session is gone — a + // fresh GQL token + new query params can land on a different CDN edge + // with a working manifest. Skip the user-facing error and retry + // immediately (handleRefresh updates _lastRefreshTime itself). + if (statusCode != null && + statusCode >= 400 && + statusCode < 500 && + !_refreshCapReached()) { if (_userPaused) { // Don't yank a user-paused stream back to life — the refresh would // reload with autoplay. Defer until they resume. _pendingRefreshOnResume = true; return; } - final attempt = _totalRefreshAttempts + 1; - final delay = playerErrorRetryDelay(statusCode: statusCode); debugPrint( - 'NativeVideoStore: player error' - '${statusCode != null ? ' (HTTP $statusCode)' : ''}' - '${errorMessage.isNotEmpty ? ' — $errorMessage' : ''}, ' - 'auto-refresh in ${delay.inMilliseconds}ms ' - '(attempt $attempt/${VideoTimingConstants.maxRefreshAttempts})', + 'NativeVideoStore: $statusCode on HLS fetch, refreshing with fresh ' + 'token (attempt ${_totalRefreshAttempts + 1}/${VideoTimingConstants.maxRefreshAttempts})', ); - _totalRefreshAttempts = attempt; - _loading = true; - _error = null; - _initRetryTimer?.cancel(); - final generation = _initGeneration; - if (delay == Duration.zero) { - _handleRefresh(userInitiated: false); - } else { - _initRetryTimer = Timer(delay, () { - if (_disposed || generation != _initGeneration) return; - // _handleRefresh parks behind _pendingRefreshOnResume when paused. - _handleRefresh(userInitiated: false); - }); - } + _totalRefreshAttempts++; + _handleRefresh(userInitiated: false); return; } @@ -1329,22 +1310,9 @@ abstract class NativeVideoStoreBase if (_isInPipMode) return; _highLatencyCount = 0; updateStreamInfo(forceUpdate: true); - - if (_userPaused || _initializing) return; - - // After screen-off / a network drop the native player is often in an - // error state (ExoPlayer "Source error"). play() alone cannot revive a - // dead session — refresh like the Reload button would. - if (_error != null || - _pendingRefreshOnResume || - _controller == null || - !_controllerInitialized) { - _pendingRefreshOnResume = false; - unawaited(_handleRefresh(userInitiated: false)); - return; + if (!_userPaused && !_initializing && _controller != null) { + _controller!.play(); } - - _controller!.play(); } @override diff --git a/lib/screens/channel/video/playback_error_recovery.dart b/lib/screens/channel/video/playback_error_recovery.dart deleted file mode 100644 index 72b83815..00000000 --- a/lib/screens/channel/video/playback_error_recovery.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:frosty/screens/channel/video/video_timing_constants.dart'; - -/// Whether [NativeVideoStore] should auto-refresh after a mid-stream player -/// error instead of pinning the Reload overlay. -/// -/// ExoPlayer's "Source error" (screen-off / network switch) arrives without a -/// status code, so callers must not rely on 4xx alone. [mediaServicesWereReset] -/// has its own dispose+rebuild path and is excluded here. -bool shouldAutoRecoverFromPlayerError({ - required bool refreshCapReached, - String? errorCode, -}) { - if (refreshCapReached) return false; - if (errorCode == 'mediaServicesWereReset') return false; - return true; -} - -/// Delay before an automatic refresh for [statusCode]. -/// -/// Token/CDN 4xx can refresh immediately; network/source failures get a short -/// pause so the interface can come back after a switch or screen wake. -Duration playerErrorRetryDelay({int? statusCode}) { - if (statusCode != null && statusCode >= 400 && statusCode < 500) { - return Duration.zero; - } - return VideoTimingConstants.initRetryDelay; -} diff --git a/pubspec.yaml b/pubspec.yaml index 1899d872..5d8af15a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 5.3.1+81 +version: 5.3.0+80 environment: sdk: ^3.9.0 diff --git a/test/screens/channel/video/playback_error_recovery_test.dart b/test/screens/channel/video/playback_error_recovery_test.dart deleted file mode 100644 index adc80903..00000000 --- a/test/screens/channel/video/playback_error_recovery_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:frosty/screens/channel/video/playback_error_recovery.dart'; -import 'package:frosty/screens/channel/video/video_timing_constants.dart'; - -void main() { - group('shouldAutoRecoverFromPlayerError', () { - test('recovers Source error / generic failures under the refresh cap', () { - expect( - shouldAutoRecoverFromPlayerError(refreshCapReached: false), - isTrue, - ); - expect( - shouldAutoRecoverFromPlayerError( - refreshCapReached: false, - errorCode: null, - ), - isTrue, - ); - }); - - test('stops once the refresh cap is reached', () { - expect( - shouldAutoRecoverFromPlayerError(refreshCapReached: true), - isFalse, - ); - }); - - test('defers mediaServicesWereReset to its dedicated rebuild path', () { - expect( - shouldAutoRecoverFromPlayerError( - refreshCapReached: false, - errorCode: 'mediaServicesWereReset', - ), - isFalse, - ); - }); - }); - - group('playerErrorRetryDelay', () { - test('refreshes 4xx token failures immediately', () { - expect(playerErrorRetryDelay(statusCode: 403), Duration.zero); - expect(playerErrorRetryDelay(statusCode: 404), Duration.zero); - }); - - test('waits briefly for network / Source errors without status', () { - expect( - playerErrorRetryDelay(), - VideoTimingConstants.initRetryDelay, - ); - expect( - playerErrorRetryDelay(statusCode: 500), - VideoTimingConstants.initRetryDelay, - ); - }); - }); -}