Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## NEXT
## 2.12.0

* Adds `VideoPlayerAndroidOptions.enableDecoderFallback` to allow Android's
ExoPlayer implementation to fall back to another decoder if decoder
initialization fails.
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.

## 2.11.1
Expand Down
2 changes: 2 additions & 0 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export 'package:video_player_platform_interface/video_player_platform_interface.
DataSourceType,
DurationRange,
VideoFormat,
VideoPlayerAndroidOptions,
VideoPlayerOptions,
VideoPlayerWebOptions,
VideoPlayerWebOptionsControls,
Expand Down Expand Up @@ -560,6 +561,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
final creationOptions = platform_interface.VideoCreationOptions(
dataSource: dataSourceDescription,
viewType: viewType,
androidOptions: videoPlayerOptions?.androidOptions,
);

if (videoPlayerOptions?.mixWithOthers != null) {
Expand Down
6 changes: 3 additions & 3 deletions packages/video_player/video_player/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,20 @@ void main() {
reason: 'view type must be passed to the platform',
);
});

test('Android options are applied', () async {
const expected = VideoPlayerAndroidOptions(enableDecoderFallback: true);

final controller = VideoPlayerController.networkUrl(
Uri.parse('https://127.0.0.1'),
videoPlayerOptions: VideoPlayerOptions(androidOptions: expected),
);
await controller.initialize();

expect(
fakeVideoPlayerPlatform.androidOptions[controller.playerId],
expected,
reason: 'Android options must be passed to the platform',
);
});
}
14 changes: 7 additions & 7 deletions packages/video_player/video_player/test/video_player_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1027,13 +1027,11 @@ void main() {
}

expect(isSorted, false, reason: 'Expected captions to be unsorted');
expect(captions.map((Caption c) => c.text).toList(), <String>[
'one',
'two',
'three',
'five',
'four',
], reason: 'Captions should be in original unsorted order');
expect(
captions.map((Caption c) => c.text).toList(),
<String>['one', 'two', 'three', 'five', 'four'],
reason: 'Captions should be in original unsorted order',
);
});

test('works when seeking, includes all captions', () async {
Expand Down Expand Up @@ -1899,6 +1897,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
List<String> calls = <String>[];
List<DataSource> dataSources = <DataSource>[];
List<VideoViewType> viewTypes = <VideoViewType>[];
List<VideoPlayerAndroidOptions?> androidOptions = <VideoPlayerAndroidOptions?>[];
final Map<int, StreamController<VideoEvent>> streams = <int, StreamController<VideoEvent>>{};
bool forceInitError = false;
int nextPlayerId = 0;
Expand Down Expand Up @@ -1943,6 +1942,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform {
}
dataSources.add(options.dataSource);
viewTypes.add(options.viewType);
androidOptions.add(options.androidOptions);
return nextPlayerId++;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.10.0

* Adds support for enabling ExoPlayer decoder fallback.

## 2.9.6

* Migrates to Built-in Kotlin to support AGP 9.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

public class VideoPlayerOptions {
public boolean mixWithOthers;
public boolean enableDecoderFallback;
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public long createForPlatformView(@NonNull CreationOptions options) {
flutterState.applicationContext,
VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance),
videoAsset,
sharedOptions);
optionsFromCreationOptions(options));

registerPlayerInstance(videoPlayer, id);
return id;
Expand All @@ -112,7 +112,7 @@ public long createForPlatformView(@NonNull CreationOptions options) {
VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance),
handle,
videoAsset,
sharedOptions);
optionsFromCreationOptions(options));

registerPlayerInstance(videoPlayer, id);
return new TexturePlayerIds(id, handle.id());
Expand Down Expand Up @@ -145,6 +145,13 @@ public long createForPlatformView(@NonNull CreationOptions options) {
}
}

private @NonNull VideoPlayerOptions optionsFromCreationOptions(@NonNull CreationOptions options) {
VideoPlayerOptions videoPlayerOptions = new VideoPlayerOptions();
videoPlayerOptions.mixWithOthers = sharedOptions.mixWithOthers;
videoPlayerOptions.enableDecoderFallback = options.getEnableDecoderFallback();
return videoPlayerOptions;
}

private void registerPlayerInstance(VideoPlayer player, long id) {
// Set up the instance-specific API handler, and make sure it is removed when the player is
// disposed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
import io.flutter.plugins.videoplayer.VideoAsset;
Expand Down Expand Up @@ -55,15 +56,26 @@ public static PlatformViewVideoPlayer create(
events,
asset.getMediaItem(),
options,
() -> {
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));
return builder.build();
});
() -> createExoPlayer(context, asset, options, new DefaultRenderersFactory(context)));
}

// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
@UnstableApi
@VisibleForTesting
@NonNull
public static ExoPlayer createExoPlayer(
@NonNull Context context,
@NonNull VideoAsset asset,
@NonNull VideoPlayerOptions options,
@NonNull DefaultRenderersFactory renderersFactory) {
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
renderersFactory.setEnableDecoderFallback(options.enableDecoderFallback);
ExoPlayer.Builder builder =
new ExoPlayer.Builder(context, renderersFactory)
.setTrackSelector(trackSelector)
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
return builder.build();
}
Comment on lines +66 to 79

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The createExoPlayer method is identical to the one implemented in TextureVideoPlayer.java. To avoid code duplication and improve maintainability, consider extracting this method into a shared utility class (e.g., ExoPlayerUtils) within the io.flutter.plugins.videoplayer package.


@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
import io.flutter.plugins.videoplayer.VideoAsset;
Expand Down Expand Up @@ -55,15 +56,26 @@ public static TextureVideoPlayer create(
surfaceProducer,
asset.getMediaItem(),
options,
() -> {
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));
return builder.build();
});
() -> createExoPlayer(context, asset, options, new DefaultRenderersFactory(context)));
}

// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
@UnstableApi
@VisibleForTesting
@NonNull
public static ExoPlayer createExoPlayer(
@NonNull Context context,
@NonNull VideoAsset asset,
@NonNull VideoPlayerOptions options,
@NonNull DefaultRenderersFactory renderersFactory) {
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
renderersFactory.setEnableDecoderFallback(options.enableDecoderFallback);
ExoPlayer.Builder builder =
new ExoPlayer.Builder(context, renderersFactory)
.setTrackSelector(trackSelector)
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
return builder.build();
}
Comment on lines +66 to 79

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The createExoPlayer method is identical to the one implemented in PlatformViewVideoPlayer.java. To avoid code duplication and improve maintainability, consider extracting this method into a shared utility class (e.g., ExoPlayerUtils) within the io.flutter.plugins.videoplayer package.


// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,17 @@ data class CreationOptions(
val uri: String,
val formatHint: PlatformVideoFormat? = null,
val httpHeaders: Map<String, String>,
val userAgent: String? = null
val userAgent: String? = null,
val enableDecoderFallback: Boolean
) {
companion object {
fun fromList(pigeonVar_list: List<Any?>): CreationOptions {
val uri = pigeonVar_list[0] as String
val formatHint = pigeonVar_list[1] as PlatformVideoFormat?
val httpHeaders = pigeonVar_list[2] as Map<String, String>
val userAgent = pigeonVar_list[3] as String?
return CreationOptions(uri, formatHint, httpHeaders, userAgent)
val enableDecoderFallback = pigeonVar_list[4] as Boolean
return CreationOptions(uri, formatHint, httpHeaders, userAgent, enableDecoderFallback)
}
}

Expand All @@ -318,6 +320,7 @@ data class CreationOptions(
formatHint,
httpHeaders,
userAgent,
enableDecoderFallback,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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.assertTrue;

import android.content.Context;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.test.core.app.ApplicationProvider;
import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer;
import java.lang.reflect.Field;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

/** Unit tests for {@link PlatformViewVideoPlayer}. */
@RunWith(RobolectricTestRunner.class)
public final class PlatformViewVideoPlayerTest {
private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4";
private FakeVideoAsset fakeVideoAsset;

@Before
public void setUp() {
fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
}

private boolean getEnableDecoderFallback(DefaultRenderersFactory renderersFactory)
throws Exception {
final Field field = DefaultRenderersFactory.class.getDeclaredField("enableDecoderFallback");
field.setAccessible(true);
return field.getBoolean(renderersFactory);
}

@OptIn(markerClass = UnstableApi.class)
@Test
public void createExoPlayerEnablesDecoderFallbackWhenSet() throws Exception {
final Context context = ApplicationProvider.getApplicationContext();
final DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(context);
final VideoPlayerOptions options = new VideoPlayerOptions();
options.enableDecoderFallback = true;

final ExoPlayer exoPlayer =
PlatformViewVideoPlayer.createExoPlayer(context, fakeVideoAsset, options, renderersFactory);

assertTrue(getEnableDecoderFallback(renderersFactory));

exoPlayer.release();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@
package io.flutter.plugins.videoplayer;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import android.content.Context;
import android.view.Surface;
import androidx.annotation.OptIn;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.test.core.app.ApplicationProvider;
import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
import io.flutter.view.TextureRegistry;
import java.lang.reflect.Field;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand Down Expand Up @@ -67,6 +74,29 @@ private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) {
mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer);
}

private boolean getEnableDecoderFallback(DefaultRenderersFactory renderersFactory)
throws Exception {
final Field field = DefaultRenderersFactory.class.getDeclaredField("enableDecoderFallback");
field.setAccessible(true);
return field.getBoolean(renderersFactory);
}

@OptIn(markerClass = UnstableApi.class)
@Test
public void createExoPlayerEnablesDecoderFallbackWhenSet() throws Exception {
final Context context = ApplicationProvider.getApplicationContext();
final DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(context);
final VideoPlayerOptions options = new VideoPlayerOptions();
options.enableDecoderFallback = true;

final ExoPlayer exoPlayer =
TextureVideoPlayer.createExoPlayer(context, fakeVideoAsset, options, renderersFactory);

assertTrue(getEnableDecoderFallback(renderersFactory));

exoPlayer.release();
}

@Test
public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {
VideoPlayer videoPlayer = createVideoPlayer();
Expand Down
Loading