mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Revert Demo and remove BitmapFactoryVideoRenderer classes
This commit is contained in:
parent
e9fcc967a3
commit
43cfc4a611
9 changed files with 3 additions and 697 deletions
|
|
@ -542,10 +542,6 @@
|
||||||
{
|
{
|
||||||
"name": "Misc",
|
"name": "Misc",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
|
||||||
"name": "User File",
|
|
||||||
"uri": "content://user"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Dizzy (MP4)",
|
"name": "Dizzy (MP4)",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
@ -41,8 +40,6 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
|
@ -76,7 +73,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
private static final String TAG = "SampleChooserActivity";
|
private static final String TAG = "SampleChooserActivity";
|
||||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
||||||
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
|
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
|
||||||
private static final Uri USER_CONTENT = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority("user").build();
|
|
||||||
|
|
||||||
private String[] uris;
|
private String[] uris;
|
||||||
private boolean useExtensionRenderers;
|
private boolean useExtensionRenderers;
|
||||||
|
|
@ -84,13 +80,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
private SampleAdapter sampleAdapter;
|
private SampleAdapter sampleAdapter;
|
||||||
private MenuItem preferExtensionDecodersMenuItem;
|
private MenuItem preferExtensionDecodersMenuItem;
|
||||||
private ExpandableListView sampleListView;
|
private ExpandableListView sampleListView;
|
||||||
private final ActivityResultLauncher<String[]> openDocumentLauncher = registerForActivityResult(
|
|
||||||
new ActivityResultContracts.OpenDocument(), uri -> {
|
|
||||||
if (uri != null) {
|
|
||||||
final MediaItem mediaItem = new MediaItem.Builder().setUri(uri).build();
|
|
||||||
startPlayer(Collections.singletonList(mediaItem));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -234,25 +223,13 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
prefEditor.apply();
|
prefEditor.apply();
|
||||||
|
|
||||||
PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
|
PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
|
||||||
final List<MediaItem> mediaItems = playlistHolder.mediaItems;
|
|
||||||
if (!mediaItems.isEmpty()) {
|
|
||||||
final MediaItem mediaItem = mediaItems.get(0);
|
|
||||||
if (mediaItem.localConfiguration != null && USER_CONTENT.equals(mediaItem.localConfiguration.uri)) {
|
|
||||||
openDocumentLauncher.launch(new String[]{"video/*","audio/*"});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startPlayer(playlistHolder.mediaItems);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startPlayer(final List<MediaItem> mediaItems) {
|
|
||||||
Intent intent = new Intent(this, PlayerActivity.class);
|
Intent intent = new Intent(this, PlayerActivity.class);
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||||
IntentUtil.addToIntent(mediaItems, intent);
|
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,7 @@ android {
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
}
|
}
|
||||||
testOptions{
|
|
||||||
unitTests.all {
|
|
||||||
jvmArgs '-noverify'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
testCoverageEnabled = true
|
testCoverageEnabled = true
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.audio.AudioSink;
|
import com.google.android.exoplayer2.audio.AudioSink;
|
||||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||||
import com.google.android.exoplayer2.video.BitmapFactoryVideoRenderer;
|
|
||||||
import com.google.android.exoplayer2.mediacodec.DefaultMediaCodecAdapterFactory;
|
import com.google.android.exoplayer2.mediacodec.DefaultMediaCodecAdapterFactory;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||||
|
|
@ -396,7 +395,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||||
eventListener,
|
eventListener,
|
||||||
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
|
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
|
||||||
out.add(videoRenderer);
|
out.add(videoRenderer);
|
||||||
out.add(new BitmapFactoryVideoRenderer(eventHandler, eventListener));
|
|
||||||
|
|
||||||
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
|
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,317 +0,0 @@
|
||||||
package com.google.android.exoplayer2.video;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.view.Surface;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.annotation.WorkerThread;
|
|
||||||
import androidx.arch.core.util.Function;
|
|
||||||
import com.google.android.exoplayer2.BaseRenderer;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.FormatHolder;
|
|
||||||
import com.google.android.exoplayer2.RendererCapabilities;
|
|
||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
|
||||||
import com.google.android.exoplayer2.source.SampleStream;
|
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public class BitmapFactoryVideoRenderer extends BaseRenderer {
|
|
||||||
static final String TAG = "BitmapFactoryRenderer";
|
|
||||||
|
|
||||||
//Sleep Reasons
|
|
||||||
static final String STREAM_END = "Stream End";
|
|
||||||
static final String STREAM_EMPTY = "Stream Empty";
|
|
||||||
static final String RENDER_WAIT = "Render Wait";
|
|
||||||
|
|
||||||
private static int threadId;
|
|
||||||
|
|
||||||
private final Rect rect = new Rect();
|
|
||||||
private final RenderRunnable renderRunnable = new RenderRunnable();
|
|
||||||
|
|
||||||
final VideoRendererEventListener.EventDispatcher eventDispatcher;
|
|
||||||
final Thread thread = new Thread(renderRunnable, getClass().getSimpleName() + threadId++);
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
volatile Surface surface;
|
|
||||||
|
|
||||||
private VideoSize lastVideoSize = VideoSize.UNKNOWN;
|
|
||||||
private long currentTimeUs;
|
|
||||||
private long frameUs;
|
|
||||||
private boolean firstFrameRendered;
|
|
||||||
@Nullable
|
|
||||||
private DecoderCounters decoderCounters;
|
|
||||||
|
|
||||||
public BitmapFactoryVideoRenderer(@Nullable Handler eventHandler,
|
|
||||||
@Nullable VideoRendererEventListener eventListener) {
|
|
||||||
super(C.TRACK_TYPE_VIDEO);
|
|
||||||
eventDispatcher = new VideoRendererEventListener.EventDispatcher(eventHandler, eventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return TAG;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
|
|
||||||
throws ExoPlaybackException {
|
|
||||||
decoderCounters = new DecoderCounters();
|
|
||||||
eventDispatcher.enabled(decoderCounters);
|
|
||||||
if (mayRenderStartOfStream) {
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStarted() throws ExoPlaybackException {
|
|
||||||
if (thread.getState() == Thread.State.NEW) {
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDisabled() {
|
|
||||||
renderRunnable.stop();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
final DecoderCounters decoderCounters = this.decoderCounters;
|
|
||||||
if (decoderCounters != null) {
|
|
||||||
eventDispatcher.disabled(decoderCounters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
|
||||||
//Log.d(TAG, "Render: us=" + positionUs);
|
|
||||||
synchronized (renderRunnable) {
|
|
||||||
currentTimeUs = positionUs;
|
|
||||||
renderRunnable.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
|
||||||
thread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
|
|
||||||
if (messageType == MSG_SET_VIDEO_OUTPUT) {
|
|
||||||
if (message instanceof Surface) {
|
|
||||||
surface = (Surface) message;
|
|
||||||
} else {
|
|
||||||
surface = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.handleMessage(messageType, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReady() {
|
|
||||||
return surface != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnded() {
|
|
||||||
return renderRunnable.isEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int supportsFormat(Format format) throws ExoPlaybackException {
|
|
||||||
//Technically could support any format BitmapFactory supports
|
|
||||||
if (MimeTypes.VIDEO_MJPEG.equals(format.sampleMimeType)) {
|
|
||||||
return RendererCapabilities.create(C.FORMAT_HANDLED);
|
|
||||||
}
|
|
||||||
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
private void onFormatChanged(@NonNull FormatHolder formatHolder) {
|
|
||||||
@Nullable final Format format = formatHolder.format;
|
|
||||||
if (format != null) {
|
|
||||||
frameUs = (long)(1_000_000L / format.frameRate);
|
|
||||||
eventDispatcher.inputFormatChanged(format, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
void renderBitmap(@NonNull final Bitmap bitmap) {
|
|
||||||
@Nullable
|
|
||||||
final Surface surface = this.surface;
|
|
||||||
if (surface == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight());
|
|
||||||
final Canvas canvas = surface.lockCanvas(null);
|
|
||||||
|
|
||||||
renderBitmap(bitmap, canvas);
|
|
||||||
|
|
||||||
surface.unlockCanvasAndPost(canvas);
|
|
||||||
@Nullable
|
|
||||||
final DecoderCounters decoderCounters = BitmapFactoryVideoRenderer.this.decoderCounters;
|
|
||||||
if (decoderCounters != null) {
|
|
||||||
decoderCounters.renderedOutputBufferCount++;
|
|
||||||
}
|
|
||||||
if (!firstFrameRendered) {
|
|
||||||
firstFrameRendered = true;
|
|
||||||
eventDispatcher.renderedFirstFrame(surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
@VisibleForTesting
|
|
||||||
void renderBitmap(Bitmap bitmap, Canvas canvas) {
|
|
||||||
final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight());
|
|
||||||
if (!videoSize.equals(lastVideoSize)) {
|
|
||||||
lastVideoSize = videoSize;
|
|
||||||
eventDispatcher.videoSizeChanged(videoSize);
|
|
||||||
}
|
|
||||||
rect.set(0,0,canvas.getWidth(), canvas.getHeight());
|
|
||||||
canvas.drawBitmap(bitmap, null, rect, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RenderRunnable implements Runnable, Function<String, Boolean> {
|
|
||||||
final DecoderInputBuffer decoderInputBuffer =
|
|
||||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
|
||||||
|
|
||||||
private volatile boolean running = true;
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
Function<String, Boolean> sleepFunction = this;
|
|
||||||
|
|
||||||
void stop() {
|
|
||||||
running = false;
|
|
||||||
thread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isEnded() {
|
|
||||||
return !running || decoderInputBuffer.isEndOfStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Bitmap decodeInputBuffer(final DecoderInputBuffer decoderInputBuffer) {
|
|
||||||
@Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data;
|
|
||||||
if (byteBuffer != null) {
|
|
||||||
final Bitmap bitmap;
|
|
||||||
try {
|
|
||||||
bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(),
|
|
||||||
byteBuffer.arrayOffset() + byteBuffer.position());
|
|
||||||
if (bitmap == null) {
|
|
||||||
throw new NullPointerException("Decode bytes failed");
|
|
||||||
} else {
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
eventDispatcher.videoCodecError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @return true if interrupted
|
|
||||||
*/
|
|
||||||
public synchronized Boolean apply(String why) {
|
|
||||||
try {
|
|
||||||
wait();
|
|
||||||
return false;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
//If we are interrupted, treat as a cancel
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean sleep(String why) {
|
|
||||||
return sleepFunction.apply(why);
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
public void run() {
|
|
||||||
final FormatHolder formatHolder = getFormatHolder();
|
|
||||||
long start = SystemClock.uptimeMillis();
|
|
||||||
main:
|
|
||||||
while (running) {
|
|
||||||
decoderInputBuffer.clear();
|
|
||||||
final int result = readSource(formatHolder, decoderInputBuffer,
|
|
||||||
formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0);
|
|
||||||
switch (result) {
|
|
||||||
case C.RESULT_BUFFER_READ: {
|
|
||||||
if (decoderInputBuffer.isEndOfStream()) {
|
|
||||||
//Wait for shutdown or stream to be changed
|
|
||||||
sleep(STREAM_END);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final long leadUs = decoderInputBuffer.timeUs - currentTimeUs;
|
|
||||||
//If we are more than 1/2 a frame behind, skip the next frame
|
|
||||||
if (leadUs < -frameUs / 2) {
|
|
||||||
eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start);
|
|
||||||
start = SystemClock.uptimeMillis();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
start = SystemClock.uptimeMillis();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer);
|
|
||||||
if (bitmap == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
while (currentTimeUs < decoderInputBuffer.timeUs) {
|
|
||||||
//Log.d(TAG, "Sleep: us=" + currentTimeUs);
|
|
||||||
if (sleep(RENDER_WAIT)) {
|
|
||||||
//Sleep was interrupted, discard Bitmap
|
|
||||||
continue main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (running) {
|
|
||||||
renderBitmap(bitmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case C.RESULT_FORMAT_READ:
|
|
||||||
onFormatChanged(formatHolder);
|
|
||||||
break;
|
|
||||||
case C.RESULT_NOTHING_READ:
|
|
||||||
sleep(STREAM_EMPTY);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
|
||||||
Rect getRect() {
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@VisibleForTesting
|
|
||||||
DecoderCounters getDecoderCounters() {
|
|
||||||
return decoderCounters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
|
||||||
Thread getThread() {
|
|
||||||
return thread;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
|
||||||
Surface getSurface() {
|
|
||||||
return surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderRunnable getRenderRunnable() {
|
|
||||||
return renderRunnable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
package com.google.android.exoplayer2.video;
|
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
|
||||||
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
|
|
||||||
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.sample;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.view.Surface;
|
|
||||||
import androidx.arch.core.util.Function;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
|
||||||
import com.google.android.exoplayer2.Renderer;
|
|
||||||
import com.google.android.exoplayer2.RendererConfiguration;
|
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeSampleStream;
|
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import java.io.IOException;
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.robolectric.annotation.Config;
|
|
||||||
import org.robolectric.shadow.api.Shadow;
|
|
||||||
import org.robolectric.shadows.ShadowBitmapFactory;
|
|
||||||
import org.robolectric.shadows.ShadowLooper;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@Config(shadows = {ShadowSurfaceExtended.class})
|
|
||||||
public class BitmapFactoryVideoRendererTest {
|
|
||||||
private final static Format FORMAT_MJPEG = new Format.Builder().
|
|
||||||
setSampleMimeType(MimeTypes.VIDEO_MJPEG).
|
|
||||||
setWidth(320).setHeight(240).
|
|
||||||
setFrameRate(15f).build();
|
|
||||||
|
|
||||||
FakeEventListener fakeEventListener = new FakeEventListener();
|
|
||||||
BitmapFactoryVideoRenderer bitmapFactoryVideoRenderer;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void before() {
|
|
||||||
fakeEventListener = new FakeEventListener();
|
|
||||||
final Handler handler = new Handler(Looper.getMainLooper());
|
|
||||||
bitmapFactoryVideoRenderer = new BitmapFactoryVideoRenderer(handler, fakeEventListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void after() {
|
|
||||||
//Kill the Thread
|
|
||||||
bitmapFactoryVideoRenderer.onDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getName() {
|
|
||||||
Assert.assertEquals(BitmapFactoryVideoRenderer.TAG, bitmapFactoryVideoRenderer.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void onEnabled_givenMayRenderStartOfStream() throws PlaybackException {
|
|
||||||
bitmapFactoryVideoRenderer.onEnabled(false, true);
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
Assert.assertNotNull(bitmapFactoryVideoRenderer.getDecoderCounters());
|
|
||||||
Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState());
|
|
||||||
Assert.assertTrue(fakeEventListener.isVideoEnabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void onStarted_givenThreadNotStarted() throws PlaybackException {
|
|
||||||
bitmapFactoryVideoRenderer.onStarted();
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void onDisabled_givenOnEnabled() throws PlaybackException, InterruptedException {
|
|
||||||
onEnabled_givenMayRenderStartOfStream();
|
|
||||||
bitmapFactoryVideoRenderer.onDisabled();
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
Assert.assertFalse(fakeEventListener.isVideoEnabled());
|
|
||||||
//Ensure Thread is shutdown
|
|
||||||
bitmapFactoryVideoRenderer.getThread().join(500L);
|
|
||||||
Assert.assertTrue(bitmapFactoryVideoRenderer.isEnded());
|
|
||||||
}
|
|
||||||
|
|
||||||
private FakeSampleStream getSampleStream() throws IOException {
|
|
||||||
final Context context = ApplicationProvider.getApplicationContext();
|
|
||||||
final byte[] bytes = TestUtil.getByteArray(context, "media/jpeg/image-320-240.jpg");
|
|
||||||
FakeSampleStream fakeSampleStream =
|
|
||||||
new FakeSampleStream(
|
|
||||||
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
|
||||||
/* mediaSourceEventDispatcher= */ null,
|
|
||||||
DrmSessionManager.DRM_UNSUPPORTED,
|
|
||||||
new DrmSessionEventListener.EventDispatcher(),
|
|
||||||
/* initialFormat= */ FORMAT_MJPEG,
|
|
||||||
ImmutableList.of(
|
|
||||||
sample(0L, C.BUFFER_FLAG_KEY_FRAME, bytes),
|
|
||||||
END_OF_STREAM_ITEM));
|
|
||||||
return fakeSampleStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Surface setSurface() throws ExoPlaybackException {
|
|
||||||
final Surface surface = ShadowSurfaceExtended.newInstance();
|
|
||||||
final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface);
|
|
||||||
shadowSurfaceExtended.setSize(1080, 1920);
|
|
||||||
bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface);
|
|
||||||
return surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void handleMessage_givenSurface() throws ExoPlaybackException {
|
|
||||||
final Surface surface = setSurface();
|
|
||||||
Assert.assertSame(surface, bitmapFactoryVideoRenderer.getSurface());
|
|
||||||
bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, null);
|
|
||||||
Assert.assertNull(bitmapFactoryVideoRenderer.getSurface());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void isReady_givenSurface() throws ExoPlaybackException {
|
|
||||||
Assert.assertFalse(bitmapFactoryVideoRenderer.isReady());
|
|
||||||
setSurface();
|
|
||||||
Assert.assertTrue(bitmapFactoryVideoRenderer.isReady());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void render_givenJpegAndSurface() throws IOException, ExoPlaybackException {
|
|
||||||
final Surface surface = setSurface();
|
|
||||||
final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface);
|
|
||||||
|
|
||||||
FakeSampleStream fakeSampleStream = getSampleStream();
|
|
||||||
fakeSampleStream.writeData(0L);
|
|
||||||
bitmapFactoryVideoRenderer.enable(RendererConfiguration.DEFAULT, new Format[]{FORMAT_MJPEG},
|
|
||||||
fakeSampleStream, 0L, false, true, 0L, 0L);
|
|
||||||
bitmapFactoryVideoRenderer.render(0L, 0L);
|
|
||||||
// This test actually decodes the JPEG (very cool!),
|
|
||||||
// May need to bump up timers for slow machines
|
|
||||||
Assert.assertTrue(shadowSurfaceExtended.waitForPost(500L));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void supportsFormat_givenMjpegFormat() throws ExoPlaybackException{
|
|
||||||
Assert.assertEquals(C.FORMAT_HANDLED,
|
|
||||||
bitmapFactoryVideoRenderer.supportsFormat(FORMAT_MJPEG) & C.FORMAT_HANDLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void supportsFormat_givenMp4vFormat() throws ExoPlaybackException{
|
|
||||||
final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build();
|
|
||||||
Assert.assertEquals(0,
|
|
||||||
bitmapFactoryVideoRenderer.supportsFormat(format) & C.FORMAT_HANDLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void renderBitmap_given4by3BitmapAnd16by9Canvas() {
|
|
||||||
final Bitmap bitmap = Bitmap.createBitmap(FORMAT_MJPEG.width, FORMAT_MJPEG.height, Bitmap.Config.ARGB_8888);
|
|
||||||
final Bitmap canvasBitmap = Bitmap.createBitmap(1080, 1920, Bitmap.Config.ARGB_8888);
|
|
||||||
final Canvas canvas = new Canvas(canvasBitmap);
|
|
||||||
bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas);
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
|
|
||||||
final Rect rect = bitmapFactoryVideoRenderer.getRect();
|
|
||||||
Assert.assertEquals(canvas.getWidth(), rect.width());
|
|
||||||
Assert.assertEquals(canvas.getHeight(), rect.height());
|
|
||||||
final VideoSize videoSize = fakeEventListener.videoSize;
|
|
||||||
Assert.assertEquals(bitmap.getWidth(), videoSize.width);
|
|
||||||
|
|
||||||
bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas);
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
Assert.assertSame(videoSize, fakeEventListener.videoSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void RenderRunnable_run_givenLateFrame() throws IOException, ExoPlaybackException {
|
|
||||||
final Function<String, Boolean> sleep = why -> {throw new RuntimeException(why);};
|
|
||||||
|
|
||||||
FakeSampleStream fakeSampleStream = getSampleStream();
|
|
||||||
fakeSampleStream.writeData(0L);
|
|
||||||
//Don't enable so the Thread is not running
|
|
||||||
bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L);
|
|
||||||
BitmapFactoryVideoRenderer.RenderRunnable renderRunnable =
|
|
||||||
bitmapFactoryVideoRenderer.getRenderRunnable();
|
|
||||||
renderRunnable.sleepFunction = sleep;
|
|
||||||
bitmapFactoryVideoRenderer.render(1_000_000L, 0L);
|
|
||||||
try {
|
|
||||||
renderRunnable.run();
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage());
|
|
||||||
}
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
Assert.assertEquals(1, fakeEventListener.getDroppedFrames());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void RenderRunnable_run_givenBadJpeg() throws IOException, ExoPlaybackException {
|
|
||||||
final Function<String, Boolean> sleep = why -> {throw new RuntimeException(why);};
|
|
||||||
FakeSampleStream fakeSampleStream =
|
|
||||||
new FakeSampleStream(
|
|
||||||
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
|
||||||
/* mediaSourceEventDispatcher= */ null,
|
|
||||||
DrmSessionManager.DRM_UNSUPPORTED,
|
|
||||||
new DrmSessionEventListener.EventDispatcher(),
|
|
||||||
/* initialFormat= */ FORMAT_MJPEG,
|
|
||||||
ImmutableList.of(
|
|
||||||
oneByteSample(0L, C.BUFFER_FLAG_KEY_FRAME),
|
|
||||||
END_OF_STREAM_ITEM));
|
|
||||||
fakeSampleStream.writeData(0L);
|
|
||||||
|
|
||||||
//Don't enable so the Thread is not running
|
|
||||||
bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L);
|
|
||||||
BitmapFactoryVideoRenderer.RenderRunnable renderRunnable =
|
|
||||||
bitmapFactoryVideoRenderer.getRenderRunnable();
|
|
||||||
renderRunnable.sleepFunction = sleep;
|
|
||||||
bitmapFactoryVideoRenderer.render(0L, 0L);
|
|
||||||
// There is a bug in Robolectric where it doesn't handle null images,
|
|
||||||
// so we won't get our Exception
|
|
||||||
ShadowBitmapFactory.setAllowInvalidImageData(false);
|
|
||||||
try {
|
|
||||||
renderRunnable.run();
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage());
|
|
||||||
}
|
|
||||||
ShadowLooper.idleMainLooper();
|
|
||||||
Assert.assertTrue(fakeEventListener.getVideoCodecError() instanceof NullPointerException);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package com.google.android.exoplayer2.video;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
|
||||||
|
|
||||||
public class FakeEventListener implements VideoRendererEventListener {
|
|
||||||
@Nullable
|
|
||||||
VideoSize videoSize;
|
|
||||||
@Nullable
|
|
||||||
DecoderCounters decoderCounters;
|
|
||||||
|
|
||||||
private long firstFrameRenderMs = Long.MIN_VALUE;
|
|
||||||
|
|
||||||
private int droppedFrames;
|
|
||||||
|
|
||||||
private Exception videoCodecError;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVideoSizeChanged(VideoSize videoSize) {
|
|
||||||
this.videoSize = videoSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVideoEnabled() {
|
|
||||||
return decoderCounters != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVideoEnabled(DecoderCounters counters) {
|
|
||||||
decoderCounters = counters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVideoDisabled(DecoderCounters counters) {
|
|
||||||
decoderCounters = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getFirstFrameRenderMs() {
|
|
||||||
return firstFrameRenderMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRenderedFirstFrame(Object output, long renderTimeMs) {
|
|
||||||
firstFrameRenderMs = renderTimeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDroppedFrames() {
|
|
||||||
return droppedFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDroppedFrames(int count, long elapsedMs) {
|
|
||||||
droppedFrames+=count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Exception getVideoCodecError() {
|
|
||||||
return videoCodecError;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVideoCodecError(Exception videoCodecError) {
|
|
||||||
this.videoCodecError = videoCodecError;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package com.google.android.exoplayer2.video;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.view.Surface;
|
|
||||||
import java.util.concurrent.Semaphore;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import org.robolectric.annotation.Implements;
|
|
||||||
import org.robolectric.shadow.api.Shadow;
|
|
||||||
import org.robolectric.shadows.ShadowSurface;
|
|
||||||
|
|
||||||
@Implements(Surface.class)
|
|
||||||
public class ShadowSurfaceExtended extends ShadowSurface {
|
|
||||||
private final Semaphore postSemaphore = new Semaphore(0);
|
|
||||||
private int width;
|
|
||||||
private int height;
|
|
||||||
|
|
||||||
public static Surface newInstance() {
|
|
||||||
return Shadow.newInstanceOf(Surface.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSize(final int width, final int height) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Canvas lockCanvas(Rect canvas) {
|
|
||||||
return new Canvas(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockCanvasAndPost(Canvas canvas) {
|
|
||||||
postSemaphore.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean waitForPost(long millis) {
|
|
||||||
try {
|
|
||||||
return postSemaphore.tryAcquire(millis, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
Loading…
Reference in a new issue