mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
SampleQueue: Let subclasses easily invalidate format adjustment
This is a nice-regardless improvement to SampleQueue, which will likely to used to fix the referenced issue. It makes it possible for SampleQueue subclasses to support dynamic changes to format adjustment in a non-hacky way. Issue: #6903 PiperOrigin-RevId: 292314720
This commit is contained in:
parent
21fe13d3d7
commit
d75aa97c0c
3 changed files with 147 additions and 43 deletions
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.source;
|
package com.google.android.exoplayer2.source;
|
||||||
|
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
@ -81,8 +82,8 @@ public class SampleQueue implements TrackOutput {
|
||||||
private Format upstreamCommittedFormat;
|
private Format upstreamCommittedFormat;
|
||||||
private int upstreamSourceId;
|
private int upstreamSourceId;
|
||||||
|
|
||||||
private boolean pendingFormatAdjustment;
|
private boolean pendingUpstreamFormatAdjustment;
|
||||||
private Format lastUnadjustedFormat;
|
private Format unadjustedUpstreamFormat;
|
||||||
private long sampleOffsetUs;
|
private long sampleOffsetUs;
|
||||||
private boolean pendingSplice;
|
private boolean pendingSplice;
|
||||||
|
|
||||||
|
|
@ -146,6 +147,7 @@ public class SampleQueue implements TrackOutput {
|
||||||
isLastSampleQueued = false;
|
isLastSampleQueued = false;
|
||||||
upstreamCommittedFormat = null;
|
upstreamCommittedFormat = null;
|
||||||
if (resetUpstreamFormat) {
|
if (resetUpstreamFormat) {
|
||||||
|
unadjustedUpstreamFormat = null;
|
||||||
upstreamFormat = null;
|
upstreamFormat = null;
|
||||||
upstreamFormatRequired = true;
|
upstreamFormatRequired = true;
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +435,7 @@ public class SampleQueue implements TrackOutput {
|
||||||
public void setSampleOffsetUs(long sampleOffsetUs) {
|
public void setSampleOffsetUs(long sampleOffsetUs) {
|
||||||
if (this.sampleOffsetUs != sampleOffsetUs) {
|
if (this.sampleOffsetUs != sampleOffsetUs) {
|
||||||
this.sampleOffsetUs = sampleOffsetUs;
|
this.sampleOffsetUs = sampleOffsetUs;
|
||||||
pendingFormatAdjustment = true;
|
invalidateUpstreamFormatAdjustment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,13 +451,13 @@ public class SampleQueue implements TrackOutput {
|
||||||
// TrackOutput implementation. Called by the loading thread.
|
// TrackOutput implementation. Called by the loading thread.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void format(Format unadjustedFormat) {
|
public final void format(Format unadjustedUpstreamFormat) {
|
||||||
Format adjustedFormat = getAdjustedSampleFormat(unadjustedFormat, sampleOffsetUs);
|
Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(unadjustedUpstreamFormat);
|
||||||
boolean formatChanged = setUpstreamFormat(adjustedFormat);
|
pendingUpstreamFormatAdjustment = false;
|
||||||
lastUnadjustedFormat = unadjustedFormat;
|
this.unadjustedUpstreamFormat = unadjustedUpstreamFormat;
|
||||||
pendingFormatAdjustment = false;
|
boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat);
|
||||||
if (upstreamFormatChangeListener != null && formatChanged) {
|
if (upstreamFormatChangeListener != null && upstreamFormatChanged) {
|
||||||
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
|
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,8 +479,8 @@ public class SampleQueue implements TrackOutput {
|
||||||
int size,
|
int size,
|
||||||
int offset,
|
int offset,
|
||||||
@Nullable CryptoData cryptoData) {
|
@Nullable CryptoData cryptoData) {
|
||||||
if (pendingFormatAdjustment) {
|
if (pendingUpstreamFormatAdjustment) {
|
||||||
format(lastUnadjustedFormat);
|
format(unadjustedUpstreamFormat);
|
||||||
}
|
}
|
||||||
timeUs += sampleOffsetUs;
|
timeUs += sampleOffsetUs;
|
||||||
if (pendingSplice) {
|
if (pendingSplice) {
|
||||||
|
|
@ -491,6 +493,32 @@ public class SampleQueue implements TrackOutput {
|
||||||
commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
|
commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the last upstream format adjustment. {@link #getAdjustedUpstreamFormat(Format)}
|
||||||
|
* will be called to adjust the upstream {@link Format} again before the next sample is queued.
|
||||||
|
*/
|
||||||
|
protected final void invalidateUpstreamFormatAdjustment() {
|
||||||
|
pendingUpstreamFormatAdjustment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the upstream {@link Format} (i.e., the {@link Format} that was most recently passed to
|
||||||
|
* {@link #format(Format)}).
|
||||||
|
*
|
||||||
|
* <p>The default implementation incorporates the sample offset passed to {@link
|
||||||
|
* #setSampleOffsetUs(long)} into {@link Format#subsampleOffsetUs}.
|
||||||
|
*
|
||||||
|
* @param format The {@link Format} to adjust.
|
||||||
|
* @return The adjusted {@link Format}.
|
||||||
|
*/
|
||||||
|
@CallSuper
|
||||||
|
protected Format getAdjustedUpstreamFormat(Format format) {
|
||||||
|
if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
|
||||||
|
format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
|
||||||
|
}
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
/** Rewinds the read position to the first sample in the queue. */
|
/** Rewinds the read position to the first sample in the queue. */
|
||||||
|
|
@ -883,23 +911,6 @@ public class SampleQueue implements TrackOutput {
|
||||||
return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
|
return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
|
|
||||||
*
|
|
||||||
* @param format The {@link Format} to adjust.
|
|
||||||
* @param sampleOffsetUs The offset to apply.
|
|
||||||
* @return The adjusted {@link Format}.
|
|
||||||
*/
|
|
||||||
private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
|
|
||||||
if (format == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
|
|
||||||
format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
|
|
||||||
}
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A holder for sample metadata not held by {@link DecoderInputBuffer}. */
|
/** A holder for sample metadata not held by {@link DecoderInputBuffer}. */
|
||||||
/* package */ static final class SampleExtrasHolder {
|
/* package */ static final class SampleExtrasHolder {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source;
|
package com.google.android.exoplayer2.source;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.C.BUFFER_FLAG_KEY_FRAME;
|
||||||
import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ;
|
import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ;
|
||||||
import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ;
|
import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ;
|
||||||
import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ;
|
import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ;
|
||||||
|
|
@ -40,6 +41,7 @@ import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
|
@ -136,7 +138,7 @@ public final class SampleQueueTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void setUp() throws Exception {
|
public void setUp() {
|
||||||
allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
|
allocator = new DefaultAllocator(false, ALLOCATION_SIZE);
|
||||||
mockDrmSessionManager =
|
mockDrmSessionManager =
|
||||||
(DrmSessionManager<ExoMediaCrypto>) Mockito.mock(DrmSessionManager.class);
|
(DrmSessionManager<ExoMediaCrypto>) Mockito.mock(DrmSessionManager.class);
|
||||||
|
|
@ -149,7 +151,7 @@ public final class SampleQueueTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() throws Exception {
|
public void tearDown() {
|
||||||
allocator = null;
|
allocator = null;
|
||||||
sampleQueue = null;
|
sampleQueue = null;
|
||||||
formatHolder = null;
|
formatHolder = null;
|
||||||
|
|
@ -837,12 +839,93 @@ public final class SampleQueueTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetSampleOffset() {
|
public void testSetSampleOffsetBeforeData() {
|
||||||
long sampleOffsetUs = 1000;
|
long sampleOffsetUs = 1000;
|
||||||
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
|
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
|
||||||
writeTestData();
|
writeTestData();
|
||||||
assertReadTestData(null, 0, 8, sampleOffsetUs);
|
assertReadTestData(
|
||||||
assertReadEndOfStream(false);
|
/* startFormat= */ null, /* firstSampleIndex= */ 0, /* sampleCount= */ 8, sampleOffsetUs);
|
||||||
|
assertReadEndOfStream(/* formatRequired= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetSampleOffsetBetweenSamples() {
|
||||||
|
writeTestData();
|
||||||
|
long sampleOffsetUs = 1000;
|
||||||
|
sampleQueue.setSampleOffsetUs(sampleOffsetUs);
|
||||||
|
|
||||||
|
// Write a final sample now the offset is set.
|
||||||
|
long unadjustedTimestampUs = LAST_SAMPLE_TIMESTAMP + 1234;
|
||||||
|
writeSample(DATA, unadjustedTimestampUs, /* sampleFlags= */ 0);
|
||||||
|
|
||||||
|
assertReadTestData();
|
||||||
|
// We expect to read the format adjusted to account for the sample offset, followed by the final
|
||||||
|
// sample and then the end of stream.
|
||||||
|
assertReadFormat(
|
||||||
|
/* formatRequired= */ false, FORMAT_2.copyWithSubsampleOffsetUs(sampleOffsetUs));
|
||||||
|
assertReadSample(
|
||||||
|
unadjustedTimestampUs + sampleOffsetUs,
|
||||||
|
/* isKeyFrame= */ false,
|
||||||
|
/* isEncrypted= */ false,
|
||||||
|
DATA,
|
||||||
|
/* offset= */ 0,
|
||||||
|
DATA.length);
|
||||||
|
assertReadEndOfStream(/* formatRequired= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdjustUpstreamFormat() {
|
||||||
|
String label = "label";
|
||||||
|
sampleQueue =
|
||||||
|
new SampleQueue(allocator, mockDrmSessionManager) {
|
||||||
|
@Override
|
||||||
|
public Format getAdjustedUpstreamFormat(Format format) {
|
||||||
|
return super.getAdjustedUpstreamFormat(format.copyWithLabel(label));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFormat(FORMAT_1);
|
||||||
|
assertReadFormat(/* formatRequired= */ false, FORMAT_1.copyWithLabel(label));
|
||||||
|
assertReadEndOfStream(/* formatRequired= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidateUpstreamFormatAdjustment() {
|
||||||
|
AtomicReference<String> label = new AtomicReference<>("label1");
|
||||||
|
sampleQueue =
|
||||||
|
new SampleQueue(allocator, mockDrmSessionManager) {
|
||||||
|
@Override
|
||||||
|
public Format getAdjustedUpstreamFormat(Format format) {
|
||||||
|
return super.getAdjustedUpstreamFormat(format.copyWithLabel(label.get()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFormat(FORMAT_1);
|
||||||
|
writeSample(DATA, /* timestampUs= */ 0, BUFFER_FLAG_KEY_FRAME);
|
||||||
|
|
||||||
|
// Make a change that'll affect the SampleQueue's format adjustment, and invalidate it.
|
||||||
|
label.set("label2");
|
||||||
|
sampleQueue.invalidateUpstreamFormatAdjustment();
|
||||||
|
|
||||||
|
writeSample(DATA, /* timestampUs= */ 1, /* sampleFlags= */ 0);
|
||||||
|
|
||||||
|
assertReadFormat(/* formatRequired= */ false, FORMAT_1.copyWithLabel("label1"));
|
||||||
|
assertReadSample(
|
||||||
|
/* timeUs= */ 0,
|
||||||
|
/* isKeyFrame= */ true,
|
||||||
|
/* isEncrypted= */ false,
|
||||||
|
DATA,
|
||||||
|
/* offset= */ 0,
|
||||||
|
DATA.length);
|
||||||
|
assertReadFormat(/* formatRequired= */ false, FORMAT_1.copyWithLabel("label2"));
|
||||||
|
assertReadSample(
|
||||||
|
/* timeUs= */ 1,
|
||||||
|
/* isKeyFrame= */ false,
|
||||||
|
/* isEncrypted= */ false,
|
||||||
|
DATA,
|
||||||
|
/* offset= */ 0,
|
||||||
|
DATA.length);
|
||||||
|
assertReadEndOfStream(/* formatRequired= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -851,7 +934,8 @@ public final class SampleQueueTest {
|
||||||
sampleQueue.splice();
|
sampleQueue.splice();
|
||||||
// Splice should succeed, replacing the last 4 samples with the sample being written.
|
// Splice should succeed, replacing the last 4 samples with the sample being written.
|
||||||
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];
|
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];
|
||||||
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
writeFormat(FORMAT_SPLICED);
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
assertReadTestData(null, 0, 4);
|
assertReadTestData(null, 0, 4);
|
||||||
assertReadFormat(false, FORMAT_SPLICED);
|
assertReadFormat(false, FORMAT_SPLICED);
|
||||||
assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length);
|
assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length);
|
||||||
|
|
@ -865,7 +949,8 @@ public final class SampleQueueTest {
|
||||||
sampleQueue.splice();
|
sampleQueue.splice();
|
||||||
// Splice should fail, leaving the last 4 samples unchanged.
|
// Splice should fail, leaving the last 4 samples unchanged.
|
||||||
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3];
|
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3];
|
||||||
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
writeFormat(FORMAT_SPLICED);
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
assertReadTestData(SAMPLE_FORMATS[3], 4, 4);
|
assertReadTestData(SAMPLE_FORMATS[3], 4, 4);
|
||||||
assertReadEndOfStream(false);
|
assertReadEndOfStream(false);
|
||||||
|
|
||||||
|
|
@ -874,7 +959,8 @@ public final class SampleQueueTest {
|
||||||
sampleQueue.splice();
|
sampleQueue.splice();
|
||||||
// Splice should succeed, replacing the last 4 samples with the sample being written
|
// Splice should succeed, replacing the last 4 samples with the sample being written
|
||||||
spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1;
|
spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1;
|
||||||
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
writeFormat(FORMAT_SPLICED);
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
assertReadFormat(false, FORMAT_SPLICED);
|
assertReadFormat(false, FORMAT_SPLICED);
|
||||||
assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length);
|
assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length);
|
||||||
assertReadEndOfStream(false);
|
assertReadEndOfStream(false);
|
||||||
|
|
@ -888,7 +974,8 @@ public final class SampleQueueTest {
|
||||||
sampleQueue.splice();
|
sampleQueue.splice();
|
||||||
// Splice should succeed, replacing the last 4 samples with the sample being written.
|
// Splice should succeed, replacing the last 4 samples with the sample being written.
|
||||||
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];
|
long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4];
|
||||||
writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME);
|
writeFormat(FORMAT_SPLICED);
|
||||||
|
writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME);
|
||||||
assertReadTestData(null, 0, 4, sampleOffsetUs);
|
assertReadTestData(null, 0, 4, sampleOffsetUs);
|
||||||
assertReadFormat(false, FORMAT_SPLICED.copyWithSubsampleOffsetUs(sampleOffsetUs));
|
assertReadFormat(false, FORMAT_SPLICED.copyWithSubsampleOffsetUs(sampleOffsetUs));
|
||||||
assertReadSample(
|
assertReadSample(
|
||||||
|
|
@ -938,9 +1025,13 @@ public final class SampleQueueTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Writes a single sample to {@code sampleQueue}. */
|
/** Writes a {@link Format} to the {@code sampleQueue}. */
|
||||||
private void writeSample(byte[] data, long timestampUs, Format format, int sampleFlags) {
|
private void writeFormat(Format format) {
|
||||||
sampleQueue.format(format);
|
sampleQueue.format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes a single sample to {@code sampleQueue}. */
|
||||||
|
private void writeSample(byte[] data, long timestampUs, int sampleFlags) {
|
||||||
sampleQueue.sampleData(new ParsableByteArray(data), data.length);
|
sampleQueue.sampleData(new ParsableByteArray(data), data.length);
|
||||||
sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null);
|
sampleQueue.sampleMetadata(timestampUs, sampleFlags, data.length, 0, null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1290,15 +1290,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void format(Format format) {
|
public Format getAdjustedUpstreamFormat(Format format) {
|
||||||
DrmInitData drmInitData = format.drmInitData;
|
@Nullable DrmInitData drmInitData = format.drmInitData;
|
||||||
if (drmInitData != null) {
|
if (drmInitData != null) {
|
||||||
|
@Nullable
|
||||||
DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);
|
DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);
|
||||||
if (overridingDrmInitData != null) {
|
if (overridingDrmInitData != null) {
|
||||||
drmInitData = overridingDrmInitData;
|
drmInitData = overridingDrmInitData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super.format(format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata)));
|
return super.getAdjustedUpstreamFormat(
|
||||||
|
format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue