Apply SeekParameters to DASH + SmoothStreaming playbacks
Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181314086
This commit is contained in:
parent
4ee971052b
commit
ff1bb2f702
@ -27,8 +27,7 @@
|
||||
performed. The `SeekParameters` class contains defaults for exact seeking and
|
||||
seeking to the closest sync points before, either side or after specified seek
|
||||
positions.
|
||||
* Note: `SeekParameters` are only currently effective when playing
|
||||
`ExtractorMediaSource`s (i.e. progressive streams).
|
||||
* Note: `SeekParameters` are not currently supported when playing HLS streams.
|
||||
* DASH: Support DASH manifest EventStream elements.
|
||||
* HLS: Add opt-in support for chunkless preparation in HLS. This allows an
|
||||
HLS source to finish preparation without downloading any chunks, which can
|
||||
|
@ -69,4 +69,22 @@ public final class SeekParameters {
|
||||
this.toleranceBeforeUs = toleranceBeforeUs;
|
||||
this.toleranceAfterUs = toleranceAfterUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
SeekParameters other = (SeekParameters) obj;
|
||||
return toleranceBeforeUs == other.toleranceBeforeUs
|
||||
&& toleranceAfterUs == other.toleranceAfterUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs;
|
||||
}
|
||||
}
|
||||
|
@ -378,28 +378,8 @@ import java.util.Arrays;
|
||||
return 0;
|
||||
}
|
||||
SeekPoints seekPoints = seekMap.getSeekPoints(positionUs);
|
||||
long minPositionUs =
|
||||
Util.subtractWithOverflowDefault(
|
||||
positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE);
|
||||
long maxPositionUs =
|
||||
Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE);
|
||||
long firstPointUs = seekPoints.first.timeUs;
|
||||
boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs;
|
||||
long secondPointUs = seekPoints.second.timeUs;
|
||||
boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs;
|
||||
if (firstPointValid && secondPointValid) {
|
||||
if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) {
|
||||
return firstPointUs;
|
||||
} else {
|
||||
return secondPointUs;
|
||||
}
|
||||
} else if (firstPointValid) {
|
||||
return firstPointUs;
|
||||
} else if (secondPointValid) {
|
||||
return secondPointUs;
|
||||
} else {
|
||||
return minPositionUs;
|
||||
}
|
||||
return Util.resolveSeekPositionUs(
|
||||
positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs);
|
||||
}
|
||||
|
||||
// SampleStream methods.
|
||||
|
@ -19,6 +19,7 @@ import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.SampleQueue;
|
||||
@ -42,7 +43,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||
|
||||
private static final String TAG = "ChunkSampleStream";
|
||||
|
||||
private final int primaryTrackType;
|
||||
public final int primaryTrackType;
|
||||
|
||||
private final int[] embeddedTrackTypes;
|
||||
private final boolean[] embeddedTracksSelected;
|
||||
private final T chunkSource;
|
||||
@ -180,6 +182,21 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
|
||||
* as sync points.
|
||||
*
|
||||
* @param positionUs The seek position in microseconds.
|
||||
* @param seekParameters Parameters that control how the seek is performed.
|
||||
* @return The adjusted seek position, in microseconds.
|
||||
*/
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
// TODO: Using this method to adjust a seek position and then passing the adjusted position to
|
||||
// seekToUs does not handle small discrepancies between the chunk boundary timestamps obtained
|
||||
// from the chunk source and the timestamps of the samples in the chunks.
|
||||
return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the specified position in microseconds.
|
||||
*
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.chunk;
|
||||
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@ -23,6 +24,16 @@ import java.util.List;
|
||||
*/
|
||||
public interface ChunkSource {
|
||||
|
||||
/**
|
||||
* Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
|
||||
* as sync points.
|
||||
*
|
||||
* @param positionUs The seek position in microseconds.
|
||||
* @param seekParameters Parameters that control how the seek is performed.
|
||||
* @return The adjusted seek position, in microseconds.
|
||||
*/
|
||||
long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
|
||||
|
||||
/**
|
||||
* If the source is currently having difficulty providing chunks, then this method throws the
|
||||
* underlying error. Otherwise does nothing.
|
||||
|
@ -34,6 +34,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
@ -762,6 +763,44 @@ public final class Util {
|
||||
return Math.round((double) mediaDuration / speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate
|
||||
* sync points.
|
||||
*
|
||||
* @param positionUs The requested seek position, in microseocnds.
|
||||
* @param seekParameters The {@link SeekParameters}.
|
||||
* @param firstSyncUs The first candidate seek point, in micrseconds.
|
||||
* @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code
|
||||
* firstSyncUs} if there's only one candidate.
|
||||
* @return The resolved seek position, in microseconds.
|
||||
*/
|
||||
public static long resolveSeekPositionUs(
|
||||
long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) {
|
||||
if (SeekParameters.EXACT.equals(seekParameters)) {
|
||||
return positionUs;
|
||||
}
|
||||
long minPositionUs =
|
||||
subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE);
|
||||
long maxPositionUs =
|
||||
addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE);
|
||||
boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs;
|
||||
boolean secondSyncPositionValid =
|
||||
minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs;
|
||||
if (firstSyncPositionValid && secondSyncPositionValid) {
|
||||
if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) {
|
||||
return firstSyncUs;
|
||||
} else {
|
||||
return secondSyncUs;
|
||||
}
|
||||
} else if (firstSyncPositionValid) {
|
||||
return firstSyncUs;
|
||||
} else if (secondSyncPositionValid) {
|
||||
return secondSyncUs;
|
||||
} else {
|
||||
return minPositionUs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of integers to a primitive array.
|
||||
*
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.dash;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
@ -25,15 +26,40 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
||||
*/
|
||||
public interface DashChunkSource extends ChunkSource {
|
||||
|
||||
/** Factory for {@link DashChunkSource}s. */
|
||||
interface Factory {
|
||||
|
||||
DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
|
||||
TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs,
|
||||
boolean enableEventMessageTrack, boolean enableCea608Track);
|
||||
|
||||
/**
|
||||
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param periodIndex The index of the corresponding period in the manifest.
|
||||
* @param adaptationSetIndices The indices of the corresponding adaptation sets in the period.
|
||||
* @param trackSelection The track selection.
|
||||
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
|
||||
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds,
|
||||
* specified as the server's unix time minus the local elapsed time. If unknown, set to 0.
|
||||
* @param enableEventMessageTrack Whether the chunks generated by the source may output an event
|
||||
* message track.
|
||||
* @param enableCea608Track Whether the chunks generated by the source may output a CEA-608
|
||||
* track.
|
||||
* @return The created {@link DashChunkSource}.
|
||||
*/
|
||||
DashChunkSource createDashChunkSource(
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest,
|
||||
int periodIndex,
|
||||
int[] adaptationSetIndices,
|
||||
TrackSelection trackSelection,
|
||||
int type,
|
||||
long elapsedRealtimeOffsetMs,
|
||||
boolean enableEventMessageTrack,
|
||||
boolean enableCea608Track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the manifest.
|
||||
*
|
||||
* @param newManifest The new manifest.
|
||||
*/
|
||||
void updateManifest(DashManifest newManifest, int periodIndex);
|
||||
|
||||
}
|
||||
|
@ -309,6 +309,11 @@ import java.util.Map;
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
||||
if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {
|
||||
return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);
|
||||
}
|
||||
}
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
@ -142,6 +143,24 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
// Segments are aligned across representations, so any segment index will do.
|
||||
for (RepresentationHolder representationHolder : representationHolders) {
|
||||
if (representationHolder.segmentIndex != null) {
|
||||
int segmentNum = representationHolder.getSegmentNum(positionUs);
|
||||
long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum);
|
||||
long secondSyncUs =
|
||||
firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1
|
||||
? representationHolder.getSegmentStartTimeUs(segmentNum + 1)
|
||||
: firstSyncUs;
|
||||
return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);
|
||||
}
|
||||
}
|
||||
// We don't have a segment index to adjust the seek position with yet.
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateManifest(DashManifest newManifest, int newPeriodIndex) {
|
||||
try {
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Track;
|
||||
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
|
||||
@ -34,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@ -62,7 +64,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
}
|
||||
|
||||
private final LoaderErrorThrower manifestLoaderErrorThrower;
|
||||
private final int elementIndex;
|
||||
private final int streamElementIndex;
|
||||
private final TrackSelection trackSelection;
|
||||
private final ChunkExtractorWrapper[] extractorWrappers;
|
||||
private final DataSource dataSource;
|
||||
@ -75,22 +77,25 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
/**
|
||||
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param elementIndex The index of the stream element in the manifest.
|
||||
* @param streamElementIndex The index of the stream element in the manifest.
|
||||
* @param trackSelection The track selection.
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param trackEncryptionBoxes Track encryption boxes for the stream.
|
||||
*/
|
||||
public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest,
|
||||
int elementIndex, TrackSelection trackSelection, DataSource dataSource,
|
||||
public DefaultSsChunkSource(
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
SsManifest manifest,
|
||||
int streamElementIndex,
|
||||
TrackSelection trackSelection,
|
||||
DataSource dataSource,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||
this.manifest = manifest;
|
||||
this.elementIndex = elementIndex;
|
||||
this.streamElementIndex = streamElementIndex;
|
||||
this.trackSelection = trackSelection;
|
||||
this.dataSource = dataSource;
|
||||
|
||||
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||
|
||||
StreamElement streamElement = manifest.streamElements[streamElementIndex];
|
||||
extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()];
|
||||
for (int i = 0; i < extractorWrappers.length; i++) {
|
||||
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);
|
||||
@ -106,11 +111,23 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
StreamElement streamElement = manifest.streamElements[streamElementIndex];
|
||||
int chunkIndex = streamElement.getChunkIndex(positionUs);
|
||||
long firstSyncUs = streamElement.getStartTimeUs(chunkIndex);
|
||||
long secondSyncUs =
|
||||
firstSyncUs < positionUs && chunkIndex < streamElement.chunkCount - 1
|
||||
? streamElement.getStartTimeUs(chunkIndex + 1)
|
||||
: firstSyncUs;
|
||||
return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateManifest(SsManifest newManifest) {
|
||||
StreamElement currentElement = manifest.streamElements[elementIndex];
|
||||
StreamElement currentElement = manifest.streamElements[streamElementIndex];
|
||||
int currentElementChunkCount = currentElement.chunkCount;
|
||||
StreamElement newElement = newManifest.streamElements[elementIndex];
|
||||
StreamElement newElement = newManifest.streamElements[streamElementIndex];
|
||||
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
|
||||
// There's no overlap between the old and new elements because at least one is empty.
|
||||
currentManifestChunkOffset += currentElementChunkCount;
|
||||
@ -155,7 +172,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
return;
|
||||
}
|
||||
|
||||
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||
StreamElement streamElement = manifest.streamElements[streamElementIndex];
|
||||
if (streamElement.chunkCount == 0) {
|
||||
// There aren't any chunks for us to load.
|
||||
out.endOfStream = !manifest.isLive;
|
||||
@ -229,7 +246,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
StreamElement currentElement = manifest.streamElements[elementIndex];
|
||||
StreamElement currentElement = manifest.streamElements[streamElementIndex];
|
||||
int lastChunkIndex = currentElement.chunkCount - 1;
|
||||
long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex)
|
||||
+ currentElement.getChunkDurationUs(lastChunkIndex);
|
||||
|
@ -26,14 +26,31 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
|
||||
*/
|
||||
public interface SsChunkSource extends ChunkSource {
|
||||
|
||||
/** Factory for {@link SsChunkSource}s. */
|
||||
interface Factory {
|
||||
|
||||
SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
SsManifest manifest, int elementIndex, TrackSelection trackSelection,
|
||||
/**
|
||||
* Creates a new {@link SsChunkSource}.
|
||||
*
|
||||
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param streamElementIndex The index of the corresponding stream element in the manifest.
|
||||
* @param trackSelection The track selection.
|
||||
* @param trackEncryptionBoxes Track encryption boxes for the stream.
|
||||
* @return The created {@link SsChunkSource}.
|
||||
*/
|
||||
SsChunkSource createChunkSource(
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
SsManifest manifest,
|
||||
int streamElementIndex,
|
||||
TrackSelection trackSelection,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the manifest.
|
||||
*
|
||||
* @param newManifest The new manifest.
|
||||
*/
|
||||
void updateManifest(SsManifest newManifest);
|
||||
|
||||
}
|
||||
|
@ -185,6 +185,11 @@ import java.util.ArrayList;
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
|
||||
if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {
|
||||
return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);
|
||||
}
|
||||
}
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSource;
|
||||
@ -28,6 +29,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@ -71,6 +74,17 @@ public final class FakeChunkSource implements ChunkSource {
|
||||
this.dataSet = dataSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
int chunkIndex = dataSet.getChunkIndexByPosition(positionUs);
|
||||
long firstSyncUs = dataSet.getStartTime(chunkIndex);
|
||||
long secondSyncUs =
|
||||
firstSyncUs < positionUs && chunkIndex < dataSet.getChunkCount() - 1
|
||||
? dataSet.getStartTime(chunkIndex + 1)
|
||||
: firstSyncUs;
|
||||
return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowError() throws IOException {
|
||||
// Do nothing.
|
||||
|
Loading…
x
Reference in New Issue
Block a user