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 <cursoragent@cursor.com>
This commit is contained in:
Tommy Chow 2026-08-16 12:21:44 -07:00
parent dc444810b1
commit fcd7a304e8
5 changed files with 73 additions and 10 deletions

View file

@ -30,6 +30,8 @@ abstract class NativeVideoStoreBase
with Store with Store
implements NativeVideoPlayerInterface { implements NativeVideoPlayerInterface {
static int _nextId = 0; static int _nextId = 0;
static const _legacyWebViewInSettings =
'the legacy WebView player in Settings';
@override @override
final String userLogin; final String userLogin;
@ -465,7 +467,7 @@ abstract class NativeVideoStoreBase
// channel is expected to fail here and the overlay handles it. // channel is expected to fail here and the overlay handles it.
if (_streamInfo != null) { if (_streamInfo != null) {
_showErrorOverlay( _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(); _stallRecoveryTimer?.cancel();
final errorMessage = final errorMessage =
eventData?['message'] as String? ?? 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 errorCode = eventData?['code'] as String?;
final statusCode = eventData?['statusCode'] as int?; final statusCode = eventData?['statusCode'] as int?;
@ -751,8 +753,8 @@ abstract class NativeVideoStoreBase
_loading = false; _loading = false;
_showErrorOverlay( _showErrorOverlay(
bouncing bouncing
? 'Twitch is having trouble with this channel right now. Try again in a moment, 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 the standard player in Settings.', : 'Stream stalled. Try refreshing or switch to $_legacyWebViewInSettings.',
); );
}); });
} }

View file

@ -8,8 +8,11 @@ part 'settings_store.g.dart';
class SettingsStore extends _SettingsStoreBase with _$SettingsStore { class SettingsStore extends _SettingsStoreBase with _$SettingsStore {
SettingsStore(); SettingsStore();
factory SettingsStore.fromJson(Map<String, dynamic> json) => factory SettingsStore.fromJson(Map<String, dynamic> json) {
_$SettingsStoreFromJson(json); final store = _$SettingsStoreFromJson(json);
store._migrateNativePlayerDefault();
return store;
}
Map<String, dynamic> toJson() => _$SettingsStoreToJson(this); Map<String, dynamic> toJson() => _$SettingsStoreToJson(this);
} }
@ -65,7 +68,7 @@ abstract class _SettingsStoreBase with Store {
static const defaultShowVideo = true; static const defaultShowVideo = true;
static const defaultDefaultToHighestQuality = false; static const defaultDefaultToHighestQuality = false;
static const defaultUseTextureRendering = true; static const defaultUseTextureRendering = true;
static const defaultUseNativePlayer = false; static const defaultUseNativePlayer = true;
static const defaultShowOverlay = true; static const defaultShowOverlay = true;
static const defaultToggleableOverlay = false; static const defaultToggleableOverlay = false;
@ -88,6 +91,19 @@ abstract class _SettingsStoreBase with Store {
@observable @observable
var useNativePlayer = defaultUseNativePlayer; 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 // Overlay options
@JsonKey(defaultValue: defaultShowOverlay) @JsonKey(defaultValue: defaultShowOverlay)
@observable @observable

View file

@ -23,7 +23,8 @@ SettingsStore _$SettingsStoreFromJson(
..showVideo = json['showVideo'] as bool? ?? true ..showVideo = json['showVideo'] as bool? ?? true
..defaultToHighestQuality = json['defaultToHighestQuality'] as bool? ?? false ..defaultToHighestQuality = json['defaultToHighestQuality'] as bool? ?? false
..useTextureRendering = json['useTextureRendering'] as bool? ?? true ..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 ..showOverlay = json['showOverlay'] as bool? ?? true
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false ..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
..showLatency = json['showLatency'] as bool? ?? false ..showLatency = json['showLatency'] as bool? ?? false
@ -106,6 +107,7 @@ Map<String, dynamic> _$SettingsStoreToJson(
'defaultToHighestQuality': instance.defaultToHighestQuality, 'defaultToHighestQuality': instance.defaultToHighestQuality,
'useTextureRendering': instance.useTextureRendering, 'useTextureRendering': instance.useTextureRendering,
'useNativePlayer': instance.useNativePlayer, 'useNativePlayer': instance.useNativePlayer,
'nativePlayerV52Migrated': instance.nativePlayerV52Migrated,
'showOverlay': instance.showOverlay, 'showOverlay': instance.showOverlay,
'toggleableOverlay': instance.toggleableOverlay, 'toggleableOverlay': instance.toggleableOverlay,
'showLatency': instance.showLatency, '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 @override
void resetVideoSettings() { void resetVideoSettings() {
final _$actionInfo = _$_SettingsStoreBaseActionController.startAction( final _$actionInfo = _$_SettingsStoreBaseActionController.startAction(

View file

@ -26,9 +26,9 @@ class VideoSettings extends StatelessWidget {
), ),
if (settingsStore.showVideo) if (settingsStore.showVideo)
SettingsListSwitch( SettingsListSwitch(
title: 'Native player (experimental)', title: 'Native player',
subtitle: const Text( 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, value: settingsStore.useNativePlayer,
onChanged: (newValue) => onChanged: (newValue) =>

View file

@ -14,6 +14,7 @@ void main() {
expect(store.showVideo, isTrue); expect(store.showVideo, isTrue);
expect(store.defaultToHighestQuality, isFalse); expect(store.defaultToHighestQuality, isFalse);
expect(store.useTextureRendering, isTrue); expect(store.useTextureRendering, isTrue);
expect(store.useNativePlayer, isTrue);
expect(store.showOverlay, isTrue); expect(store.showOverlay, isTrue);
expect(store.toggleableOverlay, isFalse); expect(store.toggleableOverlay, isFalse);
expect(store.showLatency, 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', () { group('SettingsStore reset actions', () {
test('resetGeneralSettings restores general defaults', () { test('resetGeneralSettings restores general defaults', () {
final store = SettingsStore.fromJson({}); final store = SettingsStore.fromJson({});
@ -127,6 +156,7 @@ void main() {
store.showVideo = false; store.showVideo = false;
store.defaultToHighestQuality = true; store.defaultToHighestQuality = true;
store.useTextureRendering = false; store.useTextureRendering = false;
store.useNativePlayer = false;
store.showOverlay = false; store.showOverlay = false;
store.toggleableOverlay = true; store.toggleableOverlay = true;
store.showLatency = true; store.showLatency = true;
@ -136,6 +166,7 @@ void main() {
expect(store.showVideo, isTrue); expect(store.showVideo, isTrue);
expect(store.defaultToHighestQuality, isFalse); expect(store.defaultToHighestQuality, isFalse);
expect(store.useTextureRendering, isTrue); expect(store.useTextureRendering, isTrue);
expect(store.useNativePlayer, isTrue);
expect(store.showOverlay, isTrue); expect(store.showOverlay, isTrue);
expect(store.toggleableOverlay, isFalse); expect(store.toggleableOverlay, isFalse);
expect(store.showLatency, isFalse); expect(store.showLatency, isFalse);