enable native video player on Android

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tommy Chow 2026-03-08 19:42:53 -07:00
parent d048875490
commit 8d5ccb699e
7 changed files with 72 additions and 11 deletions

View file

@ -0,0 +1,32 @@
package com.tommychow.frosty
import android.webkit.CookieManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class CookieExtractorPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, "frosty/cookie_extractor")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "extractTwitchAuthToken") {
val cookies = CookieManager.getInstance().getCookie("https://twitch.tv")
val authToken = cookies?.split(";")
?.map { it.trim() }
?.firstOrNull { it.startsWith("auth-token=") }
?.substringAfter("auth-token=")
result.success(authToken)
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

View file

@ -1,6 +1,11 @@
package com.tommychow.frosty
import cl.puntito.simple_pip_mode.PipCallbackHelperActivityWrapper
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: PipCallbackHelperActivityWrapper() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(CookieExtractorPlugin())
}
}

View file

@ -79,7 +79,7 @@ class _VideoChatState extends State<VideoChat>
);
late final VideoPlayerInterface _videoStore =
Platform.isIOS && context.settingsStore.useNativePlayer
context.settingsStore.useNativePlayer
? NativeVideoStore(
userLogin: widget.userLogin,
userId: widget.userId,

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:better_native_video_player/better_native_video_player.dart';
import 'package:flutter/material.dart';
@ -13,6 +14,7 @@ import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simple_pip_mode/simple_pip.dart';
part 'native_video_store.g.dart';
@ -43,6 +45,9 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
var _firstTimeSettingQuality = true;
int? _pendingQualityIndex;
final _pip = SimplePip();
ReactionDisposer? _disposeAndroidAutoPipReaction;
StreamSubscription<bool>? _pipSub;
StreamSubscription<List<NativeVideoPlayerQuality>>? _qualitiesSub;
@ -96,6 +101,16 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
_scheduleOverlayHide();
updateStreamInfo();
_initPlayer();
if (Platform.isAndroid) {
_disposeAndroidAutoPipReaction = autorun((_) async {
if (settingsStore.showVideo && await SimplePip.isAutoPipAvailable) {
_pip.setAutoPipMode();
} else {
_pip.setAutoPipMode(autoEnter: false);
}
});
}
}
@action
@ -337,16 +352,20 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
@override
void requestPictureInPicture() {
_controller?.enterPictureInPicture();
if (Platform.isAndroid) {
_pip.enterPipMode(autoEnter: true);
} else {
_controller?.enterPictureInPicture();
}
}
@override
@action
void togglePictureInPicture() {
if (_isInPipMode) {
if (Platform.isIOS && _isInPipMode) {
_controller?.exitPictureInPicture();
} else {
_controller?.enterPictureInPicture();
requestPictureInPicture();
}
}
@ -434,10 +453,17 @@ abstract class NativeVideoStoreBase with Store implements VideoPlayerInterface {
@override
@action
void dispose() {
if (Platform.isAndroid) {
SimplePip.isAutoPipAvailable.then((isAutoPipAvailable) {
if (isAutoPipAvailable) _pip.setAutoPipMode(autoEnter: false);
});
}
_overlayTimer?.cancel();
_latencyTimer?.cancel();
_pipSub?.cancel();
_qualitiesSub?.cancel();
_disposeAndroidAutoPipReaction?.call();
_controller?.removeActivityListener(_handleActivityEvent);
_controller?.dispose();

View file

@ -24,11 +24,11 @@ class VideoSettings extends StatelessWidget {
value: settingsStore.showVideo,
onChanged: (newValue) => settingsStore.showVideo = newValue,
),
if (Platform.isIOS && settingsStore.showVideo)
if (settingsStore.showVideo)
SettingsListSwitch(
title: 'Native player (experimental)',
subtitle: const Text(
'Native video player with auto picture-in-picture and quality selection.',
'More performant video player with auto picture-in-picture and quality selection.',
),
value: settingsStore.useNativePlayer,
onChanged: (newValue) =>
@ -74,7 +74,8 @@ class VideoSettings extends StatelessWidget {
'Allows switching between Twitch\'s overlay and the custom overlay.',
),
value: settingsStore.toggleableOverlay,
onChanged: (newValue) => settingsStore.toggleableOverlay = newValue,
onChanged: (newValue) =>
settingsStore.toggleableOverlay = newValue,
),
SettingsListSwitch(
title: 'Show latency',

View file

@ -1,12 +1,9 @@
import 'dart:io';
import 'package:flutter/services.dart';
class CookieExtractor {
static const _channel = MethodChannel('frosty/cookie_extractor');
static Future<String?> extractTwitchAuthToken() async {
if (!Platform.isIOS) return null;
return await _channel.invokeMethod<String>('extractTwitchAuthToken');
}
}

View file

@ -102,7 +102,7 @@ packages:
description:
path: "."
ref: frosty-patches
resolved-ref: "1a1330c815f09f1b3ff2ca11442276315f12030c"
resolved-ref: "72ef59c95ef282029c2ac6d0bd782f153f2fd9ef"
url: "https://github.com/tommyxchow/flutter-native-video-player.git"
source: git
version: "0.4.11"