trackSampleTables = new ArrayList<>();
+ for (int i = 0; i < moov.containerChildren.size(); i++) {
+ Atom.ContainerAtom atom = moov.containerChildren.get(i);
+ if (atom.type != Atom.TYPE_trak) {
+ continue;
+ }
+ Track track =
+ AtomParsers.parseTrak(
+ atom,
+ moov.getLeafAtomOfType(Atom.TYPE_mvhd),
+ /* duration= */ C.TIME_UNSET,
+ /* drmInitData= */ null,
+ ignoreEditLists,
+ isQuickTime);
+ if (track == null) {
+ continue;
+ }
+ Atom.ContainerAtom stblAtom =
+ atom.getContainerAtomOfType(Atom.TYPE_mdia)
+ .getContainerAtomOfType(Atom.TYPE_minf)
+ .getContainerAtomOfType(Atom.TYPE_stbl);
+ TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
+ if (trackSampleTable.sampleCount == 0) {
+ continue;
+ }
+ trackSampleTables.add(trackSampleTable);
+ }
+ return trackSampleTables;
+ }
+
/**
* Attempts to extract the next sample in the current mdat atom for the specified track.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
index 9f77c49664..56851fc1e0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class TrackSampleTable {
- /**
- * Number of samples.
- */
+ /** The track corresponding to this sample table. */
+ public final Track track;
+ /** Number of samples. */
public final int sampleCount;
- /**
- * Sample offsets in bytes.
- */
+ /** Sample offsets in bytes. */
public final long[] offsets;
- /**
- * Sample sizes in bytes.
- */
+ /** Sample sizes in bytes. */
public final int[] sizes;
- /**
- * Maximum sample size in {@link #sizes}.
- */
+ /** Maximum sample size in {@link #sizes}. */
public final int maximumSize;
- /**
- * Sample timestamps in microseconds.
- */
+ /** Sample timestamps in microseconds. */
public final long[] timestampsUs;
- /**
- * Sample flags.
- */
+ /** Sample flags. */
public final int[] flags;
/**
* The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
@@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util;
public final long durationUs;
public TrackSampleTable(
+ Track track,
long[] offsets,
int[] sizes,
int maximumSize,
@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length);
+ this.track = track;
this.offsets = offsets;
this.sizes = sizes;
this.maximumSize = maximumSize;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
index f3aad6ba6b..8acb36b41e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor {
private static final int PACKET_START_CODE_PREFIX = 0x000001;
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
+
+ // Max search length for first audio and video track in input data.
private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
+ // Max search length for additional audio and video tracks in input data after at least one audio
+ // and video track has been found.
+ private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;
public static final int PRIVATE_STREAM_1 = 0xBD;
public static final int AUDIO_STREAM = 0xC0;
@@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor {
private boolean foundAllTracks;
private boolean foundAudioTrack;
private boolean foundVideoTrack;
+ private long lastTrackPosition;
// Accessed only by the loading thread.
private ExtractorOutput output;
@@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor {
if (!foundAllTracks) {
if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null;
- if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) {
+ if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs.
elementaryStreamReader = new Ac3Reader();
foundAudioTrack = true;
- } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
elementaryStreamReader = new MpegAudioReader();
foundAudioTrack = true;
- } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
+ lastTrackPosition = input.getPosition();
+ } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
elementaryStreamReader = new H262Reader();
foundVideoTrack = true;
+ lastTrackPosition = input.getPosition();
}
if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
@@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor {
psPayloadReaders.put(streamId, payloadReader);
}
}
- if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) {
+ long maxSearchPosition =
+ foundAudioTrack && foundVideoTrack
+ ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
+ : MAX_SEARCH_LENGTH;
+ if (input.getPosition() > maxSearchPosition) {
foundAllTracks = true;
output.endTracks();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 03a0b66661..b966492d8c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
+ if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
+ // Wait for keys.
+ return;
+ }
+ }
}
if (codecInfo == null) {
@@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
- codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
+ codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
@@ -1209,6 +1218,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
+ /**
+ * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before
+ * codec configuration.
+ */
+ private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() {
+ return "Amazon".equals(Util.MANUFACTURER)
+ && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
+ || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1
+ }
+
/**
* Returns whether the decoder is known to fail when flushed.
*
@@ -1272,20 +1291,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
- * Returns whether the decoder is known to handle the propagation of the
- * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
- *
- * If true is returned, the renderer will work around the issue by approximating end of stream
+ * Returns whether the decoder is known to handle the propagation of the {@link
+ * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
+ *
+ *
If true is returned, the renderer will work around the issue by approximating end of stream
* behavior without relying on the flag being propagated through to an output buffer by the
* underlying decoder.
*
- * @param name The name of the decoder.
+ * @param codecInfo Information about the {@link MediaCodec}.
* @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
* propagation incorrectly on the host device. False otherwise.
*/
- private static boolean codecNeedsEosPropagationWorkaround(String name) {
- return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name)
- || "OMX.allwinner.video.decoder.avc".equals(name));
+ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
+ String name = codecInfo.name;
+ return (Util.SDK_INT <= 17
+ && ("OMX.rk.video_decoder.avc".equals(name)
+ || "OMX.allwinner.video.decoder.avc".equals(name)))
+ || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
new file mode 100644
index 0000000000..a828d80069
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 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.metadata.id3;
+
+import android.os.Parcel;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/** Internal ID3 frame that is intended for use by the player. */
+public final class InternalFrame extends Id3Frame {
+
+ public static final String ID = "----";
+
+ public final String domain;
+ public final String description;
+ public final String text;
+
+ public InternalFrame(String domain, String description, String text) {
+ super(ID);
+ this.domain = domain;
+ this.description = description;
+ this.text = text;
+ }
+
+ /* package */ InternalFrame(Parcel in) {
+ super(ID);
+ domain = Assertions.checkNotNull(in.readString());
+ description = Assertions.checkNotNull(in.readString());
+ text = Assertions.checkNotNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ InternalFrame other = (InternalFrame) obj;
+ return Util.areEqual(description, other.description)
+ && Util.areEqual(domain, other.domain)
+ && Util.areEqual(text, other.text);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (domain != null ? domain.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (text != null ? text.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": domain=" + domain + ", description=" + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(domain);
+ dest.writeString(text);
+ }
+
+ public static final Creator CREATOR =
+ new Creator() {
+
+ @Override
+ public InternalFrame createFromParcel(Parcel in) {
+ return new InternalFrame(in);
+ }
+
+ @Override
+ public InternalFrame[] newArray(int size) {
+ return new InternalFrame[size];
+ }
+ };
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
index 6dae3f70b3..995e71a94a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
@@ -86,6 +86,7 @@ public abstract class DownloadService extends Service {
private DownloadManagerListener downloadManagerListener;
private int lastStartId;
private boolean startedInForeground;
+ private boolean taskRemoved;
/**
* Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
@@ -219,12 +220,17 @@ public abstract class DownloadService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
lastStartId = startId;
+ taskRemoved = false;
String intentAction = null;
if (intent != null) {
intentAction = intent.getAction();
startedInForeground |=
intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
}
+ // intentAction is null if the service is restarted or no action is specified.
+ if (intentAction == null) {
+ intentAction = ACTION_INIT;
+ }
logd("onStartCommand action: " + intentAction + " startId: " + startId);
switch (intentAction) {
case ACTION_INIT:
@@ -260,6 +266,12 @@ public abstract class DownloadService extends Service {
return START_STICKY;
}
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ logd("onTaskRemoved rootIntent: " + rootIntent);
+ taskRemoved = true;
+ }
+
@Override
public void onDestroy() {
logd("onDestroy");
@@ -353,8 +365,13 @@ public abstract class DownloadService extends Service {
if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready();
}
- boolean stopSelfResult = stopSelfResult(lastStartId);
- logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
+ if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
+ stopSelf();
+ logd("stopSelf()");
+ } else {
+ boolean stopSelfResult = stopSelfResult(lastStartId);
+ logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
+ }
}
private void logd(String message) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
index 8654e94bdb..246d804c89 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
@@ -344,6 +344,14 @@ public final class AdPlaybackState {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
+ /** Returns an instance with the specified ad marked as skipped. */
+ @CheckResult
+ public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
/** Returns an instance with the specified ad marked as having a load error. */
@CheckResult
public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
index 57614ae880..725321e53f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -21,10 +21,10 @@ import android.text.Layout.Alignment;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
-import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
@@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder {
private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
- private static final int[] COLORS = new int[] {
- Color.WHITE,
- Color.GREEN,
- Color.BLUE,
- Color.CYAN,
- Color.RED,
- Color.YELLOW,
- Color.MAGENTA,
- };
+
+ private static final int[] STYLE_COLORS =
+ new int[] {
+ Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA
+ };
+ private static final int STYLE_ITALICS = 0x07;
+ private static final int STYLE_UNCHANGED = 0x08;
// The default number of rows to display in roll-up captions mode.
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
@@ -377,18 +375,10 @@ public final class Cea608Decoder extends CeaDecoder {
// A midrow control code advances the cursor.
currentCueBuilder.append(' ');
- // cc2 - 0|0|1|0|ATRBT|U
- // ATRBT is the 3-byte encoded attribute, and U is the underline toggle
- boolean isUnderlined = (cc2 & 0x01) == 0x01;
- currentCueBuilder.setUnderline(isUnderlined);
-
- int attribute = (cc2 >> 1) & 0x0F;
- if (attribute == 0x07) {
- currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2);
- currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1);
- } else {
- currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1);
- }
+ // cc2 - 0|0|1|0|STYLE|U
+ boolean underline = (cc2 & 0x01) == 0x01;
+ int style = (cc2 >> 1) & 0x07;
+ currentCueBuilder.setStyle(style, underline);
}
private void handlePreambleAddressCode(byte cc1, byte cc2) {
@@ -414,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder {
currentCueBuilder.setRow(row);
}
- if ((cc2 & 0x01) == 0x01) {
- currentCueBuilder.setPreambleStyle(new UnderlineSpan());
- }
-
// cc2 - 0|1|N|0|STYLE|U
// cc2 - 0|1|N|1|CURSR|U
- int attribute = cc2 >> 1 & 0x0F;
- if (attribute <= 0x07) {
- if (attribute == 0x07) {
- currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC));
- currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE));
- } else {
- currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute]));
- }
- } else {
- currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]);
+ boolean isCursor = (cc2 & 0x10) == 0x10;
+ boolean underline = (cc2 & 0x01) == 0x01;
+ int cursorOrStyle = (cc2 >> 1) & 0x07;
+
+ // We need to call setStyle even for the isCursor case, to update the underline bit.
+ // STYLE_UNCHANGED is used for this case.
+ currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline);
+
+ if (isCursor) {
+ currentCueBuilder.setIndent(COLUMN_INDICES[cursorOrStyle]);
}
}
@@ -585,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder {
private static class CueBuilder {
- private static final int POSITION_UNSET = -1;
-
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
// positions to normalized screen position.
private static final int SCREEN_CHARWIDTH = 32;
private static final int BASE_ROW = 15;
- private final List preambleStyles;
- private final List midrowStyles;
+ private final List cueStyles;
private final List rolledUpCaptions;
- private final SpannableStringBuilder captionStringBuilder;
+ private final StringBuilder captionStringBuilder;
private int row;
private int indent;
private int tabOffset;
private int captionMode;
private int captionRowCount;
- private int underlineStartPosition;
public CueBuilder(int captionMode, int captionRowCount) {
- preambleStyles = new ArrayList<>();
- midrowStyles = new ArrayList<>();
+ cueStyles = new ArrayList<>();
rolledUpCaptions = new ArrayList<>();
- captionStringBuilder = new SpannableStringBuilder();
+ captionStringBuilder = new StringBuilder();
reset(captionMode);
setCaptionRowCount(captionRowCount);
}
public void reset(int captionMode) {
this.captionMode = captionMode;
- preambleStyles.clear();
- midrowStyles.clear();
+ cueStyles.clear();
rolledUpCaptions.clear();
- captionStringBuilder.clear();
+ captionStringBuilder.setLength(0);
row = BASE_ROW;
indent = 0;
tabOffset = 0;
- underlineStartPosition = POSITION_UNSET;
}
public void setCaptionRowCount(int captionRowCount) {
@@ -630,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder {
}
public boolean isEmpty() {
- return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
+ return cueStyles.isEmpty()
+ && rolledUpCaptions.isEmpty()
&& captionStringBuilder.length() == 0;
}
@@ -638,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder {
int length = captionStringBuilder.length();
if (length > 0) {
captionStringBuilder.delete(length - 1, length);
+ // Decrement style start positions if necessary.
+ for (int i = cueStyles.size() - 1; i >= 0; i--) {
+ CueStyle style = cueStyles.get(i);
+ if (style.start == length) {
+ style.start--;
+ } else {
+ // All earlier cues must have style.start < length.
+ break;
+ }
+ }
}
}
@@ -651,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder {
public void rollUp() {
rolledUpCaptions.add(buildSpannableString());
- captionStringBuilder.clear();
- preambleStyles.clear();
- midrowStyles.clear();
- underlineStartPosition = POSITION_UNSET;
-
+ captionStringBuilder.setLength(0);
+ cueStyles.clear();
int numRows = Math.min(captionRowCount, row);
while (rolledUpCaptions.size() >= numRows) {
rolledUpCaptions.remove(0);
@@ -670,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder {
tabOffset = tabs;
}
- public void setPreambleStyle(CharacterStyle style) {
- preambleStyles.add(style);
- }
-
- public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) {
- midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement));
- }
-
- public void setUnderline(boolean enabled) {
- if (enabled) {
- underlineStartPosition = captionStringBuilder.length();
- } else if (underlineStartPosition != POSITION_UNSET) {
- // underline spans won't overlap, so it's safe to modify the builder directly with them
- captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
- captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- underlineStartPosition = POSITION_UNSET;
- }
+ public void setStyle(int style, boolean underline) {
+ cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length()));
}
public void append(char text) {
@@ -694,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder {
}
public SpannableString buildSpannableString() {
- int length = captionStringBuilder.length();
+ SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder);
+ int length = builder.length();
- // preamble styles apply to the entire cue
- for (int i = 0; i < preambleStyles.size(); i++) {
- captionStringBuilder.setSpan(preambleStyles.get(i), 0, length,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ int underlineStartPosition = C.INDEX_UNSET;
+ int italicStartPosition = C.INDEX_UNSET;
+ int colorStartPosition = 0;
+ int color = Color.WHITE;
+
+ boolean nextItalic = false;
+ int nextColor = Color.WHITE;
+
+ for (int i = 0; i < cueStyles.size(); i++) {
+ CueStyle cueStyle = cueStyles.get(i);
+ boolean underline = cueStyle.underline;
+ int style = cueStyle.style;
+ if (style != STYLE_UNCHANGED) {
+ // If the style is a color then italic is cleared.
+ nextItalic = style == STYLE_ITALICS;
+ // If the style is italic then the color is left unchanged.
+ nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style];
+ }
+
+ int position = cueStyle.start;
+ int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length;
+ if (position == nextPosition) {
+ // There are more cueStyles to process at the current position.
+ continue;
+ }
+
+ // Process changes to underline up to the current position.
+ if (underlineStartPosition != C.INDEX_UNSET && !underline) {
+ setUnderlineSpan(builder, underlineStartPosition, position);
+ underlineStartPosition = C.INDEX_UNSET;
+ } else if (underlineStartPosition == C.INDEX_UNSET && underline) {
+ underlineStartPosition = position;
+ }
+ // Process changes to italic up to the current position.
+ if (italicStartPosition != C.INDEX_UNSET && !nextItalic) {
+ setItalicSpan(builder, italicStartPosition, position);
+ italicStartPosition = C.INDEX_UNSET;
+ } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) {
+ italicStartPosition = position;
+ }
+ // Process changes to color up to the current position.
+ if (nextColor != color) {
+ setColorSpan(builder, colorStartPosition, position, color);
+ color = nextColor;
+ colorStartPosition = position;
+ }
}
- // midrow styles only apply to part of the cue, and after preamble styles
- for (int i = 0; i < midrowStyles.size(); i++) {
- CueStyle cueStyle = midrowStyles.get(i);
- int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement)
- ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start
- : length;
- captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ // Add any final spans.
+ if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) {
+ setUnderlineSpan(builder, underlineStartPosition, length);
+ }
+ if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) {
+ setItalicSpan(builder, italicStartPosition, length);
+ }
+ if (colorStartPosition != length) {
+ setColorSpan(builder, colorStartPosition, length, color);
}
- // special case for midrow underlines that went to the end of the cue
- if (underlineStartPosition != POSITION_UNSET) {
- captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- return new SpannableString(captionStringBuilder);
+ return new SpannableString(builder);
}
public Cue build() {
@@ -788,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder {
return captionStringBuilder.toString();
}
+ private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) {
+ builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) {
+ builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void setColorSpan(
+ SpannableStringBuilder builder, int start, int end, int color) {
+ if (color == Color.WHITE) {
+ // White is treated as the default color (i.e. no span is attached).
+ return;
+ }
+ builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
private static class CueStyle {
- public final CharacterStyle style;
- public final int start;
- public final int nextStyleIncrement;
+ public final int style;
+ public final boolean underline;
- public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) {
+ public int start;
+
+ public CueStyle(int style, boolean underline, int start) {
this.style = style;
+ this.underline = underline;
this.start = start;
- this.nextStyleIncrement = nextStyleIncrement;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index 3bbb2a7941..0067ffa552 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -153,7 +153,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class DefaultTrackSelector extends MappingTrackSelector {
/**
- * A builder for {@link Parameters}.
+ * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of
+ * the parameters that can be configured using this builder.
*/
public static final class ParametersBuilder {
@@ -177,9 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private boolean viewportOrientationMayChange;
private int tunnelingAudioSessionId;
- /**
- * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}.
- */
+ /** Creates a builder with default initial values. */
public ParametersBuilder() {
this(Parameters.DEFAULT);
}
@@ -343,15 +342,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
- * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from
- * {@link Util#getPhysicalDisplaySize(Context)}.
+ * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size
+ * obtained from {@link Util#getPhysicalDisplaySize(Context)}.
*
- * @param context The context to obtain the viewport size from.
- * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}.
+ * @param context Any context.
+ * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}.
* @return This builder.
*/
- public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context,
- boolean viewportOrientationMayChange) {
+ public ParametersBuilder setViewportSizeToPhysicalDisplaySize(
+ Context context, boolean viewportOrientationMayChange) {
// Assume the viewport is fullscreen.
Point viewportSize = Util.getPhysicalDisplaySize(context);
return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
@@ -368,13 +367,16 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
- * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and
- * {@link Parameters#viewportOrientationMayChange}.
+ * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link
+ * Parameters#viewportOrientationMayChange}.
*
+ * @param viewportWidth See {@link Parameters#viewportWidth}.
+ * @param viewportHeight See {@link Parameters#viewportHeight}.
+ * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}.
* @return This builder.
*/
- public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight,
- boolean viewportOrientationMayChange) {
+ public ParametersBuilder setViewportSize(
+ int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
this.viewportWidth = viewportWidth;
this.viewportHeight = viewportHeight;
this.viewportOrientationMayChange = viewportOrientationMayChange;
@@ -485,8 +487,10 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
- * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in
- * tunneling mode. Session ids can be generated using {@link
+ * See {@link Parameters#tunnelingAudioSessionId}.
+ *
+ * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when
+ * in tunneling mode. Session ids can be generated using {@link
* C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link
* C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and
* supported by the audio and video renderers for the selected tracks.
@@ -540,25 +544,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/** Constraint parameters for {@link DefaultTrackSelector}. */
public static final class Parameters implements Parcelable {
- /**
- * An instance with default values:
- *
- *
- * - No preferred audio language.
- *
- No preferred text language.
- *
- Text tracks with undetermined language are not selected if no track with {@link
- * #preferredTextLanguage} is available.
- *
- All selection flags are considered for text track selections.
- *
- Lowest bitrate track selections are not forced.
- *
- Adaptation between different mime types is not allowed.
- *
- Non seamless adaptation is allowed.
- *
- No max limit for video width/height.
- *
- No max video bitrate.
- *
- Video constraints are exceeded if no supported selection can be made otherwise.
- *
- Renderer capabilities are exceeded if no supported selection can be made.
- *
- No viewport constraints.
- *
- */
+ /** An instance with default values. */
public static final Parameters DEFAULT = new Parameters();
// Per renderer overrides.
@@ -568,105 +554,131 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Audio
/**
- * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag.
- * {@code null} selects the default track, or the first track if there's no default.
+ * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null}
+ * selects the default track, or the first track if there's no default. The default value is
+ * {@code null}.
*/
public final @Nullable String preferredAudioLanguage;
// Text
/**
* The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the
- * default track if there is one, or no track otherwise.
+ * default track if there is one, or no track otherwise. The default value is {@code null}.
*/
public final @Nullable String preferredTextLanguage;
/**
- * Whether a text track with undetermined language should be selected if no track with
- * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset.
+ * Whether a text track with undetermined language should be selected if no track with {@link
+ * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The
+ * default value is {@code false}.
*/
public final boolean selectUndeterminedTextLanguage;
/**
* Bitmask of selection flags that are disabled for text track selections. See {@link
- * C.SelectionFlags}.
+ * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags).
*/
public final int disabledTextTrackSelectionFlags;
// Video
/**
- * Maximum allowed video width.
+ * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no
+ * constraint).
+ *
+ * To constrain adaptive video track selections to be suitable for a given viewport (the
+ * region of the display within which video will be played), use ({@link #viewportWidth}, {@link
+ * #viewportHeight} and {@link #viewportOrientationMayChange}) instead.
*/
public final int maxVideoWidth;
/**
- * Maximum allowed video height.
+ * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no
+ * constraint).
+ *
+ *
To constrain adaptive video track selections to be suitable for a given viewport (the
+ * region of the display within which video will be played), use ({@link #viewportWidth}, {@link
+ * #viewportHeight} and {@link #viewportOrientationMayChange}) instead.
*/
public final int maxVideoHeight;
/**
- * Maximum video bitrate.
+ * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint).
*/
public final int maxVideoBitrate;
/**
- * Whether to exceed video constraints when no selection can be made otherwise.
+ * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link
+ * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is
+ * {@code true}.
*/
public final boolean exceedVideoConstraintsIfNecessary;
/**
- * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that
- * only tracks suitable for the viewport are selected.
+ * Viewport width in pixels. Constrains video track selections for adaptive content so that only
+ * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE}
+ * (i.e. no constraint).
*/
public final int viewportWidth;
/**
- * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that
- * only tracks suitable for the viewport are selected.
+ * Viewport height in pixels. Constrains video track selections for adaptive content so that
+ * only tracks suitable for the viewport are selected. The default value is {@link
+ * Integer#MAX_VALUE} (i.e. no constraint).
*/
public final int viewportHeight;
/**
- * Whether the viewport orientation may change during playback. Constrains video tracks
- * selections for adaptive playbacks so that only tracks suitable for the viewport are selected.
+ * Whether the viewport orientation may change during playback. Constrains video track
+ * selections for adaptive content so that only tracks suitable for the viewport are selected.
+ * The default value is {@code true}.
*/
public final boolean viewportOrientationMayChange;
// General
/**
* Whether to force selection of the single lowest bitrate audio and video tracks that comply
- * with all other constraints.
+ * with all other constraints. The default value is {@code false}.
*/
public final boolean forceLowestBitrate;
/**
- * Whether to allow adaptive selections containing mixed mime types.
+ * Whether to allow adaptive selections containing mixed mime types. The default value is {@code
+ * false}.
*/
public final boolean allowMixedMimeAdaptiveness;
/**
- * Whether to allow adaptive selections where adaptation may not be completely seamless.
+ * Whether to allow adaptive selections where adaptation may not be completely seamless. The
+ * default value is {@code true}.
*/
public final boolean allowNonSeamlessAdaptiveness;
/**
* Whether to exceed renderer capabilities when no selection can be made otherwise.
+ *
+ *
This parameter applies when all of the tracks available for a renderer exceed the
+ * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality
+ * track will still be selected. Playback may succeed if the renderer has under-reported its
+ * true capabilities. If {@code false} then no track will be selected. The default value is
+ * {@code true}.
*/
public final boolean exceedRendererCapabilitiesIfNecessary;
/**
* The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
- * is not to be enabled.
+ * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is
+ * disabled).
*/
public final int tunnelingAudioSessionId;
private Parameters() {
this(
- new SparseArray