Commit d2b3433a2d12f8f35595f035e4db392b324c1cdd

Authored by 吴启风
1 parent 3e35ddae

fix: 修复部分安卓设备视频持续加载

问题背景:部分希沃设备播放 1080P H.264 视频时,联发科硬件解码器 OMX.MTK.VIDEO.DECODER.AVC 初始化失败。原播放器未尝试备用解码器,页面因此持续停留在加载状态。

主要调整:
- Android 恢复使用 TextureView 渲染路径,避免 PlatformView 在部分设备上的兼容问题
- 捕获播放器初始化及异步播放错误,输出视频 ID 和错误信息,并及时结束 Loading 状态
- 基于官方 video_player_android 2.8.4 引入项目内可追踪实现
- 为 TextureView 和 PlatformView 两条 ExoPlayer 创建路径启用 Media3 setEnableDecoderFallback(true)
- 主硬件解码器初始化失败时,自动尝试设备提供的后备/软件解码器

验证结果:Flutter 3.27.4 release APK 构建及签名校验通过;希沃 W20 上联发科硬解失败后可正常降级播放。

平台影响:仅修改 Android 播放实现;iOS 继续使用 video_player_avfoundation,播放链路与行为不变。
Showing 42 changed files with 4560 additions and 8 deletions
lib/pages/video/zone/video_zone_player_page.dart
... ... @@ -65,9 +65,7 @@ class _VideoZonePlayerPageState extends State<VideoZonePlayerPage>
65 65 _lastIsPlaying = null;
66 66 final controller = VideoPlayerController.networkUrl(
67 67 Uri.parse(_currentItem.videoUrl),
68   - viewType: defaultTargetPlatform == TargetPlatform.android
69   - ? VideoViewType.platformView
70   - : VideoViewType.textureView,
  68 + viewType: VideoViewType.textureView,
71 69 );
72 70 _controller = controller;
73 71 _chewieController = null;
... ... @@ -144,6 +142,7 @@ class _VideoZonePlayerPageState extends State<VideoZonePlayerPage>
144 142 setState(() {});
145 143 } catch (error) {
146 144 if (mounted && controller == _controller) {
  145 + debugPrint('视频初始化失败(videoId=${_currentItem.id}): $error');
147 146 setState(() => _loadError = error);
148 147 }
149 148 }
... ... @@ -152,6 +151,12 @@ class _VideoZonePlayerPageState extends State<VideoZonePlayerPage>
152 151 void _onVideoChanged() {
153 152 final controller = _controller;
154 153 if (!mounted || controller == null) return;
  154 + if (controller.value.hasError && _loadError == null) {
  155 + final error = controller.value.errorDescription ?? '未知播放器错误';
  156 + debugPrint('视频播放失败(videoId=${_currentItem.id}): $error');
  157 + setState(() => _loadError = error);
  158 + return;
  159 + }
155 160 final isPlaying = controller.value.isPlaying;
156 161 if (_lastIsPlaying != null && _lastIsPlaying != isPlaying) {
157 162 unawaited(_cacheCurrentProgress());
... ...
packages/video_player_android/LICENSE 0 → 100644
  1 +Copyright 2013 The Flutter Authors. All rights reserved.
  2 +
  3 +Redistribution and use in source and binary forms, with or without modification,
  4 +are permitted provided that the following conditions are met:
  5 +
  6 + * Redistributions of source code must retain the above copyright
  7 + notice, this list of conditions and the following disclaimer.
  8 + * Redistributions in binary form must reproduce the above
  9 + copyright notice, this list of conditions and the following
  10 + disclaimer in the documentation and/or other materials provided
  11 + with the distribution.
  12 + * Neither the name of Google Inc. nor the names of its
  13 + contributors may be used to endorse or promote products derived
  14 + from this software without specific prior written permission.
  15 +
  16 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  20 +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21 +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22 +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  23 +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24 +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25 +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
... ...
packages/video_player_android/android/build.gradle 0 → 100644
  1 +group 'io.flutter.plugins.videoplayer'
  2 +version '1.0-SNAPSHOT'
  3 +
  4 +buildscript {
  5 + repositories {
  6 + google()
  7 + mavenCentral()
  8 + }
  9 +
  10 + dependencies {
  11 + classpath 'com.android.tools.build:gradle:8.5.0'
  12 + }
  13 +}
  14 +
  15 +rootProject.allprojects {
  16 + repositories {
  17 + google()
  18 + mavenCentral()
  19 + }
  20 +}
  21 +
  22 +apply plugin: 'com.android.library'
  23 +
  24 +android {
  25 + namespace 'io.flutter.plugins.videoplayer'
  26 + compileSdk = flutter.compileSdkVersion
  27 +
  28 + defaultConfig {
  29 + minSdkVersion 21
  30 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  31 + }
  32 + lintOptions {
  33 + checkAllWarnings true
  34 + warningsAsErrors true
  35 + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency', 'NewerVersionAvailable'
  36 + }
  37 + compileOptions {
  38 + sourceCompatibility JavaVersion.VERSION_11
  39 + targetCompatibility JavaVersion.VERSION_11
  40 + }
  41 +
  42 + dependencies {
  43 + def exoplayer_version = "1.4.1"
  44 + implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
  45 + implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}"
  46 + implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}"
  47 + implementation "androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}"
  48 + implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}"
  49 + testImplementation 'junit:junit:4.13.2'
  50 + testImplementation 'androidx.test:core:1.6.1'
  51 + testImplementation 'org.mockito:mockito-inline:5.2.0'
  52 + testImplementation 'org.robolectric:robolectric:4.14.1'
  53 + testImplementation "androidx.media3:media3-test-utils:${exoplayer_version}"
  54 + }
  55 +
  56 + testOptions {
  57 + unitTests.includeAndroidResources = true
  58 + unitTests.returnDefaultValues = true
  59 + unitTests.all {
  60 + // The org.gradle.jvmargs property that may be set in gradle.properties does not impact
  61 + // the Java heap size when running the Android unit tests. The following property here
  62 + // sets the heap size to a size large enough to run the robolectric tests across
  63 + // multiple SDK levels.
  64 + jvmArgs "-Xmx4G"
  65 + testLogging {
  66 + events "passed", "skipped", "failed", "standardOut", "standardError"
  67 + outputs.upToDateWhen {false}
  68 + showStandardStreams = true
  69 + }
  70 + }
  71 + }
  72 +}
... ...
packages/video_player_android/android/settings.gradle 0 → 100644
  1 +rootProject.name = 'video_player_android'
... ...
packages/video_player_android/android/src/main/AndroidManifest.xml 0 → 100644
  1 +<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  2 + package="io.flutter.plugins.videoplayer">
  3 +</manifest>
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.media3.common.PlaybackException;
  9 +import androidx.media3.common.Player;
  10 +import androidx.media3.exoplayer.ExoPlayer;
  11 +
  12 +public abstract class ExoPlayerEventListener implements Player.Listener {
  13 + private boolean isBuffering = false;
  14 + private boolean isInitialized;
  15 + protected final ExoPlayer exoPlayer;
  16 + protected final VideoPlayerCallbacks events;
  17 +
  18 + protected enum RotationDegrees {
  19 + ROTATE_0(0),
  20 + ROTATE_90(90),
  21 + ROTATE_180(180),
  22 + ROTATE_270(270);
  23 +
  24 + private final int degrees;
  25 +
  26 + RotationDegrees(int degrees) {
  27 + this.degrees = degrees;
  28 + }
  29 +
  30 + public static RotationDegrees fromDegrees(int degrees) {
  31 + for (RotationDegrees rotationDegrees : RotationDegrees.values()) {
  32 + if (rotationDegrees.degrees == degrees) {
  33 + return rotationDegrees;
  34 + }
  35 + }
  36 + throw new IllegalArgumentException("Invalid rotation degrees specified: " + degrees);
  37 + }
  38 +
  39 + public int getDegrees() {
  40 + return this.degrees;
  41 + }
  42 + }
  43 +
  44 + public ExoPlayerEventListener(
  45 + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
  46 + this.exoPlayer = exoPlayer;
  47 + this.events = events;
  48 + this.isInitialized = initialized;
  49 + }
  50 +
  51 + private void setBuffering(boolean buffering) {
  52 + if (isBuffering == buffering) {
  53 + return;
  54 + }
  55 + isBuffering = buffering;
  56 + if (buffering) {
  57 + events.onBufferingStart();
  58 + } else {
  59 + events.onBufferingEnd();
  60 + }
  61 + }
  62 +
  63 + protected abstract void sendInitialized();
  64 +
  65 + @Override
  66 + public void onPlaybackStateChanged(final int playbackState) {
  67 + switch (playbackState) {
  68 + case Player.STATE_BUFFERING:
  69 + setBuffering(true);
  70 + events.onBufferingUpdate(exoPlayer.getBufferedPosition());
  71 + break;
  72 + case Player.STATE_READY:
  73 + if (isInitialized) {
  74 + return;
  75 + }
  76 + isInitialized = true;
  77 + sendInitialized();
  78 + break;
  79 + case Player.STATE_ENDED:
  80 + events.onCompleted();
  81 + break;
  82 + case Player.STATE_IDLE:
  83 + break;
  84 + }
  85 + if (playbackState != Player.STATE_BUFFERING) {
  86 + setBuffering(false);
  87 + }
  88 + }
  89 +
  90 + @Override
  91 + public void onPlayerError(@NonNull final PlaybackException error) {
  92 + setBuffering(false);
  93 + if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
  94 + // See
  95 + // https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
  96 + exoPlayer.seekToDefaultPosition();
  97 + exoPlayer.prepare();
  98 + } else {
  99 + events.onError("VideoError", "Video player had error " + error, null);
  100 + }
  101 + }
  102 +
  103 + @Override
  104 + public void onIsPlayingChanged(boolean isPlaying) {
  105 + events.onIsPlayingStateUpdate(isPlaying);
  106 + }
  107 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.media3.common.PlaybackParameters;
  9 +import androidx.media3.exoplayer.ExoPlayer;
  10 +
  11 +/**
  12 + * Internal state representing an {@link ExoPlayer} instance at a snapshot in time.
  13 + *
  14 + * <p>During the Android application lifecycle, the underlying {@link android.view.Surface} being
  15 + * rendered to by the player can be destroyed when the application is in the background and memory
  16 + * is reclaimed. Upon <em>resume</em>, the player will need to be recreated, but start again at the
  17 + * previous point (and settings).
  18 + */
  19 +public final class ExoPlayerState {
  20 + /**
  21 + * Saves a representation of the current state of the player at the current point in time.
  22 + *
  23 + * <p>The inverse of this operation is {@link #restore(ExoPlayer)}.
  24 + *
  25 + * @param exoPlayer the active player instance.
  26 + * @return an opaque object representing the state.
  27 + */
  28 + @NonNull
  29 + public static ExoPlayerState save(@NonNull ExoPlayer exoPlayer) {
  30 + return new ExoPlayerState(
  31 + /* position= */ exoPlayer.getCurrentPosition(),
  32 + /* repeatMode= */ exoPlayer.getRepeatMode(),
  33 + /* volume= */ exoPlayer.getVolume(),
  34 + /* playbackParameters= */ exoPlayer.getPlaybackParameters());
  35 + }
  36 +
  37 + private ExoPlayerState(
  38 + long position, int repeatMode, float volume, PlaybackParameters playbackParameters) {
  39 + this.position = position;
  40 + this.repeatMode = repeatMode;
  41 + this.volume = volume;
  42 + this.playbackParameters = playbackParameters;
  43 + }
  44 +
  45 + /** Previous value of {@link ExoPlayer#getCurrentPosition()}. */
  46 + private final long position;
  47 +
  48 + /** Previous value of {@link ExoPlayer#getRepeatMode()}. */
  49 + private final int repeatMode;
  50 +
  51 + /** Previous value of {@link ExoPlayer#getVolume()}. */
  52 + private final float volume;
  53 +
  54 + /** Previous value of {@link ExoPlayer#getPlaybackParameters()}. */
  55 + private final PlaybackParameters playbackParameters;
  56 +
  57 + /**
  58 + * Restores the captured state onto the provided player.
  59 + *
  60 + * <p>This will typically be done after creating a new player, setting up a media source, and
  61 + * listening to events.
  62 + *
  63 + * @param exoPlayer the new player instance to reflect the state back to.
  64 + */
  65 + public void restore(@NonNull ExoPlayer exoPlayer) {
  66 + exoPlayer.seekTo(position);
  67 + exoPlayer.setRepeatMode(repeatMode);
  68 + exoPlayer.setVolume(volume);
  69 + exoPlayer.setPlaybackParameters(playbackParameters);
  70 + }
  71 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.annotation.OptIn;
  11 +import androidx.annotation.VisibleForTesting;
  12 +import androidx.media3.common.MediaItem;
  13 +import androidx.media3.common.MimeTypes;
  14 +import androidx.media3.common.util.UnstableApi;
  15 +import androidx.media3.datasource.DataSource;
  16 +import androidx.media3.datasource.DefaultDataSource;
  17 +import androidx.media3.datasource.DefaultHttpDataSource;
  18 +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
  19 +import androidx.media3.exoplayer.source.MediaSource;
  20 +import java.util.Map;
  21 +
  22 +final class HttpVideoAsset extends VideoAsset {
  23 + private static final String DEFAULT_USER_AGENT = "ExoPlayer";
  24 + private static final String HEADER_USER_AGENT = "User-Agent";
  25 +
  26 + @NonNull private final StreamingFormat streamingFormat;
  27 + @NonNull private final Map<String, String> httpHeaders;
  28 +
  29 + HttpVideoAsset(
  30 + @Nullable String assetUrl,
  31 + @NonNull StreamingFormat streamingFormat,
  32 + @NonNull Map<String, String> httpHeaders) {
  33 + super(assetUrl);
  34 + this.streamingFormat = streamingFormat;
  35 + this.httpHeaders = httpHeaders;
  36 + }
  37 +
  38 + @NonNull
  39 + @Override
  40 + public MediaItem getMediaItem() {
  41 + MediaItem.Builder builder = new MediaItem.Builder().setUri(assetUrl);
  42 + String mimeType = null;
  43 + switch (streamingFormat) {
  44 + case SMOOTH:
  45 + mimeType = MimeTypes.APPLICATION_SS;
  46 + break;
  47 + case DYNAMIC_ADAPTIVE:
  48 + mimeType = MimeTypes.APPLICATION_MPD;
  49 + break;
  50 + case HTTP_LIVE:
  51 + mimeType = MimeTypes.APPLICATION_M3U8;
  52 + break;
  53 + }
  54 + if (mimeType != null) {
  55 + builder.setMimeType(mimeType);
  56 + }
  57 + return builder.build();
  58 + }
  59 +
  60 + @NonNull
  61 + @Override
  62 + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
  63 + return getMediaSourceFactory(context, new DefaultHttpDataSource.Factory());
  64 + }
  65 +
  66 + /**
  67 + * Returns a configured media source factory, starting at the provided factory.
  68 + *
  69 + * <p>This method is provided for ease of testing without making real HTTP calls.
  70 + *
  71 + * @param context application context.
  72 + * @param initialFactory initial factory, to be configured.
  73 + * @return configured factory, or {@code null} if not needed for this asset type.
  74 + */
  75 + @VisibleForTesting
  76 + MediaSource.Factory getMediaSourceFactory(
  77 + Context context, DefaultHttpDataSource.Factory initialFactory) {
  78 + String userAgent = DEFAULT_USER_AGENT;
  79 + if (!httpHeaders.isEmpty() && httpHeaders.containsKey(HEADER_USER_AGENT)) {
  80 + userAgent = httpHeaders.get(HEADER_USER_AGENT);
  81 + }
  82 + unstableUpdateDataSourceFactory(initialFactory, httpHeaders, userAgent);
  83 + DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, initialFactory);
  84 + return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory);
  85 + }
  86 +
  87 + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
  88 + @OptIn(markerClass = UnstableApi.class)
  89 + private static void unstableUpdateDataSourceFactory(
  90 + @NonNull DefaultHttpDataSource.Factory factory,
  91 + @NonNull Map<String, String> httpHeaders,
  92 + @Nullable String userAgent) {
  93 + factory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
  94 + if (!httpHeaders.isEmpty()) {
  95 + factory.setDefaultRequestProperties(httpHeaders);
  96 + }
  97 + }
  98 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.media3.common.MediaItem;
  10 +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
  11 +import androidx.media3.exoplayer.source.MediaSource;
  12 +
  13 +final class LocalVideoAsset extends VideoAsset {
  14 + LocalVideoAsset(@NonNull String assetUrl) {
  15 + super(assetUrl);
  16 + }
  17 +
  18 + @NonNull
  19 + @Override
  20 + public MediaItem getMediaItem() {
  21 + return new MediaItem.Builder().setUri(assetUrl).build();
  22 + }
  23 +
  24 + @NonNull
  25 + @Override
  26 + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
  27 + return new DefaultMediaSourceFactory(context);
  28 + }
  29 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +// Autogenerated from Pigeon (v22.6.1), do not edit directly.
  5 +// See also: https://pub.dev/packages/pigeon
  6 +
  7 +package io.flutter.plugins.videoplayer;
  8 +
  9 +import static java.lang.annotation.ElementType.METHOD;
  10 +import static java.lang.annotation.RetentionPolicy.CLASS;
  11 +
  12 +import android.util.Log;
  13 +import androidx.annotation.NonNull;
  14 +import androidx.annotation.Nullable;
  15 +import io.flutter.plugin.common.BasicMessageChannel;
  16 +import io.flutter.plugin.common.BinaryMessenger;
  17 +import io.flutter.plugin.common.MessageCodec;
  18 +import io.flutter.plugin.common.StandardMessageCodec;
  19 +import java.io.ByteArrayOutputStream;
  20 +import java.lang.annotation.Retention;
  21 +import java.lang.annotation.Target;
  22 +import java.nio.ByteBuffer;
  23 +import java.util.ArrayList;
  24 +import java.util.Map;
  25 +import java.util.Objects;
  26 +
  27 +/** Generated class from Pigeon. */
  28 +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
  29 +public class Messages {
  30 +
  31 + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */
  32 + public static class FlutterError extends RuntimeException {
  33 +
  34 + /** The error code. */
  35 + public final String code;
  36 +
  37 + /** The error details. Must be a datatype supported by the api codec. */
  38 + public final Object details;
  39 +
  40 + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) {
  41 + super(message);
  42 + this.code = code;
  43 + this.details = details;
  44 + }
  45 + }
  46 +
  47 + @NonNull
  48 + protected static ArrayList<Object> wrapError(@NonNull Throwable exception) {
  49 + ArrayList<Object> errorList = new ArrayList<>(3);
  50 + if (exception instanceof FlutterError) {
  51 + FlutterError error = (FlutterError) exception;
  52 + errorList.add(error.code);
  53 + errorList.add(error.getMessage());
  54 + errorList.add(error.details);
  55 + } else {
  56 + errorList.add(exception.toString());
  57 + errorList.add(exception.getClass().getSimpleName());
  58 + errorList.add(
  59 + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
  60 + }
  61 + return errorList;
  62 + }
  63 +
  64 + @Target(METHOD)
  65 + @Retention(CLASS)
  66 + @interface CanIgnoreReturnValue {}
  67 +
  68 + /** Pigeon equivalent of VideoViewType. */
  69 + public enum PlatformVideoViewType {
  70 + TEXTURE_VIEW(0),
  71 + PLATFORM_VIEW(1);
  72 +
  73 + final int index;
  74 +
  75 + PlatformVideoViewType(final int index) {
  76 + this.index = index;
  77 + }
  78 + }
  79 +
  80 + /**
  81 + * Information passed to the platform view creation.
  82 + *
  83 + * <p>Generated class from Pigeon that represents data sent in messages.
  84 + */
  85 + public static final class PlatformVideoViewCreationParams {
  86 + private @NonNull Long playerId;
  87 +
  88 + public @NonNull Long getPlayerId() {
  89 + return playerId;
  90 + }
  91 +
  92 + public void setPlayerId(@NonNull Long setterArg) {
  93 + if (setterArg == null) {
  94 + throw new IllegalStateException("Nonnull field \"playerId\" is null.");
  95 + }
  96 + this.playerId = setterArg;
  97 + }
  98 +
  99 + /** Constructor is non-public to enforce null safety; use Builder. */
  100 + PlatformVideoViewCreationParams() {}
  101 +
  102 + @Override
  103 + public boolean equals(Object o) {
  104 + if (this == o) {
  105 + return true;
  106 + }
  107 + if (o == null || getClass() != o.getClass()) {
  108 + return false;
  109 + }
  110 + PlatformVideoViewCreationParams that = (PlatformVideoViewCreationParams) o;
  111 + return playerId.equals(that.playerId);
  112 + }
  113 +
  114 + @Override
  115 + public int hashCode() {
  116 + return Objects.hash(playerId);
  117 + }
  118 +
  119 + public static final class Builder {
  120 +
  121 + private @Nullable Long playerId;
  122 +
  123 + @CanIgnoreReturnValue
  124 + public @NonNull Builder setPlayerId(@NonNull Long setterArg) {
  125 + this.playerId = setterArg;
  126 + return this;
  127 + }
  128 +
  129 + public @NonNull PlatformVideoViewCreationParams build() {
  130 + PlatformVideoViewCreationParams pigeonReturn = new PlatformVideoViewCreationParams();
  131 + pigeonReturn.setPlayerId(playerId);
  132 + return pigeonReturn;
  133 + }
  134 + }
  135 +
  136 + @NonNull
  137 + ArrayList<Object> toList() {
  138 + ArrayList<Object> toListResult = new ArrayList<>(1);
  139 + toListResult.add(playerId);
  140 + return toListResult;
  141 + }
  142 +
  143 + static @NonNull PlatformVideoViewCreationParams fromList(
  144 + @NonNull ArrayList<Object> pigeonVar_list) {
  145 + PlatformVideoViewCreationParams pigeonResult = new PlatformVideoViewCreationParams();
  146 + Object playerId = pigeonVar_list.get(0);
  147 + pigeonResult.setPlayerId((Long) playerId);
  148 + return pigeonResult;
  149 + }
  150 + }
  151 +
  152 + /** Generated class from Pigeon that represents data sent in messages. */
  153 + public static final class CreateMessage {
  154 + private @Nullable String asset;
  155 +
  156 + public @Nullable String getAsset() {
  157 + return asset;
  158 + }
  159 +
  160 + public void setAsset(@Nullable String setterArg) {
  161 + this.asset = setterArg;
  162 + }
  163 +
  164 + private @Nullable String uri;
  165 +
  166 + public @Nullable String getUri() {
  167 + return uri;
  168 + }
  169 +
  170 + public void setUri(@Nullable String setterArg) {
  171 + this.uri = setterArg;
  172 + }
  173 +
  174 + private @Nullable String packageName;
  175 +
  176 + public @Nullable String getPackageName() {
  177 + return packageName;
  178 + }
  179 +
  180 + public void setPackageName(@Nullable String setterArg) {
  181 + this.packageName = setterArg;
  182 + }
  183 +
  184 + private @Nullable String formatHint;
  185 +
  186 + public @Nullable String getFormatHint() {
  187 + return formatHint;
  188 + }
  189 +
  190 + public void setFormatHint(@Nullable String setterArg) {
  191 + this.formatHint = setterArg;
  192 + }
  193 +
  194 + private @NonNull Map<String, String> httpHeaders;
  195 +
  196 + public @NonNull Map<String, String> getHttpHeaders() {
  197 + return httpHeaders;
  198 + }
  199 +
  200 + public void setHttpHeaders(@NonNull Map<String, String> setterArg) {
  201 + if (setterArg == null) {
  202 + throw new IllegalStateException("Nonnull field \"httpHeaders\" is null.");
  203 + }
  204 + this.httpHeaders = setterArg;
  205 + }
  206 +
  207 + private @Nullable PlatformVideoViewType viewType;
  208 +
  209 + public @Nullable PlatformVideoViewType getViewType() {
  210 + return viewType;
  211 + }
  212 +
  213 + public void setViewType(@Nullable PlatformVideoViewType setterArg) {
  214 + this.viewType = setterArg;
  215 + }
  216 +
  217 + /** Constructor is non-public to enforce null safety; use Builder. */
  218 + CreateMessage() {}
  219 +
  220 + @Override
  221 + public boolean equals(Object o) {
  222 + if (this == o) {
  223 + return true;
  224 + }
  225 + if (o == null || getClass() != o.getClass()) {
  226 + return false;
  227 + }
  228 + CreateMessage that = (CreateMessage) o;
  229 + return Objects.equals(asset, that.asset)
  230 + && Objects.equals(uri, that.uri)
  231 + && Objects.equals(packageName, that.packageName)
  232 + && Objects.equals(formatHint, that.formatHint)
  233 + && httpHeaders.equals(that.httpHeaders)
  234 + && Objects.equals(viewType, that.viewType);
  235 + }
  236 +
  237 + @Override
  238 + public int hashCode() {
  239 + return Objects.hash(asset, uri, packageName, formatHint, httpHeaders, viewType);
  240 + }
  241 +
  242 + public static final class Builder {
  243 +
  244 + private @Nullable String asset;
  245 +
  246 + @CanIgnoreReturnValue
  247 + public @NonNull Builder setAsset(@Nullable String setterArg) {
  248 + this.asset = setterArg;
  249 + return this;
  250 + }
  251 +
  252 + private @Nullable String uri;
  253 +
  254 + @CanIgnoreReturnValue
  255 + public @NonNull Builder setUri(@Nullable String setterArg) {
  256 + this.uri = setterArg;
  257 + return this;
  258 + }
  259 +
  260 + private @Nullable String packageName;
  261 +
  262 + @CanIgnoreReturnValue
  263 + public @NonNull Builder setPackageName(@Nullable String setterArg) {
  264 + this.packageName = setterArg;
  265 + return this;
  266 + }
  267 +
  268 + private @Nullable String formatHint;
  269 +
  270 + @CanIgnoreReturnValue
  271 + public @NonNull Builder setFormatHint(@Nullable String setterArg) {
  272 + this.formatHint = setterArg;
  273 + return this;
  274 + }
  275 +
  276 + private @Nullable Map<String, String> httpHeaders;
  277 +
  278 + @CanIgnoreReturnValue
  279 + public @NonNull Builder setHttpHeaders(@NonNull Map<String, String> setterArg) {
  280 + this.httpHeaders = setterArg;
  281 + return this;
  282 + }
  283 +
  284 + private @Nullable PlatformVideoViewType viewType;
  285 +
  286 + @CanIgnoreReturnValue
  287 + public @NonNull Builder setViewType(@Nullable PlatformVideoViewType setterArg) {
  288 + this.viewType = setterArg;
  289 + return this;
  290 + }
  291 +
  292 + public @NonNull CreateMessage build() {
  293 + CreateMessage pigeonReturn = new CreateMessage();
  294 + pigeonReturn.setAsset(asset);
  295 + pigeonReturn.setUri(uri);
  296 + pigeonReturn.setPackageName(packageName);
  297 + pigeonReturn.setFormatHint(formatHint);
  298 + pigeonReturn.setHttpHeaders(httpHeaders);
  299 + pigeonReturn.setViewType(viewType);
  300 + return pigeonReturn;
  301 + }
  302 + }
  303 +
  304 + @NonNull
  305 + ArrayList<Object> toList() {
  306 + ArrayList<Object> toListResult = new ArrayList<>(6);
  307 + toListResult.add(asset);
  308 + toListResult.add(uri);
  309 + toListResult.add(packageName);
  310 + toListResult.add(formatHint);
  311 + toListResult.add(httpHeaders);
  312 + toListResult.add(viewType);
  313 + return toListResult;
  314 + }
  315 +
  316 + static @NonNull CreateMessage fromList(@NonNull ArrayList<Object> pigeonVar_list) {
  317 + CreateMessage pigeonResult = new CreateMessage();
  318 + Object asset = pigeonVar_list.get(0);
  319 + pigeonResult.setAsset((String) asset);
  320 + Object uri = pigeonVar_list.get(1);
  321 + pigeonResult.setUri((String) uri);
  322 + Object packageName = pigeonVar_list.get(2);
  323 + pigeonResult.setPackageName((String) packageName);
  324 + Object formatHint = pigeonVar_list.get(3);
  325 + pigeonResult.setFormatHint((String) formatHint);
  326 + Object httpHeaders = pigeonVar_list.get(4);
  327 + pigeonResult.setHttpHeaders((Map<String, String>) httpHeaders);
  328 + Object viewType = pigeonVar_list.get(5);
  329 + pigeonResult.setViewType((PlatformVideoViewType) viewType);
  330 + return pigeonResult;
  331 + }
  332 + }
  333 +
  334 + private static class PigeonCodec extends StandardMessageCodec {
  335 + public static final PigeonCodec INSTANCE = new PigeonCodec();
  336 +
  337 + private PigeonCodec() {}
  338 +
  339 + @Override
  340 + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
  341 + switch (type) {
  342 + case (byte) 129:
  343 + {
  344 + Object value = readValue(buffer);
  345 + return value == null ? null : PlatformVideoViewType.values()[((Long) value).intValue()];
  346 + }
  347 + case (byte) 130:
  348 + return PlatformVideoViewCreationParams.fromList((ArrayList<Object>) readValue(buffer));
  349 + case (byte) 131:
  350 + return CreateMessage.fromList((ArrayList<Object>) readValue(buffer));
  351 + default:
  352 + return super.readValueOfType(type, buffer);
  353 + }
  354 + }
  355 +
  356 + @Override
  357 + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
  358 + if (value instanceof PlatformVideoViewType) {
  359 + stream.write(129);
  360 + writeValue(stream, value == null ? null : ((PlatformVideoViewType) value).index);
  361 + } else if (value instanceof PlatformVideoViewCreationParams) {
  362 + stream.write(130);
  363 + writeValue(stream, ((PlatformVideoViewCreationParams) value).toList());
  364 + } else if (value instanceof CreateMessage) {
  365 + stream.write(131);
  366 + writeValue(stream, ((CreateMessage) value).toList());
  367 + } else {
  368 + super.writeValue(stream, value);
  369 + }
  370 + }
  371 + }
  372 +
  373 + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
  374 + public interface AndroidVideoPlayerApi {
  375 +
  376 + void initialize();
  377 +
  378 + @NonNull
  379 + Long create(@NonNull CreateMessage msg);
  380 +
  381 + void dispose(@NonNull Long playerId);
  382 +
  383 + void setLooping(@NonNull Long playerId, @NonNull Boolean looping);
  384 +
  385 + void setVolume(@NonNull Long playerId, @NonNull Double volume);
  386 +
  387 + void setPlaybackSpeed(@NonNull Long playerId, @NonNull Double speed);
  388 +
  389 + void play(@NonNull Long playerId);
  390 +
  391 + @NonNull
  392 + Long position(@NonNull Long playerId);
  393 +
  394 + void seekTo(@NonNull Long playerId, @NonNull Long position);
  395 +
  396 + void pause(@NonNull Long playerId);
  397 +
  398 + void setMixWithOthers(@NonNull Boolean mixWithOthers);
  399 +
  400 + /** The codec used by AndroidVideoPlayerApi. */
  401 + static @NonNull MessageCodec<Object> getCodec() {
  402 + return PigeonCodec.INSTANCE;
  403 + }
  404 +
  405 + /**
  406 + * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the
  407 + * `binaryMessenger`.
  408 + */
  409 + static void setUp(
  410 + @NonNull BinaryMessenger binaryMessenger, @Nullable AndroidVideoPlayerApi api) {
  411 + setUp(binaryMessenger, "", api);
  412 + }
  413 +
  414 + static void setUp(
  415 + @NonNull BinaryMessenger binaryMessenger,
  416 + @NonNull String messageChannelSuffix,
  417 + @Nullable AndroidVideoPlayerApi api) {
  418 + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix;
  419 + {
  420 + BasicMessageChannel<Object> channel =
  421 + new BasicMessageChannel<>(
  422 + binaryMessenger,
  423 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize"
  424 + + messageChannelSuffix,
  425 + getCodec());
  426 + if (api != null) {
  427 + channel.setMessageHandler(
  428 + (message, reply) -> {
  429 + ArrayList<Object> wrapped = new ArrayList<>();
  430 + try {
  431 + api.initialize();
  432 + wrapped.add(0, null);
  433 + } catch (Throwable exception) {
  434 + wrapped = wrapError(exception);
  435 + }
  436 + reply.reply(wrapped);
  437 + });
  438 + } else {
  439 + channel.setMessageHandler(null);
  440 + }
  441 + }
  442 + {
  443 + BasicMessageChannel<Object> channel =
  444 + new BasicMessageChannel<>(
  445 + binaryMessenger,
  446 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.create"
  447 + + messageChannelSuffix,
  448 + getCodec());
  449 + if (api != null) {
  450 + channel.setMessageHandler(
  451 + (message, reply) -> {
  452 + ArrayList<Object> wrapped = new ArrayList<>();
  453 + ArrayList<Object> args = (ArrayList<Object>) message;
  454 + CreateMessage msgArg = (CreateMessage) args.get(0);
  455 + try {
  456 + Long output = api.create(msgArg);
  457 + wrapped.add(0, output);
  458 + } catch (Throwable exception) {
  459 + wrapped = wrapError(exception);
  460 + }
  461 + reply.reply(wrapped);
  462 + });
  463 + } else {
  464 + channel.setMessageHandler(null);
  465 + }
  466 + }
  467 + {
  468 + BasicMessageChannel<Object> channel =
  469 + new BasicMessageChannel<>(
  470 + binaryMessenger,
  471 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose"
  472 + + messageChannelSuffix,
  473 + getCodec());
  474 + if (api != null) {
  475 + channel.setMessageHandler(
  476 + (message, reply) -> {
  477 + ArrayList<Object> wrapped = new ArrayList<>();
  478 + ArrayList<Object> args = (ArrayList<Object>) message;
  479 + Long playerIdArg = (Long) args.get(0);
  480 + try {
  481 + api.dispose(playerIdArg);
  482 + wrapped.add(0, null);
  483 + } catch (Throwable exception) {
  484 + wrapped = wrapError(exception);
  485 + }
  486 + reply.reply(wrapped);
  487 + });
  488 + } else {
  489 + channel.setMessageHandler(null);
  490 + }
  491 + }
  492 + {
  493 + BasicMessageChannel<Object> channel =
  494 + new BasicMessageChannel<>(
  495 + binaryMessenger,
  496 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping"
  497 + + messageChannelSuffix,
  498 + getCodec());
  499 + if (api != null) {
  500 + channel.setMessageHandler(
  501 + (message, reply) -> {
  502 + ArrayList<Object> wrapped = new ArrayList<>();
  503 + ArrayList<Object> args = (ArrayList<Object>) message;
  504 + Long playerIdArg = (Long) args.get(0);
  505 + Boolean loopingArg = (Boolean) args.get(1);
  506 + try {
  507 + api.setLooping(playerIdArg, loopingArg);
  508 + wrapped.add(0, null);
  509 + } catch (Throwable exception) {
  510 + wrapped = wrapError(exception);
  511 + }
  512 + reply.reply(wrapped);
  513 + });
  514 + } else {
  515 + channel.setMessageHandler(null);
  516 + }
  517 + }
  518 + {
  519 + BasicMessageChannel<Object> channel =
  520 + new BasicMessageChannel<>(
  521 + binaryMessenger,
  522 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume"
  523 + + messageChannelSuffix,
  524 + getCodec());
  525 + if (api != null) {
  526 + channel.setMessageHandler(
  527 + (message, reply) -> {
  528 + ArrayList<Object> wrapped = new ArrayList<>();
  529 + ArrayList<Object> args = (ArrayList<Object>) message;
  530 + Long playerIdArg = (Long) args.get(0);
  531 + Double volumeArg = (Double) args.get(1);
  532 + try {
  533 + api.setVolume(playerIdArg, volumeArg);
  534 + wrapped.add(0, null);
  535 + } catch (Throwable exception) {
  536 + wrapped = wrapError(exception);
  537 + }
  538 + reply.reply(wrapped);
  539 + });
  540 + } else {
  541 + channel.setMessageHandler(null);
  542 + }
  543 + }
  544 + {
  545 + BasicMessageChannel<Object> channel =
  546 + new BasicMessageChannel<>(
  547 + binaryMessenger,
  548 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed"
  549 + + messageChannelSuffix,
  550 + getCodec());
  551 + if (api != null) {
  552 + channel.setMessageHandler(
  553 + (message, reply) -> {
  554 + ArrayList<Object> wrapped = new ArrayList<>();
  555 + ArrayList<Object> args = (ArrayList<Object>) message;
  556 + Long playerIdArg = (Long) args.get(0);
  557 + Double speedArg = (Double) args.get(1);
  558 + try {
  559 + api.setPlaybackSpeed(playerIdArg, speedArg);
  560 + wrapped.add(0, null);
  561 + } catch (Throwable exception) {
  562 + wrapped = wrapError(exception);
  563 + }
  564 + reply.reply(wrapped);
  565 + });
  566 + } else {
  567 + channel.setMessageHandler(null);
  568 + }
  569 + }
  570 + {
  571 + BasicMessageChannel<Object> channel =
  572 + new BasicMessageChannel<>(
  573 + binaryMessenger,
  574 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play"
  575 + + messageChannelSuffix,
  576 + getCodec());
  577 + if (api != null) {
  578 + channel.setMessageHandler(
  579 + (message, reply) -> {
  580 + ArrayList<Object> wrapped = new ArrayList<>();
  581 + ArrayList<Object> args = (ArrayList<Object>) message;
  582 + Long playerIdArg = (Long) args.get(0);
  583 + try {
  584 + api.play(playerIdArg);
  585 + wrapped.add(0, null);
  586 + } catch (Throwable exception) {
  587 + wrapped = wrapError(exception);
  588 + }
  589 + reply.reply(wrapped);
  590 + });
  591 + } else {
  592 + channel.setMessageHandler(null);
  593 + }
  594 + }
  595 + {
  596 + BasicMessageChannel<Object> channel =
  597 + new BasicMessageChannel<>(
  598 + binaryMessenger,
  599 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position"
  600 + + messageChannelSuffix,
  601 + getCodec());
  602 + if (api != null) {
  603 + channel.setMessageHandler(
  604 + (message, reply) -> {
  605 + ArrayList<Object> wrapped = new ArrayList<>();
  606 + ArrayList<Object> args = (ArrayList<Object>) message;
  607 + Long playerIdArg = (Long) args.get(0);
  608 + try {
  609 + Long output = api.position(playerIdArg);
  610 + wrapped.add(0, output);
  611 + } catch (Throwable exception) {
  612 + wrapped = wrapError(exception);
  613 + }
  614 + reply.reply(wrapped);
  615 + });
  616 + } else {
  617 + channel.setMessageHandler(null);
  618 + }
  619 + }
  620 + {
  621 + BasicMessageChannel<Object> channel =
  622 + new BasicMessageChannel<>(
  623 + binaryMessenger,
  624 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo"
  625 + + messageChannelSuffix,
  626 + getCodec());
  627 + if (api != null) {
  628 + channel.setMessageHandler(
  629 + (message, reply) -> {
  630 + ArrayList<Object> wrapped = new ArrayList<>();
  631 + ArrayList<Object> args = (ArrayList<Object>) message;
  632 + Long playerIdArg = (Long) args.get(0);
  633 + Long positionArg = (Long) args.get(1);
  634 + try {
  635 + api.seekTo(playerIdArg, positionArg);
  636 + wrapped.add(0, null);
  637 + } catch (Throwable exception) {
  638 + wrapped = wrapError(exception);
  639 + }
  640 + reply.reply(wrapped);
  641 + });
  642 + } else {
  643 + channel.setMessageHandler(null);
  644 + }
  645 + }
  646 + {
  647 + BasicMessageChannel<Object> channel =
  648 + new BasicMessageChannel<>(
  649 + binaryMessenger,
  650 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause"
  651 + + messageChannelSuffix,
  652 + getCodec());
  653 + if (api != null) {
  654 + channel.setMessageHandler(
  655 + (message, reply) -> {
  656 + ArrayList<Object> wrapped = new ArrayList<>();
  657 + ArrayList<Object> args = (ArrayList<Object>) message;
  658 + Long playerIdArg = (Long) args.get(0);
  659 + try {
  660 + api.pause(playerIdArg);
  661 + wrapped.add(0, null);
  662 + } catch (Throwable exception) {
  663 + wrapped = wrapError(exception);
  664 + }
  665 + reply.reply(wrapped);
  666 + });
  667 + } else {
  668 + channel.setMessageHandler(null);
  669 + }
  670 + }
  671 + {
  672 + BasicMessageChannel<Object> channel =
  673 + new BasicMessageChannel<>(
  674 + binaryMessenger,
  675 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers"
  676 + + messageChannelSuffix,
  677 + getCodec());
  678 + if (api != null) {
  679 + channel.setMessageHandler(
  680 + (message, reply) -> {
  681 + ArrayList<Object> wrapped = new ArrayList<>();
  682 + ArrayList<Object> args = (ArrayList<Object>) message;
  683 + Boolean mixWithOthersArg = (Boolean) args.get(0);
  684 + try {
  685 + api.setMixWithOthers(mixWithOthersArg);
  686 + wrapped.add(0, null);
  687 + } catch (Throwable exception) {
  688 + wrapped = wrapError(exception);
  689 + }
  690 + reply.reply(wrapped);
  691 + });
  692 + } else {
  693 + channel.setMessageHandler(null);
  694 + }
  695 + }
  696 + }
  697 + }
  698 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import io.flutter.plugin.common.EventChannel;
  8 +import java.util.ArrayList;
  9 +
  10 +/**
  11 + * And implementation of {@link EventChannel.EventSink} which can wrap an underlying sink.
  12 + *
  13 + * <p>It delivers messages immediately when downstream is available, but it queues messages before
  14 + * the delegate event sink is set with setDelegate.
  15 + *
  16 + * <p>This class is not thread-safe. All calls must be done on the same thread or synchronized
  17 + * externally.
  18 + */
  19 +final class QueuingEventSink implements EventChannel.EventSink {
  20 + private EventChannel.EventSink delegate;
  21 + private final ArrayList<Object> eventQueue = new ArrayList<>();
  22 + private boolean done = false;
  23 +
  24 + public void setDelegate(EventChannel.EventSink delegate) {
  25 + this.delegate = delegate;
  26 + maybeFlush();
  27 + }
  28 +
  29 + @Override
  30 + public void endOfStream() {
  31 + enqueue(new EndOfStreamEvent());
  32 + maybeFlush();
  33 + done = true;
  34 + }
  35 +
  36 + @Override
  37 + public void error(String code, String message, Object details) {
  38 + enqueue(new ErrorEvent(code, message, details));
  39 + maybeFlush();
  40 + }
  41 +
  42 + @Override
  43 + public void success(Object event) {
  44 + enqueue(event);
  45 + maybeFlush();
  46 + }
  47 +
  48 + private void enqueue(Object event) {
  49 + if (done) {
  50 + return;
  51 + }
  52 + eventQueue.add(event);
  53 + }
  54 +
  55 + private void maybeFlush() {
  56 + if (delegate == null) {
  57 + return;
  58 + }
  59 + for (Object event : eventQueue) {
  60 + if (event instanceof EndOfStreamEvent) {
  61 + delegate.endOfStream();
  62 + } else if (event instanceof ErrorEvent) {
  63 + ErrorEvent errorEvent = (ErrorEvent) event;
  64 + delegate.error(errorEvent.code, errorEvent.message, errorEvent.details);
  65 + } else {
  66 + delegate.success(event);
  67 + }
  68 + }
  69 + eventQueue.clear();
  70 + }
  71 +
  72 + static class EndOfStreamEvent {}
  73 +
  74 + private static class ErrorEvent {
  75 + String code;
  76 + String message;
  77 + Object details;
  78 +
  79 + ErrorEvent(String code, String message, Object details) {
  80 + this.code = code;
  81 + this.message = message;
  82 + this.details = details;
  83 + }
  84 + }
  85 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.OptIn;
  10 +import androidx.media3.common.MediaItem;
  11 +import androidx.media3.common.util.UnstableApi;
  12 +import androidx.media3.exoplayer.rtsp.RtspMediaSource;
  13 +import androidx.media3.exoplayer.source.MediaSource;
  14 +
  15 +final class RtspVideoAsset extends VideoAsset {
  16 + RtspVideoAsset(@NonNull String assetUrl) {
  17 + super(assetUrl);
  18 + }
  19 +
  20 + @NonNull
  21 + @Override
  22 + public MediaItem getMediaItem() {
  23 + return new MediaItem.Builder().setUri(assetUrl).build();
  24 + }
  25 +
  26 + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
  27 + @OptIn(markerClass = UnstableApi.class)
  28 + @Override
  29 + public MediaSource.Factory getMediaSourceFactory(Context context) {
  30 + return new RtspMediaSource.Factory();
  31 + }
  32 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.media3.common.MediaItem;
  11 +import androidx.media3.exoplayer.source.MediaSource;
  12 +import java.util.HashMap;
  13 +import java.util.Map;
  14 +
  15 +/** A video to be played by {@link VideoPlayer}. */
  16 +public abstract class VideoAsset {
  17 + /**
  18 + * Returns an asset from a local {@code asset:///} URL, i.e. an on-device asset.
  19 + *
  20 + * @param assetUrl local asset, beginning in {@code asset:///}.
  21 + * @return the asset.
  22 + */
  23 + @NonNull
  24 + static VideoAsset fromAssetUrl(@NonNull String assetUrl) {
  25 + if (!assetUrl.startsWith("asset:///")) {
  26 + throw new IllegalArgumentException("assetUrl must start with 'asset:///'");
  27 + }
  28 + return new LocalVideoAsset(assetUrl);
  29 + }
  30 +
  31 + /**
  32 + * Returns an asset from a remote URL.
  33 + *
  34 + * @param remoteUrl remote asset, i.e. typically beginning with {@code https://} or similar.
  35 + * @param streamingFormat which streaming format, provided as a hint if able.
  36 + * @param httpHeaders HTTP headers to set for a request.
  37 + * @return the asset.
  38 + */
  39 + @NonNull
  40 + static VideoAsset fromRemoteUrl(
  41 + @Nullable String remoteUrl,
  42 + @NonNull StreamingFormat streamingFormat,
  43 + @NonNull Map<String, String> httpHeaders) {
  44 + return new HttpVideoAsset(remoteUrl, streamingFormat, new HashMap<>(httpHeaders));
  45 + }
  46 +
  47 + /**
  48 + * Returns an asset from a RTSP URL.
  49 + *
  50 + * @param rtspUrl remote asset, beginning with {@code rtsp://}.
  51 + * @return the asset.
  52 + */
  53 + @NonNull
  54 + static VideoAsset fromRtspUrl(@NonNull String rtspUrl) {
  55 + if (!rtspUrl.startsWith("rtsp://")) {
  56 + throw new IllegalArgumentException("rtspUrl must start with 'rtsp://'");
  57 + }
  58 + return new RtspVideoAsset(rtspUrl);
  59 + }
  60 +
  61 + @Nullable protected final String assetUrl;
  62 +
  63 + protected VideoAsset(@Nullable String assetUrl) {
  64 + this.assetUrl = assetUrl;
  65 + }
  66 +
  67 + /**
  68 + * Returns the configured media item to be played.
  69 + *
  70 + * @return media item.
  71 + */
  72 + @NonNull
  73 + public abstract MediaItem getMediaItem();
  74 +
  75 + /**
  76 + * Returns the configured media source factory, if needed for this asset type.
  77 + *
  78 + * @param context application context.
  79 + * @return configured factory, or {@code null} if not needed for this asset type.
  80 + */
  81 + @NonNull
  82 + public abstract MediaSource.Factory getMediaSourceFactory(@NonNull Context context);
  83 +
  84 + /** Streaming formats that can be provided to the video player as a hint. */
  85 + enum StreamingFormat {
  86 + /** Default, if the format is either not known or not another valid format. */
  87 + UNKNOWN,
  88 +
  89 + /** Smooth Streaming. */
  90 + SMOOTH,
  91 +
  92 + /** MPEG-DASH (Dynamic Adaptive over HTTP). */
  93 + DYNAMIC_ADAPTIVE,
  94 +
  95 + /** HTTP Live Streaming (HLS). */
  96 + HTTP_LIVE
  97 + }
  98 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static androidx.media3.common.Player.REPEAT_MODE_ALL;
  8 +import static androidx.media3.common.Player.REPEAT_MODE_OFF;
  9 +
  10 +import androidx.annotation.NonNull;
  11 +import androidx.annotation.Nullable;
  12 +import androidx.media3.common.AudioAttributes;
  13 +import androidx.media3.common.C;
  14 +import androidx.media3.common.MediaItem;
  15 +import androidx.media3.common.PlaybackParameters;
  16 +import androidx.media3.exoplayer.ExoPlayer;
  17 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  18 +
  19 +/**
  20 + * A class responsible for managing video playback using {@link ExoPlayer}.
  21 + *
  22 + * <p>It provides methods to control playback, adjust volume, and handle seeking.
  23 + */
  24 +public abstract class VideoPlayer {
  25 + @NonNull private final ExoPlayerProvider exoPlayerProvider;
  26 + @NonNull private final MediaItem mediaItem;
  27 + @NonNull private final VideoPlayerOptions options;
  28 + @NonNull protected final VideoPlayerCallbacks videoPlayerEvents;
  29 + @Nullable protected final SurfaceProducer surfaceProducer;
  30 + @NonNull protected ExoPlayer exoPlayer;
  31 +
  32 + /** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
  33 + public interface ExoPlayerProvider {
  34 + /**
  35 + * Returns a new {@link ExoPlayer}.
  36 + *
  37 + * @return new instance.
  38 + */
  39 + @NonNull
  40 + ExoPlayer get();
  41 + }
  42 +
  43 + public VideoPlayer(
  44 + @NonNull VideoPlayerCallbacks events,
  45 + @NonNull MediaItem mediaItem,
  46 + @NonNull VideoPlayerOptions options,
  47 + @Nullable SurfaceProducer surfaceProducer,
  48 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  49 + this.videoPlayerEvents = events;
  50 + this.mediaItem = mediaItem;
  51 + this.options = options;
  52 + this.exoPlayerProvider = exoPlayerProvider;
  53 + this.surfaceProducer = surfaceProducer;
  54 + this.exoPlayer = createVideoPlayer();
  55 + }
  56 +
  57 + @NonNull
  58 + protected ExoPlayer createVideoPlayer() {
  59 + ExoPlayer exoPlayer = exoPlayerProvider.get();
  60 + exoPlayer.setMediaItem(mediaItem);
  61 + exoPlayer.prepare();
  62 + exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
  63 + setAudioAttributes(exoPlayer, options.mixWithOthers);
  64 +
  65 + return exoPlayer;
  66 + }
  67 +
  68 + @NonNull
  69 + protected abstract ExoPlayerEventListener createExoPlayerEventListener(
  70 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer);
  71 +
  72 + void sendBufferingUpdate() {
  73 + videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition());
  74 + }
  75 +
  76 + private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) {
  77 + exoPlayer.setAudioAttributes(
  78 + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(),
  79 + !isMixMode);
  80 + }
  81 +
  82 + void play() {
  83 + exoPlayer.play();
  84 + }
  85 +
  86 + void pause() {
  87 + exoPlayer.pause();
  88 + }
  89 +
  90 + void setLooping(boolean value) {
  91 + exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
  92 + }
  93 +
  94 + void setVolume(double value) {
  95 + float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
  96 + exoPlayer.setVolume(bracketedValue);
  97 + }
  98 +
  99 + void setPlaybackSpeed(double value) {
  100 + // We do not need to consider pitch and skipSilence for now as we do not handle them and
  101 + // therefore never diverge from the default values.
  102 + final PlaybackParameters playbackParameters = new PlaybackParameters(((float) value));
  103 +
  104 + exoPlayer.setPlaybackParameters(playbackParameters);
  105 + }
  106 +
  107 + void seekTo(int location) {
  108 + exoPlayer.seekTo(location);
  109 + }
  110 +
  111 + long getPosition() {
  112 + return exoPlayer.getCurrentPosition();
  113 + }
  114 +
  115 + @NonNull
  116 + public ExoPlayer getExoPlayer() {
  117 + return exoPlayer;
  118 + }
  119 +
  120 + public void dispose() {
  121 + exoPlayer.release();
  122 + }
  123 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.annotation.Nullable;
  9 +
  10 +/**
  11 + * Callbacks representing events invoked by {@link VideoPlayer}.
  12 + *
  13 + * <p>In the actual plugin, this will always be {@link VideoPlayerEventCallbacks}, which creates the
  14 + * expected events to send back through the plugin channel. In tests methods can be overridden in
  15 + * order to assert results.
  16 + *
  17 + * <p>See {@link androidx.media3.common.Player.Listener} for details.
  18 + */
  19 +public interface VideoPlayerCallbacks {
  20 + void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees);
  21 +
  22 + void onBufferingStart();
  23 +
  24 + void onBufferingUpdate(long bufferedPosition);
  25 +
  26 + void onBufferingEnd();
  27 +
  28 + void onCompleted();
  29 +
  30 + void onError(@NonNull String code, @Nullable String message, @Nullable Object details);
  31 +
  32 + void onIsPlayingStateUpdate(boolean isPlaying);
  33 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.annotation.Nullable;
  9 +import androidx.annotation.VisibleForTesting;
  10 +import io.flutter.plugin.common.EventChannel;
  11 +import java.util.Arrays;
  12 +import java.util.Collections;
  13 +import java.util.HashMap;
  14 +import java.util.List;
  15 +import java.util.Map;
  16 +
  17 +final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks {
  18 + private final EventChannel.EventSink eventSink;
  19 +
  20 + static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) {
  21 + QueuingEventSink eventSink = new QueuingEventSink();
  22 + eventChannel.setStreamHandler(
  23 + new EventChannel.StreamHandler() {
  24 + @Override
  25 + public void onListen(Object arguments, EventChannel.EventSink events) {
  26 + eventSink.setDelegate(events);
  27 + }
  28 +
  29 + @Override
  30 + public void onCancel(Object arguments) {
  31 + eventSink.setDelegate(null);
  32 + }
  33 + });
  34 + return VideoPlayerEventCallbacks.withSink(eventSink);
  35 + }
  36 +
  37 + @VisibleForTesting
  38 + static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) {
  39 + return new VideoPlayerEventCallbacks(eventSink);
  40 + }
  41 +
  42 + private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) {
  43 + this.eventSink = eventSink;
  44 + }
  45 +
  46 + @Override
  47 + public void onInitialized(
  48 + int width, int height, long durationInMs, int rotationCorrectionInDegrees) {
  49 + Map<String, Object> event = new HashMap<>();
  50 + event.put("event", "initialized");
  51 + event.put("width", width);
  52 + event.put("height", height);
  53 + event.put("duration", durationInMs);
  54 + if (rotationCorrectionInDegrees != 0) {
  55 + event.put("rotationCorrection", rotationCorrectionInDegrees);
  56 + }
  57 + eventSink.success(event);
  58 + }
  59 +
  60 + @Override
  61 + public void onBufferingStart() {
  62 + Map<String, Object> event = new HashMap<>();
  63 + event.put("event", "bufferingStart");
  64 + eventSink.success(event);
  65 + }
  66 +
  67 + @Override
  68 + public void onBufferingUpdate(long bufferedPosition) {
  69 + // iOS supports a list of buffered ranges, so we send as a list with a single range.
  70 + Map<String, Object> event = new HashMap<>();
  71 + event.put("event", "bufferingUpdate");
  72 +
  73 + List<? extends Number> range = Arrays.asList(0, bufferedPosition);
  74 + event.put("values", Collections.singletonList(range));
  75 + eventSink.success(event);
  76 + }
  77 +
  78 + @Override
  79 + public void onBufferingEnd() {
  80 + Map<String, Object> event = new HashMap<>();
  81 + event.put("event", "bufferingEnd");
  82 + eventSink.success(event);
  83 + }
  84 +
  85 + @Override
  86 + public void onCompleted() {
  87 + Map<String, Object> event = new HashMap<>();
  88 + event.put("event", "completed");
  89 + eventSink.success(event);
  90 + }
  91 +
  92 + @Override
  93 + public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) {
  94 + eventSink.error(code, message, details);
  95 + }
  96 +
  97 + @Override
  98 + public void onIsPlayingStateUpdate(boolean isPlaying) {
  99 + Map<String, Object> event = new HashMap<>();
  100 + event.put("event", "isPlayingStateUpdate");
  101 + event.put("isPlaying", isPlaying);
  102 + eventSink.success(event);
  103 + }
  104 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +public class VideoPlayerOptions {
  8 + public boolean mixWithOthers;
  9 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import android.util.LongSparseArray;
  9 +import androidx.annotation.NonNull;
  10 +import io.flutter.FlutterInjector;
  11 +import io.flutter.Log;
  12 +import io.flutter.embedding.engine.plugins.FlutterPlugin;
  13 +import io.flutter.plugin.common.BinaryMessenger;
  14 +import io.flutter.plugin.common.EventChannel;
  15 +import io.flutter.plugins.videoplayer.Messages.AndroidVideoPlayerApi;
  16 +import io.flutter.plugins.videoplayer.Messages.CreateMessage;
  17 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory;
  18 +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer;
  19 +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
  20 +import io.flutter.view.TextureRegistry;
  21 +
  22 +/** Android platform implementation of the VideoPlayerPlugin. */
  23 +public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi {
  24 + private static final String TAG = "VideoPlayerPlugin";
  25 + private final LongSparseArray<VideoPlayer> videoPlayers = new LongSparseArray<>();
  26 + private FlutterState flutterState;
  27 + private final VideoPlayerOptions options = new VideoPlayerOptions();
  28 +
  29 + // TODO(stuartmorgan): Decouple identifiers for platform views and texture views.
  30 + /**
  31 + * The next non-texture player ID, initialized to a high number to avoid collisions with texture
  32 + * IDs (which are generated separately).
  33 + */
  34 + private Long nextPlatformViewPlayerId = Long.MAX_VALUE;
  35 +
  36 + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */
  37 + public VideoPlayerPlugin() {}
  38 +
  39 + @Override
  40 + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
  41 + final FlutterInjector injector = FlutterInjector.instance();
  42 + this.flutterState =
  43 + new FlutterState(
  44 + binding.getApplicationContext(),
  45 + binding.getBinaryMessenger(),
  46 + injector.flutterLoader()::getLookupKeyForAsset,
  47 + injector.flutterLoader()::getLookupKeyForAsset,
  48 + binding.getTextureRegistry());
  49 + flutterState.startListening(this, binding.getBinaryMessenger());
  50 +
  51 + binding
  52 + .getPlatformViewRegistry()
  53 + .registerViewFactory(
  54 + "plugins.flutter.dev/video_player_android",
  55 + new PlatformVideoViewFactory(videoPlayers::get));
  56 + }
  57 +
  58 + @Override
  59 + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
  60 + if (flutterState == null) {
  61 + Log.wtf(TAG, "Detached from the engine before registering to it.");
  62 + }
  63 + flutterState.stopListening(binding.getBinaryMessenger());
  64 + flutterState = null;
  65 + onDestroy();
  66 + }
  67 +
  68 + private void disposeAllPlayers() {
  69 + for (int i = 0; i < videoPlayers.size(); i++) {
  70 + videoPlayers.valueAt(i).dispose();
  71 + }
  72 + videoPlayers.clear();
  73 + }
  74 +
  75 + public void onDestroy() {
  76 + // The whole FlutterView is being destroyed. Here we release resources acquired for all
  77 + // instances
  78 + // of VideoPlayer. Once https://github.com/flutter/flutter/issues/19358 is resolved this may
  79 + // be replaced with just asserting that videoPlayers.isEmpty().
  80 + // https://github.com/flutter/flutter/issues/20989 tracks this.
  81 + disposeAllPlayers();
  82 + }
  83 +
  84 + @Override
  85 + public void initialize() {
  86 + disposeAllPlayers();
  87 + }
  88 +
  89 + @Override
  90 + public @NonNull Long create(@NonNull CreateMessage arg) {
  91 + final VideoAsset videoAsset;
  92 + if (arg.getAsset() != null) {
  93 + String assetLookupKey;
  94 + if (arg.getPackageName() != null) {
  95 + assetLookupKey =
  96 + flutterState.keyForAssetAndPackageName.get(arg.getAsset(), arg.getPackageName());
  97 + } else {
  98 + assetLookupKey = flutterState.keyForAsset.get(arg.getAsset());
  99 + }
  100 + videoAsset = VideoAsset.fromAssetUrl("asset:///" + assetLookupKey);
  101 + } else if (arg.getUri().startsWith("rtsp://")) {
  102 + videoAsset = VideoAsset.fromRtspUrl(arg.getUri());
  103 + } else {
  104 + VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN;
  105 + String formatHint = arg.getFormatHint();
  106 + if (formatHint != null) {
  107 + switch (formatHint) {
  108 + case "ss":
  109 + streamingFormat = VideoAsset.StreamingFormat.SMOOTH;
  110 + break;
  111 + case "dash":
  112 + streamingFormat = VideoAsset.StreamingFormat.DYNAMIC_ADAPTIVE;
  113 + break;
  114 + case "hls":
  115 + streamingFormat = VideoAsset.StreamingFormat.HTTP_LIVE;
  116 + break;
  117 + }
  118 + }
  119 + videoAsset = VideoAsset.fromRemoteUrl(arg.getUri(), streamingFormat, arg.getHttpHeaders());
  120 + }
  121 +
  122 + long id;
  123 + VideoPlayer videoPlayer;
  124 + if (arg.getViewType() == Messages.PlatformVideoViewType.PLATFORM_VIEW) {
  125 + id = nextPlatformViewPlayerId--;
  126 + videoPlayer =
  127 + PlatformViewVideoPlayer.create(
  128 + flutterState.applicationContext,
  129 + VideoPlayerEventCallbacks.bindTo(createEventChannel(id)),
  130 + videoAsset,
  131 + options);
  132 + } else {
  133 + TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer();
  134 + id = handle.id();
  135 + videoPlayer =
  136 + TextureVideoPlayer.create(
  137 + flutterState.applicationContext,
  138 + VideoPlayerEventCallbacks.bindTo(createEventChannel(id)),
  139 + handle,
  140 + videoAsset,
  141 + options);
  142 + }
  143 +
  144 + videoPlayers.put(id, videoPlayer);
  145 + return id;
  146 + }
  147 +
  148 + @NonNull
  149 + private EventChannel createEventChannel(long id) {
  150 + return new EventChannel(
  151 + flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + id);
  152 + }
  153 +
  154 + @NonNull
  155 + private VideoPlayer getPlayer(long playerId) {
  156 + VideoPlayer player = videoPlayers.get(playerId);
  157 +
  158 + // Avoid a very ugly un-debuggable NPE that results in returning a null player.
  159 + if (player == null) {
  160 + String message = "No player found with playerId <" + playerId + ">";
  161 + if (videoPlayers.size() == 0) {
  162 + message += " and no active players created by the plugin.";
  163 + }
  164 + throw new IllegalStateException(message);
  165 + }
  166 +
  167 + return player;
  168 + }
  169 +
  170 + @Override
  171 + public void dispose(@NonNull Long playerId) {
  172 + VideoPlayer player = getPlayer(playerId);
  173 + player.dispose();
  174 + videoPlayers.remove(playerId);
  175 + }
  176 +
  177 + @Override
  178 + public void setLooping(@NonNull Long playerId, @NonNull Boolean looping) {
  179 + VideoPlayer player = getPlayer(playerId);
  180 + player.setLooping(looping);
  181 + }
  182 +
  183 + @Override
  184 + public void setVolume(@NonNull Long playerId, @NonNull Double volume) {
  185 + VideoPlayer player = getPlayer(playerId);
  186 + player.setVolume(volume);
  187 + }
  188 +
  189 + @Override
  190 + public void setPlaybackSpeed(@NonNull Long playerId, @NonNull Double speed) {
  191 + VideoPlayer player = getPlayer(playerId);
  192 + player.setPlaybackSpeed(speed);
  193 + }
  194 +
  195 + @Override
  196 + public void play(@NonNull Long playerId) {
  197 + VideoPlayer player = getPlayer(playerId);
  198 + player.play();
  199 + }
  200 +
  201 + @Override
  202 + public @NonNull Long position(@NonNull Long playerId) {
  203 + VideoPlayer player = getPlayer(playerId);
  204 + long position = player.getPosition();
  205 + player.sendBufferingUpdate();
  206 + return position;
  207 + }
  208 +
  209 + @Override
  210 + public void seekTo(@NonNull Long playerId, @NonNull Long position) {
  211 + VideoPlayer player = getPlayer(playerId);
  212 + player.seekTo(position.intValue());
  213 + }
  214 +
  215 + @Override
  216 + public void pause(@NonNull Long playerId) {
  217 + VideoPlayer player = getPlayer(playerId);
  218 + player.pause();
  219 + }
  220 +
  221 + @Override
  222 + public void setMixWithOthers(@NonNull Boolean mixWithOthers) {
  223 + options.mixWithOthers = mixWithOthers;
  224 + }
  225 +
  226 + private interface KeyForAssetFn {
  227 + String get(String asset);
  228 + }
  229 +
  230 + private interface KeyForAssetAndPackageName {
  231 + String get(String asset, String packageName);
  232 + }
  233 +
  234 + private static final class FlutterState {
  235 + final Context applicationContext;
  236 + final BinaryMessenger binaryMessenger;
  237 + final KeyForAssetFn keyForAsset;
  238 + final KeyForAssetAndPackageName keyForAssetAndPackageName;
  239 + final TextureRegistry textureRegistry;
  240 +
  241 + FlutterState(
  242 + Context applicationContext,
  243 + BinaryMessenger messenger,
  244 + KeyForAssetFn keyForAsset,
  245 + KeyForAssetAndPackageName keyForAssetAndPackageName,
  246 + TextureRegistry textureRegistry) {
  247 + this.applicationContext = applicationContext;
  248 + this.binaryMessenger = messenger;
  249 + this.keyForAsset = keyForAsset;
  250 + this.keyForAssetAndPackageName = keyForAssetAndPackageName;
  251 + this.textureRegistry = textureRegistry;
  252 + }
  253 +
  254 + void startListening(VideoPlayerPlugin methodCallHandler, BinaryMessenger messenger) {
  255 + AndroidVideoPlayerApi.setUp(messenger, methodCallHandler);
  256 + }
  257 +
  258 + void stopListening(BinaryMessenger messenger) {
  259 + AndroidVideoPlayerApi.setUp(messenger, null);
  260 + }
  261 + }
  262 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoView.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import android.content.Context;
  8 +import android.os.Build;
  9 +import android.view.SurfaceHolder;
  10 +import android.view.SurfaceView;
  11 +import android.view.View;
  12 +import androidx.annotation.NonNull;
  13 +import androidx.annotation.OptIn;
  14 +import androidx.media3.common.util.UnstableApi;
  15 +import androidx.media3.exoplayer.ExoPlayer;
  16 +import io.flutter.plugin.platform.PlatformView;
  17 +
  18 +/**
  19 + * A class used to create a native video view that can be embedded in a Flutter app. It wraps an
  20 + * {@link ExoPlayer} instance and displays its video content.
  21 + */
  22 +public final class PlatformVideoView implements PlatformView {
  23 + @NonNull private final SurfaceView surfaceView;
  24 +
  25 + /**
  26 + * Constructs a new PlatformVideoView.
  27 + *
  28 + * @param context The context in which the view is running.
  29 + * @param exoPlayer The ExoPlayer instance used to play the video.
  30 + */
  31 + @OptIn(markerClass = UnstableApi.class)
  32 + public PlatformVideoView(@NonNull Context context, @NonNull ExoPlayer exoPlayer) {
  33 + surfaceView = new SurfaceView(context);
  34 +
  35 + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
  36 + // Workaround for rendering issues on Android 9 (API 28).
  37 + // On Android 9, using setVideoSurfaceView seems to lead to issues where the first frame is
  38 + // not displayed if the video is paused initially.
  39 + // To ensure the first frame is visible, the surface is directly set using holder.getSurface()
  40 + // when the surface is created, and ExoPlayer seeks to a position to force rendering of the
  41 + // first frame.
  42 + setupSurfaceWithCallback(exoPlayer);
  43 + } else {
  44 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
  45 + // Avoid blank space instead of a video on Android versions below 8 by adjusting video's
  46 + // z-layer within the Android view hierarchy:
  47 + surfaceView.setZOrderMediaOverlay(true);
  48 + }
  49 + exoPlayer.setVideoSurfaceView(surfaceView);
  50 + }
  51 + }
  52 +
  53 + private void setupSurfaceWithCallback(@NonNull ExoPlayer exoPlayer) {
  54 + surfaceView
  55 + .getHolder()
  56 + .addCallback(
  57 + new SurfaceHolder.Callback() {
  58 + @Override
  59 + public void surfaceCreated(@NonNull SurfaceHolder holder) {
  60 + exoPlayer.setVideoSurface(holder.getSurface());
  61 + // Force first frame rendering:
  62 + exoPlayer.seekTo(1);
  63 + }
  64 +
  65 + @Override
  66 + public void surfaceChanged(
  67 + @NonNull SurfaceHolder holder, int format, int width, int height) {
  68 + // No implementation needed.
  69 + }
  70 +
  71 + @Override
  72 + public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
  73 + exoPlayer.setVideoSurface(null);
  74 + }
  75 + });
  76 + }
  77 +
  78 + /**
  79 + * Returns the view associated with this PlatformView.
  80 + *
  81 + * @return The SurfaceView used to display the video.
  82 + */
  83 + @NonNull
  84 + @Override
  85 + public View getView() {
  86 + return surfaceView;
  87 + }
  88 +
  89 + /** Disposes of the resources used by this PlatformView. */
  90 + @Override
  91 + public void dispose() {
  92 + surfaceView.getHolder().getSurface().release();
  93 + }
  94 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoViewFactory.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.media3.exoplayer.ExoPlayer;
  11 +import io.flutter.plugin.platform.PlatformView;
  12 +import io.flutter.plugin.platform.PlatformViewFactory;
  13 +import io.flutter.plugins.videoplayer.Messages;
  14 +import io.flutter.plugins.videoplayer.VideoPlayer;
  15 +import java.util.Objects;
  16 +
  17 +/**
  18 + * A factory class responsible for creating platform video views that can be embedded in a Flutter
  19 + * app.
  20 + */
  21 +public class PlatformVideoViewFactory extends PlatformViewFactory {
  22 + private final VideoPlayerProvider videoPlayerProvider;
  23 +
  24 + /** Functional interface for providing a VideoPlayer instance based on the player ID. */
  25 + @FunctionalInterface
  26 + public interface VideoPlayerProvider {
  27 + /**
  28 + * Retrieves a VideoPlayer instance based on the provided player ID.
  29 + *
  30 + * @param playerId The unique identifier for the video player.
  31 + * @return A VideoPlayer instance associated with the given player ID.
  32 + */
  33 + @NonNull
  34 + VideoPlayer getVideoPlayer(@NonNull Long playerId);
  35 + }
  36 +
  37 + /**
  38 + * Constructs a new PlatformVideoViewFactory.
  39 + *
  40 + * @param videoPlayerProvider The provider used to retrieve the video player associated with the
  41 + * view.
  42 + */
  43 + public PlatformVideoViewFactory(@NonNull VideoPlayerProvider videoPlayerProvider) {
  44 + super(Messages.AndroidVideoPlayerApi.getCodec());
  45 + this.videoPlayerProvider = videoPlayerProvider;
  46 + }
  47 +
  48 + /**
  49 + * Creates a new instance of platform view.
  50 + *
  51 + * @param context The context in which the view is running.
  52 + * @param id The unique identifier for the view.
  53 + * @param args The arguments for creating the view.
  54 + * @return A new instance of PlatformVideoView.
  55 + */
  56 + @NonNull
  57 + @Override
  58 + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
  59 + final Messages.PlatformVideoViewCreationParams params =
  60 + Objects.requireNonNull((Messages.PlatformVideoViewCreationParams) args);
  61 + final Long playerId = params.getPlayerId();
  62 +
  63 + final VideoPlayer player = videoPlayerProvider.getVideoPlayer(playerId);
  64 + final ExoPlayer exoPlayer = player.getExoPlayer();
  65 +
  66 + return new PlatformVideoView(context, exoPlayer);
  67 + }
  68 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.annotation.OptIn;
  9 +import androidx.annotation.VisibleForTesting;
  10 +import androidx.media3.common.Format;
  11 +import androidx.media3.common.util.UnstableApi;
  12 +import androidx.media3.exoplayer.ExoPlayer;
  13 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  14 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  15 +import java.util.Objects;
  16 +
  17 +public final class PlatformViewExoPlayerEventListener extends ExoPlayerEventListener {
  18 + @VisibleForTesting
  19 + public PlatformViewExoPlayerEventListener(
  20 + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) {
  21 + this(exoPlayer, events, false);
  22 + }
  23 +
  24 + public PlatformViewExoPlayerEventListener(
  25 + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
  26 + super(exoPlayer, events, initialized);
  27 + }
  28 +
  29 + @OptIn(markerClass = UnstableApi.class)
  30 + @Override
  31 + protected void sendInitialized() {
  32 + // We can't rely on VideoSize here, because at this point it is not available - the platform
  33 + // view was not created yet. We use the video format instead.
  34 + Format videoFormat = exoPlayer.getVideoFormat();
  35 + RotationDegrees rotationCorrection =
  36 + RotationDegrees.fromDegrees(Objects.requireNonNull(videoFormat).rotationDegrees);
  37 + int width = videoFormat.width;
  38 + int height = videoFormat.height;
  39 +
  40 + // Switch the width/height if video was taken in portrait mode and a rotation
  41 + // correction was detected.
  42 + if (rotationCorrection == RotationDegrees.ROTATE_90
  43 + || rotationCorrection == RotationDegrees.ROTATE_270) {
  44 + width = videoFormat.height;
  45 + height = videoFormat.width;
  46 +
  47 + rotationCorrection = RotationDegrees.fromDegrees(0);
  48 + }
  49 +
  50 + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees());
  51 + }
  52 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.annotation.VisibleForTesting;
  11 +import androidx.media3.common.MediaItem;
  12 +import androidx.media3.exoplayer.DefaultRenderersFactory;
  13 +import androidx.media3.exoplayer.ExoPlayer;
  14 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  15 +import io.flutter.plugins.videoplayer.VideoAsset;
  16 +import io.flutter.plugins.videoplayer.VideoPlayer;
  17 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  18 +import io.flutter.plugins.videoplayer.VideoPlayerOptions;
  19 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  20 +
  21 +/**
  22 + * A subclass of {@link VideoPlayer} that adds functionality related to platform view as a way of
  23 + * displaying the video in the app.
  24 + */
  25 +public class PlatformViewVideoPlayer extends VideoPlayer {
  26 + @VisibleForTesting
  27 + public PlatformViewVideoPlayer(
  28 + @NonNull VideoPlayerCallbacks events,
  29 + @NonNull MediaItem mediaItem,
  30 + @NonNull VideoPlayerOptions options,
  31 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  32 + super(events, mediaItem, options, /* surfaceProducer */ null, exoPlayerProvider);
  33 + }
  34 +
  35 + /**
  36 + * Creates a platform view video player.
  37 + *
  38 + * @param context application context.
  39 + * @param events event callbacks.
  40 + * @param asset asset to play.
  41 + * @param options options for playback.
  42 + * @return a video player instance.
  43 + */
  44 + @NonNull
  45 + public static PlatformViewVideoPlayer create(
  46 + @NonNull Context context,
  47 + @NonNull VideoPlayerCallbacks events,
  48 + @NonNull VideoAsset asset,
  49 + @NonNull VideoPlayerOptions options) {
  50 + return new PlatformViewVideoPlayer(
  51 + events,
  52 + asset.getMediaItem(),
  53 + options,
  54 + () -> {
  55 + DefaultRenderersFactory renderersFactory =
  56 + new DefaultRenderersFactory(context).setEnableDecoderFallback(true);
  57 + ExoPlayer.Builder builder =
  58 + new ExoPlayer.Builder(context, renderersFactory)
  59 + .setMediaSourceFactory(asset.getMediaSourceFactory(context));
  60 + return builder.build();
  61 + });
  62 + }
  63 +
  64 + @NonNull
  65 + @Override
  66 + protected ExoPlayerEventListener createExoPlayerEventListener(
  67 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
  68 + // Platform view video player does not suspend and re-create the exoPlayer, hence initialized
  69 + // is always false. It also does not require a reference to the SurfaceProducer.
  70 + return new PlatformViewExoPlayerEventListener(exoPlayer, videoPlayerEvents, false);
  71 + }
  72 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.texture;
  6 +
  7 +import android.os.Build;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.OptIn;
  10 +import androidx.annotation.VisibleForTesting;
  11 +import androidx.media3.common.Format;
  12 +import androidx.media3.common.VideoSize;
  13 +import androidx.media3.exoplayer.ExoPlayer;
  14 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  15 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  16 +import java.util.Objects;
  17 +
  18 +public final class TextureExoPlayerEventListener extends ExoPlayerEventListener {
  19 + private boolean surfaceProducerHandlesCropAndRotation;
  20 +
  21 + @VisibleForTesting
  22 + public TextureExoPlayerEventListener(
  23 + @NonNull ExoPlayer exoPlayer,
  24 + @NonNull VideoPlayerCallbacks events,
  25 + boolean surfaceProducerHandlesCropAndRotation) {
  26 + this(exoPlayer, events, surfaceProducerHandlesCropAndRotation, false);
  27 + }
  28 +
  29 + public TextureExoPlayerEventListener(
  30 + @NonNull ExoPlayer exoPlayer,
  31 + @NonNull VideoPlayerCallbacks events,
  32 + boolean surfaceProducerHandlesCropAndRotation,
  33 + boolean initialized) {
  34 + super(exoPlayer, events, initialized);
  35 + this.surfaceProducerHandlesCropAndRotation = surfaceProducerHandlesCropAndRotation;
  36 + }
  37 +
  38 + @Override
  39 + protected void sendInitialized() {
  40 + VideoSize videoSize = exoPlayer.getVideoSize();
  41 + RotationDegrees rotationCorrection = RotationDegrees.ROTATE_0;
  42 + int width = videoSize.width;
  43 + int height = videoSize.height;
  44 + if (width != 0 && height != 0) {
  45 + if (Build.VERSION.SDK_INT <= 21) {
  46 + // On API 21 and below, Exoplayer may not internally handle rotation correction
  47 + // and reports it through VideoSize.unappliedRotationDegrees. We may apply it to
  48 + // fix the case of upside-down playback.
  49 + try {
  50 + RotationDegrees unappliedRotation =
  51 + RotationDegrees.fromDegrees(videoSize.unappliedRotationDegrees);
  52 + rotationCorrection = getRotationCorrectionFromUnappliedRotation(unappliedRotation);
  53 + } catch (IllegalArgumentException e) {
  54 + // Unapplied rotation other than 0, 90, 180, 270 reported by VideoSize. Because this is
  55 + // unexpected, we apply no rotation correction.
  56 + rotationCorrection = RotationDegrees.ROTATE_0;
  57 + }
  58 + } else if (surfaceProducerHandlesCropAndRotation) {
  59 + // When the SurfaceTexture backend for Impeller is used, the preview should already
  60 + // be correctly rotated.
  61 + rotationCorrection = RotationDegrees.ROTATE_0;
  62 + } else {
  63 + // The video's Format also provides a rotation correction that may be used to
  64 + // correct the rotation, so we try to use that to correct the video rotation
  65 + // when the ImageReader backend for Impeller is used.
  66 + int rawVideoFormatRotation = getRotationCorrectionFromFormat(exoPlayer);
  67 +
  68 + try {
  69 + rotationCorrection = RotationDegrees.fromDegrees(rawVideoFormatRotation);
  70 + } catch (IllegalArgumentException e) {
  71 + // Rotation correction other than 0, 90, 180, 270 reported by Format. Because this is
  72 + // unexpected we apply no rotation correction.
  73 + rotationCorrection = RotationDegrees.ROTATE_0;
  74 + }
  75 + }
  76 + }
  77 + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees());
  78 + }
  79 +
  80 + private RotationDegrees getRotationCorrectionFromUnappliedRotation(
  81 + RotationDegrees unappliedRotationDegrees) {
  82 + RotationDegrees rotationCorrection = RotationDegrees.ROTATE_0;
  83 +
  84 + // Rotating the video with ExoPlayer does not seem to be possible with a Surface,
  85 + // so inform the Flutter code that the widget needs to be rotated to prevent
  86 + // upside-down playback for videos with unappliedRotationDegrees of 180 (other orientations
  87 + // work correctly without correction).
  88 + if (unappliedRotationDegrees == RotationDegrees.ROTATE_180) {
  89 + rotationCorrection = unappliedRotationDegrees;
  90 + }
  91 +
  92 + return rotationCorrection;
  93 + }
  94 +
  95 + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
  96 + // A video's Format and its rotation degrees are unstable because they are not guaranteed
  97 + // the same implementation across API versions. It is possible that this logic may need
  98 + // revisiting should the implementation change across versions of the Exoplayer API.
  99 + private int getRotationCorrectionFromFormat(ExoPlayer exoPlayer) {
  100 + Format videoFormat = Objects.requireNonNull(exoPlayer.getVideoFormat());
  101 + return videoFormat.rotationDegrees;
  102 + }
  103 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.texture;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.annotation.RestrictTo;
  11 +import androidx.annotation.VisibleForTesting;
  12 +import androidx.media3.common.MediaItem;
  13 +import androidx.media3.exoplayer.DefaultRenderersFactory;
  14 +import androidx.media3.exoplayer.ExoPlayer;
  15 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  16 +import io.flutter.plugins.videoplayer.ExoPlayerState;
  17 +import io.flutter.plugins.videoplayer.VideoAsset;
  18 +import io.flutter.plugins.videoplayer.VideoPlayer;
  19 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  20 +import io.flutter.plugins.videoplayer.VideoPlayerOptions;
  21 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  22 +
  23 +/**
  24 + * A subclass of {@link VideoPlayer} that adds functionality related to texture view as a way of
  25 + * displaying the video in the app.
  26 + *
  27 + * <p>It manages the lifecycle of the texture and ensures that the video is properly displayed on
  28 + * the texture.
  29 + */
  30 +public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProducer.Callback {
  31 + @Nullable private ExoPlayerState savedStateDuring;
  32 +
  33 + /**
  34 + * Creates a texture video player.
  35 + *
  36 + * @param context application context.
  37 + * @param events event callbacks.
  38 + * @param surfaceProducer produces a texture to render to.
  39 + * @param asset asset to play.
  40 + * @param options options for playback.
  41 + * @return a video player instance.
  42 + */
  43 + @NonNull
  44 + public static TextureVideoPlayer create(
  45 + @NonNull Context context,
  46 + @NonNull VideoPlayerCallbacks events,
  47 + @NonNull SurfaceProducer surfaceProducer,
  48 + @NonNull VideoAsset asset,
  49 + @NonNull VideoPlayerOptions options) {
  50 + return new TextureVideoPlayer(
  51 + events,
  52 + surfaceProducer,
  53 + asset.getMediaItem(),
  54 + options,
  55 + () -> {
  56 + DefaultRenderersFactory renderersFactory =
  57 + new DefaultRenderersFactory(context).setEnableDecoderFallback(true);
  58 + ExoPlayer.Builder builder =
  59 + new ExoPlayer.Builder(context, renderersFactory)
  60 + .setMediaSourceFactory(asset.getMediaSourceFactory(context));
  61 + return builder.build();
  62 + });
  63 + }
  64 +
  65 + @VisibleForTesting
  66 + public TextureVideoPlayer(
  67 + @NonNull VideoPlayerCallbacks events,
  68 + @NonNull SurfaceProducer surfaceProducer,
  69 + @NonNull MediaItem mediaItem,
  70 + @NonNull VideoPlayerOptions options,
  71 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  72 + super(events, mediaItem, options, surfaceProducer, exoPlayerProvider);
  73 +
  74 + surfaceProducer.setCallback(this);
  75 +
  76 + this.exoPlayer.setVideoSurface(surfaceProducer.getSurface());
  77 + }
  78 +
  79 + @NonNull
  80 + @Override
  81 + protected ExoPlayerEventListener createExoPlayerEventListener(
  82 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
  83 + if (surfaceProducer == null) {
  84 + throw new IllegalArgumentException(
  85 + "surfaceProducer cannot be null to create an ExoPlayerEventListener for TextureVideoPlayer.");
  86 + }
  87 + boolean surfaceProducerHandlesCropAndRotation = surfaceProducer.handlesCropAndRotation();
  88 + return new TextureExoPlayerEventListener(
  89 + exoPlayer,
  90 + videoPlayerEvents,
  91 + surfaceProducerHandlesCropAndRotation,
  92 + playerHasBeenSuspended());
  93 + }
  94 +
  95 + @RestrictTo(RestrictTo.Scope.LIBRARY)
  96 + public void onSurfaceAvailable() {
  97 + if (savedStateDuring != null) {
  98 + exoPlayer = createVideoPlayer();
  99 + exoPlayer.setVideoSurface(surfaceProducer.getSurface());
  100 + savedStateDuring.restore(exoPlayer);
  101 + savedStateDuring = null;
  102 + }
  103 + }
  104 +
  105 + @RestrictTo(RestrictTo.Scope.LIBRARY)
  106 + // TODO(bparrishMines): Replace with onSurfaceCleanup once available on stable. See
  107 + // https://github.com/flutter/flutter/issues/161256.
  108 + @SuppressWarnings({"deprecation", "removal"})
  109 + public void onSurfaceDestroyed() {
  110 + // Intentionally do not call pause/stop here, because the surface has already been released
  111 + // at this point (see https://github.com/flutter/flutter/issues/156451).
  112 + savedStateDuring = ExoPlayerState.save(exoPlayer);
  113 + exoPlayer.release();
  114 + }
  115 +
  116 + private boolean playerHasBeenSuspended() {
  117 + return savedStateDuring != null;
  118 + }
  119 +
  120 + public void dispose() {
  121 + // Super must be called first to ensure the player is released before the surface.
  122 + super.dispose();
  123 +
  124 + surfaceProducer.release();
  125 + // TODO(matanlurey): Remove when embedder no longer calls-back once released.
  126 + // https://github.com/flutter/flutter/issues/156434.
  127 + surfaceProducer.setCallback(null);
  128 + }
  129 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.mockito.ArgumentMatchers.contains;
  8 +import static org.mockito.ArgumentMatchers.eq;
  9 +import static org.mockito.ArgumentMatchers.isNull;
  10 +import static org.mockito.Mockito.verify;
  11 +import static org.mockito.Mockito.verifyNoInteractions;
  12 +import static org.mockito.Mockito.verifyNoMoreInteractions;
  13 +import static org.mockito.Mockito.when;
  14 +
  15 +import androidx.media3.common.PlaybackException;
  16 +import androidx.media3.common.Player;
  17 +import androidx.media3.exoplayer.ExoPlayer;
  18 +import org.junit.Before;
  19 +import org.junit.Rule;
  20 +import org.junit.Test;
  21 +import org.junit.runner.RunWith;
  22 +import org.mockito.Mock;
  23 +import org.mockito.junit.MockitoJUnit;
  24 +import org.mockito.junit.MockitoRule;
  25 +import org.robolectric.RobolectricTestRunner;
  26 +
  27 +/**
  28 + * Unit tests for {@link ExoPlayerEventListener}.
  29 + *
  30 + * <p>This test suite <em>narrowly verifies</em> that the events emitted by the underlying {@link
  31 + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect
  32 + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected).
  33 + */
  34 +@RunWith(RobolectricTestRunner.class)
  35 +public final class ExoPlayerEventListenerTest {
  36 + @Mock private ExoPlayer mockExoPlayer;
  37 + @Mock private VideoPlayerCallbacks mockCallbacks;
  38 + private ExoPlayerEventListener eventListener;
  39 +
  40 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  41 +
  42 + /**
  43 + * A test subclass of {@link ExoPlayerEventListener} that exposes the abstract class for testing.
  44 + */
  45 + private static final class TestExoPlayerEventListener extends ExoPlayerEventListener {
  46 + public TestExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks callbacks) {
  47 + super(exoPlayer, callbacks, false);
  48 + }
  49 +
  50 + @Override
  51 + protected void sendInitialized() {
  52 + // No implementation needed.
  53 + }
  54 + }
  55 +
  56 + @Before
  57 + public void setUp() {
  58 + eventListener = new TestExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  59 + }
  60 +
  61 + @Test
  62 + public void onPlaybackStateChangedBufferingSendsBufferingStartAndUpdates() {
  63 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  64 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  65 +
  66 + verify(mockCallbacks).onBufferingStart();
  67 + verify(mockCallbacks).onBufferingUpdate(10L);
  68 + verifyNoMoreInteractions(mockCallbacks);
  69 +
  70 + // If it's invoked again, only the update event is called.
  71 + verify(mockCallbacks).onBufferingUpdate(10L);
  72 + verifyNoMoreInteractions(mockCallbacks);
  73 + }
  74 +
  75 + @Test
  76 + public void onPlaybackStateChangedEndedSendsOnCompleted() {
  77 + eventListener.onPlaybackStateChanged(Player.STATE_ENDED);
  78 +
  79 + verify(mockCallbacks).onCompleted();
  80 + verifyNoMoreInteractions(mockCallbacks);
  81 + }
  82 +
  83 + @Test
  84 + public void onPlaybackStateChangedEndedAfterBufferingSendsBufferingEndAndOnCompleted() {
  85 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  86 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  87 + verify(mockCallbacks).onBufferingStart();
  88 + verify(mockCallbacks).onBufferingUpdate(10L);
  89 +
  90 + eventListener.onPlaybackStateChanged(Player.STATE_ENDED);
  91 + verify(mockCallbacks).onCompleted();
  92 + verify(mockCallbacks).onBufferingEnd();
  93 +
  94 + verifyNoMoreInteractions(mockCallbacks);
  95 + }
  96 +
  97 + @Test
  98 + public void onPlaybackStateChangedIdleDoNothing() {
  99 + eventListener.onPlaybackStateChanged(Player.STATE_IDLE);
  100 +
  101 + verifyNoInteractions(mockCallbacks);
  102 + }
  103 +
  104 + @Test
  105 + public void onPlaybackStateChangedIdleAfterBufferingSendsBufferingEnd() {
  106 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  107 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  108 + verify(mockCallbacks).onBufferingStart();
  109 + verify(mockCallbacks).onBufferingUpdate(10L);
  110 +
  111 + eventListener.onPlaybackStateChanged(Player.STATE_IDLE);
  112 + verify(mockCallbacks).onBufferingEnd();
  113 +
  114 + verifyNoMoreInteractions(mockCallbacks);
  115 + }
  116 +
  117 + @Test
  118 + public void onErrorVideoErrorWhenBufferingInProgressAlsoEndBuffering() {
  119 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  120 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  121 + verify(mockCallbacks).onBufferingStart();
  122 + verify(mockCallbacks).onBufferingUpdate(10L);
  123 +
  124 + eventListener.onPlayerError(
  125 + new PlaybackException("BAD", null, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED));
  126 + verify(mockCallbacks).onBufferingEnd();
  127 + verify(mockCallbacks).onError(eq("VideoError"), contains("BAD"), isNull());
  128 + }
  129 +
  130 + @Test
  131 + public void onErrorBehindLiveWindowSeekToDefaultAndPrepare() {
  132 + eventListener.onPlayerError(
  133 + new PlaybackException("SORT_OF_OK", null, PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW));
  134 +
  135 + verify(mockExoPlayer).seekToDefaultPosition();
  136 + verify(mockExoPlayer).prepare();
  137 + verifyNoInteractions(mockCallbacks);
  138 + }
  139 +
  140 + @Test
  141 + public void onIsPlayingChangedToggled() {
  142 + eventListener.onIsPlayingChanged(true);
  143 + verify(mockCallbacks).onIsPlayingStateUpdate(true);
  144 +
  145 + eventListener.onIsPlayingChanged(false);
  146 + verify(mockCallbacks).onIsPlayingStateUpdate(false);
  147 + }
  148 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/FakeVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.media3.common.MediaItem;
  10 +import androidx.media3.exoplayer.source.MediaSource;
  11 +import androidx.media3.test.utils.FakeMediaSourceFactory;
  12 +
  13 +/** A fake implementation of the {@link VideoAsset} class. */
  14 +final class FakeVideoAsset extends VideoAsset {
  15 + @NonNull private final MediaSource.Factory mediaSourceFactory;
  16 +
  17 + FakeVideoAsset(String assetUrl) {
  18 + this(assetUrl, new FakeMediaSourceFactory());
  19 + }
  20 +
  21 + FakeVideoAsset(String assetUrl, @NonNull MediaSource.Factory mediaSourceFactory) {
  22 + super(assetUrl);
  23 + this.mediaSourceFactory = mediaSourceFactory;
  24 + }
  25 +
  26 + @NonNull
  27 + @Override
  28 + public MediaItem getMediaItem() {
  29 + return new MediaItem.Builder().setUri(assetUrl).build();
  30 + }
  31 +
  32 + @NonNull
  33 + @Override
  34 + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
  35 + return mediaSourceFactory;
  36 + }
  37 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewFactoryTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertTrue;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import android.content.Context;
  11 +import androidx.media3.exoplayer.ExoPlayer;
  12 +import io.flutter.plugin.platform.PlatformView;
  13 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoView;
  14 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory;
  15 +import org.junit.Test;
  16 +
  17 +public class PlatformVideoViewFactoryTest {
  18 + @Test
  19 + public void createsPlatformVideoViewBasedOnSuppliedArguments() {
  20 + final PlatformVideoViewFactory.VideoPlayerProvider videoPlayerProvider =
  21 + mock(PlatformVideoViewFactory.VideoPlayerProvider.class);
  22 + final VideoPlayer videoPlayer = mock(VideoPlayer.class);
  23 + final ExoPlayer exoPlayer = mock(ExoPlayer.class);
  24 + final Context context = mock(Context.class);
  25 + final long playerId = 1L;
  26 +
  27 + when(videoPlayerProvider.getVideoPlayer(playerId)).thenReturn(videoPlayer);
  28 + when(videoPlayer.getExoPlayer()).thenReturn(exoPlayer);
  29 +
  30 + final PlatformVideoViewFactory factory = new PlatformVideoViewFactory(videoPlayerProvider);
  31 + final Messages.PlatformVideoViewCreationParams args =
  32 + new Messages.PlatformVideoViewCreationParams.Builder().setPlayerId(playerId).build();
  33 +
  34 + final PlatformView view = factory.create(context, 0, args);
  35 +
  36 + assertTrue(view instanceof PlatformVideoView);
  37 + verify(videoPlayerProvider).getVideoPlayer(playerId);
  38 + verify(videoPlayer).getExoPlayer();
  39 + }
  40 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertNotNull;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import android.content.Context;
  11 +import android.view.SurfaceView;
  12 +import androidx.media3.exoplayer.ExoPlayer;
  13 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoView;
  14 +import java.lang.reflect.Field;
  15 +import org.junit.Test;
  16 +
  17 +/** Unit tests for {@link PlatformVideoViewTest}. */
  18 +public class PlatformVideoViewTest {
  19 + @Test
  20 + public void createsSurfaceViewAndSetsItForExoPlayer() throws Exception {
  21 + final Context mockContext = mock(Context.class);
  22 + final ExoPlayer mockExoPlayer = mock(ExoPlayer.class);
  23 +
  24 + final PlatformVideoView view = new PlatformVideoView(mockContext, mockExoPlayer);
  25 +
  26 + final Field field = PlatformVideoView.class.getDeclaredField("surfaceView");
  27 + field.setAccessible(true);
  28 + final SurfaceView surfaceView = (SurfaceView) field.get(view);
  29 +
  30 + assertNotNull(surfaceView);
  31 + verify(mockExoPlayer).setVideoSurfaceView(surfaceView);
  32 + }
  33 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.mockito.Mockito.verify;
  8 +import static org.mockito.Mockito.when;
  9 +
  10 +import androidx.media3.common.Format;
  11 +import androidx.media3.common.Player;
  12 +import androidx.media3.exoplayer.ExoPlayer;
  13 +import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener;
  14 +import org.junit.Before;
  15 +import org.junit.Rule;
  16 +import org.junit.Test;
  17 +import org.junit.runner.RunWith;
  18 +import org.mockito.Mock;
  19 +import org.mockito.junit.MockitoJUnit;
  20 +import org.mockito.junit.MockitoRule;
  21 +import org.robolectric.RobolectricTestRunner;
  22 +
  23 +/**
  24 + * Unit tests for {@link PlatformViewExoPlayerEventListener}.
  25 + *
  26 + * <p>This test suite <em>narrowly verifies</em> that the events emitted by the underlying {@link
  27 + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect
  28 + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected).
  29 + */
  30 +@RunWith(RobolectricTestRunner.class)
  31 +public final class PlatformViewExoPlayerEventListenerTest {
  32 + @Mock private ExoPlayer mockExoPlayer;
  33 + @Mock private VideoPlayerCallbacks mockCallbacks;
  34 + private ExoPlayerEventListener eventListener;
  35 +
  36 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  37 +
  38 + @Before
  39 + public void setUp() {
  40 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  41 + }
  42 +
  43 + @Test
  44 + public void onPlaybackStateChangedReadySendInitialized() {
  45 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  46 +
  47 + Format format = new Format.Builder().setWidth(800).setHeight(400).build();
  48 + when(mockExoPlayer.getVideoFormat()).thenReturn(format);
  49 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  50 +
  51 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  52 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  53 + }
  54 +
  55 + @Test
  56 + public void onPlaybackStateChangedReadyInPortraitMode90DegreesSwapsWidthAndHeight() {
  57 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  58 +
  59 + Format format =
  60 + new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(90).build();
  61 + when(mockExoPlayer.getVideoFormat()).thenReturn(format);
  62 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  63 +
  64 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  65 + verify(mockCallbacks).onInitialized(400, 800, 10L, 0);
  66 + }
  67 +
  68 + @Test
  69 + public void onPlaybackStateChangedReadyInPortraitMode270DegreesSwapsWidthAndHeight() {
  70 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  71 +
  72 + Format format =
  73 + new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(270).build();
  74 + when(mockExoPlayer.getVideoFormat()).thenReturn(format);
  75 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  76 +
  77 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  78 + verify(mockCallbacks).onInitialized(400, 800, 10L, 0);
  79 + }
  80 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.mockito.Mockito.verify;
  8 +import static org.mockito.Mockito.when;
  9 +
  10 +import androidx.media3.common.Format;
  11 +import androidx.media3.common.Player;
  12 +import androidx.media3.common.VideoSize;
  13 +import androidx.media3.exoplayer.ExoPlayer;
  14 +import io.flutter.plugins.videoplayer.texture.TextureExoPlayerEventListener;
  15 +import org.junit.Rule;
  16 +import org.junit.Test;
  17 +import org.junit.runner.RunWith;
  18 +import org.mockito.Mock;
  19 +import org.mockito.junit.MockitoJUnit;
  20 +import org.mockito.junit.MockitoRule;
  21 +import org.robolectric.RobolectricTestRunner;
  22 +import org.robolectric.annotation.Config;
  23 +
  24 +/**
  25 + * Unit tests for {@link TextureExoPlayerEventListener}.
  26 + *
  27 + * <p>This test suite <em>narrowly verifies</em> that the events emitted by the underlying {@link
  28 + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect
  29 + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected).
  30 + */
  31 +@RunWith(RobolectricTestRunner.class)
  32 +public class TextureExoPlayerEventListenerTest {
  33 + @Mock private ExoPlayer mockExoPlayer;
  34 + @Mock private VideoPlayerCallbacks mockCallbacks;
  35 +
  36 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  37 +
  38 + @Test
  39 + @Config(maxSdk = 21)
  40 + public void onPlaybackStateChangedReadySendInitialized_belowAndroid21() {
  41 + TextureExoPlayerEventListener eventListener =
  42 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  43 + VideoSize size = new VideoSize(800, 400, 0, 0);
  44 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  45 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  46 +
  47 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  48 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  49 + }
  50 +
  51 + @Test
  52 + @Config(minSdk = 22)
  53 + public void
  54 + onPlaybackStateChangedReadySendInitialized_whenSurfaceProducerHandlesCropAndRotation() {
  55 + TextureExoPlayerEventListener eventListener =
  56 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  57 + VideoSize size = new VideoSize(800, 400, 0, 0);
  58 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  59 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  60 +
  61 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  62 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  63 + }
  64 +
  65 + @Test
  66 + @Config(minSdk = 22)
  67 + public void
  68 + onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_whenSurfaceProducerDoesNotHandleCropAndRotation() {
  69 + TextureExoPlayerEventListener eventListener =
  70 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
  71 + VideoSize size = new VideoSize(800, 400, 0, 0);
  72 + int rotationCorrection = 90;
  73 + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
  74 +
  75 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  76 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  77 + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat);
  78 +
  79 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  80 + verify(mockCallbacks).onInitialized(800, 400, 10L, rotationCorrection);
  81 + }
  82 +
  83 + @Test
  84 + @Config(maxSdk = 21)
  85 + public void
  86 + onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_belowAndroid21() {
  87 + TextureExoPlayerEventListener eventListener =
  88 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  89 + VideoSize size = new VideoSize(800, 400, 90, 0);
  90 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  91 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  92 +
  93 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  94 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  95 + }
  96 +
  97 + @Test
  98 + @Config(minSdk = 22)
  99 + public void
  100 + onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() {
  101 + TextureExoPlayerEventListener eventListener =
  102 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  103 + VideoSize size = new VideoSize(800, 400, 90, 0);
  104 +
  105 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  106 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  107 +
  108 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  109 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  110 + }
  111 +
  112 + @Test
  113 + @Config(minSdk = 22)
  114 + public void
  115 + onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() {
  116 + TextureExoPlayerEventListener eventListener =
  117 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
  118 + VideoSize size = new VideoSize(800, 400, 0, 0);
  119 + int rotationCorrection = 90;
  120 + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
  121 +
  122 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  123 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  124 + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat);
  125 +
  126 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  127 + verify(mockCallbacks).onInitialized(800, 400, 10L, 90);
  128 + }
  129 +
  130 + @Test
  131 + @Config(maxSdk = 21)
  132 + public void
  133 + onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_belowAndroid21() {
  134 + TextureExoPlayerEventListener eventListener =
  135 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  136 + VideoSize size = new VideoSize(800, 400, 270, 0);
  137 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  138 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  139 +
  140 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  141 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  142 + }
  143 +
  144 + @Test
  145 + @Config(minSdk = 22)
  146 + public void
  147 + onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() {
  148 + TextureExoPlayerEventListener eventListener =
  149 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  150 + VideoSize size = new VideoSize(800, 400, 270, 0);
  151 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  152 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  153 +
  154 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  155 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  156 + }
  157 +
  158 + @Test
  159 + @Config(minSdk = 22)
  160 + public void
  161 + onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() {
  162 + TextureExoPlayerEventListener eventListener =
  163 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
  164 + VideoSize size = new VideoSize(800, 400, 0, 0);
  165 + int rotationCorrection = 270;
  166 + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
  167 +
  168 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  169 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  170 + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat);
  171 +
  172 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  173 + verify(mockCallbacks).onInitialized(800, 400, 10L, 270);
  174 + }
  175 +
  176 + @Test
  177 + @Config(maxSdk = 21)
  178 + public void onPlaybackStateChangedReadyFlipped180DegreesInformEventHandler_belowAndroid21() {
  179 + TextureExoPlayerEventListener eventListener =
  180 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  181 + VideoSize size = new VideoSize(800, 400, 180, 0);
  182 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  183 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  184 +
  185 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  186 + verify(mockCallbacks).onInitialized(800, 400, 10L, 180);
  187 + }
  188 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.mockito.ArgumentMatchers.any;
  9 +import static org.mockito.Mockito.*;
  10 +
  11 +import android.view.Surface;
  12 +import androidx.media3.common.AudioAttributes;
  13 +import androidx.media3.common.C;
  14 +import androidx.media3.common.PlaybackParameters;
  15 +import androidx.media3.common.Player;
  16 +import androidx.media3.common.VideoSize;
  17 +import androidx.media3.exoplayer.ExoPlayer;
  18 +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
  19 +import io.flutter.view.TextureRegistry;
  20 +import org.junit.Before;
  21 +import org.junit.Rule;
  22 +import org.junit.Test;
  23 +import org.junit.runner.RunWith;
  24 +import org.mockito.ArgumentCaptor;
  25 +import org.mockito.Captor;
  26 +import org.mockito.InOrder;
  27 +import org.mockito.Mock;
  28 +import org.mockito.junit.MockitoJUnit;
  29 +import org.mockito.junit.MockitoRule;
  30 +import org.robolectric.RobolectricTestRunner;
  31 +
  32 +/**
  33 + * Unit tests for {@link TextureVideoPlayer}.
  34 + *
  35 + * <p>This test suite <em>narrowly verifies</em> that {@link TextureVideoPlayer} interfaces with the
  36 + * {@link ExoPlayer} interface <em>exactly</em> as it did when the test suite was created. That is,
  37 + * if the behavior changes, this test will need to change. However, this suite should catch bugs
  38 + * related to <em>"this is a safe refactor with no behavior changes"</em>.
  39 + */
  40 +@RunWith(RobolectricTestRunner.class)
  41 +public final class TextureVideoPlayerTest {
  42 + private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4";
  43 + private FakeVideoAsset fakeVideoAsset;
  44 +
  45 + @Mock private VideoPlayerCallbacks mockEvents;
  46 + @Mock private TextureRegistry.SurfaceProducer mockProducer;
  47 + @Mock private ExoPlayer mockExoPlayer;
  48 + @Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
  49 + @Captor private ArgumentCaptor<TextureRegistry.SurfaceProducer.Callback> callbackCaptor;
  50 + @Captor private ArgumentCaptor<Player.Listener> listenerCaptor;
  51 +
  52 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  53 +
  54 + @Before
  55 + public void setUp() {
  56 + fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
  57 + when(mockProducer.getSurface()).thenReturn(mock(Surface.class));
  58 + when(mockProducer.handlesCropAndRotation()).thenReturn(true);
  59 + }
  60 +
  61 + private VideoPlayer createVideoPlayer() {
  62 + return createVideoPlayer(new VideoPlayerOptions());
  63 + }
  64 +
  65 + private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) {
  66 + return new TextureVideoPlayer(
  67 + mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer);
  68 + }
  69 +
  70 + @Test
  71 + public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {
  72 + VideoPlayer videoPlayer = createVideoPlayer();
  73 +
  74 + verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem());
  75 + verify(mockExoPlayer).prepare();
  76 + verify(mockProducer).getSurface();
  77 + verify(mockExoPlayer).setVideoSurface(any());
  78 +
  79 + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true));
  80 + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE);
  81 +
  82 + videoPlayer.dispose();
  83 + }
  84 +
  85 + @Test
  86 + public void onSurfaceProducerDestroyedAndAvailableReleasesAndThenRecreatesAndResumesPlayer() {
  87 + VideoPlayer videoPlayer = createVideoPlayer();
  88 +
  89 + verify(mockProducer).setCallback(callbackCaptor.capture());
  90 + verify(mockExoPlayer, never()).release();
  91 +
  92 + when(mockExoPlayer.getCurrentPosition()).thenReturn(10L);
  93 + when(mockExoPlayer.getRepeatMode()).thenReturn(Player.REPEAT_MODE_ALL);
  94 + when(mockExoPlayer.getVolume()).thenReturn(0.5f);
  95 + when(mockExoPlayer.getPlaybackParameters()).thenReturn(new PlaybackParameters(2.5f));
  96 +
  97 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  98 + simulateSurfaceDestruction(producerLifecycle);
  99 +
  100 + verify(mockExoPlayer).release();
  101 +
  102 + // Create a new mock exo player so that we get a new instance.
  103 + mockExoPlayer = mock(ExoPlayer.class);
  104 + producerLifecycle.onSurfaceAvailable();
  105 +
  106 + verify(mockExoPlayer).setVideoSurface(any());
  107 + verify(mockExoPlayer).seekTo(10L);
  108 + verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);
  109 + verify(mockExoPlayer).setVolume(0.5f);
  110 + verify(mockExoPlayer).setPlaybackParameters(new PlaybackParameters(2.5f));
  111 +
  112 + videoPlayer.dispose();
  113 + }
  114 +
  115 + @Test
  116 + public void onSurfaceProducerDestroyedDoesNotStopOrPauseVideo() {
  117 + VideoPlayer videoPlayer = createVideoPlayer();
  118 +
  119 + verify(mockProducer).setCallback(callbackCaptor.capture());
  120 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  121 + simulateSurfaceDestruction(producerLifecycle);
  122 +
  123 + verify(mockExoPlayer, never()).stop();
  124 + verify(mockExoPlayer, never()).pause();
  125 + verify(mockExoPlayer, never()).setPlayWhenReady(anyBoolean());
  126 +
  127 + videoPlayer.dispose();
  128 + }
  129 +
  130 + @Test
  131 + public void onDisposeSurfaceProducerCallbackIsDisconnected() {
  132 + // Regression test for https://github.com/flutter/flutter/issues/156158.
  133 + VideoPlayer videoPlayer = createVideoPlayer();
  134 + verify(mockProducer).setCallback(any());
  135 +
  136 + videoPlayer.dispose();
  137 + verify(mockProducer).setCallback(null);
  138 + }
  139 +
  140 + @Test
  141 + public void onInitializedCalledWhenVideoPlayerInitiallyCreated() {
  142 + VideoPlayer videoPlayer = createVideoPlayer();
  143 +
  144 + // Pretend we have a video, and capture the registered event listener.
  145 + when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200));
  146 + verify(mockExoPlayer).addListener(listenerCaptor.capture());
  147 + Player.Listener listener = listenerCaptor.getValue();
  148 +
  149 + // Trigger an event that would trigger onInitialized.
  150 + listener.onPlaybackStateChanged(Player.STATE_READY);
  151 + verify(mockEvents).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
  152 +
  153 + videoPlayer.dispose();
  154 + }
  155 +
  156 + @Test
  157 + public void onSurfaceAvailableDoesNotSendInitializeEventAgain() {
  158 + // The VideoPlayer contract assumes that the event "initialized" is sent exactly once
  159 + // (duplicate events cause an error to be thrown at the shared Dart layer). This test verifies
  160 + // that the onInitialized event is sent exactly once per player.
  161 + //
  162 + // Regression test for https://github.com/flutter/flutter/issues/154602.
  163 + VideoPlayer videoPlayer = createVideoPlayer();
  164 + when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200));
  165 +
  166 + // Capture the lifecycle events so we can simulate onSurfaceAvailableDestroyed.
  167 + verify(mockProducer).setCallback(callbackCaptor.capture());
  168 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  169 +
  170 + // Trigger destroyed/available.
  171 + simulateSurfaceDestruction(producerLifecycle);
  172 + producerLifecycle.onSurfaceAvailable();
  173 +
  174 + // Initial listener, and the new one from the resume.
  175 + verify(mockExoPlayer, times(2)).addListener(listenerCaptor.capture());
  176 + Player.Listener listener = listenerCaptor.getValue();
  177 +
  178 + // Now trigger that same event, which would happen in the case of a background/resume.
  179 + listener.onPlaybackStateChanged(Player.STATE_READY);
  180 +
  181 + // Was not called because it was a result of a background/resume.
  182 + verify(mockEvents, never()).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
  183 +
  184 + videoPlayer.dispose();
  185 + }
  186 +
  187 + @Test
  188 + public void onSurfaceAvailableWithoutDestroyDoesNotRecreate() {
  189 + // Initially create the video player, which creates the initial surface.
  190 + VideoPlayer videoPlayer = createVideoPlayer();
  191 + verify(mockProducer).getSurface();
  192 + verify(mockProducer).handlesCropAndRotation();
  193 +
  194 + // Capture the lifecycle events so we can simulate onSurfaceAvailable/Destroyed.
  195 + verify(mockProducer).setCallback(callbackCaptor.capture());
  196 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  197 +
  198 + // Calling onSurfaceAvailable does not do anything, since the surface was never destroyed.
  199 + producerLifecycle.onSurfaceAvailable();
  200 + verifyNoMoreInteractions(mockProducer);
  201 +
  202 + videoPlayer.dispose();
  203 + }
  204 +
  205 + @Test
  206 + public void disposeReleasesExoPlayerBeforeTexture() {
  207 + VideoPlayer videoPlayer = createVideoPlayer();
  208 +
  209 + videoPlayer.dispose();
  210 +
  211 + // Regression test for https://github.com/flutter/flutter/issues/156158.
  212 + // The player must be destroyed before the surface it is writing to.
  213 + InOrder inOrder = inOrder(mockExoPlayer, mockProducer);
  214 + inOrder.verify(mockExoPlayer).release();
  215 + inOrder.verify(mockProducer).release();
  216 + }
  217 +
  218 + // TODO(matanlurey): Replace with inline calls to onSurfaceAvailable once
  219 + // available on stable; see https://github.com/flutter/flutter/issues/155131.
  220 + // This separate method only exists to scope the suppression.
  221 + @SuppressWarnings({"deprecation", "removal"})
  222 + void simulateSurfaceCreation(TextureRegistry.SurfaceProducer.Callback producerLifecycle) {
  223 + producerLifecycle.onSurfaceCreated();
  224 + }
  225 +
  226 + // TODO(bparrishMines): Replace with inline calls to onSurfaceCleanup once available on stable;
  227 + // see https://github.com/flutter/flutter/issues/16125. This separate method only exists to scope
  228 + // the suppression.
  229 + @SuppressWarnings({"deprecation", "removal"})
  230 + void simulateSurfaceDestruction(TextureRegistry.SurfaceProducer.Callback producerLifecycle) {
  231 + producerLifecycle.onSurfaceDestroyed();
  232 + }
  233 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoAssetTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.junit.Assert.assertThrows;
  9 +import static org.mockito.ArgumentMatchers.anyBoolean;
  10 +import static org.mockito.ArgumentMatchers.anyMap;
  11 +import static org.mockito.ArgumentMatchers.anyString;
  12 +import static org.mockito.Mockito.mock;
  13 +import static org.mockito.Mockito.never;
  14 +import static org.mockito.Mockito.verify;
  15 +import static org.mockito.Mockito.when;
  16 +
  17 +import android.net.Uri;
  18 +import androidx.media3.common.MediaItem;
  19 +import androidx.media3.datasource.DefaultHttpDataSource;
  20 +import androidx.media3.exoplayer.source.MediaSource;
  21 +import androidx.test.core.app.ApplicationProvider;
  22 +import java.util.HashMap;
  23 +import java.util.Map;
  24 +import java.util.Objects;
  25 +import org.junit.Test;
  26 +import org.junit.runner.RunWith;
  27 +import org.robolectric.RobolectricTestRunner;
  28 +
  29 +/**
  30 + * Unit tests for {@link VideoAsset}.
  31 + *
  32 + * <p>This test suite <em>narrowly verifies</em> that the {@link VideoAsset} factory methods, {@link
  33 + * VideoAsset#fromRemoteUrl(String, VideoAsset.StreamingFormat, Map)} and {@link
  34 + * VideoAsset#fromAssetUrl(String)} follow the contract they have documented.
  35 + *
  36 + * <p>In other tests of the player, a fake asset is likely to be used.
  37 + */
  38 +@RunWith(RobolectricTestRunner.class)
  39 +public final class VideoAssetTest {
  40 + @Test
  41 + public void localVideoRequiresAssetUrl() {
  42 + assertThrows(
  43 + IllegalArgumentException.class,
  44 + () -> VideoAsset.fromAssetUrl("https://not.local/video.mp4"));
  45 + }
  46 +
  47 + @Test
  48 + public void localVideoCreatesMediaItem() {
  49 + VideoAsset asset = VideoAsset.fromAssetUrl("asset:///asset-key");
  50 + MediaItem mediaItem = asset.getMediaItem();
  51 +
  52 + assert mediaItem.localConfiguration != null;
  53 + assertEquals(mediaItem.localConfiguration.uri, Uri.parse("asset:///asset-key"));
  54 + }
  55 +
  56 + private static DefaultHttpDataSource.Factory mockHttpFactory() {
  57 + DefaultHttpDataSource.Factory httpFactory = mock(DefaultHttpDataSource.Factory.class);
  58 + when(httpFactory.setUserAgent(anyString())).thenReturn(httpFactory);
  59 + when(httpFactory.setAllowCrossProtocolRedirects(anyBoolean())).thenReturn(httpFactory);
  60 + when(httpFactory.setDefaultRequestProperties(anyMap())).thenReturn(httpFactory);
  61 + return httpFactory;
  62 + }
  63 +
  64 + @Test
  65 + public void remoteVideoByDefaultSetsUserAgentAndCrossProtocolRedirects() {
  66 + VideoAsset asset =
  67 + VideoAsset.fromRemoteUrl(
  68 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, new HashMap<>());
  69 +
  70 + DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
  71 +
  72 + // Cast to HttpVideoAsset to call a testing-only method to intercept calls.
  73 + ((HttpVideoAsset) asset)
  74 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
  75 +
  76 + verify(mockFactory).setUserAgent("ExoPlayer");
  77 + verify(mockFactory).setAllowCrossProtocolRedirects(true);
  78 + verify(mockFactory, never()).setDefaultRequestProperties(anyMap());
  79 + }
  80 +
  81 + @Test
  82 + public void remoteVideoOverridesUserAgentIfProvided() {
  83 + Map<String, String> headers = new HashMap<>();
  84 + headers.put("User-Agent", "FantasticalVideoBot");
  85 +
  86 + VideoAsset asset =
  87 + VideoAsset.fromRemoteUrl(
  88 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, headers);
  89 +
  90 + DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
  91 +
  92 + // Cast to HttpVideoAsset to call a testing-only method to intercept calls.
  93 + ((HttpVideoAsset) asset)
  94 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
  95 +
  96 + verify(mockFactory).setUserAgent("FantasticalVideoBot");
  97 + verify(mockFactory).setAllowCrossProtocolRedirects(true);
  98 + verify(mockFactory).setDefaultRequestProperties(headers);
  99 + }
  100 +
  101 + // This tests that without using the overrides we get a working, non-mocked object.
  102 + //
  103 + // I guess you could also start a local HTTP server, and try fetching with it, YMMV.
  104 + @Test
  105 + public void remoteVideoGetMediaSourceFactoryInProductionReturnsRealMediaSource() {
  106 + VideoAsset asset =
  107 + VideoAsset.fromRemoteUrl(
  108 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, new HashMap<>());
  109 +
  110 + MediaSource source =
  111 + asset
  112 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext())
  113 + .createMediaSource(asset.getMediaItem());
  114 + assertEquals(
  115 + Uri.parse("https://flutter.dev/video.mp4"),
  116 + Objects.requireNonNull(source.getMediaItem().localConfiguration).uri);
  117 + }
  118 +
  119 + @Test
  120 + public void remoteVideoSetsAdditionalHttpHeadersIfProvided() {
  121 + Map<String, String> headers = new HashMap<>();
  122 + headers.put("X-Cache-Forever", "true");
  123 +
  124 + VideoAsset asset =
  125 + VideoAsset.fromRemoteUrl(
  126 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, headers);
  127 +
  128 + DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
  129 +
  130 + // Cast to HttpVideoAsset to call a testing-only method to intercept calls.
  131 + ((HttpVideoAsset) asset)
  132 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
  133 +
  134 + verify(mockFactory).setUserAgent("ExoPlayer");
  135 + verify(mockFactory).setAllowCrossProtocolRedirects(true);
  136 + verify(mockFactory).setDefaultRequestProperties(headers);
  137 + }
  138 +
  139 + @Test
  140 + public void rtspVideoRequiresRtspUrl() {
  141 + assertThrows(
  142 + IllegalArgumentException.class, () -> VideoAsset.fromRtspUrl("https://not.rtsp/video.mp4"));
  143 + }
  144 +
  145 + @Test
  146 + public void rtspVideoCreatesMediaItem() {
  147 + VideoAsset asset = VideoAsset.fromRtspUrl("rtsp://test:pass@flutter.dev/stream");
  148 + MediaItem mediaItem = asset.getMediaItem();
  149 +
  150 + assert mediaItem.localConfiguration != null;
  151 + assertEquals(
  152 + mediaItem.localConfiguration.uri, Uri.parse("rtsp://test:pass@flutter.dev/stream"));
  153 + }
  154 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.mockito.ArgumentMatchers.eq;
  9 +import static org.mockito.Mockito.verify;
  10 +
  11 +import java.util.Arrays;
  12 +import java.util.Collections;
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +import org.junit.Before;
  16 +import org.junit.Rule;
  17 +import org.junit.Test;
  18 +import org.junit.runner.RunWith;
  19 +import org.mockito.ArgumentCaptor;
  20 +import org.mockito.Captor;
  21 +import org.mockito.Mock;
  22 +import org.mockito.junit.MockitoJUnit;
  23 +import org.mockito.junit.MockitoRule;
  24 +import org.robolectric.RobolectricTestRunner;
  25 +
  26 +/**
  27 + * Unit tests {@link VideoPlayerEventCallbacks}.
  28 + *
  29 + * <p>This test suite <em>narrowly verifies</em> that calling the provided event callbacks, such as
  30 + * {@link VideoPlayerEventCallbacks#onBufferingUpdate(long)}, produces the expected data as an
  31 + * encoded {@link Map}.
  32 + *
  33 + * <p>In other words, this tests that "the Java-side of the event channel works as expected".
  34 + */
  35 +@RunWith(RobolectricTestRunner.class)
  36 +public final class VideoPlayerEventCallbacksTest {
  37 + private VideoPlayerEventCallbacks eventCallbacks;
  38 +
  39 + @Mock private QueuingEventSink mockEventSink;
  40 +
  41 + @Captor private ArgumentCaptor<Map<String, Object>> eventCaptor;
  42 +
  43 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  44 +
  45 + @Before
  46 + public void setUp() {
  47 + eventCallbacks = VideoPlayerEventCallbacks.withSink(mockEventSink);
  48 + }
  49 +
  50 + @Test
  51 + public void onInitializedSendsWidthHeightAndDuration() {
  52 + eventCallbacks.onInitialized(800, 400, 10L, 0);
  53 +
  54 + verify(mockEventSink).success(eventCaptor.capture());
  55 +
  56 + Map<String, Object> actual = eventCaptor.getValue();
  57 + Map<String, Object> expected = new HashMap<>();
  58 + expected.put("event", "initialized");
  59 + expected.put("duration", 10L);
  60 + expected.put("width", 800);
  61 + expected.put("height", 400);
  62 +
  63 + assertEquals(expected, actual);
  64 + }
  65 +
  66 + @Test
  67 + public void onInitializedIncludesRotationCorrectIfNonZero() {
  68 + eventCallbacks.onInitialized(800, 400, 10L, 180);
  69 +
  70 + verify(mockEventSink).success(eventCaptor.capture());
  71 +
  72 + Map<String, Object> actual = eventCaptor.getValue();
  73 + Map<String, Object> expected = new HashMap<>();
  74 + expected.put("event", "initialized");
  75 + expected.put("duration", 10L);
  76 + expected.put("width", 800);
  77 + expected.put("height", 400);
  78 + expected.put("rotationCorrection", 180);
  79 +
  80 + assertEquals(expected, actual);
  81 + }
  82 +
  83 + @Test
  84 + public void onBufferingStart() {
  85 + eventCallbacks.onBufferingStart();
  86 +
  87 + verify(mockEventSink).success(eventCaptor.capture());
  88 +
  89 + Map<String, Object> actual = eventCaptor.getValue();
  90 + Map<String, Object> expected = new HashMap<>();
  91 + expected.put("event", "bufferingStart");
  92 + assertEquals(expected, actual);
  93 + }
  94 +
  95 + @Test
  96 + public void onBufferingUpdateProvidesAListWithASingleRange() {
  97 + eventCallbacks.onBufferingUpdate(10L);
  98 +
  99 + verify(mockEventSink).success(eventCaptor.capture());
  100 +
  101 + Map<String, Object> actual = eventCaptor.getValue();
  102 + Map<String, Object> expected = new HashMap<>();
  103 + expected.put("event", "bufferingUpdate");
  104 + expected.put("values", Collections.singletonList(Arrays.asList(0, 10L)));
  105 + assertEquals(expected, actual);
  106 + }
  107 +
  108 + @Test
  109 + public void onBufferingEnd() {
  110 + eventCallbacks.onBufferingEnd();
  111 +
  112 + verify(mockEventSink).success(eventCaptor.capture());
  113 +
  114 + Map<String, Object> actual = eventCaptor.getValue();
  115 + Map<String, Object> expected = new HashMap<>();
  116 + expected.put("event", "bufferingEnd");
  117 + assertEquals(expected, actual);
  118 + }
  119 +
  120 + @Test
  121 + public void onCompleted() {
  122 + eventCallbacks.onCompleted();
  123 +
  124 + verify(mockEventSink).success(eventCaptor.capture());
  125 +
  126 + Map<String, Object> actual = eventCaptor.getValue();
  127 + Map<String, Object> expected = new HashMap<>();
  128 + expected.put("event", "completed");
  129 + assertEquals(expected, actual);
  130 + }
  131 +
  132 + @Test
  133 + public void onError() {
  134 + eventCallbacks.onError("code", "message", "details");
  135 +
  136 + verify(mockEventSink).error(eq("code"), eq("message"), eq("details"));
  137 + }
  138 +
  139 + @Test
  140 + public void onIsPlayingStateUpdate() {
  141 + eventCallbacks.onIsPlayingStateUpdate(true);
  142 +
  143 + verify(mockEventSink).success(eventCaptor.capture());
  144 +
  145 + Map<String, Object> actual = eventCaptor.getValue();
  146 + Map<String, Object> expected = new HashMap<>();
  147 + expected.put("event", "isPlayingStateUpdate");
  148 + expected.put("isPlaying", true);
  149 + assertEquals(expected, actual);
  150 + }
  151 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.*;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import android.content.Context;
  11 +import android.util.LongSparseArray;
  12 +import io.flutter.embedding.engine.plugins.FlutterPlugin;
  13 +import io.flutter.plugin.platform.PlatformViewRegistry;
  14 +import io.flutter.plugins.videoplayer.Messages.CreateMessage;
  15 +import io.flutter.plugins.videoplayer.Messages.PlatformVideoViewType;
  16 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory;
  17 +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer;
  18 +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
  19 +import io.flutter.view.TextureRegistry;
  20 +import java.lang.reflect.Field;
  21 +import java.util.HashMap;
  22 +import org.junit.Before;
  23 +import org.junit.Test;
  24 +import org.junit.runner.RunWith;
  25 +import org.mockito.Mock;
  26 +import org.mockito.MockedStatic;
  27 +import org.mockito.MockitoAnnotations;
  28 +import org.robolectric.RobolectricTestRunner;
  29 +
  30 +@RunWith(RobolectricTestRunner.class)
  31 +public class VideoPlayerPluginTest {
  32 + @Mock private TextureRegistry mockTextureRegistry;
  33 + @Mock private TextureRegistry.SurfaceProducer mockSurfaceProducer;
  34 + @Mock private PlatformViewRegistry mockPlatformViewRegistry;
  35 + private VideoPlayerPlugin plugin;
  36 +
  37 + @Before
  38 + public void setUp() {
  39 + MockitoAnnotations.openMocks(this);
  40 + when(mockTextureRegistry.createSurfaceProducer()).thenReturn(mockSurfaceProducer);
  41 +
  42 + FlutterPlugin.FlutterPluginBinding binding = mock(FlutterPlugin.FlutterPluginBinding.class);
  43 + when(binding.getApplicationContext()).thenReturn(mock(Context.class));
  44 + when(binding.getTextureRegistry()).thenReturn(mockTextureRegistry);
  45 + when(binding.getBinaryMessenger())
  46 + .thenReturn(mock(io.flutter.plugin.common.BinaryMessenger.class));
  47 + when(binding.getPlatformViewRegistry()).thenReturn(mockPlatformViewRegistry);
  48 +
  49 + plugin = new VideoPlayerPlugin();
  50 + plugin.onAttachedToEngine(binding);
  51 + }
  52 +
  53 + @SuppressWarnings("unchecked")
  54 + private LongSparseArray<VideoPlayer> getVideoPlayers() throws Exception {
  55 + final Field field = VideoPlayerPlugin.class.getDeclaredField("videoPlayers");
  56 + field.setAccessible(true);
  57 + return (LongSparseArray<VideoPlayer>) field.get(plugin);
  58 + }
  59 +
  60 + // This is only a placeholder test and doesn't actually initialize the plugin.
  61 + @Test
  62 + public void initPluginDoesNotThrow() {
  63 + final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
  64 + }
  65 +
  66 + @Test
  67 + public void registersPlatformVideoViewFactory() {
  68 + verify(mockPlatformViewRegistry)
  69 + .registerViewFactory(
  70 + eq("plugins.flutter.dev/video_player_android"), any(PlatformVideoViewFactory.class));
  71 + }
  72 +
  73 + @Test
  74 + public void createsPlatformViewVideoPlayer() throws Exception {
  75 + try (MockedStatic<PlatformViewVideoPlayer> mockedPlatformViewVideoPlayerStatic =
  76 + mockStatic(PlatformViewVideoPlayer.class)) {
  77 + mockedPlatformViewVideoPlayerStatic
  78 + .when(() -> PlatformViewVideoPlayer.create(any(), any(), any(), any()))
  79 + .thenReturn(mock(PlatformViewVideoPlayer.class));
  80 +
  81 + final CreateMessage createMessage =
  82 + new CreateMessage.Builder()
  83 + .setViewType(PlatformVideoViewType.PLATFORM_VIEW)
  84 + .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")
  85 + .setHttpHeaders(new HashMap<>())
  86 + .build();
  87 +
  88 + final long playerId = plugin.create(createMessage);
  89 +
  90 + final LongSparseArray<VideoPlayer> videoPlayers = getVideoPlayers();
  91 + assertNotNull(videoPlayers.get(playerId));
  92 + }
  93 + }
  94 +
  95 + @Test
  96 + public void createsTextureVideoPlayer() throws Exception {
  97 + try (MockedStatic<TextureVideoPlayer> mockedTextureVideoPlayerStatic =
  98 + mockStatic(TextureVideoPlayer.class)) {
  99 + mockedTextureVideoPlayerStatic
  100 + .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any()))
  101 + .thenReturn(mock(TextureVideoPlayer.class));
  102 +
  103 + final CreateMessage createMessage =
  104 + new CreateMessage.Builder()
  105 + .setViewType(PlatformVideoViewType.TEXTURE_VIEW)
  106 + .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")
  107 + .setHttpHeaders(new HashMap<>())
  108 + .build();
  109 +
  110 + final long playerId = plugin.create(createMessage);
  111 +
  112 + final LongSparseArray<VideoPlayer> videoPlayers = getVideoPlayers();
  113 + assertTrue(videoPlayers.get(playerId) instanceof TextureVideoPlayer);
  114 + }
  115 + }
  116 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import androidx.annotation.NonNull;
  11 +import androidx.annotation.Nullable;
  12 +import androidx.media3.common.AudioAttributes;
  13 +import androidx.media3.common.C;
  14 +import androidx.media3.common.Format;
  15 +import androidx.media3.common.MediaItem;
  16 +import androidx.media3.common.PlaybackParameters;
  17 +import androidx.media3.common.Player;
  18 +import androidx.media3.exoplayer.ExoPlayer;
  19 +import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener;
  20 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  21 +import org.junit.Before;
  22 +import org.junit.Rule;
  23 +import org.junit.Test;
  24 +import org.junit.runner.RunWith;
  25 +import org.mockito.ArgumentCaptor;
  26 +import org.mockito.Captor;
  27 +import org.mockito.Mock;
  28 +import org.mockito.junit.MockitoJUnit;
  29 +import org.mockito.junit.MockitoRule;
  30 +import org.robolectric.RobolectricTestRunner;
  31 +
  32 +/**
  33 + * Unit tests for {@link VideoPlayer}.
  34 + *
  35 + * <p>This test suite <em>narrowly verifies</em> that {@link VideoPlayer} interfaces with the {@link
  36 + * ExoPlayer} interface <em>exactly</em> as it did when the test suite was created. That is, if the
  37 + * behavior changes, this test will need to change. However, this suite should catch bugs related to
  38 + * <em>"this is a safe refactor with no behavior changes"</em>.
  39 + *
  40 + * <p>It's hypothetically possible to write better tests using {@link
  41 + * androidx.media3.test.utils.FakeMediaSource}, but you really need a PhD in the Android media APIs
  42 + * in order to figure out how to set everything up so the player "works".
  43 + */
  44 +@RunWith(RobolectricTestRunner.class)
  45 +public final class VideoPlayerTest {
  46 + private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4";
  47 + private FakeVideoAsset fakeVideoAsset;
  48 +
  49 + @Mock private VideoPlayerCallbacks mockEvents;
  50 + @Mock private ExoPlayer mockExoPlayer;
  51 + @Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
  52 + @Captor private ArgumentCaptor<Player.Listener> listenerCaptor;
  53 +
  54 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  55 +
  56 + /** A test subclass of {@link VideoPlayer} that exposes the abstract class for testing. */
  57 + private final class TestVideoPlayer extends VideoPlayer {
  58 + private TestVideoPlayer(
  59 + @NonNull VideoPlayerCallbacks events,
  60 + @NonNull MediaItem mediaItem,
  61 + @NonNull VideoPlayerOptions options,
  62 + @Nullable SurfaceProducer surfaceProducer,
  63 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  64 + super(events, mediaItem, options, surfaceProducer, exoPlayerProvider);
  65 + }
  66 +
  67 + @NonNull
  68 + @Override
  69 + protected ExoPlayerEventListener createExoPlayerEventListener(
  70 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
  71 + // Use platform view implementation for testing.
  72 + return new PlatformViewExoPlayerEventListener(exoPlayer, mockEvents);
  73 + }
  74 + }
  75 +
  76 + @Before
  77 + public void setUp() {
  78 + fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
  79 + }
  80 +
  81 + private VideoPlayer createVideoPlayer() {
  82 + return createVideoPlayer(new VideoPlayerOptions());
  83 + }
  84 +
  85 + private VideoPlayer createVideoPlayer(VideoPlayerOptions options) {
  86 + return new TestVideoPlayer(
  87 + mockEvents, fakeVideoAsset.getMediaItem(), options, null, () -> mockExoPlayer);
  88 + }
  89 +
  90 + @Test
  91 + public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {
  92 + VideoPlayer videoPlayer = createVideoPlayer();
  93 +
  94 + verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem());
  95 + verify(mockExoPlayer).prepare();
  96 +
  97 + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true));
  98 + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE);
  99 +
  100 + videoPlayer.dispose();
  101 + }
  102 +
  103 + @Test
  104 + public void loadsAndPreparesProvidedMediaDisablesAudioFocusWhenMixModeSet() {
  105 + VideoPlayerOptions options = new VideoPlayerOptions();
  106 + options.mixWithOthers = true;
  107 +
  108 + VideoPlayer videoPlayer = createVideoPlayer(options);
  109 +
  110 + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(false));
  111 + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE);
  112 +
  113 + videoPlayer.dispose();
  114 + }
  115 +
  116 + @Test
  117 + public void playsAndPausesProvidedMedia() {
  118 + VideoPlayer videoPlayer = createVideoPlayer();
  119 +
  120 + videoPlayer.play();
  121 + verify(mockExoPlayer).play();
  122 +
  123 + videoPlayer.pause();
  124 + verify(mockExoPlayer).pause();
  125 +
  126 + videoPlayer.dispose();
  127 + }
  128 +
  129 + @Test
  130 + public void sendsBufferingUpdatesOnDemand() {
  131 + VideoPlayer videoPlayer = createVideoPlayer();
  132 +
  133 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  134 + videoPlayer.sendBufferingUpdate();
  135 + verify(mockEvents).onBufferingUpdate(10L);
  136 +
  137 + videoPlayer.dispose();
  138 + }
  139 +
  140 + @Test
  141 + public void togglesLoopingEnablesAndDisablesRepeatMode() {
  142 + VideoPlayer videoPlayer = createVideoPlayer();
  143 +
  144 + videoPlayer.setLooping(true);
  145 + verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);
  146 +
  147 + videoPlayer.setLooping(false);
  148 + verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_OFF);
  149 +
  150 + videoPlayer.dispose();
  151 + }
  152 +
  153 + @Test
  154 + public void setVolumeIsClampedBetween0and1() {
  155 + VideoPlayer videoPlayer = createVideoPlayer();
  156 +
  157 + videoPlayer.setVolume(-1.0);
  158 + verify(mockExoPlayer).setVolume(0f);
  159 +
  160 + videoPlayer.setVolume(2.0);
  161 + verify(mockExoPlayer).setVolume(1f);
  162 +
  163 + videoPlayer.setVolume(0.5);
  164 + verify(mockExoPlayer).setVolume(0.5f);
  165 +
  166 + videoPlayer.dispose();
  167 + }
  168 +
  169 + @Test
  170 + public void setPlaybackSpeedSetsPlaybackParametersWithValue() {
  171 + VideoPlayer videoPlayer = createVideoPlayer();
  172 +
  173 + videoPlayer.setPlaybackSpeed(2.5);
  174 + verify(mockExoPlayer).setPlaybackParameters(new PlaybackParameters(2.5f));
  175 +
  176 + videoPlayer.dispose();
  177 + }
  178 +
  179 + @Test
  180 + public void seekAndGetPosition() {
  181 + VideoPlayer videoPlayer = createVideoPlayer();
  182 +
  183 + videoPlayer.seekTo(10);
  184 + verify(mockExoPlayer).seekTo(10);
  185 +
  186 + when(mockExoPlayer.getCurrentPosition()).thenReturn(20L);
  187 + assertEquals(20L, videoPlayer.getPosition());
  188 + }
  189 +
  190 + @Test
  191 + public void onInitializedCalledWhenVideoPlayerInitiallyAvailable() {
  192 + VideoPlayer videoPlayer = createVideoPlayer();
  193 +
  194 + // Pretend we have a video, and capture the registered event listener.
  195 + when(mockExoPlayer.getVideoFormat())
  196 + .thenReturn(
  197 + new Format.Builder().setWidth(300).setHeight(200).setRotationDegrees(0).build());
  198 + verify(mockExoPlayer).addListener(listenerCaptor.capture());
  199 + Player.Listener listener = listenerCaptor.getValue();
  200 +
  201 + // Trigger an event that would trigger onInitialized.
  202 + listener.onPlaybackStateChanged(Player.STATE_READY);
  203 + verify(mockEvents).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
  204 +
  205 + videoPlayer.dispose();
  206 + }
  207 +
  208 + @Test
  209 + public void disposeReleasesExoPlayer() {
  210 + VideoPlayer videoPlayer = createVideoPlayer();
  211 +
  212 + videoPlayer.dispose();
  213 +
  214 + verify(mockExoPlayer).release();
  215 + }
  216 +}
... ...
packages/video_player_android/lib/src/android_video_player.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +import 'dart:async';
  6 +
  7 +import 'package:flutter/services.dart';
  8 +import 'package:flutter/widgets.dart';
  9 +import 'package:video_player_platform_interface/video_player_platform_interface.dart';
  10 +
  11 +import 'messages.g.dart';
  12 +import 'platform_view_player.dart';
  13 +
  14 +/// An Android implementation of [VideoPlayerPlatform] that uses the
  15 +/// Pigeon-generated [VideoPlayerApi].
  16 +class AndroidVideoPlayer extends VideoPlayerPlatform {
  17 + final AndroidVideoPlayerApi _api = AndroidVideoPlayerApi();
  18 +
  19 + /// A map that associates player ID with a view state.
  20 + /// This is used to determine which view type to use when building a view.
  21 + final Map<int, _VideoPlayerViewState> _playerViewStates =
  22 + <int, _VideoPlayerViewState>{};
  23 +
  24 + /// Registers this class as the default instance of [PathProviderPlatform].
  25 + static void registerWith() {
  26 + VideoPlayerPlatform.instance = AndroidVideoPlayer();
  27 + }
  28 +
  29 + @override
  30 + Future<void> init() {
  31 + return _api.initialize();
  32 + }
  33 +
  34 + @override
  35 + Future<void> dispose(int playerId) async {
  36 + await _api.dispose(playerId);
  37 + _playerViewStates.remove(playerId);
  38 + }
  39 +
  40 + @override
  41 + Future<int?> create(DataSource dataSource) {
  42 + return createWithOptions(
  43 + VideoCreationOptions(
  44 + dataSource: dataSource,
  45 + // Compatibility; "create" is always a textureView (createWithOptions
  46 + // allows selecting).
  47 + viewType: VideoViewType.textureView,
  48 + ),
  49 + );
  50 + }
  51 +
  52 + @override
  53 + Future<int?> createWithOptions(VideoCreationOptions options) async {
  54 + final DataSource dataSource = options.dataSource;
  55 +
  56 + String? asset;
  57 + String? packageName;
  58 + String? uri;
  59 + String? formatHint;
  60 + Map<String, String> httpHeaders = <String, String>{};
  61 + switch (dataSource.sourceType) {
  62 + case DataSourceType.asset:
  63 + asset = dataSource.asset;
  64 + packageName = dataSource.package;
  65 + case DataSourceType.network:
  66 + uri = dataSource.uri;
  67 + formatHint = _videoFormatStringMap[dataSource.formatHint];
  68 + httpHeaders = dataSource.httpHeaders;
  69 + case DataSourceType.file:
  70 + uri = dataSource.uri;
  71 + httpHeaders = dataSource.httpHeaders;
  72 + case DataSourceType.contentUri:
  73 + uri = dataSource.uri;
  74 + }
  75 + final CreateMessage message = CreateMessage(
  76 + asset: asset,
  77 + packageName: packageName,
  78 + uri: uri,
  79 + httpHeaders: httpHeaders,
  80 + formatHint: formatHint,
  81 + viewType: _platformVideoViewTypeFromVideoViewType(options.viewType),
  82 + );
  83 +
  84 + final int playerId = await _api.create(message);
  85 + _playerViewStates[playerId] = switch (options.viewType) {
  86 + // playerId is also the textureId when using texture view.
  87 + VideoViewType.textureView =>
  88 + _VideoPlayerTextureViewState(textureId: playerId),
  89 + VideoViewType.platformView => const _VideoPlayerPlatformViewState(),
  90 + };
  91 +
  92 + return playerId;
  93 + }
  94 +
  95 + @override
  96 + Future<void> setLooping(int playerId, bool looping) {
  97 + return _api.setLooping(playerId, looping);
  98 + }
  99 +
  100 + @override
  101 + Future<void> play(int playerId) {
  102 + return _api.play(playerId);
  103 + }
  104 +
  105 + @override
  106 + Future<void> pause(int playerId) {
  107 + return _api.pause(playerId);
  108 + }
  109 +
  110 + @override
  111 + Future<void> setVolume(int playerId, double volume) {
  112 + return _api.setVolume(playerId, volume);
  113 + }
  114 +
  115 + @override
  116 + Future<void> setPlaybackSpeed(int playerId, double speed) {
  117 + assert(speed > 0);
  118 +
  119 + return _api.setPlaybackSpeed(playerId, speed);
  120 + }
  121 +
  122 + @override
  123 + Future<void> seekTo(int playerId, Duration position) {
  124 + return _api.seekTo(playerId, position.inMilliseconds);
  125 + }
  126 +
  127 + @override
  128 + Future<Duration> getPosition(int playerId) async {
  129 + final int position = await _api.position(playerId);
  130 + return Duration(milliseconds: position);
  131 + }
  132 +
  133 + @override
  134 + Stream<VideoEvent> videoEventsFor(int playerId) {
  135 + return _eventChannelFor(playerId)
  136 + .receiveBroadcastStream()
  137 + .map((dynamic event) {
  138 + final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
  139 + switch (map['event']) {
  140 + case 'initialized':
  141 + return VideoEvent(
  142 + eventType: VideoEventType.initialized,
  143 + duration: Duration(milliseconds: map['duration'] as int),
  144 + size: Size((map['width'] as num?)?.toDouble() ?? 0.0,
  145 + (map['height'] as num?)?.toDouble() ?? 0.0),
  146 + rotationCorrection: map['rotationCorrection'] as int? ?? 0,
  147 + );
  148 + case 'completed':
  149 + return VideoEvent(
  150 + eventType: VideoEventType.completed,
  151 + );
  152 + case 'bufferingUpdate':
  153 + final List<dynamic> values = map['values'] as List<dynamic>;
  154 +
  155 + return VideoEvent(
  156 + buffered: values.map<DurationRange>(_toDurationRange).toList(),
  157 + eventType: VideoEventType.bufferingUpdate,
  158 + );
  159 + case 'bufferingStart':
  160 + return VideoEvent(eventType: VideoEventType.bufferingStart);
  161 + case 'bufferingEnd':
  162 + return VideoEvent(eventType: VideoEventType.bufferingEnd);
  163 + case 'isPlayingStateUpdate':
  164 + return VideoEvent(
  165 + eventType: VideoEventType.isPlayingStateUpdate,
  166 + isPlaying: map['isPlaying'] as bool,
  167 + );
  168 + default:
  169 + return VideoEvent(eventType: VideoEventType.unknown);
  170 + }
  171 + });
  172 + }
  173 +
  174 + @override
  175 + Widget buildView(int playerId) {
  176 + return buildViewWithOptions(
  177 + VideoViewOptions(playerId: playerId),
  178 + );
  179 + }
  180 +
  181 + @override
  182 + Widget buildViewWithOptions(VideoViewOptions options) {
  183 + final int playerId = options.playerId;
  184 + final _VideoPlayerViewState? viewState = _playerViewStates[playerId];
  185 +
  186 + return switch (viewState) {
  187 + _VideoPlayerTextureViewState(:final int textureId) =>
  188 + Texture(textureId: textureId),
  189 + _VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId),
  190 + null => throw Exception(
  191 + 'Could not find corresponding view type for playerId: $playerId',
  192 + ),
  193 + };
  194 + }
  195 +
  196 + @override
  197 + Future<void> setMixWithOthers(bool mixWithOthers) {
  198 + return _api.setMixWithOthers(mixWithOthers);
  199 + }
  200 +
  201 + EventChannel _eventChannelFor(int playerId) {
  202 + return EventChannel('flutter.io/videoPlayer/videoEvents$playerId');
  203 + }
  204 +
  205 + static const Map<VideoFormat, String> _videoFormatStringMap =
  206 + <VideoFormat, String>{
  207 + VideoFormat.ss: 'ss',
  208 + VideoFormat.hls: 'hls',
  209 + VideoFormat.dash: 'dash',
  210 + VideoFormat.other: 'other',
  211 + };
  212 +
  213 + DurationRange _toDurationRange(dynamic value) {
  214 + final List<dynamic> pair = value as List<dynamic>;
  215 + return DurationRange(
  216 + Duration(milliseconds: pair[0] as int),
  217 + Duration(milliseconds: pair[1] as int),
  218 + );
  219 + }
  220 +}
  221 +
  222 +PlatformVideoViewType _platformVideoViewTypeFromVideoViewType(
  223 + VideoViewType viewType,
  224 +) {
  225 + return switch (viewType) {
  226 + VideoViewType.textureView => PlatformVideoViewType.textureView,
  227 + VideoViewType.platformView => PlatformVideoViewType.platformView,
  228 + };
  229 +}
  230 +
  231 +/// Base class representing the state of a video player view.
  232 +@immutable
  233 +sealed class _VideoPlayerViewState {
  234 + const _VideoPlayerViewState();
  235 +}
  236 +
  237 +/// Represents the state of a video player view that uses a texture.
  238 +final class _VideoPlayerTextureViewState extends _VideoPlayerViewState {
  239 + /// Creates a new instance of [_VideoPlayerTextureViewState].
  240 + const _VideoPlayerTextureViewState({
  241 + required this.textureId,
  242 + });
  243 +
  244 + /// The ID of the texture used by the video player.
  245 + final int textureId;
  246 +}
  247 +
  248 +/// Represents the state of a video player view that uses a platform view.
  249 +final class _VideoPlayerPlatformViewState extends _VideoPlayerViewState {
  250 + /// Creates a new instance of [_VideoPlayerPlatformViewState].
  251 + const _VideoPlayerPlatformViewState();
  252 +}
... ...
packages/video_player_android/lib/src/messages.g.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +// Autogenerated from Pigeon (v22.6.1), do not edit directly.
  5 +// See also: https://pub.dev/packages/pigeon
  6 +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
  7 +
  8 +import 'dart:async';
  9 +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
  10 +
  11 +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
  12 +import 'package:flutter/services.dart';
  13 +
  14 +PlatformException _createConnectionError(String channelName) {
  15 + return PlatformException(
  16 + code: 'channel-error',
  17 + message: 'Unable to establish connection on channel: "$channelName".',
  18 + );
  19 +}
  20 +
  21 +List<Object?> wrapResponse(
  22 + {Object? result, PlatformException? error, bool empty = false}) {
  23 + if (empty) {
  24 + return <Object?>[];
  25 + }
  26 + if (error == null) {
  27 + return <Object?>[result];
  28 + }
  29 + return <Object?>[error.code, error.message, error.details];
  30 +}
  31 +
  32 +/// Pigeon equivalent of VideoViewType.
  33 +enum PlatformVideoViewType {
  34 + textureView,
  35 + platformView,
  36 +}
  37 +
  38 +/// Information passed to the platform view creation.
  39 +class PlatformVideoViewCreationParams {
  40 + PlatformVideoViewCreationParams({
  41 + required this.playerId,
  42 + });
  43 +
  44 + int playerId;
  45 +
  46 + Object encode() {
  47 + return <Object?>[
  48 + playerId,
  49 + ];
  50 + }
  51 +
  52 + static PlatformVideoViewCreationParams decode(Object result) {
  53 + result as List<Object?>;
  54 + return PlatformVideoViewCreationParams(
  55 + playerId: result[0]! as int,
  56 + );
  57 + }
  58 +}
  59 +
  60 +class CreateMessage {
  61 + CreateMessage({
  62 + this.asset,
  63 + this.uri,
  64 + this.packageName,
  65 + this.formatHint,
  66 + required this.httpHeaders,
  67 + this.viewType,
  68 + });
  69 +
  70 + String? asset;
  71 +
  72 + String? uri;
  73 +
  74 + String? packageName;
  75 +
  76 + String? formatHint;
  77 +
  78 + Map<String, String> httpHeaders;
  79 +
  80 + PlatformVideoViewType? viewType;
  81 +
  82 + Object encode() {
  83 + return <Object?>[
  84 + asset,
  85 + uri,
  86 + packageName,
  87 + formatHint,
  88 + httpHeaders,
  89 + viewType,
  90 + ];
  91 + }
  92 +
  93 + static CreateMessage decode(Object result) {
  94 + result as List<Object?>;
  95 + return CreateMessage(
  96 + asset: result[0] as String?,
  97 + uri: result[1] as String?,
  98 + packageName: result[2] as String?,
  99 + formatHint: result[3] as String?,
  100 + httpHeaders:
  101 + (result[4] as Map<Object?, Object?>?)!.cast<String, String>(),
  102 + viewType: result[5] as PlatformVideoViewType?,
  103 + );
  104 + }
  105 +}
  106 +
  107 +class _PigeonCodec extends StandardMessageCodec {
  108 + const _PigeonCodec();
  109 + @override
  110 + void writeValue(WriteBuffer buffer, Object? value) {
  111 + if (value is int) {
  112 + buffer.putUint8(4);
  113 + buffer.putInt64(value);
  114 + } else if (value is PlatformVideoViewType) {
  115 + buffer.putUint8(129);
  116 + writeValue(buffer, value.index);
  117 + } else if (value is PlatformVideoViewCreationParams) {
  118 + buffer.putUint8(130);
  119 + writeValue(buffer, value.encode());
  120 + } else if (value is CreateMessage) {
  121 + buffer.putUint8(131);
  122 + writeValue(buffer, value.encode());
  123 + } else {
  124 + super.writeValue(buffer, value);
  125 + }
  126 + }
  127 +
  128 + @override
  129 + Object? readValueOfType(int type, ReadBuffer buffer) {
  130 + switch (type) {
  131 + case 129:
  132 + final int? value = readValue(buffer) as int?;
  133 + return value == null ? null : PlatformVideoViewType.values[value];
  134 + case 130:
  135 + return PlatformVideoViewCreationParams.decode(readValue(buffer)!);
  136 + case 131:
  137 + return CreateMessage.decode(readValue(buffer)!);
  138 + default:
  139 + return super.readValueOfType(type, buffer);
  140 + }
  141 + }
  142 +}
  143 +
  144 +class AndroidVideoPlayerApi {
  145 + /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is
  146 + /// available for dependency injection. If it is left null, the default
  147 + /// BinaryMessenger will be used which routes to the host platform.
  148 + AndroidVideoPlayerApi(
  149 + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
  150 + : pigeonVar_binaryMessenger = binaryMessenger,
  151 + pigeonVar_messageChannelSuffix =
  152 + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  153 + final BinaryMessenger? pigeonVar_binaryMessenger;
  154 +
  155 + static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
  156 +
  157 + final String pigeonVar_messageChannelSuffix;
  158 +
  159 + Future<void> initialize() async {
  160 + final String pigeonVar_channelName =
  161 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix';
  162 + final BasicMessageChannel<Object?> pigeonVar_channel =
  163 + BasicMessageChannel<Object?>(
  164 + pigeonVar_channelName,
  165 + pigeonChannelCodec,
  166 + binaryMessenger: pigeonVar_binaryMessenger,
  167 + );
  168 + final List<Object?>? pigeonVar_replyList =
  169 + await pigeonVar_channel.send(null) as List<Object?>?;
  170 + if (pigeonVar_replyList == null) {
  171 + throw _createConnectionError(pigeonVar_channelName);
  172 + } else if (pigeonVar_replyList.length > 1) {
  173 + throw PlatformException(
  174 + code: pigeonVar_replyList[0]! as String,
  175 + message: pigeonVar_replyList[1] as String?,
  176 + details: pigeonVar_replyList[2],
  177 + );
  178 + } else {
  179 + return;
  180 + }
  181 + }
  182 +
  183 + Future<int> create(CreateMessage msg) async {
  184 + final String pigeonVar_channelName =
  185 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.create$pigeonVar_messageChannelSuffix';
  186 + final BasicMessageChannel<Object?> pigeonVar_channel =
  187 + BasicMessageChannel<Object?>(
  188 + pigeonVar_channelName,
  189 + pigeonChannelCodec,
  190 + binaryMessenger: pigeonVar_binaryMessenger,
  191 + );
  192 + final List<Object?>? pigeonVar_replyList =
  193 + await pigeonVar_channel.send(<Object?>[msg]) as List<Object?>?;
  194 + if (pigeonVar_replyList == null) {
  195 + throw _createConnectionError(pigeonVar_channelName);
  196 + } else if (pigeonVar_replyList.length > 1) {
  197 + throw PlatformException(
  198 + code: pigeonVar_replyList[0]! as String,
  199 + message: pigeonVar_replyList[1] as String?,
  200 + details: pigeonVar_replyList[2],
  201 + );
  202 + } else if (pigeonVar_replyList[0] == null) {
  203 + throw PlatformException(
  204 + code: 'null-error',
  205 + message: 'Host platform returned null value for non-null return value.',
  206 + );
  207 + } else {
  208 + return (pigeonVar_replyList[0] as int?)!;
  209 + }
  210 + }
  211 +
  212 + Future<void> dispose(int playerId) async {
  213 + final String pigeonVar_channelName =
  214 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix';
  215 + final BasicMessageChannel<Object?> pigeonVar_channel =
  216 + BasicMessageChannel<Object?>(
  217 + pigeonVar_channelName,
  218 + pigeonChannelCodec,
  219 + binaryMessenger: pigeonVar_binaryMessenger,
  220 + );
  221 + final List<Object?>? pigeonVar_replyList =
  222 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  223 + if (pigeonVar_replyList == null) {
  224 + throw _createConnectionError(pigeonVar_channelName);
  225 + } else if (pigeonVar_replyList.length > 1) {
  226 + throw PlatformException(
  227 + code: pigeonVar_replyList[0]! as String,
  228 + message: pigeonVar_replyList[1] as String?,
  229 + details: pigeonVar_replyList[2],
  230 + );
  231 + } else {
  232 + return;
  233 + }
  234 + }
  235 +
  236 + Future<void> setLooping(int playerId, bool looping) async {
  237 + final String pigeonVar_channelName =
  238 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping$pigeonVar_messageChannelSuffix';
  239 + final BasicMessageChannel<Object?> pigeonVar_channel =
  240 + BasicMessageChannel<Object?>(
  241 + pigeonVar_channelName,
  242 + pigeonChannelCodec,
  243 + binaryMessenger: pigeonVar_binaryMessenger,
  244 + );
  245 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  246 + .send(<Object?>[playerId, looping]) as List<Object?>?;
  247 + if (pigeonVar_replyList == null) {
  248 + throw _createConnectionError(pigeonVar_channelName);
  249 + } else if (pigeonVar_replyList.length > 1) {
  250 + throw PlatformException(
  251 + code: pigeonVar_replyList[0]! as String,
  252 + message: pigeonVar_replyList[1] as String?,
  253 + details: pigeonVar_replyList[2],
  254 + );
  255 + } else {
  256 + return;
  257 + }
  258 + }
  259 +
  260 + Future<void> setVolume(int playerId, double volume) async {
  261 + final String pigeonVar_channelName =
  262 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume$pigeonVar_messageChannelSuffix';
  263 + final BasicMessageChannel<Object?> pigeonVar_channel =
  264 + BasicMessageChannel<Object?>(
  265 + pigeonVar_channelName,
  266 + pigeonChannelCodec,
  267 + binaryMessenger: pigeonVar_binaryMessenger,
  268 + );
  269 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  270 + .send(<Object?>[playerId, volume]) as List<Object?>?;
  271 + if (pigeonVar_replyList == null) {
  272 + throw _createConnectionError(pigeonVar_channelName);
  273 + } else if (pigeonVar_replyList.length > 1) {
  274 + throw PlatformException(
  275 + code: pigeonVar_replyList[0]! as String,
  276 + message: pigeonVar_replyList[1] as String?,
  277 + details: pigeonVar_replyList[2],
  278 + );
  279 + } else {
  280 + return;
  281 + }
  282 + }
  283 +
  284 + Future<void> setPlaybackSpeed(int playerId, double speed) async {
  285 + final String pigeonVar_channelName =
  286 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix';
  287 + final BasicMessageChannel<Object?> pigeonVar_channel =
  288 + BasicMessageChannel<Object?>(
  289 + pigeonVar_channelName,
  290 + pigeonChannelCodec,
  291 + binaryMessenger: pigeonVar_binaryMessenger,
  292 + );
  293 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  294 + .send(<Object?>[playerId, speed]) as List<Object?>?;
  295 + if (pigeonVar_replyList == null) {
  296 + throw _createConnectionError(pigeonVar_channelName);
  297 + } else if (pigeonVar_replyList.length > 1) {
  298 + throw PlatformException(
  299 + code: pigeonVar_replyList[0]! as String,
  300 + message: pigeonVar_replyList[1] as String?,
  301 + details: pigeonVar_replyList[2],
  302 + );
  303 + } else {
  304 + return;
  305 + }
  306 + }
  307 +
  308 + Future<void> play(int playerId) async {
  309 + final String pigeonVar_channelName =
  310 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play$pigeonVar_messageChannelSuffix';
  311 + final BasicMessageChannel<Object?> pigeonVar_channel =
  312 + BasicMessageChannel<Object?>(
  313 + pigeonVar_channelName,
  314 + pigeonChannelCodec,
  315 + binaryMessenger: pigeonVar_binaryMessenger,
  316 + );
  317 + final List<Object?>? pigeonVar_replyList =
  318 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  319 + if (pigeonVar_replyList == null) {
  320 + throw _createConnectionError(pigeonVar_channelName);
  321 + } else if (pigeonVar_replyList.length > 1) {
  322 + throw PlatformException(
  323 + code: pigeonVar_replyList[0]! as String,
  324 + message: pigeonVar_replyList[1] as String?,
  325 + details: pigeonVar_replyList[2],
  326 + );
  327 + } else {
  328 + return;
  329 + }
  330 + }
  331 +
  332 + Future<int> position(int playerId) async {
  333 + final String pigeonVar_channelName =
  334 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position$pigeonVar_messageChannelSuffix';
  335 + final BasicMessageChannel<Object?> pigeonVar_channel =
  336 + BasicMessageChannel<Object?>(
  337 + pigeonVar_channelName,
  338 + pigeonChannelCodec,
  339 + binaryMessenger: pigeonVar_binaryMessenger,
  340 + );
  341 + final List<Object?>? pigeonVar_replyList =
  342 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  343 + if (pigeonVar_replyList == null) {
  344 + throw _createConnectionError(pigeonVar_channelName);
  345 + } else if (pigeonVar_replyList.length > 1) {
  346 + throw PlatformException(
  347 + code: pigeonVar_replyList[0]! as String,
  348 + message: pigeonVar_replyList[1] as String?,
  349 + details: pigeonVar_replyList[2],
  350 + );
  351 + } else if (pigeonVar_replyList[0] == null) {
  352 + throw PlatformException(
  353 + code: 'null-error',
  354 + message: 'Host platform returned null value for non-null return value.',
  355 + );
  356 + } else {
  357 + return (pigeonVar_replyList[0] as int?)!;
  358 + }
  359 + }
  360 +
  361 + Future<void> seekTo(int playerId, int position) async {
  362 + final String pigeonVar_channelName =
  363 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo$pigeonVar_messageChannelSuffix';
  364 + final BasicMessageChannel<Object?> pigeonVar_channel =
  365 + BasicMessageChannel<Object?>(
  366 + pigeonVar_channelName,
  367 + pigeonChannelCodec,
  368 + binaryMessenger: pigeonVar_binaryMessenger,
  369 + );
  370 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  371 + .send(<Object?>[playerId, position]) as List<Object?>?;
  372 + if (pigeonVar_replyList == null) {
  373 + throw _createConnectionError(pigeonVar_channelName);
  374 + } else if (pigeonVar_replyList.length > 1) {
  375 + throw PlatformException(
  376 + code: pigeonVar_replyList[0]! as String,
  377 + message: pigeonVar_replyList[1] as String?,
  378 + details: pigeonVar_replyList[2],
  379 + );
  380 + } else {
  381 + return;
  382 + }
  383 + }
  384 +
  385 + Future<void> pause(int playerId) async {
  386 + final String pigeonVar_channelName =
  387 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause$pigeonVar_messageChannelSuffix';
  388 + final BasicMessageChannel<Object?> pigeonVar_channel =
  389 + BasicMessageChannel<Object?>(
  390 + pigeonVar_channelName,
  391 + pigeonChannelCodec,
  392 + binaryMessenger: pigeonVar_binaryMessenger,
  393 + );
  394 + final List<Object?>? pigeonVar_replyList =
  395 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  396 + if (pigeonVar_replyList == null) {
  397 + throw _createConnectionError(pigeonVar_channelName);
  398 + } else if (pigeonVar_replyList.length > 1) {
  399 + throw PlatformException(
  400 + code: pigeonVar_replyList[0]! as String,
  401 + message: pigeonVar_replyList[1] as String?,
  402 + details: pigeonVar_replyList[2],
  403 + );
  404 + } else {
  405 + return;
  406 + }
  407 + }
  408 +
  409 + Future<void> setMixWithOthers(bool mixWithOthers) async {
  410 + final String pigeonVar_channelName =
  411 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix';
  412 + final BasicMessageChannel<Object?> pigeonVar_channel =
  413 + BasicMessageChannel<Object?>(
  414 + pigeonVar_channelName,
  415 + pigeonChannelCodec,
  416 + binaryMessenger: pigeonVar_binaryMessenger,
  417 + );
  418 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  419 + .send(<Object?>[mixWithOthers]) as List<Object?>?;
  420 + if (pigeonVar_replyList == null) {
  421 + throw _createConnectionError(pigeonVar_channelName);
  422 + } else if (pigeonVar_replyList.length > 1) {
  423 + throw PlatformException(
  424 + code: pigeonVar_replyList[0]! as String,
  425 + message: pigeonVar_replyList[1] as String?,
  426 + details: pigeonVar_replyList[2],
  427 + );
  428 + } else {
  429 + return;
  430 + }
  431 + }
  432 +}
... ...
packages/video_player_android/lib/src/platform_view_player.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +import 'package:flutter/foundation.dart';
  6 +import 'package:flutter/gestures.dart';
  7 +import 'package:flutter/rendering.dart';
  8 +import 'package:flutter/services.dart';
  9 +import 'package:flutter/widgets.dart';
  10 +
  11 +import 'messages.g.dart';
  12 +
  13 +/// A widget that displays a video player using a platform view.
  14 +class PlatformViewPlayer extends StatelessWidget {
  15 + /// Creates a new instance of [PlatformViewPlayer].
  16 + const PlatformViewPlayer({
  17 + super.key,
  18 + required this.playerId,
  19 + });
  20 +
  21 + /// The ID of the player.
  22 + final int playerId;
  23 +
  24 + @override
  25 + Widget build(BuildContext context) {
  26 + const String viewType = 'plugins.flutter.dev/video_player_android';
  27 + final PlatformVideoViewCreationParams creationParams =
  28 + PlatformVideoViewCreationParams(playerId: playerId);
  29 +
  30 + // IgnorePointer so that GestureDetector can be used above the platform view.
  31 + return IgnorePointer(
  32 + child: PlatformViewLink(
  33 + viewType: viewType,
  34 + surfaceFactory: (
  35 + BuildContext context,
  36 + PlatformViewController controller,
  37 + ) {
  38 + return AndroidViewSurface(
  39 + controller: controller as AndroidViewController,
  40 + gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
  41 + hitTestBehavior: PlatformViewHitTestBehavior.opaque,
  42 + );
  43 + },
  44 + onCreatePlatformView: (PlatformViewCreationParams params) {
  45 + return PlatformViewsService.initSurfaceAndroidView(
  46 + id: params.id,
  47 + viewType: viewType,
  48 + layoutDirection:
  49 + Directionality.maybeOf(context) ?? TextDirection.ltr,
  50 + creationParams: creationParams,
  51 + creationParamsCodec: AndroidVideoPlayerApi.pigeonChannelCodec,
  52 + onFocus: () => params.onFocusChanged(true),
  53 + )
  54 + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
  55 + ..create();
  56 + },
  57 + ),
  58 + );
  59 + }
  60 +}
... ...
packages/video_player_android/lib/video_player_android.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +export 'src/android_video_player.dart';
... ...
packages/video_player_android/pubspec.yaml 0 → 100644
  1 +name: video_player_android
  2 +description: Android implementation of the video_player plugin.
  3 +repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
  4 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
  5 +version: 2.8.4
  6 +
  7 +environment:
  8 + sdk: ^3.6.0
  9 + flutter: ">=3.27.0"
  10 +
  11 +flutter:
  12 + plugin:
  13 + implements: video_player
  14 + platforms:
  15 + android:
  16 + dartPluginClass: AndroidVideoPlayer
  17 + package: io.flutter.plugins.videoplayer
  18 + pluginClass: VideoPlayerPlugin
  19 +
  20 +dependencies:
  21 + flutter:
  22 + sdk: flutter
  23 + video_player_platform_interface: ^6.3.0
  24 +
  25 +dev_dependencies:
  26 + flutter_test:
  27 + sdk: flutter
  28 + pigeon: ^22.4.2
  29 +
  30 +topics:
  31 + - video
  32 + - video-player
... ...
pubspec.lock
... ... @@ -1509,12 +1509,11 @@ packages:
1509 1509 source: hosted
1510 1510 version: "2.10.0"
1511 1511 video_player_android:
1512   - dependency: transitive
  1512 + dependency: "direct overridden"
1513 1513 description:
1514   - name: video_player_android
1515   - sha256: "28dcc4122079f40f93a0965b3679aff1a5f4251cf79611bd8011f937eb6b69de"
1516   - url: "https://pub.flutter-io.cn"
1517   - source: hosted
  1514 + path: "packages/video_player_android"
  1515 + relative: true
  1516 + source: path
1518 1517 version: "2.8.4"
1519 1518 video_player_avfoundation:
1520 1519 dependency: transitive
... ...
pubspec.yaml
... ... @@ -135,6 +135,9 @@ dependencies:
135 135 dependency_overrides:
136 136 # lottie 3.1.2 depends on http ^1.0.0 umeng_apm_sdk >=2.0.1 depends on http ^0.13.1
137 137 http: ^0.13.3
  138 + # 开启 Media3 解码器初始化失败后的备用解码器回退。
  139 + video_player_android:
  140 + path: packages/video_player_android
138 141  
139 142 dev_dependencies:
140 143 build_runner: ^2.4.4
... ...