mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Fix SampleQueue splicing when sampleOffsetUs != 0
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=212432661
This commit is contained in:
parent
ba8c22aba5
commit
6c3c71b554
2 changed files with 200 additions and 107 deletions
|
|
@ -577,13 +577,13 @@ public final class SampleQueue implements TrackOutput {
|
||||||
if (pendingFormatAdjustment) {
|
if (pendingFormatAdjustment) {
|
||||||
format(lastUnadjustedFormat);
|
format(lastUnadjustedFormat);
|
||||||
}
|
}
|
||||||
|
timeUs += sampleOffsetUs;
|
||||||
if (pendingSplice) {
|
if (pendingSplice) {
|
||||||
if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {
|
if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingSplice = false;
|
pendingSplice = false;
|
||||||
}
|
}
|
||||||
timeUs += sampleOffsetUs;
|
|
||||||
long absoluteOffset = totalBytesWritten - size - offset;
|
long absoluteOffset = totalBytesWritten - size - offset;
|
||||||
metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
|
metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static java.lang.Long.MIN_VALUE;
|
import static java.lang.Long.MIN_VALUE;
|
||||||
import static java.util.Arrays.copyOfRange;
|
import static java.util.Arrays.copyOfRange;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.FormatHolder;
|
import com.google.android.exoplayer2.FormatHolder;
|
||||||
|
|
@ -45,41 +46,51 @@ public final class SampleQueueTest {
|
||||||
|
|
||||||
private static final int ALLOCATION_SIZE = 16;
|
private static final int ALLOCATION_SIZE = 16;
|
||||||
|
|
||||||
private static final Format TEST_FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0);
|
private static final Format FORMAT_1 = Format.createSampleFormat("1", "mimeType", 0);
|
||||||
private static final Format TEST_FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0);
|
private static final Format FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0);
|
||||||
private static final Format TEST_FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0);
|
private static final Format FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0);
|
||||||
private static final byte[] TEST_DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
|
private static final Format FORMAT_SPLICED = Format.createSampleFormat("spliced", "mimeType", 0);
|
||||||
|
private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TEST_SAMPLE_SIZES and TEST_SAMPLE_OFFSETS are intended to test various boundary cases (with
|
* SAMPLE_SIZES and SAMPLE_OFFSETS are intended to test various boundary cases (with
|
||||||
* respect to the allocation size). TEST_SAMPLE_OFFSETS values are defined as the backward offsets
|
* respect to the allocation size). SAMPLE_OFFSETS values are defined as the backward offsets
|
||||||
* (as expected by SampleQueue.sampleMetadata) assuming that TEST_DATA has been written to the
|
* (as expected by SampleQueue.sampleMetadata) assuming that DATA has been written to the
|
||||||
* sampleQueue in full. The allocations are filled as follows, where | indicates a boundary
|
* sampleQueue in full. The allocations are filled as follows, where | indicates a boundary
|
||||||
* between allocations and x indicates a byte that doesn't belong to a sample:
|
* between allocations and x indicates a byte that doesn't belong to a sample:
|
||||||
*
|
*
|
||||||
* x<s1>|x<s2>x|x<s3>|<s4>x|<s5>|<s6|s6>|x<s7|s7>x|<s8>
|
* x<s1>|x<s2>x|x<s3>|<s4>x|<s5>|<s6|s6>|x<s7|s7>x|<s8>
|
||||||
*/
|
*/
|
||||||
private static final int[] TEST_SAMPLE_SIZES = new int[] {
|
private static final int[] SAMPLE_SIZES =
|
||||||
ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 2, ALLOCATION_SIZE - 1, ALLOCATION_SIZE - 1,
|
new int[] {
|
||||||
ALLOCATION_SIZE, ALLOCATION_SIZE * 2, ALLOCATION_SIZE * 2 - 2, ALLOCATION_SIZE
|
ALLOCATION_SIZE - 1,
|
||||||
|
ALLOCATION_SIZE - 2,
|
||||||
|
ALLOCATION_SIZE - 1,
|
||||||
|
ALLOCATION_SIZE - 1,
|
||||||
|
ALLOCATION_SIZE,
|
||||||
|
ALLOCATION_SIZE * 2,
|
||||||
|
ALLOCATION_SIZE * 2 - 2,
|
||||||
|
ALLOCATION_SIZE
|
||||||
};
|
};
|
||||||
private static final int[] TEST_SAMPLE_OFFSETS = new int[] {
|
private static final int[] SAMPLE_OFFSETS =
|
||||||
ALLOCATION_SIZE * 9, ALLOCATION_SIZE * 8 + 1, ALLOCATION_SIZE * 7, ALLOCATION_SIZE * 6 + 1,
|
new int[] {
|
||||||
ALLOCATION_SIZE * 5, ALLOCATION_SIZE * 3, ALLOCATION_SIZE + 1, 0
|
ALLOCATION_SIZE * 9,
|
||||||
|
ALLOCATION_SIZE * 8 + 1,
|
||||||
|
ALLOCATION_SIZE * 7,
|
||||||
|
ALLOCATION_SIZE * 6 + 1,
|
||||||
|
ALLOCATION_SIZE * 5,
|
||||||
|
ALLOCATION_SIZE * 3,
|
||||||
|
ALLOCATION_SIZE + 1,
|
||||||
|
0
|
||||||
};
|
};
|
||||||
private static final long[] TEST_SAMPLE_TIMESTAMPS = new long[] {
|
private static final long[] SAMPLE_TIMESTAMPS =
|
||||||
0, 1000, 2000, 3000, 4000, 5000, 6000, 7000
|
new long[] {0, 1000, 2000, 3000, 4000, 5000, 6000, 7000};
|
||||||
};
|
private static final long LAST_SAMPLE_TIMESTAMP = SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 1];
|
||||||
private static final long LAST_SAMPLE_TIMESTAMP =
|
private static final int[] SAMPLE_FLAGS =
|
||||||
TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 1];
|
new int[] {C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0};
|
||||||
private static final int[] TEST_SAMPLE_FLAGS = new int[] {
|
private static final Format[] SAMPLE_FORMATS =
|
||||||
C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0, C.BUFFER_FLAG_KEY_FRAME, 0, 0, 0
|
new Format[] {FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_2, FORMAT_2, FORMAT_2, FORMAT_2};
|
||||||
};
|
private static final int DATA_SECOND_KEYFRAME_INDEX = 4;
|
||||||
private static final Format[] TEST_SAMPLE_FORMATS = new Format[] {
|
|
||||||
TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_1, TEST_FORMAT_2, TEST_FORMAT_2,
|
|
||||||
TEST_FORMAT_2, TEST_FORMAT_2
|
|
||||||
};
|
|
||||||
private static final int TEST_DATA_SECOND_KEYFRAME_INDEX = 4;
|
|
||||||
|
|
||||||
private Allocator allocator;
|
private Allocator allocator;
|
||||||
private SampleQueue sampleQueue;
|
private SampleQueue sampleQueue;
|
||||||
|
|
@ -117,37 +128,37 @@ public final class SampleQueueTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadFormatDeduplicated() {
|
public void testReadFormatDeduplicated() {
|
||||||
sampleQueue.format(TEST_FORMAT_1);
|
sampleQueue.format(FORMAT_1);
|
||||||
assertReadFormat(false, TEST_FORMAT_1);
|
assertReadFormat(false, FORMAT_1);
|
||||||
// If the same format is input then it should be de-duplicated (i.e. not output again).
|
// If the same format is input then it should be de-duplicated (i.e. not output again).
|
||||||
sampleQueue.format(TEST_FORMAT_1);
|
sampleQueue.format(FORMAT_1);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_1);
|
assertNoSamplesToRead(FORMAT_1);
|
||||||
// The same applies for a format that's equal (but a different object).
|
// The same applies for a format that's equal (but a different object).
|
||||||
sampleQueue.format(TEST_FORMAT_1_COPY);
|
sampleQueue.format(FORMAT_1_COPY);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_1);
|
assertNoSamplesToRead(FORMAT_1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadSingleSamples() {
|
public void testReadSingleSamples() {
|
||||||
sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
|
sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE);
|
||||||
|
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
// Nothing to read.
|
// Nothing to read.
|
||||||
assertNoSamplesToRead(null);
|
assertNoSamplesToRead(null);
|
||||||
|
|
||||||
sampleQueue.format(TEST_FORMAT_1);
|
sampleQueue.format(FORMAT_1);
|
||||||
|
|
||||||
// Read the format.
|
// Read the format.
|
||||||
assertReadFormat(false, TEST_FORMAT_1);
|
assertReadFormat(false, FORMAT_1);
|
||||||
// Nothing to read.
|
// Nothing to read.
|
||||||
assertNoSamplesToRead(TEST_FORMAT_1);
|
assertNoSamplesToRead(FORMAT_1);
|
||||||
|
|
||||||
sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
|
sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
|
||||||
|
|
||||||
// If formatRequired, should read the format rather than the sample.
|
// If formatRequired, should read the format rather than the sample.
|
||||||
assertReadFormat(true, TEST_FORMAT_1);
|
assertReadFormat(true, FORMAT_1);
|
||||||
// Otherwise should read the sample.
|
// Otherwise should read the sample.
|
||||||
assertSampleRead(1000, true, TEST_DATA, 0, ALLOCATION_SIZE);
|
assertReadSample(1000, true, DATA, 0, ALLOCATION_SIZE);
|
||||||
// Allocation should still be held.
|
// Allocation should still be held.
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardToRead();
|
sampleQueue.discardToRead();
|
||||||
|
|
@ -155,16 +166,16 @@ public final class SampleQueueTest {
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
|
|
||||||
// Nothing to read.
|
// Nothing to read.
|
||||||
assertNoSamplesToRead(TEST_FORMAT_1);
|
assertNoSamplesToRead(FORMAT_1);
|
||||||
|
|
||||||
// Write a second sample followed by one byte that does not belong to it.
|
// Write a second sample followed by one byte that does not belong to it.
|
||||||
sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
|
sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE);
|
||||||
sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null);
|
sampleQueue.sampleMetadata(2000, 0, ALLOCATION_SIZE - 1, 1, null);
|
||||||
|
|
||||||
// If formatRequired, should read the format rather than the sample.
|
// If formatRequired, should read the format rather than the sample.
|
||||||
assertReadFormat(true, TEST_FORMAT_1);
|
assertReadFormat(true, FORMAT_1);
|
||||||
// Read the sample.
|
// Read the sample.
|
||||||
assertSampleRead(2000, false, TEST_DATA, 0, ALLOCATION_SIZE - 1);
|
assertReadSample(2000, false, DATA, 0, ALLOCATION_SIZE - 1);
|
||||||
// Allocation should still be held.
|
// Allocation should still be held.
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardToRead();
|
sampleQueue.discardToRead();
|
||||||
|
|
@ -176,9 +187,9 @@ public final class SampleQueueTest {
|
||||||
sampleQueue.sampleMetadata(3000, 0, 1, 0, null);
|
sampleQueue.sampleMetadata(3000, 0, 1, 0, null);
|
||||||
|
|
||||||
// If formatRequired, should read the format rather than the sample.
|
// If formatRequired, should read the format rather than the sample.
|
||||||
assertReadFormat(true, TEST_FORMAT_1);
|
assertReadFormat(true, FORMAT_1);
|
||||||
// Read the sample.
|
// Read the sample.
|
||||||
assertSampleRead(3000, false, TEST_DATA, ALLOCATION_SIZE - 1, 1);
|
assertReadSample(3000, false, DATA, ALLOCATION_SIZE - 1, 1);
|
||||||
// Allocation should still be held.
|
// Allocation should still be held.
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardToRead();
|
sampleQueue.discardToRead();
|
||||||
|
|
@ -202,8 +213,8 @@ public final class SampleQueueTest {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
writeTestData();
|
writeTestData();
|
||||||
assertAllocationCount(20);
|
assertAllocationCount(20);
|
||||||
assertReadTestData(TEST_FORMAT_2);
|
assertReadTestData(FORMAT_2);
|
||||||
assertReadTestData(TEST_FORMAT_2);
|
assertReadTestData(FORMAT_2);
|
||||||
assertAllocationCount(20);
|
assertAllocationCount(20);
|
||||||
sampleQueue.discardToRead();
|
sampleQueue.discardToRead();
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
|
|
@ -251,15 +262,15 @@ public final class SampleQueueTest {
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
// Despite skipping all samples, we should still read the last format, since this is the
|
// Despite skipping all samples, we should still read the last format, since this is the
|
||||||
// expected format for a subsequent sample.
|
// expected format for a subsequent sample.
|
||||||
assertReadFormat(false, TEST_FORMAT_2);
|
assertReadFormat(false, FORMAT_2);
|
||||||
// Once the format has been read, there's nothing else to read.
|
// Once the format has been read, there's nothing else to read.
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAdvanceToEndRetainsUnassignedData() {
|
public void testAdvanceToEndRetainsUnassignedData() {
|
||||||
sampleQueue.format(TEST_FORMAT_1);
|
sampleQueue.format(FORMAT_1);
|
||||||
sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE);
|
sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE);
|
||||||
sampleQueue.advanceToEnd();
|
sampleQueue.advanceToEnd();
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardToRead();
|
sampleQueue.discardToRead();
|
||||||
|
|
@ -267,14 +278,14 @@ public final class SampleQueueTest {
|
||||||
// written.
|
// written.
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
// We should be able to read the format.
|
// We should be able to read the format.
|
||||||
assertReadFormat(false, TEST_FORMAT_1);
|
assertReadFormat(false, FORMAT_1);
|
||||||
// Once the format has been read, there's nothing else to read.
|
// Once the format has been read, there's nothing else to read.
|
||||||
assertNoSamplesToRead(TEST_FORMAT_1);
|
assertNoSamplesToRead(FORMAT_1);
|
||||||
|
|
||||||
sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
|
sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null);
|
||||||
// Once the metadata has been written, check the sample can be read as expected.
|
// Once the metadata has been written, check the sample can be read as expected.
|
||||||
assertSampleRead(0, true, TEST_DATA, 0, ALLOCATION_SIZE);
|
assertReadSample(0, true, DATA, 0, ALLOCATION_SIZE);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_1);
|
assertNoSamplesToRead(FORMAT_1);
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardToRead();
|
sampleQueue.discardToRead();
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
|
|
@ -283,21 +294,21 @@ public final class SampleQueueTest {
|
||||||
@Test
|
@Test
|
||||||
public void testAdvanceToBeforeBuffer() {
|
public void testAdvanceToBeforeBuffer() {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
|
int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0] - 1, true, false);
|
||||||
// Should fail and have no effect.
|
// Should fail and have no effect.
|
||||||
assertThat(skipCount).isEqualTo(ADVANCE_FAILED);
|
assertThat(skipCount).isEqualTo(ADVANCE_FAILED);
|
||||||
assertReadTestData();
|
assertReadTestData();
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAdvanceToStartOfBuffer() {
|
public void testAdvanceToStartOfBuffer() {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
|
int skipCount = sampleQueue.advanceTo(SAMPLE_TIMESTAMPS[0], true, false);
|
||||||
// Should succeed but have no effect (we're already at the first frame).
|
// Should succeed but have no effect (we're already at the first frame).
|
||||||
assertThat(skipCount).isEqualTo(0);
|
assertThat(skipCount).isEqualTo(0);
|
||||||
assertReadTestData();
|
assertReadTestData();
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -306,8 +317,8 @@ public final class SampleQueueTest {
|
||||||
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
|
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
|
||||||
// Should succeed and skip to 2nd keyframe (the 4th frame).
|
// Should succeed and skip to 2nd keyframe (the 4th frame).
|
||||||
assertThat(skipCount).isEqualTo(4);
|
assertThat(skipCount).isEqualTo(4);
|
||||||
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
|
assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -317,7 +328,7 @@ public final class SampleQueueTest {
|
||||||
// Should fail and have no effect.
|
// Should fail and have no effect.
|
||||||
assertThat(skipCount).isEqualTo(ADVANCE_FAILED);
|
assertThat(skipCount).isEqualTo(ADVANCE_FAILED);
|
||||||
assertReadTestData();
|
assertReadTestData();
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -326,8 +337,8 @@ public final class SampleQueueTest {
|
||||||
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
|
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
|
||||||
// Should succeed and skip to 2nd keyframe (the 4th frame).
|
// Should succeed and skip to 2nd keyframe (the 4th frame).
|
||||||
assertThat(skipCount).isEqualTo(4);
|
assertThat(skipCount).isEqualTo(4);
|
||||||
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
|
assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -339,10 +350,10 @@ public final class SampleQueueTest {
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(8);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(8);
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
// We should still be able to read the upstream format.
|
// We should still be able to read the upstream format.
|
||||||
assertReadFormat(false, TEST_FORMAT_2);
|
assertReadFormat(false, FORMAT_2);
|
||||||
// We should be able to write and read subsequent samples.
|
// We should be able to write and read subsequent samples.
|
||||||
writeTestData();
|
writeTestData();
|
||||||
assertReadTestData(TEST_FORMAT_2);
|
assertReadTestData(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -356,12 +367,12 @@ public final class SampleQueueTest {
|
||||||
// Read the first sample.
|
// Read the first sample.
|
||||||
assertReadTestData(null, 0, 1);
|
assertReadTestData(null, 0, 1);
|
||||||
// Shouldn't discard anything.
|
// Shouldn't discard anything.
|
||||||
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true);
|
sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1] - 1, false, true);
|
||||||
assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
||||||
assertAllocationCount(10);
|
assertAllocationCount(10);
|
||||||
// Should discard the read sample.
|
// Should discard the read sample.
|
||||||
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true);
|
sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1], false, true);
|
||||||
assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
||||||
assertAllocationCount(9);
|
assertAllocationCount(9);
|
||||||
|
|
@ -371,7 +382,7 @@ public final class SampleQueueTest {
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
||||||
assertAllocationCount(9);
|
assertAllocationCount(9);
|
||||||
// Should be able to read the remaining samples.
|
// Should be able to read the remaining samples.
|
||||||
assertReadTestData(TEST_FORMAT_1, 1, 7);
|
assertReadTestData(FORMAT_1, 1, 7);
|
||||||
assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(8);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(8);
|
||||||
// Should discard up to the second last sample
|
// Should discard up to the second last sample
|
||||||
|
|
@ -390,17 +401,17 @@ public final class SampleQueueTest {
|
||||||
public void testDiscardToDontStopAtReadPosition() {
|
public void testDiscardToDontStopAtReadPosition() {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
// Shouldn't discard anything.
|
// Shouldn't discard anything.
|
||||||
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false);
|
sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1] - 1, false, false);
|
||||||
assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(0);
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(0);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(0);
|
||||||
assertAllocationCount(10);
|
assertAllocationCount(10);
|
||||||
// Should discard the first sample.
|
// Should discard the first sample.
|
||||||
sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false);
|
sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1], false, false);
|
||||||
assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(1);
|
||||||
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
assertThat(sampleQueue.getReadIndex()).isEqualTo(1);
|
||||||
assertAllocationCount(9);
|
assertAllocationCount(9);
|
||||||
// Should be able to read the remaining samples.
|
// Should be able to read the remaining samples.
|
||||||
assertReadTestData(TEST_FORMAT_1, 1, 7);
|
assertReadTestData(FORMAT_1, 1, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -424,8 +435,8 @@ public final class SampleQueueTest {
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardUpstreamSamples(0);
|
sampleQueue.discardUpstreamSamples(0);
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
assertReadFormat(false, TEST_FORMAT_2);
|
assertReadFormat(false, FORMAT_2);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -435,8 +446,8 @@ public final class SampleQueueTest {
|
||||||
assertAllocationCount(4);
|
assertAllocationCount(4);
|
||||||
sampleQueue.discardUpstreamSamples(0);
|
sampleQueue.discardUpstreamSamples(0);
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
assertReadFormat(false, TEST_FORMAT_2);
|
assertReadFormat(false, FORMAT_2);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -445,8 +456,8 @@ public final class SampleQueueTest {
|
||||||
sampleQueue.discardUpstreamSamples(4);
|
sampleQueue.discardUpstreamSamples(4);
|
||||||
assertAllocationCount(4);
|
assertAllocationCount(4);
|
||||||
assertReadTestData(null, 0, 4);
|
assertReadTestData(null, 0, 4);
|
||||||
assertReadFormat(false, TEST_FORMAT_2);
|
assertReadFormat(false, FORMAT_2);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -467,18 +478,18 @@ public final class SampleQueueTest {
|
||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
sampleQueue.discardUpstreamSamples(3);
|
sampleQueue.discardUpstreamSamples(3);
|
||||||
assertAllocationCount(0);
|
assertAllocationCount(0);
|
||||||
assertReadFormat(false, TEST_FORMAT_2);
|
assertReadFormat(false, FORMAT_2);
|
||||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
assertNoSamplesToRead(FORMAT_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLargestQueuedTimestampWithDiscardUpstream() {
|
public void testLargestQueuedTimestampWithDiscardUpstream() {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
|
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
|
||||||
sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 1);
|
sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 1);
|
||||||
// Discarding from upstream should reduce the largest timestamp.
|
// Discarding from upstream should reduce the largest timestamp.
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs())
|
assertThat(sampleQueue.getLargestQueuedTimestampUs())
|
||||||
.isEqualTo(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2]);
|
.isEqualTo(SAMPLE_TIMESTAMPS[SAMPLE_TIMESTAMPS.length - 2]);
|
||||||
sampleQueue.discardUpstreamSamples(0);
|
sampleQueue.discardUpstreamSamples(0);
|
||||||
// Discarding everything from upstream without reading should unset the largest timestamp.
|
// Discarding everything from upstream without reading should unset the largest timestamp.
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE);
|
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE);
|
||||||
|
|
@ -487,14 +498,14 @@ public final class SampleQueueTest {
|
||||||
@Test
|
@Test
|
||||||
public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() {
|
public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() {
|
||||||
long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};
|
long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000};
|
||||||
writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, decodeOrderTimestamps,
|
writeTestData(
|
||||||
TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
|
DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, decodeOrderTimestamps, SAMPLE_FORMATS, SAMPLE_FLAGS);
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);
|
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);
|
||||||
sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 2);
|
sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 2);
|
||||||
// Discarding the last two samples should not change the largest timestamp, due to the decode
|
// Discarding the last two samples should not change the largest timestamp, due to the decode
|
||||||
// ordering of the timestamps.
|
// ordering of the timestamps.
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);
|
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000);
|
||||||
sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3);
|
sampleQueue.discardUpstreamSamples(SAMPLE_TIMESTAMPS.length - 3);
|
||||||
// Once a third sample is discarded, the largest timestamp should have changed.
|
// Once a third sample is discarded, the largest timestamp should have changed.
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000);
|
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000);
|
||||||
sampleQueue.discardUpstreamSamples(0);
|
sampleQueue.discardUpstreamSamples(0);
|
||||||
|
|
@ -511,21 +522,77 @@ public final class SampleQueueTest {
|
||||||
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
|
assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetSampleOffset() {
|
||||||
|
long sampleOffsetUs = 1000;
|
||||||
|
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
|
||||||
|
writeTestData();
|
||||||
|
assertReadTestData(null, 0, 8, sampleOffsetUs);
|
||||||
|
assertReadEndOfStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSplice() {
|
||||||
|
writeTestData();
|
||||||
|
sampleQueue.splice();
|
||||||
|
// Splice should succeed, replacing the last 4 samples with the sample being written.
|
||||||
|
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
|
assertReadTestData(null, 0, 4);
|
||||||
|
assertReadFormat(false, FORMAT_SPLICED);
|
||||||
|
assertReadSample(spliceSampleTimeUs, true, DATA, 0, DATA.length);
|
||||||
|
assertReadEndOfStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSpliceAfterRead() {
|
||||||
|
writeTestData();
|
||||||
|
assertReadTestData(null, 0, 4);
|
||||||
|
sampleQueue.splice();
|
||||||
|
// Splice should fail, leaving the last 4 samples unchanged.
|
||||||
|
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3];
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
|
assertReadTestData(SAMPLE_FORMATS[3], 4, 4);
|
||||||
|
assertReadEndOfStream(false);
|
||||||
|
|
||||||
|
sampleQueue.rewind();
|
||||||
|
assertReadTestData(null, 0, 4);
|
||||||
|
sampleQueue.splice();
|
||||||
|
// Splice should succeed, replacing the last 4 samples with the sample being written
|
||||||
|
spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1;
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
|
assertReadFormat(false, FORMAT_SPLICED);
|
||||||
|
assertReadSample(spliceSampleTimeUs, true, DATA, 0, DATA.length);
|
||||||
|
assertReadEndOfStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSpliceWithSampleOffset() {
|
||||||
|
long sampleOffsetUs = 30000;
|
||||||
|
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
|
||||||
|
writeTestData();
|
||||||
|
sampleQueue.splice();
|
||||||
|
// Splice should succeed, replacing the last 4 samples with the sample being written.
|
||||||
|
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
|
assertReadTestData(null, 0, 4, sampleOffsetUs);
|
||||||
|
assertReadFormat(false, FORMAT_SPLICED.copyWithSubsampleOffsetUs(sampleOffsetUs));
|
||||||
|
assertReadSample(spliceSampleTimeUs + sampleOffsetUs, true, DATA, 0, DATA.length);
|
||||||
|
assertReadEndOfStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes standard test data to {@code sampleQueue}.
|
* Writes standard test data to {@code sampleQueue}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ReferenceEquality")
|
|
||||||
private void writeTestData() {
|
private void writeTestData() {
|
||||||
writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, TEST_SAMPLE_TIMESTAMPS,
|
writeTestData(
|
||||||
TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS);
|
DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, SAMPLE_TIMESTAMPS, SAMPLE_FORMATS, SAMPLE_FLAGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the specified test data to {@code sampleQueue}.
|
* Writes the specified test data to {@code sampleQueue}.
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ReferenceEquality")
|
@SuppressWarnings("ReferenceEquality")
|
||||||
private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets,
|
private void writeTestData(byte[] data, int[] sampleSizes, int[] sampleOffsets,
|
||||||
|
|
@ -542,6 +609,13 @@ public final class SampleQueueTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Writes a single sample to {@code sampleQueue}. */
|
||||||
|
private void writeSample(byte[] data, long timestampUs, Format format, int sampleFlags) {
|
||||||
|
sampleQueue.format(format);
|
||||||
|
sampleQueue.sampleData(new ParsableByteArray(data), data.length);
|
||||||
|
sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts correct reading of standard test data from {@code sampleQueue}.
|
* Asserts correct reading of standard test data from {@code sampleQueue}.
|
||||||
*/
|
*/
|
||||||
|
|
@ -565,8 +639,7 @@ public final class SampleQueueTest {
|
||||||
* @param firstSampleIndex The index of the first sample that's expected to be read.
|
* @param firstSampleIndex The index of the first sample that's expected to be read.
|
||||||
*/
|
*/
|
||||||
private void assertReadTestData(Format startFormat, int firstSampleIndex) {
|
private void assertReadTestData(Format startFormat, int firstSampleIndex) {
|
||||||
assertReadTestData(startFormat, firstSampleIndex,
|
assertReadTestData(startFormat, firstSampleIndex, SAMPLE_TIMESTAMPS.length - firstSampleIndex);
|
||||||
TEST_SAMPLE_TIMESTAMPS.length - firstSampleIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -577,23 +650,38 @@ public final class SampleQueueTest {
|
||||||
* @param sampleCount The number of samples to read.
|
* @param sampleCount The number of samples to read.
|
||||||
*/
|
*/
|
||||||
private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) {
|
private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) {
|
||||||
Format format = startFormat;
|
assertReadTestData(startFormat, firstSampleIndex, sampleCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts correct reading of standard test data from {@code sampleQueue}.
|
||||||
|
*
|
||||||
|
* @param startFormat The format of the last sample previously read from {@code sampleQueue}.
|
||||||
|
* @param firstSampleIndex The index of the first sample that's expected to be read.
|
||||||
|
* @param sampleCount The number of samples to read.
|
||||||
|
* @param sampleOffsetUs The expected sample offset.
|
||||||
|
*/
|
||||||
|
private void assertReadTestData(
|
||||||
|
Format startFormat, int firstSampleIndex, int sampleCount, long sampleOffsetUs) {
|
||||||
|
Format format = adjustFormat(startFormat, sampleOffsetUs);
|
||||||
for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) {
|
for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) {
|
||||||
// Use equals() on the read side despite using referential equality on the write side, since
|
// Use equals() on the read side despite using referential equality on the write side, since
|
||||||
// sampleQueue de-duplicates written formats using equals().
|
// sampleQueue de-duplicates written formats using equals().
|
||||||
if (!TEST_SAMPLE_FORMATS[i].equals(format)) {
|
Format testSampleFormat = adjustFormat(SAMPLE_FORMATS[i], sampleOffsetUs);
|
||||||
|
if (!testSampleFormat.equals(format)) {
|
||||||
// If the format has changed, we should read it.
|
// If the format has changed, we should read it.
|
||||||
assertReadFormat(false, TEST_SAMPLE_FORMATS[i]);
|
assertReadFormat(false, testSampleFormat);
|
||||||
format = TEST_SAMPLE_FORMATS[i];
|
format = testSampleFormat;
|
||||||
}
|
}
|
||||||
// If we require the format, we should always read it.
|
// If we require the format, we should always read it.
|
||||||
assertReadFormat(true, TEST_SAMPLE_FORMATS[i]);
|
assertReadFormat(true, testSampleFormat);
|
||||||
// Assert the sample is as expected.
|
// Assert the sample is as expected.
|
||||||
assertSampleRead(TEST_SAMPLE_TIMESTAMPS[i],
|
assertReadSample(
|
||||||
(TEST_SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0,
|
SAMPLE_TIMESTAMPS[i] + sampleOffsetUs,
|
||||||
TEST_DATA,
|
(SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0,
|
||||||
TEST_DATA.length - TEST_SAMPLE_OFFSETS[i] - TEST_SAMPLE_SIZES[i],
|
DATA,
|
||||||
TEST_SAMPLE_SIZES[i]);
|
DATA.length - SAMPLE_OFFSETS[i] - SAMPLE_SIZES[i],
|
||||||
|
SAMPLE_SIZES[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -688,8 +776,8 @@ public final class SampleQueueTest {
|
||||||
* @param offset The offset in {@code sampleData} of the expected sample data.
|
* @param offset The offset in {@code sampleData} of the expected sample data.
|
||||||
* @param length The length of the expected sample data.
|
* @param length The length of the expected sample data.
|
||||||
*/
|
*/
|
||||||
private void assertSampleRead(long timeUs, boolean isKeyframe, byte[] sampleData, int offset,
|
private void assertReadSample(
|
||||||
int length) {
|
long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) {
|
||||||
clearFormatHolderAndInputBuffer();
|
clearFormatHolderAndInputBuffer();
|
||||||
int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0);
|
int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0);
|
||||||
assertThat(result).isEqualTo(RESULT_BUFFER_READ);
|
assertThat(result).isEqualTo(RESULT_BUFFER_READ);
|
||||||
|
|
@ -738,4 +826,9 @@ public final class SampleQueueTest {
|
||||||
inputBuffer.clear();
|
inputBuffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Format adjustFormat(@Nullable Format format, long sampleOffsetUs) {
|
||||||
|
return format == null || sampleOffsetUs == 0
|
||||||
|
? format
|
||||||
|
: format.copyWithSubsampleOffsetUs(sampleOffsetUs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue