From b18a5f1166288a0a54b2431050e131d14ba46c1b Mon Sep 17 00:00:00 2001 From: Sailendra Bathi Date: Tue, 7 Jul 2026 13:59:14 +0000 Subject: [PATCH 1/4] [video_player_android] Implement backBufferDurationMs in ExoPlayer --- .../video_player_android/CHANGELOG.md | 4 + .../videoplayer/VideoPlayerOptions.java | 16 + .../videoplayer/VideoPlayerPlugin.java | 10 +- .../platformview/PlatformViewVideoPlayer.java | 20 +- .../texture/TextureVideoPlayer.java | 20 +- .../flutter/plugins/videoplayer/Messages.kt | 308 ++++++++++-- .../PlatformViewVideoPlayerTest.java | 70 +++ .../videoplayer/TextureVideoPlayerTest.java | 27 ++ .../videoplayer/VideoPlayerPluginTest.java | 2 + .../lib/src/android_video_player.dart | 1 + .../lib/src/messages.g.dart | 444 ++++++++---------- .../pigeons/messages.dart | 1 + .../video_player_android/pubspec.yaml | 6 +- .../test/android_video_player_test.dart | 41 ++ 14 files changed, 669 insertions(+), 301 deletions(-) create mode 100644 packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewVideoPlayerTest.java diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 05a1c5a7e039..467d0b1dce3b 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.11.0 + +* Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration. + ## 2.10.0 * Implements `getVideoTracks()` and `selectVideoTrack()` methods for video track (quality) selection using ExoPlayer. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java index 20f7c5d2dbab..851ed1150d8e 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java @@ -4,6 +4,22 @@ package io.flutter.plugins.videoplayer; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public class VideoPlayerOptions { public boolean mixWithOthers; + + /** + * The duration of the back buffer in milliseconds, used to configure ExoPlayer's load control. + */ + @Nullable public Long backBufferDurationMs; + + public VideoPlayerOptions() {} + + /** Copy constructor to ensure all options are reliably copied. */ + public VideoPlayerOptions(@NonNull VideoPlayerOptions other) { + this.mixWithOthers = other.mixWithOthers; + this.backBufferDurationMs = other.backBufferDurationMs; + } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 49adaf4b7b33..5d520ac1f827 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -87,12 +87,15 @@ public long createForPlatformView(@NonNull CreationOptions options) { long id = nextPlayerIdentifier++; final String streamInstance = Long.toString(id); + VideoPlayerOptions playerOptions = new VideoPlayerOptions(sharedOptions); + playerOptions.backBufferDurationMs = options.getBackBufferDurationMs(); + VideoPlayer videoPlayer = PlatformViewVideoPlayer.create( flutterState.applicationContext, VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), videoAsset, - sharedOptions); + playerOptions); registerPlayerInstance(videoPlayer, id); return id; @@ -106,13 +109,16 @@ public long createForPlatformView(@NonNull CreationOptions options) { long id = nextPlayerIdentifier++; final String streamInstance = Long.toString(id); TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer(); + VideoPlayerOptions playerOptions = new VideoPlayerOptions(sharedOptions); + playerOptions.backBufferDurationMs = options.getBackBufferDurationMs(); + VideoPlayer videoPlayer = TextureVideoPlayer.create( flutterState.applicationContext, VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), handle, videoAsset, - sharedOptions); + playerOptions); registerPlayerInstance(videoPlayer, id); return new TexturePlayerIds(id, handle.id()); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java index a7c079773b58..716a8eb12daf 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java @@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.ExoPlayer; import io.flutter.plugins.videoplayer.ExoPlayerEventListener; import io.flutter.plugins.videoplayer.VideoAsset; @@ -56,12 +57,23 @@ public static PlatformViewVideoPlayer create( asset.getMediaItem(), options, () -> { + ExoPlayer.Builder builder = new ExoPlayer.Builder(context); + if (options.backBufferDurationMs != null && options.backBufferDurationMs > 0) { + // Clamp the value to ensure it fits within the int range expected by + // DefaultLoadControl. + int backBufferInt = + (int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE); + DefaultLoadControl loadControl = + new DefaultLoadControl.Builder() + .setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true) + .build(); + builder.setLoadControl(loadControl); + } androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); - ExoPlayer.Builder builder = - new ExoPlayer.Builder(context) - .setTrackSelector(trackSelector) - .setMediaSourceFactory(asset.getMediaSourceFactory(context)); + builder + .setTrackSelector(trackSelector) + .setMediaSourceFactory(asset.getMediaSourceFactory(context)); return builder.build(); }); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java index d623ddc88608..805ed100e1e8 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java @@ -12,6 +12,7 @@ import androidx.annotation.VisibleForTesting; import androidx.media3.common.MediaItem; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.ExoPlayer; import io.flutter.plugins.videoplayer.ExoPlayerEventListener; import io.flutter.plugins.videoplayer.VideoAsset; @@ -56,12 +57,23 @@ public static TextureVideoPlayer create( asset.getMediaItem(), options, () -> { + ExoPlayer.Builder builder = new ExoPlayer.Builder(context); + if (options.backBufferDurationMs != null && options.backBufferDurationMs > 0) { + // Clamp the value to ensure it fits within the int range expected by + // DefaultLoadControl. + int backBufferInt = + (int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE); + DefaultLoadControl loadControl = + new DefaultLoadControl.Builder() + .setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true) + .build(); + builder.setLoadControl(loadControl); + } androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); - ExoPlayer.Builder builder = - new ExoPlayer.Builder(context) - .setTrackSelector(trackSelector) - .setMediaSourceFactory(asset.getMediaSourceFactory(context)); + builder + .setTrackSelector(trackSelector) + .setMediaSourceFactory(asset.getMediaSourceFactory(context)); return builder.build(); }); } diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index e1a73f1bb326..5dad2b2b57f0 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -34,7 +34,36 @@ private object MessagesPigeonUtils { } } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } if (a is ByteArray && b is ByteArray) { return a.contentEquals(b) } @@ -45,20 +74,109 @@ private object MessagesPigeonUtils { return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true } if (a is List<*> && b is List<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true } if (a is Map<*, *> && b is Map<*, *>) { - return a.size == b.size && - a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false + } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) } return a == b } + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } } /** @@ -72,7 +190,7 @@ class FlutterError( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() /** Pigeon equivalent of video_platform_interface's VideoFormat. */ enum class PlatformVideoFormat(val raw: Int) { @@ -145,16 +263,27 @@ data class InitializationEvent( } override fun equals(other: Any?): Boolean { - if (other !is InitializationEvent) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as InitializationEvent + return MessagesPigeonUtils.deepEquals(this.duration, other.duration) && + MessagesPigeonUtils.deepEquals(this.width, other.width) && + MessagesPigeonUtils.deepEquals(this.height, other.height) && + MessagesPigeonUtils.deepEquals(this.rotationCorrection, other.rotationCorrection) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.duration) + result = 31 * result + MessagesPigeonUtils.deepHash(this.width) + result = 31 * result + MessagesPigeonUtils.deepHash(this.height) + result = 31 * result + MessagesPigeonUtils.deepHash(this.rotationCorrection) + return result + } } /** @@ -179,16 +308,21 @@ data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : Platform } override fun equals(other: Any?): Boolean { - if (other !is PlaybackStateChangeEvent) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as PlaybackStateChangeEvent + return MessagesPigeonUtils.deepEquals(this.state, other.state) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.state) + return result + } } /** @@ -213,16 +347,21 @@ data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { } override fun equals(other: Any?): Boolean { - if (other !is IsPlayingStateEvent) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as IsPlayingStateEvent + return MessagesPigeonUtils.deepEquals(this.isPlaying, other.isPlaying) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.isPlaying) + return result + } } /** @@ -251,16 +390,21 @@ data class AudioTrackChangedEvent( } override fun equals(other: Any?): Boolean { - if (other !is AudioTrackChangedEvent) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as AudioTrackChangedEvent + return MessagesPigeonUtils.deepEquals(this.selectedTrackId, other.selectedTrackId) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.selectedTrackId) + return result + } } /** @@ -324,16 +468,21 @@ data class PlatformVideoViewCreationParams(val playerId: Long) { } override fun equals(other: Any?): Boolean { - if (other !is PlatformVideoViewCreationParams) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as PlatformVideoViewCreationParams + return MessagesPigeonUtils.deepEquals(this.playerId, other.playerId) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.playerId) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -341,7 +490,8 @@ data class CreationOptions( val uri: String, val formatHint: PlatformVideoFormat? = null, val httpHeaders: Map, - val userAgent: String? = null + val userAgent: String? = null, + val backBufferDurationMs: Long? = null ) { companion object { fun fromList(pigeonVar_list: List): CreationOptions { @@ -349,7 +499,8 @@ data class CreationOptions( val formatHint = pigeonVar_list[1] as PlatformVideoFormat? val httpHeaders = pigeonVar_list[2] as Map val userAgent = pigeonVar_list[3] as String? - return CreationOptions(uri, formatHint, httpHeaders, userAgent) + val backBufferDurationMs = pigeonVar_list[4] as Long? + return CreationOptions(uri, formatHint, httpHeaders, userAgent, backBufferDurationMs) } } @@ -359,20 +510,34 @@ data class CreationOptions( formatHint, httpHeaders, userAgent, + backBufferDurationMs, ) } override fun equals(other: Any?): Boolean { - if (other !is CreationOptions) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as CreationOptions + return MessagesPigeonUtils.deepEquals(this.uri, other.uri) && + MessagesPigeonUtils.deepEquals(this.formatHint, other.formatHint) && + MessagesPigeonUtils.deepEquals(this.httpHeaders, other.httpHeaders) && + MessagesPigeonUtils.deepEquals(this.userAgent, other.userAgent) && + MessagesPigeonUtils.deepEquals(this.backBufferDurationMs, other.backBufferDurationMs) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.uri) + result = 31 * result + MessagesPigeonUtils.deepHash(this.formatHint) + result = 31 * result + MessagesPigeonUtils.deepHash(this.httpHeaders) + result = 31 * result + MessagesPigeonUtils.deepHash(this.userAgent) + result = 31 * result + MessagesPigeonUtils.deepHash(this.backBufferDurationMs) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -393,16 +558,23 @@ data class TexturePlayerIds(val playerId: Long, val textureId: Long) { } override fun equals(other: Any?): Boolean { - if (other !is TexturePlayerIds) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as TexturePlayerIds + return MessagesPigeonUtils.deepEquals(this.playerId, other.playerId) && + MessagesPigeonUtils.deepEquals(this.textureId, other.textureId) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.playerId) + result = 31 * result + MessagesPigeonUtils.deepHash(this.textureId) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -428,16 +600,23 @@ data class PlaybackState( } override fun equals(other: Any?): Boolean { - if (other !is PlaybackState) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as PlaybackState + return MessagesPigeonUtils.deepEquals(this.playPosition, other.playPosition) && + MessagesPigeonUtils.deepEquals(this.bufferPosition, other.bufferPosition) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.playPosition) + result = 31 * result + MessagesPigeonUtils.deepHash(this.bufferPosition) + return result + } } /** @@ -484,16 +663,35 @@ data class AudioTrackMessage( } override fun equals(other: Any?): Boolean { - if (other !is AudioTrackMessage) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as AudioTrackMessage + return MessagesPigeonUtils.deepEquals(this.id, other.id) && + MessagesPigeonUtils.deepEquals(this.label, other.label) && + MessagesPigeonUtils.deepEquals(this.language, other.language) && + MessagesPigeonUtils.deepEquals(this.isSelected, other.isSelected) && + MessagesPigeonUtils.deepEquals(this.bitrate, other.bitrate) && + MessagesPigeonUtils.deepEquals(this.sampleRate, other.sampleRate) && + MessagesPigeonUtils.deepEquals(this.channelCount, other.channelCount) && + MessagesPigeonUtils.deepEquals(this.codec, other.codec) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.id) + result = 31 * result + MessagesPigeonUtils.deepHash(this.label) + result = 31 * result + MessagesPigeonUtils.deepHash(this.language) + result = 31 * result + MessagesPigeonUtils.deepHash(this.isSelected) + result = 31 * result + MessagesPigeonUtils.deepHash(this.bitrate) + result = 31 * result + MessagesPigeonUtils.deepHash(this.sampleRate) + result = 31 * result + MessagesPigeonUtils.deepHash(this.channelCount) + result = 31 * result + MessagesPigeonUtils.deepHash(this.codec) + return result + } } /** @@ -551,16 +749,37 @@ data class ExoPlayerAudioTrackData( } override fun equals(other: Any?): Boolean { - if (other !is ExoPlayerAudioTrackData) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as ExoPlayerAudioTrackData + return MessagesPigeonUtils.deepEquals(this.groupIndex, other.groupIndex) && + MessagesPigeonUtils.deepEquals(this.trackIndex, other.trackIndex) && + MessagesPigeonUtils.deepEquals(this.label, other.label) && + MessagesPigeonUtils.deepEquals(this.language, other.language) && + MessagesPigeonUtils.deepEquals(this.isSelected, other.isSelected) && + MessagesPigeonUtils.deepEquals(this.bitrate, other.bitrate) && + MessagesPigeonUtils.deepEquals(this.sampleRate, other.sampleRate) && + MessagesPigeonUtils.deepEquals(this.channelCount, other.channelCount) && + MessagesPigeonUtils.deepEquals(this.codec, other.codec) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.groupIndex) + result = 31 * result + MessagesPigeonUtils.deepHash(this.trackIndex) + result = 31 * result + MessagesPigeonUtils.deepHash(this.label) + result = 31 * result + MessagesPigeonUtils.deepHash(this.language) + result = 31 * result + MessagesPigeonUtils.deepHash(this.isSelected) + result = 31 * result + MessagesPigeonUtils.deepHash(this.bitrate) + result = 31 * result + MessagesPigeonUtils.deepHash(this.sampleRate) + result = 31 * result + MessagesPigeonUtils.deepHash(this.channelCount) + result = 31 * result + MessagesPigeonUtils.deepHash(this.codec) + return result + } } /** @@ -586,16 +805,21 @@ data class NativeAudioTrackData( } override fun equals(other: Any?): Boolean { - if (other !is NativeAudioTrackData) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as NativeAudioTrackData + return MessagesPigeonUtils.deepEquals(this.exoPlayerTracks, other.exoPlayerTracks) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.exoPlayerTracks) + return result + } } /** diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewVideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewVideoPlayerTest.java new file mode 100644 index 000000000000..affbb6b9aa92 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewVideoPlayerTest.java @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class PlatformViewVideoPlayerTest { + private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4"; + private FakeVideoAsset fakeVideoAsset; + + @Mock private VideoPlayerCallbacks mockEvents; + @Mock private ExoPlayer mockExoPlayer; + + @Rule public MockitoRule initRule = MockitoJUnit.rule(); + + @Before + public void setUp() { + fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL); + } + + @OptIn(markerClass = UnstableApi.class) + @Test + public void create_withBackBufferDuration_setsLoadControl() { + Context mockContext = mock(Context.class); + VideoPlayerOptions options = new VideoPlayerOptions(); + options.backBufferDurationMs = 20000L; + + try (MockedConstruction mockedBuilder = + mockConstruction( + ExoPlayer.Builder.class, + (mock, context) -> { + when(mock.setLoadControl(any())).thenReturn(mock); + when(mock.setTrackSelector(any())).thenReturn(mock); + when(mock.setMediaSourceFactory(any())).thenReturn(mock); + when(mock.build()).thenReturn(mockExoPlayer); + })) { + + PlatformViewVideoPlayer player = + PlatformViewVideoPlayer.create(mockContext, mockEvents, fakeVideoAsset, options); + + assertEquals(1, mockedBuilder.constructed().size()); + ExoPlayer.Builder builderMock = mockedBuilder.constructed().get(0); + verify(builderMock).setLoadControl(any()); + player.dispose(); + } + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java index 6631d35a899f..befb22778650 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java @@ -25,6 +25,7 @@ import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; @@ -200,4 +201,30 @@ public void disposeReleasesExoPlayerBeforeTexture() { inOrder.verify(mockExoPlayer).release(); inOrder.verify(mockProducer).release(); } + + @Test + public void create_withBackBufferDuration_setsLoadControl() { + android.content.Context mockContext = mock(android.content.Context.class); + VideoPlayerOptions options = new VideoPlayerOptions(); + options.backBufferDurationMs = 20000L; + + try (MockedConstruction mockedBuilder = + mockConstruction( + ExoPlayer.Builder.class, + (mock, context) -> { + when(mock.setLoadControl(any())).thenReturn(mock); + when(mock.setTrackSelector(any())).thenReturn(mock); + when(mock.setMediaSourceFactory(any())).thenReturn(mock); + when(mock.build()).thenReturn(mockExoPlayer); + })) { + + TextureVideoPlayer player = + TextureVideoPlayer.create(mockContext, mockEvents, mockProducer, fakeVideoAsset, options); + + assertEquals(1, mockedBuilder.constructed().size()); + ExoPlayer.Builder builderMock = mockedBuilder.constructed().get(0); + verify(builderMock).setLoadControl(any()); + player.dispose(); + } + } } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java index 6093dc86573c..9f5e42fa66d3 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java @@ -81,6 +81,7 @@ public void createsPlatformViewVideoPlayer() throws Exception { "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", null, new HashMap<>(), + null, null); final long playerId = plugin.createForPlatformView(options); @@ -103,6 +104,7 @@ public void createsTextureVideoPlayer() throws Exception { "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", null, new HashMap<>(), + null, null); final TexturePlayerIds ids = plugin.createForTextureView(options); diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 4d4330e235d0..2024f18c5e97 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -103,6 +103,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { httpHeaders: httpHeaders, userAgent: userAgent, formatHint: formatHint, + backBufferDurationMs: options.videoPlayerOptions?.backBufferDurationMs, ); final int playerId; diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index e430203114fe..a7227eaf3a55 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -1,39 +1,103 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), 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, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == 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])); } 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]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + /// Pigeon equivalent of video_platform_interface's VideoFormat. enum PlatformVideoFormat { dash, hls, ss } @@ -91,12 +155,15 @@ class InitializationEvent extends PlatformVideoEvent { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(duration, other.duration) && + _deepEquals(width, other.width) && + _deepEquals(height, other.height) && + _deepEquals(rotationCorrection, other.rotationCorrection); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Sent when the video state changes. @@ -129,12 +196,12 @@ class PlaybackStateChangeEvent extends PlatformVideoEvent { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(state, other.state); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Sent when the video starts or stops playing. @@ -167,12 +234,12 @@ class IsPlayingStateEvent extends PlatformVideoEvent { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(isPlaying, other.isPlaying); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Sent when audio tracks change. @@ -207,12 +274,12 @@ class AudioTrackChangedEvent extends PlatformVideoEvent { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(selectedTrackId, other.selectedTrackId); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Sent when video tracks change. @@ -284,16 +351,22 @@ class PlatformVideoViewCreationParams { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(playerId, other.playerId); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class CreationOptions { - CreationOptions({required this.uri, this.formatHint, required this.httpHeaders, this.userAgent}); + CreationOptions({ + required this.uri, + this.formatHint, + required this.httpHeaders, + this.userAgent, + this.backBufferDurationMs, + }); String uri; @@ -303,8 +376,10 @@ class CreationOptions { String? userAgent; + int? backBufferDurationMs; + List _toList() { - return [uri, formatHint, httpHeaders, userAgent]; + return [uri, formatHint, httpHeaders, userAgent, backBufferDurationMs]; } Object encode() { @@ -318,6 +393,7 @@ class CreationOptions { formatHint: result[1] as PlatformVideoFormat?, httpHeaders: (result[2] as Map?)!.cast(), userAgent: result[3] as String?, + backBufferDurationMs: result[4] as int?, ); } @@ -330,12 +406,16 @@ class CreationOptions { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(uri, other.uri) && + _deepEquals(formatHint, other.formatHint) && + _deepEquals(httpHeaders, other.httpHeaders) && + _deepEquals(userAgent, other.userAgent) && + _deepEquals(backBufferDurationMs, other.backBufferDurationMs); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class TexturePlayerIds { @@ -367,12 +447,12 @@ class TexturePlayerIds { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(playerId, other.playerId) && _deepEquals(textureId, other.textureId); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class PlaybackState { @@ -406,12 +486,13 @@ class PlaybackState { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(playPosition, other.playPosition) && + _deepEquals(bufferPosition, other.bufferPosition); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Represents an audio track in a video. @@ -474,12 +555,19 @@ class AudioTrackMessage { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(id, other.id) && + _deepEquals(label, other.label) && + _deepEquals(language, other.language) && + _deepEquals(isSelected, other.isSelected) && + _deepEquals(bitrate, other.bitrate) && + _deepEquals(sampleRate, other.sampleRate) && + _deepEquals(channelCount, other.channelCount) && + _deepEquals(codec, other.codec); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Raw audio track data from ExoPlayer Format objects. @@ -556,12 +644,20 @@ class ExoPlayerAudioTrackData { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(groupIndex, other.groupIndex) && + _deepEquals(trackIndex, other.trackIndex) && + _deepEquals(label, other.label) && + _deepEquals(language, other.language) && + _deepEquals(isSelected, other.isSelected) && + _deepEquals(bitrate, other.bitrate) && + _deepEquals(sampleRate, other.sampleRate) && + _deepEquals(channelCount, other.channelCount) && + _deepEquals(codec, other.codec); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Container for raw audio track data from Android ExoPlayer. @@ -595,12 +691,12 @@ class NativeAudioTrackData { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(exoPlayerTracks, other.exoPlayerTracks); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Raw video track data from ExoPlayer Format objects. @@ -854,17 +950,8 @@ class AndroidVideoPlayerApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future createForPlatformView(CreationOptions options) async { @@ -877,22 +964,13 @@ class AndroidVideoPlayerApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; } Future createForTextureView(CreationOptions options) async { @@ -905,22 +983,13 @@ class AndroidVideoPlayerApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([options]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as TexturePlayerIds?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as TexturePlayerIds; } Future dispose(int playerId) async { @@ -933,17 +1002,8 @@ class AndroidVideoPlayerApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([playerId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future setMixWithOthers(bool mixWithOthers) async { @@ -956,17 +1016,8 @@ class AndroidVideoPlayerApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future getLookupKeyForAsset(String asset, String? packageName) async { @@ -982,22 +1033,13 @@ class AndroidVideoPlayerApi { packageName, ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as String?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; } } @@ -1027,17 +1069,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Sets the volume, with 0.0 being muted and 1.0 being full volume. @@ -1051,17 +1084,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Sets the playback speed as a multiple of normal speed. @@ -1075,17 +1099,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Begins playback if the video is not currently playing. @@ -1099,17 +1114,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Pauses playback if the video is currently playing. @@ -1123,17 +1129,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Seeks to the given playback position, in milliseconds. @@ -1147,17 +1144,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Returns the current playback position, in milliseconds. @@ -1171,22 +1159,13 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; } /// Returns the current buffer position, in milliseconds. @@ -1200,22 +1179,13 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as int?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; } /// Gets the available audio tracks for the video. @@ -1229,22 +1199,13 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as NativeAudioTrackData?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as NativeAudioTrackData; } /// Selects which audio track is chosen for playback from its [groupIndex] and [trackIndex] @@ -1261,17 +1222,8 @@ class VideoPlayerInstanceApi { trackIndex, ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Gets the available video tracks for the video. diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index c6ed0f4b842b..75a82aa6389c 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -82,6 +82,7 @@ class CreationOptions { PlatformVideoFormat? formatHint; Map httpHeaders; String? userAgent; + int? backBufferDurationMs; } class TexturePlayerIds { diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index a86cfe53e1ae..cf67c91cf488 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.10.0 +version: 2.11.0 environment: sdk: ^3.12.0 @@ -20,14 +20,14 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.7.0 + video_player_platform_interface: ^6.9.0 dev_dependencies: build_runner: ^2.3.3 flutter_test: sdk: flutter mockito: ^5.4.4 - pigeon: ^26.1.5 + pigeon: ^26.3.4 topics: - video diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 6fe80ac5aa28..3a09bb033490 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -426,6 +426,47 @@ void main() { ); }); + test('createWithOptions passes backBufferDurationMs for texture view', () async { + final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer( + playerId: 1, + textureId: 100, + ); + when( + api.createForTextureView(any), + ).thenAnswer((_) async => TexturePlayerIds(playerId: 2, textureId: 100)); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource(sourceType: DataSourceType.network, uri: 'https://example.com'), + viewType: VideoViewType.textureView, + videoPlayerOptions: VideoPlayerOptions(backBufferDurationMs: 20000), + ), + ); + + final VerificationResult verification = verify(api.createForTextureView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.backBufferDurationMs, 20000); + }); + + test('createWithOptions passes backBufferDurationMs for platform view', () async { + final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = setUpMockPlayer( + playerId: 1, + ); + when(api.createForPlatformView(any)).thenAnswer((_) async => 2); + + await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource(sourceType: DataSourceType.network, uri: 'https://example.com'), + viewType: VideoViewType.platformView, + videoPlayerOptions: VideoPlayerOptions(backBufferDurationMs: 20000), + ), + ); + + final VerificationResult verification = verify(api.createForPlatformView(captureAny)); + final creationOptions = verification.captured[0] as CreationOptions; + expect(creationOptions.backBufferDurationMs, 20000); + }); + test('setLooping', () async { final (AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi) = setUpMockPlayer( playerId: 1, From d10a89c9abb3edf6d90eb2bd7254723d7993505e Mon Sep 17 00:00:00 2001 From: sailendrabathi <44525804+sailendrabathi@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:11:21 +0530 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../platformview/PlatformViewVideoPlayer.java | 25 +++++++++++-------- .../texture/TextureVideoPlayer.java | 25 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java index 716a8eb12daf..a4b5a93890a4 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java @@ -58,16 +58,21 @@ public static PlatformViewVideoPlayer create( options, () -> { ExoPlayer.Builder builder = new ExoPlayer.Builder(context); - if (options.backBufferDurationMs != null && options.backBufferDurationMs > 0) { - // Clamp the value to ensure it fits within the int range expected by - // DefaultLoadControl. - int backBufferInt = - (int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE); - DefaultLoadControl loadControl = - new DefaultLoadControl.Builder() - .setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true) - .build(); - builder.setLoadControl(loadControl); + if (options.backBufferDurationMs != null) { + if (options.backBufferDurationMs < 0) { + throw new IllegalArgumentException("backBufferDurationMs must be at least 0"); + } + if (options.backBufferDurationMs > 0) { + // Clamp the value to ensure it fits within the int range expected by + // DefaultLoadControl. + int backBufferInt = + (int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE); + DefaultLoadControl loadControl = + new DefaultLoadControl.Builder() + .setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true) + .build(); + builder.setLoadControl(loadControl); + } } androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java index 805ed100e1e8..c0a2d4189176 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java @@ -58,16 +58,21 @@ public static TextureVideoPlayer create( options, () -> { ExoPlayer.Builder builder = new ExoPlayer.Builder(context); - if (options.backBufferDurationMs != null && options.backBufferDurationMs > 0) { - // Clamp the value to ensure it fits within the int range expected by - // DefaultLoadControl. - int backBufferInt = - (int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE); - DefaultLoadControl loadControl = - new DefaultLoadControl.Builder() - .setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true) - .build(); - builder.setLoadControl(loadControl); + if (options.backBufferDurationMs != null) { + if (options.backBufferDurationMs < 0) { + throw new IllegalArgumentException("backBufferDurationMs must be at least 0"); + } + if (options.backBufferDurationMs > 0) { + // Clamp the value to ensure it fits within the int range expected by + // DefaultLoadControl. + int backBufferInt = + (int) Math.min(options.backBufferDurationMs.longValue(), Integer.MAX_VALUE); + DefaultLoadControl loadControl = + new DefaultLoadControl.Builder() + .setBackBuffer(backBufferInt, /* retainBackBufferFromKeyframe= */ true) + .build(); + builder.setLoadControl(loadControl); + } } androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector = new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context); From d93df701332e5ea0c285afb196c1f94ee236aca3 Mon Sep 17 00:00:00 2001 From: Sailendra Bathi Date: Thu, 9 Jul 2026 06:12:09 +0000 Subject: [PATCH 3/4] Regenerate messages after sync to main --- .../flutter/plugins/videoplayer/Messages.kt | 49 ++++++++++--- .../lib/src/messages.g.dart | 71 +++++++------------ 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt index 5dad2b2b57f0..40ffc57d8721 100644 --- a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -436,16 +436,21 @@ data class VideoTrackChangedEvent( } override fun equals(other: Any?): Boolean { - if (other !is VideoTrackChangedEvent) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as VideoTrackChangedEvent + return MessagesPigeonUtils.deepEquals(this.selectedTrackId, other.selectedTrackId) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.selectedTrackId) + return result + } } /** @@ -869,16 +874,37 @@ data class ExoPlayerVideoTrackData( } override fun equals(other: Any?): Boolean { - if (other !is ExoPlayerVideoTrackData) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as ExoPlayerVideoTrackData + return MessagesPigeonUtils.deepEquals(this.groupIndex, other.groupIndex) && + MessagesPigeonUtils.deepEquals(this.trackIndex, other.trackIndex) && + MessagesPigeonUtils.deepEquals(this.label, other.label) && + MessagesPigeonUtils.deepEquals(this.isSelected, other.isSelected) && + MessagesPigeonUtils.deepEquals(this.bitrate, other.bitrate) && + MessagesPigeonUtils.deepEquals(this.width, other.width) && + MessagesPigeonUtils.deepEquals(this.height, other.height) && + MessagesPigeonUtils.deepEquals(this.frameRate, other.frameRate) && + MessagesPigeonUtils.deepEquals(this.codec, other.codec) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.groupIndex) + result = 31 * result + MessagesPigeonUtils.deepHash(this.trackIndex) + result = 31 * result + MessagesPigeonUtils.deepHash(this.label) + result = 31 * result + MessagesPigeonUtils.deepHash(this.isSelected) + result = 31 * result + MessagesPigeonUtils.deepHash(this.bitrate) + result = 31 * result + MessagesPigeonUtils.deepHash(this.width) + result = 31 * result + MessagesPigeonUtils.deepHash(this.height) + result = 31 * result + MessagesPigeonUtils.deepHash(this.frameRate) + result = 31 * result + MessagesPigeonUtils.deepHash(this.codec) + return result + } } /** @@ -904,16 +930,21 @@ data class NativeVideoTrackData( } override fun equals(other: Any?): Boolean { - if (other !is NativeVideoTrackData) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as NativeVideoTrackData + return MessagesPigeonUtils.deepEquals(this.exoPlayerTracks, other.exoPlayerTracks) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.exoPlayerTracks) + return result + } } private open class MessagesPigeonCodec : StandardMessageCodec() { diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index a7227eaf3a55..e51d72172a3b 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -315,12 +315,12 @@ class VideoTrackChangedEvent extends PlatformVideoEvent { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(selectedTrackId, other.selectedTrackId); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Information passed to the platform view creation. @@ -391,7 +391,7 @@ class CreationOptions { return CreationOptions( uri: result[0]! as String, formatHint: result[1] as PlatformVideoFormat?, - httpHeaders: (result[2] as Map?)!.cast(), + httpHeaders: (result[2]! as Map).cast(), userAgent: result[3] as String?, backBufferDurationMs: result[4] as int?, ); @@ -773,12 +773,20 @@ class ExoPlayerVideoTrackData { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(groupIndex, other.groupIndex) && + _deepEquals(trackIndex, other.trackIndex) && + _deepEquals(label, other.label) && + _deepEquals(isSelected, other.isSelected) && + _deepEquals(bitrate, other.bitrate) && + _deepEquals(width, other.width) && + _deepEquals(height, other.height) && + _deepEquals(frameRate, other.frameRate) && + _deepEquals(codec, other.codec); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// Container for raw video track data from Android ExoPlayer. @@ -812,12 +820,12 @@ class NativeVideoTrackData { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(exoPlayerTracks, other.exoPlayerTracks); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class _PigeonCodec extends StandardMessageCodec { @@ -1237,22 +1245,13 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as NativeVideoTrackData?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as NativeVideoTrackData; } /// Selects which video track is chosen for playback from its [groupIndex] and [trackIndex]. @@ -1269,17 +1268,8 @@ class VideoPlayerInstanceApi { trackIndex, ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } /// Enables automatic video quality selection, allowing the player to adaptively @@ -1294,17 +1284,8 @@ class VideoPlayerInstanceApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } } From 08744e66010c72c779dd6f373420fb049cc28568 Mon Sep 17 00:00:00 2001 From: stuartmorgan-g Date: Thu, 9 Jul 2026 10:36:25 -0400 Subject: [PATCH 4/4] Add meta for Pigeon update --- packages/video_player/video_player_android/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index cf67c91cf488..11e0efc18141 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -20,6 +20,7 @@ flutter: dependencies: flutter: sdk: flutter + meta: ^1.10.0 video_player_platform_interface: ^6.9.0 dev_dependencies: