Add CapturingRenderersFactory and use it in Mp4PlaybackTest
I decided not to migrate all the tests in one CL to keep the diff manageable. I'll make follow-up CLs to migrate the tests, and eventually delete TeeCodec and all associated logic. I couldn't completely remove the dump diff because ShadowMediaCodec.getCodecInfo() (which would give me access to the MIME type) doesn't seem to work properly - it returned video/avc when name=exotest.audio.aac, and looking into the code it looks like there's some native methods that are missing shadow implementations. PiperOrigin-RevId: 347991956
This commit is contained in:
parent
43886491f6
commit
1a00da4c19
@ -77,14 +77,18 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
/**
|
||||
* Allow use of extension renderers. Extension renderers are indexed before core renderers of the
|
||||
* same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
|
||||
* prefer to use an extension renderer to a core renderer in the case that both are able to play
|
||||
* a given track.
|
||||
* prefer to use an extension renderer to a core renderer in the case that both are able to play a
|
||||
* given track.
|
||||
*/
|
||||
public static final int EXTENSION_RENDERER_MODE_PREFER = 2;
|
||||
|
||||
private static final String TAG = "DefaultRenderersFactory";
|
||||
/**
|
||||
* The maximum number of frames that can be dropped between invocations of {@link
|
||||
* VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||
*/
|
||||
public static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
|
||||
|
||||
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
|
||||
private static final String TAG = "DefaultRenderersFactory";
|
||||
|
||||
private final Context context;
|
||||
@ExtensionRendererMode private int extensionRendererMode;
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.e2etest;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.view.Surface;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@ -25,6 +26,7 @@ import com.google.android.exoplayer2.robolectric.PlaybackOutput;
|
||||
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
|
||||
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
|
||||
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
||||
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
|
||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.junit.Rule;
|
||||
@ -75,12 +77,15 @@ public class Mp4PlaybackTest {
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||
CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(applicationContext);
|
||||
SimpleExoPlayer player =
|
||||
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
|
||||
new SimpleExoPlayer.Builder(applicationContext, renderersFactory)
|
||||
.setClock(new AutoAdvancingFakeClock())
|
||||
.build();
|
||||
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig);
|
||||
|
||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||
|
||||
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/" + inputFile));
|
||||
player.prepare();
|
||||
@ -89,8 +94,6 @@ public class Mp4PlaybackTest {
|
||||
player.release();
|
||||
|
||||
DumpFileAsserts.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
playbackOutput,
|
||||
"playbackdumps/mp4/" + inputFile + ".dump");
|
||||
applicationContext, playbackOutput, "playbackdumps/mp4/" + inputFile + ".dump");
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import android.graphics.Bitmap;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
|
||||
import com.google.android.exoplayer2.testutil.Dumper;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -38,13 +39,18 @@ import java.util.List;
|
||||
*/
|
||||
public final class PlaybackOutput implements Dumper.Dumpable {
|
||||
|
||||
private final ShadowMediaCodecConfig codecConfig;
|
||||
@Nullable private final ShadowMediaCodecConfig codecConfig;
|
||||
@Nullable private final CapturingRenderersFactory capturingRenderersFactory;
|
||||
|
||||
private final List<Metadata> metadatas;
|
||||
private final List<List<Cue>> subtitles;
|
||||
|
||||
private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) {
|
||||
private PlaybackOutput(
|
||||
SimpleExoPlayer player,
|
||||
@Nullable ShadowMediaCodecConfig codecConfig,
|
||||
@Nullable CapturingRenderersFactory capturingRenderersFactory) {
|
||||
this.codecConfig = codecConfig;
|
||||
this.capturingRenderersFactory = capturingRenderersFactory;
|
||||
|
||||
metadatas = Collections.synchronizedList(new ArrayList<>());
|
||||
subtitles = Collections.synchronizedList(new ArrayList<>());
|
||||
@ -57,27 +63,38 @@ public final class PlaybackOutput implements Dumper.Dumpable {
|
||||
|
||||
/**
|
||||
* Create an instance that captures the metadata and text output from {@code player} and the audio
|
||||
* and video output via the {@link TeeCodec TeeCodecs} exposed by {@code mediaCodecConfig}.
|
||||
* and video output via {@code capturingRenderersFactory}.
|
||||
*
|
||||
* <p>Must be called <b>before</b> playback to ensure metadata and text output is captured
|
||||
* correctly.
|
||||
*
|
||||
* @param player The {@link SimpleExoPlayer} to capture metadata and text output from.
|
||||
* @param mediaCodecConfig The {@link ShadowMediaCodecConfig} to capture audio and video output
|
||||
* from.
|
||||
* @param capturingRenderersFactory The {@link CapturingRenderersFactory} to capture audio and
|
||||
* video output from.
|
||||
* @return A new instance that can be used to dump the playback output.
|
||||
*/
|
||||
public static PlaybackOutput register(
|
||||
SimpleExoPlayer player, CapturingRenderersFactory capturingRenderersFactory) {
|
||||
return new PlaybackOutput(player, /* codecConfig= */ null, capturingRenderersFactory);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #register(SimpleExoPlayer, CapturingRenderersFactory)}. */
|
||||
@Deprecated
|
||||
public static PlaybackOutput register(
|
||||
SimpleExoPlayer player, ShadowMediaCodecConfig mediaCodecConfig) {
|
||||
return new PlaybackOutput(player, mediaCodecConfig);
|
||||
return new PlaybackOutput(player, mediaCodecConfig, /* capturingRenderersFactory= */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Dumper dumper) {
|
||||
ImmutableMap<String, TeeCodec> codecs = codecConfig.getCodecs();
|
||||
ImmutableList<String> mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet());
|
||||
for (String mimeType : mimeTypes) {
|
||||
dumper.add(Assertions.checkNotNull(codecs.get(mimeType)));
|
||||
if (codecConfig != null) {
|
||||
ImmutableMap<String, TeeCodec> codecs = codecConfig.getCodecs();
|
||||
ImmutableList<String> mimeTypes = ImmutableList.sortedCopyOf(codecs.keySet());
|
||||
for (String mimeType : mimeTypes) {
|
||||
dumper.add(Assertions.checkNotNull(codecs.get(mimeType)));
|
||||
}
|
||||
} else {
|
||||
Assertions.checkNotNull(capturingRenderersFactory).dump(dumper);
|
||||
}
|
||||
|
||||
dumpMetadata(dumper);
|
||||
|
@ -15,8 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.robolectric;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@ -48,6 +50,11 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
|
||||
return new ShadowMediaCodecConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link CapturingRenderersFactory} to access {@link MediaCodec} interactions
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public ImmutableMap<String, TeeCodec> getCodecs() {
|
||||
return ImmutableMap.copyOf(codecsByMimeType);
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.robolectric;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
|
||||
import com.google.android.exoplayer2.testutil.Dumper;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@ -25,12 +27,10 @@ import java.util.List;
|
||||
import org.robolectric.shadows.ShadowMediaCodec;
|
||||
|
||||
/**
|
||||
* A {@link ShadowMediaCodec.CodecConfig.Codec} for Robolectric's {@link ShadowMediaCodec} that
|
||||
* records the contents of buffers passed to it before copying the contents into the output buffer.
|
||||
*
|
||||
* <p>This also implements {@link Dumper.Dumpable} so the recorded buffers can be written out to a
|
||||
* dump file.
|
||||
* @deprecated Use {@link CapturingRenderersFactory} to access {@link MediaCodec} interactions
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class TeeCodec implements ShadowMediaCodec.CodecConfig.Codec, Dumper.Dumpable {
|
||||
|
||||
private final String mimeType;
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 218
|
||||
buffers[0] = length 21, hash D57A2CCC
|
||||
buffers[1] = length 4, hash EE9DF
|
||||
@ -218,7 +218,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[215] = length 4, hash EE9DF
|
||||
buffers[216] = length 4, hash EE9DF
|
||||
buffers[217] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 126
|
||||
buffers[0] = length 5252, hash 13893A4C
|
||||
buffers[1] = length 44, hash A05B3BEA
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 218
|
||||
buffers[0] = length 21, hash D57A2CCC
|
||||
buffers[1] = length 4, hash EE9DF
|
||||
@ -218,7 +218,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[215] = length 4, hash EE9DF
|
||||
buffers[216] = length 4, hash EE9DF
|
||||
buffers[217] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 126
|
||||
buffers[0] = length 5384, hash F220EEFD
|
||||
buffers[1] = length 58, hash 897F4173
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 218
|
||||
buffers[0] = length 21, hash D57A2CCC
|
||||
buffers[1] = length 4, hash EE9DF
|
||||
@ -218,7 +218,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[215] = length 4, hash EE9DF
|
||||
buffers[216] = length 4, hash EE9DF
|
||||
buffers[217] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 126
|
||||
buffers[0] = length 5245, hash C090A41E
|
||||
buffers[1] = length 63, hash 5141C80D
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 46
|
||||
buffers[0] = length 23, hash 47DE9131
|
||||
buffers[1] = length 6, hash 31EC5206
|
||||
@ -46,7 +46,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[43] = length 229, hash FFF98DF0
|
||||
buffers[44] = length 6, hash 31B22286
|
||||
buffers[45] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 36692, hash D216076E
|
||||
buffers[1] = length 5312, hash D45D3CA0
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/ac3):
|
||||
MediaCodecAdapter (exotest.audio.ac3):
|
||||
buffers.length = 10
|
||||
buffers[0] = length 1536, hash 7108D5C2
|
||||
buffers[1] = length 1536, hash 80BF3B34
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/ac3):
|
||||
MediaCodecAdapter (exotest.audio.ac3):
|
||||
buffers.length = 10
|
||||
buffers[0] = length 1536, hash 7108D5C2
|
||||
buffers[1] = length 1536, hash 80BF3B34
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/ac4):
|
||||
MediaCodecAdapter (exotest.audio.ac4):
|
||||
buffers.length = 20
|
||||
buffers[0] = length 367, hash D2762FA
|
||||
buffers[1] = length 367, hash BDD3224A
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/ac4):
|
||||
MediaCodecAdapter (exotest.audio.ac4):
|
||||
buffers.length = 20
|
||||
buffers[0] = length 367, hash D2762FA
|
||||
buffers[1] = length 367, hash BDD3224A
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 8
|
||||
buffers[0] = length 34656, hash D92B66FF
|
||||
buffers[1] = length 768, hash D0C3B229
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/eac3):
|
||||
MediaCodecAdapter (exotest.audio.eac3):
|
||||
buffers.length = 55
|
||||
buffers[0] = length 4000, hash BAEAFB2A
|
||||
buffers[1] = length 4000, hash E3C5EBF0
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/eac3):
|
||||
MediaCodecAdapter (exotest.audio.eac3):
|
||||
buffers.length = 55
|
||||
buffers[0] = length 4000, hash BAEAFB2A
|
||||
buffers[1] = length 4000, hash E3C5EBF0
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/eac3-joc):
|
||||
MediaCodecAdapter (exotest.audio.eac3joc):
|
||||
buffers.length = 65
|
||||
buffers[0] = length 2560, hash 882594AD
|
||||
buffers[1] = length 2560, hash 41EC8B22
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/eac3-joc):
|
||||
MediaCodecAdapter (exotest.audio.eac3joc):
|
||||
buffers.length = 65
|
||||
buffers[0] = length 2560, hash 882594AD
|
||||
buffers[1] = length 2560, hash 41EC8B22
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 47
|
||||
buffers[0] = length 18, hash 96519432
|
||||
buffers[1] = length 4, hash EE9DF
|
||||
@ -47,7 +47,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[44] = length 446, hash D6735B8A
|
||||
buffers[45] = length 10, hash A453EEBE
|
||||
buffers[46] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 38070, hash B58E1AEE
|
||||
buffers[1] = length 8340, hash 8AC449FF
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 47
|
||||
buffers[0] = length 18, hash 96519432
|
||||
buffers[1] = length 4, hash EE9DF
|
||||
@ -47,7 +47,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[44] = length 446, hash D6735B8A
|
||||
buffers[45] = length 10, hash A453EEBE
|
||||
buffers[46] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 38070, hash B58E1AEE
|
||||
buffers[1] = length 8340, hash 8AC449FF
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 47
|
||||
buffers[0] = length 18, hash 96519432
|
||||
buffers[1] = length 4, hash EE9DF
|
||||
@ -47,7 +47,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[44] = length 446, hash D6735B8A
|
||||
buffers[45] = length 10, hash A453EEBE
|
||||
buffers[46] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 38070, hash B58E1AEE
|
||||
buffers[1] = length 8340, hash 8AC449FF
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 46
|
||||
buffers[0] = length 23, hash 47DE9131
|
||||
buffers[1] = length 6, hash 31EC5206
|
||||
@ -46,7 +46,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[43] = length 229, hash FFF98DF0
|
||||
buffers[44] = length 6, hash 31B22286
|
||||
buffers[45] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 36692, hash D216076E
|
||||
buffers[1] = length 5312, hash D45D3CA0
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/opus):
|
||||
MediaCodecAdapter (exotest.audio.opus):
|
||||
buffers.length = 102
|
||||
buffers[0] = length 3, hash 4732
|
||||
buffers[1] = length 3, hash 4732
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/opus):
|
||||
MediaCodecAdapter (exotest.audio.opus):
|
||||
buffers.length = 251
|
||||
buffers[0] = length 326, hash ECC9FF90
|
||||
buffers[1] = length 326, hash B041EAAC
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 46
|
||||
buffers[0] = length 21, hash D57A2CCC
|
||||
buffers[1] = length 6, hash 336D5819
|
||||
@ -46,7 +46,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[43] = length 208, hash 4A050A0D
|
||||
buffers[44] = length 13, hash 2555A7DC
|
||||
buffers[45] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 37655, hash 265F7BA7
|
||||
buffers[1] = length 5023, hash 30768D40
|
||||
|
@ -1,4 +1,4 @@
|
||||
MediaCodec (audio/mp4a-latm):
|
||||
MediaCodecAdapter (exotest.audio.aac):
|
||||
buffers.length = 45
|
||||
buffers[0] = length 9, hash 67CB703F
|
||||
buffers[1] = length 9, hash A820BF4B
|
||||
@ -45,7 +45,7 @@ MediaCodec (audio/mp4a-latm):
|
||||
buffers[42] = length 242, hash B5863406
|
||||
buffers[43] = length 239, hash F56D62C3
|
||||
buffers[44] = length 0, hash 1
|
||||
MediaCodec (video/avc):
|
||||
MediaCodecAdapter (exotest.video.avc):
|
||||
buffers.length = 31
|
||||
buffers[0] = length 16086, hash 5D23AFBA
|
||||
buffers[1] = length 2539, hash 597403A0
|
||||
|
@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.google.android.exoplayer2.testutil;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.text.TextOutput;
|
||||
import com.google.android.exoplayer2.text.TextRenderer;
|
||||
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A {@link RenderersFactory} that captures interactions with the audio and video {@link
|
||||
* MediaCodecAdapter} instances.
|
||||
*
|
||||
* <p>The captured interactions can be used in a test assertion via the {@link Dumper.Dumpable}
|
||||
* interface.
|
||||
*/
|
||||
// TODO(internal b/174661563): Add support for capturing subtitles on the output of the
|
||||
// SubtitleDecoder. And possibly Metadata too (for consistency).
|
||||
public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpable {
|
||||
|
||||
private final Context context;
|
||||
private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory;
|
||||
|
||||
public CapturingRenderersFactory(Context context) {
|
||||
this.context = context;
|
||||
this.mediaCodecAdapterFactory = new CapturingMediaCodecAdapter.Factory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Renderer[] createRenderers(
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener videoRendererEventListener,
|
||||
AudioRendererEventListener audioRendererEventListener,
|
||||
TextOutput textRendererOutput,
|
||||
MetadataOutput metadataRendererOutput) {
|
||||
return new Renderer[] {
|
||||
new MediaCodecVideoRenderer(
|
||||
context,
|
||||
mediaCodecAdapterFactory,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,
|
||||
/* enableDecoderFallback= */ false,
|
||||
eventHandler,
|
||||
videoRendererEventListener,
|
||||
DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY),
|
||||
new MediaCodecAudioRenderer(
|
||||
context,
|
||||
mediaCodecAdapterFactory,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
/* enableDecoderFallback= */ false,
|
||||
eventHandler,
|
||||
audioRendererEventListener,
|
||||
new DefaultAudioSink(AudioCapabilities.getCapabilities(context), new AudioProcessor[0])),
|
||||
new TextRenderer(textRendererOutput, eventHandler.getLooper()),
|
||||
new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper())
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Dumper dumper) {
|
||||
mediaCodecAdapterFactory.dump(dumper);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MediaCodecAdapter} that captures interactions and exposes them for test assertions via
|
||||
* {@link Dumper.Dumpable}.
|
||||
*/
|
||||
private static class CapturingMediaCodecAdapter implements MediaCodecAdapter, Dumper.Dumpable {
|
||||
|
||||
private static class Factory implements MediaCodecAdapter.Factory, Dumper.Dumpable {
|
||||
|
||||
private final List<CapturingMediaCodecAdapter> constructedAdapters;
|
||||
|
||||
private Factory() {
|
||||
constructedAdapters = new ArrayList<>();
|
||||
}
|
||||
|
||||
@RequiresApi(18)
|
||||
@Override
|
||||
public MediaCodecAdapter createAdapter(MediaCodec codec) {
|
||||
CapturingMediaCodecAdapter adapter =
|
||||
new CapturingMediaCodecAdapter(
|
||||
MediaCodecAdapter.Factory.DEFAULT.createAdapter(codec), codec.getName());
|
||||
constructedAdapters.add(adapter);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Dumper dumper) {
|
||||
ImmutableList<CapturingMediaCodecAdapter> sortedAdapters =
|
||||
ImmutableList.sortedCopyOf(
|
||||
(adapter1, adapter2) -> adapter1.codecName.compareTo(adapter2.codecName),
|
||||
constructedAdapters);
|
||||
for (int i = 0; i < sortedAdapters.size(); i++) {
|
||||
sortedAdapters.get(i).dump(dumper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final MediaCodecAdapter delegate;
|
||||
// TODO(internal b/175710547): Consider using MediaCodecInfo, but currently Robolectric (v4.5)
|
||||
// doesn't correctly implement MediaCodec#getCodecInfo() (getName() works).
|
||||
private final String codecName;
|
||||
|
||||
/**
|
||||
* The client-owned buffers, keyed by the index used by {@link #dequeueInputBufferIndex()} and
|
||||
* {@link #getInputBuffer(int)}.
|
||||
*/
|
||||
private final SparseArray<ByteBuffer> dequeuedInputBuffers;
|
||||
|
||||
/** All interactions recorded with this adapter. */
|
||||
private final List<CapturedInteraction> capturedInteractions;
|
||||
|
||||
private final AtomicBoolean isReleased;
|
||||
|
||||
private CapturingMediaCodecAdapter(MediaCodecAdapter delegate, String codecName) {
|
||||
this.delegate = delegate;
|
||||
this.codecName = codecName;
|
||||
dequeuedInputBuffers = new SparseArray<>();
|
||||
capturedInteractions = new ArrayList<>();
|
||||
isReleased = new AtomicBoolean();
|
||||
}
|
||||
|
||||
// MediaCodecAdapter implementation
|
||||
|
||||
@Override
|
||||
public void configure(
|
||||
@Nullable MediaFormat mediaFormat,
|
||||
@Nullable Surface surface,
|
||||
@Nullable MediaCrypto crypto,
|
||||
int flags) {
|
||||
delegate.configure(mediaFormat, surface, crypto, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
delegate.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueInputBufferIndex() {
|
||||
return delegate.dequeueInputBufferIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
return delegate.dequeueOutputBufferIndex(bufferInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getOutputFormat() {
|
||||
return delegate.getOutputFormat();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ByteBuffer getInputBuffer(int index) {
|
||||
@Nullable ByteBuffer inputBuffer = delegate.getInputBuffer(index);
|
||||
if (inputBuffer != null) {
|
||||
dequeuedInputBuffers.put(index, inputBuffer);
|
||||
}
|
||||
return inputBuffer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ByteBuffer getOutputBuffer(int index) {
|
||||
return delegate.getOutputBuffer(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInputBuffer(
|
||||
int index, int offset, int size, long presentationTimeUs, int flags) {
|
||||
ByteBuffer inputBuffer = checkNotNull(dequeuedInputBuffers.get(index));
|
||||
capturedInteractions.add(new CapturedInputBuffer(peekBytes(inputBuffer, offset, size)));
|
||||
|
||||
delegate.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
|
||||
dequeuedInputBuffers.delete(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueSecureInputBuffer(
|
||||
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) {
|
||||
delegate.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseOutputBuffer(int index, boolean render) {
|
||||
delegate.releaseOutputBuffer(index, render);
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
@Override
|
||||
public void releaseOutputBuffer(int index, long renderTimeStampNs) {
|
||||
delegate.releaseOutputBuffer(index, renderTimeStampNs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
dequeuedInputBuffers.clear();
|
||||
delegate.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
dequeuedInputBuffers.clear();
|
||||
isReleased.set(true);
|
||||
delegate.release();
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
@Override
|
||||
public void setOnFrameRenderedListener(OnFrameRenderedListener listener, Handler handler) {
|
||||
delegate.setOnFrameRenderedListener(listener, handler);
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
@Override
|
||||
public void setOutputSurface(Surface surface) {
|
||||
delegate.setOutputSurface(surface);
|
||||
}
|
||||
|
||||
@RequiresApi(19)
|
||||
@Override
|
||||
public void setParameters(Bundle params) {
|
||||
delegate.setParameters(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoScalingMode(int scalingMode) {
|
||||
delegate.setVideoScalingMode(scalingMode);
|
||||
}
|
||||
|
||||
// Dumpable implementation
|
||||
|
||||
@Override
|
||||
public void dump(Dumper dumper) {
|
||||
checkState(isReleased.get());
|
||||
|
||||
dumper.startBlock("MediaCodecAdapter (" + codecName + ")");
|
||||
// TODO: Update this when capturedInteractions contains more than just input buffers.
|
||||
dumper.add("buffers.length", capturedInteractions.size());
|
||||
for (int i = 0; i < capturedInteractions.size(); i++) {
|
||||
CapturedInputBuffer inputBuffer = (CapturedInputBuffer) capturedInteractions.get(i);
|
||||
dumper.add("buffers[" + i + "]", inputBuffer.contents);
|
||||
}
|
||||
dumper.endBlock();
|
||||
}
|
||||
|
||||
private static byte[] peekBytes(ByteBuffer buffer, int offset, int size) {
|
||||
int originalPosition = buffer.position();
|
||||
buffer.position(offset);
|
||||
byte[] bytes = new byte[size];
|
||||
buffer.get(bytes);
|
||||
buffer.position(originalPosition);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/** A marker interface for different interactions with {@link CapturingMediaCodecAdapter}. */
|
||||
private interface CapturedInteraction {}
|
||||
|
||||
/**
|
||||
* Records the data passed to {@link CapturingMediaCodecAdapter#queueInputBuffer(int, int, int,
|
||||
* long, int)}.
|
||||
*/
|
||||
private static class CapturedInputBuffer implements CapturedInteraction {
|
||||
// TODO: Add other fields
|
||||
private final byte[] contents;
|
||||
|
||||
private CapturedInputBuffer(byte[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user