From ae45bc69761e0efc2e5c494596345be41a20b87a Mon Sep 17 00:00:00 2001 From: Sirulex Date: Thu, 11 Jun 2026 10:22:16 +0200 Subject: [PATCH 1/4] remove hdr10plus metadata if dovi --- android/app/build.gradle | 2 + .../fladder/player/DvBitstreamSanitizer.kt | 113 +++++++++++++++ .../player/DvSanitizingRenderersFactory.kt | 107 ++++++++++++++ .../nl/jknaapen/fladder/player/ExoPlayer.kt | 2 +- .../player/DvBitstreamSanitizerTest.kt | 131 ++++++++++++++++++ 5 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt create mode 100644 android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt create mode 100644 android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index c79846f7e..cd40645f4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -140,6 +140,8 @@ dependencies { implementation("org.jellyfin.media3:media3-ffmpeg-decoder:$media3_version") implementation("io.github.peerless2012:ass-media:0.3.0") + testImplementation("junit:junit:4.13.2") + //UI implementation("io.github.rabehx:iconsax-compose:0.0.5") implementation("io.coil-kt.coil3:coil-compose:3.3.0") diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt new file mode 100644 index 000000000..81920b42f --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt @@ -0,0 +1,113 @@ +package nl.jknaapen.fladder.player + +import java.nio.ByteBuffer + +/** + * In-place sanitizer for HEVC Annex B buffers carrying both Dolby Vision and HDR10+ + * dynamic metadata. Some Android TV chipsets fail when a native DV decoder also + * receives in-band HDR10+ SEI, so keep only the dynamic metadata for the selected + * decode path. + */ +object DvBitstreamSanitizer { + private const val NAL_TYPE_PREFIX_SEI = 39 + private const val NAL_TYPE_SUFFIX_SEI = 40 + private const val NAL_TYPE_UNSPEC62 = 62 + private const val NAL_TYPE_UNSPEC63 = 63 + + private const val SEI_PAYLOAD_TYPE_ITU_T_T35 = 4 + + fun sanitize(data: ByteBuffer, stripHdr10PlusSei: Boolean, stripDvRpu: Boolean) { + val startPos = data.position() + val limit = data.limit() + var writePos = startPos + var nalStartIndex = -1 + var startCodeLen = 0 + + var i = startPos + while (i <= limit) { + val atEnd = i == limit + var foundStartCode = false + var nextStartCodeLen = 0 + if (!atEnd && i + 2 < limit && data.get(i).toInt() == 0 && data.get(i + 1).toInt() == 0) { + if (data.get(i + 2).toInt() == 1) { + foundStartCode = true + nextStartCodeLen = 3 + } else if (data.get(i + 2).toInt() == 0 && i + 3 < limit && data.get(i + 3).toInt() == 1) { + foundStartCode = true + nextStartCodeLen = 4 + } + } + + if (foundStartCode || atEnd) { + if (nalStartIndex >= 0) { + val nalDataStart = nalStartIndex + startCodeLen + val nalEnd = i + var strip = false + + if (nalEnd - nalDataStart >= 2) { + val nalUnitType = (data.get(nalDataStart).toInt() and 0x7E) shr 1 + strip = when (nalUnitType) { + NAL_TYPE_UNSPEC62, NAL_TYPE_UNSPEC63 -> stripDvRpu + NAL_TYPE_PREFIX_SEI, NAL_TYPE_SUFFIX_SEI -> + stripHdr10PlusSei && isHdr10PlusSeiNalUnit(data, nalDataStart + 2, nalEnd) + else -> false + } + } + + if (!strip) { + if (writePos != nalStartIndex) { + for (j in nalStartIndex until nalEnd) { + data.put(writePos++, data.get(j)) + } + } else { + writePos = nalEnd + } + } + } + nalStartIndex = i + startCodeLen = nextStartCodeLen + i += if (nextStartCodeLen > 0) nextStartCodeLen else 1 + } else { + i++ + } + } + + data.limit(writePos) + data.position(startPos) + } + + private fun isHdr10PlusSeiNalUnit(data: ByteBuffer, rbspStart: Int, nalEnd: Int): Boolean { + var pos = rbspStart + if (pos >= nalEnd) return false + + var payloadType = 0 + while (pos < nalEnd) { + val value = data.get(pos++).toInt() and 0xFF + payloadType += value + if (value != 0xFF) break + } + + var payloadSize = 0 + while (pos < nalEnd) { + val value = data.get(pos++).toInt() and 0xFF + payloadSize += value + if (value != 0xFF) break + } + + if (payloadType != SEI_PAYLOAD_TYPE_ITU_T_T35 || payloadSize < 7 || pos + 7 > nalEnd) { + return false + } + + val countryCode = data.get(pos).toInt() and 0xFF + val providerCode = ((data.get(pos + 1).toInt() and 0xFF) shl 8) or (data.get(pos + 2).toInt() and 0xFF) + val orientedCode = ((data.get(pos + 3).toInt() and 0xFF) shl 8) or (data.get(pos + 4).toInt() and 0xFF) + val appIdentifier = data.get(pos + 5).toInt() and 0xFF + val appVersion = data.get(pos + 6).toInt() and 0xFF + + return countryCode == 0xB5 && + providerCode == 0x003C && + orientedCode == 0x0001 && + appIdentifier == 4 && + (appVersion == 0 || appVersion == 1) + } +} diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt new file mode 100644 index 000000000..b8381c390 --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt @@ -0,0 +1,107 @@ +package nl.jknaapen.fladder.player + +import android.content.Context +import android.os.Handler +import android.util.Log +import androidx.annotation.OptIn +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.media3.decoder.DecoderInputBuffer +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.Renderer +import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter +import androidx.media3.exoplayer.mediacodec.MediaCodecSelector +import androidx.media3.exoplayer.video.MediaCodecVideoRenderer +import androidx.media3.exoplayer.video.VideoRendererEventListener + +private const val TAG = "FladderPlayer" + +@OptIn(UnstableApi::class) +class DvSanitizingRenderersFactory(context: Context) : DefaultRenderersFactory(context) { + override fun buildVideoRenderers( + context: Context, + extensionRendererMode: Int, + mediaCodecSelector: MediaCodecSelector, + enableDecoderFallback: Boolean, + eventHandler: Handler, + eventListener: VideoRendererEventListener, + allowedVideoJoiningTimeMs: Long, + out: ArrayList + ) { + super.buildVideoRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + enableDecoderFallback, + eventHandler, + eventListener, + allowedVideoJoiningTimeMs, + out + ) + + val rendererIndex = out.indexOfFirst { it.javaClass == MediaCodecVideoRenderer::class.java } + if (rendererIndex < 0) return + + out[rendererIndex] = DvSanitizingVideoRenderer( + MediaCodecVideoRenderer.Builder(context) + .setCodecAdapterFactory(codecAdapterFactory) + .setMediaCodecSelector(mediaCodecSelector) + .setAllowedJoiningTimeMs(allowedVideoJoiningTimeMs) + .setEnableDecoderFallback(enableDecoderFallback) + .setEventHandler(eventHandler) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY) + ) + } +} + +@OptIn(UnstableApi::class) +private class DvSanitizingVideoRenderer(builder: MediaCodecVideoRenderer.Builder) : MediaCodecVideoRenderer(builder) { + private var stripHdr10PlusSei = false + private var stripDvRpu = false + + override fun onCodecInitialized( + name: String, + configuration: MediaCodecAdapter.Configuration, + initializedTimestampMs: Long, + initializationDurationMs: Long + ) { + super.onCodecInitialized(name, configuration, initializedTimestampMs, initializationDurationMs) + + val codecs = configuration.format.codecs?.lowercase() ?: "" + val dvHevcFormat = configuration.format.sampleMimeType == MimeTypes.VIDEO_DOLBY_VISION && + (codecs.startsWith("dvhe.") || codecs.startsWith("dvh1.")) + val codecMimeType = configuration.codecInfo.codecMimeType + val newStripHdr10PlusSei = dvHevcFormat && codecMimeType == MimeTypes.VIDEO_DOLBY_VISION + val newStripDvRpu = dvHevcFormat && + codecMimeType == MimeTypes.VIDEO_H265 && + isBlCompatibleDvProfile(codecs) + + if (newStripHdr10PlusSei != stripHdr10PlusSei || newStripDvRpu != stripDvRpu) { + Log.i( + TAG, + "DV bitstream sanitizing: stripHdr10PlusSei=$newStripHdr10PlusSei, " + + "stripDvRpu=$newStripDvRpu (codec=$name, codecs=${configuration.format.codecs})" + ) + } + + stripHdr10PlusSei = newStripHdr10PlusSei + stripDvRpu = newStripDvRpu + } + + override fun onQueueInputBuffer(buffer: DecoderInputBuffer) { + if (stripHdr10PlusSei || stripDvRpu) { + val data = buffer.data + if (data != null && data.hasRemaining() && !buffer.isEncrypted) { + DvBitstreamSanitizer.sanitize(data, stripHdr10PlusSei, stripDvRpu) + } + } + super.onQueueInputBuffer(buffer) + } + + private fun isBlCompatibleDvProfile(codecs: String): Boolean = + codecs.startsWith("dvhe.07") || + codecs.startsWith("dvh1.07") || + codecs.startsWith("dvhe.08") || + codecs.startsWith("dvh1.08") +} 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 44bb2a8d5..b49e0d29f 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 @@ -90,7 +90,7 @@ internal fun ExoPlayer( .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) .build() - val renderersFactory = DefaultRenderersFactory(context) + val renderersFactory = DvSanitizingRenderersFactory(context) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) .setEnableDecoderFallback(true) diff --git a/android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt b/android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt new file mode 100644 index 000000000..f26040da4 --- /dev/null +++ b/android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt @@ -0,0 +1,131 @@ +package nl.jknaapen.fladder.player + +import java.nio.ByteBuffer +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class DvBitstreamSanitizerTest { + @Test + fun stripsHdr10PlusPrefixSeiBetweenVclNals() { + val vcl1 = annexBNal(1, byteArrayOf(0x01, 0x02)) + val vcl2 = annexBNal(1, byteArrayOf(0x03, 0x04)) + val buffer = bufferOf(vcl1, hdr10PlusSei(), vcl2) + val originalLimit = buffer.limit() + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + + assertArrayEquals(concat(vcl1, vcl2), remainingBytes(buffer)) + assertTrue(buffer.limit() < originalLimit) + assertEquals(0, buffer.position()) + } + + @Test + fun stripsSuffixSei() { + val vcl = annexBNal(1, byteArrayOf(0x01)) + val suffixSei = annexBNal(40, hdr10PlusSeiPayload()) + val buffer = bufferOf(vcl, suffixSei) + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + + assertArrayEquals(vcl, remainingBytes(buffer)) + } + + @Test + fun handles3ByteStartCodes() { + val vcl1 = annexBNal(1, byteArrayOf(0x01, 0x02), startCodeLen = 3) + val sei = annexBNal(39, hdr10PlusSeiPayload(), startCodeLen = 3) + val vcl2 = annexBNal(1, byteArrayOf(0x03), startCodeLen = 3) + val buffer = bufferOf(vcl1, sei, vcl2) + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + + assertArrayEquals(concat(vcl1, vcl2), remainingBytes(buffer)) + } + + @Test + fun preservesNonHdr10PlusT35Sei() { + val sei = annexBNal( + 39, + byteArrayOf(0x04, 0x07, 0x00, 0x00, 0x3C, 0x00, 0x01, 0x04, 0x00) + ) + val buffer = bufferOf(annexBNal(1, byteArrayOf(0x01)), sei, annexBNal(1, byteArrayOf(0x02))) + val original = remainingBytes(buffer) + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + + assertArrayEquals(original, remainingBytes(buffer)) + } + + @Test + fun rpuModeStripsRpuAndElButKeepsHdr10PlusSei() { + val vcl = annexBNal(1, byteArrayOf(0x01)) + val rpu = annexBNal(62, byteArrayOf(0x19, 0x08)) + val el = annexBNal(63, byteArrayOf(0x42)) + val sei = hdr10PlusSei() + val buffer = bufferOf(vcl, rpu, sei, el) + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = false, stripDvRpu = true) + + assertArrayEquals(concat(vcl, sei), remainingBytes(buffer)) + } + + @Test + fun respectsPositionAndRestoresIt() { + val prefix = byteArrayOf(0xAA.toByte(), 0xBB.toByte()) + val vcl = annexBNal(1, byteArrayOf(0x01)) + val content = concat(prefix, vcl, hdr10PlusSei()) + val buffer = ByteBuffer.wrap(content.copyOf()) + buffer.position(prefix.size) + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + + assertEquals(prefix.size, buffer.position()) + assertArrayEquals(vcl, remainingBytes(buffer)) + assertEquals(0xAA.toByte(), buffer.get(0)) + assertEquals(0xBB.toByte(), buffer.get(1)) + } + + @Test + fun worksOnDirectBuffers() { + val vcl = annexBNal(1, byteArrayOf(0x01, 0x02)) + val content = concat(vcl, hdr10PlusSei()) + val buffer = ByteBuffer.allocateDirect(content.size) + buffer.put(content) + buffer.flip() + + DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + + assertArrayEquals(vcl, remainingBytes(buffer)) + } + + private fun annexBNal(nalUnitType: Int, payload: ByteArray, startCodeLen: Int = 4): ByteArray { + val startCode = if (startCodeLen == 3) byteArrayOf(0, 0, 1) else byteArrayOf(0, 0, 0, 1) + val header = byteArrayOf(((nalUnitType shl 1) and 0x7E).toByte(), 0x01) + return concat(startCode, header, payload) + } + + private fun hdr10PlusSeiPayload(): ByteArray = + byteArrayOf(0x04, 0x07, 0xB5.toByte(), 0x00, 0x3C, 0x00, 0x01, 0x04, 0x00, 0x80.toByte()) + + private fun hdr10PlusSei(): ByteArray = annexBNal(39, hdr10PlusSeiPayload()) + + private fun concat(vararg parts: ByteArray): ByteArray { + val result = ByteArray(parts.sumOf { it.size }) + var offset = 0 + for (part in parts) { + part.copyInto(result, offset) + offset += part.size + } + return result + } + + private fun bufferOf(vararg parts: ByteArray): ByteBuffer = ByteBuffer.wrap(concat(*parts)) + + private fun remainingBytes(buffer: ByteBuffer): ByteArray { + val copy = ByteArray(buffer.remaining()) + buffer.duplicate().get(copy) + return copy + } +} From 9500dd20872b829a9ce3195092ef97c937c2ebc2 Mon Sep 17 00:00:00 2001 From: Sirulex Date: Sun, 21 Jun 2026 17:13:57 +0200 Subject: [PATCH 2/4] feat: Add GitHub Actions workflow for building Android APKs --- .github/workflows/build-android-apks.yml | 108 +++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 109 insertions(+) create mode 100644 .github/workflows/build-android-apks.yml diff --git a/.github/workflows/build-android-apks.yml b/.github/workflows/build-android-apks.yml new file mode 100644 index 000000000..29540bb58 --- /dev/null +++ b/.github/workflows/build-android-apks.yml @@ -0,0 +1,108 @@ +name: Build Android APKs + +on: + push: + branches: + - develop + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + build-android-apks: + name: Build Android APKs + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.1 + + - name: Fetch Flutter version + run: | + FLUTTER_VERSION=$(jq -r '.flutter' .fvmrc) + echo "FLUTTER_VERSION=${FLUTTER_VERSION}" >> "$GITHUB_ENV" + shell: bash + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: "17" + cache: gradle + check-latest: true + + - name: Prepare release signing + env: + ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} + RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} + RELEASE_KEYSTORE_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} + RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} + run: | + if [[ -z "$ENCODED_STRING" || -z "$RELEASE_KEYSTORE_PASSWORD" || -z "$RELEASE_KEYSTORE_ALIAS" || -z "$RELEASE_KEY_PASSWORD" ]]; then + echo "Missing one or more signing secrets." + echo "Required secrets: KEYSTORE_BASE_64, RELEASE_KEYSTORE_PASSWORD, RELEASE_KEYSTORE_ALIAS, RELEASE_KEY_PASSWORD" + exit 1 + fi + + echo "$ENCODED_STRING" | base64 -d > android/app/keystore.jks + echo "SIGNING_SOURCE=repository signing secrets" >> "$GITHUB_ENV" + + cat > android/app/key.properties < Date: Mon, 22 Jun 2026 08:10:51 +0200 Subject: [PATCH 3/4] implement toggle to change player backend (ignore hdr10plus) --- .../fladder/api/PlayerSettingsHelper.g.kt | 23 +- .../nl/jknaapen/fladder/player/ExoPlayer.kt | 6 +- ...kt => StripHDR10PlusBitstreamSanitizer.kt} | 2 +- ...y.kt => StripHDR10PlusRenderersFactory.kt} | 8 +- ...> StripHDR10PlusBitstreamSanitizerTest.kt} | 16 +- lib/l10n/app_en.arb | 4 +- .../settings/video_player_settings.dart | 80 +++-- .../video_player_settings.freezed.dart | 29 +- .../settings/video_player_settings.g.dart | 3 + .../pigeon_player_settings_provider.dart | 34 ++- .../video_player_settings_provider.dart | 92 ++++-- .../settings/player_settings_page.dart | 277 ++++++++++++------ lib/src/player_settings_helper.g.dart | 86 +++--- pigeons/player_settings_pigeon.dart | 5 +- 14 files changed, 440 insertions(+), 225 deletions(-) rename android/app/src/main/kotlin/nl/jknaapen/fladder/player/{DvBitstreamSanitizer.kt => StripHDR10PlusBitstreamSanitizer.kt} (99%) rename android/app/src/main/kotlin/nl/jknaapen/fladder/player/{DvSanitizingRenderersFactory.kt => StripHDR10PlusRenderersFactory.kt} (91%) rename android/app/src/test/kotlin/nl/jknaapen/fladder/player/{DvBitstreamSanitizerTest.kt => StripHDR10PlusBitstreamSanitizerTest.kt} (83%) 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 e60c2c4a6..fa288cfd2 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 @@ -150,6 +150,7 @@ enum class SegmentSkip(val raw: Int) { /** Generated class from Pigeon that represents data sent in messages. */ data class PlayerSettings ( val enableTunneling: Boolean, + val ignoreHdr10Plus: Boolean, val skipTypes: Map, val themeColor: Long? = null, val skipForward: Long, @@ -164,21 +165,23 @@ data class PlayerSettings ( companion object { fun fromList(pigeonVar_list: List): PlayerSettings { val enableTunneling = pigeonVar_list[0] as Boolean - val skipTypes = pigeonVar_list[1] as Map - val themeColor = pigeonVar_list[2] as Long? - val skipForward = pigeonVar_list[3] as Long - val skipBackward = pigeonVar_list[4] as Long - val autoNextType = pigeonVar_list[5] as AutoNextType - val acceptedOrientations = pigeonVar_list[6] as List - val fillScreen = pigeonVar_list[7] as Boolean - val videoFit = pigeonVar_list[8] as VideoPlayerFit - val screensaver = pigeonVar_list[9] as Screensaver - return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit, screensaver) + val ignoreHdr10Plus = pigeonVar_list[1] as Boolean + val skipTypes = pigeonVar_list[2] as Map + val themeColor = pigeonVar_list[3] as Long? + val skipForward = pigeonVar_list[4] as Long + val skipBackward = pigeonVar_list[5] as Long + val autoNextType = pigeonVar_list[6] as AutoNextType + val acceptedOrientations = pigeonVar_list[7] as List + val fillScreen = pigeonVar_list[8] as Boolean + val videoFit = pigeonVar_list[9] as VideoPlayerFit + val screensaver = pigeonVar_list[10] as Screensaver + return PlayerSettings(enableTunneling, ignoreHdr10Plus, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit, screensaver) } } fun toList(): List { return listOf( enableTunneling, + ignoreHdr10Plus, skipTypes, themeColor, skipForward, 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 b49e0d29f..22f85355a 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 @@ -90,7 +90,11 @@ internal fun ExoPlayer( .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) .build() - val renderersFactory = DvSanitizingRenderersFactory(context) + val renderersFactory = (if (PlayerSettingsObject.settings.value?.ignoreHdr10Plus == true) { + StripHDR10PlusRenderersFactory(context) + } else { + DefaultRenderersFactory(context) + }) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) .setEnableDecoderFallback(true) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusBitstreamSanitizer.kt similarity index 99% rename from android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt rename to android/app/src/main/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusBitstreamSanitizer.kt index 81920b42f..66c650881 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizer.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusBitstreamSanitizer.kt @@ -8,7 +8,7 @@ import java.nio.ByteBuffer * receives in-band HDR10+ SEI, so keep only the dynamic metadata for the selected * decode path. */ -object DvBitstreamSanitizer { +object StripHDR10PlusBitstreamSanitizer { private const val NAL_TYPE_PREFIX_SEI = 39 private const val NAL_TYPE_SUFFIX_SEI = 40 private const val NAL_TYPE_UNSPEC62 = 62 diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusRenderersFactory.kt similarity index 91% rename from android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt rename to android/app/src/main/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusRenderersFactory.kt index b8381c390..4445a124c 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/DvSanitizingRenderersFactory.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusRenderersFactory.kt @@ -17,7 +17,7 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener private const val TAG = "FladderPlayer" @OptIn(UnstableApi::class) -class DvSanitizingRenderersFactory(context: Context) : DefaultRenderersFactory(context) { +class StripHDR10PlusRenderersFactory(context: Context) : DefaultRenderersFactory(context) { override fun buildVideoRenderers( context: Context, extensionRendererMode: Int, @@ -42,7 +42,7 @@ class DvSanitizingRenderersFactory(context: Context) : DefaultRenderersFactory(c val rendererIndex = out.indexOfFirst { it.javaClass == MediaCodecVideoRenderer::class.java } if (rendererIndex < 0) return - out[rendererIndex] = DvSanitizingVideoRenderer( + out[rendererIndex] = StripHDR10PlusVideoRenderer( MediaCodecVideoRenderer.Builder(context) .setCodecAdapterFactory(codecAdapterFactory) .setMediaCodecSelector(mediaCodecSelector) @@ -56,7 +56,7 @@ class DvSanitizingRenderersFactory(context: Context) : DefaultRenderersFactory(c } @OptIn(UnstableApi::class) -private class DvSanitizingVideoRenderer(builder: MediaCodecVideoRenderer.Builder) : MediaCodecVideoRenderer(builder) { +private class StripHDR10PlusVideoRenderer(builder: MediaCodecVideoRenderer.Builder) : MediaCodecVideoRenderer(builder) { private var stripHdr10PlusSei = false private var stripDvRpu = false @@ -93,7 +93,7 @@ private class DvSanitizingVideoRenderer(builder: MediaCodecVideoRenderer.Builder if (stripHdr10PlusSei || stripDvRpu) { val data = buffer.data if (data != null && data.hasRemaining() && !buffer.isEncrypted) { - DvBitstreamSanitizer.sanitize(data, stripHdr10PlusSei, stripDvRpu) + StripHDR10PlusBitstreamSanitizer.sanitize(data, stripHdr10PlusSei, stripDvRpu) } } super.onQueueInputBuffer(buffer) diff --git a/android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt b/android/app/src/test/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusBitstreamSanitizerTest.kt similarity index 83% rename from android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt rename to android/app/src/test/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusBitstreamSanitizerTest.kt index f26040da4..363472727 100644 --- a/android/app/src/test/kotlin/nl/jknaapen/fladder/player/DvBitstreamSanitizerTest.kt +++ b/android/app/src/test/kotlin/nl/jknaapen/fladder/player/StripHDR10PlusBitstreamSanitizerTest.kt @@ -6,7 +6,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -class DvBitstreamSanitizerTest { +class StripHDR10PlusBitstreamSanitizerTest { @Test fun stripsHdr10PlusPrefixSeiBetweenVclNals() { val vcl1 = annexBNal(1, byteArrayOf(0x01, 0x02)) @@ -14,7 +14,7 @@ class DvBitstreamSanitizerTest { val buffer = bufferOf(vcl1, hdr10PlusSei(), vcl2) val originalLimit = buffer.limit() - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) assertArrayEquals(concat(vcl1, vcl2), remainingBytes(buffer)) assertTrue(buffer.limit() < originalLimit) @@ -27,7 +27,7 @@ class DvBitstreamSanitizerTest { val suffixSei = annexBNal(40, hdr10PlusSeiPayload()) val buffer = bufferOf(vcl, suffixSei) - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) assertArrayEquals(vcl, remainingBytes(buffer)) } @@ -39,7 +39,7 @@ class DvBitstreamSanitizerTest { val vcl2 = annexBNal(1, byteArrayOf(0x03), startCodeLen = 3) val buffer = bufferOf(vcl1, sei, vcl2) - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) assertArrayEquals(concat(vcl1, vcl2), remainingBytes(buffer)) } @@ -53,7 +53,7 @@ class DvBitstreamSanitizerTest { val buffer = bufferOf(annexBNal(1, byteArrayOf(0x01)), sei, annexBNal(1, byteArrayOf(0x02))) val original = remainingBytes(buffer) - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) assertArrayEquals(original, remainingBytes(buffer)) } @@ -66,7 +66,7 @@ class DvBitstreamSanitizerTest { val sei = hdr10PlusSei() val buffer = bufferOf(vcl, rpu, sei, el) - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = false, stripDvRpu = true) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = false, stripDvRpu = true) assertArrayEquals(concat(vcl, sei), remainingBytes(buffer)) } @@ -79,7 +79,7 @@ class DvBitstreamSanitizerTest { val buffer = ByteBuffer.wrap(content.copyOf()) buffer.position(prefix.size) - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) assertEquals(prefix.size, buffer.position()) assertArrayEquals(vcl, remainingBytes(buffer)) @@ -95,7 +95,7 @@ class DvBitstreamSanitizerTest { buffer.put(content) buffer.flip() - DvBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) + StripHDR10PlusBitstreamSanitizer.sanitize(buffer, stripHdr10PlusSei = true, stripDvRpu = false) assertArrayEquals(vcl, remainingBytes(buffer)) } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 65fc489e4..84acaa717 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1487,6 +1487,8 @@ "qualityOptionsAuto": "Auto", "advancedVideoOptionsTitle": "Advanced video options", "advancedVideoOptionsDesc": "Enable HDR, tone mapping, and advanced color handling (unstable for some devices)", + "ignoreHdr10PlusTitle": "Ignore HDR10+", + "ignoreHdr10PlusDesc": "Strips HDR10+ metadata from Dolby Vision streams to fix broken playback on some devices (Fire TVs)", "version": "Version", "mediaSegmentActions": "Media segment actions", "segmentActionNone": "None", @@ -2768,4 +2770,4 @@ "loud": "Loud", "share": "Share", "setAs": "Set as" -} \ No newline at end of file +} diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index 4afc33f09..b79ff816c 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -95,18 +95,21 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(Bitrate.original) Bitrate maxHomeBitrate, @Default(Bitrate.original) Bitrate maxInternetBitrate, String? audioDevice, - @Default(defaultSegmentSkipValues) Map segmentSkipSettings, + @Default(defaultSegmentSkipValues) + Map segmentSkipSettings, @Default({}) Map hotKeys, @Default(Screensaver.logo) Screensaver screensaver, @Default(false) bool enableSpeedBoost, @Default(2.0) double speedBoostRate, @Default(true) bool enableDoubleTapSeek, @Default(false) bool enableAdvancedVideoOptions, + @Default(false) bool ignoreHdr10Plus, @Default(true) bool enableEdgeGestures, @Default(false) bool reverseEdgeGestures, @Default(true) bool enablePictureInPicture, @Default(true) bool enableReplayGain, - @Default(ReplayGainVolumeLevel.quiet) ReplayGainVolumeLevel replayGainVolumeLevel, + @Default(ReplayGainVolumeLevel.quiet) + ReplayGainVolumeLevel replayGainVolumeLevel, @Default(true) bool enablePlayPauseFade, @Default(true) bool enableCrossfade, @Default(400) int crossfadeDurationMs, @@ -114,21 +117,25 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { double get volume => internalVolume; - factory VideoPlayerSettingsModel.fromJson(Map json) => _$VideoPlayerSettingsModelFromJson(json); + factory VideoPlayerSettingsModel.fromJson(Map json) => + _$VideoPlayerSettingsModelFromJson(json); - PlayerOptions get wantedPlayer => - leanBackMode ? PlayerOptions.nativePlayer : playerOptions ?? PlayerOptions.platformDefaults; + PlayerOptions get wantedPlayer => leanBackMode + ? PlayerOptions.nativePlayer + : playerOptions ?? PlayerOptions.platformDefaults; - Map get currentShortcuts => - _defaultVideoHotKeys.map((key, value) => MapEntry(key, hotKeys[key] ?? value)); + Map get currentShortcuts => _defaultVideoHotKeys + .map((key, value) => MapEntry(key, hotKeys[key] ?? value)); - Map get defaultShortCuts => _defaultVideoHotKeys; + Map get defaultShortCuts => + _defaultVideoHotKeys; bool get canUseCrossfade => crossfadeSupportedOnCurrentPlatform; bool playerSame(VideoPlayerSettingsModel other) { return other.hardwareAccel == hardwareAccel && other.enableTunneling == enableTunneling && + other.ignoreHdr10Plus == ignoreHdr10Plus && other.useLibass == useLibass && other.bufferSize == bufferSize && other.wantedPlayer == wantedPlayer; @@ -145,6 +152,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { other.hardwareAccel == hardwareAccel && other.useLibass == useLibass && other.enableTunneling == enableTunneling && + other.ignoreHdr10Plus == ignoreHdr10Plus && other.bufferSize == bufferSize && other.internalVolume == internalVolume && other.playerOptions == playerOptions && @@ -159,6 +167,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { hardwareAccel.hashCode ^ useLibass.hashCode ^ enableTunneling.hashCode ^ + ignoreHdr10Plus.hashCode ^ bufferSize.hashCode ^ internalVolume.hashCode ^ audioDevice.hashCode; @@ -290,27 +299,42 @@ Map get _defaultVideoHotKeys => { altKey: LogicalKeyboardKey.keyJ, altModifier: LogicalKeyboardKey.shiftLeft, ), - VideoHotKeys.stepForward => KeyCombination(key: LogicalKeyboardKey.period), - VideoHotKeys.stepBack => KeyCombination(key: LogicalKeyboardKey.comma), + VideoHotKeys.stepForward => + KeyCombination(key: LogicalKeyboardKey.period), + VideoHotKeys.stepBack => + KeyCombination(key: LogicalKeyboardKey.comma), VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM), - VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp), - VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown), - VideoHotKeys.speedUp => - KeyCombination(key: LogicalKeyboardKey.arrowUp, modifier: LogicalKeyboardKey.controlLeft), - VideoHotKeys.speedDown => - KeyCombination(key: LogicalKeyboardKey.arrowDown, modifier: LogicalKeyboardKey.controlLeft), - VideoHotKeys.prevVideo => - KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft), - VideoHotKeys.nextVideo => - KeyCombination(key: LogicalKeyboardKey.keyN, modifier: LogicalKeyboardKey.shiftLeft), - VideoHotKeys.nextChapter => KeyCombination(key: LogicalKeyboardKey.pageUp), - VideoHotKeys.prevChapter => KeyCombination(key: LogicalKeyboardKey.pageDown), - VideoHotKeys.fullScreen => KeyCombination(key: LogicalKeyboardKey.keyF), - VideoHotKeys.skipMediaSegment => KeyCombination(key: LogicalKeyboardKey.keyS), - VideoHotKeys.takeScreenshot => KeyCombination(key: LogicalKeyboardKey.keyG), - VideoHotKeys.takeScreenshotClean => - KeyCombination(key: LogicalKeyboardKey.keyG, modifier: LogicalKeyboardKey.controlLeft), - VideoHotKeys.toggleSubtitles => KeyCombination(key: LogicalKeyboardKey.keyT), + VideoHotKeys.volumeUp => + KeyCombination(key: LogicalKeyboardKey.arrowUp), + VideoHotKeys.volumeDown => + KeyCombination(key: LogicalKeyboardKey.arrowDown), + VideoHotKeys.speedUp => KeyCombination( + key: LogicalKeyboardKey.arrowUp, + modifier: LogicalKeyboardKey.controlLeft), + VideoHotKeys.speedDown => KeyCombination( + key: LogicalKeyboardKey.arrowDown, + modifier: LogicalKeyboardKey.controlLeft), + VideoHotKeys.prevVideo => KeyCombination( + key: LogicalKeyboardKey.keyP, + modifier: LogicalKeyboardKey.shiftLeft), + VideoHotKeys.nextVideo => KeyCombination( + key: LogicalKeyboardKey.keyN, + modifier: LogicalKeyboardKey.shiftLeft), + VideoHotKeys.nextChapter => + KeyCombination(key: LogicalKeyboardKey.pageUp), + VideoHotKeys.prevChapter => + KeyCombination(key: LogicalKeyboardKey.pageDown), + VideoHotKeys.fullScreen => + KeyCombination(key: LogicalKeyboardKey.keyF), + VideoHotKeys.skipMediaSegment => + KeyCombination(key: LogicalKeyboardKey.keyS), + VideoHotKeys.takeScreenshot => + KeyCombination(key: LogicalKeyboardKey.keyG), + VideoHotKeys.takeScreenshotClean => KeyCombination( + key: LogicalKeyboardKey.keyG, + modifier: LogicalKeyboardKey.controlLeft), + VideoHotKeys.toggleSubtitles => + KeyCombination(key: LogicalKeyboardKey.keyT), VideoHotKeys.exit => KeyCombination(key: LogicalKeyboardKey.escape), }, }; diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index 0046a082f..972ce3536 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -35,6 +35,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { double get speedBoostRate; bool get enableDoubleTapSeek; bool get enableAdvancedVideoOptions; + bool get ignoreHdr10Plus; bool get enableEdgeGestures; bool get reverseEdgeGestures; bool get enablePictureInPicture; @@ -81,6 +82,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty( 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)) + ..add(DiagnosticsProperty('ignoreHdr10Plus', ignoreHdr10Plus)) ..add(DiagnosticsProperty('enableEdgeGestures', enableEdgeGestures)) ..add(DiagnosticsProperty('reverseEdgeGestures', reverseEdgeGestures)) ..add( @@ -94,7 +96,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, ignoreHdr10Plus: $ignoreHdr10Plus, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; } } @@ -126,6 +128,7 @@ abstract mixin class $VideoPlayerSettingsModelCopyWith<$Res> { double speedBoostRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, + bool ignoreHdr10Plus, bool enableEdgeGestures, bool reverseEdgeGestures, bool enablePictureInPicture, @@ -170,6 +173,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? speedBoostRate = null, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, + Object? ignoreHdr10Plus = null, Object? enableEdgeGestures = null, Object? reverseEdgeGestures = null, Object? enablePictureInPicture = null, @@ -264,6 +268,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enableAdvancedVideoOptions : enableAdvancedVideoOptions // ignore: cast_nullable_to_non_nullable as bool, + ignoreHdr10Plus: null == ignoreHdr10Plus + ? _self.ignoreHdr10Plus + : ignoreHdr10Plus // ignore: cast_nullable_to_non_nullable + as bool, enableEdgeGestures: null == enableEdgeGestures ? _self.enableEdgeGestures : enableEdgeGestures // ignore: cast_nullable_to_non_nullable @@ -415,6 +423,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { double speedBoostRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, + bool ignoreHdr10Plus, bool enableEdgeGestures, bool reverseEdgeGestures, bool enablePictureInPicture, @@ -451,6 +460,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.speedBoostRate, _that.enableDoubleTapSeek, _that.enableAdvancedVideoOptions, + _that.ignoreHdr10Plus, _that.enableEdgeGestures, _that.reverseEdgeGestures, _that.enablePictureInPicture, @@ -501,6 +511,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { double speedBoostRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, + bool ignoreHdr10Plus, bool enableEdgeGestures, bool reverseEdgeGestures, bool enablePictureInPicture, @@ -536,6 +547,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.speedBoostRate, _that.enableDoubleTapSeek, _that.enableAdvancedVideoOptions, + _that.ignoreHdr10Plus, _that.enableEdgeGestures, _that.reverseEdgeGestures, _that.enablePictureInPicture, @@ -585,6 +597,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { double speedBoostRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, + bool ignoreHdr10Plus, bool enableEdgeGestures, bool reverseEdgeGestures, bool enablePictureInPicture, @@ -620,6 +633,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.speedBoostRate, _that.enableDoubleTapSeek, _that.enableAdvancedVideoOptions, + _that.ignoreHdr10Plus, _that.enableEdgeGestures, _that.reverseEdgeGestures, _that.enablePictureInPicture, @@ -661,6 +675,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel this.speedBoostRate = 2.0, this.enableDoubleTapSeek = true, this.enableAdvancedVideoOptions = false, + this.ignoreHdr10Plus = false, this.enableEdgeGestures = true, this.reverseEdgeGestures = false, this.enablePictureInPicture = true, @@ -759,6 +774,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel final bool enableAdvancedVideoOptions; @override @JsonKey() + final bool ignoreHdr10Plus; + @override + @JsonKey() final bool enableEdgeGestures; @override @JsonKey() @@ -824,6 +842,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty( 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)) + ..add(DiagnosticsProperty('ignoreHdr10Plus', ignoreHdr10Plus)) ..add(DiagnosticsProperty('enableEdgeGestures', enableEdgeGestures)) ..add(DiagnosticsProperty('reverseEdgeGestures', reverseEdgeGestures)) ..add( @@ -837,7 +856,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, ignoreHdr10Plus: $ignoreHdr10Plus, enableEdgeGestures: $enableEdgeGestures, reverseEdgeGestures: $reverseEdgeGestures, enablePictureInPicture: $enablePictureInPicture, enableReplayGain: $enableReplayGain, replayGainVolumeLevel: $replayGainVolumeLevel, enablePlayPauseFade: $enablePlayPauseFade, enableCrossfade: $enableCrossfade, crossfadeDurationMs: $crossfadeDurationMs)'; } } @@ -871,6 +890,7 @@ abstract mixin class _$VideoPlayerSettingsModelCopyWith<$Res> double speedBoostRate, bool enableDoubleTapSeek, bool enableAdvancedVideoOptions, + bool ignoreHdr10Plus, bool enableEdgeGestures, bool reverseEdgeGestures, bool enablePictureInPicture, @@ -915,6 +935,7 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? speedBoostRate = null, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, + Object? ignoreHdr10Plus = null, Object? enableEdgeGestures = null, Object? reverseEdgeGestures = null, Object? enablePictureInPicture = null, @@ -1009,6 +1030,10 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enableAdvancedVideoOptions : enableAdvancedVideoOptions // ignore: cast_nullable_to_non_nullable as bool, + ignoreHdr10Plus: null == ignoreHdr10Plus + ? _self.ignoreHdr10Plus + : ignoreHdr10Plus // ignore: cast_nullable_to_non_nullable + as bool, enableEdgeGestures: null == enableEdgeGestures ? _self.enableEdgeGestures : enableEdgeGestures // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index c6e599658..73276f4c3 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -52,6 +52,7 @@ _VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson( enableDoubleTapSeek: json['enableDoubleTapSeek'] as bool? ?? true, enableAdvancedVideoOptions: json['enableAdvancedVideoOptions'] as bool? ?? false, + ignoreHdr10Plus: json['ignoreHdr10Plus'] as bool? ?? false, enableEdgeGestures: json['enableEdgeGestures'] as bool? ?? true, reverseEdgeGestures: json['reverseEdgeGestures'] as bool? ?? false, enablePictureInPicture: json['enablePictureInPicture'] as bool? ?? true, @@ -93,6 +94,7 @@ Map _$VideoPlayerSettingsModelToJson( 'speedBoostRate': instance.speedBoostRate, 'enableDoubleTapSeek': instance.enableDoubleTapSeek, 'enableAdvancedVideoOptions': instance.enableAdvancedVideoOptions, + 'ignoreHdr10Plus': instance.ignoreHdr10Plus, 'enableEdgeGestures': instance.enableEdgeGestures, 'reverseEdgeGestures': instance.reverseEdgeGestures, 'enablePictureInPicture': instance.enablePictureInPicture, @@ -189,6 +191,7 @@ const _$VideoHotKeysEnumMap = { VideoHotKeys.skipMediaSegment: 'skipMediaSegment', VideoHotKeys.takeScreenshot: 'takeScreenshot', VideoHotKeys.takeScreenshotClean: 'takeScreenshotClean', + VideoHotKeys.toggleSubtitles: 'toggleSubtitles', VideoHotKeys.exit: 'exit', }; diff --git a/lib/providers/settings/pigeon_player_settings_provider.dart b/lib/providers/settings/pigeon_player_settings_provider.dart index 0a896acd0..6632b50c4 100644 --- a/lib/providers/settings/pigeon_player_settings_provider.dart +++ b/lib/providers/settings/pigeon_player_settings_provider.dart @@ -28,6 +28,7 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { pigeon.PlayerSettingsPigeon().sendPlayerSettings( pigeon.PlayerSettings( enableTunneling: value.enableTunneling, + ignoreHdr10Plus: value.ignoreHdr10Plus, screensaver: switch (value.screensaver) { Screensaver.disabled => pigeon.Screensaver.disabled, Screensaver.dvd => pigeon.Screensaver.dvd, @@ -59,8 +60,12 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { AutoNextType.static => pigeon.AutoNextType.static, AutoNextType.smart => pigeon.AutoNextType.smart, }, - skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds, - skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds, + 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, @@ -71,16 +76,21 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { BoxFit.none => pigeon.VideoPlayerFit.none, BoxFit.scaleDown => pigeon.VideoPlayerFit.scaleDown, }, - acceptedOrientations: (value.allowedOrientations?.toList() ?? DeviceOrientation.values) - .map( - (e) => switch (e) { - DeviceOrientation.portraitUp => pigeon.PlayerOrientations.portraitUp, - DeviceOrientation.portraitDown => pigeon.PlayerOrientations.portraitDown, - DeviceOrientation.landscapeLeft => pigeon.PlayerOrientations.landScapeLeft, - DeviceOrientation.landscapeRight => pigeon.PlayerOrientations.landScapeRight, - }, - ) - .toList(), + acceptedOrientations: + (value.allowedOrientations?.toList() ?? DeviceOrientation.values) + .map( + (e) => switch (e) { + DeviceOrientation.portraitUp => + pigeon.PlayerOrientations.portraitUp, + DeviceOrientation.portraitDown => + pigeon.PlayerOrientations.portraitDown, + DeviceOrientation.landscapeLeft => + pigeon.PlayerOrientations.landScapeLeft, + DeviceOrientation.landscapeRight => + pigeon.PlayerOrientations.landScapeRight, + }, + ) + .toList(), ), ); } diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index ce883c021..ad7bd26db 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -14,15 +14,17 @@ import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; -final videoPlayerSettingsProvider = - StateNotifierProvider((ref) { +final videoPlayerSettingsProvider = StateNotifierProvider< + VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) { return VideoPlayerSettingsProviderNotifier(ref); }); final playbackRateProvider = StateProvider((ref) => 1.0); -class VideoPlayerSettingsProviderNotifier extends StateNotifier { - VideoPlayerSettingsProviderNotifier(this.ref) : super(_sanitizeCrossfade(VideoPlayerSettingsModel())) { +class VideoPlayerSettingsProviderNotifier + extends StateNotifier { + VideoPlayerSettingsProviderNotifier(this.ref) + : super(_sanitizeCrossfade(VideoPlayerSettingsModel())) { _initVolumeSync(); } @@ -70,7 +72,8 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier state = state.copyWith(hardwareAccel: value ?? true); - void setUseLibass(bool? value) => state = state.copyWith(useLibass: value ?? false); - void setMediaTunneling(bool? value) => state = state.copyWith(enableTunneling: value ?? false); - void setBufferSize(int? value) => state = state.copyWith(bufferSize: value ?? 32); - void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value ?? BoxFit.contain); - void setScreensaver(Screensaver? value) => state = state.copyWith(screensaver: value ?? Screensaver.black); + void setHardwareAccel(bool? value) => + state = state.copyWith(hardwareAccel: value ?? true); + void setUseLibass(bool? value) => + state = state.copyWith(useLibass: value ?? false); + void setMediaTunneling(bool? value) => + state = state.copyWith(enableTunneling: value ?? false); + void setIgnoreHdr10Plus(bool value) => + state = state.copyWith(ignoreHdr10Plus: value); + void setBufferSize(int? value) => + state = state.copyWith(bufferSize: value ?? 32); + void setFitType(BoxFit? value) => + state = state.copyWith(videoFit: value ?? BoxFit.contain); + void setScreensaver(Screensaver? value) => + state = state.copyWith(screensaver: value ?? Screensaver.black); void setVolume(double value) { state = state.copyWith(internalVolume: value); @@ -127,20 +139,25 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier newEntry) { - state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); + state = state.copyWith( + hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); } void nextChapter() { final chapters = ref.read(playBackModel)?.chapters ?? []; - final currentPosition = ref.read(videoPlayerProvider.select((value) => value.lastState?.position)); + final currentPosition = ref + .read(videoPlayerProvider.select((value) => value.lastState?.position)); if (chapters.isNotEmpty && currentPosition != null) { - final currentChapter = chapters.lastWhereOrNull((element) => element.startPosition <= currentPosition); + final currentChapter = chapters.lastWhereOrNull( + (element) => element.startPosition <= currentPosition); if (currentChapter != null) { final nextChapterIndex = chapters.indexOf(currentChapter) + 1; if (nextChapterIndex < chapters.length) { - ref.read(videoPlayerProvider).seek(chapters[nextChapterIndex].startPosition); + ref + .read(videoPlayerProvider) + .seek(chapters[nextChapterIndex].startPosition); } else { ref.read(videoPlayerProvider).seek(currentChapter.startPosition); } @@ -150,15 +167,19 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier value.lastState?.position)); + final currentPosition = ref + .read(videoPlayerProvider.select((value) => value.lastState?.position)); if (chapters.isNotEmpty && currentPosition != null) { - final currentChapter = chapters.lastWhereOrNull((element) => element.startPosition <= currentPosition); + final currentChapter = chapters.lastWhereOrNull( + (element) => element.startPosition <= currentPosition); if (currentChapter != null) { final prevChapterIndex = chapters.indexOf(currentChapter) - 1; if (prevChapterIndex >= 0) { - ref.read(videoPlayerProvider).seek(chapters[prevChapterIndex].startPosition); + ref + .read(videoPlayerProvider) + .seek(chapters[prevChapterIndex].startPosition); } else { ref.read(videoPlayerProvider).seek(currentChapter.startPosition); } @@ -166,36 +187,47 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier state = state.copyWith(enableSpeedBoost: value); + void setEnableSpeedBoost(bool value) => + state = state.copyWith(enableSpeedBoost: value); void setSpeedBoostRate(double value) { final clampedValue = value.clamp(0.25, 3.0); state = state.copyWith(speedBoostRate: clampedValue); } - void setEnableDoubleTapSeek(bool value) => state = state.copyWith(enableDoubleTapSeek: value); + void setEnableDoubleTapSeek(bool value) => + state = state.copyWith(enableDoubleTapSeek: value); - void setEnableAdvancedVideoOptions(bool value) => state = state.copyWith(enableAdvancedVideoOptions: value); + void setEnableAdvancedVideoOptions(bool value) => + state = state.copyWith(enableAdvancedVideoOptions: value); - void setEnableEdgeGestures(bool value) => state = state.copyWith(enableEdgeGestures: value); + void setEnableEdgeGestures(bool value) => + state = state.copyWith(enableEdgeGestures: value); - void setReverseEdgeGestures(bool value) => state = state.copyWith(reverseEdgeGestures: value); + void setReverseEdgeGestures(bool value) => + state = state.copyWith(reverseEdgeGestures: value); - void setEnablePictureInPicture(bool value) => state = state.copyWith(enablePictureInPicture: value); + void setEnablePictureInPicture(bool value) => + state = state.copyWith(enablePictureInPicture: value); - void setEnableReplayGain(bool value) => state = state.copyWith(enableReplayGain: value); + void setEnableReplayGain(bool value) => + state = state.copyWith(enableReplayGain: value); - void setEnablePlayPauseFade(bool value) => state = state.copyWith(enablePlayPauseFade: value); + void setEnablePlayPauseFade(bool value) => + state = state.copyWith(enablePlayPauseFade: value); - void setReplayGainVolumeLevel(ReplayGainVolumeLevel value) => state = state.copyWith(replayGainVolumeLevel: value); + void setReplayGainVolumeLevel(ReplayGainVolumeLevel value) => + state = state.copyWith(replayGainVolumeLevel: value); void setEnableCrossfade(bool value) { state = state.copyWith(enableCrossfade: value && state.canUseCrossfade); } - void setCrossfadeDurationMs(int value) => state = state.copyWith(crossfadeDurationMs: value); + void setCrossfadeDurationMs(int value) => + state = state.copyWith(crossfadeDurationMs: value); - static VideoPlayerSettingsModel _sanitizeCrossfade(VideoPlayerSettingsModel value) { + static VideoPlayerSettingsModel _sanitizeCrossfade( + VideoPlayerSettingsModel value) { if (!value.canUseCrossfade && value.enableCrossfade) { return value.copyWith(enableCrossfade: false); } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index a710a9fbb..5995329b5 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -35,7 +35,8 @@ class PlayerSettingsPage extends ConsumerStatefulWidget { const PlayerSettingsPage({super.key}); @override - ConsumerState createState() => _PlayerSettingsPageState(); + ConsumerState createState() => + _PlayerSettingsPageState(); } class _PlayerSettingsPageState extends ConsumerState { @@ -46,7 +47,8 @@ class _PlayerSettingsPageState extends ConsumerState { final connectionState = ref.watch(connectivityStatusProvider); - final userSettings = ref.watch(userProvider.select((value) => value?.userSettings)); + final userSettings = + ref.watch(userProvider.select((value) => value?.userSettings)); final currentPlayer = videoSettings.wantedPlayer; final crossfadeSupported = videoSettings.canUseCrossfade; @@ -63,8 +65,10 @@ class _PlayerSettingsPageState extends ConsumerState { children: [ SettingsListTile( label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: Text(context.localized.videoScalingFillScreenDesc), - onTap: () => provider.setFillScreen(!videoSettings.fillScreen), + subLabel: + Text(context.localized.videoScalingFillScreenDesc), + onTap: () => + provider.setFillScreen(!videoSettings.fillScreen), trailing: Switch( value: videoSettings.fillScreen, onChanged: (value) => provider.setFillScreen(value), @@ -84,10 +88,12 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.pictureInPictureAutoTitle), subLabel: Text(context.localized.pictureInPictureSubtitle), - onTap: () => provider.setEnablePictureInPicture(!videoSettings.enablePictureInPicture), + onTap: () => provider.setEnablePictureInPicture( + !videoSettings.enablePictureInPicture), trailing: Switch( value: videoSettings.enablePictureInPicture, - onChanged: (value) => provider.setEnablePictureInPicture(value), + onChanged: (value) => + provider.setEnablePictureInPicture(value), ), ), SettingsListTileEnum( @@ -97,7 +103,9 @@ class _PlayerSettingsPageState extends ConsumerState { .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry), + action: () => ref + .read(videoPlayerSettingsProvider.notifier) + .setFitType(entry), ), ) .toList(), @@ -109,14 +117,16 @@ class _PlayerSettingsPageState extends ConsumerState { ), subLabel: Text(context.localized.homeStreamingQualityDesc), current: ref.watch( - videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), + videoPlayerSettingsProvider + .select((value) => value.maxHomeBitrate.label(context)), ), itemBuilder: (context) => Bitrate.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(maxHomeBitrate: entry), + action: () => + ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(maxHomeBitrate: entry), ), ) .toList(), @@ -128,14 +138,16 @@ class _PlayerSettingsPageState extends ConsumerState { ), subLabel: Text(context.localized.internetStreamingQualityDesc), current: ref.watch( - videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), + videoPlayerSettingsProvider + .select((value) => value.maxInternetBitrate.label(context)), ), itemBuilder: (context) => Bitrate.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(maxInternetBitrate: entry), + action: () => + ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(maxInternetBitrate: entry), ), ) .toList(), @@ -143,27 +155,40 @@ class _PlayerSettingsPageState extends ConsumerState { ], ), const SizedBox(height: 12), - ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ - ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( - (entry) => SettingsListTileEnum( - label: Text(entry.key.label(context)), - current: entry.value.label(context), - itemBuilder: (context) => SegmentSkip.values - .map( - (value) => ItemActionButton( - label: Text(value.label(context)), - action: () { - final newEntries = videoSettings.segmentSkipSettings - .map((key, currentValue) => MapEntry(key, key == entry.key ? value : currentValue)); - ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(segmentSkipSettings: newEntries); - }, - ), - ) - .toList(), - ), - ), - ]), + ...settingsListGroup( + context, + SettingsLabelDivider(label: context.localized.mediaSegmentActions), + [ + ...videoSettings.segmentSkipSettings.entries + .sorted((a, b) => b.key.index.compareTo(a.key.index)) + .map( + (entry) => SettingsListTileEnum( + label: Text(entry.key.label(context)), + current: entry.value.label(context), + itemBuilder: (context) => SegmentSkip.values + .map( + (value) => ItemActionButton( + label: Text(value.label(context)), + action: () { + final newEntries = videoSettings + .segmentSkipSettings + .map((key, currentValue) => MapEntry( + key, + key == entry.key + ? value + : currentValue)); + ref + .read(videoPlayerSettingsProvider.notifier) + .state = + videoSettings.copyWith( + segmentSkipSettings: newEntries); + }, + ), + ) + .toList(), + ), + ), + ]), const SizedBox(height: 12), ...settingsListGroup( context, @@ -174,7 +199,8 @@ class _PlayerSettingsPageState extends ConsumerState { label: Text(context.localized.skipBackLength), trailing: IntInputField( suffix: context.localized.seconds(10), - controller: TextEditingController(text: userSettings.skipBackDuration.inSeconds.toString()), + controller: TextEditingController( + text: userSettings.skipBackDuration.inSeconds.toString()), onSubmitted: (value) { if (value != null) { ref.read(userProvider.notifier).setBackwardSpeed(value); @@ -186,7 +212,9 @@ class _PlayerSettingsPageState extends ConsumerState { label: Text(context.localized.skipForwardLength), trailing: IntInputField( suffix: context.localized.seconds(10), - controller: TextEditingController(text: userSettings!.skipForwardDuration.inSeconds.toString()), + controller: TextEditingController( + text: + userSettings!.skipForwardDuration.inSeconds.toString()), onSubmitted: (value) { if (value != null) { ref.read(userProvider.notifier).setForwardSpeed(value); @@ -203,7 +231,8 @@ class _PlayerSettingsPageState extends ConsumerState { children: VideoHotKeys.values .map( (entry) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), child: Row( children: [ Expanded( @@ -215,9 +244,11 @@ class _PlayerSettingsPageState extends ConsumerState { Flexible( child: KeyCombinationWidget( currentKey: videoSettings.hotKeys[entry], - defaultKey: videoSettings.defaultShortCuts[entry]!, - onChanged: (value) => - ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), + defaultKey: + videoSettings.defaultShortCuts[entry]!, + onChanged: (value) => ref + .read(videoPlayerSettingsProvider.notifier) + .setShortcuts(MapEntry(entry, value)), ), ), ], @@ -237,7 +268,8 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.enableDoubleTapSeekTitle), subLabel: Text(context.localized.enableDoubleTapSeekDesc), - onTap: () => provider.setEnableDoubleTapSeek(!videoSettings.enableDoubleTapSeek), + onTap: () => provider + .setEnableDoubleTapSeek(!videoSettings.enableDoubleTapSeek), trailing: Switch( value: videoSettings.enableDoubleTapSeek, onChanged: (value) => provider.setEnableDoubleTapSeek(value), @@ -246,7 +278,8 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.enableEdgeGesturesTitle), subLabel: Text(context.localized.enableEdgeGesturesDesc), - onTap: () => provider.setEnableEdgeGestures(!videoSettings.enableEdgeGestures), + onTap: () => provider + .setEnableEdgeGestures(!videoSettings.enableEdgeGestures), trailing: Switch( value: videoSettings.enableEdgeGestures, onChanged: (value) => provider.setEnableEdgeGestures(value), @@ -255,7 +288,8 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.reverseEdgeGesturesTitle), subLabel: Text(context.localized.reverseEdgeGesturesDesc), - onTap: () => provider.setReverseEdgeGestures(!videoSettings.reverseEdgeGestures), + onTap: () => provider + .setReverseEdgeGestures(!videoSettings.reverseEdgeGestures), trailing: Switch( value: videoSettings.reverseEdgeGestures, onChanged: (value) => provider.setReverseEdgeGestures(value), @@ -265,7 +299,8 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.enableSpeedBoostTitle), subLabel: Text(context.localized.enableSpeedBoostDesc), - onTap: () => provider.setEnableSpeedBoost(!videoSettings.enableSpeedBoost), + onTap: () => + provider.setEnableSpeedBoost(!videoSettings.enableSpeedBoost), trailing: Switch( value: videoSettings.enableSpeedBoost, onChanged: (value) => provider.setEnableSpeedBoost(value), @@ -273,7 +308,8 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (videoSettings.enableSpeedBoost) Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -297,7 +333,8 @@ class _PlayerSettingsPageState extends ConsumerState { max: 3.0, value: videoSettings.speedBoostRate, divisions: 55, - onChanged: (value) => provider.setSpeedBoostRate(value), + onChanged: (value) => + provider.setSpeedBoostRate(value), ), ), const SizedBox(width: 12), @@ -320,23 +357,33 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.rememberAudioSelections), subLabel: Text(context.localized.rememberAudioSelectionsDesc), - onTap: () => ref.read(userProvider.notifier).setRememberAudioSelections(), + onTap: () => + ref.read(userProvider.notifier).setRememberAudioSelections(), trailing: Switch( value: ref.watch(userProvider.select( - (value) => value?.userConfiguration?.rememberAudioSelections ?? true, + (value) => + value?.userConfiguration?.rememberAudioSelections ?? true, )), - onChanged: (_) => ref.read(userProvider.notifier).setRememberAudioSelections(), + onChanged: (_) => ref + .read(userProvider.notifier) + .setRememberAudioSelections(), ), ), SettingsListTile( label: Text(context.localized.rememberSubtitleSelections), subLabel: Text(context.localized.rememberSubtitleSelectionsDesc), - onTap: () => ref.read(userProvider.notifier).setRememberSubtitleSelections(), + onTap: () => ref + .read(userProvider.notifier) + .setRememberSubtitleSelections(), trailing: Switch( value: ref.watch(userProvider.select( - (value) => value?.userConfiguration?.rememberSubtitleSelections ?? true, + (value) => + value?.userConfiguration?.rememberSubtitleSelections ?? + true, )), - onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(), + onChanged: (_) => ref + .read(userProvider.notifier) + .setRememberSubtitleSelections(), ), ), ], @@ -355,26 +402,33 @@ class _PlayerSettingsPageState extends ConsumerState { : videoSettings.wantedPlayer.label(context), itemBuilder: (context) => [ ItemActionButton( - label: Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"), - action: () => ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(playerOptions: null), + label: Text( + "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"), + action: () => ref + .read(videoPlayerSettingsProvider.notifier) + .state = videoSettings.copyWith(playerOptions: null), ), ...PlayerOptions.available.map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(playerOptions: entry), + action: () => ref + .read(videoPlayerSettingsProvider.notifier) + .state = videoSettings.copyWith(playerOptions: entry), ), ) ], ), ...[ - if (currentPlayer == PlayerOptions.libMPV) SettingsLabelDivider(label: context.localized.video(1)), + if (currentPlayer == PlayerOptions.libMPV) + SettingsLabelDivider(label: context.localized.video(1)), if (currentPlayer == PlayerOptions.libMPV) ...[ SettingsListTile( - label: Text(context.localized.settingsPlayerVideoHWAccelTitle), - subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc), - onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel), + label: + Text(context.localized.settingsPlayerVideoHWAccelTitle), + subLabel: + Text(context.localized.settingsPlayerVideoHWAccelDesc), + onTap: () => + provider.setHardwareAccel(!videoSettings.hardwareAccel), trailing: Switch( value: videoSettings.hardwareAccel, onChanged: (value) => provider.setHardwareAccel(value), @@ -382,9 +436,12 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (!kIsWeb) SettingsListTile( - label: Text(context.localized.settingsPlayerNativeLibassAccelTitle), - subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc), - onTap: () => provider.setUseLibass(!videoSettings.useLibass), + label: Text( + context.localized.settingsPlayerNativeLibassAccelTitle), + subLabel: Text( + context.localized.settingsPlayerNativeLibassAccelDesc), + onTap: () => + provider.setUseLibass(!videoSettings.useLibass), trailing: Switch( value: videoSettings.useLibass, onChanged: (value) => provider.setUseLibass(value), @@ -395,16 +452,29 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.mediaTunnelingTitle), subLabel: Text(context.localized.mediaTunnelingDesc), - onTap: () => provider.setMediaTunneling(!videoSettings.enableTunneling), + onTap: () => provider + .setMediaTunneling(!videoSettings.enableTunneling), trailing: Switch( value: videoSettings.enableTunneling, onChanged: (value) => provider.setMediaTunneling(value), ), ), + if (currentPlayer == PlayerOptions.nativePlayer) + SettingsListTile( + label: Text(context.localized.ignoreHdr10PlusTitle), + subLabel: Text(context.localized.ignoreHdr10PlusDesc), + onTap: () => provider + .setIgnoreHdr10Plus(!videoSettings.ignoreHdr10Plus), + trailing: Switch( + value: videoSettings.ignoreHdr10Plus, + onChanged: (value) => provider.setIgnoreHdr10Plus(value), + ), + ), if (ref.read(argumentsStateProvider).leanBackMode) SettingsListTileEnum( label: Text(context.localized.playerSettingsScreensaverTitle), - subLabel: Text(context.localized.playerSettingsScreensaverDesc), + subLabel: + Text(context.localized.playerSettingsScreensaverDesc), current: videoSettings.screensaver.label(context), itemBuilder: (context) => Screensaver.values .map( @@ -416,8 +486,10 @@ class _PlayerSettingsPageState extends ConsumerState { .toList(), ), SettingsListTile( - label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), - subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc), + label: + Text(context.localized.settingsPlayerCustomSubtitlesTitle), + subLabel: + Text(context.localized.settingsPlayerCustomSubtitlesDesc), onTap: () { showDialog( context: context, @@ -429,21 +501,27 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (currentPlayer == PlayerOptions.libMPV) SettingsListTile( - label: Text(context.localized.settingsPlayerPlayPauseFadeTitle), - subLabel: Text(context.localized.settingsPlayerPlayPauseFadeDesc), - onTap: () => provider.setEnablePlayPauseFade(!videoSettings.enablePlayPauseFade), + label: + Text(context.localized.settingsPlayerPlayPauseFadeTitle), + subLabel: + Text(context.localized.settingsPlayerPlayPauseFadeDesc), + onTap: () => provider.setEnablePlayPauseFade( + !videoSettings.enablePlayPauseFade), trailing: Switch( value: videoSettings.enablePlayPauseFade, - onChanged: (value) => provider.setEnablePlayPauseFade(value), + onChanged: (value) => + provider.setEnablePlayPauseFade(value), ), ), if (currentPlayer == PlayerOptions.libMPV) SettingsListTile( label: Text(context.localized.settingsPlayerBufferSizeTitle), - subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), + subLabel: + Text(context.localized.settingsPlayerBufferSizeDesc), trailing: IntInputField( suffix: 'MB', - controller: TextEditingController(text: videoSettings.bufferSize.toString()), + controller: TextEditingController( + text: videoSettings.bufferSize.toString()), onSubmitted: (value) { if (value != null) { provider.setBufferSize(value); @@ -465,26 +543,33 @@ class _PlayerSettingsPageState extends ConsumerState { .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref.read(videoPlayerSettingsProvider.notifier).state = + action: () => ref + .read(videoPlayerSettingsProvider.notifier) + .state = videoSettings.copyWith(nextVideoType: entry), ), ) .toList(), ), AnimatedFadeSize( - child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { - AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), - AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), + child: switch (ref.watch(videoPlayerSettingsProvider + .select((value) => value.nextVideoType))) { + AutoNextType.smart => + SettingsMessageBox(AutoNextType.smart.desc(context)), + AutoNextType.static => + SettingsMessageBox(AutoNextType.static.desc(context)), _ => const SizedBox.shrink(), }, ), ], ), - if (currentPlayer == PlayerOptions.libMPV) SettingsLabelDivider(label: context.localized.audio(1)), + if (currentPlayer == PlayerOptions.libMPV) + SettingsLabelDivider(label: context.localized.audio(1)), SettingsListTile( label: Text(context.localized.playerSettingsReplayGainTitle), subLabel: Text(context.localized.playerSettingsReplayGainDesc), - onTap: () => provider.setEnableReplayGain(!videoSettings.enableReplayGain), + onTap: () => provider + .setEnableReplayGain(!videoSettings.enableReplayGain), trailing: Switch( value: videoSettings.enableReplayGain, onChanged: (value) => provider.setEnableReplayGain(value), @@ -492,14 +577,17 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (videoSettings.enableReplayGain) SettingsListTileEnum( - label: Text(context.localized.playerSettingsReplayGainLevelTitle), - subLabel: Text(context.localized.playerSettingsReplayGainLevelDesc), + label: Text( + context.localized.playerSettingsReplayGainLevelTitle), + subLabel: + Text(context.localized.playerSettingsReplayGainLevelDesc), current: videoSettings.replayGainVolumeLevel.label(context), itemBuilder: (context) => ReplayGainVolumeLevel.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => provider.setReplayGainVolumeLevel(entry), + action: () => + provider.setReplayGainVolumeLevel(entry), ), ) .toList(), @@ -508,15 +596,19 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.settingsPlayerCrossfadeTitle), subLabel: Text(context.localized.settingsPlayerCrossfadeDesc), - onTap: () => provider.setEnableCrossfade(!videoSettings.enableCrossfade), + onTap: () => provider + .setEnableCrossfade(!videoSettings.enableCrossfade), trailing: Switch( value: videoSettings.enableCrossfade, onChanged: (value) => provider.setEnableCrossfade(value), ), ), - if (currentPlayer == PlayerOptions.libMPV && crossfadeSupported && videoSettings.enableCrossfade) + if (currentPlayer == PlayerOptions.libMPV && + crossfadeSupported && + videoSettings.enableCrossfade) Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -537,9 +629,11 @@ class _PlayerSettingsPageState extends ConsumerState { child: FladderSlider( min: 200, max: 3000, - value: videoSettings.crossfadeDurationMs.toDouble(), + value: + videoSettings.crossfadeDurationMs.toDouble(), divisions: 28, - onChanged: (value) => provider.setCrossfadeDurationMs(value.round()), + onChanged: (value) => provider + .setCrossfadeDurationMs(value.round()), ), ), const SizedBox(width: 12), @@ -557,7 +651,8 @@ class _PlayerSettingsPageState extends ConsumerState { label: Text(context.localized.advancedVideoOptionsTitle), subLabel: Text(context.localized.advancedVideoOptionsDesc), onTap: () { - provider.setEnableAdvancedVideoOptions(!videoSettings.enableAdvancedVideoOptions); + provider.setEnableAdvancedVideoOptions( + !videoSettings.enableAdvancedVideoOptions); ref.read(videoPlayerProvider.notifier).init(); }, trailing: Switch( @@ -569,7 +664,9 @@ class _PlayerSettingsPageState extends ConsumerState { ), ), ], - if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode) + if (!AdaptiveLayout.of(context).isDesktop && + !kIsWeb && + !ref.read(argumentsStateProvider).htpcMode) SettingsListTile( label: Text(context.localized.playerSettingsOrientationTitle), subLabel: Text(context.localized.playerSettingsOrientationDesc), diff --git a/lib/src/player_settings_helper.g.dart b/lib/src/player_settings_helper.g.dart index 2b44241d0..62ee7cd61 100644 --- a/lib/src/player_settings_helper.g.dart +++ b/lib/src/player_settings_helper.g.dart @@ -14,21 +14,22 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } + bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } - enum Screensaver { disabled, dvd, @@ -78,6 +79,7 @@ enum SegmentSkip { class PlayerSettings { PlayerSettings({ required this.enableTunneling, + required this.ignoreHdr10Plus, required this.skipTypes, this.themeColor, required this.skipForward, @@ -91,6 +93,8 @@ class PlayerSettings { bool enableTunneling; + bool ignoreHdr10Plus; + Map skipTypes; int? themeColor; @@ -112,6 +116,7 @@ class PlayerSettings { List _toList() { return [ enableTunneling, + ignoreHdr10Plus, skipTypes, themeColor, skipForward, @@ -125,21 +130,25 @@ class PlayerSettings { } Object encode() { - return _toList(); } + return _toList(); + } static PlayerSettings decode(Object result) { result as List; return PlayerSettings( enableTunneling: result[0]! as bool, - skipTypes: (result[1] as Map?)!.cast(), - themeColor: result[2] as int?, - skipForward: result[3]! as int, - 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, - screensaver: result[9]! as Screensaver, + ignoreHdr10Plus: result[1]! as bool, + skipTypes: (result[2] as Map?)! + .cast(), + themeColor: result[3] as int?, + skipForward: result[4]! as int, + skipBackward: result[5]! as int, + autoNextType: result[6]! as AutoNextType, + acceptedOrientations: + (result[7] as List?)!.cast(), + fillScreen: result[8]! as bool, + videoFit: result[9]! as VideoPlayerFit, + screensaver: result[10]! as Screensaver, ); } @@ -157,11 +166,9 @@ class PlayerSettings { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()) -; + int get hashCode => Object.hashAll(_toList()); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -169,25 +176,25 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is Screensaver) { + } else if (value is Screensaver) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is VideoPlayerFit) { + } else if (value is VideoPlayerFit) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is PlayerOrientations) { + } else if (value is PlayerOrientations) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is AutoNextType) { + } else if (value is AutoNextType) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is SegmentType) { + } else if (value is SegmentType) { buffer.putUint8(133); writeValue(buffer, value.index); - } else if (value is SegmentSkip) { + } else if (value is SegmentSkip) { buffer.putUint8(134); writeValue(buffer, value.index); - } else if (value is PlayerSettings) { + } else if (value is PlayerSettings) { buffer.putUint8(135); writeValue(buffer, value.encode()); } else { @@ -198,25 +205,25 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final int? value = readValue(buffer) as int?; return value == null ? null : Screensaver.values[value]; - case 130: + case 130: final int? value = readValue(buffer) as int?; return value == null ? null : VideoPlayerFit.values[value]; - case 131: + case 131: final int? value = readValue(buffer) as int?; return value == null ? null : PlayerOrientations.values[value]; - case 132: + case 132: final int? value = readValue(buffer) as int?; return value == null ? null : AutoNextType.values[value]; - case 133: + case 133: final int? value = readValue(buffer) as int?; return value == null ? null : SegmentType.values[value]; - case 134: + case 134: final int? value = readValue(buffer) as int?; return value == null ? null : SegmentSkip.values[value]; - case 135: + case 135: return PlayerSettings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -228,9 +235,11 @@ class PlayerSettingsPigeon { /// Constructor for [PlayerSettingsPigeon]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - PlayerSettingsPigeon({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + PlayerSettingsPigeon( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -238,13 +247,16 @@ class PlayerSettingsPigeon { final String pigeonVar_messageChannelSuffix; Future sendPlayerSettings(PlayerSettings playerSettings) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerSettings]); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([playerSettings]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { diff --git a/pigeons/player_settings_pigeon.dart b/pigeons/player_settings_pigeon.dart index 1953a0d6c..df826d879 100644 --- a/pigeons/player_settings_pigeon.dart +++ b/pigeons/player_settings_pigeon.dart @@ -4,7 +4,8 @@ import 'package:pigeon/pigeon.dart'; PigeonOptions( dartOut: 'lib/src/player_settings_helper.g.dart', dartOptions: DartOptions(), - kotlinOut: 'android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt', + kotlinOut: + 'android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt', kotlinOptions: KotlinOptions( includeErrorClass: false, ), @@ -13,6 +14,7 @@ import 'package:pigeon/pigeon.dart'; ) class PlayerSettings { final bool enableTunneling; + final bool ignoreHdr10Plus; final Map skipTypes; //Color in ARGB32 format final int? themeColor; @@ -26,6 +28,7 @@ class PlayerSettings { const PlayerSettings({ required this.enableTunneling, + required this.ignoreHdr10Plus, required this.skipTypes, required this.themeColor, required this.skipForward, From 69278284f746390cf5ed9cc7de6789988b2c23f2 Mon Sep 17 00:00:00 2001 From: Sirulex Date: Mon, 22 Jun 2026 08:20:04 +0200 Subject: [PATCH 4/4] fix formatting --- .../settings/video_player_settings.dart | 76 ++--- .../pigeon_player_settings_provider.dart | 33 +-- .../video_player_settings_provider.dart | 93 ++---- .../settings/player_settings_page.dart | 269 ++++++------------ lib/src/player_settings_helper.g.dart | 36 +-- pigeons/player_settings_pigeon.dart | 3 +- 6 files changed, 177 insertions(+), 333 deletions(-) diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index b79ff816c..5d093ae37 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -95,8 +95,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(Bitrate.original) Bitrate maxHomeBitrate, @Default(Bitrate.original) Bitrate maxInternetBitrate, String? audioDevice, - @Default(defaultSegmentSkipValues) - Map segmentSkipSettings, + @Default(defaultSegmentSkipValues) Map segmentSkipSettings, @Default({}) Map hotKeys, @Default(Screensaver.logo) Screensaver screensaver, @Default(false) bool enableSpeedBoost, @@ -108,8 +107,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(false) bool reverseEdgeGestures, @Default(true) bool enablePictureInPicture, @Default(true) bool enableReplayGain, - @Default(ReplayGainVolumeLevel.quiet) - ReplayGainVolumeLevel replayGainVolumeLevel, + @Default(ReplayGainVolumeLevel.quiet) ReplayGainVolumeLevel replayGainVolumeLevel, @Default(true) bool enablePlayPauseFade, @Default(true) bool enableCrossfade, @Default(400) int crossfadeDurationMs, @@ -117,18 +115,15 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { double get volume => internalVolume; - factory VideoPlayerSettingsModel.fromJson(Map json) => - _$VideoPlayerSettingsModelFromJson(json); + factory VideoPlayerSettingsModel.fromJson(Map json) => _$VideoPlayerSettingsModelFromJson(json); - PlayerOptions get wantedPlayer => leanBackMode - ? PlayerOptions.nativePlayer - : playerOptions ?? PlayerOptions.platformDefaults; + PlayerOptions get wantedPlayer => + leanBackMode ? PlayerOptions.nativePlayer : playerOptions ?? PlayerOptions.platformDefaults; - Map get currentShortcuts => _defaultVideoHotKeys - .map((key, value) => MapEntry(key, hotKeys[key] ?? value)); + Map get currentShortcuts => + _defaultVideoHotKeys.map((key, value) => MapEntry(key, hotKeys[key] ?? value)); - Map get defaultShortCuts => - _defaultVideoHotKeys; + Map get defaultShortCuts => _defaultVideoHotKeys; bool get canUseCrossfade => crossfadeSupportedOnCurrentPlatform; @@ -299,42 +294,27 @@ Map get _defaultVideoHotKeys => { altKey: LogicalKeyboardKey.keyJ, altModifier: LogicalKeyboardKey.shiftLeft, ), - VideoHotKeys.stepForward => - KeyCombination(key: LogicalKeyboardKey.period), - VideoHotKeys.stepBack => - KeyCombination(key: LogicalKeyboardKey.comma), + VideoHotKeys.stepForward => KeyCombination(key: LogicalKeyboardKey.period), + VideoHotKeys.stepBack => KeyCombination(key: LogicalKeyboardKey.comma), VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM), - VideoHotKeys.volumeUp => - KeyCombination(key: LogicalKeyboardKey.arrowUp), - VideoHotKeys.volumeDown => - KeyCombination(key: LogicalKeyboardKey.arrowDown), - VideoHotKeys.speedUp => KeyCombination( - key: LogicalKeyboardKey.arrowUp, - modifier: LogicalKeyboardKey.controlLeft), - VideoHotKeys.speedDown => KeyCombination( - key: LogicalKeyboardKey.arrowDown, - modifier: LogicalKeyboardKey.controlLeft), - VideoHotKeys.prevVideo => KeyCombination( - key: LogicalKeyboardKey.keyP, - modifier: LogicalKeyboardKey.shiftLeft), - VideoHotKeys.nextVideo => KeyCombination( - key: LogicalKeyboardKey.keyN, - modifier: LogicalKeyboardKey.shiftLeft), - VideoHotKeys.nextChapter => - KeyCombination(key: LogicalKeyboardKey.pageUp), - VideoHotKeys.prevChapter => - KeyCombination(key: LogicalKeyboardKey.pageDown), - VideoHotKeys.fullScreen => - KeyCombination(key: LogicalKeyboardKey.keyF), - VideoHotKeys.skipMediaSegment => - KeyCombination(key: LogicalKeyboardKey.keyS), - VideoHotKeys.takeScreenshot => - KeyCombination(key: LogicalKeyboardKey.keyG), - VideoHotKeys.takeScreenshotClean => KeyCombination( - key: LogicalKeyboardKey.keyG, - modifier: LogicalKeyboardKey.controlLeft), - VideoHotKeys.toggleSubtitles => - KeyCombination(key: LogicalKeyboardKey.keyT), + VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp), + VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown), + VideoHotKeys.speedUp => + KeyCombination(key: LogicalKeyboardKey.arrowUp, modifier: LogicalKeyboardKey.controlLeft), + VideoHotKeys.speedDown => + KeyCombination(key: LogicalKeyboardKey.arrowDown, modifier: LogicalKeyboardKey.controlLeft), + VideoHotKeys.prevVideo => + KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft), + VideoHotKeys.nextVideo => + KeyCombination(key: LogicalKeyboardKey.keyN, modifier: LogicalKeyboardKey.shiftLeft), + VideoHotKeys.nextChapter => KeyCombination(key: LogicalKeyboardKey.pageUp), + VideoHotKeys.prevChapter => KeyCombination(key: LogicalKeyboardKey.pageDown), + VideoHotKeys.fullScreen => KeyCombination(key: LogicalKeyboardKey.keyF), + VideoHotKeys.skipMediaSegment => KeyCombination(key: LogicalKeyboardKey.keyS), + VideoHotKeys.takeScreenshot => KeyCombination(key: LogicalKeyboardKey.keyG), + VideoHotKeys.takeScreenshotClean => + KeyCombination(key: LogicalKeyboardKey.keyG, modifier: LogicalKeyboardKey.controlLeft), + VideoHotKeys.toggleSubtitles => KeyCombination(key: LogicalKeyboardKey.keyT), VideoHotKeys.exit => KeyCombination(key: LogicalKeyboardKey.escape), }, }; diff --git a/lib/providers/settings/pigeon_player_settings_provider.dart b/lib/providers/settings/pigeon_player_settings_provider.dart index 6632b50c4..7ce444e87 100644 --- a/lib/providers/settings/pigeon_player_settings_provider.dart +++ b/lib/providers/settings/pigeon_player_settings_provider.dart @@ -60,12 +60,8 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { AutoNextType.static => pigeon.AutoNextType.static, AutoNextType.smart => pigeon.AutoNextType.smart, }, - skipBackward: (userData?.userSettings?.skipBackDuration ?? - const Duration(seconds: 15)) - .inMilliseconds, - skipForward: (userData?.userSettings?.skipForwardDuration ?? - const Duration(seconds: 30)) - .inMilliseconds, + 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, @@ -76,21 +72,16 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { BoxFit.none => pigeon.VideoPlayerFit.none, BoxFit.scaleDown => pigeon.VideoPlayerFit.scaleDown, }, - acceptedOrientations: - (value.allowedOrientations?.toList() ?? DeviceOrientation.values) - .map( - (e) => switch (e) { - DeviceOrientation.portraitUp => - pigeon.PlayerOrientations.portraitUp, - DeviceOrientation.portraitDown => - pigeon.PlayerOrientations.portraitDown, - DeviceOrientation.landscapeLeft => - pigeon.PlayerOrientations.landScapeLeft, - DeviceOrientation.landscapeRight => - pigeon.PlayerOrientations.landScapeRight, - }, - ) - .toList(), + acceptedOrientations: (value.allowedOrientations?.toList() ?? DeviceOrientation.values) + .map( + (e) => switch (e) { + DeviceOrientation.portraitUp => pigeon.PlayerOrientations.portraitUp, + DeviceOrientation.portraitDown => pigeon.PlayerOrientations.portraitDown, + DeviceOrientation.landscapeLeft => pigeon.PlayerOrientations.landScapeLeft, + DeviceOrientation.landscapeRight => pigeon.PlayerOrientations.landScapeRight, + }, + ) + .toList(), ), ); } diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index ad7bd26db..8abac576e 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -14,17 +14,15 @@ import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; -final videoPlayerSettingsProvider = StateNotifierProvider< - VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) { +final videoPlayerSettingsProvider = + StateNotifierProvider((ref) { return VideoPlayerSettingsProviderNotifier(ref); }); final playbackRateProvider = StateProvider((ref) => 1.0); -class VideoPlayerSettingsProviderNotifier - extends StateNotifier { - VideoPlayerSettingsProviderNotifier(this.ref) - : super(_sanitizeCrossfade(VideoPlayerSettingsModel())) { +class VideoPlayerSettingsProviderNotifier extends StateNotifier { + VideoPlayerSettingsProviderNotifier(this.ref) : super(_sanitizeCrossfade(VideoPlayerSettingsModel())) { _initVolumeSync(); } @@ -72,8 +70,7 @@ class VideoPlayerSettingsProviderNotifier screenBrightness: value, ); if (state.screenBrightness != null) { - ScreenBrightness() - .setApplicationScreenBrightness(state.screenBrightness!); + ScreenBrightness().setApplicationScreenBrightness(state.screenBrightness!); } else { ScreenBrightness().resetApplicationScreenBrightness(); } @@ -81,8 +78,7 @@ class VideoPlayerSettingsProviderNotifier void setSavedBrightness() { if (state.screenBrightness != null) { - ScreenBrightness() - .setApplicationScreenBrightness(state.screenBrightness!); + ScreenBrightness().setApplicationScreenBrightness(state.screenBrightness!); } } @@ -90,20 +86,13 @@ class VideoPlayerSettingsProviderNotifier state = state.copyWith(fillScreen: value ?? false); } - void setHardwareAccel(bool? value) => - state = state.copyWith(hardwareAccel: value ?? true); - void setUseLibass(bool? value) => - state = state.copyWith(useLibass: value ?? false); - void setMediaTunneling(bool? value) => - state = state.copyWith(enableTunneling: value ?? false); - void setIgnoreHdr10Plus(bool value) => - state = state.copyWith(ignoreHdr10Plus: value); - void setBufferSize(int? value) => - state = state.copyWith(bufferSize: value ?? 32); - void setFitType(BoxFit? value) => - state = state.copyWith(videoFit: value ?? BoxFit.contain); - void setScreensaver(Screensaver? value) => - state = state.copyWith(screensaver: value ?? Screensaver.black); + void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value ?? true); + void setUseLibass(bool? value) => state = state.copyWith(useLibass: value ?? false); + void setMediaTunneling(bool? value) => state = state.copyWith(enableTunneling: value ?? false); + void setIgnoreHdr10Plus(bool value) => state = state.copyWith(ignoreHdr10Plus: value); + void setBufferSize(int? value) => state = state.copyWith(bufferSize: value ?? 32); + void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value ?? BoxFit.contain); + void setScreensaver(Screensaver? value) => state = state.copyWith(screensaver: value ?? Screensaver.black); void setVolume(double value) { state = state.copyWith(internalVolume: value); @@ -139,25 +128,20 @@ class VideoPlayerSettingsProviderNotifier state = state.copyWith(allowedOrientations: orientation); void setShortcuts(MapEntry newEntry) { - state = state.copyWith( - hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); + state = state.copyWith(hotKeys: state.hotKeys.setOrRemove(newEntry, state.defaultShortCuts)); } void nextChapter() { final chapters = ref.read(playBackModel)?.chapters ?? []; - final currentPosition = ref - .read(videoPlayerProvider.select((value) => value.lastState?.position)); + final currentPosition = ref.read(videoPlayerProvider.select((value) => value.lastState?.position)); if (chapters.isNotEmpty && currentPosition != null) { - final currentChapter = chapters.lastWhereOrNull( - (element) => element.startPosition <= currentPosition); + final currentChapter = chapters.lastWhereOrNull((element) => element.startPosition <= currentPosition); if (currentChapter != null) { final nextChapterIndex = chapters.indexOf(currentChapter) + 1; if (nextChapterIndex < chapters.length) { - ref - .read(videoPlayerProvider) - .seek(chapters[nextChapterIndex].startPosition); + ref.read(videoPlayerProvider).seek(chapters[nextChapterIndex].startPosition); } else { ref.read(videoPlayerProvider).seek(currentChapter.startPosition); } @@ -167,19 +151,15 @@ class VideoPlayerSettingsProviderNotifier void prevChapter() { final chapters = ref.read(playBackModel)?.chapters ?? []; - final currentPosition = ref - .read(videoPlayerProvider.select((value) => value.lastState?.position)); + final currentPosition = ref.read(videoPlayerProvider.select((value) => value.lastState?.position)); if (chapters.isNotEmpty && currentPosition != null) { - final currentChapter = chapters.lastWhereOrNull( - (element) => element.startPosition <= currentPosition); + final currentChapter = chapters.lastWhereOrNull((element) => element.startPosition <= currentPosition); if (currentChapter != null) { final prevChapterIndex = chapters.indexOf(currentChapter) - 1; if (prevChapterIndex >= 0) { - ref - .read(videoPlayerProvider) - .seek(chapters[prevChapterIndex].startPosition); + ref.read(videoPlayerProvider).seek(chapters[prevChapterIndex].startPosition); } else { ref.read(videoPlayerProvider).seek(currentChapter.startPosition); } @@ -187,47 +167,36 @@ class VideoPlayerSettingsProviderNotifier } } - void setEnableSpeedBoost(bool value) => - state = state.copyWith(enableSpeedBoost: value); + void setEnableSpeedBoost(bool value) => state = state.copyWith(enableSpeedBoost: value); void setSpeedBoostRate(double value) { final clampedValue = value.clamp(0.25, 3.0); state = state.copyWith(speedBoostRate: clampedValue); } - void setEnableDoubleTapSeek(bool value) => - state = state.copyWith(enableDoubleTapSeek: value); + void setEnableDoubleTapSeek(bool value) => state = state.copyWith(enableDoubleTapSeek: value); - void setEnableAdvancedVideoOptions(bool value) => - state = state.copyWith(enableAdvancedVideoOptions: value); + void setEnableAdvancedVideoOptions(bool value) => state = state.copyWith(enableAdvancedVideoOptions: value); - void setEnableEdgeGestures(bool value) => - state = state.copyWith(enableEdgeGestures: value); + void setEnableEdgeGestures(bool value) => state = state.copyWith(enableEdgeGestures: value); - void setReverseEdgeGestures(bool value) => - state = state.copyWith(reverseEdgeGestures: value); + void setReverseEdgeGestures(bool value) => state = state.copyWith(reverseEdgeGestures: value); - void setEnablePictureInPicture(bool value) => - state = state.copyWith(enablePictureInPicture: value); + void setEnablePictureInPicture(bool value) => state = state.copyWith(enablePictureInPicture: value); - void setEnableReplayGain(bool value) => - state = state.copyWith(enableReplayGain: value); + void setEnableReplayGain(bool value) => state = state.copyWith(enableReplayGain: value); - void setEnablePlayPauseFade(bool value) => - state = state.copyWith(enablePlayPauseFade: value); + void setEnablePlayPauseFade(bool value) => state = state.copyWith(enablePlayPauseFade: value); - void setReplayGainVolumeLevel(ReplayGainVolumeLevel value) => - state = state.copyWith(replayGainVolumeLevel: value); + void setReplayGainVolumeLevel(ReplayGainVolumeLevel value) => state = state.copyWith(replayGainVolumeLevel: value); void setEnableCrossfade(bool value) { state = state.copyWith(enableCrossfade: value && state.canUseCrossfade); } - void setCrossfadeDurationMs(int value) => - state = state.copyWith(crossfadeDurationMs: value); + void setCrossfadeDurationMs(int value) => state = state.copyWith(crossfadeDurationMs: value); - static VideoPlayerSettingsModel _sanitizeCrossfade( - VideoPlayerSettingsModel value) { + static VideoPlayerSettingsModel _sanitizeCrossfade(VideoPlayerSettingsModel value) { if (!value.canUseCrossfade && value.enableCrossfade) { return value.copyWith(enableCrossfade: false); } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 5995329b5..4a5ac6bdb 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -35,8 +35,7 @@ class PlayerSettingsPage extends ConsumerStatefulWidget { const PlayerSettingsPage({super.key}); @override - ConsumerState createState() => - _PlayerSettingsPageState(); + ConsumerState createState() => _PlayerSettingsPageState(); } class _PlayerSettingsPageState extends ConsumerState { @@ -47,8 +46,7 @@ class _PlayerSettingsPageState extends ConsumerState { final connectionState = ref.watch(connectivityStatusProvider); - final userSettings = - ref.watch(userProvider.select((value) => value?.userSettings)); + final userSettings = ref.watch(userProvider.select((value) => value?.userSettings)); final currentPlayer = videoSettings.wantedPlayer; final crossfadeSupported = videoSettings.canUseCrossfade; @@ -65,10 +63,8 @@ class _PlayerSettingsPageState extends ConsumerState { children: [ SettingsListTile( label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: - Text(context.localized.videoScalingFillScreenDesc), - onTap: () => - provider.setFillScreen(!videoSettings.fillScreen), + subLabel: Text(context.localized.videoScalingFillScreenDesc), + onTap: () => provider.setFillScreen(!videoSettings.fillScreen), trailing: Switch( value: videoSettings.fillScreen, onChanged: (value) => provider.setFillScreen(value), @@ -88,12 +84,10 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.pictureInPictureAutoTitle), subLabel: Text(context.localized.pictureInPictureSubtitle), - onTap: () => provider.setEnablePictureInPicture( - !videoSettings.enablePictureInPicture), + onTap: () => provider.setEnablePictureInPicture(!videoSettings.enablePictureInPicture), trailing: Switch( value: videoSettings.enablePictureInPicture, - onChanged: (value) => - provider.setEnablePictureInPicture(value), + onChanged: (value) => provider.setEnablePictureInPicture(value), ), ), SettingsListTileEnum( @@ -103,9 +97,7 @@ class _PlayerSettingsPageState extends ConsumerState { .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref - .read(videoPlayerSettingsProvider.notifier) - .setFitType(entry), + action: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry), ), ) .toList(), @@ -117,16 +109,14 @@ class _PlayerSettingsPageState extends ConsumerState { ), subLabel: Text(context.localized.homeStreamingQualityDesc), current: ref.watch( - videoPlayerSettingsProvider - .select((value) => value.maxHomeBitrate.label(context)), + videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), ), itemBuilder: (context) => Bitrate.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => - ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(maxHomeBitrate: entry), + action: () => ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(maxHomeBitrate: entry), ), ) .toList(), @@ -138,16 +128,14 @@ class _PlayerSettingsPageState extends ConsumerState { ), subLabel: Text(context.localized.internetStreamingQualityDesc), current: ref.watch( - videoPlayerSettingsProvider - .select((value) => value.maxInternetBitrate.label(context)), + videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), ), itemBuilder: (context) => Bitrate.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => - ref.read(videoPlayerSettingsProvider.notifier).state = - videoSettings.copyWith(maxInternetBitrate: entry), + action: () => ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(maxInternetBitrate: entry), ), ) .toList(), @@ -155,40 +143,27 @@ class _PlayerSettingsPageState extends ConsumerState { ], ), const SizedBox(height: 12), - ...settingsListGroup( - context, - SettingsLabelDivider(label: context.localized.mediaSegmentActions), - [ - ...videoSettings.segmentSkipSettings.entries - .sorted((a, b) => b.key.index.compareTo(a.key.index)) - .map( - (entry) => SettingsListTileEnum( - label: Text(entry.key.label(context)), - current: entry.value.label(context), - itemBuilder: (context) => SegmentSkip.values - .map( - (value) => ItemActionButton( - label: Text(value.label(context)), - action: () { - final newEntries = videoSettings - .segmentSkipSettings - .map((key, currentValue) => MapEntry( - key, - key == entry.key - ? value - : currentValue)); - ref - .read(videoPlayerSettingsProvider.notifier) - .state = - videoSettings.copyWith( - segmentSkipSettings: newEntries); - }, - ), - ) - .toList(), - ), - ), - ]), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ + ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( + (entry) => SettingsListTileEnum( + label: Text(entry.key.label(context)), + current: entry.value.label(context), + itemBuilder: (context) => SegmentSkip.values + .map( + (value) => ItemActionButton( + label: Text(value.label(context)), + action: () { + final newEntries = videoSettings.segmentSkipSettings + .map((key, currentValue) => MapEntry(key, key == entry.key ? value : currentValue)); + ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(segmentSkipSettings: newEntries); + }, + ), + ) + .toList(), + ), + ), + ]), const SizedBox(height: 12), ...settingsListGroup( context, @@ -199,8 +174,7 @@ class _PlayerSettingsPageState extends ConsumerState { label: Text(context.localized.skipBackLength), trailing: IntInputField( suffix: context.localized.seconds(10), - controller: TextEditingController( - text: userSettings.skipBackDuration.inSeconds.toString()), + controller: TextEditingController(text: userSettings.skipBackDuration.inSeconds.toString()), onSubmitted: (value) { if (value != null) { ref.read(userProvider.notifier).setBackwardSpeed(value); @@ -212,9 +186,7 @@ class _PlayerSettingsPageState extends ConsumerState { label: Text(context.localized.skipForwardLength), trailing: IntInputField( suffix: context.localized.seconds(10), - controller: TextEditingController( - text: - userSettings!.skipForwardDuration.inSeconds.toString()), + controller: TextEditingController(text: userSettings!.skipForwardDuration.inSeconds.toString()), onSubmitted: (value) { if (value != null) { ref.read(userProvider.notifier).setForwardSpeed(value); @@ -231,8 +203,7 @@ class _PlayerSettingsPageState extends ConsumerState { children: VideoHotKeys.values .map( (entry) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Expanded( @@ -244,11 +215,9 @@ class _PlayerSettingsPageState extends ConsumerState { Flexible( child: KeyCombinationWidget( currentKey: videoSettings.hotKeys[entry], - defaultKey: - videoSettings.defaultShortCuts[entry]!, - onChanged: (value) => ref - .read(videoPlayerSettingsProvider.notifier) - .setShortcuts(MapEntry(entry, value)), + defaultKey: videoSettings.defaultShortCuts[entry]!, + onChanged: (value) => + ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)), ), ), ], @@ -268,8 +237,7 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.enableDoubleTapSeekTitle), subLabel: Text(context.localized.enableDoubleTapSeekDesc), - onTap: () => provider - .setEnableDoubleTapSeek(!videoSettings.enableDoubleTapSeek), + onTap: () => provider.setEnableDoubleTapSeek(!videoSettings.enableDoubleTapSeek), trailing: Switch( value: videoSettings.enableDoubleTapSeek, onChanged: (value) => provider.setEnableDoubleTapSeek(value), @@ -278,8 +246,7 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.enableEdgeGesturesTitle), subLabel: Text(context.localized.enableEdgeGesturesDesc), - onTap: () => provider - .setEnableEdgeGestures(!videoSettings.enableEdgeGestures), + onTap: () => provider.setEnableEdgeGestures(!videoSettings.enableEdgeGestures), trailing: Switch( value: videoSettings.enableEdgeGestures, onChanged: (value) => provider.setEnableEdgeGestures(value), @@ -288,8 +255,7 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.reverseEdgeGesturesTitle), subLabel: Text(context.localized.reverseEdgeGesturesDesc), - onTap: () => provider - .setReverseEdgeGestures(!videoSettings.reverseEdgeGestures), + onTap: () => provider.setReverseEdgeGestures(!videoSettings.reverseEdgeGestures), trailing: Switch( value: videoSettings.reverseEdgeGestures, onChanged: (value) => provider.setReverseEdgeGestures(value), @@ -299,8 +265,7 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.enableSpeedBoostTitle), subLabel: Text(context.localized.enableSpeedBoostDesc), - onTap: () => - provider.setEnableSpeedBoost(!videoSettings.enableSpeedBoost), + onTap: () => provider.setEnableSpeedBoost(!videoSettings.enableSpeedBoost), trailing: Switch( value: videoSettings.enableSpeedBoost, onChanged: (value) => provider.setEnableSpeedBoost(value), @@ -308,8 +273,7 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (videoSettings.enableSpeedBoost) Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -333,8 +297,7 @@ class _PlayerSettingsPageState extends ConsumerState { max: 3.0, value: videoSettings.speedBoostRate, divisions: 55, - onChanged: (value) => - provider.setSpeedBoostRate(value), + onChanged: (value) => provider.setSpeedBoostRate(value), ), ), const SizedBox(width: 12), @@ -357,33 +320,23 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.rememberAudioSelections), subLabel: Text(context.localized.rememberAudioSelectionsDesc), - onTap: () => - ref.read(userProvider.notifier).setRememberAudioSelections(), + onTap: () => ref.read(userProvider.notifier).setRememberAudioSelections(), trailing: Switch( value: ref.watch(userProvider.select( - (value) => - value?.userConfiguration?.rememberAudioSelections ?? true, + (value) => value?.userConfiguration?.rememberAudioSelections ?? true, )), - onChanged: (_) => ref - .read(userProvider.notifier) - .setRememberAudioSelections(), + onChanged: (_) => ref.read(userProvider.notifier).setRememberAudioSelections(), ), ), SettingsListTile( label: Text(context.localized.rememberSubtitleSelections), subLabel: Text(context.localized.rememberSubtitleSelectionsDesc), - onTap: () => ref - .read(userProvider.notifier) - .setRememberSubtitleSelections(), + onTap: () => ref.read(userProvider.notifier).setRememberSubtitleSelections(), trailing: Switch( value: ref.watch(userProvider.select( - (value) => - value?.userConfiguration?.rememberSubtitleSelections ?? - true, + (value) => value?.userConfiguration?.rememberSubtitleSelections ?? true, )), - onChanged: (_) => ref - .read(userProvider.notifier) - .setRememberSubtitleSelections(), + onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(), ), ), ], @@ -402,33 +355,26 @@ class _PlayerSettingsPageState extends ConsumerState { : videoSettings.wantedPlayer.label(context), itemBuilder: (context) => [ ItemActionButton( - label: Text( - "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"), - action: () => ref - .read(videoPlayerSettingsProvider.notifier) - .state = videoSettings.copyWith(playerOptions: null), + label: Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"), + action: () => ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(playerOptions: null), ), ...PlayerOptions.available.map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref - .read(videoPlayerSettingsProvider.notifier) - .state = videoSettings.copyWith(playerOptions: entry), + action: () => ref.read(videoPlayerSettingsProvider.notifier).state = + videoSettings.copyWith(playerOptions: entry), ), ) ], ), ...[ - if (currentPlayer == PlayerOptions.libMPV) - SettingsLabelDivider(label: context.localized.video(1)), + if (currentPlayer == PlayerOptions.libMPV) SettingsLabelDivider(label: context.localized.video(1)), if (currentPlayer == PlayerOptions.libMPV) ...[ SettingsListTile( - label: - Text(context.localized.settingsPlayerVideoHWAccelTitle), - subLabel: - Text(context.localized.settingsPlayerVideoHWAccelDesc), - onTap: () => - provider.setHardwareAccel(!videoSettings.hardwareAccel), + label: Text(context.localized.settingsPlayerVideoHWAccelTitle), + subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc), + onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel), trailing: Switch( value: videoSettings.hardwareAccel, onChanged: (value) => provider.setHardwareAccel(value), @@ -436,12 +382,9 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (!kIsWeb) SettingsListTile( - label: Text( - context.localized.settingsPlayerNativeLibassAccelTitle), - subLabel: Text( - context.localized.settingsPlayerNativeLibassAccelDesc), - onTap: () => - provider.setUseLibass(!videoSettings.useLibass), + label: Text(context.localized.settingsPlayerNativeLibassAccelTitle), + subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc), + onTap: () => provider.setUseLibass(!videoSettings.useLibass), trailing: Switch( value: videoSettings.useLibass, onChanged: (value) => provider.setUseLibass(value), @@ -452,8 +395,7 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.mediaTunnelingTitle), subLabel: Text(context.localized.mediaTunnelingDesc), - onTap: () => provider - .setMediaTunneling(!videoSettings.enableTunneling), + onTap: () => provider.setMediaTunneling(!videoSettings.enableTunneling), trailing: Switch( value: videoSettings.enableTunneling, onChanged: (value) => provider.setMediaTunneling(value), @@ -463,8 +405,7 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.ignoreHdr10PlusTitle), subLabel: Text(context.localized.ignoreHdr10PlusDesc), - onTap: () => provider - .setIgnoreHdr10Plus(!videoSettings.ignoreHdr10Plus), + onTap: () => provider.setIgnoreHdr10Plus(!videoSettings.ignoreHdr10Plus), trailing: Switch( value: videoSettings.ignoreHdr10Plus, onChanged: (value) => provider.setIgnoreHdr10Plus(value), @@ -473,8 +414,7 @@ class _PlayerSettingsPageState extends ConsumerState { if (ref.read(argumentsStateProvider).leanBackMode) SettingsListTileEnum( label: Text(context.localized.playerSettingsScreensaverTitle), - subLabel: - Text(context.localized.playerSettingsScreensaverDesc), + subLabel: Text(context.localized.playerSettingsScreensaverDesc), current: videoSettings.screensaver.label(context), itemBuilder: (context) => Screensaver.values .map( @@ -486,10 +426,8 @@ class _PlayerSettingsPageState extends ConsumerState { .toList(), ), SettingsListTile( - label: - Text(context.localized.settingsPlayerCustomSubtitlesTitle), - subLabel: - Text(context.localized.settingsPlayerCustomSubtitlesDesc), + label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), + subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc), onTap: () { showDialog( context: context, @@ -501,27 +439,21 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (currentPlayer == PlayerOptions.libMPV) SettingsListTile( - label: - Text(context.localized.settingsPlayerPlayPauseFadeTitle), - subLabel: - Text(context.localized.settingsPlayerPlayPauseFadeDesc), - onTap: () => provider.setEnablePlayPauseFade( - !videoSettings.enablePlayPauseFade), + label: Text(context.localized.settingsPlayerPlayPauseFadeTitle), + subLabel: Text(context.localized.settingsPlayerPlayPauseFadeDesc), + onTap: () => provider.setEnablePlayPauseFade(!videoSettings.enablePlayPauseFade), trailing: Switch( value: videoSettings.enablePlayPauseFade, - onChanged: (value) => - provider.setEnablePlayPauseFade(value), + onChanged: (value) => provider.setEnablePlayPauseFade(value), ), ), if (currentPlayer == PlayerOptions.libMPV) SettingsListTile( label: Text(context.localized.settingsPlayerBufferSizeTitle), - subLabel: - Text(context.localized.settingsPlayerBufferSizeDesc), + subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), trailing: IntInputField( suffix: 'MB', - controller: TextEditingController( - text: videoSettings.bufferSize.toString()), + controller: TextEditingController(text: videoSettings.bufferSize.toString()), onSubmitted: (value) { if (value != null) { provider.setBufferSize(value); @@ -543,33 +475,26 @@ class _PlayerSettingsPageState extends ConsumerState { .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => ref - .read(videoPlayerSettingsProvider.notifier) - .state = + action: () => ref.read(videoPlayerSettingsProvider.notifier).state = videoSettings.copyWith(nextVideoType: entry), ), ) .toList(), ), AnimatedFadeSize( - child: switch (ref.watch(videoPlayerSettingsProvider - .select((value) => value.nextVideoType))) { - AutoNextType.smart => - SettingsMessageBox(AutoNextType.smart.desc(context)), - AutoNextType.static => - SettingsMessageBox(AutoNextType.static.desc(context)), + child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { + AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), + AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), _ => const SizedBox.shrink(), }, ), ], ), - if (currentPlayer == PlayerOptions.libMPV) - SettingsLabelDivider(label: context.localized.audio(1)), + if (currentPlayer == PlayerOptions.libMPV) SettingsLabelDivider(label: context.localized.audio(1)), SettingsListTile( label: Text(context.localized.playerSettingsReplayGainTitle), subLabel: Text(context.localized.playerSettingsReplayGainDesc), - onTap: () => provider - .setEnableReplayGain(!videoSettings.enableReplayGain), + onTap: () => provider.setEnableReplayGain(!videoSettings.enableReplayGain), trailing: Switch( value: videoSettings.enableReplayGain, onChanged: (value) => provider.setEnableReplayGain(value), @@ -577,17 +502,14 @@ class _PlayerSettingsPageState extends ConsumerState { ), if (videoSettings.enableReplayGain) SettingsListTileEnum( - label: Text( - context.localized.playerSettingsReplayGainLevelTitle), - subLabel: - Text(context.localized.playerSettingsReplayGainLevelDesc), + label: Text(context.localized.playerSettingsReplayGainLevelTitle), + subLabel: Text(context.localized.playerSettingsReplayGainLevelDesc), current: videoSettings.replayGainVolumeLevel.label(context), itemBuilder: (context) => ReplayGainVolumeLevel.values .map( (entry) => ItemActionButton( label: Text(entry.label(context)), - action: () => - provider.setReplayGainVolumeLevel(entry), + action: () => provider.setReplayGainVolumeLevel(entry), ), ) .toList(), @@ -596,19 +518,15 @@ class _PlayerSettingsPageState extends ConsumerState { SettingsListTile( label: Text(context.localized.settingsPlayerCrossfadeTitle), subLabel: Text(context.localized.settingsPlayerCrossfadeDesc), - onTap: () => provider - .setEnableCrossfade(!videoSettings.enableCrossfade), + onTap: () => provider.setEnableCrossfade(!videoSettings.enableCrossfade), trailing: Switch( value: videoSettings.enableCrossfade, onChanged: (value) => provider.setEnableCrossfade(value), ), ), - if (currentPlayer == PlayerOptions.libMPV && - crossfadeSupported && - videoSettings.enableCrossfade) + if (currentPlayer == PlayerOptions.libMPV && crossfadeSupported && videoSettings.enableCrossfade) Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -629,11 +547,9 @@ class _PlayerSettingsPageState extends ConsumerState { child: FladderSlider( min: 200, max: 3000, - value: - videoSettings.crossfadeDurationMs.toDouble(), + value: videoSettings.crossfadeDurationMs.toDouble(), divisions: 28, - onChanged: (value) => provider - .setCrossfadeDurationMs(value.round()), + onChanged: (value) => provider.setCrossfadeDurationMs(value.round()), ), ), const SizedBox(width: 12), @@ -651,8 +567,7 @@ class _PlayerSettingsPageState extends ConsumerState { label: Text(context.localized.advancedVideoOptionsTitle), subLabel: Text(context.localized.advancedVideoOptionsDesc), onTap: () { - provider.setEnableAdvancedVideoOptions( - !videoSettings.enableAdvancedVideoOptions); + provider.setEnableAdvancedVideoOptions(!videoSettings.enableAdvancedVideoOptions); ref.read(videoPlayerProvider.notifier).init(); }, trailing: Switch( @@ -664,9 +579,7 @@ class _PlayerSettingsPageState extends ConsumerState { ), ), ], - if (!AdaptiveLayout.of(context).isDesktop && - !kIsWeb && - !ref.read(argumentsStateProvider).htpcMode) + if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode) SettingsListTile( label: Text(context.localized.playerSettingsOrientationTitle), subLabel: Text(context.localized.playerSettingsOrientationDesc), diff --git a/lib/src/player_settings_helper.g.dart b/lib/src/player_settings_helper.g.dart index 62ee7cd61..7d71cc87f 100644 --- a/lib/src/player_settings_helper.g.dart +++ b/lib/src/player_settings_helper.g.dart @@ -1,6 +1,8 @@ // Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import +// ignore_for_file: unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types +// ignore_for_file: unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -17,15 +19,12 @@ PlatformException _createConnectionError(String channelName) { bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { - return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { return a.length == b.length && a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key])); } return a == b; } @@ -138,14 +137,12 @@ class PlayerSettings { return PlayerSettings( enableTunneling: result[0]! as bool, ignoreHdr10Plus: result[1]! as bool, - skipTypes: (result[2] as Map?)! - .cast(), + skipTypes: (result[2] as Map?)!.cast(), themeColor: result[3] as int?, skipForward: result[4]! as int, skipBackward: result[5]! as int, autoNextType: result[6]! as AutoNextType, - acceptedOrientations: - (result[7] as List?)!.cast(), + acceptedOrientations: (result[7] as List?)!.cast(), fillScreen: result[8]! as bool, videoFit: result[9]! as VideoPlayerFit, screensaver: result[10]! as Screensaver, @@ -235,11 +232,9 @@ class PlayerSettingsPigeon { /// Constructor for [PlayerSettingsPigeon]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - PlayerSettingsPigeon( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + PlayerSettingsPigeon({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -247,18 +242,15 @@ class PlayerSettingsPigeon { final String pigeonVar_messageChannelSuffix; Future sendPlayerSettings(PlayerSettings playerSettings) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final String pigeonVar_channelName = 'dev.flutter.pigeon.nl_jknaapen_fladder.settings.' + 'PlayerSettingsPigeon.sendPlayerSettings$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = - pigeonVar_channel.send([playerSettings]); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; + final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerSettings]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/pigeons/player_settings_pigeon.dart b/pigeons/player_settings_pigeon.dart index df826d879..ce9afef01 100644 --- a/pigeons/player_settings_pigeon.dart +++ b/pigeons/player_settings_pigeon.dart @@ -4,8 +4,7 @@ import 'package:pigeon/pigeon.dart'; PigeonOptions( dartOut: 'lib/src/player_settings_helper.g.dart', dartOptions: DartOptions(), - kotlinOut: - 'android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt', + kotlinOut: 'android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt', kotlinOptions: KotlinOptions( includeErrorClass: false, ),