Skip to content

Commit

Permalink
Move Player.Listener impl, remove `@VisibleForTesting isInitialized…
Browse files Browse the repository at this point in the history
…`. (#6922)

Similar to #6908, as part of flutter/flutter#148417.

I'm working on re-landing #6456, this time without using the `ActivityAware` interface (see flutter/flutter#148417). As part of that work, I'll need to better control the `ExoPlayer` lifecycle and save/restore internal state.

In this PR, I've removed the concept of the class being "initialized" or not - the only thing "initialized" means is "for a given instance of `ExoPlayer`, has received the `'initialized'` event. As a result I removed the quasi-public API that was used for testing only and replaced it with observing what the real production instance does (`Player.STATE_READY`).

After this PR, I'll likely do one more pass around the constructors - the constructor that takes an `ExoPlayer` that is marked `@VisibleForTesting` _also_ doesn't make sense once we'll support suspending/resuming video players, so it will need to get reworked (probably into taking a factory method).
  • Loading branch information
matanlurey committed Jun 13, 2024
1 parent a0f2552 commit dd04ab1
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 129 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.videoplayer;

import androidx.annotation.NonNull;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.VideoSize;
import androidx.media3.exoplayer.ExoPlayer;

final class ExoPlayerEventListener implements Player.Listener {
private final ExoPlayer exoPlayer;
private final VideoPlayerCallbacks events;
private boolean isBuffering = false;
private boolean isInitialized = false;

ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events) {
this.exoPlayer = exoPlayer;
this.events = events;
}

private void setBuffering(boolean buffering) {
if (isBuffering == buffering) {
return;
}
isBuffering = buffering;
if (buffering) {
events.onBufferingStart();
} else {
events.onBufferingEnd();
}
}

@SuppressWarnings("SuspiciousNameCombination")
private void sendInitialized() {
if (isInitialized) {
return;
}
isInitialized = true;
VideoSize videoSize = exoPlayer.getVideoSize();
int rotationCorrection = 0;
int width = videoSize.width;
int height = videoSize.height;
if (width != 0 && height != 0) {
int rotationDegrees = videoSize.unappliedRotationDegrees;
// Switch the width/height if video was taken in portrait mode
if (rotationDegrees == 90 || rotationDegrees == 270) {
width = videoSize.height;
height = videoSize.width;
}
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
// so inform the Flutter code that the widget needs to be rotated to prevent
// upside-down playback for videos with rotationDegrees of 180 (other orientations work
// correctly without correction).
if (rotationDegrees == 180) {
rotationCorrection = rotationDegrees;
}
}
events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection);
}

@Override
public void onPlaybackStateChanged(final int playbackState) {
switch (playbackState) {
case Player.STATE_BUFFERING:
setBuffering(true);
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
break;
case Player.STATE_READY:
sendInitialized();
break;
case Player.STATE_ENDED:
events.onCompleted();
break;
case Player.STATE_IDLE:
break;
}
if (playbackState != Player.STATE_BUFFERING) {
setBuffering(false);
}
}

@Override
public void onPlayerError(@NonNull final PlaybackException error) {
setBuffering(false);
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
// See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
exoPlayer.seekToDefaultPosition();
exoPlayer.prepare();
} else {
events.onError("VideoError", "Video player had error " + error, null);
}
}

@Override
public void onIsPlayingChanged(boolean isPlaying) {
events.onIsPlayingStateUpdate(isPlaying);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Listener;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
Expand All @@ -47,8 +43,6 @@ final class VideoPlayer {

private static final String USER_AGENT = "User-Agent";

@VisibleForTesting boolean isInitialized = false;

private final VideoPlayerOptions options;

private final DefaultHttpDataSource.Factory httpDataSourceFactory;
Expand Down Expand Up @@ -116,61 +110,7 @@ private void setUpVideoPlayer(ExoPlayer exoPlayer) {
surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);
setAudioAttributes(exoPlayer, options.mixWithOthers);

// Avoids synthetic accessor.
VideoPlayerCallbacks events = this.videoPlayerEvents;

exoPlayer.addListener(
new Listener() {
private boolean isBuffering = false;

public void setBuffering(boolean buffering) {
if (isBuffering == buffering) {
return;
}
isBuffering = buffering;
if (buffering) {
events.onBufferingStart();
} else {
events.onBufferingEnd();
}
}

@Override
public void onPlaybackStateChanged(final int playbackState) {
if (playbackState == Player.STATE_BUFFERING) {
setBuffering(true);
sendBufferingUpdate();
} else if (playbackState == Player.STATE_READY) {
if (!isInitialized) {
isInitialized = true;
sendInitialized();
}
} else if (playbackState == Player.STATE_ENDED) {
events.onCompleted();
}
if (playbackState != Player.STATE_BUFFERING) {
setBuffering(false);
}
}

@Override
public void onPlayerError(@NonNull final PlaybackException error) {
setBuffering(false);
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
// See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
exoPlayer.seekToDefaultPosition();
exoPlayer.prepare();
} else {
events.onError("VideoError", "Video player had error " + error, null);
}
}

@Override
public void onIsPlayingChanged(boolean isPlaying) {
events.onIsPlayingStateUpdate(isPlaying);
}
});
exoPlayer.addListener(new ExoPlayerEventListener(exoPlayer, videoPlayerEvents));
}

void sendBufferingUpdate() {
Expand Down Expand Up @@ -216,38 +156,7 @@ long getPosition() {
return exoPlayer.getCurrentPosition();
}

@SuppressWarnings("SuspiciousNameCombination")
@VisibleForTesting
void sendInitialized() {
if (!isInitialized) {
return;
}
VideoSize videoSize = exoPlayer.getVideoSize();
int rotationCorrection = 0;
int width = videoSize.width;
int height = videoSize.height;
if (width != 0 && height != 0) {
int rotationDegrees = videoSize.unappliedRotationDegrees;
// Switch the width/height if video was taken in portrait mode
if (rotationDegrees == 90 || rotationDegrees == 270) {
width = videoSize.height;
height = videoSize.width;
}
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
// so inform the Flutter code that the widget needs to be rotated to prevent
// upside-down playback for videos with rotationDegrees of 180 (other orientations work
// correctly without correction).
if (rotationDegrees == 180) {
rotationCorrection = rotationDegrees;
}
}
videoPlayerEvents.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection);
}

void dispose() {
if (isInitialized) {
exoPlayer.stop();
}
textureEntry.release();
if (surface != null) {
surface.release();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugin.common.EventChannel;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks {
Expand Down Expand Up @@ -66,7 +68,10 @@ public void onBufferingStart() {
public void onBufferingUpdate(long bufferedPosition) {
// iOS supports a list of buffered ranges, so we send as a list with a single range.
Map<String, Object> event = new HashMap<>();
event.put("values", Collections.singletonList(bufferedPosition));
event.put("event", "bufferingUpdate");

List<? extends Number> range = Arrays.asList(0, bufferedPosition);
event.put("values", Collections.singletonList(range));
eventSink.success(event);
}

Expand Down
Loading

0 comments on commit dd04ab1

Please sign in to comment.