mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Move HlsSampleQueue to be static inner-class
Backout making the original `FormatAdjustingSampleQueue` an outer class and combining the new timestamp checking logic. This way the diff from `HlsSampleStreamWrapper` to dev-v2 are easier to see.
This commit is contained in:
parent
0d46e24f94
commit
ccf2ba3e1b
@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.source.hls;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
|
||||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
|
||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
|
||||||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
|
||||||
import com.google.android.exoplayer2.source.SampleQueue;
|
|
||||||
import com.google.android.exoplayer2.source.UnexpectedDiscontinuityException;
|
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Log;
|
|
||||||
import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
|
|
||||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend base SampleQueue to add HLS specific processing of the samples, including:
|
|
||||||
*
|
|
||||||
* <ul>
|
|
||||||
* <li>segment time boundary checks on timestamps of committed samples</li>
|
|
||||||
* <li>cleaning the {@link Format#metadata} to avoid excessive format changes</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* The timestamp check verifies that the adjusted sample time (via {@link TimestampAdjuster}) does not
|
|
||||||
* fall outside of a set percentage ({@link #MAX_TIMESTAMP_DEVIATION_PERCENTAGE}) of the time
|
|
||||||
* boundaries of the segment as expressed by the segment duration ((@link HlsMediaChunk#endTimeUs} -
|
|
||||||
* {@link HlsMediaChunk#startTimeUs}). This is loosely mandated by the Pantos spec and checked by
|
|
||||||
* Apple's mediastreamvalidator.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class HlsSampleQueue extends SampleQueue {
|
|
||||||
|
|
||||||
private static final String TAG = "HlsSampleQueue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* largest timestamp deviation from the segment time bounds expressed as a percentage of
|
|
||||||
* the segment duration.
|
|
||||||
*/
|
|
||||||
public static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.50;
|
|
||||||
|
|
||||||
private long lowestTimeUs = C.TIME_UNSET;
|
|
||||||
private long highestTimeUs = C.TIME_UNSET;
|
|
||||||
|
|
||||||
@Nullable private HlsMediaChunk chunk;
|
|
||||||
private long lastValidTimeUs;
|
|
||||||
|
|
||||||
private final Map<String, DrmInitData> overridingDrmInitData;
|
|
||||||
@Nullable private DrmInitData drmInitData;
|
|
||||||
|
|
||||||
public HlsSampleQueue(Allocator allocator,
|
|
||||||
DrmSessionManager<?> drmSessionManager,
|
|
||||||
MediaSourceEventDispatcher eventDispatcher,
|
|
||||||
Map<String, DrmInitData> overridingDrmInitData) {
|
|
||||||
super(allocator, drmSessionManager, eventDispatcher);
|
|
||||||
this.overridingDrmInitData = overridingDrmInitData;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCurrentLoadingChunk(HlsMediaChunk chunk) {
|
|
||||||
double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE;
|
|
||||||
this.lowestTimeUs = (long) (chunk.startTimeUs - tolerance);
|
|
||||||
this.highestTimeUs = (long) (chunk.endTimeUs + tolerance);
|
|
||||||
this.chunk = chunk;
|
|
||||||
lastValidTimeUs = C.TIME_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDrmInitData(@Nullable DrmInitData drmInitData) {
|
|
||||||
this.drmInitData = drmInitData;
|
|
||||||
invalidateUpstreamFormatAdjustment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("ReferenceEquality")
|
|
||||||
@Override
|
|
||||||
public Format getAdjustedUpstreamFormat(Format format) {
|
|
||||||
@Nullable
|
|
||||||
DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData;
|
|
||||||
if (drmInitData != null) {
|
|
||||||
@Nullable
|
|
||||||
DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);
|
|
||||||
if (overridingDrmInitData != null) {
|
|
||||||
drmInitData = overridingDrmInitData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Nullable Metadata metadata = getAdjustedMetadata(format.metadata);
|
|
||||||
if (drmInitData != format.drmInitData || metadata != format.metadata) {
|
|
||||||
format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build();
|
|
||||||
}
|
|
||||||
return super.getAdjustedUpstreamFormat(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips the private timestamp frame from metadata, if present. See:
|
|
||||||
* https://github.com/google/ExoPlayer/issues/5063
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private Metadata getAdjustedMetadata(@Nullable Metadata metadata) {
|
|
||||||
if (metadata == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int length = metadata.length();
|
|
||||||
int transportStreamTimestampMetadataIndex = C.INDEX_UNSET;
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
Metadata.Entry metadataEntry = metadata.get(i);
|
|
||||||
if (metadataEntry instanceof PrivFrame) {
|
|
||||||
PrivFrame privFrame = (PrivFrame) metadataEntry;
|
|
||||||
if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
|
|
||||||
transportStreamTimestampMetadataIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) {
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
if (length == 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1];
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
if (i != transportStreamTimestampMetadataIndex) {
|
|
||||||
int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1;
|
|
||||||
newMetadataEntries[newIndex] = metadata.get(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Metadata(newMetadataEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) {
|
|
||||||
// TODO - chunkless prepare, sampleQueue list is not yet initialized for first chunk
|
|
||||||
// Assertions.checkNotNull(chunk, "sampleMetadata without a MediaChunk?");
|
|
||||||
if (chunk == null) {
|
|
||||||
super.sampleMetadata(timeUs, flags, size, offset, cryptoData);
|
|
||||||
} else if (timeUs > highestTimeUs || timeUs < lowestTimeUs) {
|
|
||||||
throw new UnexpectedDiscontinuityException(chunk, lastValidTimeUs, timeUs);
|
|
||||||
} else {
|
|
||||||
lastValidTimeUs = timeUs;
|
|
||||||
super.sampleMetadata(timeUs, flags, size, offset, cryptoData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -43,6 +43,7 @@ import com.google.android.exoplayer2.source.SampleStream;
|
|||||||
import com.google.android.exoplayer2.source.SequenceableLoader;
|
import com.google.android.exoplayer2.source.SequenceableLoader;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
import com.google.android.exoplayer2.source.UnexpectedDiscontinuityException;
|
||||||
import com.google.android.exoplayer2.source.chunk.Chunk;
|
import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||||
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
|
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
@ -56,6 +57,7 @@ import com.google.android.exoplayer2.util.Log;
|
|||||||
import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
|
import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -1338,6 +1340,134 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend base SampleQueue to add HLS specific processing of the samples, including:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>segment time boundary checks on timestamps of committed samples</li>
|
||||||
|
* <li>cleaning the {@link Format#metadata} to avoid excessive format changes</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* The timestamp check verifies that the adjusted sample time (via {@link TimestampAdjuster}) does not
|
||||||
|
* fall outside of a set percentage ({@link #MAX_TIMESTAMP_DEVIATION_PERCENTAGE}) of the time
|
||||||
|
* boundaries of the segment as expressed by the segment duration ((@link HlsMediaChunk#endTimeUs} -
|
||||||
|
* {@link HlsMediaChunk#startTimeUs}). This is loosely mandated by the Pantos spec and checked by
|
||||||
|
* Apple's mediastreamvalidator.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final class HlsSampleQueue extends SampleQueue {
|
||||||
|
|
||||||
|
private static final String TAG = "HlsSampleQueue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* largest timestamp deviation from the segment time bounds expressed as a percentage of
|
||||||
|
* the segment duration.
|
||||||
|
*/
|
||||||
|
private static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.50;
|
||||||
|
|
||||||
|
private long lowestTimeUs = C.TIME_UNSET;
|
||||||
|
private long highestTimeUs = C.TIME_UNSET;
|
||||||
|
|
||||||
|
@Nullable private HlsMediaChunk chunk;
|
||||||
|
private long lastValidTimeUs;
|
||||||
|
|
||||||
|
private final Map<String, DrmInitData> overridingDrmInitData;
|
||||||
|
@Nullable private DrmInitData drmInitData;
|
||||||
|
|
||||||
|
private HlsSampleQueue(Allocator allocator,
|
||||||
|
DrmSessionManager<?> drmSessionManager,
|
||||||
|
MediaSourceEventDispatcher eventDispatcher,
|
||||||
|
Map<String, DrmInitData> overridingDrmInitData) {
|
||||||
|
super(allocator, drmSessionManager, eventDispatcher);
|
||||||
|
this.overridingDrmInitData = overridingDrmInitData;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrentLoadingChunk(HlsMediaChunk chunk) {
|
||||||
|
double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE;
|
||||||
|
this.lowestTimeUs = (long) (chunk.startTimeUs - tolerance);
|
||||||
|
this.highestTimeUs = (long) (chunk.endTimeUs + tolerance);
|
||||||
|
this.chunk = chunk;
|
||||||
|
lastValidTimeUs = C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDrmInitData(@Nullable DrmInitData drmInitData) {
|
||||||
|
this.drmInitData = drmInitData;
|
||||||
|
invalidateUpstreamFormatAdjustment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
|
@Override
|
||||||
|
public Format getAdjustedUpstreamFormat(Format format) {
|
||||||
|
@Nullable
|
||||||
|
DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData;
|
||||||
|
if (drmInitData != null) {
|
||||||
|
@Nullable
|
||||||
|
DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType);
|
||||||
|
if (overridingDrmInitData != null) {
|
||||||
|
drmInitData = overridingDrmInitData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nullable Metadata metadata = getAdjustedMetadata(format.metadata);
|
||||||
|
if (drmInitData != format.drmInitData || metadata != format.metadata) {
|
||||||
|
format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build();
|
||||||
|
}
|
||||||
|
return super.getAdjustedUpstreamFormat(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips the private timestamp frame from metadata, if present. See:
|
||||||
|
* https://github.com/google/ExoPlayer/issues/5063
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private Metadata getAdjustedMetadata(@Nullable Metadata metadata) {
|
||||||
|
if (metadata == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int length = metadata.length();
|
||||||
|
int transportStreamTimestampMetadataIndex = C.INDEX_UNSET;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
Metadata.Entry metadataEntry = metadata.get(i);
|
||||||
|
if (metadataEntry instanceof PrivFrame) {
|
||||||
|
PrivFrame privFrame = (PrivFrame) metadataEntry;
|
||||||
|
if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
|
||||||
|
transportStreamTimestampMetadataIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
if (length == 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1];
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
if (i != transportStreamTimestampMetadataIndex) {
|
||||||
|
int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1;
|
||||||
|
newMetadataEntries[newIndex] = metadata.get(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Metadata(newMetadataEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) {
|
||||||
|
// TODO - chunkless prepare, sampleQueue list is not yet initialized for first chunk
|
||||||
|
// Assertions.checkNotNull(chunk, "sampleMetadata without a MediaChunk?");
|
||||||
|
if (chunk == null) {
|
||||||
|
super.sampleMetadata(timeUs, flags, size, offset, cryptoData);
|
||||||
|
} else if (timeUs > highestTimeUs || timeUs < lowestTimeUs) {
|
||||||
|
throw new UnexpectedDiscontinuityException(chunk, lastValidTimeUs, timeUs);
|
||||||
|
} else {
|
||||||
|
lastValidTimeUs = timeUs;
|
||||||
|
super.sampleMetadata(timeUs, flags, size, offset, cryptoData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static DummyTrackOutput createDummyTrackOutput(int id, int type) {
|
private static DummyTrackOutput createDummyTrackOutput(int id, int type) {
|
||||||
Log.w(TAG, "Unmapped track with id " + id + " of type " + type);
|
Log.w(TAG, "Unmapped track with id " + id + " of type " + type);
|
||||||
return new DummyTrackOutput();
|
return new DummyTrackOutput();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user