From 318f32c7e60bb65613edc9f8febf1d4496fe3dd8 Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Fri, 17 Oct 2025 14:07:38 +0200 Subject: [PATCH] feat: Add fillScreen and videoFit to native player options --- .../nl/jknaapen/fladder/MainActivity.kt | 4 ++ .../fladder/api/PlayerSettingsHelper.g.kt | 53 +++++++++++++++---- .../fladder/objects/PlayerSettingsObject.kt | 9 ++++ .../nl/jknaapen/fladder/player/ExoPlayer.kt | 18 +++++-- .../nl/jknaapen/fladder/utility/Mappers.kt | 18 +++++++ .../pigeon_player_settings_provider.dart | 11 ++++ lib/src/player_settings_helper.g.dart | 44 +++++++++++---- pigeons/player_settings_pigeon.dart | 14 +++++ 8 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Mappers.kt diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt index b132db1..593ab7a 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/MainActivity.kt @@ -55,6 +55,8 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity { } callback?.invoke(Result.success(startResult)) + VideoPlayerObject.implementation.player?.stop() + VideoPlayerObject.implementation.player?.release() } } @@ -72,6 +74,8 @@ class MainActivity : AudioServiceFragmentActivity(), NativeVideoActivity { } override fun disposeActivity() { + VideoPlayerObject.implementation.player?.stop() + VideoPlayerObject.implementation.player?.release() VideoPlayerObject.currentActivity?.finish() } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt index 7cb8ef8..7bf691e 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt @@ -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) { PORTRAIT_UP(0), PORTRAIT_DOWN(1), @@ -124,7 +140,9 @@ data class PlayerSettings ( val skipForward: Long, val skipBackward: Long, val autoNextType: AutoNextType, - val acceptedOrientations: List + val acceptedOrientations: List, + val fillScreen: Boolean, + val videoFit: VideoPlayerFit ) { companion object { @@ -136,7 +154,9 @@ data class PlayerSettings ( val skipBackward = pigeonVar_list[4] as Long val autoNextType = pigeonVar_list[5] as AutoNextType val acceptedOrientations = pigeonVar_list[6] as List - 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 { @@ -148,6 +168,8 @@ data class PlayerSettings ( skipBackward, autoNextType, acceptedOrientations, + fillScreen, + videoFit, ) } override fun equals(other: Any?): Boolean { @@ -166,25 +188,30 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() { return when (type) { 129.toByte() -> { return (readValue(buffer) as Long?)?.let { - PlayerOrientations.ofRaw(it.toInt()) + VideoPlayerFit.ofRaw(it.toInt()) } } 130.toByte() -> { return (readValue(buffer) as Long?)?.let { - AutoNextType.ofRaw(it.toInt()) + PlayerOrientations.ofRaw(it.toInt()) } } 131.toByte() -> { return (readValue(buffer) as Long?)?.let { - SegmentType.ofRaw(it.toInt()) + AutoNextType.ofRaw(it.toInt()) } } 132.toByte() -> { return (readValue(buffer) as Long?)?.let { - SegmentSkip.ofRaw(it.toInt()) + SegmentType.ofRaw(it.toInt()) } } 133.toByte() -> { + return (readValue(buffer) as Long?)?.let { + SegmentSkip.ofRaw(it.toInt()) + } + } + 134.toByte() -> { return (readValue(buffer) as? List)?.let { PlayerSettings.fromList(it) } @@ -194,24 +221,28 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() { } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is PlayerOrientations -> { + is VideoPlayerFit -> { stream.write(129) writeValue(stream, value.raw) } - is AutoNextType -> { + is PlayerOrientations -> { stream.write(130) writeValue(stream, value.raw) } - is SegmentType -> { + is AutoNextType -> { stream.write(131) writeValue(stream, value.raw) } - is SegmentSkip -> { + is SegmentType -> { stream.write(132) writeValue(stream, value.raw) } - is PlayerSettings -> { + is SegmentSkip -> { stream.write(133) + writeValue(stream, value.raw) + } + is PlayerSettings -> { + stream.write(134) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/PlayerSettingsObject.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/PlayerSettingsObject.kt index 0b7fa32..ad7c4f5 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/PlayerSettingsObject.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/PlayerSettingsObject.kt @@ -6,6 +6,7 @@ import PlayerSettingsPigeon import androidx.compose.ui.graphics.Color import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map +import nl.jknaapen.fladder.utility.toExoPlayerFit import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -33,6 +34,14 @@ object PlayerSettingsObject : PlayerSettingsPigeon { 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) { settings.value = playerSettings } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt index 928d206..ab8cf4c 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt @@ -7,12 +7,15 @@ import android.view.WindowManager import androidx.activity.compose.LocalActivity import androidx.annotation.OptIn import androidx.compose.foundation.background +import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier 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.extractor.DefaultExtractorsFactory import androidx.media3.extractor.ts.TsExtractor +import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.PlayerView 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.VideoPlayerObject import nl.jknaapen.fladder.utility.AllowedOrientations +import nl.jknaapen.fladder.utility.conditional import nl.jknaapen.fladder.utility.getAudioTracks import nl.jknaapen.fladder.utility.getSubtitleTracks import kotlin.time.Duration.Companion.seconds @@ -139,7 +144,6 @@ internal fun ExoPlayer( val listener = object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { activity?.window?.let { - println("Changing playback state") if (isPlaying) { it.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } 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( - PlayerSettingsObject.settings.value?.acceptedOrientations ?: emptyList() + acceptedOrientations ) { NextUpOverlay( modifier = Modifier @@ -210,11 +218,15 @@ internal fun ExoPlayer( AndroidView( modifier = Modifier .fillMaxSize() - .background(color = Color.Black), + .background(color = Color.Black) + .conditional(!fillScreen) { + displayCutoutPadding() + }, factory = { PlayerView(it).apply { player = exoPlayer useController = false + resizeMode = videoFit layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Mappers.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Mappers.kt new file mode 100644 index 0000000..7cf24bd --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Mappers.kt @@ -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 + } diff --git a/lib/providers/settings/pigeon_player_settings_provider.dart b/lib/providers/settings/pigeon_player_settings_provider.dart index cfc6446..28f1208 100644 --- a/lib/providers/settings/pigeon_player_settings_provider.dart +++ b/lib/providers/settings/pigeon_player_settings_provider.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -51,6 +52,16 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { }, skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).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) .map( (e) => switch (e) { diff --git a/lib/src/player_settings_helper.g.dart b/lib/src/player_settings_helper.g.dart index 3fac3d6..21d4c6c 100644 --- a/lib/src/player_settings_helper.g.dart +++ b/lib/src/player_settings_helper.g.dart @@ -29,6 +29,16 @@ bool _deepEquals(Object? a, Object? b) { } +enum VideoPlayerFit { + fill, + contain, + cover, + fitWidth, + fitHeight, + none, + scaleDown, +} + enum PlayerOrientations { portraitUp, portraitDown, @@ -65,6 +75,8 @@ class PlayerSettings { required this.skipBackward, required this.autoNextType, required this.acceptedOrientations, + required this.fillScreen, + required this.videoFit, }); bool enableTunneling; @@ -81,6 +93,10 @@ class PlayerSettings { List acceptedOrientations; + bool fillScreen; + + VideoPlayerFit videoFit; + List _toList() { return [ enableTunneling, @@ -90,6 +106,8 @@ class PlayerSettings { skipBackward, autoNextType, acceptedOrientations, + fillScreen, + videoFit, ]; } @@ -106,6 +124,8 @@ class PlayerSettings { skipBackward: result[4]! as int, autoNextType: result[5]! as AutoNextType, acceptedOrientations: (result[6] as List?)!.cast(), + fillScreen: result[7]! as bool, + videoFit: result[8]! as VideoPlayerFit, ); } @@ -135,20 +155,23 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlayerOrientations) { + } else if (value is VideoPlayerFit) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is AutoNextType) { + } else if (value is PlayerOrientations) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is SegmentType) { + } else if (value is AutoNextType) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is SegmentSkip) { + } else if (value is SegmentType) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is PlayerSettings) { + } else if (value is SegmentSkip) { buffer.putUint8(133); + writeValue(buffer, value.index); + } else if (value is PlayerSettings) { + buffer.putUint8(134); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -160,17 +183,20 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: final int? value = readValue(buffer) as int?; - return value == null ? null : PlayerOrientations.values[value]; + return value == null ? null : VideoPlayerFit.values[value]; case 130: final int? value = readValue(buffer) as int?; - return value == null ? null : AutoNextType.values[value]; + return value == null ? null : PlayerOrientations.values[value]; case 131: final int? value = readValue(buffer) as int?; - return value == null ? null : SegmentType.values[value]; + return value == null ? null : AutoNextType.values[value]; case 132: final int? value = readValue(buffer) as int?; - return value == null ? null : SegmentSkip.values[value]; + return value == null ? null : SegmentType.values[value]; case 133: + final int? value = readValue(buffer) as int?; + return value == null ? null : SegmentSkip.values[value]; + case 134: return PlayerSettings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/pigeons/player_settings_pigeon.dart b/pigeons/player_settings_pigeon.dart index a9d0de7..340eff9 100644 --- a/pigeons/player_settings_pigeon.dart +++ b/pigeons/player_settings_pigeon.dart @@ -20,6 +20,8 @@ class PlayerSettings { final int skipBackward; final AutoNextType autoNextType; final List acceptedOrientations; + final bool fillScreen; + final VideoPlayerFit videoFit; const PlayerSettings({ required this.enableTunneling, @@ -29,9 +31,21 @@ class PlayerSettings { required this.skipBackward, required this.autoNextType, required this.acceptedOrientations, + required this.fillScreen, + required this.videoFit, }); } +enum VideoPlayerFit { + fill, + contain, + cover, + fitWidth, + fitHeight, + none, + scaleDown, +} + enum PlayerOrientations { portraitUp, portraitDown,