From fcd7a304e8b99585b3393eb24646eb66aa679a0c Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 16 Aug 2026 12:21:44 -0700 Subject: [PATCH] feat(video): make the native player the default 5.1.0 already serialized useNativePlayer: false, so existing installs need a one-shot migrate. WebView stays as a legacy opt-out. Co-authored-by: Cursor --- .../channel/video/native_video_store.dart | 10 +++--- .../settings/stores/settings_store.dart | 22 +++++++++++-- .../settings/stores/settings_store.g.dart | 16 +++++++++- lib/screens/settings/video_settings.dart | 4 +-- .../settings/stores/settings_store_test.dart | 31 +++++++++++++++++++ 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/lib/screens/channel/video/native_video_store.dart b/lib/screens/channel/video/native_video_store.dart index e32a276c..faeb5c59 100644 --- a/lib/screens/channel/video/native_video_store.dart +++ b/lib/screens/channel/video/native_video_store.dart @@ -30,6 +30,8 @@ abstract class NativeVideoStoreBase with Store implements NativeVideoPlayerInterface { static int _nextId = 0; + static const _legacyWebViewInSettings = + 'the legacy WebView player in Settings'; @override final String userLogin; @@ -465,7 +467,7 @@ abstract class NativeVideoStoreBase // channel is expected to fail here and the overlay handles it. if (_streamInfo != null) { _showErrorOverlay( - 'Native player failed to load. Try the standard player in Settings.', + 'Native player failed to load. Try $_legacyWebViewInSettings.', ); } }); @@ -560,7 +562,7 @@ abstract class NativeVideoStoreBase _stallRecoveryTimer?.cancel(); final errorMessage = eventData?['message'] as String? ?? - 'Playback error. Try refreshing or switch to the standard player in Settings.'; + 'Playback error. Try refreshing or switch to $_legacyWebViewInSettings.'; final errorCode = eventData?['code'] as String?; final statusCode = eventData?['statusCode'] as int?; @@ -751,8 +753,8 @@ abstract class NativeVideoStoreBase _loading = false; _showErrorOverlay( bouncing - ? 'Twitch is having trouble with this channel right now. Try again in a moment, or switch to the standard player in Settings.' - : 'Stream stalled. Try refreshing or switch to the standard player in Settings.', + ? 'Twitch is having trouble with this channel right now. Try again in a moment, or switch to $_legacyWebViewInSettings.' + : 'Stream stalled. Try refreshing or switch to $_legacyWebViewInSettings.', ); }); } diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index 333f4e38..9330ad83 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -8,8 +8,11 @@ part 'settings_store.g.dart'; class SettingsStore extends _SettingsStoreBase with _$SettingsStore { SettingsStore(); - factory SettingsStore.fromJson(Map json) => - _$SettingsStoreFromJson(json); + factory SettingsStore.fromJson(Map json) { + final store = _$SettingsStoreFromJson(json); + store._migrateNativePlayerDefault(); + return store; + } Map toJson() => _$SettingsStoreToJson(this); } @@ -65,7 +68,7 @@ abstract class _SettingsStoreBase with Store { static const defaultShowVideo = true; static const defaultDefaultToHighestQuality = false; static const defaultUseTextureRendering = true; - static const defaultUseNativePlayer = false; + static const defaultUseNativePlayer = true; static const defaultShowOverlay = true; static const defaultToggleableOverlay = false; @@ -88,6 +91,19 @@ abstract class _SettingsStoreBase with Store { @observable var useNativePlayer = defaultUseNativePlayer; + /// 5.2.0 made native the default. 5.1.0 already wrote + /// `useNativePlayer: false` into saved settings, so a Dart default + /// change alone would leave existing installs on WebView. + @JsonKey(defaultValue: false) + var nativePlayerV52Migrated = false; + + @action + void _migrateNativePlayerDefault() { + if (nativePlayerV52Migrated) return; + useNativePlayer = true; + nativePlayerV52Migrated = true; + } + // Overlay options @JsonKey(defaultValue: defaultShowOverlay) @observable diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index 2e713294..4be11e2e 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -23,7 +23,8 @@ SettingsStore _$SettingsStoreFromJson( ..showVideo = json['showVideo'] as bool? ?? true ..defaultToHighestQuality = json['defaultToHighestQuality'] as bool? ?? false ..useTextureRendering = json['useTextureRendering'] as bool? ?? true - ..useNativePlayer = json['useNativePlayer'] as bool? ?? false + ..useNativePlayer = json['useNativePlayer'] as bool? ?? true + ..nativePlayerV52Migrated = json['nativePlayerV52Migrated'] as bool? ?? false ..showOverlay = json['showOverlay'] as bool? ?? true ..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false ..showLatency = json['showLatency'] as bool? ?? false @@ -106,6 +107,7 @@ Map _$SettingsStoreToJson( 'defaultToHighestQuality': instance.defaultToHighestQuality, 'useTextureRendering': instance.useTextureRendering, 'useNativePlayer': instance.useNativePlayer, + 'nativePlayerV52Migrated': instance.nativePlayerV52Migrated, 'showOverlay': instance.showOverlay, 'toggleableOverlay': instance.toggleableOverlay, 'showLatency': instance.showLatency, @@ -1159,6 +1161,18 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { } } + @override + void _migrateNativePlayerDefault() { + final _$actionInfo = _$_SettingsStoreBaseActionController.startAction( + name: '_SettingsStoreBase._migrateNativePlayerDefault', + ); + try { + return super._migrateNativePlayerDefault(); + } finally { + _$_SettingsStoreBaseActionController.endAction(_$actionInfo); + } + } + @override void resetVideoSettings() { final _$actionInfo = _$_SettingsStoreBaseActionController.startAction( diff --git a/lib/screens/settings/video_settings.dart b/lib/screens/settings/video_settings.dart index 4e2aedd4..3eb39ac8 100644 --- a/lib/screens/settings/video_settings.dart +++ b/lib/screens/settings/video_settings.dart @@ -26,9 +26,9 @@ class VideoSettings extends StatelessWidget { ), if (settingsStore.showVideo) SettingsListSwitch( - title: 'Native player (experimental)', + title: 'Native player', subtitle: const Text( - 'More performant video player with auto picture-in-picture and quality selection.', + 'Picture-in-Picture, quality selection, and lower latency. Turn off to use the legacy WebView player.', ), value: settingsStore.useNativePlayer, onChanged: (newValue) => diff --git a/test/screens/settings/stores/settings_store_test.dart b/test/screens/settings/stores/settings_store_test.dart index d2fad875..12e5c5b9 100644 --- a/test/screens/settings/stores/settings_store_test.dart +++ b/test/screens/settings/stores/settings_store_test.dart @@ -14,6 +14,7 @@ void main() { expect(store.showVideo, isTrue); expect(store.defaultToHighestQuality, isFalse); expect(store.useTextureRendering, isTrue); + expect(store.useNativePlayer, isTrue); expect(store.showOverlay, isTrue); expect(store.toggleableOverlay, isFalse); expect(store.showLatency, isFalse); @@ -102,6 +103,34 @@ void main() { }); }); + group('native player default migration', () { + test('5.1.0 WebView default is flipped on once', () { + final store = SettingsStore.fromJson({'useNativePlayer': false}); + + expect(store.useNativePlayer, isTrue); + expect(store.nativePlayerV52Migrated, isTrue); + }); + + test('opt-out is kept after the one-shot migration', () { + final store = SettingsStore.fromJson({ + 'useNativePlayer': false, + 'nativePlayerV52Migrated': true, + }); + + expect(store.useNativePlayer, isFalse); + }); + + test('roundtrip after opt-out stays on WebView', () { + final store = SettingsStore.fromJson({}); + store.useNativePlayer = false; + + final restored = SettingsStore.fromJson(store.toJson()); + + expect(restored.useNativePlayer, isFalse); + expect(restored.nativePlayerV52Migrated, isTrue); + }); + }); + group('SettingsStore reset actions', () { test('resetGeneralSettings restores general defaults', () { final store = SettingsStore.fromJson({}); @@ -127,6 +156,7 @@ void main() { store.showVideo = false; store.defaultToHighestQuality = true; store.useTextureRendering = false; + store.useNativePlayer = false; store.showOverlay = false; store.toggleableOverlay = true; store.showLatency = true; @@ -136,6 +166,7 @@ void main() { expect(store.showVideo, isTrue); expect(store.defaultToHighestQuality, isFalse); expect(store.useTextureRendering, isTrue); + expect(store.useNativePlayer, isTrue); expect(store.showOverlay, isTrue); expect(store.toggleableOverlay, isFalse); expect(store.showLatency, isFalse);