From 959dbb3e56d4c96ecec2312e6565ad493c36f932 Mon Sep 17 00:00:00 2001 From: Sailendra Bathi Date: Wed, 27 May 2026 13:51:22 +0000 Subject: [PATCH 1/3] [video_player] Improve seek performance on Android Adds `backBufferDurationMs` to `VideoPlayerOptions` and configures ExoPlayer's `DefaultLoadControl` to retain the back buffer from keyframes. This significantly improves seek performance and responsiveness during video playback on Android. --- .../video_player/video_player/CHANGELOG.md | 4 + .../video_player/example/lib/main.dart | 2 +- .../video_player/lib/video_player.dart | 1 + .../video_player/video_player/pubspec.yaml | 6 +- .../video_player_initialization_test.dart | 22 +- .../video_player/test/video_player_test.dart | 14 +- .../video_player_android/CHANGELOG.md | 4 + .../videoplayer/VideoPlayerOptions.java | 13 + .../videoplayer/VideoPlayerPlugin.java | 10 +- .../platformview/PlatformViewVideoPlayer.java | 20 +- .../texture/TextureVideoPlayer.java | 20 +- .../flutter/plugins/videoplayer/Messages.kt | 308 ++++++++++-- .../PlatformViewVideoPlayerTest.java | 68 +++ .../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 ++ .../test/android_video_player_test.mocks.dart | 1 + .../CHANGELOG.md | 4 + .../lib/video_player_platform_interface.dart | 20 +- .../pubspec.yaml | 2 +- .../test/video_player_options_test.dart | 12 +- 25 files changed, 734 insertions(+), 319 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/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 4e4c13b0a0f..eb02504e15a 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -2,6 +2,10 @@ * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. +## 2.12.0 + +* Passes `backBufferDurationMs` from `VideoPlayerOptions` to the underlying platform interface. + ## 2.11.1 * Optimizes caption retrieval with binary search. diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index 61e24a89107..b018e92e5cf 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -296,7 +296,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { _controller = VideoPlayerController.networkUrl( Uri.parse('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), closedCaptionFile: _loadCaptions(), - videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), + videoPlayerOptions: const VideoPlayerOptions(mixWithOthers: true), viewType: widget.viewType, ); diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b287a39fed2..cd5a110ec28 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -560,6 +560,7 @@ class VideoPlayerController extends ValueNotifier { final creationOptions = platform_interface.VideoCreationOptions( dataSource: dataSourceDescription, viewType: viewType, + videoPlayerOptions: videoPlayerOptions, ); if (videoPlayerOptions?.mixWithOthers != null) { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c55f4e823c4..f4ea96251aa 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, macOS and web. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.11.1 +version: 2.12.0 environment: sdk: ^3.10.0 @@ -26,9 +26,9 @@ dependencies: flutter: sdk: flutter html: ^0.15.0 - video_player_android: ^2.9.1 + video_player_android: ^2.10.0 video_player_avfoundation: ^2.9.0 - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: ^6.8.0 video_player_web: ^2.1.0 dev_dependencies: diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 4524e8aca16..40a1e748fb8 100644 --- a/packages/video_player/video_player/test/video_player_initialization_test.dart +++ b/packages/video_player/video_player/test/video_player_initialization_test.dart @@ -33,7 +33,7 @@ void main() { final controller = VideoPlayerController.networkUrl( Uri.parse('https://127.0.0.1'), - videoPlayerOptions: VideoPlayerOptions(webOptions: expected), + videoPlayerOptions: const VideoPlayerOptions(webOptions: expected), ); await controller.initialize(); @@ -73,4 +73,24 @@ void main() { reason: 'view type must be passed to the platform', ); }); + + test('back buffer duration is forwarded to platform', () async { + const expectedBackBufferDurationMs = 20000; + + final controller = VideoPlayerController.networkUrl( + Uri.parse('https://127.0.0.1'), + videoPlayerOptions: const VideoPlayerOptions( + backBufferDurationMs: expectedBackBufferDurationMs, + ), + ); + + await controller.initialize(); + + expect( + fakeVideoPlayerPlatform.videoPlayerOptions.last?.backBufferDurationMs, + expectedBackBufferDurationMs, + reason: + 'backBufferDurationMs must be forwarded to the platform via VideoCreationOptions.videoPlayerOptions', + ); + }); } diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 56cd402f122..11cb340068b 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -1027,13 +1027,11 @@ void main() { } expect(isSorted, false, reason: 'Expected captions to be unsorted'); - expect(captions.map((Caption c) => c.text).toList(), [ - 'one', - 'two', - 'three', - 'five', - 'four', - ], reason: 'Captions should be in original unsorted order'); + expect( + captions.map((Caption c) => c.text).toList(), + ['one', 'two', 'three', 'five', 'four'], + reason: 'Captions should be in original unsorted order', + ); }); test('works when seeking, includes all captions', () async { @@ -1900,6 +1898,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { List dataSources = []; List viewTypes = []; final Map> streams = >{}; + List videoPlayerOptions = []; bool forceInitError = false; int nextPlayerId = 0; final Map _positions = {}; @@ -1943,6 +1942,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { } dataSources.add(options.dataSource); viewTypes.add(options.viewType); + videoPlayerOptions.add(options.videoPlayerOptions); return nextPlayerId++; } diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 99cda569f5c..699849d31fd 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -3,6 +3,10 @@ * Migrates to Built-in Kotlin to support AGP 9. * Updates minimum supported SDK version to Flutter 3.44/Dart 3.12. +## 2.10.0 + +* Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration. + ## 2.9.5 * Updates build files from Groovy to Kotlin. 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 20f7c5d2dba..a9b4ba3fb74 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 @@ -6,4 +6,17 @@ public class VideoPlayerOptions { public boolean mixWithOthers; + + /** + * The duration of the back buffer in milliseconds, used to configure ExoPlayer's load control. + */ + public Long backBufferDurationMs; + + public VideoPlayerOptions() {} + + /** Copy constructor to ensure all options are reliably copied. */ + public VideoPlayerOptions(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 49adaf4b7b3..5d520ac1f82 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 a7c079773b5..716a8eb12da 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 d623ddc8860..805ed100e1e 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 e546c744e56..dcd83db115e 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 + } } /** @@ -283,16 +427,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. */ @@ -300,7 +449,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 { @@ -308,7 +458,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) } } @@ -318,20 +469,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. */ @@ -352,16 +517,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. */ @@ -387,16 +559,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 + } } /** @@ -443,16 +622,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 + } } /** @@ -510,16 +708,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 + } } /** @@ -545,16 +764,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 + } } private open class MessagesPigeonCodec : StandardMessageCodec() { 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 00000000000..f1449d2d714 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewVideoPlayerTest.java @@ -0,0 +1,68 @@ +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); + mockEvents = mock(VideoPlayerCallbacks.class); + mockExoPlayer = mock(ExoPlayer.class); + } + + @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 6631d35a899..befb2277865 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 6093dc86573..9f5e42fa66d 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 5ecf673a7ae..e886ee8119e 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 2782e80e8c1..52ee98931ac 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()]); } /// Information passed to the platform view creation. @@ -243,16 +310,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; @@ -262,8 +335,10 @@ class CreationOptions { String? userAgent; + int? backBufferDurationMs; + List _toList() { - return [uri, formatHint, httpHeaders, userAgent]; + return [uri, formatHint, httpHeaders, userAgent, backBufferDurationMs]; } Object encode() { @@ -277,6 +352,7 @@ class CreationOptions { formatHint: result[1] as PlatformVideoFormat?, httpHeaders: (result[2] as Map?)!.cast(), userAgent: result[3] as String?, + backBufferDurationMs: result[4] as int?, ); } @@ -289,12 +365,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 { @@ -326,12 +406,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 { @@ -365,12 +445,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. @@ -433,12 +514,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. @@ -515,12 +603,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. @@ -554,12 +650,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()]); } class _PigeonCodec extends StandardMessageCodec { @@ -677,17 +773,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 { @@ -700,22 +787,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 { @@ -728,22 +806,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 { @@ -756,17 +825,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 { @@ -779,17 +839,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 { @@ -805,22 +856,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; } } @@ -850,17 +892,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. @@ -874,17 +907,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. @@ -898,17 +922,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. @@ -922,17 +937,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. @@ -946,17 +952,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. @@ -970,17 +967,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. @@ -994,22 +982,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. @@ -1023,22 +1002,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. @@ -1052,22 +1022,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] @@ -1084,17 +1045,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); } } diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 5b67adb40fa..6f21091fc86 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -72,6 +72,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 d2cdfc394bc..f214bb77b35 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.9.6 +version: 2.10.0 environment: sdk: ^3.12.0 @@ -20,14 +20,14 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: ^6.8.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 84239afc78c..9200d834b80 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 @@ -416,6 +416,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: const 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: const 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, diff --git a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart index 7fdb39f1ba1..1c805452480 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart @@ -22,6 +22,7 @@ import 'package:video_player_android/src/messages.g.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeTexturePlayerIds_0 extends _i1.SmartFake implements _i2.TexturePlayerIds { _FakeTexturePlayerIds_0(Object parent, Invocation parentInvocation) diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 203f913e937..5f125ab1efb 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -2,6 +2,10 @@ * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. +## 6.8.0 + +* Adds `backBufferDurationMs` to `VideoPlayerOptions` to support configuring ExoPlayer back buffer duration on Android. + ## 6.7.0 * Adds `VideoTrack` class and `getVideoTracks()`, `selectVideoTrack()`, `isVideoTrackSupportAvailable()` methods for video track (quality) selection. diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 16c83ed3501..aaea70ba531 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -458,11 +458,15 @@ class VideoPlayerOptions { // in all of the other video player packages, fix this, and then update // the other packages to use const. // ignore: prefer_const_constructors_in_immutables - VideoPlayerOptions({ + const VideoPlayerOptions({ this.mixWithOthers = false, this.allowBackgroundPlayback = false, this.webOptions, - }); + this.backBufferDurationMs, + }) : assert( + backBufferDurationMs == null || backBufferDurationMs > 0, + 'backBufferDurationMs must be greater than zero', + ); /// Set this to true to keep playing video in background, when app goes in background. /// The default value is false. @@ -477,6 +481,9 @@ class VideoPlayerOptions { /// Additional web controls final VideoPlayerWebOptions? webOptions; + + /// ** Android only **. Sets ExoPlayer's back buffer duration in milliseconds. + final int? backBufferDurationMs; } /// [VideoPlayerWebOptions] can be optionally used to set additional web settings @@ -576,13 +583,20 @@ class VideoViewOptions { @immutable class VideoCreationOptions { /// Constructs an instance of [VideoCreationOptions]. - const VideoCreationOptions({required this.dataSource, required this.viewType}); + const VideoCreationOptions({ + required this.dataSource, + required this.viewType, + this.videoPlayerOptions, + }); /// The data source used to create the player. final DataSource dataSource; /// The type of view to be used for displaying the video player final VideoViewType viewType; + + /// Additional configuration options for the video player. + final VideoPlayerOptions? videoPlayerOptions; } /// Represents an audio track in a video with its metadata. diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 1742dec5a53..c8d03f31704 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/video_player/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 6.7.0 +version: 6.8.0 environment: sdk: ^3.10.0 diff --git a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart index 6af3e57f6c0..089ae34acc4 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart @@ -7,11 +7,19 @@ import 'package:video_player_platform_interface/video_player_platform_interface. void main() { test('VideoPlayerOptions allowBackgroundPlayback defaults to false', () { - final options = VideoPlayerOptions(); + const options = VideoPlayerOptions(); expect(options.allowBackgroundPlayback, false); }); test('VideoPlayerOptions mixWithOthers defaults to false', () { - final options = VideoPlayerOptions(); + const options = VideoPlayerOptions(); expect(options.mixWithOthers, false); }); + test('VideoPlayerOptions backBufferDurationMs defaults to null', () { + const options = VideoPlayerOptions(); + expect(options.backBufferDurationMs, null); + }); + test('VideoPlayerOptions backBufferDurationMs stores configured value', () { + const options = VideoPlayerOptions(backBufferDurationMs: 20000); + expect(options.backBufferDurationMs, 20000); + }); } From de56e20f506a6259624daa8371d857541117be4f Mon Sep 17 00:00:00 2001 From: Sailendra Bathi Date: Wed, 10 Jun 2026 17:07:50 +0000 Subject: [PATCH 2/3] Address PR review comments and format files --- packages/video_player/video_player/CHANGELOG.md | 5 +---- packages/video_player/video_player_android/CHANGELOG.md | 8 ++++---- .../video_player_platform_interface/CHANGELOG.md | 5 +---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index eb02504e15a..20671d46a4c 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,10 +1,7 @@ -## NEXT - -* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. - ## 2.12.0 * Passes `backBufferDurationMs` from `VideoPlayerOptions` to the underlying platform interface. +* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.11.1 diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 699849d31fd..2937028a889 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,12 +1,12 @@ +## 2.10.0 + +* Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration. + ## 2.9.6 * Migrates to Built-in Kotlin to support AGP 9. * Updates minimum supported SDK version to Flutter 3.44/Dart 3.12. -## 2.10.0 - -* Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration. - ## 2.9.5 * Updates build files from Groovy to Kotlin. diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 5f125ab1efb..fddb2825741 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,10 +1,7 @@ -## NEXT - -* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. - ## 6.8.0 * Adds `backBufferDurationMs` to `VideoPlayerOptions` to support configuring ExoPlayer back buffer duration on Android. +* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 6.7.0 From 5a891dac6e60ba864c468f8b4a519f138adbaff1 Mon Sep 17 00:00:00 2001 From: Sailendra Bathi Date: Wed, 10 Jun 2026 18:10:57 +0000 Subject: [PATCH 3/3] Revert VideoPlayerOptions const constructor to match main and satisfy linter --- packages/video_player/video_player/example/lib/main.dart | 2 +- .../test/video_player_initialization_test.dart | 6 ++---- .../test/android_video_player_test.dart | 4 ++-- .../lib/video_player_platform_interface.dart | 2 +- .../test/video_player_options_test.dart | 8 ++++---- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index b018e92e5cf..61e24a89107 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -296,7 +296,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { _controller = VideoPlayerController.networkUrl( Uri.parse('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), closedCaptionFile: _loadCaptions(), - videoPlayerOptions: const VideoPlayerOptions(mixWithOthers: true), + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), viewType: widget.viewType, ); diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 40a1e748fb8..470eb1f9819 100644 --- a/packages/video_player/video_player/test/video_player_initialization_test.dart +++ b/packages/video_player/video_player/test/video_player_initialization_test.dart @@ -33,7 +33,7 @@ void main() { final controller = VideoPlayerController.networkUrl( Uri.parse('https://127.0.0.1'), - videoPlayerOptions: const VideoPlayerOptions(webOptions: expected), + videoPlayerOptions: VideoPlayerOptions(webOptions: expected), ); await controller.initialize(); @@ -79,9 +79,7 @@ void main() { final controller = VideoPlayerController.networkUrl( Uri.parse('https://127.0.0.1'), - videoPlayerOptions: const VideoPlayerOptions( - backBufferDurationMs: expectedBackBufferDurationMs, - ), + videoPlayerOptions: VideoPlayerOptions(backBufferDurationMs: expectedBackBufferDurationMs), ); await controller.initialize(); 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 9200d834b80..78868c2f1df 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 @@ -429,7 +429,7 @@ void main() { VideoCreationOptions( dataSource: DataSource(sourceType: DataSourceType.network, uri: 'https://example.com'), viewType: VideoViewType.textureView, - videoPlayerOptions: const VideoPlayerOptions(backBufferDurationMs: 20000), + videoPlayerOptions: VideoPlayerOptions(backBufferDurationMs: 20000), ), ); @@ -448,7 +448,7 @@ void main() { VideoCreationOptions( dataSource: DataSource(sourceType: DataSourceType.network, uri: 'https://example.com'), viewType: VideoViewType.platformView, - videoPlayerOptions: const VideoPlayerOptions(backBufferDurationMs: 20000), + videoPlayerOptions: VideoPlayerOptions(backBufferDurationMs: 20000), ), ); diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index aaea70ba531..d318c81ed65 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -458,7 +458,7 @@ class VideoPlayerOptions { // in all of the other video player packages, fix this, and then update // the other packages to use const. // ignore: prefer_const_constructors_in_immutables - const VideoPlayerOptions({ + VideoPlayerOptions({ this.mixWithOthers = false, this.allowBackgroundPlayback = false, this.webOptions, diff --git a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart index 089ae34acc4..dfd0ffdac6a 100644 --- a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart +++ b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart @@ -7,19 +7,19 @@ import 'package:video_player_platform_interface/video_player_platform_interface. void main() { test('VideoPlayerOptions allowBackgroundPlayback defaults to false', () { - const options = VideoPlayerOptions(); + final options = VideoPlayerOptions(); expect(options.allowBackgroundPlayback, false); }); test('VideoPlayerOptions mixWithOthers defaults to false', () { - const options = VideoPlayerOptions(); + final options = VideoPlayerOptions(); expect(options.mixWithOthers, false); }); test('VideoPlayerOptions backBufferDurationMs defaults to null', () { - const options = VideoPlayerOptions(); + final options = VideoPlayerOptions(); expect(options.backBufferDurationMs, null); }); test('VideoPlayerOptions backBufferDurationMs stores configured value', () { - const options = VideoPlayerOptions(backBufferDurationMs: 20000); + final options = VideoPlayerOptions(backBufferDurationMs: 20000); expect(options.backBufferDurationMs, 20000); }); }