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:
olly 2018-01-09 06:52:54 -08:00 committed by Oliver Woodman
parent 4ee971052b
commit ff1bb2f702
13 changed files with 213 additions and 46 deletions

View File

@ -27,8 +27,7 @@
performed. The `SeekParameters` class contains defaults for exact seeking and performed. The `SeekParameters` class contains defaults for exact seeking and
seeking to the closest sync points before, either side or after specified seek seeking to the closest sync points before, either side or after specified seek
positions. positions.
* Note: `SeekParameters` are only currently effective when playing * Note: `SeekParameters` are not currently supported when playing HLS streams.
`ExtractorMediaSource`s (i.e. progressive streams).
* DASH: Support DASH manifest EventStream elements. * DASH: Support DASH manifest EventStream elements.
* HLS: Add opt-in support for chunkless preparation in HLS. This allows an * HLS: Add opt-in support for chunkless preparation in HLS. This allows an
HLS source to finish preparation without downloading any chunks, which can HLS source to finish preparation without downloading any chunks, which can

View File

@ -69,4 +69,22 @@ public final class SeekParameters {
this.toleranceBeforeUs = toleranceBeforeUs; this.toleranceBeforeUs = toleranceBeforeUs;
this.toleranceAfterUs = toleranceAfterUs; 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;
}
} }

View File

@ -378,28 +378,8 @@ import java.util.Arrays;
return 0; return 0;
} }
SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); SeekPoints seekPoints = seekMap.getSeekPoints(positionUs);
long minPositionUs = return Util.resolveSeekPositionUs(
Util.subtractWithOverflowDefault( positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs);
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;
}
} }
// SampleStream methods. // SampleStream methods.

View File

@ -19,6 +19,7 @@ import android.util.Log;
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;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue; 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 static final String TAG = "ChunkSampleStream";
private final int primaryTrackType; public final int primaryTrackType;
private final int[] embeddedTrackTypes; private final int[] embeddedTrackTypes;
private final boolean[] embeddedTracksSelected; private final boolean[] embeddedTracksSelected;
private final T chunkSource; 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. * Seeks to the specified position in microseconds.
* *

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.chunk; package com.google.android.exoplayer2.source.chunk;
import com.google.android.exoplayer2.SeekParameters;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -23,6 +24,16 @@ import java.util.List;
*/ */
public interface ChunkSource { 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 * If the source is currently having difficulty providing chunks, then this method throws the
* underlying error. Otherwise does nothing. * underlying error. Otherwise does nothing.

View File

@ -34,6 +34,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.Closeable; import java.io.Closeable;
@ -762,6 +763,44 @@ public final class Util {
return Math.round((double) mediaDuration / speed); 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. * Converts a list of integers to a primitive array.
* *

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.dash; 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.chunk.ChunkSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -25,15 +26,40 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
*/ */
public interface DashChunkSource extends ChunkSource { public interface DashChunkSource extends ChunkSource {
/** Factory for {@link DashChunkSource}s. */
interface Factory { interface Factory {
DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, /**
DashManifest manifest, int periodIndex, int[] adaptationSetIndices, * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs, * @param manifest The initial manifest.
boolean enableEventMessageTrack, boolean enableCea608Track); * @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); void updateManifest(DashManifest newManifest, int periodIndex);
} }

View File

@ -309,6 +309,11 @@ import java.util.Map;
@Override @Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { 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; return positionUs;
} }

View File

@ -19,6 +19,7 @@ import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
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.SeekParameters;
import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.SeekMap; 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 @Override
public void updateManifest(DashManifest newManifest, int newPeriodIndex) { public void updateManifest(DashManifest newManifest, int newPeriodIndex) {
try { try {

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming;
import android.net.Uri; import android.net.Uri;
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.SeekParameters;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Track; import com.google.android.exoplayer2.extractor.mp4.Track;
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; 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.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -62,7 +64,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
} }
private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoaderErrorThrower manifestLoaderErrorThrower;
private final int elementIndex; private final int streamElementIndex;
private final TrackSelection trackSelection; private final TrackSelection trackSelection;
private final ChunkExtractorWrapper[] extractorWrappers; private final ChunkExtractorWrapper[] extractorWrappers;
private final DataSource dataSource; private final DataSource dataSource;
@ -75,22 +77,25 @@ public class DefaultSsChunkSource implements SsChunkSource {
/** /**
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest. * @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 trackSelection The track selection.
* @param dataSource A {@link DataSource} suitable for loading the media data. * @param dataSource A {@link DataSource} suitable for loading the media data.
* @param trackEncryptionBoxes Track encryption boxes for the stream. * @param trackEncryptionBoxes Track encryption boxes for the stream.
*/ */
public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, public DefaultSsChunkSource(
int elementIndex, TrackSelection trackSelection, DataSource dataSource, LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int streamElementIndex,
TrackSelection trackSelection,
DataSource dataSource,
TrackEncryptionBox[] trackEncryptionBoxes) { TrackEncryptionBox[] trackEncryptionBoxes) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest; this.manifest = manifest;
this.elementIndex = elementIndex; this.streamElementIndex = streamElementIndex;
this.trackSelection = trackSelection; this.trackSelection = trackSelection;
this.dataSource = dataSource; this.dataSource = dataSource;
StreamElement streamElement = manifest.streamElements[elementIndex]; StreamElement streamElement = manifest.streamElements[streamElementIndex];
extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()]; extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()];
for (int i = 0; i < extractorWrappers.length; i++) { for (int i = 0; i < extractorWrappers.length; i++) {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(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 @Override
public void updateManifest(SsManifest newManifest) { public void updateManifest(SsManifest newManifest) {
StreamElement currentElement = manifest.streamElements[elementIndex]; StreamElement currentElement = manifest.streamElements[streamElementIndex];
int currentElementChunkCount = currentElement.chunkCount; int currentElementChunkCount = currentElement.chunkCount;
StreamElement newElement = newManifest.streamElements[elementIndex]; StreamElement newElement = newManifest.streamElements[streamElementIndex];
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
// There's no overlap between the old and new elements because at least one is empty. // There's no overlap between the old and new elements because at least one is empty.
currentManifestChunkOffset += currentElementChunkCount; currentManifestChunkOffset += currentElementChunkCount;
@ -155,7 +172,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
return; return;
} }
StreamElement streamElement = manifest.streamElements[elementIndex]; StreamElement streamElement = manifest.streamElements[streamElementIndex];
if (streamElement.chunkCount == 0) { if (streamElement.chunkCount == 0) {
// There aren't any chunks for us to load. // There aren't any chunks for us to load.
out.endOfStream = !manifest.isLive; out.endOfStream = !manifest.isLive;
@ -229,7 +246,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
StreamElement currentElement = manifest.streamElements[elementIndex]; StreamElement currentElement = manifest.streamElements[streamElementIndex];
int lastChunkIndex = currentElement.chunkCount - 1; int lastChunkIndex = currentElement.chunkCount - 1;
long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex) long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex)
+ currentElement.getChunkDurationUs(lastChunkIndex); + currentElement.getChunkDurationUs(lastChunkIndex);

View File

@ -26,14 +26,31 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
*/ */
public interface SsChunkSource extends ChunkSource { public interface SsChunkSource extends ChunkSource {
/** Factory for {@link SsChunkSource}s. */
interface Factory { 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); TrackEncryptionBox[] trackEncryptionBoxes);
} }
/**
* Updates the manifest.
*
* @param newManifest The new manifest.
*/
void updateManifest(SsManifest newManifest); void updateManifest(SsManifest newManifest);
} }

View File

@ -185,6 +185,11 @@ import java.util.ArrayList;
@Override @Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { 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; return positionUs;
} }

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
import android.net.Uri; import android.net.Uri;
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.SeekParameters;
import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkHolder;
import com.google.android.exoplayer2.source.chunk.ChunkSource; 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.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -71,6 +74,17 @@ public final class FakeChunkSource implements ChunkSource {
this.dataSet = dataSet; 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 @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
// Do nothing. // Do nothing.