End to end playback test for gapless playback

In the test, a real instance of SimpleExoplayer plays two identical Mp3 files.
The GaplessMp3Decoder will write randomized data to decoder output on receiving
input. The test compares the bytes written by the decoder with the bytes
received by the AudioTrack, to verify that the trimming of encoder delay/
padding is correctly carried out.

Test mp3 has delay 576 frames and padding 1404 frames. File generated from:
ffmpeg -f lavfi -i "sine=frequency=1000:duration=1" test.mp3

This change needs robolectric version 4.5, which is not currently released (2020 Sep 30).

PiperOrigin-RevId: 334648486
This commit is contained in:
claincly 2020-09-30 19:59:39 +01:00 committed by kim-vde
parent 8cee5c5f6b
commit 4d3a781ca4
4 changed files with 245 additions and 1 deletions

View File

@ -24,7 +24,7 @@ project.ext {
guavaVersion = '27.1-android'
mockitoVersion = '2.28.2'
mockWebServerVersion = '3.12.0'
robolectricVersion = '4.4'
robolectricVersion = '4.5-SNAPSHOT'
checkerframeworkVersion = '3.3.0'
checkerframeworkCompatVersion = '2.5.0'
jsr305Version = '3.0.2'

View File

@ -0,0 +1,150 @@
/*
* Copyright 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.e2etest;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Integer.max;
import android.media.AudioFormat;
import android.media.MediaFormat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.robolectric.RandomizedMp3Decoder;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.TestExoPlayer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Bytes;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.MediaCodecInfoBuilder;
import org.robolectric.shadows.ShadowAudioTrack;
import org.robolectric.shadows.ShadowMediaCodec;
import org.robolectric.shadows.ShadowMediaCodecList;
/** End to end playback test for gapless audio playbacks. */
@RunWith(AndroidJUnit4.class)
@Config(sdk = 29)
public class EndToEndGaplessTest {
private static final int CODEC_INPUT_BUFFER_SIZE = 5120;
private static final int CODEC_OUTPUT_BUFFER_SIZE = 5120;
private static final String DECODER_NAME = "RandomizedMp3Decoder";
private RandomizedMp3Decoder mp3Decoder;
private AudioTrackListener audioTrackListener;
@Before
public void setUp() throws Exception {
audioTrackListener = new AudioTrackListener();
ShadowAudioTrack.addAudioDataListener(audioTrackListener);
mp3Decoder = new RandomizedMp3Decoder();
ShadowMediaCodec.addDecoder(
DECODER_NAME,
new ShadowMediaCodec.CodecConfig(
CODEC_INPUT_BUFFER_SIZE, CODEC_OUTPUT_BUFFER_SIZE, mp3Decoder));
MediaFormat mp3Format = new MediaFormat();
mp3Format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_MPEG);
ShadowMediaCodecList.addCodec(
MediaCodecInfoBuilder.newBuilder()
.setName(DECODER_NAME)
.setCapabilities(
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
.setMediaFormat(mp3Format)
.build())
.build());
}
@Test
public void testPlayback_twoIdenticalMp3Files() throws Exception {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
.setClock(new AutoAdvancingFakeClock())
.build();
player.setMediaItems(
ImmutableList.of(
MediaItem.fromUri("asset:///media/mp3/test.mp3"),
MediaItem.fromUri("asset:///media/mp3/test.mp3")));
player.prepare();
player.play();
TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED);
Format playerAudioFormat = player.getAudioFormat();
assertThat(playerAudioFormat).isNotNull();
int bytesPerFrame = audioTrackListener.getAudioTrackOutputFormat().getFrameSizeInBytes();
int paddingBytes = max(0, playerAudioFormat.encoderPadding) * bytesPerFrame;
int delayBytes = max(0, playerAudioFormat.encoderDelay) * bytesPerFrame;
assertThat(paddingBytes).isEqualTo(2808);
assertThat(delayBytes).isEqualTo(1152);
byte[] decoderOutputBytes = Bytes.concat(mp3Decoder.getAllOutputBytes().toArray(new byte[0][]));
int bytesPerAudioFile = decoderOutputBytes.length / 2;
assertThat(bytesPerAudioFile).isEqualTo(92160);
byte[] expectedTrimmedByteContent =
Bytes.concat(
// Track one is trimmed at its beginning and its end.
Arrays.copyOfRange(decoderOutputBytes, delayBytes, bytesPerAudioFile - paddingBytes),
// Track two is only trimmed at its beginning, but not its end.
Arrays.copyOfRange(
decoderOutputBytes, bytesPerAudioFile + delayBytes, decoderOutputBytes.length));
byte[] audioTrackReceivedBytes = audioTrackListener.getAllReceivedBytes();
assertThat(audioTrackReceivedBytes).isEqualTo(expectedTrimmedByteContent);
}
private static class AudioTrackListener implements ShadowAudioTrack.OnAudioDataWrittenListener {
private final ByteArrayOutputStream audioTrackReceivedBytesStream = new ByteArrayOutputStream();
// Output format from the audioTrack.
private AudioFormat format;
private ShadowAudioTrack audioTrack;
@Override
public synchronized void onAudioDataWritten(
ShadowAudioTrack audioTrack, byte[] audioData, AudioFormat format) {
if (this.audioTrack == null) {
this.audioTrack = audioTrack;
} else {
Assertions.checkArgument(
audioTrack == this.audioTrack, "Data written from a different AudioTrack");
}
if (!format.equals(this.format)) {
this.format = format;
}
audioTrackReceivedBytesStream.write(audioData, 0, audioData.length);
}
public byte[] getAllReceivedBytes() {
return audioTrackReceivedBytesStream.toByteArray();
}
public AudioFormat getAudioTrackOutputFormat() {
return format;
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 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.robolectric;
import android.media.AudioFormat;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.view.Surface;
import com.google.android.exoplayer2.audio.MpegAudioUtil;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.robolectric.shadows.ShadowMediaCodec;
/**
* Generates randomized, but correct amount of data on MP3 audio input.
*
* <p>The decoder reads the MP3 header for each input MP3 frame, determines the number of bytes the
* input frame should inflate to, and writes randomized data of that amount to the output buffer.
* Decoder randomness can help us identify possible errors in downstream renderers and audio
* processors. The random bahavior is deterministic, it outputs the same bytes across multiple runs.
*
* <p>All the data written to the output by the decoder can be obtained by getAllOutputBytes().
*/
public final class RandomizedMp3Decoder implements ShadowMediaCodec.CodecConfig.Codec {
private final List<byte[]> decoderOutput = new ArrayList<>();
private int frameSizeInBytes;
@Override
public void process(ByteBuffer in, ByteBuffer out) {
if (in.remaining() == 0) {
// An empty frame will be queued by the MediaCodecRenderer on END_OF_STREAM.
return;
}
Assertions.checkState(
in.remaining() >= 4, "Frame size too small, should be at least 4 to hold an MP3 header");
// Get the desired output size for every input.
int headerDataBigEndian = Util.getBigEndianInt(in, in.position());
int frameCount = MpegAudioUtil.parseMpegAudioFrameSampleCount(headerDataBigEndian);
int expectedNumBytes = frameCount * frameSizeInBytes;
byte[] bytesToWrite = TestUtil.buildTestData(expectedNumBytes);
out.put(bytesToWrite);
decoderOutput.add(bytesToWrite);
in.position(in.limit());
}
@Override
public void onConfigured(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) {
// Both getInteger and getString require API29. This class is only used in EndToEndGaplessTest
// that only runs on
// API29.
int pcmEncoding =
format.getInteger(
MediaFormat.KEY_PCM_ENCODING, /* defaultValue= */ AudioFormat.ENCODING_PCM_16BIT);
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
Assertions.checkArgument(
format.getString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_MPEG).equals(MimeTypes.AUDIO_MPEG));
frameSizeInBytes = Util.getPcmFrameSize(pcmEncoding, channelCount);
}
/**
* Returns all arrays of bytes output from the decoder.
*
* @return a list of byte arrays (for each MP3 frame input) that were previously output from the
* decoder.
*/
public ImmutableList<byte[]> getAllOutputBytes() {
return ImmutableList.copyOf(decoderOutput);
}
}

Binary file not shown.