blob: b3c6bf7117ba10f905fa77c6258b3c2d619dd1e9 [file] [log] [blame]
/*
* Copyright (C) 2016 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.common.truth.Truth.assertThat;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.testutil.Dumper.Dumpable;
import com.google.android.exoplayer2.upstream.DataReader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Function;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* A fake {@link TrackOutput}.
*/
public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable {
private final ArrayList<DumpableSampleInfo> sampleInfos;
private final ArrayList<Dumpable> dumpables;
private byte[] sampleData;
private int formatCount;
private boolean receivedSampleInFormat;
@Nullable public Format lastFormat;
public FakeTrackOutput() {
sampleInfos = new ArrayList<>();
dumpables = new ArrayList<>();
sampleData = Util.EMPTY_BYTE_ARRAY;
formatCount = 0;
receivedSampleInFormat = true;
}
public void clear() {
sampleInfos.clear();
dumpables.clear();
sampleData = Util.EMPTY_BYTE_ARRAY;
formatCount = 0;
receivedSampleInFormat = true;
}
@Override
public void format(Format format) {
receivedSampleInFormat = false;
addFormat(format);
}
@Override
public int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException {
byte[] newData = new byte[length];
int bytesAppended = input.read(newData, 0, length);
if (bytesAppended == C.RESULT_END_OF_INPUT) {
if (allowEndOfInput) {
return C.RESULT_END_OF_INPUT;
}
throw new EOFException();
}
newData = Arrays.copyOf(newData, bytesAppended);
sampleData = TestUtil.joinByteArrays(sampleData, newData);
return bytesAppended;
}
@Override
public void sampleData(ParsableByteArray data, int length) {
byte[] newData = new byte[length];
data.readBytes(newData, 0, length);
sampleData = TestUtil.joinByteArrays(sampleData, newData);
}
@Override
public void sampleMetadata(
long timeUs,
@C.BufferFlags int flags,
int size,
int offset,
@Nullable CryptoData cryptoData) {
receivedSampleInFormat = true;
if (lastFormat == null) {
throw new IllegalStateException("TrackOutput must receive format before sampleMetadata");
}
if (lastFormat.maxInputSize != Format.NO_VALUE && size > lastFormat.maxInputSize) {
throw new IllegalStateException("Sample size exceeds Format.maxInputSize");
}
if (dumpables.isEmpty()) {
addFormat(lastFormat);
}
addSampleInfo(
timeUs, flags, sampleData.length - offset - size, sampleData.length - offset, cryptoData);
}
public void assertSampleCount(int count) {
assertThat(sampleInfos).hasSize(count);
}
public void assertSample(
int index, byte[] data, long timeUs, int flags, @Nullable CryptoData cryptoData) {
byte[] actualData = getSampleData(index);
assertThat(actualData).isEqualTo(data);
assertThat(getSampleTimeUs(index)).isEqualTo(timeUs);
assertThat(getSampleFlags(index)).isEqualTo(flags);
assertThat(getSampleCryptoData(index)).isEqualTo(cryptoData);
}
public byte[] getSampleData(int index) {
return Arrays.copyOfRange(sampleData, getSampleStartOffset(index), getSampleEndOffset(index));
}
private byte[] getSampleData(int fromIndex, int toIndex) {
return Arrays.copyOfRange(sampleData, fromIndex, toIndex);
}
public long getSampleTimeUs(int index) {
return sampleInfos.get(index).timeUs;
}
public int getSampleFlags(int index) {
return sampleInfos.get(index).flags;
}
@Nullable
public CryptoData getSampleCryptoData(int index) {
return sampleInfos.get(index).cryptoData;
}
public int getSampleCount() {
return sampleInfos.size();
}
public List<Long> getSampleTimesUs() {
List<Long> sampleTimesUs = new ArrayList<>();
for (DumpableSampleInfo sampleInfo : sampleInfos) {
sampleTimesUs.add(sampleInfo.timeUs);
}
return Collections.unmodifiableList(sampleTimesUs);
}
@Override
public void dump(Dumper dumper) {
dumper.add("total output bytes", sampleData.length);
dumper.add("sample count", sampleInfos.size());
if (dumpables.isEmpty() && lastFormat != null) {
new DumpableFormat(lastFormat, 0).dump(dumper);
}
for (int i = 0; i < dumpables.size(); i++) {
dumpables.get(i).dump(dumper);
}
}
private int getSampleStartOffset(int index) {
return sampleInfos.get(index).startOffset;
}
private int getSampleEndOffset(int index) {
return sampleInfos.get(index).endOffset;
}
private void addFormat(Format format) {
lastFormat = format;
dumpables.add(new DumpableFormat(format, formatCount));
formatCount++;
}
private void addSampleInfo(
long timeUs, int flags, int startOffset, int endOffset, @Nullable CryptoData cryptoData) {
DumpableSampleInfo sampleInfo =
new DumpableSampleInfo(timeUs, flags, startOffset, endOffset, cryptoData, getSampleCount());
sampleInfos.add(sampleInfo);
dumpables.add(sampleInfo);
}
private final class DumpableSampleInfo implements Dumper.Dumpable {
public final long timeUs;
public final int flags;
public final int startOffset;
public final int endOffset;
@Nullable public final CryptoData cryptoData;
public final int index;
public DumpableSampleInfo(
long timeUs,
int flags,
int startOffset,
int endOffset,
@Nullable CryptoData cryptoData,
int index) {
this.timeUs = timeUs;
this.flags = flags;
this.startOffset = startOffset;
this.endOffset = endOffset;
this.cryptoData = cryptoData;
this.index = index;
}
@Override
public void dump(Dumper dumper) {
dumper
.startBlock("sample " + index)
.add("time", timeUs)
.add("flags", flags)
.add("data", getSampleData(startOffset, endOffset));
if (cryptoData != null) {
dumper.add("crypto mode", cryptoData.cryptoMode);
dumper.add("encryption key", cryptoData.encryptionKey);
}
dumper.endBlock();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DumpableSampleInfo that = (DumpableSampleInfo) o;
return timeUs == that.timeUs
&& flags == that.flags
&& startOffset == that.startOffset
&& endOffset == that.endOffset
&& index == that.index
&& Util.areEqual(cryptoData, that.cryptoData);
}
@Override
public int hashCode() {
int result = (int) timeUs;
result = 31 * result + flags;
result = 31 * result + startOffset;
result = 31 * result + endOffset;
result = 31 * result + (cryptoData == null ? 0 : cryptoData.hashCode());
result = 31 * result + index;
return result;
}
}
private static final class DumpableFormat implements Dumper.Dumpable {
private final Format format;
public final int index;
private static final Format DEFAULT_FORMAT = new Format.Builder().build();
public DumpableFormat(Format format, int index) {
this.format = format;
this.index = index;
}
@Override
public void dump(Dumper dumper) {
dumper.startBlock("format " + index);
addIfNonDefault(dumper, "averageBitrate", format -> format.averageBitrate);
addIfNonDefault(dumper, "peakBitrate", format -> format.peakBitrate);
addIfNonDefault(dumper, "id", format -> format.id);
addIfNonDefault(dumper, "containerMimeType", format -> format.containerMimeType);
addIfNonDefault(dumper, "sampleMimeType", format -> format.sampleMimeType);
addIfNonDefault(dumper, "codecs", format -> format.codecs);
addIfNonDefault(dumper, "maxInputSize", format -> format.maxInputSize);
addIfNonDefault(dumper, "width", format -> format.width);
addIfNonDefault(dumper, "height", format -> format.height);
addIfNonDefault(dumper, "frameRate", format -> format.frameRate);
addIfNonDefault(dumper, "rotationDegrees", format -> format.rotationDegrees);
addIfNonDefault(dumper, "pixelWidthHeightRatio", format -> format.pixelWidthHeightRatio);
addIfNonDefault(dumper, "channelCount", format -> format.channelCount);
addIfNonDefault(dumper, "sampleRate", format -> format.sampleRate);
addIfNonDefault(dumper, "pcmEncoding", format -> format.pcmEncoding);
addIfNonDefault(dumper, "encoderDelay", format -> format.encoderDelay);
addIfNonDefault(dumper, "encoderPadding", format -> format.encoderPadding);
addIfNonDefault(dumper, "subsampleOffsetUs", format -> format.subsampleOffsetUs);
addIfNonDefault(dumper, "selectionFlags", format -> format.selectionFlags);
addIfNonDefault(dumper, "language", format -> format.language);
if (format.drmInitData != null) {
dumper.add("drmInitData", format.drmInitData.hashCode());
}
addIfNonDefault(dumper, "metadata", format -> format.metadata);
if (!format.initializationData.isEmpty()) {
dumper.startBlock("initializationData");
for (int i = 0; i < format.initializationData.size(); i++) {
dumper.add("data", format.initializationData.get(i));
}
dumper.endBlock();
}
dumper.endBlock();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DumpableFormat that = (DumpableFormat) o;
return index == that.index && format.equals(that.format);
}
@Override
public int hashCode() {
int result = format.hashCode();
result = 31 * result + index;
return result;
}
private void addIfNonDefault(
Dumper dumper, String field, Function<Format, @NullableType Object> getFieldFunction) {
@Nullable Object thisValue = getFieldFunction.apply(format);
@Nullable Object defaultValue = getFieldFunction.apply(DEFAULT_FORMAT);
if (!Util.areEqual(thisValue, defaultValue)) {
dumper.add(field, thisValue);
}
}
}
}