Add available segment logic to SegmentIndex

This allows to use the same logic from multiple places without duplicating
it, encapsulates in its logical place, and allows to change the available
segments based on the new availabilityTimeOffset value.

The overall effect of this change is a no-op.

PiperOrigin-RevId: 333044186
This commit is contained in:
tonihei 2020-09-22 11:38:05 +01:00 committed by kim-vde
parent fb4b705cfe
commit 12e887438b
6 changed files with 174 additions and 65 deletions

View File

@ -64,38 +64,54 @@ public interface DashSegmentIndex {
*/ */
RangedUri getSegmentUrl(long segmentNum); RangedUri getSegmentUrl(long segmentNum);
/** /** Returns the segment number of the first defined segment in the index. */
* Returns the segment number of the first segment.
*
* @return The segment number of the first segment.
*/
long getFirstSegmentNum(); long getFirstSegmentNum();
/** /**
* Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}. * Returns the segment number of the first available segment in the index.
* <p>
* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
* SegmentTimeline element, and if the period duration is not yet known. In this case the caller
* must manually determine the window of currently available segments.
* *
* @param periodDurationUs The duration of the enclosing period in microseconds, or * @param periodDurationUs The duration of the enclosing period in microseconds, or {@link
* {@link C#TIME_UNSET} if the period's duration is not yet known. * C#TIME_UNSET} if the period's duration is not yet known.
* @param nowUnixTimeUs The current time in milliseconds since the Unix epoch.
* @return The number of the first available segment.
*/
long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs);
/**
* Returns the number of segments defined in the index, or {@link #INDEX_UNBOUNDED}.
*
* <p>An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
* SegmentTimeline element, and if the period duration is not yet known. In this case the caller
* can query the available segment using {@link #getFirstAvailableSegmentNum(long, long)} and
* {@link #getAvailableSegmentCount(long, long)}.
*
* @param periodDurationUs The duration of the enclosing period in microseconds, or {@link
* C#TIME_UNSET} if the period's duration is not yet known.
* @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}.
*/ */
int getSegmentCount(long periodDurationUs); int getSegmentCount(long periodDurationUs);
/**
* Returns the number of available segments in the index.
*
* @param periodDurationUs The duration of the enclosing period in microseconds, or {@link
* C#TIME_UNSET} if the period's duration is not yet known.
* @param nowUnixTimeUs The current time in milliseconds since the Unix epoch.
* @return The number of available segments in the index.
*/
int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs);
/** /**
* Returns true if segments are defined explicitly by the index. * Returns true if segments are defined explicitly by the index.
* <p> *
* If true is returned, each segment is defined explicitly by the index data, and all of the * <p>If true is returned, each segment is defined explicitly by the index data, and all of the
* listed segments are guaranteed to be available at the time when the index was obtained. * listed segments are guaranteed to be available at the time when the index was obtained.
* <p> *
* If false is returned then segment information was derived from properties such as a fixed * <p>If false is returned then segment information was derived from properties such as a fixed
* segment duration. If the presentation is dynamic, it's possible that only a subset of the * segment duration. If the presentation is dynamic, it's possible that only a subset of the
* segments are available. * segments are available.
* *
* @return Whether segments are defined explicitly by the index. * @return Whether segments are defined explicitly by the index.
*/ */
boolean isExplicit(); boolean isExplicit();
} }

View File

@ -41,11 +41,21 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex {
return 0; return 0;
} }
@Override
public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) {
return 0;
}
@Override @Override
public int getSegmentCount(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return chunkIndex.length; return chunkIndex.length;
} }
@Override
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
return chunkIndex.length;
}
@Override @Override
public long getTimeUs(long segmentNum) { public long getTimeUs(long segmentNum) {
return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs; return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs;

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.source.dash; package com.google.android.exoplayer2.source.dash;
import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
@ -288,9 +287,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
chunkIterators[i] = MediaChunkIterator.EMPTY; chunkIterators[i] = MediaChunkIterator.EMPTY;
} else { } else {
long firstAvailableSegmentNum = long firstAvailableSegmentNum =
representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs);
long lastAvailableSegmentNum = long lastAvailableSegmentNum =
representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs);
long segmentNum = long segmentNum =
getSegmentNum( getSegmentNum(
representationHolder, representationHolder,
@ -342,10 +341,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
return; return;
} }
long firstAvailableSegmentNum = long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs);
representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); long lastAvailableSegmentNum = representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs);
long lastAvailableSegmentNum =
representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);
updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum);
@ -739,6 +736,11 @@ public class DefaultDashChunkSource implements DashChunkSource {
return segmentIndex.getFirstSegmentNum() + segmentNumShift; return segmentIndex.getFirstSegmentNum() + segmentNumShift;
} }
public long getFirstAvailableSegmentNum(long nowUnixTimeUs) {
return segmentIndex.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs)
+ segmentNumShift;
}
public int getSegmentCount() { public int getSegmentCount() {
return segmentIndex.getSegmentCount(periodDurationUs); return segmentIndex.getSegmentCount(periodDurationUs);
} }
@ -760,35 +762,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift); return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift);
} }
public long getFirstAvailableSegmentNum( public long getLastAvailableSegmentNum(long nowUnixTimeUs) {
DashManifest manifest, int periodIndex, long nowUnixTimeUs) { return getFirstAvailableSegmentNum(nowUnixTimeUs)
if (getSegmentCount() == DashSegmentIndex.INDEX_UNBOUNDED + segmentIndex.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs)
&& manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { - 1;
// The index is itself unbounded. We need to use the current time to calculate the range of
// available segments.
long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);
long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);
long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;
long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);
return max(getFirstSegmentNum(), getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs));
}
return getFirstSegmentNum();
}
public long getLastAvailableSegmentNum(
DashManifest manifest, int periodIndex, long nowUnixTimeUs) {
int availableSegmentCount = getSegmentCount();
if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) {
// The index is itself unbounded. We need to use the current time to calculate the range of
// available segments.
long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);
long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);
long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;
// getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get
// the index of the last completed segment.
return getSegmentNum(liveEdgeTimeInPeriodUs) - 1;
}
return getFirstSegmentNum() + availableSegmentCount - 1;
} }
@Nullable @Nullable

View File

@ -138,7 +138,13 @@ public class DashManifestParser extends DefaultHandler
location = Uri.parse(xpp.nextText()); location = Uri.parse(xpp.nextText());
} else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
Pair<Period, Long> periodWithDurationMs = Pair<Period, Long> periodWithDurationMs =
parsePeriod(xpp, baseUrl, nextPeriodStartMs, baseUrlAvailabilityTimeOffsetUs); parsePeriod(
xpp,
baseUrl,
nextPeriodStartMs,
baseUrlAvailabilityTimeOffsetUs,
availabilityStartTime,
timeShiftBufferDepthMs);
Period period = periodWithDurationMs.first; Period period = periodWithDurationMs.first;
if (period.startMs == C.TIME_UNSET) { if (period.startMs == C.TIME_UNSET) {
if (dynamic) { if (dynamic) {
@ -226,10 +232,17 @@ public class DashManifestParser extends DefaultHandler
} }
protected Pair<Period, Long> parsePeriod( protected Pair<Period, Long> parsePeriod(
XmlPullParser xpp, String baseUrl, long defaultStartMs, long baseUrlAvailabilityTimeOffsetUs) XmlPullParser xpp,
String baseUrl,
long defaultStartMs,
long baseUrlAvailabilityTimeOffsetUs,
long availabilityStartTimeMs,
long timeShiftBufferDepthMs)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
@Nullable String id = xpp.getAttributeValue(null, "id"); @Nullable String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", defaultStartMs); long startMs = parseDuration(xpp, "start", defaultStartMs);
long periodStartUnixTimeMs =
availabilityStartTimeMs != C.TIME_UNSET ? availabilityStartTimeMs + startMs : C.TIME_UNSET;
long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
@Nullable SegmentBase segmentBase = null; @Nullable SegmentBase segmentBase = null;
@Nullable Descriptor assetIdentifier = null; @Nullable Descriptor assetIdentifier = null;
@ -254,7 +267,9 @@ public class DashManifestParser extends DefaultHandler
segmentBase, segmentBase,
durationMs, durationMs,
baseUrlAvailabilityTimeOffsetUs, baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs)); segmentBaseAvailabilityTimeOffsetUs,
periodStartUnixTimeMs,
timeShiftBufferDepthMs));
} else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) {
eventStreams.add(parseEventStream(xpp)); eventStreams.add(parseEventStream(xpp));
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
@ -308,7 +323,9 @@ public class DashManifestParser extends DefaultHandler
@Nullable SegmentBase segmentBase, @Nullable SegmentBase segmentBase,
long periodDurationMs, long periodDurationMs,
long baseUrlAvailabilityTimeOffsetUs, long baseUrlAvailabilityTimeOffsetUs,
long segmentBaseAvailabilityTimeOffsetUs) long segmentBaseAvailabilityTimeOffsetUs,
long periodStartUnixTimeMs,
long timeShiftBufferDepthMs)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET);
int contentType = parseContentType(xpp); int contentType = parseContentType(xpp);
@ -428,7 +445,9 @@ public class DashManifestParser extends DefaultHandler
label, label,
drmSchemeType, drmSchemeType,
drmSchemeDatas, drmSchemeDatas,
inbandEventStreams)); inbandEventStreams,
periodStartUnixTimeMs,
timeShiftBufferDepthMs));
} }
return buildAdaptationSet( return buildAdaptationSet(
@ -725,7 +744,9 @@ public class DashManifestParser extends DefaultHandler
@Nullable String label, @Nullable String label,
@Nullable String extraDrmSchemeType, @Nullable String extraDrmSchemeType,
ArrayList<SchemeData> extraDrmSchemeDatas, ArrayList<SchemeData> extraDrmSchemeDatas,
ArrayList<Descriptor> extraInbandEventStreams) { ArrayList<Descriptor> extraInbandEventStreams,
long periodStartUnixTimeMs,
long timeShiftBufferDepthMs) {
Format.Builder formatBuilder = representationInfo.format.buildUpon(); Format.Builder formatBuilder = representationInfo.format.buildUpon();
if (label != null) { if (label != null) {
formatBuilder.setLabel(label); formatBuilder.setLabel(label);
@ -747,7 +768,9 @@ public class DashManifestParser extends DefaultHandler
formatBuilder.build(), formatBuilder.build(),
representationInfo.baseUrl, representationInfo.baseUrl,
representationInfo.segmentBase, representationInfo.segmentBase,
inbandEventStreams); inbandEventStreams,
periodStartUnixTimeMs,
timeShiftBufferDepthMs);
} }
// SegmentBase, SegmentList and SegmentTemplate parsing. // SegmentBase, SegmentList and SegmentTemplate parsing.

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.source.dash.manifest; package com.google.android.exoplayer2.source.dash.manifest;
import static java.lang.Math.max;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -71,7 +73,14 @@ public abstract class Representation {
*/ */
public static Representation newInstance( public static Representation newInstance(
long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { long revisionId, Format format, String baseUrl, SegmentBase segmentBase) {
return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); return newInstance(
revisionId,
format,
baseUrl,
segmentBase,
/* inbandEventStreams= */ null,
/* periodStartUnixTimeMs= */ C.TIME_UNSET,
/* timeShiftBufferDepthMs= */ C.TIME_UNSET);
} }
/** /**
@ -82,6 +91,9 @@ public abstract class Representation {
* @param baseUrl The base URL. * @param baseUrl The base URL.
* @param segmentBase A segment base element for the representation. * @param segmentBase A segment base element for the representation.
* @param inbandEventStreams The in-band event streams in the representation. May be null. * @param inbandEventStreams The in-band event streams in the representation. May be null.
* @param periodStartUnixTimeMs The start time of the enclosing {@link Period} in milliseconds
* since the Unix epoch, or {@link C#TIME_UNSET} is not applicable.
* @param timeShiftBufferDepthMs The {@link DashManifest#timeShiftBufferDepthMs}.
* @return The constructed instance. * @return The constructed instance.
*/ */
public static Representation newInstance( public static Representation newInstance(
@ -89,9 +101,18 @@ public abstract class Representation {
Format format, Format format,
String baseUrl, String baseUrl,
SegmentBase segmentBase, SegmentBase segmentBase,
@Nullable List<Descriptor> inbandEventStreams) { @Nullable List<Descriptor> inbandEventStreams,
long periodStartUnixTimeMs,
long timeShiftBufferDepthMs) {
return newInstance( return newInstance(
revisionId, format, baseUrl, segmentBase, inbandEventStreams, /* cacheKey= */ null); revisionId,
format,
baseUrl,
segmentBase,
inbandEventStreams,
periodStartUnixTimeMs,
timeShiftBufferDepthMs,
/* cacheKey= */ null);
} }
/** /**
@ -102,6 +123,9 @@ public abstract class Representation {
* @param baseUrl The base URL of the representation. * @param baseUrl The base URL of the representation.
* @param segmentBase A segment base element for the representation. * @param segmentBase A segment base element for the representation.
* @param inbandEventStreams The in-band event streams in the representation. May be null. * @param inbandEventStreams The in-band event streams in the representation. May be null.
* @param periodStartUnixTimeMs The start time of the enclosing {@link Period} in milliseconds
* since the Unix epoch, or {@link C#TIME_UNSET} is not applicable.
* @param timeShiftBufferDepthMs The {@link DashManifest#timeShiftBufferDepthMs}.
* @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This
* parameter is ignored if {@code segmentBase} consists of multiple segments. * parameter is ignored if {@code segmentBase} consists of multiple segments.
* @return The constructed instance. * @return The constructed instance.
@ -112,6 +136,8 @@ public abstract class Representation {
String baseUrl, String baseUrl,
SegmentBase segmentBase, SegmentBase segmentBase,
@Nullable List<Descriptor> inbandEventStreams, @Nullable List<Descriptor> inbandEventStreams,
long periodStartUnixTimeMs,
long timeShiftBufferDepthMs,
@Nullable String cacheKey) { @Nullable String cacheKey) {
if (segmentBase instanceof SingleSegmentBase) { if (segmentBase instanceof SingleSegmentBase) {
return new SingleSegmentRepresentation( return new SingleSegmentRepresentation(
@ -124,7 +150,13 @@ public abstract class Representation {
C.LENGTH_UNSET); C.LENGTH_UNSET);
} else if (segmentBase instanceof MultiSegmentBase) { } else if (segmentBase instanceof MultiSegmentBase) {
return new MultiSegmentRepresentation( return new MultiSegmentRepresentation(
revisionId, format, baseUrl, (MultiSegmentBase) segmentBase, inbandEventStreams); revisionId,
format,
baseUrl,
(MultiSegmentBase) segmentBase,
inbandEventStreams,
periodStartUnixTimeMs,
timeShiftBufferDepthMs);
} else { } else {
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+ "MultiSegmentBase"); + "MultiSegmentBase");
@ -277,22 +309,33 @@ public abstract class Representation {
implements DashSegmentIndex { implements DashSegmentIndex {
@VisibleForTesting /* package */ final MultiSegmentBase segmentBase; @VisibleForTesting /* package */ final MultiSegmentBase segmentBase;
private final long periodStartUnixTimeUs;
private final long timeShiftBufferDepthUs;
/** /**
* Creates the multi-segment Representation.
*
* @param revisionId Identifies the revision of the content. * @param revisionId Identifies the revision of the content.
* @param format The format of the representation. * @param format The format of the representation.
* @param baseUrl The base URL of the representation. * @param baseUrl The base URL of the representation.
* @param segmentBase The segment base underlying the representation. * @param segmentBase The segment base underlying the representation.
* @param inbandEventStreams The in-band event streams in the representation. May be null. * @param inbandEventStreams The in-band event streams in the representation. May be null.
* @param periodStartUnixTimeMs The start time of the enclosing {@link Period} in milliseconds
* since the Unix epoch, or {@link C#TIME_UNSET} is not applicable.
* @param timeShiftBufferDepthMs The {@link DashManifest#timeShiftBufferDepthMs}.
*/ */
public MultiSegmentRepresentation( public MultiSegmentRepresentation(
long revisionId, long revisionId,
Format format, Format format,
String baseUrl, String baseUrl,
MultiSegmentBase segmentBase, MultiSegmentBase segmentBase,
@Nullable List<Descriptor> inbandEventStreams) { @Nullable List<Descriptor> inbandEventStreams,
long periodStartUnixTimeMs,
long timeShiftBufferDepthMs) {
super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); super(revisionId, format, baseUrl, segmentBase, inbandEventStreams);
this.segmentBase = segmentBase; this.segmentBase = segmentBase;
this.periodStartUnixTimeUs = C.msToUs(periodStartUnixTimeMs);
this.timeShiftBufferDepthUs = C.msToUs(timeShiftBufferDepthMs);
} }
@Override @Override
@ -339,11 +382,41 @@ public abstract class Representation {
return segmentBase.getFirstSegmentNum(); return segmentBase.getFirstSegmentNum();
} }
@Override
public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) {
long segmentCount = segmentBase.getSegmentCount(periodDurationUs);
if (segmentCount != INDEX_UNBOUNDED || timeShiftBufferDepthUs == C.TIME_UNSET) {
return segmentBase.getFirstSegmentNum();
}
// The index is itself unbounded. We need to use the current time to calculate the range of
// available segments.
long liveEdgeTimeInPeriodUs = nowUnixTimeUs - periodStartUnixTimeUs;
long timeShiftBufferStartInPeriodUs = liveEdgeTimeInPeriodUs - timeShiftBufferDepthUs;
long timeShiftBufferStartSegmentNum =
getSegmentNum(timeShiftBufferStartInPeriodUs, periodDurationUs);
return max(getFirstSegmentNum(), timeShiftBufferStartSegmentNum);
}
@Override @Override
public int getSegmentCount(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return segmentBase.getSegmentCount(periodDurationUs); return segmentBase.getSegmentCount(periodDurationUs);
} }
@Override
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
int segmentCount = segmentBase.getSegmentCount(periodDurationUs);
if (segmentCount != INDEX_UNBOUNDED) {
return segmentCount;
}
// The index is itself unbounded. We need to use the current time to calculate the range of
// available segments.
long liveEdgeTimeInPeriodUs = nowUnixTimeUs - periodStartUnixTimeUs;
// getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet.
long firstIncompleteSegmentNum = getSegmentNum(liveEdgeTimeInPeriodUs, periodDurationUs);
long firstAvailableSegmentNum = getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs);
return (int) (firstIncompleteSegmentNum - firstAvailableSegmentNum);
}
@Override @Override
public boolean isExplicit() { public boolean isExplicit() {
return segmentBase.isExplicit(); return segmentBase.isExplicit();

View File

@ -56,11 +56,21 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
return 0; return 0;
} }
@Override
public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) {
return 0;
}
@Override @Override
public int getSegmentCount(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return 1; return 1;
} }
@Override
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
return 1;
}
@Override @Override
public boolean isExplicit() { public boolean isExplicit() {
return true; return true;