feat: Add fillScreen and videoFit to native player options

This commit is contained in:
PartyDonut 2025-10-17 14:07:38 +02:00
parent 63203f39df
commit 318f32c7e6
8 changed files with 148 additions and 23 deletions

View file

@ -55,6 +55,8 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity {
} }
callback?.invoke(Result.success(startResult)) callback?.invoke(Result.success(startResult))
VideoPlayerObject.implementation.player?.stop()
VideoPlayerObject.implementation.player?.release()
} }
} }
@ -72,6 +74,8 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity {
} }
override fun disposeActivity() { override fun disposeActivity() {
VideoPlayerObject.implementation.player?.stop()
VideoPlayerObject.implementation.player?.release()
VideoPlayerObject.currentActivity?.finish() VideoPlayerObject.currentActivity?.finish()
} }

View file

@ -65,6 +65,22 @@ private object PlayerSettingsHelperPigeonUtils {
} }
enum class VideoPlayerFit(val raw: Int) {
FILL(0),
CONTAIN(1),
COVER(2),
FIT_WIDTH(3),
FIT_HEIGHT(4),
NONE(5),
SCALE_DOWN(6);
companion object {
fun ofRaw(raw: Int): VideoPlayerFit? {
return values().firstOrNull { it.raw == raw }
}
}
}
enum class PlayerOrientations(val raw: Int) { enum class PlayerOrientations(val raw: Int) {
PORTRAIT_UP(0), PORTRAIT_UP(0),
PORTRAIT_DOWN(1), PORTRAIT_DOWN(1),
@ -124,7 +140,9 @@ data class PlayerSettings (
val skipForward: Long, val skipForward: Long,
val skipBackward: Long, val skipBackward: Long,
val autoNextType: AutoNextType, val autoNextType: AutoNextType,
val acceptedOrientations: List<PlayerOrientations> val acceptedOrientations: List<PlayerOrientations>,
val fillScreen: Boolean,
val videoFit: VideoPlayerFit
) )
{ {
companion object { companion object {
@ -136,7 +154,9 @@ data class PlayerSettings (
val skipBackward = pigeonVar_list[4] as Long val skipBackward = pigeonVar_list[4] as Long
val autoNextType = pigeonVar_list[5] as AutoNextType val autoNextType = pigeonVar_list[5] as AutoNextType
val acceptedOrientations = pigeonVar_list[6] as List<PlayerOrientations> val acceptedOrientations = pigeonVar_list[6] as List<PlayerOrientations>
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations) val fillScreen = pigeonVar_list[7] as Boolean
val videoFit = pigeonVar_list[8] as VideoPlayerFit
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
@ -148,6 +168,8 @@ data class PlayerSettings (
skipBackward, skipBackward,
autoNextType, autoNextType,
acceptedOrientations, acceptedOrientations,
fillScreen,
videoFit,
) )
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -166,25 +188,30 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
return when (type) { return when (type) {
129.toByte() -> { 129.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
PlayerOrientations.ofRaw(it.toInt()) VideoPlayerFit.ofRaw(it.toInt())
} }
} }
130.toByte() -> { 130.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
AutoNextType.ofRaw(it.toInt()) PlayerOrientations.ofRaw(it.toInt())
} }
} }
131.toByte() -> { 131.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
SegmentType.ofRaw(it.toInt()) AutoNextType.ofRaw(it.toInt())
} }
} }
132.toByte() -> { 132.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
SegmentSkip.ofRaw(it.toInt()) SegmentType.ofRaw(it.toInt())
} }
} }
133.toByte() -> { 133.toByte() -> {
return (readValue(buffer) as Long?)?.let {
SegmentSkip.ofRaw(it.toInt())
}
}
134.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
PlayerSettings.fromList(it) PlayerSettings.fromList(it)
} }
@ -194,24 +221,28 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
} }
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) { when (value) {
is PlayerOrientations -> { is VideoPlayerFit -> {
stream.write(129) stream.write(129)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is AutoNextType -> { is PlayerOrientations -> {
stream.write(130) stream.write(130)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is SegmentType -> { is AutoNextType -> {
stream.write(131) stream.write(131)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is SegmentSkip -> { is SegmentType -> {
stream.write(132) stream.write(132)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is PlayerSettings -> { is SegmentSkip -> {
stream.write(133) stream.write(133)
writeValue(stream, value.raw)
}
is PlayerSettings -> {
stream.write(134)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
else -> super.writeValue(stream, value) else -> super.writeValue(stream, value)

View file

@ -6,6 +6,7 @@ import PlayerSettingsPigeon
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import nl.jknaapen.fladder.utility.toExoPlayerFit
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
@ -33,6 +34,14 @@ object PlayerSettingsObject : PlayerSettingsPigeon {
settings?.autoNextType ?: AutoNextType.OFF settings?.autoNextType ?: AutoNextType.OFF
} }
val acceptedOrientations = settings.map { settings ->
settings?.acceptedOrientations ?: emptyList()
}
val fillScreen = settings.map { settings -> settings?.fillScreen ?: false }
val videoFit = settings.map { settings -> settings?.videoFit.toExoPlayerFit }
override fun sendPlayerSettings(playerSettings: PlayerSettings) { override fun sendPlayerSettings(playerSettings: PlayerSettings) {
settings.value = playerSettings settings.value = playerSettings
} }

View file

@ -7,12 +7,15 @@ import android.view.WindowManager
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -33,6 +36,7 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.extractor.DefaultExtractorsFactory import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.ts.TsExtractor import androidx.media3.extractor.ts.TsExtractor
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import io.github.peerless2012.ass.media.kt.buildWithAssSupport import io.github.peerless2012.ass.media.kt.buildWithAssSupport
@ -43,6 +47,7 @@ import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks
import nl.jknaapen.fladder.objects.PlayerSettingsObject import nl.jknaapen.fladder.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.AllowedOrientations import nl.jknaapen.fladder.utility.AllowedOrientations
import nl.jknaapen.fladder.utility.conditional
import nl.jknaapen.fladder.utility.getAudioTracks import nl.jknaapen.fladder.utility.getAudioTracks
import nl.jknaapen.fladder.utility.getSubtitleTracks import nl.jknaapen.fladder.utility.getSubtitleTracks
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -139,7 +144,6 @@ internal fun ExoPlayer(
val listener = object : Player.Listener { val listener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
activity?.window?.let { activity?.window?.let {
println("Changing playback state")
if (isPlaying) { if (isPlaying) {
it.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) it.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else { } else {
@ -200,8 +204,12 @@ internal fun ExoPlayer(
} }
} }
val acceptedOrientations by PlayerSettingsObject.acceptedOrientations.collectAsState(emptyList())
val fillScreen by PlayerSettingsObject.fillScreen.collectAsState(false)
val videoFit by PlayerSettingsObject.videoFit.collectAsState(AspectRatioFrameLayout.RESIZE_MODE_FIT)
AllowedOrientations( AllowedOrientations(
PlayerSettingsObject.settings.value?.acceptedOrientations ?: emptyList() acceptedOrientations
) { ) {
NextUpOverlay( NextUpOverlay(
modifier = Modifier modifier = Modifier
@ -210,11 +218,15 @@ internal fun ExoPlayer(
AndroidView( AndroidView(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = Color.Black), .background(color = Color.Black)
.conditional(!fillScreen) {
displayCutoutPadding()
},
factory = { factory = {
PlayerView(it).apply { PlayerView(it).apply {
player = exoPlayer player = exoPlayer
useController = false useController = false
resizeMode = videoFit
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,

View file

@ -0,0 +1,18 @@
package nl.jknaapen.fladder.utility
import VideoPlayerFit
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout
val VideoPlayerFit?.toExoPlayerFit: Int
@UnstableApi
get() = when (this) {
VideoPlayerFit.FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
VideoPlayerFit.CONTAIN -> AspectRatioFrameLayout.RESIZE_MODE_FIT
VideoPlayerFit.COVER -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
VideoPlayerFit.FIT_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
VideoPlayerFit.FIT_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
VideoPlayerFit.NONE -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
VideoPlayerFit.SCALE_DOWN -> AspectRatioFrameLayout.RESIZE_MODE_FIT
null -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}

View file

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -51,6 +52,16 @@ final pigeonPlayerSettingsSyncProvider = Provider<void>((ref) {
}, },
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds, skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds, skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
fillScreen: value.fillScreen,
videoFit: switch (value.videoFit) {
BoxFit.fill => pigeon.VideoPlayerFit.fill,
BoxFit.contain => pigeon.VideoPlayerFit.contain,
BoxFit.cover => pigeon.VideoPlayerFit.cover,
BoxFit.fitWidth => pigeon.VideoPlayerFit.fitWidth,
BoxFit.fitHeight => pigeon.VideoPlayerFit.fitHeight,
BoxFit.none => pigeon.VideoPlayerFit.none,
BoxFit.scaleDown => pigeon.VideoPlayerFit.scaleDown,
},
acceptedOrientations: (value.allowedOrientations?.toList() ?? DeviceOrientation.values) acceptedOrientations: (value.allowedOrientations?.toList() ?? DeviceOrientation.values)
.map( .map(
(e) => switch (e) { (e) => switch (e) {

View file

@ -29,6 +29,16 @@ bool _deepEquals(Object? a, Object? b) {
} }
enum VideoPlayerFit {
fill,
contain,
cover,
fitWidth,
fitHeight,
none,
scaleDown,
}
enum PlayerOrientations { enum PlayerOrientations {
portraitUp, portraitUp,
portraitDown, portraitDown,
@ -65,6 +75,8 @@ class PlayerSettings {
required this.skipBackward, required this.skipBackward,
required this.autoNextType, required this.autoNextType,
required this.acceptedOrientations, required this.acceptedOrientations,
required this.fillScreen,
required this.videoFit,
}); });
bool enableTunneling; bool enableTunneling;
@ -81,6 +93,10 @@ class PlayerSettings {
List<PlayerOrientations> acceptedOrientations; List<PlayerOrientations> acceptedOrientations;
bool fillScreen;
VideoPlayerFit videoFit;
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[ return <Object?>[
enableTunneling, enableTunneling,
@ -90,6 +106,8 @@ class PlayerSettings {
skipBackward, skipBackward,
autoNextType, autoNextType,
acceptedOrientations, acceptedOrientations,
fillScreen,
videoFit,
]; ];
} }
@ -106,6 +124,8 @@ class PlayerSettings {
skipBackward: result[4]! as int, skipBackward: result[4]! as int,
autoNextType: result[5]! as AutoNextType, autoNextType: result[5]! as AutoNextType,
acceptedOrientations: (result[6] as List<Object?>?)!.cast<PlayerOrientations>(), acceptedOrientations: (result[6] as List<Object?>?)!.cast<PlayerOrientations>(),
fillScreen: result[7]! as bool,
videoFit: result[8]! as VideoPlayerFit,
); );
} }
@ -135,20 +155,23 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is PlayerOrientations) { } else if (value is VideoPlayerFit) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is AutoNextType) { } else if (value is PlayerOrientations) {
buffer.putUint8(130); buffer.putUint8(130);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is SegmentType) { } else if (value is AutoNextType) {
buffer.putUint8(131); buffer.putUint8(131);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is SegmentSkip) { } else if (value is SegmentType) {
buffer.putUint8(132); buffer.putUint8(132);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is PlayerSettings) { } else if (value is SegmentSkip) {
buffer.putUint8(133); buffer.putUint8(133);
writeValue(buffer, value.index);
} else if (value is PlayerSettings) {
buffer.putUint8(134);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else { } else {
super.writeValue(buffer, value); super.writeValue(buffer, value);
@ -160,17 +183,20 @@ class _PigeonCodec extends StandardMessageCodec {
switch (type) { switch (type) {
case 129: case 129:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : PlayerOrientations.values[value]; return value == null ? null : VideoPlayerFit.values[value];
case 130: case 130:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : AutoNextType.values[value]; return value == null ? null : PlayerOrientations.values[value];
case 131: case 131:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentType.values[value]; return value == null ? null : AutoNextType.values[value];
case 132: case 132:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentSkip.values[value]; return value == null ? null : SegmentType.values[value];
case 133: case 133:
final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentSkip.values[value];
case 134:
return PlayerSettings.decode(readValue(buffer)!); return PlayerSettings.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);

View file

@ -20,6 +20,8 @@ class PlayerSettings {
final int skipBackward; final int skipBackward;
final AutoNextType autoNextType; final AutoNextType autoNextType;
final List<PlayerOrientations> acceptedOrientations; final List<PlayerOrientations> acceptedOrientations;
final bool fillScreen;
final VideoPlayerFit videoFit;
const PlayerSettings({ const PlayerSettings({
required this.enableTunneling, required this.enableTunneling,
@ -29,9 +31,21 @@ class PlayerSettings {
required this.skipBackward, required this.skipBackward,
required this.autoNextType, required this.autoNextType,
required this.acceptedOrientations, required this.acceptedOrientations,
required this.fillScreen,
required this.videoFit,
}); });
} }
enum VideoPlayerFit {
fill,
contain,
cover,
fitWidth,
fitHeight,
none,
scaleDown,
}
enum PlayerOrientations { enum PlayerOrientations {
portraitUp, portraitUp,
portraitDown, portraitDown,