blob: dee801534bbf2bf111c3b4d15c61f42b3b6eb3cb [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media2.widget;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.PixelCopy;
import android.view.SurfaceView;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media2.common.FileMediaItem;
import androidx.media2.common.MediaItem;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.VideoSize;
import androidx.media2.session.MediaController;
import androidx.media2.widget.test.R;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Test {@link VideoView} with a {@link SessionPlayer} or a {@link MediaController}.
*/
@RunWith(Parameterized.class)
@LargeTest
public class VideoView_WithPlayerTest extends MediaWidgetTestBase {
static final String TAG = "VideoView_WithPlayerTest";
@Parameterized.Parameters(name = "PlayerType={0}")
public static List<String> getPlayerTypes() {
return Arrays.asList(PLAYER_TYPE_MEDIA_CONTROLLER, PLAYER_TYPE_MEDIA_PLAYER);
}
private String mPlayerType;
private Activity mActivity;
private VideoView mVideoView;
private MediaItem mMediaItem;
private SynchronousPixelCopy mPixelCopyHelper;
@Rule
public ActivityTestRule<VideoViewTestActivity> mActivityRule =
new ActivityTestRule<>(VideoViewTestActivity.class);
public VideoView_WithPlayerTest(String playerType) {
mPlayerType = playerType;
}
@Before
public void setup() throws Throwable {
mActivity = mActivityRule.getActivity();
mVideoView = mActivity.findViewById(R.id.videoview);
mMediaItem = createTestMediaItem();
mPixelCopyHelper = new SynchronousPixelCopy();
setKeepScreenOn(mActivityRule);
checkAttachedToWindow(mVideoView);
}
@After
public void tearDown() throws Throwable {
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
closeAll();
}
});
mPixelCopyHelper.release();
}
@Test
public void testPlayVideo() throws Throwable {
DefaultPlayerCallback callback = new DefaultPlayerCallback();
PlayerWrapper playerWrapper = createPlayerWrapper(callback, mMediaItem, null);
setPlayerWrapper(playerWrapper);
assertTrue(callback.mItemLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertTrue(callback.mPausedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertEquals(1, callback.mPlayingLatch.getCount());
assertEquals(SessionPlayer.PLAYER_STATE_PAUSED, playerWrapper.getPlayerState());
playerWrapper.play();
assertTrue(callback.mPlayingLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
checkVideoRendering(true);
}
@Test
public void testPlayVideoWithMediaItemFromFileDescriptor() throws Throwable {
AssetFileDescriptor afd = mContext.getResources()
.openRawResourceFd(R.raw.testvideo_with_2_subtitle_tracks);
final MediaItem item = new FileMediaItem.Builder(
ParcelFileDescriptor.dup(afd.getFileDescriptor()))
.setFileDescriptorOffset(afd.getStartOffset())
.setFileDescriptorLength(afd.getLength())
.build();
afd.close();
DefaultPlayerCallback callback = new DefaultPlayerCallback();
PlayerWrapper playerWrapper = createPlayerWrapper(callback, item, null);
setPlayerWrapper(playerWrapper);
assertTrue(callback.mItemLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertTrue(callback.mPausedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
playerWrapper.play();
assertTrue(callback.mPlayingLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
checkVideoRendering(true);
}
@Test
public void testPlayVideoOnTextureView() throws Throwable {
final VideoView.OnViewTypeChangedListener mockViewTypeListener =
mock(VideoView.OnViewTypeChangedListener.class);
DefaultPlayerCallback callback = new DefaultPlayerCallback();
PlayerWrapper playerWrapper = createPlayerWrapper(callback, mMediaItem, null);
setPlayerWrapper(playerWrapper);
// The default view type is surface view.
assertEquals(mVideoView.getViewType(), VideoView.VIEW_TYPE_SURFACEVIEW);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
mVideoView.setViewType(VideoView.VIEW_TYPE_TEXTUREVIEW);
}
});
verify(mockViewTypeListener, timeout(WAIT_TIME_MS))
.onViewTypeChanged(mVideoView, VideoView.VIEW_TYPE_TEXTUREVIEW);
assertTrue(callback.mItemLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertTrue(callback.mPausedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
playerWrapper.play();
assertTrue(callback.mPlayingLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
checkVideoRendering(true);
}
@Test
public void testPlayVideoWithVisibilityChange() throws Throwable {
final VideoView.OnViewTypeChangedListener mockViewTypeListener =
mock(VideoView.OnViewTypeChangedListener.class);
DefaultPlayerCallback callback = new DefaultPlayerCallback();
PlayerWrapper playerWrapper = createPlayerWrapper(callback, mMediaItem, null);
setPlayerWrapper(playerWrapper);
// The default view type is surface view.
assertEquals(mVideoView.getViewType(), VideoView.VIEW_TYPE_SURFACEVIEW);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
mVideoView.setViewType(VideoView.VIEW_TYPE_TEXTUREVIEW);
mVideoView.setVisibility(View.GONE);
}
});
assertTrue(callback.mItemLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertTrue(callback.mPausedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
playerWrapper.play();
assertTrue(callback.mPlayingLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
checkVideoRendering(false);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setVisibility(View.VISIBLE);
}
});
// Note: Actual view type change is done when VideoView has a valid surface.
verify(mockViewTypeListener, timeout(WAIT_TIME_MS))
.onViewTypeChanged(mVideoView, VideoView.VIEW_TYPE_TEXTUREVIEW);
assertEquals(mVideoView.getViewType(), VideoView.VIEW_TYPE_TEXTUREVIEW);
checkVideoRendering(true);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setViewType(VideoView.VIEW_TYPE_SURFACEVIEW);
}
});
verify(mockViewTypeListener, timeout(WAIT_TIME_MS))
.onViewTypeChanged(mVideoView, VideoView.VIEW_TYPE_SURFACEVIEW);
assertEquals(mVideoView.getViewType(), VideoView.VIEW_TYPE_SURFACEVIEW);
checkVideoRendering(true);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setVisibility(View.GONE);
}
});
// Although it is not flaky, since checkVideoRendering() waits a bit before actual
// screen capturing, we might need to define a listener to ensure the player's surface
// has been released.
checkVideoRendering(false);
}
@Test
public void testSetViewType() throws Throwable {
final VideoView.OnViewTypeChangedListener mockViewTypeListener =
mock(VideoView.OnViewTypeChangedListener.class);
DefaultPlayerCallback callback = new DefaultPlayerCallback();
PlayerWrapper playerWrapper = createPlayerWrapper(callback, mMediaItem, null);
setPlayerWrapper(playerWrapper);
// The default view type is surface view.
assertEquals(mVideoView.getViewType(), VideoView.VIEW_TYPE_SURFACEVIEW);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
mVideoView.setViewType(VideoView.VIEW_TYPE_TEXTUREVIEW);
mVideoView.setViewType(VideoView.VIEW_TYPE_SURFACEVIEW);
mVideoView.setViewType(VideoView.VIEW_TYPE_TEXTUREVIEW);
mVideoView.setViewType(VideoView.VIEW_TYPE_SURFACEVIEW);
}
});
assertTrue(callback.mItemLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
// WAIT_TIME_MS multiplied by the number of operations.
assertTrue(callback.mPausedLatch.await(WAIT_TIME_MS * 5, TimeUnit.MILLISECONDS));
assertEquals(mVideoView.getViewType(), VideoView.VIEW_TYPE_SURFACEVIEW);
playerWrapper.play();
assertTrue(callback.mPlayingLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
checkVideoRendering(true);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mVideoView.setViewType(VideoView.VIEW_TYPE_TEXTUREVIEW);
}
});
verify(mockViewTypeListener, timeout(WAIT_TIME_MS))
.onViewTypeChanged(mVideoView, VideoView.VIEW_TYPE_TEXTUREVIEW);
checkVideoRendering(true);
}
// @UiThreadTest will be ignored by Parameterized test runner (b/30746303)
@Test
public void testAttachedMediaControlView_setPlayerOrController() throws Throwable {
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
PlayerWrapper playerWrapper = createPlayerWrapper(new DefaultPlayerCallback(),
mMediaItem, null);
MediaControlView defaultMediaControlView = mVideoView.getMediaControlView();
assertNotNull(defaultMediaControlView);
try {
if (playerWrapper.mPlayer != null) {
defaultMediaControlView.setPlayer(playerWrapper.mPlayer);
} else if (playerWrapper.mController != null) {
defaultMediaControlView.setMediaController(playerWrapper.mController);
} else {
fail("playerWrapper doesn't have neither mPlayer or mController");
}
fail("setPlayer or setMediaController should not be allowed "
+ "for MediaControlView attached to VideoView");
} catch (IllegalStateException ex) {
// expected
}
MediaControlView newMediaControlView = new MediaControlView(mContext);
mVideoView.setMediaControlView(newMediaControlView, -1);
try {
if (playerWrapper.mPlayer != null) {
newMediaControlView.setPlayer(playerWrapper.mPlayer);
} else if (playerWrapper.mController != null) {
newMediaControlView.setMediaController(playerWrapper.mController);
} else {
fail("playerWrapper doesn't have neither mPlayer or mController");
}
fail("setPlayer or setMediaController should not be allowed "
+ "for MediaControlView attached to VideoView");
} catch (IllegalStateException ex) {
// expected
}
}
});
}
@Test
public void testOnVideoSizeChanged() throws Throwable {
final Uri nonMusicUri = Uri.parse("android.resource://" + mContext.getPackageName() + "/"
+ R.raw.testvideo_with_2_subtitle_tracks);
final Uri musicUri = Uri.parse("android.resource://" + mContext.getPackageName() + "/"
+ R.raw.test_music);
final String nonMusicMediaId = "nonMusic";
final String musicMediaId = "music";
final VideoSize nonMusicVideoSize = new VideoSize(160, 90);
final VideoSize musicVideoSize = new VideoSize(0, 0);
final CountDownLatch latchForNonMusicItem = new CountDownLatch(1);
final CountDownLatch latchForMusicItem = new CountDownLatch(1);
List<MediaItem> playlist = new ArrayList<>();
playlist.add(createTestMediaItem(nonMusicUri, nonMusicMediaId));
playlist.add(createTestMediaItem(musicUri, musicMediaId));
final PlayerWrapper playerWrapper = createPlayerWrapper(new PlayerWrapper.PlayerCallback() {
@Override
void onVideoSizeChanged(@NonNull PlayerWrapper player, @NonNull VideoSize videoSize) {
MediaItem item = player.getCurrentMediaItem();
if (item == null) {
return;
}
String mediaId = item.getMediaId();
if (nonMusicMediaId.equals(mediaId)) {
if (player.mController != null
&& videoSize.getHeight() == 0 && videoSize.getWidth() == 0) {
// PlayerWrapper could notify onVideoSizeChanged with VideoSize of (0, 0)
// right after its MediaController is connected. Ignore this case.
return;
}
assertEquals(nonMusicVideoSize, videoSize);
latchForNonMusicItem.countDown();
} else if (musicMediaId.equals(mediaId)) {
assertEquals(musicVideoSize, videoSize);
latchForMusicItem.countDown();
}
}
}, null, playlist);
setPlayerWrapper(playerWrapper);
assertTrue(latchForNonMusicItem.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
playerWrapper.skipToNextItem();
assertTrue(latchForMusicItem.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
private void setPlayerWrapper(final PlayerWrapper playerWrapper) throws Throwable {
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
if (playerWrapper.mPlayer != null) {
mVideoView.setPlayer(playerWrapper.mPlayer);
} else if (playerWrapper.mController != null) {
mVideoView.setMediaController(playerWrapper.mController);
}
}
});
}
private PlayerWrapper createPlayerWrapper(@NonNull PlayerWrapper.PlayerCallback callback,
@Nullable MediaItem item, @Nullable List<MediaItem> playlist) {
return createPlayerWrapperOfType(callback, item, playlist, mPlayerType);
}
private void checkVideoRendering(boolean expectRendering) throws InterruptedException {
if (Build.DEVICE.equals("sailfish") && Build.VERSION.SDK_INT == 28) {
// See b/137321781
return;
}
if (Build.DEVICE.equals("fugu") && Build.VERSION.SDK_INT == 24) {
return;
}
if (Build.VERSION.SDK_INT >= 24) {
final int bufferQueueToleranceMs = 200;
final int elapsedTimeForSecondScreenshotMs = 400;
// Tolerance until the video buffers are actually queued.
Thread.sleep(bufferQueueToleranceMs);
Bitmap beforeBitmap = getVideoScreenshot();
Thread.sleep(elapsedTimeForSecondScreenshotMs);
Bitmap afterBitmap = getVideoScreenshot();
assertEquals(expectRendering, !afterBitmap.sameAs(beforeBitmap));
}
}
private Bitmap getVideoScreenshot() {
Bitmap bitmap = Bitmap.createBitmap(mVideoView.getWidth(),
mVideoView.getHeight(), Bitmap.Config.RGB_565);
if (mVideoView.getViewType() == mVideoView.VIEW_TYPE_SURFACEVIEW) {
if (mVideoView.mSurfaceView.hasAvailableSurface()) {
int copyResult = mPixelCopyHelper.request(mVideoView.mSurfaceView, bitmap);
if (copyResult != PixelCopy.ERROR_SOURCE_NO_DATA) {
assertEquals("PixelCopy failed.", PixelCopy.SUCCESS, copyResult);
}
}
} else {
bitmap = mVideoView.mTextureView.getBitmap(bitmap);
}
return bitmap;
}
private static class SynchronousPixelCopy {
private Handler mHandler;
private HandlerThread mHandlerThread;
private int mStatus = PixelCopy.SUCCESS;
SynchronousPixelCopy() {
if (Build.VERSION.SDK_INT >= 24) {
this.mHandlerThread = new HandlerThread("PixelCopyHelper");
mHandlerThread.start();
this.mHandler = new Handler(mHandlerThread.getLooper());
}
}
public void release() {
if (Build.VERSION.SDK_INT >= 24) {
if (mHandlerThread.isAlive()) {
mHandlerThread.quitSafely();
}
}
}
public int request(SurfaceView source, Bitmap dest) {
if (Build.VERSION.SDK_INT < 24) {
return -1;
}
synchronized (this) {
try {
PixelCopy.request(source, dest, new PixelCopy.OnPixelCopyFinishedListener() {
@Override
public void onPixelCopyFinished(int copyResult) {
synchronized (this) {
mStatus = copyResult;
this.notify();
}
}
}, mHandler);
return getResultLocked();
} catch (Exception e) {
Log.e(TAG, "Exception occurred when copying a SurfaceView.", e);
return -1;
}
}
}
private int getResultLocked() {
try {
this.wait(1000);
} catch (InterruptedException e) {
/* PixelCopy request didn't complete within 1s */
mStatus = PixelCopy.ERROR_TIMEOUT;
}
return mStatus;
}
}
}